| // Copyright 2026 Google LLC | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package cosboot | |
| import ( | |
| "bytes" | |
| "debug/elf" | |
| "encoding/binary" | |
| "errors" | |
| "fmt" | |
| "log" | |
| "os/exec" | |
| ) | |
| func bzImagePayloadSize(img []byte) (int, error) { | |
| // See arch/x86/include/uapi/asm/bootparam.h | |
| payloadLengthOffset := 0x24c | |
| if payloadLengthOffset+4 >= len(img) { | |
| return 0, fmt.Errorf("bzImage is too small (%d): cannot find payload_length", len(img)) | |
| } | |
| return int(binary.LittleEndian.Uint32(img[payloadLengthOffset : payloadLengthOffset+4])), nil | |
| } | |
| type compression struct { | |
| name string | |
| magic []byte | |
| unzipCmd string | |
| unzipArgs []string | |
| zipCmd string | |
| zipArgs []string | |
| } | |
| var supportedCompressions = []compression{ | |
| { | |
| name: "gzip", | |
| magic: []byte{0x1f, 0x8b, 0x08}, | |
| unzipCmd: "gunzip", | |
| unzipArgs: []string{"-c", "--quiet"}, | |
| zipCmd: "gzip", | |
| zipArgs: []string{"-c", "-9"}, | |
| }, | |
| { | |
| name: "xz", | |
| magic: []byte{0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00}, | |
| unzipCmd: "unxz", | |
| unzipArgs: []string{"-c"}, | |
| zipCmd: "xz", | |
| zipArgs: []string{"-c", "--check=crc32", "--threads=1", "--x86", "--lzma2=,dict=128MiB"}, | |
| }, | |
| { | |
| name: "bzip2", | |
| magic: []byte{0x42, 0x5a, 0x68}, | |
| unzipCmd: "bunzip2", | |
| unzipArgs: []string{"-c"}, | |
| zipCmd: "bzip2", | |
| zipArgs: []string{"-c", "-9"}, | |
| }, | |
| } | |
| func elfFromBZImageWithOffset(img []byte) ([]byte, int, compression, error) { | |
| for searchOffset := 0; searchOffset < len(img); { | |
| var foundComp compression | |
| var foundIndex int | |
| // Find the earliest magic | |
| earliest := -1 | |
| for _, comp := range supportedCompressions { | |
| i := bytes.Index(img[searchOffset:], comp.magic) | |
| if i != -1 { | |
| i += searchOffset | |
| if earliest == -1 || i < earliest { | |
| earliest = i | |
| foundComp = comp | |
| } | |
| } | |
| } | |
| if earliest == -1 { | |
| break | |
| } | |
| foundIndex = earliest | |
| searchOffset = foundIndex + 1 | |
| // Found a potential header, attempt decompression. | |
| var stdout bytes.Buffer | |
| var stderr bytes.Buffer | |
| buf := append([]byte{}, img[foundIndex:]...) | |
| cmd := exec.Command(foundComp.unzipCmd, foundComp.unzipArgs...) | |
| cmd.Stdout = &stdout | |
| cmd.Stderr = &stderr | |
| cmd.Stdin = bytes.NewBuffer(buf) | |
| if err := cmd.Run(); err != nil { | |
| // For gunzip, return code of 2 is a non-fatal warning, ignore those. | |
| if foundComp.name == "gzip" { | |
| var exitErr *exec.ExitError | |
| if !(errors.As(err, &exitErr) && exitErr.ExitCode() == 2) { | |
| continue | |
| } | |
| } else if foundComp.name == "xz" { | |
| // Fall through to the ELF check as the error might be non-fatal | |
| // (e.g. trailing garbage) and the output might still be valid. | |
| } else { | |
| continue | |
| } | |
| } | |
| // Verify if the decompressed content is a valid ELF file. | |
| if f, err := elf.NewFile(bytes.NewReader(stdout.Bytes())); err == nil { | |
| f.Close() | |
| return stdout.Bytes(), foundIndex, foundComp, nil | |
| } | |
| } | |
| return nil, 0, compression{}, errors.New("could not find a compressed ELF payload in the bzImage") | |
| } | |
| // elfFromBZImage converts a given bzImage into an ELF. Adapted from | |
| // extract-vmlinux in the kernel tree. | |
| func elfFromBZImage(img []byte) ([]byte, error) { | |
| data, _, _, err := elfFromBZImageWithOffset(img) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return data, nil | |
| } | |
| // packBZImage packs a given ELF into a given bzImage. | |
| func packBZImage(bzImg, elfImg []byte) ([]byte, error) { | |
| payloadSize, err := bzImagePayloadSize(bzImg) | |
| if err != nil { | |
| return nil, err | |
| } | |
| _, offset, comp, err := elfFromBZImageWithOffset(bzImg) | |
| if err != nil { | |
| return nil, err | |
| } | |
| // It's critical that the compressed ELF is as small as possible. Go's stdlib | |
| // compression implementations are much poorer than the cli tools, so use cli. | |
| var stdout bytes.Buffer | |
| var stderr bytes.Buffer | |
| cmd := exec.Command(comp.zipCmd, comp.zipArgs...) | |
| cmd.Stdout = &stdout | |
| cmd.Stderr = &stderr | |
| cmd.Stdin = bytes.NewBuffer(elfImg) | |
| if err := cmd.Run(); err != nil { | |
| log.Printf("Command %q failed with stderr: %v", cmd.String(), string(stderr.Bytes())) | |
| return nil, fmt.Errorf("cannot %s ELF", comp.zipCmd) | |
| } | |
| // newSize is the size of the new compressed vmlinux (different from bzImage). | |
| newSize := len(stdout.Bytes()) | |
| if newSize > payloadSize { | |
| return nil, fmt.Errorf("new bzImage is larger than old bzImage by %d bytes", newSize-payloadSize) | |
| } | |
| padCount := payloadSize - newSize | |
| var padding []byte | |
| for i := 0; i < padCount; i++ { | |
| padding = append(padding, 0x00) | |
| } | |
| var compressed []byte | |
| compressed = append(compressed, stdout.Bytes()...) | |
| // Add padding | |
| compressed = append(compressed, padding...) | |
| // Add bzImage trailing content | |
| compressed = append(compressed, bzImg[offset+payloadSize:]...) | |
| return append(bzImg[:offset], compressed...), nil | |
| } |