Skip to content

Commit 5432ce9

Browse files
committed
loader: add support for initrd
Add support for booting the micro VM with a user provided initrd file: - For x86_64, the initrd is loaded at the end of the low (4G) memory, and passed to the kernel in the bootparams table. - For aarch64, the initrd is loaded in a memory segment just before the FDT. The initrd start and end addresses are specified in the FDT "/chosen" node. Users can specify an initrd file using the "initrd_path" property of the "boot-source" the REST API. This also adds integration tests to verify VMs boot with initrd and without a rootfs. Categories of tests added: - Functional: the VM shall boot and the "/" path should be mounted as "rootfs" filesystem type. - Performance: VM shall boot in less than 160ms (for x86). Co-developed-by: Marco Vedovati <[email protected]> Co-developed-by: Tim Deegan <[email protected]> Signed-off-by: Marco Vedovati <[email protected]>
1 parent 9fd8571 commit 5432ce9

File tree

18 files changed

+551
-28
lines changed

18 files changed

+551
-28
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@
5050
all parameters specified after `--` are forwarded verbatim to Firecracker.
5151
- Added `KVM_PTP` support to the recommended guest kernel config.
5252
- Added entry in FAQ.md for Firecracker Guest timekeeping.
53+
- Support for booting with an initial RAM disk image. This image can be
54+
specified through the new `initrd_path` field of the `/boot-source` API
55+
request.
5356

5457
### Changed
5558

src/api_server/src/request/boot_source.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ mod tests {
2626

2727
let body = r#"{
2828
"kernel_image_path": "/foo/bar",
29+
"initrd_path": "/bar/foo",
2930
"boot_args": "foobar"
3031
}"#;
3132
let same_body = BootSourceConfig {
3233
kernel_image_path: String::from("/foo/bar"),
34+
initrd_path: Some(String::from("/bar/foo")),
3335
boot_args: Some(String::from("foobar")),
3436
};
3537
let result = parse_put_boot_source(&Body::new(body));

src/api_server/swagger/firecracker.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,9 @@ definitions:
391391
kernel_image_path:
392392
type: string
393393
description: Host level path to the kernel image used to boot the guest
394+
initrd_path:
395+
type: string
396+
description: Host level path to the initrd image used to boot the guest
394397
boot_args:
395398
type: string
396399
description: Kernel boot arguments

src/arch/src/aarch64/fdt.rs

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::ptr::null;
1313
use std::{io, result};
1414

