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 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). 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")] 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