diff --git a/Cargo.lock b/Cargo.lock index 7551623381..08e3d4ca88 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 aaf27e400f..9250af93ce 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 069705f9ce..6464066976 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 0f74981033..7ddf8fc876 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 b38e223a8b..4a19e33ead 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; diff --git a/bootloader/src/efi/boot.rs b/bootloader/src/efi/boot.rs new file mode 100644 index 0000000000..8009db982e --- /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 0000000000..b0525001e8 --- /dev/null +++ b/bootloader/src/efi/mod.rs @@ -0,0 +1,560 @@ +//! 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()); + +/// 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()); + +/// 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; + } + }; + EXITING.store(true, Ordering::Release); + 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 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)] +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 +/// From the first `ExitBootServices` call, and where the console refuses the +/// line. +pub fn print(args: fmt::Arguments) { + 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"); +} + +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; 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 console().is_some() && !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 0000000000..c33d49b2f8 --- /dev/null +++ b/bootloader/src/efi/proto.rs @@ -0,0 +1,663 @@ +//! 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: + /// 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; + } + 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 0000000000..16bca2e348 --- /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 c1682f0d55..f6d3060e8d 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 c0f93626e1..dac9adddfb 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 890f8887ae..c67a58b4cc 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 e5130ef5f0..aa1da11175 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 803d53b291..e146ab8ac7 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 4440cfd59b..6adae80274 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 6f96d7356d..c83c33fcdf 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 9c05b2a802..8ade748684 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, never the `uefi` crate's + (the owner's ruling on the dependency audit). PR #539 does not land. Its pieces: @@ -83,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 @@ -94,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 @@ -145,13 +151,13 @@ 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. + 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 @@ -181,11 +187,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 diff --git a/issues/toyos-runs-on-arm64.md b/issues/toyos-runs-on-arm64.md index 6f98b61323..0caf929a53 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.