strat9_abi/boot.rs
1//! Bootloader-to-kernel handoff ABI (v2).
2//!
3//! This module defines the data structures passed from the bootloader
4//! to the kernel at entry point. The kernel reads these structures
5//! to discover memory layout, ACPI tables, framebuffer configuration,
6//! and kernel modules.
7//!
8//! # Boot flow
9//!
10//! ```text
11//! UEFI/BIOS => bootloader => kernel_main(KernelArgs)
12//! ```
13//!
14//! The bootloader populates `KernelArgs` in a reserved memory region,
15//! then jumps to the kernel entry point with a pointer to this structure
16//! in RDI (System V AMD64 ABI first argument).
17//!
18//! # ABI stability
19//!
20//! The `KernelArgs` layout is frozen per ABI version. Changing the layout
21//! requires bumping [`STRAT9_BOOT_ABI_VERSION`] and updating both
22//! bootloader and kernel simultaneously.
23//!
24//! # Virtual memory layout (BOOTBOOT-inspired)
25//!
26//! ```text
27//! 0xFFFF_DEAD_0000_0000 => Framebuffer (read-only after boot)
28//! 0xFFFF_BEEF_0000_0000 => Environment string (key=value)
29//! 0xFFFFFFFF_8000_0000 => Kernel code/data
30//! 0x0000_0000_0000_0000 => Identity map (first 4GB)
31//! ```
32//!
33//! # Example (kernel side)
34//!
35//! ```ignore
36//! unsafe fn kernel_main(args: *const KernelArgs) -> ! {
37//! let args = &*args;
38//! assert_eq!(args.magic, STRAT9_BOOT_MAGIC);
39//! assert_eq!(args.abi_version, STRAT9_BOOT_ABI_VERSION);
40//!
41//! // Memory map
42//! for region in unsafe { args.memory_regions() }.expect("invalid boot memory map") {
43//! match region.kind {
44//! MemoryKind::Free => { /* add to buddy allocator */ }
45//! _ => {}
46//! }
47//! }
48//!
49//! // Framebuffer (already mapped at 0xFFFF_DEAD_0000_0000)
50//! let fb = args.framebuffer_addr as *mut u32;
51//!
52//! // Environment (key=value pairs)
53//! if let Some(baud) = args.env_get("console.baud") {
54//! // baud = "115200"
55//! }
56//!
57//! // Modules
58//! for module in unsafe { args.modules() }.expect("invalid boot module table") {
59//! // module.name_str(), module.base, module.size
60//! }
61//! }
62//! ```
63
64use zerocopy::{FromBytes, IntoBytes};
65
66/// ABI version for the boot handoff structure.
67pub const STRAT9_BOOT_ABI_VERSION: u32 = 4;
68
69/// Magic number validating the boot handoff (`"ST9B"` in ASCII).
70pub const STRAT9_BOOT_MAGIC: u32 = 0x5354_3942; // "ST9B"
71
72/// Capacity of the fixed module table shared by the loader and kernel.
73pub const MAX_BOOT_MODULES: usize = 64;
74/// Maximum number of descriptors accepted by the kernel's boot-map work buffer.
75pub const MAX_BOOT_MEMORY_REGIONS: usize = 1024;
76pub const MODULE_TABLE_SIZE: usize = core::mem::size_of::<ModuleTable>();
77const MODULE_TABLE_HEADER_SIZE: usize = core::mem::offset_of!(ModuleTable, entries);
78
79/// Bootloader-to-kernel handoff structure (ABI v2, 136 bytes).
80///
81/// Field layout is ordered to avoid internal padding:
82/// - u64 fields first (8-byte aligned)
83/// - u32 fields next
84/// - u16 field
85/// - u8 fields last
86///
87/// # Field groups
88///
89/// ## Identity (8 bytes)
90/// - `magic`: must equal [`STRAT9_BOOT_MAGIC`] (`0x5354_3942`)
91/// - `abi_version`: must equal [`STRAT9_BOOT_ABI_VERSION`] (currently `3`)
92///
93/// ## Kernel memory (16 bytes)
94/// - `kernel_base`: physical address of the kernel ELF image
95/// - `kernel_size`: size of the kernel image in bytes
96///
97/// ## ACPI (8 bytes)
98/// - `acpi_rsdp_base`: physical address of the RSDP
99///
100/// ## Memory map (16 bytes)
101/// - `memory_map_base`: physical address of the [`MemoryRegion`] array
102/// - `memory_map_size`: total size of the memory map in bytes
103///
104/// ## Framebuffer (8 bytes + masks)
105/// - `framebuffer_addr`: **virtual** address (`0xFFFF_DEAD_0000_0000`)
106///
107/// ## HHDM (8 bytes)
108/// - `hhdm_offset`: Higher Half Direct Map offset
109///
110/// ## Environment (16 bytes)
111/// - `cmdline_ptr`: physical address of key=value string
112/// - `cmdline_len`: length of the string in bytes
113///
114/// ## Modules (16 bytes)
115/// - `modules_base`: physical address of the [`ModuleTable`]
116/// - `modules_size`: total size of the module table in bytes
117#[derive(Debug, FromBytes, IntoBytes)]
118#[repr(C, packed)]
119pub struct KernelArgs {
120 // --- u64 fields (aligned to 8) ---
121 pub magic: u32,
122 pub abi_version: u32,
123 pub kernel_base: u64,
124 pub kernel_size: u64,
125 pub acpi_rsdp_base: u64,
126 pub memory_map_base: u64,
127 pub memory_map_size: u64,
128 pub framebuffer_addr: u64,
129 pub hhdm_offset: u64,
130 pub cmdline_ptr: u64,
131 pub cmdline_len: u64,
132 pub modules_base: u64,
133 pub modules_size: u64,
134 // --- u32 fields ---
135 pub framebuffer_width: u32,
136 pub framebuffer_height: u32,
137 pub framebuffer_stride: u32,
138 // --- u16 field ---
139 pub framebuffer_bpp: u16,
140 // --- u8 fields ---
141 pub framebuffer_red_mask_size: u8,
142 pub framebuffer_red_mask_shift: u8,
143 pub framebuffer_green_mask_size: u8,
144 pub framebuffer_green_mask_shift: u8,
145 pub framebuffer_blue_mask_size: u8,
146 pub framebuffer_blue_mask_shift: u8,
147 // --- BSS region ---
148 pub bss_virt_base: u64,
149 pub bss_virt_size: u64,
150}
151
152// Ensure struct is exactly 132 bytes with no padding
153const _: () = assert!(core::mem::size_of::<KernelArgs>() == 132);
154
155impl KernelArgs {
156 /// Read the memory map, rejecting malformed lengths instead of truncating.
157 ///
158 /// # Safety
159 /// Any range that passes the numeric checks must be readable through the
160 /// current identity mapping, initialized and immutable for the returned
161 /// slice's lifetime. These checks cannot establish physical memory ownership.
162 pub unsafe fn memory_regions(&self) -> Result<&[MemoryRegion], &'static str> {
163 if self.memory_map_base == 0 && self.memory_map_size == 0 {
164 return Ok(&[]);
165 }
166 let size = checked_handoff_range(
167 self.memory_map_base,
168 self.memory_map_size,
169 core::mem::align_of::<MemoryRegion>(),
170 )?;
171 let entry_size = core::mem::size_of::<MemoryRegion>();
172 if size % entry_size != 0 {
173 return Err("memory map contains a partial descriptor");
174 }
175 let count = size / entry_size;
176 if count > MAX_BOOT_MEMORY_REGIONS {
177 return Err("memory map exceeds kernel capacity");
178 }
179 let ptr = self.memory_map_base as *const MemoryRegion;
180 Ok(unsafe { core::slice::from_raw_parts(ptr, count) })
181 }
182
183 /// Environment string as bytes (null-terminated key=value pairs).
184 pub fn cmdline_bytes(&self) -> &[u8] {
185 if self.cmdline_ptr == 0 || self.cmdline_len == 0 {
186 return &[];
187 }
188 let ptr = self.cmdline_ptr as *const u8;
189 let len = self.cmdline_len as usize;
190 unsafe { core::slice::from_raw_parts(ptr, len) }
191 }
192
193 /// Environment string as `&str` (without null terminator).
194 pub fn cmdline_str(&self) -> &str {
195 let bytes = self.cmdline_bytes();
196 let bytes = bytes.strip_suffix(&[0]).unwrap_or(bytes);
197 core::str::from_utf8(bytes).unwrap_or("")
198 }
199
200 /// Get the value of an environment variable by key.
201 ///
202 /// # Example
203 /// ```ignore
204 /// if let Some(baud) = args.env_get("console.baud") {
205 /// // baud = "115200"
206 /// }
207 /// ```
208 pub fn env_get(&self, key: &str) -> Option<&str> {
209 for line in self.cmdline_str().lines() {
210 if let Some((k, v)) = line.split_once('=') {
211 if k == key {
212 return Some(v);
213 }
214 }
215 }
216 None
217 }
218
219 /// Read the fixed-capacity module table after checking its advertised extent.
220 ///
221 /// # Safety
222 /// If the numeric extent checks succeed, the first MODULE_TABLE_SIZE bytes
223 /// must be readable through the current identity mapping, initialized and
224 /// immutable for the returned slice's lifetime. Module payloads are not read.
225 pub unsafe fn modules(&self) -> Result<&[ModuleEntry], &'static str> {
226 if self.modules_base == 0 && self.modules_size == 0 {
227 return Ok(&[]);
228 }
229 let size = checked_handoff_range(
230 self.modules_base,
231 self.modules_size,
232 core::mem::align_of::<ModuleTable>(),
233 )?;
234 if size < MODULE_TABLE_SIZE {
235 return Err("truncated fixed module table");
236 }
237 // Do not manufacture a reference to a table until its extent is checked.
238 let bytes = unsafe {
239 core::slice::from_raw_parts(self.modules_base as *const u8, MODULE_TABLE_SIZE)
240 };
241 ModuleTable::read_from(bytes)
242 }
243}
244
245/// Pure metadata validation, performed before dereferencing a handoff address.
246fn checked_handoff_range(base: u64, size: u64, align: usize) -> Result<usize, &'static str> {
247 if base == 0 || size == 0 {
248 return Err("inconsistent empty handoff range");
249 }
250 let end = base.checked_add(size).ok_or("handoff address overflow")?;
251 if base % align as u64 != 0 {
252 return Err("unaligned handoff range");
253 }
254 if end > usize::MAX as u64 || size > isize::MAX as u64 {
255 return Err("handoff range exceeds addressable size");
256 }
257 Ok(size as usize)
258}
259
260/// Module table header + entries.
261///
262/// The bootloader builds this in physical memory and passes the
263/// address via `KernelArgs::modules_base`.
264#[repr(C)]
265pub struct ModuleTable {
266 pub count: u32,
267 pub entries: [ModuleEntry; MAX_BOOT_MODULES],
268}
269
270impl ModuleTable {
271 /// Write the complete fixed table. All checks precede the first write.
272 /// Bytes beyond MODULE_TABLE_SIZE, including allocation padding, are untouched.
273 pub fn write_into(storage: &mut [u8], modules: &[ModuleEntry]) -> Result<(), &'static str> {
274 if modules.len() > MAX_BOOT_MODULES {
275 return Err("too many boot modules (maximum 64)");
276 }
277 if storage.len() < MODULE_TABLE_SIZE {
278 return Err("module table allocation too small");
279 }
280 if storage.as_ptr() as usize % core::mem::align_of::<Self>() != 0 {
281 return Err("unaligned module table allocation");
282 }
283 storage[..MODULE_TABLE_SIZE].fill(0);
284 // SAFETY: extent/alignment are checked, all fields accept zero, and the
285 // mutable byte slice supplies exclusive ownership for this borrow.
286 let table = unsafe { &mut *storage.as_mut_ptr().cast::<Self>() };
287 table.entries[..modules.len()].copy_from_slice(modules);
288 table.count = modules.len() as u32;
289 Ok(())
290 }
291
292 /// Validate the fixed table before forming a slice of initialized entries.
293 /// A shortened header-plus-count representation is not this ABI's format.
294 pub fn read_from(storage: &[u8]) -> Result<&[ModuleEntry], &'static str> {
295 if storage.len() < MODULE_TABLE_SIZE {
296 return Err("truncated fixed module table");
297 }
298 if storage.as_ptr() as usize % core::mem::align_of::<Self>() != 0 {
299 return Err("unaligned module table");
300 }
301 let count = u32::from_ne_bytes([storage[0], storage[1], storage[2], storage[3]]) as usize;
302 if count > MAX_BOOT_MODULES {
303 return Err("module count exceeds table capacity");
304 }
305 let end = count
306 .checked_mul(core::mem::size_of::<ModuleEntry>())
307 .and_then(|size| MODULE_TABLE_HEADER_SIZE.checked_add(size))
308 .ok_or("module table length overflow")?;
309 if end > storage.len() {
310 return Err("module entries exceed advertised table size");
311 }
312 // SAFETY: storage covers the fixed table, has the required alignment,
313 // ModuleEntry contains only integers, and the checked slice stays inside it.
314 let entries = unsafe {
315 storage
316 .as_ptr()
317 .add(MODULE_TABLE_HEADER_SIZE)
318 .cast::<ModuleEntry>()
319 };
320 Ok(unsafe { core::slice::from_raw_parts(entries, count) })
321 }
322}
323
324/// A single loaded module (userspace binary or config file).
325#[repr(C)]
326#[derive(Clone, Copy, Debug, PartialEq, Eq)]
327pub struct ModuleEntry {
328 /// Module name (null-terminated, max 63 chars).
329 pub name: [u8; 64],
330 /// Physical address of the module data.
331 pub base: u64,
332 /// Size of the module data in bytes.
333 pub size: u64,
334}
335
336impl ModuleEntry {
337 /// Validate a portable, single-component initfs name without normalization.
338 pub fn checked_name(&self) -> Result<&str, &'static str> {
339 let len = self
340 .name
341 .iter()
342 .position(|&b| b == 0)
343 .ok_or("module name is not NUL-terminated")?;
344 validate_module_name(&self.name[..len])
345 }
346
347 /// Module name as a string slice.
348 pub fn name_str(&self) -> &str {
349 let len = self.name.iter().position(|&b| b == 0).unwrap_or(63);
350 core::str::from_utf8(&self.name[..len]).unwrap_or("")
351 }
352}
353
354/// Names shared by the ESP producer, loader and initfs consumer: 1..=63 ASCII
355/// letters/digits, '.', '_' or '-'. Reject path components and FAT-ambiguous dots.
356pub fn validate_module_name(name: &[u8]) -> Result<&str, &'static str> {
357 if name.is_empty() || name.len() > 63 {
358 return Err("module name must contain 1 to 63 bytes");
359 }
360 if name == b"."
361 || name == b".."
362 || name.last() == Some(&b'.')
363 || !name
364 .iter()
365 .all(|b| b.is_ascii_alphanumeric() || matches!(*b, b'.' | b'_' | b'-'))
366 {
367 return Err("module name must be a portable ASCII filename");
368 }
369 core::str::from_utf8(name).map_err(|_| "module name is not ASCII")
370}
371
372/// Memory region descriptor for the bootloader memory map.
373#[derive(Debug, Clone, Copy, FromBytes, IntoBytes)]
374#[repr(C)]
375pub struct MemoryRegion {
376 pub base: u64,
377 pub size: u64,
378 pub kind: MemoryKind,
379}
380
381/// Memory region type identifier.
382#[derive(Clone, Copy, Debug, PartialEq, Eq, FromBytes, IntoBytes)]
383#[repr(transparent)]
384pub struct MemoryKind(pub u64);
385
386#[allow(non_upper_case_globals)]
387impl MemoryKind {
388 pub const Null: Self = Self(0);
389 pub const Free: Self = Self(1);
390 pub const Reclaim: Self = Self(2);
391 pub const Reserved: Self = Self(3);
392}
393
394// ABI size assertions (packed: no padding, 132 bytes with BSS fields)
395const _: () = assert!(core::mem::size_of::<KernelArgs>() == 132);
396const _: () = assert!(core::mem::align_of::<KernelArgs>() == 1);
397static_assertions::assert_eq_size!(MemoryRegion, [u8; 24]);
398static_assertions::const_assert_eq!(core::mem::align_of::<MemoryRegion>(), 8);
399static_assertions::assert_eq_size!(MemoryKind, [u8; 8]);
400static_assertions::assert_eq_size!(ModuleEntry, [u8; 80]);
401static_assertions::assert_eq_size!(ModuleTable, [u8; 5128]);
402static_assertions::const_assert_eq!(MODULE_TABLE_HEADER_SIZE, 8);