use alloc::vec::Vec;
use super::linux::{
BOOT_PARAMS_SIZE, X86LinuxHeader, X86LinuxLayoutError, X86LinuxLoadLayout, X86LinuxRange,
};
const SETUP_HEADER_START: usize = 0x1f1;
const SETUP_HEADER_END: usize = 0x290;
const EXT_RAMDISK_IMAGE_OFFSET: usize = 0x0c0;
const EXT_RAMDISK_SIZE_OFFSET: usize = 0x0c4;
const EXT_CMD_LINE_PTR_OFFSET: usize = 0x0c8;
const E820_ENTRIES_OFFSET: usize = 0x1e8;
const SENTINEL_OFFSET: usize = 0x1ef;
const TYPE_OF_LOADER_OFFSET: usize = 0x210;
const LOADFLAGS_OFFSET: usize = 0x211;
const CODE32_START_OFFSET: usize = 0x214;
const RAMDISK_IMAGE_OFFSET: usize = 0x218;
const RAMDISK_SIZE_OFFSET: usize = 0x21c;
const HEAP_END_PTR_OFFSET: usize = 0x224;
const CMD_LINE_PTR_OFFSET: usize = 0x228;
const SETUP_DATA_OFFSET: usize = 0x250;
const E820_TABLE_OFFSET: usize = 0x2d0;
const COMMAND_LINE_OFFSET: usize = 0xe00;
const TYPE_OF_LOADER_UNSPECIFIED: u8 = 0xff;
const LOADFLAG_CAN_USE_HEAP: u8 = 0x80;
const E820_ENTRY_SIZE: usize = 20;
const E820_MAX_ENTRIES: usize = 128;
const E820_TYPE_RAM: u32 = 1;
const E820_TYPE_RESERVED: u32 = 2;
const LEGACY_RESERVED_START: usize = 0x000a_0000;
const LEGACY_RESERVED_SIZE: usize = 0x0006_0000;
pub const DEFAULT_COMMAND_LINE: &str = concat!(
"console=ttyS0 root=/dev/vda rw rootwait devtmpfs.mount=1 init=/sbin/getty ",
"acpi=off pci=conf1 pci=nomsi nox2apic ",
"tsc=unstable initcall_blacklist=ahci_pci_driver_init,i8042_init ",
"-- -n -l /bin/sh -L 115200 ttyS0 dumb"
);
pub struct BootParamsBuilder<'a> {
kernel_image: &'a [u8],
header: X86LinuxHeader,
layout: X86LinuxLoadLayout,
ram_ranges: Vec<X86LinuxRange>,
reserved_ranges: Vec<X86LinuxRange>,
command_line: &'a str,
}
impl<'a> BootParamsBuilder<'a> {
pub fn new(
kernel_image: &'a [u8],
header: X86LinuxHeader,
layout: X86LinuxLoadLayout,
main_memory: X86LinuxRange,
) -> Self {
Self {
kernel_image,
header,
layout,
ram_ranges: alloc::vec![main_memory],
reserved_ranges: alloc::vec![
layout.boot_params,
layout.boot_stub,
X86LinuxRange::new(LEGACY_RESERVED_START, LEGACY_RESERVED_SIZE),
],
command_line: DEFAULT_COMMAND_LINE,
}
}
pub fn add_ram_range(&mut self, range: X86LinuxRange) {
if range.size != 0 {
self.ram_ranges.push(range);
}
}
pub fn set_command_line(&mut self, command_line: &'a str) -> Result<(), BootParamsError> {
self.validate_command_line(command_line)?;
self.command_line = command_line;
Ok(())
}
pub fn add_reserved_range(&mut self, range: X86LinuxRange) {
if range.size != 0 {
self.reserved_ranges.push(range);
}
}
pub fn build(mut self) -> Result<[u8; BOOT_PARAMS_SIZE], BootParamsError> {
let mut boot_params = [0u8; BOOT_PARAMS_SIZE];
self.copy_setup_header(&mut boot_params)?;
self.patch_setup_header(&mut boot_params)?;
self.write_e820(&mut boot_params)?;
Ok(boot_params)
}
fn copy_setup_header(&self, boot_params: &mut [u8]) -> Result<(), BootParamsError> {
let source = self
.kernel_image
.get(SETUP_HEADER_START..SETUP_HEADER_END)
.ok_or(BootParamsError::SetupHeaderTruncated {
image_size: self.kernel_image.len(),
required: SETUP_HEADER_END,
})?;
boot_params[SETUP_HEADER_START..SETUP_HEADER_END].copy_from_slice(source);
Ok(())
}
fn patch_setup_header(&self, boot_params: &mut [u8]) -> Result<(), BootParamsError> {
write_u8(boot_params, SENTINEL_OFFSET, 0xff);
write_u8(
boot_params,
TYPE_OF_LOADER_OFFSET,
TYPE_OF_LOADER_UNSPECIFIED,
);
write_u8(
boot_params,
LOADFLAGS_OFFSET,
self.header.loadflags | LOADFLAG_CAN_USE_HEAP,
);
write_u16(boot_params, HEAP_END_PTR_OFFSET, self.header.heap_end_ptr);
write_u32(
boot_params,
CODE32_START_OFFSET,
self.layout.kernel.start as u32,
);
write_u64(boot_params, SETUP_DATA_OFFSET, 0);
let cmdline_ptr = self
.layout
.boot_params
.start
.checked_add(COMMAND_LINE_OFFSET)
.ok_or(BootParamsError::AddressOverflow)?;
write_u32(boot_params, CMD_LINE_PTR_OFFSET, cmdline_ptr as u32);
write_u32(
boot_params,
EXT_CMD_LINE_PTR_OFFSET,
(cmdline_ptr >> 32) as u32,
);
self.write_command_line(boot_params)?;
if let Some(initrd) = self.layout.initrd {
write_u32(boot_params, RAMDISK_IMAGE_OFFSET, initrd.start as u32);
write_u32(boot_params, RAMDISK_SIZE_OFFSET, initrd.size as u32);
write_u32(
boot_params,
EXT_RAMDISK_IMAGE_OFFSET,
(initrd.start >> 32) as u32,
);
write_u32(
boot_params,
EXT_RAMDISK_SIZE_OFFSET,
(initrd.size >> 32) as u32,
);
}
Ok(())
}
fn write_command_line(&self, boot_params: &mut [u8]) -> Result<(), BootParamsError> {
self.validate_command_line(self.command_line)?;
let bytes = self.command_line.as_bytes();
let end = COMMAND_LINE_OFFSET + bytes.len();
boot_params[COMMAND_LINE_OFFSET..end].copy_from_slice(bytes);
write_u8(boot_params, end, 0);
Ok(())
}
fn validate_command_line(&self, command_line: &str) -> Result<(), BootParamsError> {
if command_line.as_bytes().contains(&0) {
return Err(BootParamsError::CommandLineContainsNul);
}
let max_len = self.command_line_capacity();
if command_line.len() > max_len {
return Err(BootParamsError::CommandLineTooLong {
len: command_line.len(),
max: max_len,
});
}
Ok(())
}
fn command_line_capacity(&self) -> usize {
let zero_page_capacity = BOOT_PARAMS_SIZE - COMMAND_LINE_OFFSET - 1;
if self.header.cmdline_size == 0 {
zero_page_capacity
} else {
zero_page_capacity.min(self.header.cmdline_size as usize)
}
}
fn write_e820(&mut self, boot_params: &mut [u8]) -> Result<(), BootParamsError> {
let entries = self.e820_entries()?;
if entries.len() > E820_MAX_ENTRIES {
return Err(BootParamsError::TooManyE820Entries {
entries: entries.len(),
});
}
write_u8(boot_params, E820_ENTRIES_OFFSET, entries.len() as u8);
for (idx, entry) in entries.iter().enumerate() {
let offset = E820_TABLE_OFFSET + idx * E820_ENTRY_SIZE;
write_u64(boot_params, offset, entry.addr);
write_u64(boot_params, offset + 8, entry.size);
write_u32(boot_params, offset + 16, entry.entry_type);
}
Ok(())
}
fn e820_entries(&mut self) -> Result<Vec<E820Entry>, BootParamsError> {
let mut entries = Vec::new();
let ram_ranges = normalized_ranges(&self.ram_ranges)?;
let reserved = normalized_ranges(&self.reserved_ranges)?;
for ram in ram_ranges.iter().copied() {
let ram_end = ram.end().map_err(BootParamsError::Layout)?;
let mut cursor = ram.start;
for range in reserved.iter().copied() {
let range_end = range.end().map_err(BootParamsError::Layout)?;
if range_end <= ram.start || range.start >= ram_end {
continue;
}
let reserved_start = range.start.max(ram.start);
let reserved_end = range_end.min(ram_end);
if cursor < reserved_start {
entries.push(E820Entry::ram(cursor, reserved_start - cursor)?);
}
entries.push(E820Entry::reserved(X86LinuxRange::new(
reserved_start,
reserved_end - reserved_start,
))?);
cursor = cursor.max(reserved_end);
}
if cursor < ram_end {
entries.push(E820Entry::ram(cursor, ram_end - cursor)?);
}
}
for range in reserved {
let overlaps_ram = ram_ranges
.iter()
.try_fold(false, |found, ram| Ok(found || range.overlaps(ram)?))
.map_err(BootParamsError::Layout)?;
if !overlaps_ram {
entries.push(E820Entry::reserved(range)?);
}
}
entries.sort_by_key(|entry| entry.addr);
Ok(entries)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BootParamsError {
SetupHeaderTruncated { image_size: usize, required: usize },
CommandLineContainsNul,
CommandLineTooLong { len: usize, max: usize },
AddressOverflow,
Layout(X86LinuxLayoutError),
TooManyE820Entries { entries: usize },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct E820Entry {
addr: u64,
size: u64,
entry_type: u32,
}
impl E820Entry {
fn ram(start: usize, size: usize) -> Result<Self, BootParamsError> {
Self::new(start, size, E820_TYPE_RAM)
}
fn reserved(range: X86LinuxRange) -> Result<Self, BootParamsError> {
Self::new(range.start, range.size, E820_TYPE_RESERVED)
}
fn new(start: usize, size: usize, entry_type: u32) -> Result<Self, BootParamsError> {
Ok(Self {
addr: start as u64,
size: size as u64,
entry_type,
})
}
}
fn normalized_ranges(ranges: &[X86LinuxRange]) -> Result<Vec<X86LinuxRange>, BootParamsError> {
let mut ranges = ranges
.iter()
.copied()
.filter(|range| range.size != 0)
.collect::<Vec<_>>();
ranges.sort_by_key(|range| range.start);
let mut normalized = Vec::<X86LinuxRange>::new();
for range in ranges {
range.end().map_err(BootParamsError::Layout)?;
if let Some(last) = normalized.last_mut() {
let last_end = last.end().map_err(BootParamsError::Layout)?;
let range_end = range.end().map_err(BootParamsError::Layout)?;
if range.start <= last_end {
last.size = range_end.max(last_end) - last.start;
continue;
}
}
normalized.push(range);
}
Ok(normalized)
}
fn write_u8(buffer: &mut [u8], offset: usize, value: u8) {
buffer[offset] = value;
}
fn write_u16(buffer: &mut [u8], offset: usize, value: u16) {
buffer[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
}
fn write_u32(buffer: &mut [u8], offset: usize, value: u32) {
buffer[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}
fn write_u64(buffer: &mut [u8], offset: usize, value: u64) {
buffer[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
}
#[cfg(test)]
mod tests {
use super::{
linux::{BOOT_PARAMS_GPA, BOOT_STUB_GPA, BOOT_STUB_SIZE},
*,
};
const SETUP_SECTS_OFFSET: usize = 0x1f1;
const BOOT_FLAG_OFFSET: usize = 0x1fe;
const HEADER_OFFSET: usize = 0x202;
const VERSION_OFFSET: usize = 0x206;
const LOADFLAGS_OFFSET: usize = 0x211;
const CODE32_START_OFFSET: usize = 0x214;
const INITRD_ADDR_MAX_OFFSET: usize = 0x22c;
const KERNEL_ALIGNMENT_OFFSET: usize = 0x230;
const RELOCATABLE_KERNEL_OFFSET: usize = 0x234;
const CMDLINE_SIZE_OFFSET: usize = 0x238;
fn read_u8(buffer: &[u8], offset: usize) -> u8 {
buffer[offset]
}
fn read_u32(buffer: &[u8], offset: usize) -> u32 {
u32::from_le_bytes(buffer[offset..offset + 4].try_into().unwrap())
}
fn read_e820_entry(buffer: &[u8], idx: usize) -> E820Entry {
let offset = E820_TABLE_OFFSET + idx * E820_ENTRY_SIZE;
E820Entry {
addr: u64::from_le_bytes(buffer[offset..offset + 8].try_into().unwrap()),
size: u64::from_le_bytes(buffer[offset + 8..offset + 16].try_into().unwrap()),
entry_type: u32::from_le_bytes(buffer[offset + 16..offset + 20].try_into().unwrap()),
}
}
fn write_header_u16(image: &mut [u8], offset: usize, value: u16) {
image[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
}
fn write_header_u32(image: &mut [u8], offset: usize, value: u32) {
image[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}
fn valid_image() -> Vec<u8> {
let mut image = alloc::vec![0u8; SETUP_HEADER_END + 0x1000];
image[SETUP_SECTS_OFFSET] = 5;
write_header_u16(&mut image, BOOT_FLAG_OFFSET, 0xaa55);
write_header_u32(&mut image, HEADER_OFFSET, u32::from_le_bytes(*b"HdrS"));
write_header_u16(&mut image, VERSION_OFFSET, 0x020f);
image[LOADFLAGS_OFFSET] = 0x01;
write_header_u32(&mut image, CODE32_START_OFFSET, 0x100000);
write_header_u32(&mut image, INITRD_ADDR_MAX_OFFSET, 0x7fff_ffff);
write_header_u32(&mut image, KERNEL_ALIGNMENT_OFFSET, 0x20_0000);
image[RELOCATABLE_KERNEL_OFFSET] = 1;
write_header_u32(&mut image, CMDLINE_SIZE_OFFSET, 4096);
image
}
fn valid_layout(header: &X86LinuxHeader) -> X86LinuxLoadLayout {
X86LinuxLoadLayout::new(
header,
0x20_0000,
0x10_0000,
Some(X86LinuxRange::new(0x40_0000, 0x20_0000)),
)
.unwrap()
}
#[test]
fn builds_boot_params_with_patched_header_and_initrd() {
let image = valid_image();
let header = X86LinuxHeader::parse(&image).unwrap();
let layout = valid_layout(&header);
let mut builder =
BootParamsBuilder::new(&image, header, layout, X86LinuxRange::new(0, 0x80_0000));
builder
.set_command_line("console=ttyS0 rdinit=/init")
.unwrap();
let params = builder.build().unwrap();
assert_eq!(read_u8(¶ms, SENTINEL_OFFSET), 0xff);
assert_eq!(
read_u8(¶ms, TYPE_OF_LOADER_OFFSET),
TYPE_OF_LOADER_UNSPECIFIED
);
assert_eq!(
read_u8(¶ms, LOADFLAGS_OFFSET),
0x01 | LOADFLAG_CAN_USE_HEAP
);
assert_eq!(read_u32(¶ms, CODE32_START_OFFSET), 0x20_0000);
assert_eq!(read_u32(¶ms, RAMDISK_IMAGE_OFFSET), 0x40_0000);
assert_eq!(read_u32(¶ms, RAMDISK_SIZE_OFFSET), 0x20_0000);
assert_eq!(
read_u32(¶ms, CMD_LINE_PTR_OFFSET),
(BOOT_PARAMS_GPA + COMMAND_LINE_OFFSET) as u32
);
assert_eq!(
¶ms[COMMAND_LINE_OFFSET..COMMAND_LINE_OFFSET + 26],
b"console=ttyS0 rdinit=/init"
);
assert_eq!(read_u8(¶ms, COMMAND_LINE_OFFSET + 26), 0);
}
#[test]
fn uses_default_command_line_when_not_overridden() {
let image = valid_image();
let header = X86LinuxHeader::parse(&image).unwrap();
let layout = valid_layout(&header);
let params =
BootParamsBuilder::new(&image, header, layout, X86LinuxRange::new(0, 0x80_0000))
.build()
.unwrap();
assert_eq!(
¶ms[COMMAND_LINE_OFFSET..COMMAND_LINE_OFFSET + DEFAULT_COMMAND_LINE.len()],
DEFAULT_COMMAND_LINE.as_bytes()
);
assert_eq!(
read_u8(¶ms, COMMAND_LINE_OFFSET + DEFAULT_COMMAND_LINE.len()),
0
);
}
#[test]
fn rejects_command_line_that_does_not_fit_zero_page() {
let image = valid_image();
let header = X86LinuxHeader::parse(&image).unwrap();
let layout = valid_layout(&header);
let mut builder =
BootParamsBuilder::new(&image, header, layout, X86LinuxRange::new(0, 0x20_0000));
let long_command_line = alloc::string::String::from_utf8(alloc::vec![
b'a';
BOOT_PARAMS_SIZE - COMMAND_LINE_OFFSET
])
.unwrap();
assert_eq!(
builder.set_command_line(&long_command_line),
Err(BootParamsError::CommandLineTooLong {
len: BOOT_PARAMS_SIZE - COMMAND_LINE_OFFSET,
max: BOOT_PARAMS_SIZE - COMMAND_LINE_OFFSET - 1,
})
);
}
#[test]
fn rejects_command_line_with_nul() {
let image = valid_image();
let header = X86LinuxHeader::parse(&image).unwrap();
let layout = valid_layout(&header);
let mut builder =
BootParamsBuilder::new(&image, header, layout, X86LinuxRange::new(0, 0x20_0000));
assert_eq!(
builder.set_command_line("console=ttyS0\0rdinit=/init"),
Err(BootParamsError::CommandLineContainsNul)
);
}
#[test]
fn builds_e820_with_ram_and_reserved_low_ranges() {
let image = valid_image();
let header = X86LinuxHeader::parse(&image).unwrap();
let layout = valid_layout(&header);
let params =
BootParamsBuilder::new(&image, header, layout, X86LinuxRange::new(0, 0x20_0000))
.build()
.unwrap();
let entries = read_u8(¶ms, E820_ENTRIES_OFFSET) as usize;
assert!(entries >= 5);
assert_eq!(
read_e820_entry(¶ms, 0),
E820Entry::new(0, BOOT_PARAMS_GPA, 1).unwrap()
);
assert_eq!(
read_e820_entry(¶ms, 1),
E820Entry::reserved(X86LinuxRange::new(
BOOT_PARAMS_GPA,
BOOT_STUB_GPA + BOOT_STUB_SIZE - BOOT_PARAMS_GPA
))
.unwrap()
);
}
#[test]
fn builds_e820_with_multiple_ram_ranges() {
let image = valid_image();
let header = X86LinuxHeader::parse(&image).unwrap();
let layout = valid_layout(&header);
let mut builder = BootParamsBuilder::new(
&image,
header,
layout,
X86LinuxRange::new(0x0960_0000, 0x0800_0000),
);
builder.add_ram_range(X86LinuxRange::new(0, 0x10_0000));
let params = builder.build().unwrap();
let entries = read_u8(¶ms, E820_ENTRIES_OFFSET) as usize;
let has_low_usable = (0..entries).any(|idx| {
read_e820_entry(¶ms, idx) == E820Entry::new(0, BOOT_PARAMS_GPA, 1).unwrap()
});
let has_trampoline_usable = (0..entries).any(|idx| {
read_e820_entry(¶ms, idx)
== E820Entry::new(BOOT_STUB_GPA + BOOT_STUB_SIZE, 0xa0000 - 0x9000, 1).unwrap()
});
let has_high_usable = (0..entries).any(|idx| {
read_e820_entry(¶ms, idx) == E820Entry::new(0x0960_0000, 0x0800_0000, 1).unwrap()
});
assert!(has_low_usable);
assert!(has_trampoline_usable);
assert!(has_high_usable);
}
#[test]
fn records_reserved_passthrough_ranges_in_e820() {
let image = valid_image();
let header = X86LinuxHeader::parse(&image).unwrap();
let layout = valid_layout(&header);
let mut builder =
BootParamsBuilder::new(&image, header, layout, X86LinuxRange::new(0, 0x20_0000));
builder.add_reserved_range(X86LinuxRange::new(0xfec0_0000, 0x1000));
let params = builder.build().unwrap();
let entries = read_u8(¶ms, E820_ENTRIES_OFFSET) as usize;
let found = (0..entries).any(|idx| {
read_e820_entry(¶ms, idx)
== E820Entry::reserved(X86LinuxRange::new(0xfec0_0000, 0x1000)).unwrap()
});
assert!(found);
}
#[test]
fn rejects_truncated_setup_header_copy() {
let image = valid_image();
let header = X86LinuxHeader::parse(&image).unwrap();
let layout = valid_layout(&header);
let short_image = &image[..SETUP_HEADER_END - 1];
assert_eq!(
BootParamsBuilder::new(
short_image,
header,
layout,
X86LinuxRange::new(0, 0x20_0000)
)
.build(),
Err(BootParamsError::SetupHeaderTruncated {
image_size: SETUP_HEADER_END - 1,
required: SETUP_HEADER_END,
})
);
}
}