From 6b7c9d9f04b640e602e8ea21f506205cf23814dc Mon Sep 17 00:00:00 2001 From: Riccardo Mancini Date: Fri, 17 Jul 2026 18:12:31 +0000 Subject: [PATCH 1/4] feat(kernel): support bzImage direct boot on x86_64 Previously only uncompressed ELF (vmlinux) kernels could boot on x86_64. load_kernel now falls back to the linux-loader bzImage loader when the image is not a valid ELF (InvalidElfMagicNumber). A bzImage boots via the Linux 64-bit boot protocol at code32_start + 0x200, using the same guest CPU state as the ELF LinuxBoot path. The only extra step is seeding the zero page with the image's own setup header, so EntryPoint now carries that header (x86_64 only) for configure_64bit_boot to merge into boot_params. Enable the linux-loader "bzimage" feature. Unit tests cover bzImage selection, ELF carrying no setup header, and rejecting invalid images. Signed-off-by: Riccardo Mancini --- src/vmm/Cargo.toml | 2 +- src/vmm/src/arch/mod.rs | 4 + src/vmm/src/arch/x86_64/mod.rs | 204 ++++++++++++++++++++++++++------ src/vmm/src/arch/x86_64/regs.rs | 1 + src/vmm/src/arch/x86_64/vcpu.rs | 4 + src/vmm/src/vstate/vcpu.rs | 3 + 6 files changed, 179 insertions(+), 39 deletions(-) diff --git a/src/vmm/Cargo.toml b/src/vmm/Cargo.toml index ec8852d5f4b..6b867426ca7 100644 --- a/src/vmm/Cargo.toml +++ b/src/vmm/Cargo.toml @@ -36,7 +36,7 @@ gdbstub_arch = { version = "0.3.3", optional = true } kvm-bindings = { version = "0.14.0", features = ["fam-wrappers", "serde"] } kvm-ioctls = "0.25.0" libc = "0.2.186" -linux-loader = "0.14.0" +linux-loader = { version = "0.14.0", features = ["bzimage"] } log = { version = "0.4.31", features = ["std", "serde"] } log-instrument = { path = "../log-instrument", optional = true } memfd = "0.6.5" diff --git a/src/vmm/src/arch/mod.rs b/src/vmm/src/arch/mod.rs index a35c61d3117..7553e8d1d7f 100644 --- a/src/vmm/src/arch/mod.rs +++ b/src/vmm/src/arch/mod.rs @@ -110,6 +110,10 @@ pub struct EntryPoint { pub entry_addr: GuestAddress, /// Specifies which boot protocol to use pub protocol: BootProtocol, + /// Setup header from a bzImage kernel, used to seed the zero page. + /// `None` for ELF (`vmlinux`) kernels. + #[cfg(target_arch = "x86_64")] + pub setup_header: Option, } /// Adds in [`regions`] the valid memory regions suitable for RAM taking into account a gap in the diff --git a/src/vmm/src/arch/x86_64/mod.rs b/src/vmm/src/arch/x86_64/mod.rs index 11571eba66f..da61889e855 100644 --- a/src/vmm/src/arch/x86_64/mod.rs +++ b/src/vmm/src/arch/x86_64/mod.rs @@ -58,12 +58,16 @@ use layout::{ use linux_loader::configurator::linux::LinuxBootConfigurator; use linux_loader::configurator::pvh::PvhBootConfigurator; use linux_loader::configurator::{BootConfigurator, BootParams}; -use linux_loader::loader::bootparam::boot_params; +use linux_loader::loader::bootparam::{boot_params, setup_header}; +use linux_loader::loader::bzimage::BzImage; use linux_loader::loader::elf::Elf as Loader; +use linux_loader::loader::elf::Error as ElfError; use linux_loader::loader::elf::start_info::{ hvm_memmap_table_entry, hvm_modlist_entry, hvm_start_info, }; -use linux_loader::loader::{Cmdline, KernelLoader, PvhBootCapability, load_cmdline}; +use linux_loader::loader::{ + Cmdline, Error as KernelLoaderError, KernelLoader, PvhBootCapability, load_cmdline, +}; use vm_memory::GuestMemoryBackend; // Value taken from https://elixir.bootlin.com/linux/v5.10.68/source/arch/x86/include/uapi/asm/e820.h#L31 @@ -234,6 +238,7 @@ pub fn configure_system_for_boot( GuestAddress(CMDLINE_START), cmdline_size, initrd, + entry_point.setup_header, )?; } } @@ -346,6 +351,7 @@ fn configure_64bit_boot( cmdline_addr: GuestAddress, cmdline_size: usize, initrd: &Option, + setup_header: Option, ) -> Result<(), ConfigurationError> { const KERNEL_BOOT_FLAG_MAGIC: u16 = 0xaa55; const KERNEL_HDR_MAGIC: u32 = 0x5372_6448; @@ -354,23 +360,29 @@ fn configure_64bit_boot( let himem_start = GuestAddress(layout::HIMEM_START); + // A bzImage carries its own setup header; for ELF there is none, so + // start from a zeroed header with the minimum required alignment. + let mut hdr = setup_header.unwrap_or_else(|| setup_header { + kernel_alignment: KERNEL_MIN_ALIGNMENT_BYTES, + ..Default::default() + }); + hdr.type_of_loader = KERNEL_LOADER_OTHER; + hdr.boot_flag = KERNEL_BOOT_FLAG_MAGIC; + hdr.header = KERNEL_HDR_MAGIC; + hdr.cmd_line_ptr = u32::try_from(cmdline_addr.raw_value()).unwrap(); + hdr.cmdline_size = u32::try_from(cmdline_size).unwrap(); + if let Some(initrd_config) = initrd { + hdr.ramdisk_image = u32::try_from(initrd_config.address.raw_value()).unwrap(); + hdr.ramdisk_size = u32::try_from(initrd_config.size).unwrap(); + } + // Set the location of RSDP in Boot Parameters to help the guest kernel find it faster. let mut params = boot_params { + hdr, acpi_rsdp_addr: layout::RSDP_ADDR, ..Default::default() }; - params.hdr.type_of_loader = KERNEL_LOADER_OTHER; - params.hdr.boot_flag = KERNEL_BOOT_FLAG_MAGIC; - params.hdr.header = KERNEL_HDR_MAGIC; - params.hdr.cmd_line_ptr = u32::try_from(cmdline_addr.raw_value()).unwrap(); - params.hdr.cmdline_size = u32::try_from(cmdline_size).unwrap(); - params.hdr.kernel_alignment = KERNEL_MIN_ALIGNMENT_BYTES; - if let Some(initrd_config) = initrd { - params.hdr.ramdisk_image = u32::try_from(initrd_config.address.raw_value()).unwrap(); - params.hdr.ramdisk_size = u32::try_from(initrd_config.size).unwrap(); - } - // We mark first [0x0, SYSTEM_MEM_START) region as usable RAM and the subsequent // [SYSTEM_MEM_START, (SYSTEM_MEM_START + SYSTEM_MEM_SIZE)) as reserved (note // SYSTEM_MEM_SIZE + SYSTEM_MEM_SIZE == HIMEM_START). @@ -429,7 +441,14 @@ fn add_e820_entry( Ok(()) } -/// Load linux kernel into guest memory. +/// Offset of the 64-bit entry point from the start of a loaded bzImage, +/// per the Linux/x86 64-bit boot protocol. +const BZIMAGE_64BIT_ENTRY_OFFSET: u64 = 0x200; + +/// Load a linux kernel into guest memory. +/// +/// Supports uncompressed ELF (`vmlinux`) and `bzImage`; ELF is tried +/// first, falling back to the bzImage loader. pub fn load_kernel( kernel: &File, guest_memory: &GuestMemoryMmap, @@ -440,28 +459,50 @@ pub fn load_kernel( .try_clone() .map_err(|_| ConfigurationError::KernelFile)?; - let entry_addr = Loader::load( - guest_memory, - None, - &mut kernel_file, - Some(GuestAddress(get_kernel_start())), - ) - .map_err(ConfigurationError::KernelLoader)?; - - let mut entry_point_addr: GuestAddress = entry_addr.kernel_load; - let mut boot_prot: BootProtocol = BootProtocol::LinuxBoot; - if let PvhBootCapability::PvhEntryPresent(pvh_entry_addr) = entry_addr.pvh_boot_cap { - // Use the PVH kernel entry point to boot the guest - entry_point_addr = pvh_entry_addr; - boot_prot = BootProtocol::PvhBoot; + let highmem_start = Some(GuestAddress(get_kernel_start())); + + match Loader::load(guest_memory, None, &mut kernel_file, highmem_start) { + Ok(elf_result) => { + let mut entry_point_addr: GuestAddress = elf_result.kernel_load; + let mut boot_prot: BootProtocol = BootProtocol::LinuxBoot; + if let PvhBootCapability::PvhEntryPresent(pvh_entry_addr) = elf_result.pvh_boot_cap { + // Use the PVH kernel entry point to boot the guest + entry_point_addr = pvh_entry_addr; + boot_prot = BootProtocol::PvhBoot; + } + + debug!("Kernel loaded using {boot_prot}"); + + Ok(EntryPoint { + entry_addr: entry_point_addr, + protocol: boot_prot, + setup_header: None, + }) + } + // Not an ELF image: fall back to the bzImage loader. + Err(KernelLoaderError::Elf(ElfError::InvalidElfMagicNumber)) => { + let bzimage_result = BzImage::load(guest_memory, None, &mut kernel_file, highmem_start) + .map_err(ConfigurationError::KernelLoader)?; + + // Enter at the 64-bit entry point (BZIMAGE_64BIT_ENTRY_OFFSET + // past the load address); the setup header seeds the zero page. + let entry_addr = bzimage_result + .kernel_load + .checked_add(BZIMAGE_64BIT_ENTRY_OFFSET) + .ok_or(ConfigurationError::KernelLoader( + KernelLoaderError::MemoryOverflow, + ))?; + + debug!("Kernel loaded using {} (bzImage)", BootProtocol::LinuxBoot); + + Ok(EntryPoint { + entry_addr, + protocol: BootProtocol::LinuxBoot, + setup_header: bzimage_result.setup_header, + }) + } + Err(err) => Err(ConfigurationError::KernelLoader(err)), } - - debug!("Kernel loaded using {boot_prot}"); - - Ok(EntryPoint { - entry_addr: entry_point_addr, - protocol: boot_prot, - }) } #[cfg(kani)] @@ -584,7 +625,7 @@ mod tests { let gm = arch_mem(mem_size); let mut resource_allocator = ResourceAllocator::new(); mptable::setup_mptable(&gm, &mut resource_allocator, no_vcpus).unwrap(); - configure_64bit_boot(&gm, GuestAddress(0), 0, &None).unwrap(); + configure_64bit_boot(&gm, GuestAddress(0), 0, &None, None).unwrap(); configure_pvh(&gm, GuestAddress(0), &None).unwrap(); // Now assigning some memory that is equal to the start of the 32bit memory hole. @@ -592,7 +633,7 @@ mod tests { let gm = arch_mem(mem_size); let mut resource_allocator = ResourceAllocator::new(); mptable::setup_mptable(&gm, &mut resource_allocator, no_vcpus).unwrap(); - configure_64bit_boot(&gm, GuestAddress(0), 0, &None).unwrap(); + configure_64bit_boot(&gm, GuestAddress(0), 0, &None, None).unwrap(); configure_pvh(&gm, GuestAddress(0), &None).unwrap(); // Now assigning some memory that falls after the 32bit memory hole. @@ -600,10 +641,97 @@ mod tests { let gm = arch_mem(mem_size); let mut resource_allocator = ResourceAllocator::new(); mptable::setup_mptable(&gm, &mut resource_allocator, no_vcpus).unwrap(); - configure_64bit_boot(&gm, GuestAddress(0), 0, &None).unwrap(); + configure_64bit_boot(&gm, GuestAddress(0), 0, &None, None).unwrap(); configure_pvh(&gm, GuestAddress(0), &None).unwrap(); } + /// Builds a minimal bzImage that satisfies `linux_loader`'s bzImage + /// loader checks. Not a runnable kernel, only enough to exercise + /// loader selection and zero-page seeding in `load_kernel`. + fn make_fake_bzimage() -> Vec { + const SETUP_SECTS: u8 = 4; + // Setup area (`(setup_sects + 1) * 512`) followed by a small protected-mode payload. + let setup_len = (SETUP_SECTS as usize + 1) * 512; + let mut image = vec![0u8; setup_len + 0x1000]; + + // setup_sects @ 0x1f1 + image[0x1f1] = SETUP_SECTS; + // boot_flag (0xaa55) @ 0x1fe + image[0x1fe] = 0x55; + image[0x1ff] = 0xaa; + // "HdrS" header magic @ 0x202 + image[0x202..0x206].copy_from_slice(&0x5372_6448u32.to_le_bytes()); + // version @ 0x206 (>= 0x0200) + image[0x206..0x208].copy_from_slice(&0x020fu16.to_le_bytes()); + // loadflags @ 0x211 with LOADED_HIGH (bit 0) set + image[0x211] = 0x01; + // code32_start @ 0x214: default load address, must be >= HIMEM_START + let code32_start = u32::try_from(get_kernel_start()).unwrap(); + image[0x214..0x218].copy_from_slice(&code32_start.to_le_bytes()); + + image + } + + #[test] + fn test_load_kernel_bzimage() { + use std::io::{Seek, SeekFrom, Write}; + + use vmm_sys_util::tempfile::TempFile; + + let gm = arch_mem(mib_to_bytes(128)); + + let image = make_fake_bzimage(); + let tempfile = TempFile::new().unwrap(); + let mut file = tempfile.into_file(); + file.write_all(&image).unwrap(); + file.seek(SeekFrom::Start(0)).unwrap(); + + let entry_point = load_kernel(&file, &gm).unwrap(); + + assert_eq!(entry_point.protocol, BootProtocol::LinuxBoot); + assert_eq!( + entry_point.entry_addr, + GuestAddress(get_kernel_start() + BZIMAGE_64BIT_ENTRY_OFFSET) + ); + let hdr = entry_point + .setup_header + .expect("bzImage must yield a setup header"); + // Copy out of the packed struct to avoid unaligned references. + let (header, setup_sects) = (hdr.header, hdr.setup_sects); + assert_eq!(header, 0x5372_6448); + assert_eq!(setup_sects, 4); + } + + #[test] + fn test_load_kernel_elf_has_no_setup_header() { + use std::fs::File; + + use crate::test_utils::mock_resources::kernel_image_path; + + let gm = arch_mem(mib_to_bytes(128)); + + let file = File::open(kernel_image_path(None)).unwrap(); + let entry_point = load_kernel(&file, &gm).unwrap(); + assert!(entry_point.setup_header.is_none()); + } + + #[test] + fn test_load_kernel_invalid_image_is_rejected() { + use std::io::{Seek, SeekFrom, Write}; + + use vmm_sys_util::tempfile::TempFile; + + let gm = arch_mem(mib_to_bytes(128)); + + // Neither valid ELF nor valid bzImage. + let tempfile = TempFile::new().unwrap(); + let mut file = tempfile.into_file(); + file.write_all(&[0u8; 8192]).unwrap(); + file.seek(SeekFrom::Start(0)).unwrap(); + + load_kernel(&file, &gm).unwrap_err(); + } + #[test] fn test_add_e820_entry() { let e820_map = [(boot_e820_entry { diff --git a/src/vmm/src/arch/x86_64/regs.rs b/src/vmm/src/arch/x86_64/regs.rs index 998dddc6399..44e44cf177a 100644 --- a/src/vmm/src/arch/x86_64/regs.rs +++ b/src/vmm/src/arch/x86_64/regs.rs @@ -388,6 +388,7 @@ mod tests { let entry_point: EntryPoint = EntryPoint { entry_addr: GuestAddress(expected_regs.rip), protocol: BootProtocol::LinuxBoot, + setup_header: None, }; setup_regs(&vcpu, entry_point).unwrap(); diff --git a/src/vmm/src/arch/x86_64/vcpu.rs b/src/vmm/src/arch/x86_64/vcpu.rs index 98563aab2e3..d4e9ee0bc8a 100644 --- a/src/vmm/src/arch/x86_64/vcpu.rs +++ b/src/vmm/src/arch/x86_64/vcpu.rs @@ -876,6 +876,7 @@ mod tests { EntryPoint { entry_addr: GuestAddress(0), protocol: BootProtocol::LinuxBoot, + setup_header: None, }, &vcpu_config, ), @@ -893,6 +894,7 @@ mod tests { EntryPoint { entry_addr: GuestAddress(crate::arch::get_kernel_start()), protocol: BootProtocol::LinuxBoot, + setup_header: None, }, &config, ) @@ -1023,6 +1025,7 @@ mod tests { EntryPoint { entry_addr: GuestAddress(0), protocol: BootProtocol::LinuxBoot, + setup_header: None, }, &vcpu_config, ) @@ -1093,6 +1096,7 @@ mod tests { EntryPoint { entry_addr: GuestAddress(0), protocol: BootProtocol::LinuxBoot, + setup_header: None, }, &vcpu_config, ) diff --git a/src/vmm/src/vstate/vcpu.rs b/src/vmm/src/vstate/vcpu.rs index ffc473a70ac..7050c68ed87 100644 --- a/src/vmm/src/vstate/vcpu.rs +++ b/src/vmm/src/vstate/vcpu.rs @@ -893,6 +893,9 @@ pub(crate) mod tests { let entry_point = EntryPoint { entry_addr: load_good_kernel(vm.guest_memory()), protocol: BootProtocol::LinuxBoot, + // `setup_header` is only a field of `EntryPoint` on x86_64. + #[cfg(target_arch = "x86_64")] + setup_header: None, }; #[cfg(target_arch = "x86_64")] From e3f5fad91b994f2e67fb74ffb8d22dd0d18dafc9 Mon Sep 17 00:00:00 2001 From: Riccardo Mancini Date: Fri, 17 Jul 2026 18:12:31 +0000 Subject: [PATCH 2/4] test(kernel): add bzImage direct boot integration test Add an x86_64-only functional test, test_bzimage_boots_to_userspace, that boots the bzImage sibling of the default guest vmlinux, confirms the guest reaches userspace over SSH, and asserts (via the Debug log) that Firecracker took the bzImage load path. Signed-off-by: Riccardo Mancini --- .../functional/test_bzimage_boot.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/integration_tests/functional/test_bzimage_boot.py diff --git a/tests/integration_tests/functional/test_bzimage_boot.py b/tests/integration_tests/functional/test_bzimage_boot.py new file mode 100644 index 00000000000..dd63fc4247a --- /dev/null +++ b/tests/integration_tests/functional/test_bzimage_boot.py @@ -0,0 +1,30 @@ +# Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test booting a guest from a bzImage kernel (x86_64 direct boot).""" + +import platform + +import pytest + +pytestmark = pytest.mark.skipif( + platform.machine() != "x86_64", + reason="bzImage direct boot is only supported on x86_64", +) + + +def test_bzimage_boots_to_userspace(uvm): + """Boot the bzImage built from the same source as the guest's vmlinux.""" + # Every x86_64 vmlinux artifact has a bzImage sibling. + uvm.kernel_file = uvm.kernel_file.with_name( + uvm.kernel_file.name.replace("vmlinux-", "bzImage-", 1) + ) + + uvm.spawn(log_level="Debug") + uvm.basic_config() + uvm.add_net_iface() + uvm.start() + + # Reached userspace. + uvm.ssh.check_output("true") + # Firecracker took the bzImage load path. + assert "(bzImage)" in uvm.log_data From ddec46ce09f613501b806111b974ddb5d4200f68 Mon Sep 17 00:00:00 2001 From: Riccardo Mancini Date: Fri, 17 Jul 2026 18:12:46 +0000 Subject: [PATCH 3/4] docs(changelog): add entry for bzImage direct boot on x86_64 Note under Unreleased that x86_64 now supports booting bzImage guest kernels in addition to uncompressed ELF vmlinux images. Signed-off-by: Riccardo Mancini --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed869e79600..0d4be5dd2cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ and this project adheres to will use transparent huge pages for the guest memory via `madvise(MADV_HUGEPAGE)`. Guest memory must be a multiple of 2MB when using this option. +- [#6037](https://github.com/firecracker-microvm/firecracker/pull/6037): Added + support for booting `bzImage` guest kernels on x86_64, in addition to the + existing uncompressed ELF (`vmlinux`) images. The kernel image format is + detected automatically, so no configuration change is required. ### Changed From ff3cd54290f06326a8ed9b3072ec2fc855f168ec Mon Sep 17 00:00:00 2001 From: Riccardo Mancini Date: Fri, 17 Jul 2026 18:12:46 +0000 Subject: [PATCH 4/4] docs: note bzImage support in kernel setup guide Update the kernel image format statement and manual build steps to reflect that x86_64 now supports bzImage kernels in addition to uncompressed ELF, and note the boot time and memory trade-off. Signed-off-by: Riccardo Mancini --- docs/rootfs-and-kernel-setup.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/rootfs-and-kernel-setup.md b/docs/rootfs-and-kernel-setup.md index ecffc58b720..efb7f0d2aec 100644 --- a/docs/rootfs-and-kernel-setup.md +++ b/docs/rootfs-and-kernel-setup.md @@ -4,8 +4,16 @@ ### Manual compilation -Currently, Firecracker supports uncompressed ELF kernel images on x86_64 while -on aarch64 it supports PE formatted images. +Currently, on x86_64 Firecracker supports uncompressed ELF (`vmlinux`) kernel +images as well as `bzImage` images, while on aarch64 it supports PE formatted +(`Image`) images. On x86_64 the image format is detected automatically. + +> [!NOTE] +> +> On x86_64, `bzImage` support is provided for compatibility; `vmlinux` is +> recommended. A `bzImage` is compressed and is decompressed by the guest at +> boot, which costs additional boot time and guest memory (and therefore host +> memory) compared to booting the equivalent uncompressed `vmlinux`. Here's a quick step-by-step guide to building your own kernel that Firecracker can boot: @@ -49,6 +57,9 @@ can boot: fi ``` + On x86_64 you can alternatively build a `bzImage` (`make bzImage`), which + Firecracker also supports; it is produced under `./arch/x86/boot/bzImage`. + 1. Upon a successful build, you can find the kernel image under `./vmlinux` (for x86) or `./arch/arm64/boot/Image` (for aarch64).