1515
use super::super::DeviceType;
16+
use super::super::InitrdConfig;
1617
use super::get_fdt_addr;
1718
use super::gic::GICDevice;
1819
use super::layout::FDT_MAX_SIZE;
@@ -89,6 +90,7 @@ pub fn create_fdt<T: DeviceInfoForFDT + Clone + Debug>(
8990
cmdline: &CStr,
9091
device_info: Option<&HashMap<(DeviceType, String), T>>,
9192
gic_device: &Box<dyn GICDevice>,
93+
initrd: &Option<InitrdConfig>,
9294
) -> Result<(Vec<u8>)> {
9395
// Alocate stuff necessary for the holding the blob.
9496
let mut fdt = vec![0; FDT_MAX_SIZE];
@@ -111,7 +113,7 @@ pub fn create_fdt<T: DeviceInfoForFDT + Clone + Debug>(
111113
append_property_u32(&mut fdt, "interrupt-parent", GIC_PHANDLE)?;
112114
create_cpu_nodes(&mut fdt, &vcpu_mpidr)?;
113115
create_memory_node(&mut fdt, guest_mem)?;
114-
create_chosen_node(&mut fdt, cmdline)?;
116+
create_chosen_node(&mut fdt, cmdline, initrd)?;
115117
create_gic_node(&mut fdt, gic_device)?;
116118
create_timer_node(&mut fdt)?;
117119
create_clock_node(&mut fdt)?;
@@ -341,9 +343,27 @@ fn create_memory_node(fdt: &mut Vec<u8>, guest_mem: &GuestMemory) -> Result<()>
341343
Ok(())
342344
}
343345

344-
fn create_chosen_node(fdt: &mut Vec<u8>, cmdline: &CStr) -> Result<()> {
346+
fn create_chosen_node(
347+
fdt: &mut Vec<u8>,
348+
cmdline: &CStr,
349+
initrd: &Option<InitrdConfig>,
350+
) -> Result<()> {
345351
append_begin_node(fdt, "chosen")?;
346352
append_property_cstring(fdt, "bootargs", cmdline)?;
353+
354+
if let Some(initrd_config) = initrd {
355+
append_property_u64(
356+
fdt,
357+
"linux,initrd-start",
358+
initrd_config.address.raw_value() as u64,
359+
)?;
360+
append_property_u64(
361+
fdt,
362+
"linux,initrd-end",
363+
initrd_config.address.raw_value() + initrd_config.size as u64,
364+
)?;
365+
}
366+
347367
append_end_node(fdt)?;
348368

349369
Ok(())
@@ -581,6 +601,7 @@ mod tests {
581601
&CString::new("console=tty0").unwrap(),
582602
Some(&dev_info),
583603
&gic,
604+
&None,
584605
)
585606
.is_ok())
586607
}
@@ -598,6 +619,7 @@ mod tests {
598619
&CString::new("console=tty0").unwrap(),
599620
None::<&std::collections::HashMap<(DeviceType, std::string::String), MMIODeviceInfo>>,
600621
&gic,
622+
&None,
601623
)
602624
.unwrap();
603625

@@ -628,4 +650,54 @@ mod tests {
628650
let generated_fdt = device_tree::DeviceTree::load(&dtb).unwrap();
629651
assert!(format!("{:?}", original_fdt) == format!("{:?}", generated_fdt));
630652
}
653+
654+
#[test]
655+
fn test_create_fdt_with_initrd() {
656+
let regions = arch_memory_regions(layout::FDT_MAX_SIZE + 0x1000);
657+
let mem = GuestMemory::new(&regions).expect("Cannot initialize memory");
658+
let kvm = Kvm::new().unwrap();
659+
let vm = kvm.create_vm().unwrap();
660+
let gic = create_gic(&vm, 1).unwrap();
661+
let initrd = InitrdConfig {
662+
address: GuestAddress(0x10000000),
663+
size: 0x1000,
664+
};
665+
666+
let mut dtb = create_fdt(
667+
&mem,
668+
vec![0],
669+
&CString::new("console=tty0").unwrap(),
670+
None::<&std::collections::HashMap<(DeviceType, std::string::String), MMIODeviceInfo>>,
671+
&gic,
672+
&Some(initrd),
673+
)
674+
.unwrap();
675+
676+
/* Use this code when wanting to generate a new DTB sample.
677+
{
678+
use std::fs;
679+
use std::io::Write;
680+
use std::path::PathBuf;
681+
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
682+
let mut output = fs::OpenOptions::new()
683+
.write(true)
684+
.create(true)
685+
.open(path.join("src/aarch64/output_with_initrd.dtb"))
686+
.unwrap();
687+
output.write_all(&dtb).unwrap();
688+
}
689+
*/
690+
691+
let bytes = include_bytes!("output_with_initrd.dtb");
692+
let pos = 4;
693+
let val = layout::FDT_MAX_SIZE;
694+
let mut buf = vec![];
695+
buf.extend_from_slice(bytes);
696+
697+
set_size(&mut buf, pos, val);
698+
set_size(&mut dtb, pos, val);
699+
let original_fdt = device_tree::DeviceTree::load(&buf).unwrap();
700+
let generated_fdt = device_tree::DeviceTree::load(&dtb).unwrap();
701+
assert!(format!("{:?}", original_fdt) == format!("{:?}", generated_fdt));
702+
}
631703
}

src/arch/src/aarch64/mod.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ use memory_model::{Address, GuestAddress, GuestMemory};
2424
pub enum Error {
2525
/// Failed to create a Flattened Device Tree for this aarch64 microVM.
2626
SetupFDT(fdt::Error),
27+
/// Failed to compute the initrd address.
28+
InitrdAddress,
2729
}
2830

