From 00e4c6076d6e1d2ae57ed69c27d7b098c51ae551 Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 18:09:11 +0200 Subject: [PATCH 1/3] The loader calls UEFI through its own bindings, and uefi and uefi-services go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner ruled on the dependency audit's uefi row: own bindings, in place of the loader track's plan to move to the current uefi release. `bootloader/src/efi/` holds UEFI 2.10's tables, the protocols the loader opens (LoadedImage, DevicePath, SimpleFileSystem and File, PartitionInfo, BlockIo, GraphicsOutput, Rng, PciRootBridgeIo), the console it writes, the status codes and GUIDs, each struct cited to its section and pinned by compile-time size_of/offset_of! asserts that run on both loader targets at every build. A module and not a crate: the loader is its one user, and a crate would add a manifest, a description and a workspace entry for no second reader; a host test cannot link a no_std, no_main UEFI binary, and the asserts check the layout on the targets themselves. The wrappers are safe where the hazard was ours: - a name crosses to firmware only as CStr16, which carries its NUL; - a protocol is opened only through BootServices::get (GET_PROTOCOL) or ::exclusive, whose bound is the Exclusive marker; the opener is private, so the two clippy.toml rules that guarded uefi's openers go with them; - an open protocol closes when its Scoped drops, a handle buffer is freed; - the console and the allocator refuse once exit_boot_services has run, which nulls the table itself: no SIGNAL_EXIT_BOOT_SERVICES callback is registered, so end_this_pass has no event to close; - BlockIo reads OptimalTransferLengthGranularity only at revision 3 and later, which deletes the unsafe cast rootimage.rs needed to reach the revision through uefi's type; - the memory map is taken into &mut [u64], which deletes watchdog.rs's unsafe byte-slice cast. The panic handler says `[PANIC]: ` on the console, as uefi-services' did, and powers the machine off; uefi-services' ten-second stall before the power-off, a flat wait, does not come with it. One HARDDRIVE decoder (efi::HardDrive::parse, UEFI 2.10 §10.3.5.1) serves boot_partition, bootnext's protocol path and its NVRAM load options, and boot_disk; bootnext's own byte decoder goes, and a load option's HARDDRIVE node is now held to the spec's 42 bytes. The ESP's \toyos\log.guid is read off LoadedImage's DeviceHandle, the volume firmware loaded the image from (§9.1.1), not through LocateDevicePath. A status prints by its Appendix D name: `(NOT_FOUND)` where the loader wrote `(UEFI Error NOT_FOUND: ())`. That is the one change to the loader's text, and it is on refusal lines alone. The loader track's stage 4 loses its uefi bullet and its uefi-services exit, and the owner's ruling joins its bounds. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- Cargo.lock | 91 --- bootloader/Cargo.toml | 4 - bootloader/src/attempt.rs | 22 +- bootloader/src/blackbox.rs | 11 +- bootloader/src/bootnext.rs | 63 +- bootloader/src/efi/boot.rs | 402 +++++++++++ bootloader/src/efi/mod.rs | 546 +++++++++++++++ bootloader/src/efi/proto.rs | 661 ++++++++++++++++++ bootloader/src/efi/runtime.rs | 175 +++++ bootloader/src/floor.rs | 28 +- bootloader/src/gcd.rs | 8 +- bootloader/src/loaderlog.rs | 43 +- bootloader/src/main.rs | 186 +++-- bootloader/src/protocol.rs | 41 -- bootloader/src/rootbridge.rs | 57 +- bootloader/src/rootimage.rs | 67 +- bootloader/src/seed.rs | 13 +- bootloader/src/slot.rs | 28 +- bootloader/src/stamp.rs | 2 +- bootloader/src/watchdog.rs | 21 +- clippy.toml | 2 - ...oes-only-what-must-precede-the-handover.md | 17 +- 22 files changed, 1997 insertions(+), 491 deletions(-) create mode 100644 bootloader/src/efi/boot.rs create mode 100644 bootloader/src/efi/mod.rs create mode 100644 bootloader/src/efi/proto.rs create mode 100644 bootloader/src/efi/runtime.rs delete mode 100644 bootloader/src/protocol.rs diff --git a/Cargo.lock b/Cargo.lock index 75516233810..08e3d4ca888 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -278,12 +278,6 @@ dependencies = [ "sha2 0.10.9", ] -[[package]] -name = "bit_field" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" - [[package]] name = "bitflags" version = "1.3.2" @@ -387,8 +381,6 @@ dependencies = [ "toyos-tsc", "toyos-update", "toyos-wallclock", - "uefi", - "uefi-services", ] [[package]] @@ -4323,26 +4315,6 @@ dependencies = [ "parking_lot", ] -[[package]] -name = "ptr_meta" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcada80daa06c42ed5f48c9a043865edea5dc44cbf9ac009fda3b89526e28607" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca9224df2e20e7c5548aeb5f110a0f3b77ef05f8585139b7148b59056168ed2" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "pxfm" version = "0.1.28" @@ -6213,69 +6185,6 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" -[[package]] -name = "ucs2" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad643914094137d475641b6bab89462505316ec2ce70907ad20102d28a79ab8" -dependencies = [ - "bit_field", -] - -[[package]] -name = "uefi" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ead9f748a4646479b850add36b527113a80e80a7e0f44d7b0334291850dcc5" -dependencies = [ - "bitflags 2.11.0", - "log", - "ptr_meta", - "ucs2", - "uefi-macros", - "uefi-raw", - "uguid", -] - -[[package]] -name = "uefi-macros" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a7b1c2c808c3db854a54d5215e3f7e7aaf5dcfbce095598cba6af29895695d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "uefi-raw" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864ac69eadd877bfb34e7814be1928122ed0057d9f975169a56ee496aa7bdfd7" -dependencies = [ - "bitflags 2.11.0", - "ptr_meta", - "uguid", -] - -[[package]] -name = "uefi-services" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a79fcb420624743c895bad0f9480fbc2f64e7c8d8611fb1ada6bdd799942feb4" -dependencies = [ - "cfg-if", - "log", - "uefi", -] - -[[package]] -name = "uguid" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab14ea9660d240e7865ce9d54ecdbd1cd9fa5802ae6f4512f093c7907e921533" - [[package]] name = "unicode-bom" version = "2.0.3" diff --git a/bootloader/Cargo.toml b/bootloader/Cargo.toml index aaf27e400f4..9250af93cee 100644 --- a/bootloader/Cargo.toml +++ b/bootloader/Cargo.toml @@ -39,7 +39,3 @@ toyos-update = { path = "../toyos-update" } # with: this target is soft-float, and the x86 backend reaches for SSE and # SHA-NI registers a UEFI application may not assume are its own. sha2 = { version = "0.10", default-features = false, features = ["force-soft"] } -# `alloc`: `variable_keys` and `get_variable_boxed`, which are how the boot -# entry pointing at this image is found among the firmware's own variables. -uefi = { version = "0.26.0", default-features = false, features = ["alloc"] } -uefi-services = { version = "0.23.0", features = ["panic_handler", "logger"] } diff --git a/bootloader/src/attempt.rs b/bootloader/src/attempt.rs index 069705f9ce0..64640669762 100644 --- a/bootloader/src/attempt.rs +++ b/bootloader/src/attempt.rs @@ -7,9 +7,7 @@ use alloc::string::String; use toyos_update::record::{self, Record}; -use uefi::proto::media::file::{Directory, File, FileAttribute, FileMode}; -use uefi::prelude::*; -use uefi::{cstr16, CStr16}; +use crate::efi::{cstr16, CStr16, File, Mode, Status, SystemTable}; use crate::loaderlog; @@ -25,12 +23,12 @@ const NAME: &CStr16 = cstr16!("attempts"); /// **`Err` is not zero.** A volume this cannot read is one the bound is off on, /// and the caller says so rather than treating an unreadable stick as a first /// attempt — which would be a bound that silently never fires. -pub fn read(system_table: &SystemTable, guid: &[u8; 16]) -> Result { +pub fn read(system_table: &SystemTable, guid: &[u8; 16]) -> Result { loaderlog::with_volume(system_table, guid, |root| { - let file = match root.open(NAME, FileMode::Read, FileAttribute::empty()) { + let file = match root.open(NAME, Mode::Read) { Ok(file) => file, // No file is a first attempt, which is every freshly flashed image. - Err(e) if e.status() == Status::NOT_FOUND => return Ok(Record::default()), + Err(Status::NOT_FOUND) => return Ok(Record::default()), Err(e) => return Err(alloc::format!("{NAME} would not open ({e})")), }; let Some(mut file) = file.into_regular_file() else { @@ -50,7 +48,7 @@ pub fn read(system_table: &SystemTable, guid: &[u8; 16]) -> Result, guid: &[u8; 16], record: &Record) -> Result<(), String> { +pub fn write(system_table: &SystemTable, guid: &[u8; 16], record: &Record) -> Result<(), String> { loaderlog::with_volume(system_table, guid, |root| put(root, guid, record)).and_then(|inner| inner) } @@ -60,25 +58,25 @@ pub fn write_chosen(guid: &[u8; 16], record: &Record) -> Result<(), String> { loaderlog::with_open_volume(|root| put(root, guid, record)).and_then(|inner| inner) } -fn put(root: &mut Directory, guid: &[u8; 16], record: &Record) -> Result<(), String> { +fn put(root: &mut File, guid: &[u8; 16], record: &Record) -> Result<(), String> { // Deleted and recreated rather than rewound: a fixed-width record is // still a record a shorter write would leave the tail of. - match root.open(NAME, FileMode::ReadWrite, FileAttribute::empty()) { + match root.open(NAME, Mode::ReadWrite) { Ok(stale) => { if let Err(e) = stale.delete() { return Err(alloc::format!("{NAME} would not delete ({e})")); } } - Err(e) if e.status() == Status::NOT_FOUND => {} + Err(Status::NOT_FOUND) => {} Err(e) => return Err(alloc::format!("{NAME} would not open ({e})")), } let file = root - .open(NAME, FileMode::CreateReadWrite, FileAttribute::empty()) + .open(NAME, Mode::CreateReadWrite) .map_err(|e| alloc::format!("{NAME} would not be created ({e})"))?; let Some(mut file) = file.into_regular_file() else { return Err(alloc::format!("{NAME} on the log partition is a directory")); }; - file.write(&record.encode(guid)).map_err(|e| alloc::format!("{NAME} would not write ({e})"))?; + file.write(&record.encode(guid)).map_err(|(e, _)| alloc::format!("{NAME} would not write ({e})"))?; // **Flushed here and not at the handoff.** What this file exists to // survive is a power cut, and a byte in a cache survives nothing. file.flush().map_err(|e| alloc::format!("{NAME} would not flush ({e})"))?; diff --git a/bootloader/src/blackbox.rs b/bootloader/src/blackbox.rs index 0f749810338..7ddf8fc8768 100644 --- a/bootloader/src/blackbox.rs +++ b/bootloader/src/blackbox.rs @@ -16,8 +16,7 @@ use alloc::string::String; use alloc::vec::Vec; -use uefi::prelude::*; -use uefi::table::boot::{AllocateType, MemoryType}; +use crate::efi::{AllocateType, SystemTable}; use toyos_blackbox::{BYTES, PHYS, State}; use toyos_wallclock::Civil; @@ -53,12 +52,8 @@ pub struct Page(u64); /// last one's file or replaces it is what the page decides — so a refusal is /// returned as a line for the caller to write rather than printed here, where a /// machine with no console would lose it. -pub fn claim(system_table: &SystemTable) -> (Option, Option) { - match system_table.boot_services().allocate_pages( - AllocateType::Address(PHYS), - MemoryType::LOADER_DATA, - toyos_blackbox::PAGES, - ) { +pub fn claim(system_table: &SystemTable) -> (Option, Option) { + match system_table.boot_services().allocate_pages(AllocateType::Address(PHYS), toyos_blackbox::PAGES) { Ok(at) => { // `AllocateType::Address` allocates that address or fails; firmware // answering with another one has not done what was asked of it. diff --git a/bootloader/src/bootnext.rs b/bootloader/src/bootnext.rs index b38e223a8bc..54c260c9fb0 100644 --- a/bootloader/src/bootnext.rs +++ b/bootloader/src/bootnext.rs @@ -15,12 +15,7 @@ //! that booted us from a removable-media fallback path has no entry of ours at //! all and must be told so rather than have one guessed at. -use uefi::prelude::*; -use uefi::proto::device_path::media::PartitionSignature; -use uefi::proto::device_path::{DevicePath, DeviceSubType, DeviceType}; -use uefi::proto::loaded_image::LoadedImage; -use uefi::table::runtime::{VariableAttributes, VariableVendor}; -use uefi::CStr16; +use crate::efi::{cstr16, CStr16, DevicePath, Handle, HardDrive, LoadedImage, SystemTable, VariableAttributes, GLOBAL_VARIABLE}; /// The head of every line this module writes. const HEAD: &str = "Boot chain:"; @@ -39,7 +34,7 @@ const LOAD_OPTION_HEAD: usize = 6; /// A refusal is not a failure of the boot: the kernel still runs and still seals /// its page. What is lost is the *next* boot, so the line says exactly that /// rather than reporting a variable write. -pub fn point_at_us(handle: Handle, system_table: &SystemTable) { +pub fn point_at_us(handle: Handle, system_table: &SystemTable) { let Some(ours) = our_partition(handle, system_table) else { return println!( "{HEAD} firmware did not load this image off a GPT partition, so there is no entry \ @@ -54,7 +49,7 @@ pub fn point_at_us(handle: Handle, system_table: &SystemTable) { }; let write = system_table.runtime_services().set_variable( cstr16!("BootNext"), - &VariableVendor::GLOBAL_VARIABLE, + &GLOBAL_VARIABLE, // Non-volatile, because it has to survive the reset that is the whole point. VariableAttributes::NON_VOLATILE | VariableAttributes::BOOTSERVICE_ACCESS @@ -71,24 +66,21 @@ pub fn point_at_us(handle: Handle, system_table: &SystemTable) { } /// The GPT partition GUID of the volume firmware loaded this image from. -fn our_partition(handle: Handle, system_table: &SystemTable) -> Option<[u8; 16]> { +fn our_partition(handle: Handle, system_table: &SystemTable) -> Option<[u8; 16]> { let bs = system_table.boot_services(); - let image = crate::protocol::exclusive::(bs, handle).ok()?; + let image = bs.exclusive::(handle).ok()?; let device = image.device()?; - let path = crate::protocol::exclusive::(bs, device).ok()?; - hard_drive_guid(path.node_iter()) + let path = bs.exclusive::(device).ok()?; + hard_drive_guid(path.nodes()) } /// The GPT signature of the first HARDDRIVE node in a device path, or `None` /// where the path has none — a network boot, or a disk with no GPT. -fn hard_drive_guid<'a>(nodes: impl Iterator) -> Option<[u8; 16]> { - for node in nodes { - if node.full_type() != (DeviceType::MEDIA, DeviceSubType::MEDIA_HARD_DRIVE) { - continue; - } - let hd = <&uefi::proto::device_path::media::HardDrive>::try_from(node).ok()?; - if let PartitionSignature::Guid(guid) = hd.partition_signature() { - return Some(guid.to_bytes()); +fn hard_drive_guid<'a>(nodes: impl Iterator) -> Option<[u8; 16]> { + for node in nodes.filter(|node| (node[0], node[1]) == HardDrive::TYPE) { + let hd = HardDrive::parse(node)?; + if hd.signature_type == HardDrive::GUID_SIGNATURE { + return Some(hd.signature); } } None @@ -98,17 +90,16 @@ fn hard_drive_guid<'a>(nodes: impl Iterator, ours: &[u8; 16]) -> Option { +fn entry_for(system_table: &SystemTable, ours: &[u8; 16]) -> Option { let rt = system_table.runtime_services(); let keys = rt.variable_keys().ok()?; let mut found: Option = None; - for key in keys { - if key.vendor != VariableVendor::GLOBAL_VARIABLE { + for (name, vendor) in keys { + if vendor != GLOBAL_VARIABLE { continue; } - let Ok(name) = key.name() else { continue }; - let Some(number) = entry_number(name) else { continue }; - let Ok((bytes, _)) = rt.get_variable_boxed(name, &key.vendor) else { continue }; + let Some(number) = entry_number(&name) else { continue }; + let Ok((bytes, _)) = rt.get_variable(&name, &vendor) else { continue }; if !load_option_names(&bytes, ours) { continue; } @@ -121,7 +112,7 @@ fn entry_for(system_table: &SystemTable, ours: &[u8; 16]) -> Option { /// `Boot0003` is entry 3; anything else here is some other global variable. fn entry_number(name: &CStr16) -> Option { - let mut chars = name.iter().map(|c| char::from(*c)); + let mut chars = name.units().iter().map(|&unit| char::from_u32(u32::from(unit)).unwrap_or(char::REPLACEMENT_CHARACTER)); for want in ENTRY_PREFIX.chars() { if chars.next()? != want { return None; @@ -162,7 +153,7 @@ fn load_option_names(option: &[u8], ours: &[u8; 16]) -> bool { // A node shorter than its own header, or longer than what is left, ends // the walk: neither can be stepped over. let Some(this) = path.get(..len).filter(|_| len >= NODE_HEADER) else { return false }; - if this[0] == MEDIA_HARD_DRIVE.0 && this[1] == MEDIA_HARD_DRIVE.1 { + if (this[0], this[1]) == HardDrive::TYPE { if let Some(guid) = gpt_signature(this) { return guid == *ours; } @@ -175,18 +166,8 @@ fn load_option_names(option: &[u8], ours: &[u8; 16]) -> bool { /// A device path node's type, subtype and length (UEFI 2.10 §10.2). const NODE_HEADER: usize = 4; -/// The MEDIA/HARD_DRIVE node this looks for, as the two bytes it is on the wire. -const MEDIA_HARD_DRIVE: (u8, u8) = (4, 1); - -/// A HARD_DRIVE node's GPT signature, or `None` where it names an MBR one or -/// the node is short (UEFI 2.10 §10.3.6: the signature is sixteen bytes at -/// offset 24, and `SignatureType` 2 is the GPT one). +/// A HARD_DRIVE node's GUID signature, or `None` where it carries another or +/// is not the length §10.3.5.1 gives it. fn gpt_signature(node: &[u8]) -> Option<[u8; 16]> { - const SIGNATURE: usize = 24; - const SIGNATURE_TYPE: usize = 41; - const GPT: u8 = 2; - if node.get(SIGNATURE_TYPE) != Some(&GPT) { - return None; - } - node.get(SIGNATURE..SIGNATURE + 16)?.try_into().ok() + HardDrive::parse(node).filter(|hd| hd.signature_type == HardDrive::GUID_SIGNATURE).map(|hd| hd.signature) } diff --git a/bootloader/src/efi/boot.rs b/bootloader/src/efi/boot.rs new file mode 100644 index 00000000000..8009db982e2 --- /dev/null +++ b/bootloader/src/efi/boot.rs @@ -0,0 +1,402 @@ +//! `EFI_BOOT_SERVICES` (UEFI 2.10 §4.4) and what this loader calls of them: +//! pages and pool, the memory map, the watchdog, handles, protocols, and the +//! exit. +//! +//! **A protocol is opened two ways and no other.** EXCLUSIVE calls `Stop` on +//! every driver holding the protocol BY_DRIVER (UEFI 2.10 §7.3.9, +//! `OpenProtocol()`), and on `GraphicsOutput` that is the firmware's graphics +//! console, whose screen the loader's own lines are on. [`BootServices::get`] +//! opens GET_PROTOCOL, which stops nothing; [`BootServices::exclusive`] opens +//! only an [`Exclusive`] protocol, one no firmware console drives. + +use core::ffi::c_void; +use core::mem::offset_of; +use core::ptr::{self, NonNull}; + +use super::{Guid, Handle, Status, TableHeader}; + +/// Every page UEFI counts (§7.2.1). +pub const PAGE_SIZE: usize = 4096; + +/// `EfiLoaderData` (§7.2.1): what this loader allocates, and what the +/// firmware gives an application's data. +const LOADER_DATA: u32 = 2; + +type Unused = *const c_void; + +/// `EFI_BOOT_SERVICES` (§4.4), in the spec's own field order. +#[repr(C)] +pub struct BootServices { + hdr: TableHeader, + raise_tpl: Unused, + restore_tpl: Unused, + allocate_pages: unsafe extern "efiapi" fn(kind: u32, memory: u32, pages: usize, at: *mut u64) -> Status, + free_pages: unsafe extern "efiapi" fn(at: u64, pages: usize) -> Status, + get_memory_map: unsafe extern "efiapi" fn( + size: *mut usize, + map: *mut u64, + key: *mut usize, + descriptor_size: *mut usize, + descriptor_version: *mut u32, + ) -> Status, + allocate_pool: unsafe extern "efiapi" fn(memory: u32, size: usize, at: *mut *mut u8) -> Status, + free_pool: unsafe extern "efiapi" fn(at: *mut u8) -> Status, + create_event: Unused, + set_timer: Unused, + wait_for_event: Unused, + signal_event: Unused, + close_event: Unused, + check_event: Unused, + install_protocol_interface: Unused, + reinstall_protocol_interface: Unused, + uninstall_protocol_interface: Unused, + handle_protocol: Unused, + reserved: Unused, + register_protocol_notify: Unused, + locate_handle: Unused, + locate_device_path: Unused, + install_configuration_table: Unused, + load_image: Unused, + start_image: Unused, + exit: Unused, + unload_image: Unused, + exit_boot_services: unsafe extern "efiapi" fn(image: *mut c_void, key: usize) -> Status, + get_next_monotonic_count: Unused, + stall: Unused, + set_watchdog_timer: + unsafe extern "efiapi" fn(seconds: usize, code: u64, data_size: usize, data: *const u16) -> Status, + connect_controller: Unused, + disconnect_controller: Unused, + open_protocol: unsafe extern "efiapi" fn( + handle: *mut c_void, + protocol: *const Guid, + interface: *mut *mut c_void, + agent: *mut c_void, + controller: *mut c_void, + attributes: u32, + ) -> Status, + close_protocol: unsafe extern "efiapi" fn( + handle: *mut c_void, + protocol: *const Guid, + agent: *mut c_void, + controller: *mut c_void, + ) -> Status, + open_protocol_information: Unused, + protocols_per_handle: Unused, + locate_handle_buffer: unsafe extern "efiapi" fn( + search: u32, + protocol: *const Guid, + key: *const c_void, + count: *mut usize, + buffer: *mut *mut Handle, + ) -> Status, + locate_protocol: Unused, + install_multiple_protocol_interfaces: Unused, + uninstall_multiple_protocol_interfaces: Unused, + calculate_crc32: Unused, + copy_mem: Unused, + set_mem: Unused, + create_event_ex: Unused, +} + +const _: () = { + assert!(size_of::() == 24 + 44 * 8); + assert!(offset_of!(BootServices, allocate_pages) == 40); + assert!(offset_of!(BootServices, free_pages) == 48); + assert!(offset_of!(BootServices, get_memory_map) == 56); + assert!(offset_of!(BootServices, allocate_pool) == 64); + assert!(offset_of!(BootServices, free_pool) == 72); + assert!(offset_of!(BootServices, exit_boot_services) == 232); + assert!(offset_of!(BootServices, set_watchdog_timer) == 256); + assert!(offset_of!(BootServices, open_protocol) == 280); + assert!(offset_of!(BootServices, close_protocol) == 288); + assert!(offset_of!(BootServices, locate_handle_buffer) == 312); +}; + +/// `EFI_ALLOCATE_TYPE` (§7.2.1), as this loader asks. +pub enum AllocateType { + AnyPages, + Address(u64), +} + +/// `OpenProtocol`'s two attributes this loader passes (§7.3.9). +const GET_PROTOCOL: u32 = 0x02; +const EXCLUSIVE: u32 = 0x20; + +/// `ByProtocol` of `EFI_LOCATE_SEARCH_TYPE` (§7.3.15). +const BY_PROTOCOL: u32 = 2; + +/// An interface firmware installs on a handle. +/// +/// # Safety +/// `Self` is the layout of the interface installed under [`Protocol::GUID`]. +pub unsafe trait Protocol { + const GUID: Guid; +} + +/// A protocol this loader may hold EXCLUSIVE: one no firmware console drives. +pub trait Exclusive: Protocol {} + +/// A protocol open on a handle, closed when this drops. +pub struct Scoped<'a, P: Protocol> { + bs: &'a BootServices, + handle: Handle, + interface: NonNull

