diff --git a/crates/boot/src/actions/chainload.rs b/crates/boot/src/actions/chainload.rs index 87cf877..5af8c2c 100644 --- a/crates/boot/src/actions/chainload.rs +++ b/crates/boot/src/actions/chainload.rs @@ -1,16 +1,19 @@ use crate::context::SproutContext; use crate::phases::before_handoff; use alloc::boxed::Box; +use alloc::format; use alloc::rc::Rc; use alloc::vec::Vec; use anyhow::{Context, Result, bail}; use edera_sprout_config::actions::chainload::ChainloadConfiguration; use edera_sprout_parsing::{append_initrd, combine_options, empty_is_none}; use eficore::bootloader_interface::BootloaderInterface; +use eficore::devicetree::DeviceTree; use eficore::loader::source::ImageSource; use eficore::loader::{ImageLoadRequest, ImageLoader}; use eficore::media_loader::MediaLoaderHandle; use eficore::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID; +use eficore::secure::SecureBoot; use log::warn; use uefi::proto::loaded_image::LoadedImage; use uefi::{CString16, Handle}; @@ -18,19 +21,31 @@ use uefi::{CString16, Handle}; /// Read the initrd at the stamped `path` relative to the sprout image. /// Provides [None] if the path refers to the root of a filesystem rather than a file. pub fn read_initrd(context: &Rc, path: &str) -> Result>> { - let resolved = eficore::path::resolve_path(Some(context.root().loaded_image_path()?), path) - .context("unable to resolve initrd path")?; + read_optional_file(context, path, "initrd") +} - // A path without a file component refers to the root of the filesystem, not an initrd. +/// Read the `what` at the stamped `path` relative to the sprout image. +/// Provides [None] if the path refers to the root of a filesystem rather than a file. +fn read_optional_file( + context: &Rc, + path: &str, + what: &str, +) -> Result>> { + let resolved = eficore::path::resolve_path(Some(context.root().loaded_image_path()?), path) + .with_context(|| format!("unable to resolve {} path", what))?; + + // A path without a file component refers to the root of the filesystem, not a file. // This happens when a path template like "$root\\$initrd-0" is stamped with an empty - // initrd value, such as a BLS entry without an initrd or an unused BLS initrd slot. + // value, such as a BLS entry without an initrd or an unused BLS initrd slot. let subpath = eficore::path::device_path_subpath(&resolved.full_path) - .context("unable to get initrd subpath")?; + .with_context(|| format!("unable to get {} subpath", what))?; if subpath.trim_matches('\\').is_empty() { return Ok(None); } - let content = resolved.read_file().context("unable to read initrd")?; + let content = resolved + .read_file() + .with_context(|| format!("unable to read {}", what))?; Ok(Some(content)) } @@ -143,6 +158,24 @@ pub fn chainload(context: Rc, configuration: &ChainloadConfigurat initrd_handle = Some(handle); } + // Install the devicetree, if there is one, for the image to find. It is restored when it is + // dropped, which is once the image has returned or failed to start. A devicetree is not + // verified, so it is not used when Secure Boot is enabled, or when that can't be told. + let mut devicetree = None; + if let Some(path) = empty_is_none( + configuration + .devicetree + .as_ref() + .map(|path| context.stamp(path)), + ) { + if SecureBoot::enabled().unwrap_or(true) { + warn!("ignoring the devicetree, as Secure Boot is enabled"); + } else if let Some(content) = read_optional_file(&context, &path, "devicetree")? { + devicetree = + Some(DeviceTree::install(&content).context("unable to install the devicetree")?); + } + } + // Mark execution of an entry in the bootloader interface. // This is only informational, so it should not prevent booting. if let Err(error) = BootloaderInterface::mark_exec(context.root().timer()) { @@ -175,6 +208,9 @@ pub fn chainload(context: Rc, configuration: &ChainloadConfigurat // Explicitly drop the initrd handle to clarify when it should be unregistered. drop(initrd_handle); + // Explicitly drop the devicetree to clarify when the original is restored. + drop(devicetree); + // Return control to sprout. Ok(()) } diff --git a/crates/boot/src/actions/edera.rs b/crates/boot/src/actions/edera.rs index 641eb9d..0aad623 100644 --- a/crates/boot/src/actions/edera.rs +++ b/crates/boot/src/actions/edera.rs @@ -132,6 +132,7 @@ pub fn edera(context: Rc, configuration: &EderaConfiguration) -> options: vec![], linux_initrd: None, linux_initrd_chain: vec![], + devicetree: None, }, ) .context("unable to chainload to xen"); diff --git a/crates/boot/src/autoconfigure/bls.rs b/crates/boot/src/autoconfigure/bls.rs index d19ad52..25e1240 100644 --- a/crates/boot/src/autoconfigure/bls.rs +++ b/crates/boot/src/autoconfigure/bls.rs @@ -125,6 +125,8 @@ pub fn scan( linux_initrd_chain: (0..BLS_INITRD_SLOTS) .map(|slot| format!("{}\\$initrd-{}", root, slot)) .collect(), + // An unset devicetree stamps to the root of the filesystem, which the action skips. + devicetree: Some(format!("{}\\$devicetree", root)), }; // Insert the chainload action into the configuration. diff --git a/crates/boot/src/autoconfigure/linux.rs b/crates/boot/src/autoconfigure/linux.rs index 3b5c7f0..efdbad1 100644 --- a/crates/boot/src/autoconfigure/linux.rs +++ b/crates/boot/src/autoconfigure/linux.rs @@ -237,6 +237,7 @@ pub fn scan( options: vec!["$linux-options".to_string()], linux_initrd: Some("$initrd".to_string()), linux_initrd_chain: vec![], + devicetree: None, }; // Insert the chainload action into the configuration. diff --git a/crates/boot/src/generators/bls.rs b/crates/boot/src/generators/bls.rs index 1dad3b5..57ecd04 100644 --- a/crates/boot/src/generators/bls.rs +++ b/crates/boot/src/generators/bls.rs @@ -88,6 +88,7 @@ fn bootable_entry( context.set("options", options); // The command line and kernel version embedded in a unified kernel image. context.set("cmdline", entry.cmdline.clone().unwrap_or_default()); + context.set("devicetree", entry.devicetree_path().unwrap_or_default()); context.set("uname", entry.uname.clone().unwrap_or_default()); // The initrd value keeps the last initrd, which is what it held before // multiple initrds were supported. diff --git a/crates/config/src/actions/chainload.rs b/crates/config/src/actions/chainload.rs index f4984a0..70d89af 100644 --- a/crates/config/src/actions/chainload.rs +++ b/crates/config/src/actions/chainload.rs @@ -23,4 +23,9 @@ pub struct ChainloadConfiguration { /// using the same mechanism as `linux-initrd`. This cannot be used with `linux-initrd`. #[serde(default, rename = "linux-initrd-chain")] pub linux_initrd_chain: Vec, + /// An optional path to a flattened devicetree to give to the image. + /// It is installed as the devicetree of the machine until the image returns. It is not + /// used when Secure Boot is enabled, as it can't be verified. + #[serde(default)] + pub devicetree: Option, } diff --git a/crates/eficore/src/bootloader_interface.rs b/crates/eficore/src/bootloader_interface.rs index c776105..02f9630 100644 --- a/crates/eficore/src/bootloader_interface.rs +++ b/crates/eficore/src/bootloader_interface.rs @@ -67,6 +67,7 @@ impl BootloaderInterface { | LoaderFeatures::SavedEntry | LoaderFeatures::EntryPreferred | LoaderFeatures::SortKey + | LoaderFeatures::DeviceTree } /// Tell the system that Sprout was initialized at the current time. diff --git a/crates/eficore/src/devicetree.rs b/crates/eficore/src/devicetree.rs new file mode 100644 index 0000000..253eb57 --- /dev/null +++ b/crates/eficore/src/devicetree.rs @@ -0,0 +1,183 @@ +use crate::handle::find_handle; +use anyhow::{Context, Result, anyhow, bail}; +use core::ffi::c_void; +use core::ptr::{self, NonNull}; +use log::{info, warn}; +use uefi::boot::{AllocateType, MemoryType, PAGE_SIZE}; +use uefi::proto::unsafe_protocol; +use uefi_raw::{Guid, Status, guid}; + +/// The configuration table that holds the flattened devicetree. +const DTB_TABLE_GUID: Guid = guid!("b1b621d5-f19c-41a5-830b-d9152c69aae0"); + +/// The protocol that firmware such as U-Boot provides to patch a devicetree for the machine. +const DT_FIXUP_GUID: Guid = guid!("e617d64c-fe08-46da-f4dc-bbd5870c7300"); + +/// The magic number at the start of a flattened devicetree. +const FDT_MAGIC: u32 = 0xd00d_feed; + +/// The size of the fixed header of a flattened devicetree. +const FDT_HEADER_SIZE: usize = 7 * 4; + +/// The largest devicetree that is accepted. The Linux stub copies it into a smaller buffer. +const MAX_DTB_SIZE: usize = 32 * 1024 * 1024; + +/// Apply the fixups for the machine to the devicetree. +const DT_APPLY_FIXUPS: u32 = 0x1; + +/// Reserve the memory that the devicetree says to reserve. +const DT_RESERVE_MEMORY: u32 = 0x2; + +/// The protocol that patches a devicetree for the machine. +#[unsafe_protocol(DT_FIXUP_GUID)] +#[repr(C)] +struct DtFixupProtocol { + revision: u64, + fixup: unsafe extern "efiapi" fn( + this: *mut DtFixupProtocol, + fdt: *mut c_void, + buffer_size: *mut usize, + flags: u32, + ) -> Status, +} + +/// A devicetree that is installed for the image that is about to start. When this is dropped, +/// which is when the image returned or failed to start, the devicetree of the firmware is +/// put back and the memory is freed. +pub struct DeviceTree { + /// The pages that hold the devicetree. + pages: NonNull, + /// The number of pages. + count: usize, + /// The devicetree table of the firmware, which is null if there was none. + original: *const c_void, + /// Whether the table points to the pages. + installed: bool, +} + +impl DeviceTree { + /// Check that `dtb` looks like a flattened devicetree that can be installed. + fn validate(dtb: &[u8]) -> Result<()> { + if dtb.len() < FDT_HEADER_SIZE || dtb.len() > MAX_DTB_SIZE { + bail!("the devicetree is {} bytes, which is not valid", dtb.len()); + } + // The header is big endian: the magic number, then the total size. + let magic = u32::from_be_bytes([dtb[0], dtb[1], dtb[2], dtb[3]]); + let total = u32::from_be_bytes([dtb[4], dtb[5], dtb[6], dtb[7]]) as usize; + if magic != FDT_MAGIC { + bail!("the file is not a flattened devicetree"); + } + if total > dtb.len() { + bail!("the devicetree is truncated"); + } + Ok(()) + } + + /// Allocate `count` pages of memory that belongs to the firmware tables, as a devicetree + /// has to be in memory of the type that ACPI tables are in. + fn allocate(count: usize) -> Result> { + uefi::boot::allocate_pages(AllocateType::AnyPages, MemoryType::ACPI_RECLAIM, count) + .context("unable to allocate memory for the devicetree") + } + + /// Install `dtb` as the devicetree of the machine until the returned value is dropped. + pub fn install(dtb: &[u8]) -> Result { + Self::validate(dtb)?; + + // Remember the table of the firmware so it can be put back. + let original = uefi::system::with_config_table(|tables| { + tables + .iter() + .find(|entry| entry.guid == DTB_TABLE_GUID) + .map(|entry| entry.address) + .unwrap_or(ptr::null()) + }); + + let count = dtb.len().div_ceil(PAGE_SIZE); + let pages = Self::allocate(count)?; + // SAFETY: The pages are at least as large as the devicetree, and don't overlap it. + unsafe { ptr::copy_nonoverlapping(dtb.as_ptr(), pages.as_ptr(), dtb.len()) }; + let mut tree = Self { + pages, + count, + original, + installed: false, + }; + + tree.fixup(dtb)?; + + // SAFETY: The table points to pages that stay allocated until this is dropped, which + // puts the original table back before the pages are freed. + unsafe { + uefi::boot::install_configuration_table(&DTB_TABLE_GUID, tree.pages.as_ptr().cast()) + } + .context("unable to install the devicetree")?; + tree.installed = true; + + info!( + "installed the devicetree at {:p} ({} bytes)", + tree.pages.as_ptr(), + dtb.len() + ); + Ok(tree) + } + + /// Let the firmware patch the devicetree for the machine, if it can. + fn fixup(&mut self, dtb: &[u8]) -> Result<()> { + let Some(handle) = find_handle(&DT_FIXUP_GUID)? else { + return Ok(()); + }; + let mut protocol = uefi::boot::open_protocol_exclusive::(handle) + .context("unable to open the devicetree fixup protocol")?; + let fixup = protocol.fixup; + let this: *mut DtFixupProtocol = &mut *protocol; + let flags = DT_APPLY_FIXUPS | DT_RESERVE_MEMORY; + + // The firmware gets all the memory that was allocated. If that is not enough, it says + // how much it needs, and it is given a copy of the original in a larger allocation. + let mut size = self.count * PAGE_SIZE; + // SAFETY: The pages are valid for the size that is passed. + let mut status = unsafe { fixup(this, self.pages.as_ptr().cast(), &mut size, flags) }; + if status == Status::BUFFER_TOO_SMALL { + let count = size.div_ceil(PAGE_SIZE); + let pages = Self::allocate(count)?; + // SAFETY: The new pages are larger than the devicetree, and don't overlap it. + unsafe { ptr::copy_nonoverlapping(dtb.as_ptr(), pages.as_ptr(), dtb.len()) }; + // SAFETY: The old pages came from the same allocator and nothing refers to them yet. + if let Err(error) = unsafe { uefi::boot::free_pages(self.pages, self.count) } { + warn!("unable to free the devicetree memory: {}", error); + } + self.pages = pages; + self.count = count; + size = count * PAGE_SIZE; + // SAFETY: The new pages are valid for the size that is passed. + status = unsafe { fixup(this, self.pages.as_ptr().cast(), &mut size, flags) }; + } + if status != Status::SUCCESS { + return Err(anyhow!("the devicetree fixup failed: {:?}", status)); + } + Ok(()) + } +} + +impl Drop for DeviceTree { + fn drop(&mut self) { + if self.installed { + // SAFETY: This puts back the table that the firmware had, which is null if it had + // none, and which removes the table. + let result = + unsafe { uefi::boot::install_configuration_table(&DTB_TABLE_GUID, self.original) }; + if let Err(error) = result { + // The table can still point to the pages, so they must not be freed. + if !(error.status() == Status::NOT_FOUND && self.original.is_null()) { + warn!("unable to restore the devicetree: {}", error); + return; + } + } + } + // SAFETY: Nothing refers to the pages any more. + if let Err(error) = unsafe { uefi::boot::free_pages(self.pages, self.count) } { + warn!("unable to free the devicetree memory: {}", error); + } + } +} diff --git a/crates/eficore/src/lib.rs b/crates/eficore/src/lib.rs index de0ee96..af7b742 100644 --- a/crates/eficore/src/lib.rs +++ b/crates/eficore/src/lib.rs @@ -6,6 +6,9 @@ #![no_std] extern crate alloc; +/// Installing a devicetree for the image that is started. +pub mod devicetree; + /// EFI handle helpers. pub mod handle;