2931
/// The start of the memory area reserved for MMIO devices.
@@ -48,19 +50,23 @@ pub fn arch_memory_regions(size: usize) -> Vec<(GuestAddress, usize)> {
4850
/// * `cmdline_cstring` - The kernel commandline.
4951
/// * `vcpu_mpidr` - Array of MPIDR register values per vcpu.
5052
/// * `device_info` - A hashmap containing the attached devices for building FDT device nodes.
53+
/// * `gic_device` - The GIC device.
54+
/// * `initrd` - Information about an optional initrd.
5155
pub fn configure_system<T: DeviceInfoForFDT + Clone + Debug>(
5256
guest_mem: &GuestMemory,
5357
cmdline_cstring: &CStr,
5458
vcpu_mpidr: Vec<u64>,
5559
device_info: Option<&HashMap<(DeviceType, String), T>>,
5660
gic_device: &Box<dyn GICDevice>,
61+
initrd: &Option<super::InitrdConfig>,
5762
) -> super::Result<()> {
5863
fdt::create_fdt(
5964
guest_mem,
6065
vcpu_mpidr,
6166
cmdline_cstring,
6267
device_info,
6368
gic_device,
69+
initrd,
6470
)
6571
.map_err(Error::SetupFDT)?;
6672
Ok(())
@@ -71,6 +77,22 @@ pub fn get_kernel_start() -> u64 {
7177
layout::DRAM_MEM_START
7278
}
7379

80+
/// Returns the memory address where the initrd could be loaded.
81+
pub fn initrd_load_addr(guest_mem: &GuestMemory, initrd_size: usize) -> super::Result<u64> {
82+
let round_to_pagesize = |size| (size + (super::PAGE_SIZE - 1)) & !(super::PAGE_SIZE - 1);
83+
match GuestAddress(get_fdt_addr(&guest_mem)).checked_sub(round_to_pagesize(initrd_size) as u64)
84+
{
85+
Some(offset) => {
86+
if guest_mem.address_in_range(offset) {
87+
return Ok(offset.raw_value());
88+
} else {
89+
return Err(Error::InitrdAddress);
90+
}
91+
}
92+
None => return Err(Error::InitrdAddress),
93+
}
94+
}
95+
7496
// Auxiliary function to get the address where the device tree blob is loaded.
7597
fn get_fdt_addr(mem: &GuestMemory) -> u64 {
7698
// If the memory allocated is smaller than the size allocated for the FDT,
2 MB
Binary file not shown.

src/arch/src/lib.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ pub mod aarch64;
2020

2121
#[cfg(target_arch = "aarch64")]
2222
pub use aarch64::{
23-
arch_memory_regions, configure_system, get_kernel_start, layout::CMDLINE_MAX_SIZE,
24-
layout::IRQ_BASE, layout::IRQ_MAX, Error, MMIO_MEM_START,
23+
arch_memory_regions, configure_system, get_kernel_start, initrd_load_addr,
24+
layout::CMDLINE_MAX_SIZE, layout::IRQ_BASE, layout::IRQ_MAX, Error, MMIO_MEM_START,
2525
};
2626

2727
/// Module for x86_64 related functionality.
@@ -30,8 +30,8 @@ pub mod x86_64;
3030

3131
#[cfg(target_arch = "x86_64")]
3232
pub use x86_64::{
33-
arch_memory_regions, configure_system, get_kernel_start, layout::CMDLINE_MAX_SIZE,
34-
layout::IRQ_BASE, layout::IRQ_MAX, Error, MMIO_MEM_START,
33+
arch_memory_regions, configure_system, get_kernel_start, initrd_load_addr,
34+
layout::CMDLINE_MAX_SIZE, layout::IRQ_BASE, layout::IRQ_MAX, Error, MMIO_MEM_START,
3535
};
3636

3737
/// Type for returning public functions outcome.
@@ -50,6 +50,17 @@ pub enum DeviceType {
5050
RTC,
5151
}
5252

53+
/// Type for passing information about the initrd in the guest memory.
54+
pub struct InitrdConfig {
55+
/// Load address of initrd in guest memory
56+
pub address: memory_model::GuestAddress,
57+
/// Size of initrd in guest memory
58+
pub size: usize,
59+
}
60+
61+
/// Default (smallest) memory page size for the supported architectures.
62+
pub const PAGE_SIZE: usize = 4096;
63+
5364
impl fmt::Display for DeviceType {
5465
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
5566
write!(f, "{:?}", self)

src/arch/src/x86_64/mod.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use std::mem;
1818

1919
use arch_gen::x86::bootparam::{boot_params, E820_RAM};
2020
use memory_model::{Address, ByteValued, GuestAddress, GuestMemory};
21+
use InitrdConfig;
2122

2223
// This is a workaround to the Rust enforcement specifying that any implementation of a foreign
2324
// trait (in this case `ByteValued`) where:
@@ -41,6 +42,8 @@ pub enum Error {
4142
ZeroPagePastRamEnd,
4243
/// Error writing the zero page of guest memory.
4344
ZeroPageSetup,
45+
/// Failed to compute initrd address.
46+
InitrdAddress,
4447
}
4548

4649
// Where BIOS/VGA magic would live on a real PC.
@@ -73,18 +76,32 @@ pub fn get_kernel_start() -> u64 {
7376
layout::HIMEM_START
7477
}
7578

79+
/// Returns the memory address where the initrd could be loaded.
80+
pub fn initrd_load_addr(guest_mem: &GuestMemory, initrd_size: usize) -> super::Result<u64> {
81+
let lowmem_size: usize = guest_mem.region_size(0).map_err(|_| Error::InitrdAddress)?;
82+
83+
if lowmem_size < initrd_size {
84+
return Err(Error::InitrdAddress);
85+
}
86+
87+
let align_to_pagesize = |address| address & !(super::PAGE_SIZE - 1);
88+
Ok(align_to_pagesize(lowmem_size - initrd_size) as u64)
89+
}
90+
7691
/// Configures the system and should be called once per vm before starting vcpu threads.
7792
///
7893
/// # Arguments
7994
///
8095
/// * `guest_mem` - The memory to be used by the guest.
8196
/// * `cmdline_addr` - Address in `guest_mem` where the kernel command line was loaded.
8297
/// * `cmdline_size` - Size of the kernel command line in bytes including the null terminator.
98+
/// * `initrd` - Information about where the ramdisk image was loaded in the `guest_mem`.
8399
/// * `num_cpus` - Number of virtual CPUs the guest will have.
84100
pub fn configure_system(
85101
guest_mem: &GuestMemory,
86102
cmdline_addr: GuestAddress,
87103
cmdline_size: usize,
104+
initrd: &Option<InitrdConfig>,
88105
num_cpus: u8,
89106
) -> super::Result<()> {
90107
const KERNEL_BOOT_FLAG_MAGIC: u16 = 0xaa55;
@@ -107,6 +124,10 @@ pub fn configure_system(
107124
params.0.hdr.cmd_line_ptr = cmdline_addr.raw_value() as u32;
108125
params.0.hdr.cmdline_size = cmdline_size as u32;
109126
params.0.hdr.kernel_alignment = KERNEL_MIN_ALIGNMENT_BYTES;
127+
if let Some(initrd_config) = initrd {
128+
params.0.hdr.ramdisk_image = initrd_config.address.raw_value() as u32;
129+
params.0.hdr.ramdisk_size = initrd_config.size as u32;
130+
}
110131

111132
add_e820_entry(&mut params.0, 0, EBDA_START, E820_RAM)?;
112133

@@ -197,7 +218,7 @@ mod tests {
197218
fn test_system_configuration() {
198219
let no_vcpus = 4;
199220
let gm = GuestMemory::new(&[(GuestAddress(0), 0x10000)]).unwrap();
200-
let config_err = configure_system(&gm, GuestAddress(0), 0, 1);
221+
let config_err = configure_system(&gm, GuestAddress(0), 0, &None, 1);
201222
assert!(config_err.is_err());
202223
assert_eq!(
203224
config_err.unwrap_err(),
@@ -208,19 +229,19 @@ mod tests {
208229
let mem_size = 128 << 20;
209230
let arch_mem_regions = arch_memory_regions(mem_size);
210231
let gm = GuestMemory::new(&arch_mem_regions).unwrap();
211-
configure_system(&gm, GuestAddress(0), 0, no_vcpus).unwrap();
232+
configure_system(&gm, GuestAddress(0), 0, &None, no_vcpus).unwrap();
212233

213234
// Now assigning some memory that is equal to the start of the 32bit memory hole.
214235
let mem_size = 3328 << 20;
215236
let arch_mem_regions = arch_memory_regions(mem_size);
216237
let gm = GuestMemory::new(&arch_mem_regions).unwrap();
217-
configure_system(&gm, GuestAddress(0), 0, no_vcpus).unwrap();
238+
configure_system(&gm, GuestAddress(0), 0, &None, no_vcpus).unwrap();
218239

219240
// Now assigning some memory that falls after the 32bit memory hole.
220241
let mem_size = 3330 << 20;
221242
let arch_mem_regions = arch_memory_regions(mem_size);
222243
let gm = GuestMemory::new(&arch_mem_regions).unwrap();
223-
configure_system(&gm, GuestAddress(0), 0, no_vcpus).unwrap();
244+
configure_system(&gm, GuestAddress(0), 0, &None, no_vcpus).unwrap();
224245
}
225246

226247
#[test]

src/memory_model/src/guest_memory.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,15 @@ impl GuestMemory {
162162
self.regions.len()
163163
}
164164

165+
/// Returns the size of the region identified by passed index
166+
pub fn region_size(&self, index: usize) -> Result<usize> {
167+
if index >= self.regions.len() {
168+
return Err(Error::NoMemoryRegions);
169+
}
170+
171+
Ok(self.regions[index].mapping.size())
172+
}
173+
165174
/// Perform the specified action on each region's addresses.
166175
pub fn with_regions<F, E>(&self, cb: F) -> result::Result<(), E>
167176
where
@@ -747,4 +756,15 @@ mod tests {
747756
3
748757
);
749758
}
759+
760+
#[test]
761+
fn test_region_size() {
762+
let start_addr1 = GuestAddress(0x0);
763+
let start_addr2 = GuestAddress(0x1000);
764+
let mem = GuestMemory::new(&[(start_addr1, 0x100), (start_addr2, 0x400)]).unwrap();
765+
766+
assert_eq!(mem.region_size(0).unwrap(), 0x100);
767+
assert_eq!(mem.region_size(1).unwrap(), 0x400);
768+
assert!(mem.region_size(2).is_err());
769+
}
750770
}

0 commit comments

Comments
 (0)