mirror of
https://github.com/edera-dev/sprout.git
synced 2026-11-04 16:32:20 +00:00
Only warn about a devicetree under Secure Boot when the entry has one, skip the fixup if its protocol can't be opened, and reject a devicetree whose size is smaller than its header. Announce a reboot before the delay, advertise the multi-profile UKI feature, and treat a Type 1 entry with a profile after the first as an extra profile that is not picked as the fallback default. A failure to read Sprout's own partition no longer drops the entries of the XBOOTLDR partition, and the README lists every initrd slot and describes the reboot behaviour more exactly.
193 lines
7.7 KiB
Rust
193 lines
7.7 KiB
Rust
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 < FDT_HEADER_SIZE || total > dtb.len() {
|
|
bail!("the devicetree has an invalid size");
|
|
}
|
|
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(());
|
|
};
|
|
// Without the protocol the devicetree is used as it is, as in systemd-boot.
|
|
let Ok(mut protocol) = crate::handle::open_shared::<DtFixupProtocol>(handle) else {
|
|
warn!("unable to open the devicetree fixup protocol, skipping the fixup");
|
|
return Ok(());
|
|
};
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
if self.installed {
|
|
info!(
|
|
"restored the devicetree table of the firmware ({:p})",
|
|
self.original
|
|
);
|
|
}
|
|
// 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);
|
|
}
|
|
}
|
|
}
|