mirror of
https://github.com/edera-dev/sprout.git
synced 2026-11-04 16:42:20 +00:00
feat(boot): install the devicetree of a BLS entry
Add a devicetree option to the chainload action, set from the devicetree key of a BLS entry. The file is installed as the EFI devicetree table with the memory type that a devicetree has to be in, patched by the fixup protocol when the firmware has one, and the table of the firmware is put back when the image returns or fails to start. It is ignored when Secure Boot is enabled, as it can't be verified, and the DeviceTree loader feature is advertised.
This commit is contained in:
1 parent
ef14e5bd98
commit
ac4ec69c02
9 files changed
+239
-6
No files matched your search
@@ -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<SproutContext>, path: &str) -> Result<Option<Vec<u8>>> {
|
||||
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<SproutContext>,
|
||||
path: &str,
|
||||
what: &str,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
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<SproutContext>, 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<SproutContext>, 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(())
|
||||
}
|
||||
@@ -132,6 +132,7 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
|
||||
options: vec![],
|
||||
linux_initrd: None,
|
||||
linux_initrd_chain: vec![],
|
||||
devicetree: None,
|
||||
},
|
||||
)
|
||||
.context("unable to chainload to xen");
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<String>,
|
||||
/// 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<String>,
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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<u8>,
|
||||
/// 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<NonNull<u8>> {
|
||||
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> {
|
||||
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::<DtFixupProtocol>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user