Skip to main content

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);