, +} + +impl core::ops::Deref for Scoped<'_, P> { + type Target = P; + fn deref(&self) -> &P { + // SAFETY: firmware's interface, installed under `P::GUID` and held + // open for as long as `self`. + unsafe { self.interface.as_ref() } + } +} + +impl Scoped<'_, P> { + /// The interface as firmware's functions take it, `This`. + pub fn this(&self) -> *mut P { + self.interface.as_ptr() + } +} + +impl Drop for Scoped<'_, P> { + fn drop(&mut self) { + // SAFETY: the open this undoes, under the same agent. + let status = unsafe { + (self.bs.close_protocol)(self.handle.as_ptr(), &P::GUID, super::image().as_ptr(), ptr::null_mut()) + }; + // Only a close that names another open fails, and this names its own. + assert!(status.is_success(), "CloseProtocol({}) answered {status}", P::GUID); + } +} + +/// The handles `LocateHandleBuffer` answered, in its pool buffer. +pub struct Handles<'a> { + bs: &'a BootServices, + at: NonNull, + count: usize, +} + +impl core::ops::Deref for Handles<'_> { + type Target = [Handle]; + fn deref(&self) -> &[Handle] { + // SAFETY: firmware answered `count` handles at `at`. + unsafe { core::slice::from_raw_parts(self.at.as_ptr(), self.count) } + } +} + +impl Drop for Handles<'_> { + fn drop(&mut self) { + // SAFETY: the pool buffer `LocateHandleBuffer` allocated for this answer. + let freed = unsafe { self.bs.free_pool(self.at.as_ptr().cast()) }; + assert!(freed.is_ok(), "firmware would not take back the handle buffer it gave"); + } +} + +/// `EFI_MEMORY_DESCRIPTOR` (§7.2.3). Firmware's stride is its own +/// `DescriptorSize`, never this struct's size. +#[derive(Clone, Copy)] +#[repr(C)] +pub struct MemoryDescriptor { + pub ty: u32, + pub phys_start: u64, + pub virt_start: u64, + pub page_count: u64, + pub att: u64, +} + +const _: () = { + assert!(size_of::() == 40); + assert!(offset_of!(MemoryDescriptor, phys_start) == 8); + assert!(offset_of!(MemoryDescriptor, page_count) == 24); + assert!(offset_of!(MemoryDescriptor, att) == 32); +}; + +impl MemoryDescriptor { + /// `EfiMemoryMappedIO` and `EfiMemoryMappedIOPortSpace` (§7.2.1). + pub const MMIO: u32 = 11; + pub const MMIO_PORT_SPACE: u32 = 12; + /// `EFI_MEMORY_WB` (§7.2.3). + pub const WRITE_BACK: u64 = 0x8; +} + +/// What `GetMemoryMap` wrote: its key and its extent. +#[derive(Clone, Copy)] +pub(super) struct Filled { + pub(super) key: usize, + size: usize, + entry_size: usize, +} + +/// The memory map in the buffer it was taken into. +pub struct MemoryMap<'a> { + words: &'a [u64], + filled: Filled, +} + +impl<'a> MemoryMap<'a> { + pub(super) fn new(words: &'a [u64], filled: Filled) -> Self { + MemoryMap { words, filled } + } + + pub fn entries(&self) -> Entries<'_> { + Entries { map: self, next: 0 } + } +} + +/// A [`MemoryMap`]'s descriptors, in firmware's order. +pub struct Entries<'a> { + map: &'a MemoryMap<'a>, + next: usize, +} + +impl Iterator for Entries<'_> { + type Item = MemoryDescriptor; + + fn next(&mut self) -> Option { + let Filled { size, entry_size, .. } = self.map.filled; + if self.next >= size / entry_size { + return None; + } + let at = self.next * entry_size; + self.next += 1; + // SAFETY: `fill_memory_map` checked the descriptors lie inside the + // buffer and that each is at least a `MemoryDescriptor` long. + Some(unsafe { self.map.words.as_ptr().cast::().add(at).cast::().read_unaligned() }) + } +} + +impl BootServices { + /// `AllocatePages` (§7.2.1), as `EfiLoaderData`. + pub fn allocate_pages(&self, kind: AllocateType, pages: usize) -> Result { + let (kind, mut at) = match kind { + AllocateType::AnyPages => (0, 0), + AllocateType::Address(at) => (2, at), + }; + // SAFETY: the out parameter is a live `u64`. + unsafe { (self.allocate_pages)(kind, LOADER_DATA, pages, &mut at) }.ok().map(|()| at) + } + + /// `FreePages` (§7.2.2). + /// + /// # Safety + /// `at` and `pages` are an allocation `allocate_pages` made, and nothing + /// reads or writes it after. + pub unsafe fn free_pages(&self, at: u64, pages: usize) -> Result<(), Status> { + // SAFETY: the caller's contract. + unsafe { (self.free_pages)(at, pages) }.ok() + } + + /// `AllocatePool` (§7.2.4), as `EfiLoaderData`: 8-byte aligned. + pub fn allocate_pool(&self, size: usize) -> Result<*mut u8, Status> { + let mut at = ptr::null_mut(); + // SAFETY: the out parameter is a live pointer. + unsafe { (self.allocate_pool)(LOADER_DATA, size, &mut at) }.ok().map(|()| at) + } + + /// `FreePool` (§7.2.5). + /// + /// # Safety + /// `at` is a pool allocation of firmware's, and nothing reads or writes it + /// after. + pub unsafe fn free_pool(&self, at: *mut u8) -> Result<(), Status> { + // SAFETY: the caller's contract. + unsafe { (self.free_pool)(at) }.ok() + } + + /// The bytes the memory map takes now, and the size of one descriptor. + pub fn memory_map_size(&self) -> (usize, usize) { + let (mut size, mut key, mut entry_size, mut version) = (0, 0, 0, 0); + // SAFETY: a zero size with no buffer asks only for the size (§7.2.3). + let status = + unsafe { (self.get_memory_map)(&mut size, ptr::null_mut(), &mut key, &mut entry_size, &mut version) }; + assert!(status == Status::BUFFER_TOO_SMALL, "GetMemoryMap with no buffer answered {status}"); + (size, entry_size) + } + + /// The memory map, taken into `words`. + pub fn memory_map<'b>(&self, words: &'b mut [u64]) -> Result, Status> { + let filled = self.fill_memory_map(words)?; + Ok(MemoryMap::new(words, filled)) + } + + pub(super) fn fill_memory_map(&self, words: &mut [u64]) -> Result { + let capacity = size_of_val(words); + let (mut size, mut key, mut entry_size, mut version) = (capacity, 0, 0, 0); + // SAFETY: `words` is `size` writable bytes, aligned for a descriptor. + unsafe { (self.get_memory_map)(&mut size, words.as_mut_ptr(), &mut key, &mut entry_size, &mut version) } + .ok()?; + assert!( + size <= capacity && entry_size >= size_of::(), + "GetMemoryMap answered {size} bytes of {entry_size}-byte descriptors into {capacity}" + ); + Ok(Filled { key, size, entry_size }) + } + + /// `ExitBootServices` (§7.4.6). + pub(super) fn exit(&self, image: Handle, key: usize) -> Status { + // SAFETY: the call is the spec's; the caller stops using boot services + // when it succeeds. + unsafe { (self.exit_boot_services)(image.as_ptr(), key) } + } + + /// This image's handle. + pub fn image_handle(&self) -> Handle { + super::image() + } + + /// `SetWatchdogTimer` (§7.5.1) with no data. + pub fn set_watchdog_timer(&self, seconds: usize, code: u64) -> Result<(), Status> { + // SAFETY: no data, so no pointer is read. + unsafe { (self.set_watchdog_timer)(seconds, code, 0, ptr::null()) }.ok() + } + + /// Every handle carrying `P` (`LocateHandleBuffer`, §7.3.15). + pub fn handles(&self) -> Result, Status> { + let (mut count, mut at) = (0usize, ptr::null_mut()); + // SAFETY: both out parameters are live. + unsafe { (self.locate_handle_buffer)(BY_PROTOCOL, &P::GUID, ptr::null(), &mut count, &mut at) }.ok()?; + let at = NonNull::new(at).expect("LocateHandleBuffer answered success with no buffer"); + Ok(Handles { bs: self, at, count }) + } + + /// The first handle carrying `P`. + pub fn handle_for(&self) -> Result { + self.handles::

