Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 13 additions & 2 deletions docs/rootfs-and-kernel-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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).

Expand Down
2 changes: 1 addition & 1 deletion src/vmm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions src/vmm/src/arch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<linux_loader::loader::bootparam::setup_header>,
}

/// Adds in [`regions`] the valid memory regions suitable for RAM taking into account a gap in the
Expand Down
204 changes: 166 additions & 38 deletions src/vmm/src/arch/x86_64/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -234,6 +238,7 @@ pub fn configure_system_for_boot(
GuestAddress(CMDLINE_START),
cmdline_size,
initrd,
entry_point.setup_header,
)?;
}
}
Expand Down Expand Up @@ -346,6 +351,7 @@ fn configure_64bit_boot(
cmdline_addr: GuestAddress,
cmdline_size: usize,
initrd: &Option<InitrdConfig>,
setup_header: Option<setup_header>,
) -> Result<(), ConfigurationError> {
const KERNEL_BOOT_FLAG_MAGIC: u16 = 0xaa55;
const KERNEL_HDR_MAGIC: u32 = 0x5372_6448;
Expand All @@ -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).
Expand Down Expand Up @@ -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,
Expand All @@ -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)]
Expand Down Expand Up @@ -584,26 +625,113 @@ 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.
let mem_size = mib_to_bytes(3328);
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.
let mem_size = mib_to_bytes(3330);
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<u8> {
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 {
Expand Down
1 change: 1 addition & 0 deletions src/vmm/src/arch/x86_64/regs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions src/vmm/src/arch/x86_64/vcpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,7 @@ mod tests {
EntryPoint {
entry_addr: GuestAddress(0),
protocol: BootProtocol::LinuxBoot,
setup_header: None,
},
&vcpu_config,
),
Expand All @@ -893,6 +894,7 @@ mod tests {
EntryPoint {
entry_addr: GuestAddress(crate::arch::get_kernel_start()),
protocol: BootProtocol::LinuxBoot,
setup_header: None,
},
&config,
)
Expand Down Expand Up @@ -1023,6 +1025,7 @@ mod tests {
EntryPoint {
entry_addr: GuestAddress(0),
protocol: BootProtocol::LinuxBoot,
setup_header: None,
},
&vcpu_config,
)
Expand Down Expand Up @@ -1093,6 +1096,7 @@ mod tests {
EntryPoint {
entry_addr: GuestAddress(0),
protocol: BootProtocol::LinuxBoot,
setup_header: None,
},
&vcpu_config,
)
Expand Down
3 changes: 3 additions & 0 deletions src/vmm/src/vstate/vcpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
Loading
Loading