blob: 7ab9487d294842dcc73a57af5d82d2f4c57c6584 [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
}
func elfFromBZImageWithOffset(img []byte) ([]byte, int, error) {
gzipMagic := []byte{0x1f, 0x8b, 0x08}
for searchOffset := 0; searchOffset < len(img); {
i := bytes.Index(img[searchOffset:], gzipMagic)
if i == -1 {
break
}
i += searchOffset
searchOffset = i + 1
// Found a potential gzip header, attempt decompression.
var stdout bytes.Buffer
var stderr bytes.Buffer
buf := append([]byte{}, img[i:]...)
cmd := exec.Command("gunzip", "-c", "--quiet")
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Stdin = bytes.NewBuffer(buf)
if err := cmd.Run(); err != nil {
// Return code of 2 is a non-fatal warning, ignore those. Do not
// "continue" in that case, "continue" in all other cases.
var exitErr *exec.ExitError
if !(errors.As(err, &exitErr) && exitErr.ExitCode() == 2) {
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(), i, nil
}
}
return nil, 0, 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, err := elfFromBZImageWithOffset(bzImg)
if err != nil {
return nil, err
}
// It's critical that the compressed ELF is as small as possible. Go's stdlib
// gzip implementation is much poorer than the cli tool, so use the cli tool.
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command("gzip", "-c", "-9")
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, errors.New("cannot gzip ELF")
}
// 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
}