()?.first().copied().ok_or(Status::NOT_FOUND) + } + + /// `P` on `handle`, GET_PROTOCOL: it stops no driver. + /// + /// Nothing between the open and the drop can uninstall the protocol: the + /// loader is the one image running, it registers no event callback, and it + /// calls no boot service that connects or disconnects a controller. + pub fn get(&self, handle: Handle) -> Result, Status> { + self.open(handle, GET_PROTOCOL) + } + + /// `P` on `handle`, EXCLUSIVE. + pub fn exclusive(&self, handle: Handle) -> Result, Status> { + self.open(handle, EXCLUSIVE) + } + + fn open(&self, handle: Handle, attributes: u32) -> Result, Status> { + let mut interface = ptr::null_mut(); + // SAFETY: the out parameter is live, and the agent is this image. + unsafe { + (self.open_protocol)( + handle.as_ptr(), + &P::GUID, + &mut interface, + super::image().as_ptr(), + ptr::null_mut(), + attributes, + ) + } + .ok()?; + let interface = NonNull::new(interface.cast::

()) + .unwrap_or_else(|| panic!("OpenProtocol({}) answered success with no interface", P::GUID)); + Ok(Scoped { bs: self, handle, interface }) + } +} + diff --git a/bootloader/src/efi/mod.rs b/bootloader/src/efi/mod.rs new file mode 100644 index 00000000000..39b9ec9db6c --- /dev/null +++ b/bootloader/src/efi/mod.rs @@ -0,0 +1,546 @@ +//! The UEFI this loader calls, and nothing it does not: UEFI 2.10's tables, +//! protocols, GUIDs and status codes, each laid out from the section cited at +//! it and held there by compile-time `size_of`/`offset_of!` asserts, so a field +//! added, dropped or resized fails the build of both targets. +//! +//! Firmware is the loader's host and is trusted to keep its own contracts; a +//! length it writes back about a buffer of this side's is still held to that +//! buffer. The safe wrappers each remove a hazard that is this side's: a +//! name handed over without its NUL, a protocol left open or opened in a way +//! that stops a driver, a boot service called once `ExitBootServices` has run. +//! +//! **Boot services end at [`SystemTable::exit_boot_services`]**, which consumes +//! the table; the console and the allocator, which reach firmware without it, +//! refuse from then on. + +mod boot; +mod proto; +mod runtime; + +use core::ffi::c_void; +use core::fmt; +use core::ptr::{self, NonNull}; +use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; + +use alloc::vec::Vec; + +pub use boot::{AllocateType, BootServices, MemoryDescriptor, MemoryMap, Scoped, PAGE_SIZE}; +pub use proto::{ + BlockIo, DevicePath, File, Gop, HardDrive, LoadedImage, Mode, PartitionInfo, PciRootBridgeIo, + PixelFormat, Rng, SimpleFileSystem, +}; +pub use runtime::{ResetType, RuntimeServices, Time, VariableAttributes, GLOBAL_VARIABLE}; + +/// `EFI_STATUS` (UEFI 2.10 Appendix D): success, a warning, or an error, +/// which carries the top bit. +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +#[must_use] +pub struct Status(pub usize); + +const ERROR: usize = 1 << (usize::BITS - 1); + +/// Appendix D's codes by name: every refusal this loader writes carries one. +const NAMES: [(usize, &str); 41] = [ + (0, "SUCCESS"), + (1, "WARN_UNKNOWN_GLYPH"), + (2, "WARN_DELETE_FAILURE"), + (3, "WARN_WRITE_FAILURE"), + (4, "WARN_BUFFER_TOO_SMALL"), + (5, "WARN_STALE_DATA"), + (6, "WARN_FILE_SYSTEM"), + (7, "WARN_RESET_REQUIRED"), + (ERROR | 1, "LOAD_ERROR"), + (ERROR | 2, "INVALID_PARAMETER"), + (ERROR | 3, "UNSUPPORTED"), + (ERROR | 4, "BAD_BUFFER_SIZE"), + (ERROR | 5, "BUFFER_TOO_SMALL"), + (ERROR | 6, "NOT_READY"), + (ERROR | 7, "DEVICE_ERROR"), + (ERROR | 8, "WRITE_PROTECTED"), + (ERROR | 9, "OUT_OF_RESOURCES"), + (ERROR | 10, "VOLUME_CORRUPTED"), + (ERROR | 11, "VOLUME_FULL"), + (ERROR | 12, "NO_MEDIA"), + (ERROR | 13, "MEDIA_CHANGED"), + (ERROR | 14, "NOT_FOUND"), + (ERROR | 15, "ACCESS_DENIED"), + (ERROR | 16, "NO_RESPONSE"), + (ERROR | 17, "NO_MAPPING"), + (ERROR | 18, "TIMEOUT"), + (ERROR | 19, "NOT_STARTED"), + (ERROR | 20, "ALREADY_STARTED"), + (ERROR | 21, "ABORTED"), + (ERROR | 22, "ICMP_ERROR"), + (ERROR | 23, "TFTP_ERROR"), + (ERROR | 24, "PROTOCOL_ERROR"), + (ERROR | 25, "INCOMPATIBLE_VERSION"), + (ERROR | 26, "SECURITY_VIOLATION"), + (ERROR | 27, "CRC_ERROR"), + (ERROR | 28, "END_OF_MEDIA"), + (ERROR | 31, "END_OF_FILE"), + (ERROR | 32, "INVALID_LANGUAGE"), + (ERROR | 33, "COMPROMISED_DATA"), + (ERROR | 34, "IP_ADDRESS_CONFLICT"), + (ERROR | 35, "HTTP_ERROR"), +]; + +impl Status { + pub const SUCCESS: Status = Status(0); + pub const BUFFER_TOO_SMALL: Status = Status(ERROR | 5); + pub const NOT_FOUND: Status = Status(ERROR | 14); + pub const ABORTED: Status = Status(ERROR | 21); + + pub fn is_success(self) -> bool { + self == Self::SUCCESS + } + + /// `Ok` for `SUCCESS` alone: a warning is not what was asked for, and is + /// refused like an error. + pub fn ok(self) -> Result<(), Status> { + if self.is_success() { Ok(()) } else { Err(self) } + } +} + +impl fmt::Debug for Status { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match NAMES.iter().find(|(code, _)| *code == self.0) { + Some((_, name)) => f.write_str(name), + None => write!(f, "Status({:#x})", self.0), + } + } +} + +impl fmt::Display for Status { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + +/// `EFI_GUID` (UEFI 2.10 Appendix A): its three leading fields little-endian, +/// as it is held in memory and on a GPT disk. +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(C, align(4))] +pub struct Guid([u8; 16]); + +const _: () = assert!(size_of::() == 16); + +impl Guid { + /// The GUID the specification prints as `{a, b, c, {d...}}`. + pub const fn new(a: u32, b: u16, c: u16, d: [u8; 8]) -> Self { + let (a, b, c) = (a.to_le_bytes(), b.to_le_bytes(), c.to_le_bytes()); + Guid([a[0], a[1], a[2], a[3], b[0], b[1], c[0], c[1], d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7]]) + } +} + +impl fmt::Display for Guid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let b = &self.0; + write!( + f, + "{:08x}-{:04x}-{:04x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + u32::from_le_bytes([b[0], b[1], b[2], b[3]]), + u16::from_le_bytes([b[4], b[5]]), + u16::from_le_bytes([b[6], b[7]]), + b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15], + ) + } +} + +/// `EFI_HANDLE` (UEFI 2.10 §2.3.1): opaque, and never null where firmware +/// hands one back. +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +pub struct Handle(NonNull); + +impl Handle { + fn from_ptr(ptr: *mut c_void) -> Option { + NonNull::new(ptr).map(Handle) + } + + fn as_ptr(self) -> *mut c_void { + self.0.as_ptr() + } +} + +/// A NUL-terminated `CHAR16` string (UEFI 2.10 §2.3.1), the only form a name +/// crosses to firmware in: the type is what keeps firmware from reading past +/// one handed over without its terminator. +#[repr(transparent)] +pub struct CStr16([u16]); + +impl CStr16 { + /// `units` as a name, where its last unit is its only NUL. + pub const fn from_units(units: &[u16]) -> Option<&CStr16> { + let Some((last, body)) = units.split_last() else { return None }; + if *last != 0 { + return None; + } + let mut i = 0; + while i < body.len() { + if body[i] == 0 { + return None; + } + i += 1; + } + // SAFETY: `CStr16` is `repr(transparent)` over `[u16]`. + Some(unsafe { &*(units as *const [u16] as *const CStr16) }) + } + + fn as_ptr(&self) -> *const u16 { + self.0.as_ptr() + } + + /// The units before the NUL. + pub fn units(&self) -> &[u16] { + &self.0[..self.0.len() - 1] + } +} + +impl fmt::Display for CStr16 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for unit in self.units() { + fmt::Write::write_char(f, char::from_u32(u32::from(*unit)).unwrap_or(char::REPLACEMENT_CHARACTER))?; + } + Ok(()) + } +} + +/// An owned [`CStr16`]. +pub struct CString16(Vec); + +impl CString16 { + /// `text` as a name, or `None` where it holds a NUL or a character past + /// UCS-2, which `CHAR16` cannot carry. + pub fn new(text: &str) -> Option { + let mut units = Vec::with_capacity(text.len() + 1); + for ch in text.chars() { + let unit = u16::try_from(u32::from(ch)).ok().filter(|&u| u != 0)?; + units.push(unit); + } + units.push(0); + Some(CString16(units)) + } +} + +impl core::ops::Deref for CString16 { + type Target = CStr16; + fn deref(&self) -> &CStr16 { + CStr16::from_units(&self.0).expect("a CString16 ends in its only NUL") + } +} + +impl fmt::Display for CString16 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&**self, f) + } +} + +/// `text`, ASCII, as `CHAR16` units with the NUL: [`cstr16!`]'s body. +pub const fn ucs2(text: &str) -> [u16; N] { + let bytes = text.as_bytes(); + assert!(bytes.len() + 1 == N, "the array is the text and its NUL"); + let mut units = [0u16; N]; + let mut i = 0; + while i < bytes.len() { + assert!(bytes[i] != 0 && bytes[i] < 0x80, "a literal name is ASCII with no NUL"); + units[i] = bytes[i] as u16; + i += 1; + } + units +} + +/// A literal name as a `&'static CStr16`, checked at compile time. +macro_rules! cstr16 { + ($text:literal) => {{ + const UNITS: [u16; $text.len() + 1] = $crate::efi::ucs2($text); + const NAME: &$crate::efi::CStr16 = match $crate::efi::CStr16::from_units(&UNITS) { + Some(name) => name, + None => panic!("a literal name is its text and one NUL"), + }; + NAME + }}; +} +pub(crate) use cstr16; + +/// `EFI_TABLE_HEADER` (UEFI 2.10 §4.2). +#[repr(C)] +struct TableHeader { + signature: u64, + revision: u32, + header_size: u32, + crc32: u32, + reserved: u32, +} + +const _: () = assert!(size_of::() == 24); + +/// `EFI_SYSTEM_TABLE` (UEFI 2.10 §4.3). +#[repr(C)] +struct RawSystemTable { + hdr: TableHeader, + firmware_vendor: *const u16, + firmware_revision: u32, + console_in_handle: *mut c_void, + con_in: *mut c_void, + console_out_handle: *mut c_void, + con_out: *mut proto::TextOutput, + standard_error_handle: *mut c_void, + std_err: *mut c_void, + runtime_services: *const RuntimeServices, + boot_services: *const BootServices, + number_of_table_entries: usize, + configuration_table: *const ConfigurationTable, +} + +const _: () = { + assert!(size_of::() == 120); + assert!(core::mem::offset_of!(RawSystemTable, firmware_revision) == 32); + assert!(core::mem::offset_of!(RawSystemTable, con_out) == 64); + assert!(core::mem::offset_of!(RawSystemTable, runtime_services) == 88); + assert!(core::mem::offset_of!(RawSystemTable, boot_services) == 96); + assert!(core::mem::offset_of!(RawSystemTable, number_of_table_entries) == 104); + assert!(core::mem::offset_of!(RawSystemTable, configuration_table) == 112); +}; + +/// `EFI_CONFIGURATION_TABLE` (UEFI 2.10 §4.6). +#[repr(C)] +pub struct ConfigurationTable { + pub guid: Guid, + pub address: *const c_void, +} + +const _: () = assert!(size_of::() == 24); +const _: () = assert!(core::mem::offset_of!(ConfigurationTable, address) == 16); + +/// `EFI_ACPI_20_TABLE_GUID` (UEFI 2.10 §4.6.1). +pub const ACPI2_GUID: Guid = Guid::new(0x8868e871, 0xe4f1, 0x11d3, [0xbc, 0x22, 0x00, 0x80, 0xc7, 0x3c, 0x88, 0x81]); + +/// The system table firmware handed `efi_main`, while boot services live: +/// null before the entry and from [`SystemTable::exit_boot_services`] on. The +/// console and the allocator read it; nothing else does. +static SYSTEM: AtomicPtr = AtomicPtr::new(ptr::null_mut()); + +/// This image's handle, the agent every protocol open names. +static IMAGE: AtomicPtr = AtomicPtr::new(ptr::null_mut()); + +/// The system table while boot services live: made once, by the entry, and +/// consumed by [`SystemTable::exit_boot_services`]. +pub struct SystemTable { + raw: &'static RawSystemTable, +} + +impl SystemTable { + pub fn boot_services(&self) -> &BootServices { + // SAFETY: firmware's table names its boot services for as long as they + // live, which is as long as `self`. + unsafe { &*self.raw.boot_services } + } + + pub fn runtime_services(&self) -> &RuntimeServices { + // SAFETY: as `boot_services`; runtime services outlive them. + unsafe { &*self.raw.runtime_services } + } + + pub fn config_table(&self) -> &[ConfigurationTable] { + // SAFETY: firmware's table names this many entries at this address. + unsafe { core::slice::from_raw_parts(self.raw.configuration_table, self.raw.number_of_table_entries) } + } + + /// `ClearScreen` (UEFI 2.10 §12.4.8), which also homes the cursor. + pub fn clear_screen(&self) -> Result<(), Status> { + let out = self.raw.con_out; + // SAFETY: `ConOut` is firmware's console for as long as boot services live. + unsafe { ((*out).clear_screen)(out) }.ok() + } + + /// `ExitBootServices` (UEFI 2.10 §7.4.6) on the map it is handed, taken + /// into pool memory it never frees; tried twice, as a map key gone stale + /// between the two calls is answered by asking again, and a machine that + /// refuses twice is reset. The console and the allocator refuse from here. + pub fn exit_boot_services(self) -> MemoryMap<'static> { + let bs = self.boot_services(); + let reset = |status: Status| -> ! { self.runtime_services().reset(ResetType::COLD, status) }; + // Eight descriptors past the measured map, for the ones the pool + // allocation below adds. + let (map_size, entry_size) = bs.memory_map_size(); + let Some(bytes) = entry_size.checked_mul(8).and_then(|extra| map_size.checked_add(extra)) else { + reset(Status::ABORTED) + }; + let words = bytes.div_ceil(size_of::()); + let buffer = match bs.allocate_pool(words * size_of::()) { + // SAFETY: the pool gives 8-byte-aligned memory (§7.2.4) of the + // size asked, never freed: it is this map's for good. + Ok(at) => unsafe { core::slice::from_raw_parts_mut(at.cast::(), words) }, + Err(status) => reset(status), + }; + let image = image(); + let mut status = Status::ABORTED; + for _ in 0..2 { + let filled = match bs.fill_memory_map(buffer) { + Ok(filled) => filled, + Err(why) => { + status = why; + continue; + } + }; + status = bs.exit(image, filled.key); + if status.is_success() { + SYSTEM.store(ptr::null_mut(), Ordering::Release); + return MemoryMap::new(buffer, filled); + } + } + reset(status) + } +} + +fn image() -> Handle { + Handle::from_ptr(IMAGE.load(Ordering::Acquire)).expect("the entry stored this image's handle") +} + +/// The system table while boot services live, for the two readers that run +/// without one in hand. +fn live() -> Option<&'static RawSystemTable> { + // SAFETY: `SYSTEM` is null or the table firmware handed the entry, whose + // boot services are live until `exit_boot_services` nulls it. + unsafe { SYSTEM.load(Ordering::Acquire).as_ref() } +} + +/// The image's entry (UEFI 2.10 §4.1, `EFI_IMAGE_ENTRY_POINT`), under the name +/// the UEFI targets link as the PE entry point. +#[unsafe(no_mangle)] +extern "efiapi" fn efi_main(image: *mut c_void, system_table: *mut RawSystemTable) -> Status { + // SAFETY: firmware hands its system table and this image's handle, live + // until `ExitBootServices`. + let raw = unsafe { system_table.as_ref() }.expect("firmware hands the entry its system table"); + let image = Handle::from_ptr(image).expect("firmware hands the entry this image's handle"); + IMAGE.store(image.as_ptr(), Ordering::Release); + SYSTEM.store(system_table, Ordering::Release); + crate::main(image, SystemTable { raw }) +} + +/// One line to firmware's console, `\n` as `\r\n`, in `CHAR16` chunks; a +/// character past UCS-2 is written as U+FFFD. +/// +/// # Panics +/// Once boot services are gone, and where the console refuses the line. +pub fn print(args: fmt::Arguments) { + let raw = live().expect("the console is gone with boot services"); + let mut out = Console { out: raw.con_out, units: [0; Console::CHUNK + 1], len: 0 }; + let written = fmt::write(&mut out, args).and_then(|()| out.flush()); + assert!(written.is_ok(), "firmware's console refused a line"); +} + +struct Console { + out: *mut proto::TextOutput, + units: [u16; Console::CHUNK + 1], + len: usize, +} + +impl Console { + const CHUNK: usize = 128; + + fn push(&mut self, unit: u16) -> fmt::Result { + self.units[self.len] = unit; + self.len += 1; + if self.len == Self::CHUNK { self.flush() } else { Ok(()) } + } + + fn flush(&mut self) -> fmt::Result { + if self.len == 0 { + return Ok(()); + } + self.units[self.len] = 0; + self.len = 0; + // SAFETY: `ConOut` is live while `SYSTEM` is set, and `units` is + // NUL-terminated at the count just written. + let status = unsafe { ((*self.out).output_string)(self.out, self.units.as_ptr()) }; + status.ok().map_err(|_| fmt::Error) + } +} + +impl fmt::Write for Console { + fn write_str(&mut self, s: &str) -> fmt::Result { + for ch in s.chars() { + if ch == '\n' { + self.push(u16::from(b'\r'))?; + } + self.push(u16::try_from(u32::from(ch)).unwrap_or(0xFFFD))?; + } + Ok(()) + } +} + +/// Rust's heap, in `EfiLoaderData` pool memory (UEFI 2.10 §7.2.4: a pool +/// allocation is 8-byte aligned; an application's data is `EfiLoaderData`, +/// §7.4.1), and null once boot services are gone. A wider alignment is cut +/// out of a larger allocation, the allocation's own address kept in the word +/// before it. +struct Pool; + +#[global_allocator] +static POOL: Pool = Pool; + +/// What `AllocatePool` aligns to. +const POOL_ALIGN: usize = 8; + +// SAFETY: every pointer handed out is pool memory of at least the layout's +// size at its alignment, and is given back only through `dealloc`. +unsafe impl core::alloc::GlobalAlloc for Pool { + unsafe fn alloc(&self, layout: core::alloc::Layout) -> *mut u8 { + let Some(raw) = live() else { return ptr::null_mut() }; + // SAFETY: live boot services. + let bs = unsafe { &*raw.boot_services }; + if layout.align() <= POOL_ALIGN { + return bs.allocate_pool(layout.size()).unwrap_or(ptr::null_mut()); + } + let Some(size) = layout.size().checked_add(layout.align()) else { return ptr::null_mut() }; + let Ok(whole) = bs.allocate_pool(size) else { return ptr::null_mut() }; + // At least one word ahead of the aligned address, for the allocation's own. + let offset = match whole.align_offset(layout.align()) { + 0 => layout.align(), + n => n, + }; + // SAFETY: `offset <= align`, inside the `size + align` bytes; the + // word before the aligned address is inside them too, as + // `offset >= POOL_ALIGN`, and aligned, as both addresses are. + unsafe { + let aligned = whole.add(offset); + aligned.cast::<*mut u8>().sub(1).write(whole); + aligned + } + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: core::alloc::Layout) { + let raw = live().expect("nothing is freed once boot services are gone"); + let whole = if layout.align() <= POOL_ALIGN { + ptr + } else { + // SAFETY: `alloc` wrote the allocation's address in the word before. + unsafe { ptr.cast::<*mut u8>().sub(1).read() } + }; + // SAFETY: live boot services, and `whole` is a pool allocation of theirs. + let freed = unsafe { (*raw.boot_services).free_pool(whole) }; + assert!(freed.is_ok(), "firmware would not take back pool memory it gave"); + } +} + +/// Set by the first panic, so a panic while saying one goes straight to the +/// power-off. +static PANICKED: AtomicBool = AtomicBool::new(false); + +/// Say the panic on firmware's console and power the machine off; once boot +/// services are gone neither is there, and the CPU spins. +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + if let Some(raw) = live() { + if !PANICKED.swap(true, Ordering::Relaxed) { + print(format_args!("[PANIC]: {info}\n")); + } + // SAFETY: runtime services live as long as the table. + unsafe { &*raw.runtime_services }.reset(ResetType::SHUTDOWN, Status::ABORTED) + } + loop { + core::hint::spin_loop(); + } +} diff --git a/bootloader/src/efi/proto.rs b/bootloader/src/efi/proto.rs new file mode 100644 index 00000000000..d60c848a73b --- /dev/null +++ b/bootloader/src/efi/proto.rs @@ -0,0 +1,661 @@ +//! The protocols this loader opens, each in its spec section's own field +//! order, and the console it writes through. + +use core::ffi::c_void; +use core::mem::offset_of; +use core::ptr::{self, NonNull}; + +use alloc::vec; + +use super::boot::{Exclusive, Protocol}; +use super::{CStr16, Guid, Handle, Status}; + +type Unused = *const c_void; + +/// `EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL` (UEFI 2.10 §12.4.1), firmware's console: +/// reached through the system table's `ConOut`, never opened. +#[repr(C)] +pub(super) struct TextOutput { + reset: Unused, + pub(super) output_string: unsafe extern "efiapi" fn(this: *mut TextOutput, string: *const u16) -> Status, + test_string: Unused, + query_mode: Unused, + set_mode: Unused, + set_attribute: Unused, + pub(super) clear_screen: unsafe extern "efiapi" fn(this: *mut TextOutput) -> Status, + set_cursor_position: Unused, + enable_cursor: Unused, + mode: Unused, +} + +const _: () = { + assert!(size_of::() == 80); + assert!(offset_of!(TextOutput, output_string) == 8); + assert!(offset_of!(TextOutput, clear_screen) == 48); +}; + +/// `EFI_LOADED_IMAGE_PROTOCOL` (UEFI 2.10 §9.1.1). +#[repr(C)] +pub struct LoadedImage { + revision: u32, + parent_handle: *mut c_void, + system_table: Unused, + device_handle: *mut c_void, + file_path: Unused, + reserved: Unused, + load_options_size: u32, + load_options: Unused, + image_base: *const c_void, + image_size: u64, + image_code_type: u32, + image_data_type: u32, + unload: Unused, +} + +const _: () = { + assert!(size_of::() == 96); + assert!(offset_of!(LoadedImage, device_handle) == 24); + assert!(offset_of!(LoadedImage, load_options_size) == 48); + assert!(offset_of!(LoadedImage, image_base) == 64); + assert!(offset_of!(LoadedImage, image_size) == 72); + assert!(offset_of!(LoadedImage, image_data_type) == 84); +}; + +// SAFETY: §9.1.1's layout, under its GUID. +unsafe impl Protocol for LoadedImage { + const GUID: Guid = Guid::new(0x5b1b31a1, 0x9562, 0x11d2, [0x8e, 0x3f, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b]); +} +impl Exclusive for LoadedImage {} + +impl LoadedImage { + /// The handle of the device firmware loaded this image from, if it says. + pub fn device(&self) -> Option { + Handle::from_ptr(self.device_handle) + } + + /// Where firmware loaded the image, and its size in bytes. + pub fn info(&self) -> (*const c_void, u64) { + (self.image_base, self.image_size) + } +} + +/// `EFI_DEVICE_PATH_PROTOCOL` (UEFI 2.10 §10.2): the first node's header, +/// which the rest of the path follows. +#[repr(C)] +pub struct DevicePath { + ty: u8, + sub_type: u8, + length: [u8; 2], +} + +const _: () = assert!(size_of::() == 4); + +// SAFETY: §10.2's layout, under its GUID. +unsafe impl Protocol for DevicePath { + const GUID: Guid = Guid::new(0x09576e91, 0x6d3f, 0x11d2, [0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b]); +} +impl Exclusive for DevicePath {} + +/// A device path node's header: type, subtype, and a length that counts it. +const NODE_HEADER: usize = 4; + +/// End of Entire Device Path (§10.3.1). +const END_ENTIRE: (u8, u8) = (0x7f, 0xff); + +impl DevicePath { + /// Each node, whole, up to the End of Entire node. + /// + /// # Panics + /// Where a node's length does not cover its own header: the walk could + /// not step past it. + pub fn nodes(&self) -> impl Iterator { + let mut at = ptr::from_ref(self).cast::(); + core::iter::from_fn(move || { + // SAFETY: firmware's path is a run of nodes ending in End of + // Entire, and `at` is the start of one of them. + let header = unsafe { core::slice::from_raw_parts(at, NODE_HEADER) }; + if (header[0], header[1]) == END_ENTIRE { + return None; + } + let len = usize::from(u16::from_le_bytes([header[2], header[3]])); + assert!(len >= NODE_HEADER, "firmware's device path has a {len}-byte node"); + // SAFETY: the node's own length, which counts its header. + let node = unsafe { core::slice::from_raw_parts(at, len) }; + // SAFETY: the next node follows this one. + at = unsafe { at.add(len) }; + Some(node) + }) + } +} + +/// The MEDIA/HARDDRIVE node (§10.3.5.1). +pub struct HardDrive { + pub start: u64, + pub size: u64, + pub signature: [u8; 16], + /// `MBRType`: 2 for a GPT partition. + pub format: u8, + /// `SignatureType`: 2 for a GUID signature. + pub signature_type: u8, +} + +impl HardDrive { + pub const TYPE: (u8, u8) = (4, 1); + /// `MBRType` of a GPT partition. + pub const GPT: u8 = 2; + /// `SignatureType` of a GUID signature. + pub const GUID_SIGNATURE: u8 = 2; + const LEN: usize = 42; + + /// `node`, where it is a HARDDRIVE node of the length §10.3.5.1 gives it. + pub fn parse(node: &[u8]) -> Option { + if node.len() != Self::LEN || (node[0], node[1]) != Self::TYPE { + return None; + } + let u64_at = |at: usize| u64::from_le_bytes(node[at..at + 8].try_into().expect("eight bytes")); + Some(HardDrive { + start: u64_at(8), + size: u64_at(16), + signature: node[24..40].try_into().expect("sixteen bytes"), + format: node[40], + signature_type: node[41], + }) + } +} + +/// `EFI_SIMPLE_FILE_SYSTEM_PROTOCOL` (UEFI 2.10 §13.4.1). +#[repr(C)] +pub struct SimpleFileSystem { + revision: u64, + open_volume: unsafe extern "efiapi" fn(this: *mut SimpleFileSystem, root: *mut *mut FileProtocol) -> Status, +} + +const _: () = assert!(size_of::() == 16); + +// SAFETY: §13.4.1's layout, under its GUID. +unsafe impl Protocol for SimpleFileSystem { + const GUID: Guid = Guid::new(0x964e5b22, 0x6459, 0x11d2, [0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b]); +} +impl Exclusive for SimpleFileSystem {} + +impl super::Scoped<'_, SimpleFileSystem> { + /// `OpenVolume` (§13.4.2): the volume's root directory. + pub fn open_volume(&mut self) -> Result { + let mut root = ptr::null_mut(); + // SAFETY: the open interface, and a live out parameter. + unsafe { (self.open_volume)(self.this(), &mut root) }.ok()?; + Ok(File(NonNull::new(root).expect("OpenVolume answered success with no root"))) + } +} + +/// `EFI_FILE_PROTOCOL` (UEFI 2.10 §13.5.1), through `Flush`: the revision 2 +/// functions after it are never called. +#[repr(C)] +pub struct FileProtocol { + revision: u64, + open: unsafe extern "efiapi" fn( + this: *mut FileProtocol, + new: *mut *mut FileProtocol, + name: *const u16, + mode: u64, + attributes: u64, + ) -> Status, + close: unsafe extern "efiapi" fn(this: *mut FileProtocol) -> Status, + delete: unsafe extern "efiapi" fn(this: *mut FileProtocol) -> Status, + read: unsafe extern "efiapi" fn(this: *mut FileProtocol, size: *mut usize, buffer: *mut u8) -> Status, + write: unsafe extern "efiapi" fn(this: *mut FileProtocol, size: *mut usize, buffer: *const u8) -> Status, + get_position: Unused, + set_position: unsafe extern "efiapi" fn(this: *mut FileProtocol, position: u64) -> Status, + get_info: unsafe extern "efiapi" fn( + this: *mut FileProtocol, + kind: *const Guid, + size: *mut usize, + buffer: *mut u8, + ) -> Status, + set_info: Unused, + flush: unsafe extern "efiapi" fn(this: *mut FileProtocol) -> Status, +} + +const _: () = { + assert!(size_of::() == 88); + assert!(offset_of!(FileProtocol, open) == 8); + assert!(offset_of!(FileProtocol, close) == 16); + assert!(offset_of!(FileProtocol, delete) == 24); + assert!(offset_of!(FileProtocol, read) == 32); + assert!(offset_of!(FileProtocol, write) == 40); + assert!(offset_of!(FileProtocol, set_position) == 56); + assert!(offset_of!(FileProtocol, get_info) == 64); + assert!(offset_of!(FileProtocol, flush) == 80); +}; + +/// `Open`'s modes (§13.5.2). +#[derive(Clone, Copy)] +pub enum Mode { + Read, + ReadWrite, + CreateReadWrite, +} + +impl Mode { + fn bits(self) -> u64 { + const READ: u64 = 0x1; + const WRITE: u64 = 0x2; + const CREATE: u64 = 0x8000_0000_0000_0000; + match self { + Mode::Read => READ, + Mode::ReadWrite => READ | WRITE, + Mode::CreateReadWrite => CREATE | READ | WRITE, + } + } +} + +/// An open file or directory, closed when this drops. +pub struct File(NonNull); + +/// `EFI_FILE_INFO` (§13.5.16), up to the name this loader never reads. +#[repr(C)] +struct RawFileInfo { + size: u64, + file_size: u64, + physical_size: u64, + create_time: super::Time, + last_access_time: super::Time, + modification_time: super::Time, + attribute: u64, +} + +const _: () = { + assert!(size_of::() == 80); + assert!(offset_of!(RawFileInfo, file_size) == 8); + assert!(offset_of!(RawFileInfo, attribute) == 72); +}; + +/// `EFI_FILE_INFO_ID` (§13.5.16). +const FILE_INFO: Guid = Guid::new(0x09576e92, 0x6d3f, 0x11d2, [0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b]); + +/// `EFI_FILE_DIRECTORY` (§13.5.16). +const DIRECTORY: u64 = 0x10; + +/// What this loader reads of a file's `EFI_FILE_INFO`. +pub struct FileInfo { + pub file_size: u64, + pub directory: bool, +} + +impl File { + fn call(&mut self, f: impl FnOnce(&FileProtocol, *mut FileProtocol) -> R) -> R { + // SAFETY: an open handle's interface, live until it is closed. + f(unsafe { self.0.as_ref() }, self.0.as_ptr()) + } + + /// `Open` (§13.5.2), relative to this directory. + pub fn open(&mut self, name: &CStr16, mode: Mode) -> Result { + let mut new = ptr::null_mut(); + // SAFETY: a NUL-terminated name and a live out parameter. + self.call(|f, this| unsafe { (f.open)(this, &mut new, name.as_ptr(), mode.bits(), 0) }).ok()?; + Ok(File(NonNull::new(new).expect("Open answered success with no handle"))) + } + + /// `Delete` (§13.5.4), which closes the handle whatever it answers. + pub fn delete(mut self) -> Result<(), Status> { + // SAFETY: the handle, which `Delete` closes, so `Drop` must not. + let status = self.call(|f, this| unsafe { (f.delete)(this) }); + #[expect(clippy::disallowed_methods, reason = "`Delete` closed the handle `Drop` would close again")] + core::mem::forget(self); + status.ok() + } + + /// `Read` (§13.5.5): the bytes read, at most `buffer`'s length. + pub fn read(&mut self, buffer: &mut [u8]) -> Result { + let mut size = buffer.len(); + // SAFETY: `buffer` is `size` writable bytes. + self.call(|f, this| unsafe { (f.read)(this, &mut size, buffer.as_mut_ptr()) }).ok()?; + assert!(size <= buffer.len(), "Read answered {size} bytes into {}", buffer.len()); + Ok(size) + } + + /// `Write` (§13.5.6): on a refusal, the status and the bytes written + /// before it. + pub fn write(&mut self, buffer: &[u8]) -> Result<(), (Status, usize)> { + let mut size = buffer.len(); + // SAFETY: `buffer` is `size` readable bytes. + self.call(|f, this| unsafe { (f.write)(this, &mut size, buffer.as_ptr()) }).ok().map_err(|status| (status, size)) + } + + /// `SetPosition` (§13.5.8); `u64::MAX` is the end of the file. + pub fn set_position(&mut self, position: u64) -> Result<(), Status> { + // SAFETY: a plain call on the handle. + self.call(|f, this| unsafe { (f.set_position)(this, position) }).ok() + } + + /// `Flush` (§13.5.11). + pub fn flush(&mut self) -> Result<(), Status> { + // SAFETY: a plain call on the handle. + self.call(|f, this| unsafe { (f.flush)(this) }).ok() + } + + /// `GetInfo` (§13.5.12) of `EFI_FILE_INFO`, sized by asking first. + pub fn info(&mut self) -> Result { + let mut size = 0usize; + // SAFETY: a zero size with no buffer asks for the size. + let status = self.call(|f, this| unsafe { (f.get_info)(this, &FILE_INFO, &mut size, ptr::null_mut()) }); + if status != Status::BUFFER_TOO_SMALL { + return Err(if status.is_success() { Status::BUFFER_TOO_SMALL } else { status }); + } + assert!(size >= size_of::(), "GetInfo wants {size} bytes for an EFI_FILE_INFO"); + let mut words = vec![0u64; size.div_ceil(size_of::())]; + // SAFETY: `words` is at least `size` writable bytes, aligned for the info. + self.call(|f, this| unsafe { (f.get_info)(this, &FILE_INFO, &mut size, words.as_mut_ptr().cast()) }).ok()?; + // SAFETY: firmware wrote an `EFI_FILE_INFO`, whose head is this. + let info = unsafe { words.as_ptr().cast::().read() }; + Ok(FileInfo { file_size: info.file_size, directory: info.attribute & DIRECTORY != 0 }) + } + + /// This handle, where it is a file and not a directory; `None` for a + /// directory, and for a handle that would not say which it is. + pub fn into_regular_file(mut self) -> Option { + match self.info() { + Ok(FileInfo { directory: false, .. }) => Some(self), + _ => None, + } + } +} + +impl Drop for File { + fn drop(&mut self) { + // SAFETY: the handle, closed once. `Close` always succeeds (§13.5.3). + let _ = self.call(|f, this| unsafe { (f.close)(this) }); + } +} + +/// `EFI_PARTITION_INFO_PROTOCOL` (UEFI 2.10 §13.18), packed as the spec +/// declares it; the record is MBR or GPT by `kind`, read as bytes. +#[repr(C, packed)] +pub struct PartitionInfo { + revision: u32, + kind: u32, + system: u8, + reserved: [u8; 7], + record: [u8; 128], +} + +const _: () = { + assert!(size_of::() == 144); + assert!(offset_of!(PartitionInfo, kind) == 4); + assert!(offset_of!(PartitionInfo, system) == 8); + assert!(offset_of!(PartitionInfo, record) == 16); +}; + +// SAFETY: §13.18's layout, under its GUID. +unsafe impl Protocol for PartitionInfo { + const GUID: Guid = Guid::new(0x8cf2f62c, 0xbc9b, 0x4821, [0x80, 0x8d, 0xec, 0x9e, 0xc4, 0x21, 0xa1, 0xa0]); +} + +impl PartitionInfo { + /// `EFI_PARTITION_INFO_PROTOCOL_REVISION`, which the spec spells `0x0001000`. + const REVISION: u32 = 0x1000; + /// `PARTITION_TYPE_GPT`. + const GPT: u32 = 0x02; + + /// The unique partition GUID of the GPT entry this carries (UEFI 2.10 + /// §5.3.3, bytes 16 to 32 of the entry), or `None` for any other record. + pub fn gpt_unique_guid(&self) -> Option<[u8; 16]> { + let (revision, kind) = (self.revision, self.kind); + if revision != Self::REVISION || kind != Self::GPT { + return None; + } + let record = self.record; + Some(record[16..32].try_into().expect("sixteen bytes")) + } +} + +/// `EFI_BLOCK_IO_PROTOCOL` (UEFI 2.10 §13.9.1). +#[repr(C)] +pub struct BlockIo { + pub revision: u64, + media: *const BlockIoMedia, + reset: Unused, + read_blocks: unsafe extern "efiapi" fn( + this: *mut BlockIo, + media_id: u32, + lba: u64, + size: usize, + buffer: *mut u8, + ) -> Status, + write_blocks: Unused, + flush_blocks: Unused, +} + +const _: () = { + assert!(size_of::() == 48); + assert!(offset_of!(BlockIo, media) == 8); + assert!(offset_of!(BlockIo, read_blocks) == 24); +}; + +// SAFETY: §13.9.1's layout, under its GUID. +unsafe impl Protocol for BlockIo { + const GUID: Guid = Guid::new(0x964e5b21, 0x6459, 0x11d2, [0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b]); +} + +/// `EFI_BLOCK_IO_MEDIA` (§13.9.1); `optimal_transfer_length_granularity` is +/// there only from `EFI_BLOCK_IO_PROTOCOL_REVISION3` on. +#[derive(Clone, Copy)] +#[repr(C)] +pub struct BlockIoMedia { + pub media_id: u32, + removable_media: u8, + media_present: u8, + logical_partition: u8, + read_only: u8, + write_caching: u8, + pub block_size: u32, + pub io_align: u32, + pub last_block: u64, +} + +/// The revision 2 and 3 fields after `LastBlock`. +#[repr(C)] +struct BlockIoMediaRevision3 { + head: BlockIoMedia, + lowest_aligned_lba: u64, + logical_blocks_per_physical_block: u32, + optimal_transfer_length_granularity: u32, +} + +const _: () = { + assert!(size_of::() == 32); + assert!(offset_of!(BlockIoMedia, media_present) == 5); + assert!(offset_of!(BlockIoMedia, block_size) == 12); + assert!(offset_of!(BlockIoMedia, io_align) == 16); + assert!(offset_of!(BlockIoMedia, last_block) == 24); + assert!(size_of::() == 48); + assert!(offset_of!(BlockIoMediaRevision3, optimal_transfer_length_granularity) == 44); +}; + +impl BlockIoMedia { + pub fn is_media_present(&self) -> bool { + self.media_present != 0 + } +} + +impl BlockIo { + /// The first revision whose media carries `OptimalTransferLengthGranularity`. + pub const REVISION3: u64 = 0x0002_001f; + + /// The media as firmware describes it now, through `LastBlock`. + pub fn media(&self) -> BlockIoMedia { + // SAFETY: every revision's media begins with this head. + unsafe { self.media.read() } + } + + /// `OptimalTransferLengthGranularity`, where the revision carries it. + pub fn optimal_transfer_length_granularity(&self) -> Option { + if self.revision < Self::REVISION3 { + return None; + } + // SAFETY: a revision 3 media carries the field. + Some(unsafe { (*self.media.cast::()).optimal_transfer_length_granularity }) + } +} + +impl super::Scoped<'_, BlockIo> { + /// `ReadBlocks` (§13.9.3): `buffer`'s length in blocks from `lba`. + pub fn read_blocks(&self, media_id: u32, lba: u64, buffer: &mut [u8]) -> Result<(), Status> { + // SAFETY: the open interface, and `buffer` is its length of writable bytes. + unsafe { (self.read_blocks)(self.this(), media_id, lba, buffer.len(), buffer.as_mut_ptr()) }.ok() + } +} + +/// `EFI_GRAPHICS_OUTPUT_PROTOCOL` (UEFI 2.10 §12.9.2). +#[repr(C)] +pub struct Gop { + query_mode: Unused, + set_mode: Unused, + blt: Unused, + mode: *const GopMode, +} + +/// `EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE` (§12.9.2). +#[repr(C)] +struct GopMode { + max_mode: u32, + mode: u32, + info: *const GopModeInfo, + size_of_info: usize, + frame_buffer_base: u64, + frame_buffer_size: usize, +} + +/// `EFI_GRAPHICS_OUTPUT_MODE_INFORMATION` (§12.9.2). +#[repr(C)] +pub struct GopModeInfo { + version: u32, + pub horizontal_resolution: u32, + pub vertical_resolution: u32, + pub pixel_format: PixelFormat, + pixel_information: [u32; 4], + pub pixels_per_scan_line: u32, +} + +const _: () = { + assert!(size_of::() == 32); + assert!(offset_of!(Gop, mode) == 24); + assert!(size_of::() == 40); + assert!(offset_of!(GopMode, info) == 8); + assert!(offset_of!(GopMode, frame_buffer_base) == 24); + assert!(offset_of!(GopMode, frame_buffer_size) == 32); + assert!(size_of::() == 36); + assert!(offset_of!(GopModeInfo, pixel_format) == 12); + assert!(offset_of!(GopModeInfo, pixels_per_scan_line) == 32); +}; + +/// `EFI_GRAPHICS_PIXEL_FORMAT` (§12.9.2), as the raw word firmware wrote. +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +pub struct PixelFormat(pub u32); + +impl PixelFormat { + pub const RGB: PixelFormat = PixelFormat(0); + pub const BGR: PixelFormat = PixelFormat(1); + pub const BIT_MASK: PixelFormat = PixelFormat(2); + pub const BLT_ONLY: PixelFormat = PixelFormat(3); +} + +impl core::fmt::Debug for PixelFormat { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match *self { + Self::RGB => f.write_str("PixelRedGreenBlueReserved8BitPerColor"), + Self::BGR => f.write_str("PixelBlueGreenRedReserved8BitPerColor"), + Self::BIT_MASK => f.write_str("PixelBitMask"), + Self::BLT_ONLY => f.write_str("PixelBltOnly"), + PixelFormat(other) => write!(f, "PixelFormat({other})"), + } + } +} + +// SAFETY: §12.9.2's layout, under its GUID. +unsafe impl Protocol for Gop { + const GUID: Guid = Guid::new(0x9042a9de, 0x23dc, 0x4a38, [0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a]); +} + +impl Gop { + /// The mode firmware set. + pub fn current_mode_info(&self) -> &GopModeInfo { + // SAFETY: an open GOP names its mode and the mode its information. + unsafe { &*(*self.mode).info } + } + + /// The frame buffer's physical base and size in bytes. + pub fn frame_buffer(&self) -> (u64, u64) { + // SAFETY: an open GOP names its mode. + let mode = unsafe { &*self.mode }; + (mode.frame_buffer_base, mode.frame_buffer_size as u64) + } +} + +/// `EFI_RNG_PROTOCOL` (UEFI 2.10 §37.5.1). +#[repr(C)] +pub struct Rng { + get_info: Unused, + get_rng: unsafe extern "efiapi" fn(this: *mut Rng, algorithm: *const Guid, size: usize, value: *mut u8) -> Status, +} + +const _: () = assert!(size_of::() == 16); + +// SAFETY: §37.5.1's layout, under its GUID. +unsafe impl Protocol for Rng { + const GUID: Guid = Guid::new(0x3152bca5, 0xeade, 0x433d, [0x86, 0x2e, 0xc0, 0x1c, 0xdc, 0x29, 0x1f, 0x44]); +} + +impl super::Scoped<'_, Rng> { + /// `GetRNG` (§37.5.3) with no algorithm named: firmware's default. + pub fn get_rng(&self, into: &mut [u8]) -> Result<(), Status> { + // SAFETY: the open interface, and `into` is its length of writable bytes. + unsafe { (self.get_rng)(self.this(), ptr::null(), into.len(), into.as_mut_ptr()) }.ok() + } +} + +/// `EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL` (UEFI 2.10 §14.2.1). +#[repr(C)] +pub struct PciRootBridgeIo { + parent_handle: Unused, + poll_mem: Unused, + poll_io: Unused, + mem_read: Unused, + mem_write: Unused, + io_read: Unused, + io_write: Unused, + pci_read: Unused, + pci_write: Unused, + copy_mem: Unused, + map: Unused, + unmap: Unused, + allocate_buffer: Unused, + free_buffer: Unused, + flush: Unused, + get_attributes: Unused, + set_attributes: Unused, + configuration: unsafe extern "efiapi" fn(this: *mut PciRootBridgeIo, resources: *mut *const c_void) -> Status, + pub segment_number: u32, +} + +const _: () = { + assert!(size_of::() == 18 * 8 + 8); + assert!(offset_of!(PciRootBridgeIo, configuration) == 17 * 8); + assert!(offset_of!(PciRootBridgeIo, segment_number) == 18 * 8); +}; + +// SAFETY: §14.2.1's layout, under its GUID. +unsafe impl Protocol for PciRootBridgeIo { + const GUID: Guid = Guid::new(0x2f707ebb, 0x4a1a, 0x11d4, [0x9a, 0x38, 0x00, 0x90, 0x27, 0x3f, 0xc1, 0x4d]); +} + +impl super::Scoped<'_, PciRootBridgeIo> { + /// `Configuration()` (§14.2.13): the bridge's ACPI resource descriptors, + /// in memory firmware owns. + pub fn configuration(&self) -> Result<*const c_void, Status> { + let mut resources = ptr::null(); + // SAFETY: the open interface, and a live out parameter. + unsafe { (self.configuration)(self.this(), &mut resources) }.ok().map(|()| resources) + } +} diff --git a/bootloader/src/efi/runtime.rs b/bootloader/src/efi/runtime.rs new file mode 100644 index 00000000000..16bca2e3487 --- /dev/null +++ b/bootloader/src/efi/runtime.rs @@ -0,0 +1,175 @@ +//! `EFI_RUNTIME_SERVICES` (UEFI 2.10 §4.5) and what this loader calls of +//! them: the time, variables, and the reset. + +use core::ffi::c_void; +use core::mem::{offset_of, MaybeUninit}; +use core::ptr; + +use alloc::vec; +use alloc::vec::Vec; + +use super::{CStr16, CString16, Guid, Status, TableHeader}; + +type Unused = *const c_void; + +/// `EFI_RUNTIME_SERVICES` (§4.5), in the spec's own field order. +#[repr(C)] +pub struct RuntimeServices { + hdr: TableHeader, + get_time: unsafe extern "efiapi" fn(time: *mut Time, capabilities: *mut c_void) -> Status, + set_time: Unused, + get_wakeup_time: Unused, + set_wakeup_time: Unused, + set_virtual_address_map: Unused, + convert_pointer: Unused, + get_variable: unsafe extern "efiapi" fn( + name: *const u16, + vendor: *const Guid, + attributes: *mut u32, + size: *mut usize, + data: *mut u8, + ) -> Status, + get_next_variable_name: unsafe extern "efiapi" fn(size: *mut usize, name: *mut u16, vendor: *mut Guid) -> Status, + set_variable: unsafe extern "efiapi" fn( + name: *const u16, + vendor: *const Guid, + attributes: u32, + size: usize, + data: *const u8, + ) -> Status, + get_next_high_monotonic_count: Unused, + reset_system: unsafe extern "efiapi" fn(kind: ResetType, status: Status, size: usize, data: *const c_void) -> !, + update_capsule: Unused, + query_capsule_capabilities: Unused, + query_variable_info: Unused, +} + +const _: () = { + assert!(size_of::() == 24 + 14 * 8); + assert!(offset_of!(RuntimeServices, get_time) == 24); + assert!(offset_of!(RuntimeServices, get_variable) == 72); + assert!(offset_of!(RuntimeServices, get_next_variable_name) == 80); + assert!(offset_of!(RuntimeServices, set_variable) == 88); + assert!(offset_of!(RuntimeServices, reset_system) == 104); +}; + +/// `EFI_TIME` (§8.3). +#[derive(Clone, Copy)] +#[repr(C)] +pub struct Time { + pub year: u16, + pub month: u8, + pub day: u8, + pub hour: u8, + pub minute: u8, + pub second: u8, + pad1: u8, + pub nanosecond: u32, + pub time_zone: i16, + pub daylight: u8, + pad2: u8, +} + +const _: () = { + assert!(size_of::

