From 106064d3e7ab5abac2a0e4a161488c7c12168ee0 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Sun, 19 Oct 2025 23:03:28 -0700 Subject: [PATCH] document by hand much more of the sprout code --- src/drivers.rs | 26 +++++++++- src/extractors.rs | 11 ++++ src/extractors/filesystem_device_match.rs | 40 +++++++++++++++ src/phases.rs | 3 ++ src/setup.rs | 7 ++- src/utils.rs | 3 ++ src/utils/framebuffer.rs | 7 +++ src/utils/media_loader.rs | 61 +++++++++++++++++++++-- src/utils/media_loader/constants.rs | 3 +- 9 files changed, 153 insertions(+), 8 deletions(-) diff --git a/src/drivers.rs b/src/drivers.rs index 908e657..25004d6 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -19,20 +19,26 @@ pub struct DriverDeclaration { pub path: String, } +/// Loads the driver specified by the [driver] declaration. fn load_driver(context: Rc, driver: &DriverDeclaration) -> Result<()> { + // Acquire the handle and device path of the loaded image. let sprout_image = uefi::boot::image_handle(); let image_device_path_protocol = uefi::boot::open_protocol_exclusive::(sprout_image) .context("unable to open loaded image device path protocol")?; + // Get the device path root of the sprout image. let mut full_path = utils::device_path_root(&image_device_path_protocol)?; + // Push the path of the driver from the root. full_path.push_str(&context.stamp(&driver.path)); info!("driver path: {}", full_path); + // Convert the path to a device path. let device_path = utils::text_to_device_path(&full_path)?; + // Load the driver image. let image = uefi::boot::load_image( sprout_image, uefi::boot::LoadImageSource::FromDevicePath { @@ -42,37 +48,55 @@ fn load_driver(context: Rc, driver: &DriverDeclaration) -> Result ) .context("unable to load image")?; + // Start the driver image, this is expected to return control to sprout. + // There is no guarantee that the driver will actually return control as it is + // just a standard EFI image. uefi::boot::start_image(image).context("unable to start driver image")?; Ok(()) } +/// Reconnects all handles to their controllers. +/// This is effectively a UEFI stack reload in a sense. +/// After we load all the drivers, we need to reconnect all of their handles +/// so that filesystems are recognized again. fn reconnect() -> Result<()> { + // Locate all of the handles in the UEFI stack. let handles = uefi::boot::locate_handle_buffer(SearchType::AllHandles) .context("unable to locate handles buffer")?; for handle in handles.iter() { - // ignore result as there is nothing we can do if it doesn't work. + // Ignore the result as there is nothing we can do if reconnecting a controller fails. + // This is also likely to fail in some cases but should fail safely. let _ = uefi::boot::connect_controller(*handle, None, None, true); } Ok(()) } +/// Load all the drivers specified in `drivers`. +/// There is no driver order currently. This will reconnect all the controllers +/// to all handles if at least one driver was loaded. pub fn load( context: Rc, drivers: &BTreeMap, ) -> Result<()> { + // If there are no drivers, we don't need to do anything. if drivers.is_empty() { return Ok(()); } info!("loading drivers"); + + // Load all the drivers in no particular order. for (name, driver) in drivers { load_driver(context.clone(), driver).context(format!("unable to load driver: {}", name))?; } + // Reconnect all the controllers to all handles. reconnect().context("unable to reconnect drivers")?; info!("loaded drivers"); + + // We've now loaded all the drivers, so we can return. Ok(()) } diff --git a/src/extractors.rs b/src/extractors.rs index d376a47..7320958 100644 --- a/src/extractors.rs +++ b/src/extractors.rs @@ -4,14 +4,25 @@ use anyhow::{Result, bail}; use serde::{Deserialize, Serialize}; use std::rc::Rc; +/// The filesystem device match extractor. pub mod filesystem_device_match; +/// Declares an extractor configuration. +/// Extractors allow calculating values at runtime +/// using built-in sprout modules. #[derive(Serialize, Deserialize, Default, Clone)] pub struct ExtractorDeclaration { + /// The filesystem device match extractor. + /// This extractor finds a filesystem using some search criteria and returns + /// the device root path that can concatenated with subpaths to access files + /// on a particular filesystem. #[serde(default, rename = "filesystem-device-match")] pub filesystem_device_match: Option, } +/// Extracts the value using the specified `extractor` under the provided `context`. +/// The extractor must return a value, and if a value cannot be determined, an error +/// should be returned. pub fn extract(context: Rc, extractor: &ExtractorDeclaration) -> Result { if let Some(filesystem) = &extractor.filesystem_device_match { filesystem_device_match::extract(context, filesystem) diff --git a/src/extractors/filesystem_device_match.rs b/src/extractors/filesystem_device_match.rs index f44de50..59ebf76 100644 --- a/src/extractors/filesystem_device_match.rs +++ b/src/extractors/filesystem_device_match.rs @@ -13,34 +13,57 @@ use uefi::proto::media::partition::PartitionInfo; use uefi::{CString16, Guid}; use uefi_raw::Status; +/// The filesystem device match extractor. +/// This extractor finds a filesystem using some search criteria and returns +/// the device root path that can concatenated with subpaths to access files +/// on a particular filesystem. +/// +/// This function only requires one of the criteria to match. +/// The fallback value can be used to provide a value if none is found. #[derive(Serialize, Deserialize, Default, Clone)] pub struct FilesystemDeviceMatchExtractor { + /// Matches a filesystem that has the specified label. #[serde(default, rename = "has-label")] pub has_label: Option, + /// Matches a filesystem that has the specified item. + /// An item is either a directory or file. #[serde(default, rename = "has-item")] pub has_item: Option, + /// Matches a filesystem that has the specified partition UUID. #[serde(default, rename = "has-partition-uuid")] pub has_partition_uuid: Option, + /// Matches a filesystem that has the specified partition type UUID. #[serde(default, rename = "has-partition-type-uuid")] pub has_partition_type_uuid: Option, + /// The fallback value to use if no filesystem matches the criteria. #[serde(default)] pub fallback: Option, } +/// Extract a filesystem device path using the specified `context` and `extractor` configuration. pub fn extract( context: Rc, extractor: &FilesystemDeviceMatchExtractor, ) -> Result { + // Find all the filesystems inside the UEFI stack. let handles = uefi::boot::find_handles::() .context("unable to find filesystem handles")?; + + // Iterate over all the filesystems and check if they match the criteria. for handle in handles { + // This defines whether a match has been found. let mut has_match = false; + // Extract the partition info for this filesystem. + // There is no guarantee that the filesystem has a partition. let partition_info = { + // Open the partition info protocol for this handle. let partition_info = uefi::boot::open_protocol_exclusive::(handle); match partition_info { Ok(partition_info) => { + // GPT partitions have a unique partition GUID. + // MBR does not. if let Some(gpt) = partition_info.gpt_partition_entry() { let uuid = gpt.unique_partition_guid; let type_uuid = gpt.partition_type_guid; @@ -51,10 +74,12 @@ pub fn extract( } Err(error) => { + // If the filesystem does not have a partition, that is okay. if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED { None } else { + // We should still handle other errors gracefully. Err(error).context("unable to open filesystem partition info")?; None } @@ -62,6 +87,7 @@ pub fn extract( } }; + // Check if the partition info matches partition uuid criteria. if let Some((partition_uuid, _partition_type_guid)) = partition_info && let Some(ref has_partition_uuid) = extractor.has_partition_uuid { @@ -73,6 +99,7 @@ pub fn extract( has_match = true; } + // Check if the partition info matches partition type uuid criteria. if let Some((_partition_uuid, partition_type_guid)) = partition_info && let Some(ref has_partition_type_uuid) = extractor.has_partition_type_uuid { @@ -84,9 +111,11 @@ pub fn extract( has_match = true; } + // Open the filesystem protocol for this handle. let mut filesystem = uefi::boot::open_protocol_exclusive::(handle) .context("unable to open filesystem protocol")?; + // Check if the filesystem matches label criteria. if let Some(ref label) = extractor.has_label { let want_label = CString16::try_from(context.stamp(label).as_str()) .context("unable to convert label to CString16")?; @@ -103,35 +132,46 @@ pub fn extract( has_match = true; } + // Check if the filesystem matches item criteria. if let Some(ref item) = extractor.has_item { let want_item = CString16::try_from(context.stamp(item).as_str()) .context("unable to convert item to CString16")?; let mut filesystem = FileSystem::new(filesystem); + + // Check the metadata of the item. let metadata = filesystem.metadata(Path::new(&want_item)); + // Ignore filesystem errors as we can't do anything useful with the error. if metadata.is_err() { continue; } let metadata = metadata?; + // Only check directories and files. if !(metadata.is_directory() || metadata.is_regular_file()) { continue; } has_match = true; } + // If there is no match, continue to the next filesystem. if !has_match { continue; } + // If we have a match, return the device root path. let path = uefi::boot::open_protocol_exclusive::(handle) .context("unable to open filesystem device path")?; let path = path.deref(); + // Acquire the device path root as a string. return utils::device_path_root(path).context("unable to get device path root"); } + // If there is a fallback value, use it at this point. if let Some(fallback) = &extractor.fallback { return Ok(fallback.clone()); } + + // Without a fallback, we can't continue, so bail. bail!("unable to find matching filesystem") } diff --git a/src/phases.rs b/src/phases.rs index d637c38..68de3c4 100644 --- a/src/phases.rs +++ b/src/phases.rs @@ -20,6 +20,9 @@ pub struct PhasesConfiguration { pub late: Vec, } +/// Configures a single phase of the boot process. +/// There can be multiple phase configurations that are +/// executed sequentially. #[derive(Serialize, Deserialize, Default, Clone)] pub struct PhaseConfiguration { /// The actions to run when the phase is executed. diff --git a/src/setup.rs b/src/setup.rs index f40282e..127f6cd 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -6,19 +6,22 @@ use std::os::uefi as uefi_std; /// This fetches the system table and current image handle from uefi_std and injects /// them into the uefi crate. pub fn init() -> Result<()> { + // Acquire the system table and image handle from the uefi_std environment. let system_table = uefi_std::env::system_table(); let image_handle = uefi_std::env::image_handle(); - // SAFETY: The uefi variables above come from the Rust std. - // These variables are nonnull and calling the uefi crates with these values is validated + // SAFETY: The UEFI variables above come from the Rust std. + // These variables are not-null and calling the uefi crates with these values is validated // to be corrected by hand. unsafe { + // Set the system table and image handle. uefi::table::set_system_table(system_table.as_ptr().cast()); let handle = uefi::Handle::from_ptr(image_handle.as_ptr().cast()) .context("unable to resolve image handle")?; uefi::boot::set_image_handle(handle); } + // Initialize the uefi logger mechanism and other helpers. uefi::helpers::init().context("unable to initialize uefi")?; Ok(()) } diff --git a/src/utils.rs b/src/utils.rs index 176ee67..d6ebf86 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,7 +6,10 @@ use uefi::proto::device_path::{DevicePath, PoolDevicePath}; use uefi::proto::media::fs::SimpleFileSystem; use uefi::{CString16, Handle}; +/// Support code for the EFI framebuffer. pub mod framebuffer; + +/// Support code for the media loader protocol. pub mod media_loader; /// Parses the input [path] as a [DevicePath]. diff --git a/src/utils/framebuffer.rs b/src/utils/framebuffer.rs index efcb60d..7fcf55a 100644 --- a/src/utils/framebuffer.rs +++ b/src/utils/framebuffer.rs @@ -1,13 +1,18 @@ use anyhow::{Context, Result}; use uefi::proto::console::gop::{BltOp, BltPixel, BltRegion, GraphicsOutput}; +/// Represents the EFI framebuffer. pub struct Framebuffer { + /// The width of the framebuffer in pixels. width: usize, + /// The height of the framebuffer in pixels. height: usize, + /// The pixels of the framebuffer. pixels: Vec, } impl Framebuffer { + /// Creates a new framebuffer of the specified `width` and `height`. pub fn new(width: usize, height: usize) -> Self { Framebuffer { width, @@ -16,10 +21,12 @@ impl Framebuffer { } } + /// Mutably acquires a pixel of the framebuffer at the specified `x` and `y` coordinate. pub fn pixel(&mut self, x: usize, y: usize) -> Option<&mut BltPixel> { self.pixels.get_mut(y * self.width + x) } + /// Blit the framebuffer to the specified `gop` [GraphicsOutput]. pub fn blit(&self, gop: &mut GraphicsOutput) -> Result<()> { gop.blt(BltOp::BufferToVideo { buffer: &self.pixels, diff --git a/src/utils/media_loader.rs b/src/utils/media_loader.rs index 19894ab..573427d 100644 --- a/src/utils/media_loader.rs +++ b/src/utils/media_loader.rs @@ -11,9 +11,11 @@ use uefi_raw::{Boolean, Status}; pub mod constants; +/// The media loader protocol. #[derive(Debug)] #[repr(C)] struct MediaLoaderProtocol { + /// This is the standard EFI LoadFile2 protocol. pub load_file: unsafe extern "efiapi" fn( this: *mut MediaLoaderProtocol, file_path: *const DevicePathProtocol, @@ -21,7 +23,9 @@ struct MediaLoaderProtocol { buffer_size: *mut usize, buffer: *mut c_void, ) -> Status, + /// A pointer to a Box<[u8]> containing the data to load. pub address: *mut c_void, + /// The length of the data to load. pub length: usize, } @@ -29,9 +33,13 @@ struct MediaLoaderProtocol { /// You MUST call [MediaLoaderHandle::unregister] when ready to unregister. /// [Drop] is not implemented for this type. pub struct MediaLoaderHandle { + /// The vendor GUID of the media loader. guid: Guid, + /// The handle of the media loader in the UEFI stack. handle: Handle, + /// The protocol interface pointer. protocol: *mut MediaLoaderProtocol, + /// The device path pointer. path: *mut DevicePath, } @@ -50,11 +58,12 @@ impl MediaLoaderHandle { buffer_size: *mut usize, buffer: *mut c_void, ) -> Status { + // Check if the pointers are non-null first. if this.is_null() || buffer_size.is_null() || file_path.is_null() { return Status::INVALID_PARAMETER; } - // Boot policy must not be true, as that's special behavior that is irrelevant + // Boot policy must not be true, and if it is, that is special behavior that is irrelevant // for the media loader concept. if boot_policy == Boolean::TRUE { return Status::UNSUPPORTED; @@ -63,48 +72,68 @@ impl MediaLoaderHandle { // SAFETY: Validated as safe because this is checked to be non-null. It is the caller's // responsibility to ensure that the right pointer is passed for [this]. unsafe { + // Check if the length and address are valid. if (*this).length == 0 || (*this).address.is_null() { return Status::NOT_FOUND; } + // Check if the buffer is large enough. + // If it is not, we need to set the buffer size to the length of the data. + // This is the way that Linux calls this function, to check the size to allocate + // for the buffer that holds the data. if buffer.is_null() || *buffer_size < (*this).length { *buffer_size = (*this).length; return Status::BUFFER_TOO_SMALL; } + // Copy the data into the buffer. buffer.copy_from((*this).address, (*this).length); + // Set the buffer size to the length of the data. *buffer_size = (*this).length; } + // We've successfully loaded the data. Status::SUCCESS } + /// Creates a new device path for the media loader based on a vendor `guid`. fn device_path(guid: Guid) -> Box { + // The buffer for the device path. let mut path = Vec::new(); + // Build a device path for the media loader with a vendor-specific guid. let path = DevicePathBuilder::with_vec(&mut path) .push(&Vendor { vendor_guid: guid, vendor_defined_data: &[], }) - .unwrap() + .unwrap() // We know that the device path is valid, so we can unwrap. .finalize() - .unwrap(); + .unwrap(); // We know that the device path is valid, so we can unwrap. + // Convert the device path to a boxed device path. + // This is safer than dealing with a pooled device path. path.to_boxed() } + /// Checks if the media loader is already registered with the UEFI stack. fn already_registered(guid: Guid) -> Result { + // Acquire the device path for the media loader. let path = Self::device_path(guid); let mut existing_path = path.as_ref(); + + // Locate the LoadFile2 protocol for the media loader based on the device path. let result = uefi::boot::locate_device_path::(&mut existing_path); + // If the result is okay, the media loader is already registered. if result.is_ok() { return Ok(true); } else if let Err(error) = result && error.status() != Status::NOT_FOUND + // If the error is not found, that means it's not registered. { bail!("unable to locate media loader device path: {}", error); } + // The media loader is not registered. Ok(false) } @@ -112,13 +141,20 @@ impl MediaLoaderHandle { /// This uses a special device path that other EFI programs will look at /// to load the data from. pub fn register(guid: Guid, data: Box<[u8]>) -> Result { + // Acquire the vendor device path for the media loader. let path = Self::device_path(guid); - let path = Box::leak(path); + // Check if the media loader is already registered. + // If it is, we can't register it again safely. if Self::already_registered(guid)? { bail!("media loader already registered"); } + // Leak the device path to pass it to the UEFI stack. + let path = Box::leak(path); + + // Install a protocol interface for the device path. + // This ensures it can be located by other EFI programs. let mut handle = unsafe { uefi::boot::install_protocol_interface( None, @@ -128,16 +164,20 @@ impl MediaLoaderHandle { } .context("unable to install media loader device path handle")?; + // Leak the data we need to pass to the UEFI stack. let data = Box::leak(data); + // Allocate a new box for the protocol interface. let protocol = Box::new(MediaLoaderProtocol { load_file: Self::load_file, address: data.as_ptr() as *mut _, length: data.len(), }); + // Leak the protocol interface to pass it to the UEFI stack. let protocol = Box::leak(protocol); + // Install a protocol interface for the load file protocol for the media loader protocol. handle = unsafe { uefi::boot::install_protocol_interface( Some(handle), @@ -147,10 +187,13 @@ impl MediaLoaderHandle { } .context("unable to install media loader load file handle")?; + // Check if the media loader is registered. + // If it is not, we can't continue safely because something went wrong. if !Self::already_registered(guid)? { bail!("media loader not registered when expected to be registered"); } + // Return a handle to the media loader. Ok(Self { guid, handle, @@ -162,11 +205,16 @@ impl MediaLoaderHandle { /// Unregisters a media loader from the UEFI stack. /// This will free the memory allocated by the passed data. pub fn unregister(self) -> Result<()> { + // Check if the media loader is registered. + // If it is not, we don't need to do anything. if !Self::already_registered(self.guid)? { return Ok(()); } + // SAFETY: We know that the media loader is registered, so we can safely uninstall it. + // We should have allocated the pointers involved, so we can safely free them. unsafe { + // Uninstall the protocol interface for the device path protocol. uefi::boot::uninstall_protocol_interface( self.handle, &DevicePathProtocol::GUID, @@ -174,6 +222,7 @@ impl MediaLoaderHandle { ) .context("unable to uninstall media loader device path handle")?; + // Uninstall the protocol interface for the load file protocol. uefi::boot::uninstall_protocol_interface( self.handle, &LoadFile2Protocol::GUID, @@ -181,12 +230,16 @@ impl MediaLoaderHandle { ) .context("unable to uninstall media loader load file handle")?; + // Retrieve a box for the device path and protocols. let path = Box::from_raw(self.path); let protocol = Box::from_raw(self.protocol); + + // Retrieve a box for the data we passed in. let slice = std::ptr::slice_from_raw_parts_mut(protocol.address as *mut u8, protocol.length); let data = Box::from_raw(slice); + // Drop all the allocations explicitly, as we don't want to leak them. drop(path); drop(protocol); drop(data); diff --git a/src/utils/media_loader/constants.rs b/src/utils/media_loader/constants.rs index 3b094a3..85eade7 100644 --- a/src/utils/media_loader/constants.rs +++ b/src/utils/media_loader/constants.rs @@ -13,7 +13,8 @@ pub mod xen { /// The device path GUID for the Xen EFI config. pub const XEN_EFI_CONFIG_MEDIA_GUID: Guid = guid!("bf61f458-a28e-46cd-93d7-07dac5e8cd66"); - /// The device path GUID for the Xen EFI config. + /// The device path GUID for the Xen EFI kernel. pub const XEN_EFI_KERNEL_MEDIA_GUID: Guid = guid!("4010c8bf-6ced-40f5-a53f-e820aee8f34b"); + /// The device path GUID for the Xen EFI ramdisk. pub const XEN_EFI_RAMDISK_MEDIA_GUID: Guid = guid!("5db1fd01-c3cb-4812-b2ba-8791e52d4a89"); }