blob: 993b3bde545a91e271f0e3efb33f357540021155 [file]
// 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
}