(handle) -} - -#[allow(clippy::disallowed_methods, reason = "the one attribute it passes stops no driver")] -pub fn get(bs: &BootServices, handle: Handle) -> uefi::Result> { - // SAFETY: `open_protocol`'s obligation is that the handle and its protocol - // stay installed until the `ScopedProtocol` drops. Nothing between the two - // can uninstall either: the loader is the one image running, it registers - // no event callback, and it calls no boot service that connects or - // disconnects a controller. - unsafe { - bs.open_protocol::

( - OpenProtocolParams { handle, agent: bs.image_handle(), controller: None }, - OpenProtocolAttributes::GetProtocol, - ) - } -} diff --git a/bootloader/src/rootbridge.rs b/bootloader/src/rootbridge.rs index c1682f0d55b..f6d3060e8d9 100644 --- a/bootloader/src/rootbridge.rs +++ b/bootloader/src/rootbridge.rs @@ -12,47 +12,14 @@ //! the reason [`toyos_abi::boot::KernelArgs::root_bridge_windows`] states: a //! list missing one window is worse than no list. -use core::ffi::c_void; use core::ptr::read_volatile; use toyos_abi::boot::RootBridgeWindow; use toyos_acpi::{memory_windows, Phys, MAX_LIST_BYTES}; -use uefi::prelude::*; -use uefi::proto::unsafe_protocol; +use crate::efi::{PciRootBridgeIo, Status, SystemTable}; const HEAD: &str = "Root bridge:"; -/// UEFI 2.10 §14.2.2, in the spec's own field order. -#[repr(C)] -#[unsafe_protocol("2f707ebb-4a1a-11d4-9a38-0090273fc14d")] -struct PciRootBridgeIo { - parent_handle: *mut c_void, - poll_mem: *mut c_void, - poll_io: *mut c_void, - mem_read: *mut c_void, - mem_write: *mut c_void, - io_read: *mut c_void, - io_write: *mut c_void, - pci_read: *mut c_void, - pci_write: *mut c_void, - copy_mem: *mut c_void, - map: *mut c_void, - unmap: *mut c_void, - allocate_buffer: *mut c_void, - free_buffer: *mut c_void, - flush: *mut c_void, - get_attributes: *mut c_void, - set_attributes: *mut c_void, - configuration: - unsafe extern "efiapi" fn(this: *const PciRootBridgeIo, resources: *mut *const c_void) - -> Status, - segment_number: u32, -} - -/// A field added or dropped above moves `configuration`, and the symptom is -/// firmware calling something else. -const _: () = assert!(core::mem::size_of::() == 18 * 8 + 8); - /// The descriptor list firmware answered with. /// /// Boot services identity-map physical memory, so the pointer is the address @@ -79,9 +46,9 @@ impl Phys for List { /// Every memory window this machine's root bridges decode, written into `out`; /// the count, or zero on a machine that would not say. -pub fn windows(system_table: &SystemTable, out: &mut [RootBridgeWindow]) -> usize { +pub fn windows(system_table: &SystemTable, out: &mut [RootBridgeWindow]) -> usize { let bs = system_table.boot_services(); - let handles = match bs.find_handles::() { + let handles = match bs.handles::() { Ok(handles) => handles, Err(e) => { println!("{HEAD} no handle carries the PCI Root Bridge I/O protocol ({e}), so the kernel is handed no window"); @@ -91,7 +58,7 @@ pub fn windows(system_table: &SystemTable, out: &mut [RootBridgeWindow]) - let mut found = 0usize; for (index, handle) in handles.iter().enumerate() { - let bridge = match crate::protocol::get::(bs, *handle) { + let bridge = match bs.get::(*handle) { Ok(bridge) => bridge, Err(e) => { println!("{HEAD} handle {index} would not open ({e}), so the kernel is handed no window"); @@ -99,19 +66,15 @@ pub fn windows(system_table: &SystemTable, out: &mut [RootBridgeWindow]) - } }; - let mut resources: *const c_void = core::ptr::null(); - let this: *const PciRootBridgeIo = &*bridge; - // SAFETY: `this` is the protocol firmware installed on this handle and - // the call is the spec's — one out parameter, which firmware fills with - // a pointer it owns and this loader only reads. - let status = unsafe { (bridge.configuration)(this, &mut resources) }; - if !status.is_success() || resources.is_null() { + let answer = bridge.configuration(); + let Some(&resources) = answer.as_ref().ok().filter(|resources| !resources.is_null()) else { println!( - "{HEAD} {index} (segment {}) answered {status:?} to Configuration(), so the kernel is handed no window", - bridge.segment_number + "{HEAD} {index} (segment {}) answered {:?} to Configuration(), so the kernel is handed no window", + bridge.segment_number, + answer.err().unwrap_or(Status::SUCCESS), ); return 0; - } + }; let list = List { at: resources as u64 }; match memory_windows(list, list.at, &mut out[found..]) { diff --git a/bootloader/src/rootimage.rs b/bootloader/src/rootimage.rs index c0f93626e12..dac9adddfbc 100644 --- a/bootloader/src/rootimage.rs +++ b/bootloader/src/rootimage.rs @@ -24,11 +24,7 @@ use core::num::NonZeroU64; use toyos_gpt::{Guid, Located, Sectors}; use toyos_rootimage::chunk; use toyos_update::slots::{self, Table}; -use uefi::prelude::*; -use uefi::proto::device_path::{DevicePath, DevicePathNode, DeviceSubType, DeviceType}; -use uefi::proto::loaded_image::LoadedImage; -use uefi::proto::media::block::{BlockIO, BlockIoProtocol}; -use uefi::table::boot::{AllocateType, MemoryType, ScopedProtocol}; +use crate::efi::{AllocateType, BlockIo, BootServices, DevicePath, Handle, HardDrive, LoadedImage, Scoped, Status}; /// The unit ROOT's filesystem is written in, and the alignment every buffer /// here is allocated at. @@ -44,10 +40,6 @@ const BLOCK: usize = 4096; /// a read that fails is named to within 1 MiB. const CHUNK_BOUND: usize = 1 << 20; -/// UEFI 2.11 §13.9's `EFI_BLOCK_IO_PROTOCOL_REVISION3`, the first whose media -/// carries `OptimalTransferLengthGranularity`. -const BLOCK_IO_REVISION3: u64 = 0x0002_001F; - /// The line a read ROOT is reported on, with where it went and what it cost. pub const READ_AT: &str = "ROOT: read into memory at"; @@ -92,31 +84,30 @@ impl RootImage { /// image from: the one handle whose device path is the partition's without /// its last node, the HARDDRIVE one. pub fn boot_disk(handle: Handle, bs: &BootServices) -> Result { - let image = crate::protocol::exclusive::(bs, handle) + let image = bs + .exclusive::(handle) .map_err(|e| alloc::format!("this image's LoadedImage: {e:?}"))?; let device = image.device().ok_or("firmware names no device this image was loaded from")?; - let path = crate::protocol::get::(bs, device) - .map_err(|e| alloc::format!("the boot device's path: {e:?}"))?; - let nodes: alloc::vec::Vec<&DevicePathNode> = path.node_iter().collect(); + let path = bs.get::(device).map_err(|e| alloc::format!("the boot device's path: {e:?}"))?; + let nodes: alloc::vec::Vec<&[u8]> = path.nodes().collect(); let Some((last, disk_nodes)) = nodes.split_last() else { return Err("the boot device's path is empty".into()); }; - if last.full_type() != (DeviceType::MEDIA, DeviceSubType::MEDIA_HARD_DRIVE) { + if (last[0], last[1]) != HardDrive::TYPE { return Err("the boot device is not a partition, so there is no disk to find the slots on".into()); } - let handles = bs - .find_handles::() - .map_err(|e| alloc::format!("firmware lists no block devices: {e:?}"))?; + let handles = bs.handles::().map_err(|e| alloc::format!("firmware lists no block devices: {e:?}"))?; let disks: alloc::vec::Vec = handles - .into_iter() + .iter() + .copied() // Not the partition itself, whose path is held open above: a second // open by this agent is closed along with the first, and the second // close then fails. .filter(|&candidate| candidate != device) .filter(|&candidate| { - let Ok(path) = crate::protocol::get::(bs, candidate) else { return false }; - path.node_iter().eq(disk_nodes.iter().copied()) + let Ok(path) = bs.get::(candidate) else { return false }; + path.nodes().eq(disk_nodes.iter().copied()) }) .collect(); match disks[..] { @@ -127,7 +118,7 @@ pub fn boot_disk(handle: Handle, bs: &BootServices) -> Result { /// The boot disk, read through the firmware's block I/O. pub struct Disk<'a> { - io: ScopedProtocol<'a, BlockIO>, + io: Scoped<'a, BlockIo>, media_id: u32, lba_bytes: u32, lba_count: u64, @@ -138,21 +129,21 @@ pub struct Disk<'a> { impl<'a> Disk<'a> { pub fn open(bs: &'a BootServices, handle: Handle) -> Result { - let io = crate::protocol::get::(bs, handle).map_err(|e| alloc::format!("the boot disk's block I/O: {e:?}"))?; + let io = bs.get::(handle).map_err(|e| alloc::format!("the boot disk's block I/O: {e:?}"))?; let media = io.media(); if !media.is_media_present() { return Err("the boot disk reports no media".into()); } - let lba_bytes = media.block_size(); + let lba_bytes = media.block_size; // UEFI 2.11 §13.9: `IoAlign` is 0 or 1 for none, else a power of two. - let align = media.io_align().max(1) as usize; + let align = media.io_align.max(1) as usize; if !align.is_power_of_two() || align > BLOCK { return Err(alloc::format!("the boot disk wants buffers aligned to {align} bytes")); } if lba_bytes == 0 || !BLOCK.is_multiple_of(lba_bytes as usize) { return Err(alloc::format!("the boot disk's {lba_bytes}-byte block does not divide {BLOCK}")); } - let (media_id, lba_count) = (media.media_id(), media.last_block() + 1); + let (media_id, lba_count) = (media.media_id, media.last_block + 1); Ok(Disk { media_id, lba_bytes, lba_count, io, scratch: aligned(BLOCK) }) } @@ -185,7 +176,7 @@ impl<'a> Disk<'a> { let at = part.first_lba() + i as u64 * lbas; self.io .read_blocks(self.media_id, at, self.scratch) - .map_err(|e| alloc::format!("the slot table's copy {i} would not read: {:?}", e.status()))?; + .map_err(|e| alloc::format!("the slot table's copy {i} would not read: {e:?}"))?; copy.copy_from_slice(self.scratch); } slots::current([&copies[0], &copies[1]]) @@ -212,7 +203,7 @@ impl<'a> Disk<'a> { // `LoaderData`, as the black box's page is, for the reason its module // header gives. let at = bs - .allocate_pages(AllocateType::AnyPages, MemoryType::LOADER_DATA, pages) + .allocate_pages(AllocateType::AnyPages, pages) .map_err(|e| alloc::format!("firmware would not give {len} bytes for ROOT: {e:?}"))?; // SAFETY: the `pages` pages at `at` were just allocated to this loader, // are identity-mapped while boot services live, and nothing else holds them. @@ -231,7 +222,7 @@ impl<'a> Disk<'a> { "the read of {} blocks at LBA {} failed: {:?}, after {} of {len} bytes read", failed.blocks, failed.lba, - failed.error.status(), + failed.error, failed.read ); image.free(bs); @@ -253,27 +244,19 @@ impl<'a> Disk<'a> { /// `OptimalTransferLengthGranularity`, where the media is revision 3 or /// later and so carries the field, and reports a non-zero one. fn granularity_lbas(&self) -> Option { - let io: &BlockIO = &self.io; - // SAFETY: uefi 0.26 declares `BlockIO` `repr(transparent)` over - // `BlockIoProtocol`, so the one is the other's layout; `revision` is - // the field the crate does not expose. - let revision = unsafe { &*core::ptr::from_ref(io).cast::() }.revision; - if revision < BLOCK_IO_REVISION3 { - return None; - } - Some(self.io.media().optimal_transfer_length_granularity()).filter(|&lbas| lbas != 0) + self.io.optimal_transfer_length_granularity().filter(|&lbas| lbas != 0) } } /// The boot disk as [`chunk::read`] asks for it. -struct Firmware<'a> { - io: &'a BlockIO, +struct Firmware<'a, 'b> { + io: &'a Scoped<'b, BlockIo>, media_id: u32, } -impl chunk::Blocks for Firmware<'_> { - type Error = uefi::Error; - fn read(&mut self, lba: u64, into: &mut [u8]) -> uefi::Result { +impl chunk::Blocks for Firmware<'_, '_> { + type Error = Status; + fn read(&mut self, lba: u64, into: &mut [u8]) -> Result<(), Status> { self.io.read_blocks(self.media_id, lba, into) } } diff --git a/bootloader/src/seed.rs b/bootloader/src/seed.rs index 890f8887ae4..c67a58b4cca 100644 --- a/bootloader/src/seed.rs +++ b/bootloader/src/seed.rs @@ -6,30 +6,27 @@ use toyos_abi::boot::SEED_LEN; use toyos_random::{wipe, Seed}; -use uefi::prelude::*; -use uefi::proto::rng::Rng; - -use crate::protocol; +use crate::efi::{Rng, SystemTable}; /// Fill `into` with firmware's seed and answer its length: [`SEED_LEN`], or 0 /// with `into` zero. Judged with the kernel's own [`Seed::judge`], so the line /// here says what the kernel will find. -pub fn read(system_table: &SystemTable, into: &mut [u8; SEED_LEN]) -> u64 { +pub fn read(system_table: &SystemTable, into: &mut [u8; SEED_LEN]) -> u64 { let bs = system_table.boot_services(); let none = |why: core::fmt::Arguments| { println!("Seed: {why}, so the kernel's generator is handed none"); 0 }; - let Ok(handle) = bs.get_handle_for_protocol::() else { + let Ok(handle) = bs.handle_for::() else { return none(format_args!("firmware has no EFI_RNG_PROTOCOL")); }; // GET_PROTOCOL: an exclusive open would stop the driver behind it. - let mut rng = match protocol::get::(bs, handle) { + let rng = match bs.get::(handle) { Ok(rng) => rng, Err(e) => return none(format_args!("EFI_RNG_PROTOCOL would not open ({e})")), }; // No algorithm named: firmware's default (§37.5.2). - if let Err(e) = rng.get_rng(None, into) { + if let Err(e) = rng.get_rng(into) { wipe(into); return none(format_args!("EFI_RNG_PROTOCOL's GetRNG failed ({e})")); } diff --git a/bootloader/src/slot.rs b/bootloader/src/slot.rs index e5130ef5f07..aa1da111752 100644 --- a/bootloader/src/slot.rs +++ b/bootloader/src/slot.rs @@ -27,10 +27,7 @@ use toyos_update::policy::{self, Refusal}; use toyos_update::record::{self, Booted, Record}; use toyos_update::slots::{self, Slot, Which}; use toyos_update::{sig, Digest}; -use uefi::prelude::*; -use uefi::proto::media::file::{File, FileAttribute, FileInfo, FileMode}; -use uefi::proto::media::fs::SimpleFileSystem; -use uefi::CString16; +use crate::efi::{BootServices, CString16, Handle, Mode, SimpleFileSystem, Status, SystemTable}; use crate::rootimage::{Disk, RootImage}; @@ -57,7 +54,7 @@ pub struct Chosen { /// The slot to boot, or every refusal and why there is nothing to boot. pub fn choose( handle: Handle, - system_table: &SystemTable, + system_table: &SystemTable, floor: u64, record: &Record, ) -> Result { @@ -192,7 +189,7 @@ fn signed_header(bs: &BootServices, which: Which, slot: &Slot) -> Result<(Header /// The version the image the record says the last boot proved carries, read /// out of its slot's signed header, verified in this pass; or why no version /// is. -pub fn proven(handle: Handle, system_table: &SystemTable, booted: &Booted) -> Result { +pub fn proven(handle: Handle, system_table: &SystemTable, booted: &Booted) -> Result { let bs = system_table.boot_services(); let letter = booted.slot.letter(); let mut disk = Disk::open(bs, crate::rootimage::boot_disk(handle, bs)?)?; @@ -246,21 +243,22 @@ enum FileRefused { /// `max` bytes. fn read_file(bs: &BootServices, guid: &[u8; 16], path: &str, max: u64) -> Result, FileRefused> { let handle = crate::loaderlog::volume_handle(bs, guid).map_err(FileRefused::Other)?; - let mut fs = crate::protocol::exclusive::(bs, handle) + let mut fs = bs + .exclusive::(handle) .map_err(|e| FileRefused::Other(alloc::format!("would not open its volume ({e})")))?; let mut root = fs.open_volume().map_err(|e| FileRefused::Other(alloc::format!("has no volume ({e})")))?; - let name = CString16::try_from(path.replace('/', "\\").as_str()) - .map_err(|_| FileRefused::Other(String::from("is no UCS-2 path")))?; - let file = match root.open(&name, FileMode::Read, FileAttribute::empty()) { + let name = CString16::new(&path.replace('/', "\\")) + .ok_or_else(|| FileRefused::Other(String::from("is no UCS-2 path")))?; + let file = match root.open(&name, Mode::Read) { Ok(file) => file, - Err(e) if e.status() == Status::NOT_FOUND => return Err(FileRefused::Missing), + Err(Status::NOT_FOUND) => return Err(FileRefused::Missing), Err(e) => return Err(FileRefused::Other(alloc::format!("would not open ({e})"))), }; let mut file = file.into_regular_file().ok_or(FileRefused::Other(String::from("is a directory")))?; - let info = file - .get_boxed_info::() - .map_err(|e| FileRefused::Other(alloc::format!("would not say its size ({e})")))?; - let size = info.file_size(); + let size = file + .info() + .map_err(|e| FileRefused::Other(alloc::format!("would not say its size ({e})")))? + .file_size; if size > max { return Err(FileRefused::Other(alloc::format!("is {size} bytes, past the {max} expected"))); } diff --git a/bootloader/src/stamp.rs b/bootloader/src/stamp.rs index 803d53b291d..e146ab8ac75 100644 --- a/bootloader/src/stamp.rs +++ b/bootloader/src/stamp.rs @@ -35,6 +35,6 @@ pub fn now() -> Head<'static> { /// One line, stamped, to the console and then to `loader.log`. pub fn say(args: fmt::Arguments) { let head = now(); - uefi_services::println!("{head} {args}"); + crate::efi::print(format_args!("{head} {args}\n")); crate::loaderlog::line(format_args!("{head} {args}")); } diff --git a/bootloader/src/watchdog.rs b/bootloader/src/watchdog.rs index 4440cfd59b5..6adae80274d 100644 --- a/bootloader/src/watchdog.rs +++ b/bootloader/src/watchdog.rs @@ -12,7 +12,6 @@ //! Every machine this cannot arm is refused by name on the console and boots //! anyway. -use core::mem::{align_of, size_of}; use core::ptr::read_volatile; use toyos_acpi::Phys; @@ -20,8 +19,7 @@ use toyos_tco::{ Chipset, TCO1_CNT, TCO1_CNT_LOCK, TCO1_CNT_NO_REBOOT, TCO1_CNT_RUN, TCO1_STS, TCO2_STS, TCO1_STS_TIMEOUT, TCO_RLD, TCO_TMR, }; -use uefi::prelude::*; -use uefi::table::boot::{MemoryDescriptor, PAGE_SIZE}; +use crate::efi::{SystemTable, PAGE_SIZE}; /// x86-64's 52-bit physical-address ceiling. const MAX_PHYS: u64 = 1 << 52; @@ -49,7 +47,7 @@ impl Phys for Identity { /// Arm the watchdog when `cmdline` names it, and say on the console what was /// armed or why nothing was. -pub fn arm(system_table: &SystemTable, rsdp_addr: u64, cmdline: &str) { +pub fn arm(system_table: &SystemTable, rsdp_addr: u64, cmdline: &str) { if !toyos_abi::boot::actuators(cmdline).any(|token| token == toyos_tco::PARAM) { return; } @@ -152,21 +150,14 @@ fn chipset(ecam: u64) -> Option<(&'static Chipset, u32, u32)> { } /// Whether firmware's own map describes `[at, at + len)` inside one region. -fn described(system_table: &SystemTable, at: u64, len: u64) -> bool { +fn described(system_table: &SystemTable, at: u64, len: u64) -> bool { let bs = system_table.boot_services(); - let size = bs.memory_map_size(); + let (map_size, entry_size) = bs.memory_map_size(); // Eight descriptors of slack: the map can grow between the two calls, and // this one allocates in between. - let bytes = size.map_size + 8 * size.entry_size; + let bytes = map_size + 8 * entry_size; let mut words: alloc::vec::Vec = alloc::vec![0; bytes.div_ceil(size_of::())]; - const _: () = assert!(align_of::() <= align_of::()); - // SAFETY: `words` is a live allocation of exactly this many bytes, aligned - // for `u64` and so for `MemoryDescriptor`, and nothing else names it while - // the slice is alive. - let buffer = unsafe { - core::slice::from_raw_parts_mut(words.as_mut_ptr().cast::(), words.len() * 8) - }; - let Ok(map) = bs.memory_map(buffer) else { return false }; + let Ok(map) = bs.memory_map(&mut words) else { return false }; let Some(end) = at.checked_add(len) else { return false }; // Every field here is firmware's, so a region whose own end overflows // describes nothing rather than wrapping into one that covers `at`. diff --git a/clippy.toml b/clippy.toml index 6f96d7356d0..c83c33fcdf9 100644 --- a/clippy.toml +++ b/clippy.toml @@ -4,6 +4,4 @@ disallowed-methods = [ { path = "alloc::sync::Arc::increment_strong_count", reason = "hand-rolled refcounting is the bug class the object layer deletes" }, { path = "alloc::sync::Arc::decrement_strong_count", reason = "hand-rolled refcounting, and the half that frees" }, { path = "core::mem::forget", reason = "a resource nobody gives back is a leak unless its site says why" }, - { path = "uefi::table::boot::BootServices::open_protocol_exclusive", reason = "EXCLUSIVE stops every driver holding the protocol, the firmware's graphics console among them: open through `protocol::exclusive`" }, - { path = "uefi::table::boot::BootServices::open_protocol", reason = "its caller picks the attribute, and EXCLUSIVE or BY_DRIVER stops the driver holding the protocol: open through `protocol::get`" }, ] diff --git a/issues/the-loader-does-only-what-must-precede-the-handover.md b/issues/the-loader-does-only-what-must-precede-the-handover.md index 9c05b2a802f..45755df5684 100644 --- a/issues/the-loader-does-only-what-must-precede-the-handover.md +++ b/issues/the-loader-does-only-what-must-precede-the-handover.md @@ -13,7 +13,9 @@ The owner's bounds: - the kernel calls no UEFI service, and every UEFI call ToyOS makes is the loader's, before `ExitBootServices` (root `CLAUDE.md`); - the anti-rollback floor counts a signed security version, raised only by a - release that fixes a security hole. + release that fixes a security hole; +- the loader calls UEFI through its own bindings (`bootloader/src/efi/`), + never the `uefi` crate's (the owner's ruling on the dependency audit). PR #539 does not land. Its pieces: @@ -145,11 +147,8 @@ Each stage lands on its own, in this order. (`fwvars::live` in `tests/common/fwvars.rs`): each floor's name and its UEFI 2.10 §8.2 attributes. -4. **The loader on current `uefi`, sound, with a typed handover.** - - `uefi` and `uefi-raw` move to their current releases, and `uefi-services` - goes. The unsafe `BlockIO` media cast in `rootimage.rs` goes with the old - layout. - - The loader's own `#[panic_handler]` writes `loader: panicked at +4. **The loader sound, with a typed handover.** + - The loader's `#[panic_handler]` writes `loader: panicked at :: ` through `loaderlog`, then powers the machine off. It never resets: a panic with a fixed cause would reset into itself. The refused-floor site stops writing its reason to `loader.log` before @@ -181,11 +180,11 @@ Each stage lands on its own, in this order. change. - `toyos-update`'s slot table takes `toyos-gpt`'s CRC32 and loses its own. - **Exit**: `bootloader/Cargo.toml` names no `uefi-services`. - `loader_panic_powers_off` plants this key's floor in 9 bytes. It finds the + **Exit**: `loader_panic_powers_off` plants this key's floor in 9 bytes. It finds the handler's `loader: panicked at bootloader/src/` line in `loader.log`, which no other code writes, and QEMU reports `guest-shutdown`. It fails under - `uefi::helpers`' handler, which writes no `loader.log`. + the handler `bootloader/src/efi/mod.rs` holds before this stage, which + writes no `loader.log`. `kernel_args_last_layout_refused` boots with `loader-writes-the-last-layout` and finds the kernel's refusal naming both words before any `black box:` record. Moving `layout` after From a29d650e828f873dc049e3abe8d22a52db7343ff Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 19:56:44 +0200 Subject: [PATCH 2/3] A load option's HARDDRIVE node is read by main's rule again, and a panic after a refused ExitBootServices goes straight to the reset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The round-1 review of #815 found the load-option walk in bootnext.rs narrowed from main's rule (SignatureType at byte 41, signature at 24..40, any node at least 42 bytes long) to HardDrive::parse's exact 42 bytes. Those bytes are any OS's to write through runtime variable access, and no test at any tier can see the rule change: QEMU only reaches the "no Boot#### entry" arm. Main's decoder is restored byte for byte; HardDrive::parse stays for device paths firmware built (boot_partition, our_partition), and the loader track's stage 2 still unifies the two under its host tests. UEFI 2.10 §7.4.6 allows only the memory allocation services after a first ExitBootServices call, failed or not. A panic in the retry (fill_memory_map's assert) reached a handler that still wrote through ConOut. EXITING is set before each exit call; print and the panic handler read the console only while it is clear, so the handler goes straight to ResetSystem. The loader track's bound no longer puts the module path in the owner's ruling, and the arm64 issue no longer says the bootloader is the uefi crate. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- bootloader/src/bootnext.rs | 18 +++++++++++---- bootloader/src/efi/mod.rs | 22 +++++++++++++++---- bootloader/src/efi/proto.rs | 4 +++- ...oes-only-what-must-precede-the-handover.md | 4 ++-- issues/toyos-runs-on-arm64.md | 4 ++-- 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/bootloader/src/bootnext.rs b/bootloader/src/bootnext.rs index 54c260c9fb0..4a19e33eadc 100644 --- a/bootloader/src/bootnext.rs +++ b/bootloader/src/bootnext.rs @@ -153,7 +153,7 @@ fn load_option_names(option: &[u8], ours: &[u8; 16]) -> bool { // A node shorter than its own header, or longer than what is left, ends // the walk: neither can be stepped over. let Some(this) = path.get(..len).filter(|_| len >= NODE_HEADER) else { return false }; - if (this[0], this[1]) == HardDrive::TYPE { + if this[0] == MEDIA_HARD_DRIVE.0 && this[1] == MEDIA_HARD_DRIVE.1 { if let Some(guid) = gpt_signature(this) { return guid == *ours; } @@ -166,8 +166,18 @@ fn load_option_names(option: &[u8], ours: &[u8; 16]) -> bool { /// A device path node's type, subtype and length (UEFI 2.10 §10.2). const NODE_HEADER: usize = 4; -/// A HARD_DRIVE node's GUID signature, or `None` where it carries another or -/// is not the length §10.3.5.1 gives it. +/// The MEDIA/HARD_DRIVE node this looks for, as the two bytes it is on the wire. +const MEDIA_HARD_DRIVE: (u8, u8) = (4, 1); + +/// A HARD_DRIVE node's GPT signature, or `None` where it names an MBR one or +/// the node is short (UEFI 2.10 §10.3.6: the signature is sixteen bytes at +/// offset 24, and `SignatureType` 2 is the GPT one). fn gpt_signature(node: &[u8]) -> Option<[u8; 16]> { - HardDrive::parse(node).filter(|hd| hd.signature_type == HardDrive::GUID_SIGNATURE).map(|hd| hd.signature) + const SIGNATURE: usize = 24; + const SIGNATURE_TYPE: usize = 41; + const GPT: u8 = 2; + if node.get(SIGNATURE_TYPE) != Some(&GPT) { + return None; + } + node.get(SIGNATURE..SIGNATURE + 16)?.try_into().ok() } diff --git a/bootloader/src/efi/mod.rs b/bootloader/src/efi/mod.rs index 39b9ec9db6c..b0525001e80 100644 --- a/bootloader/src/efi/mod.rs +++ b/bootloader/src/efi/mod.rs @@ -321,6 +321,11 @@ pub const ACPI2_GUID: Guid = Guid::new(0x8868e871, 0xe4f1, 0x11d3, [0xbc, 0x22, /// console and the allocator read it; nothing else does. static SYSTEM: AtomicPtr = AtomicPtr::new(ptr::null_mut()); +/// Set before the first `ExitBootServices` call: from then on only the memory +/// allocation services may be called, even where the call fails (UEFI 2.10 +/// §7.4.6), so firmware's console is not written again. +static EXITING: AtomicBool = AtomicBool::new(false); + /// This image's handle, the agent every protocol open names. static IMAGE: AtomicPtr = AtomicPtr::new(ptr::null_mut()); @@ -384,6 +389,7 @@ impl SystemTable { continue; } }; + EXITING.store(true, Ordering::Release); status = bs.exit(image, filled.key); if status.is_success() { SYSTEM.store(ptr::null_mut(), Ordering::Release); @@ -406,6 +412,12 @@ fn live() -> Option<&'static RawSystemTable> { unsafe { SYSTEM.load(Ordering::Acquire).as_ref() } } +/// The system table while firmware's console may be written: until the first +/// `ExitBootServices` call. +fn console() -> Option<&'static RawSystemTable> { + live().filter(|_| !EXITING.load(Ordering::Acquire)) +} + /// The image's entry (UEFI 2.10 §4.1, `EFI_IMAGE_ENTRY_POINT`), under the name /// the UEFI targets link as the PE entry point. #[unsafe(no_mangle)] @@ -423,9 +435,10 @@ extern "efiapi" fn efi_main(image: *mut c_void, system_table: *mut RawSystemTabl /// character past UCS-2 is written as U+FFFD. /// /// # Panics -/// Once boot services are gone, and where the console refuses the line. +/// From the first `ExitBootServices` call, and where the console refuses the +/// line. pub fn print(args: fmt::Arguments) { - let raw = live().expect("the console is gone with boot services"); + let raw = console().expect("the console is gone with boot services"); let mut out = Console { out: raw.con_out, units: [0; Console::CHUNK + 1], len: 0 }; let written = fmt::write(&mut out, args).and_then(|()| out.flush()); assert!(written.is_ok(), "firmware's console refused a line"); @@ -529,12 +542,13 @@ unsafe impl core::alloc::GlobalAlloc for Pool { /// power-off. static PANICKED: AtomicBool = AtomicBool::new(false); -/// Say the panic on firmware's console and power the machine off; once boot +/// Say the panic on firmware's console and power the machine off; from the +/// first `ExitBootServices` call the console is not written, and once boot /// services are gone neither is there, and the CPU spins. #[panic_handler] fn panic(info: &core::panic::PanicInfo) -> ! { if let Some(raw) = live() { - if !PANICKED.swap(true, Ordering::Relaxed) { + if console().is_some() && !PANICKED.swap(true, Ordering::Relaxed) { print(format_args!("[PANIC]: {info}\n")); } // SAFETY: runtime services live as long as the table. diff --git a/bootloader/src/efi/proto.rs b/bootloader/src/efi/proto.rs index d60c848a73b..c33d49b2f85 100644 --- a/bootloader/src/efi/proto.rs +++ b/bootloader/src/efi/proto.rs @@ -147,7 +147,9 @@ impl HardDrive { pub const GUID_SIGNATURE: u8 = 2; const LEN: usize = 42; - /// `node`, where it is a HARDDRIVE node of the length §10.3.5.1 gives it. + /// `node`, where it is a HARDDRIVE node of the length §10.3.5.1 gives it: + /// a node of a device path firmware built. A load option's path is any + /// writer's bytes, and `bootnext` walks it by its own rule. pub fn parse(node: &[u8]) -> Option { if node.len() != Self::LEN || (node[0], node[1]) != Self::TYPE { return None; diff --git a/issues/the-loader-does-only-what-must-precede-the-handover.md b/issues/the-loader-does-only-what-must-precede-the-handover.md index 45755df5684..df223edec5e 100644 --- a/issues/the-loader-does-only-what-must-precede-the-handover.md +++ b/issues/the-loader-does-only-what-must-precede-the-handover.md @@ -14,8 +14,8 @@ The owner's bounds: loader's, before `ExitBootServices` (root `CLAUDE.md`); - the anti-rollback floor counts a signed security version, raised only by a release that fixes a security hole; -- the loader calls UEFI through its own bindings (`bootloader/src/efi/`), - never the `uefi` crate's (the owner's ruling on the dependency audit). +- the loader calls UEFI through its own bindings, never the `uefi` crate's + (the owner's ruling on the dependency audit). PR #539 does not land. Its pieces: diff --git a/issues/toyos-runs-on-arm64.md b/issues/toyos-runs-on-arm64.md index 6f98b613235..0caf929a530 100644 --- a/issues/toyos-runs-on-arm64.md +++ b/issues/toyos-runs-on-arm64.md @@ -182,8 +182,8 @@ refuses anything but `EM_X86_64` (`toyos-elf/src/header.rs:24,75`). `irq_guard`, `halt`, `need_resched`, `switch`), with `kernel/src/arch/x86_64/hw.rs` as the one x86 implementation and a simulator as the other. PCI is ECAM/MMIO-only (`drivers/pci.rs:134-154`), no `0xCF8`. NVMe, xHCI and virtio -have no ISA dependence beyond TSC-based waits. The bootloader is the `uefi` -crate (0.26, aarch64-capable already); only 12 of its 2,881 lines are x86. +have no ISA dependence beyond TSC-based waits. The bootloader calls UEFI +through its own bindings, which boot both targets. The pure decision crates carry no arch at all: `toyos-acpi`, `-gpt`, `-fat32`, `-dma`, `-userbound`, `-proclife`, `-blackbox`, `-elide` and others. From ab977259620c694ff897869443af26b8ddeb72ce Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 9 Oct 2026 20:27:02 +0200 Subject: [PATCH 3/3] The loader issue's stage 4 panics straight to the shutdown from EXITING on, and stage 2 names both HARDDRIVE decoders it folds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 4's handler writes loader.log through the File protocol, which UEFI 2.10 §7.4.6 forbids from the first ExitBootServices call as it forbids the console; as written it would undo the EXITING fix. It now skips loaderlog from EXITING on and goes straight to ResetSystem's shutdown. Stage 2 called toyos_update::entry::partition the one HARDDRIVE rule but named neither of the loader's two decoders going, efi::HardDrive::parse and bootnext::gpt_signature, so it could close with three. Both are named, and the exit gains an rg that finds them today. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- ...loader-does-only-what-must-precede-the-handover.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/issues/the-loader-does-only-what-must-precede-the-handover.md b/issues/the-loader-does-only-what-must-precede-the-handover.md index df223edec5e..8ade7486848 100644 --- a/issues/the-loader-does-only-what-must-precede-the-handover.md +++ b/issues/the-loader-does-only-what-must-precede-the-handover.md @@ -85,7 +85,10 @@ Each stage lands on its own, in this order. 2. **One boot disk.** - `toyos_update::entry::partition` is the one HARDDRIVE rule, taken by - `boot_partition`, `rootimage::boot_disk` and `bootnext.rs`. + `boot_partition`, `rootimage::boot_disk` and `bootnext.rs`. The loader's + two decoders go into it: `efi::HardDrive::parse` (`efi/proto.rs`), which + reads firmware-built paths, and `bootnext::gpt_signature`, which reads a + load option's. - Every volume the loader opens (the slot's FAT partition, the log partition and the attempts file on it) is found on the boot disk, where exactly one match is taken. `loaderlog::volume_handle`'s machine-wide @@ -96,7 +99,8 @@ Each stage lands on its own, in this order. one `Disk::open` and slot-table read, and one file reader. `load_file_bytes`, `MAX_ESP_FILE` and the unsound `alloc_uninit` go. - **Exit**: `a_path_names_the_partition_of_its_one_hard_drive_node` and + **Exit**: `rg 'fn gpt_signature|fn parse' bootloader/src/{bootnext.rs,efi/proto.rs}` + finds nothing. `a_path_names_the_partition_of_its_one_hard_drive_node` and `a_partitions_disk_is_the_path_before_its_hard_drive_node` pass on the host, and fail under two mutations: cutting the disk before the path's last node, and taking a second HARDDRIVE node. `root_named_twice` plants a twin whose @@ -151,6 +155,9 @@ Each stage lands on its own, in this order. - The loader's `#[panic_handler]` writes `loader: panicked at :: ` through `loaderlog`, then powers the machine off. It never resets: a panic with a fixed cause would reset into itself. + From `EXITING` on (the first `ExitBootServices` call) it skips `loaderlog` + and goes straight to `ResetSystem`'s shutdown, since UEFI 2.10 §7.4.6 + forbids the File protocol from that call on as it forbids the console. The refused-floor site stops writing its reason to `loader.log` before its `panic!`: the handler writes it. - `alloc_kernel_memory` stops building a `Vec` over a 2 MiB-aligned