15 Commits

Author SHA1 Message Date
6f60a279c3 sprout: version 0.0.14 2025-10-28 01:47:15 -04:00
2e66d8c72e chore(docs): update readme with secure boot notes and roadmap items 2025-10-28 01:43:07 -04:00
86e08c2400 fix(doc/extractors/filesystem-device-match): the extractor will error if no criteria is provided 2025-10-28 00:19:38 -04:00
852823e2eb chore(doc/bls/entry): clarify why char::is_whitespace is used despite newline matching 2025-10-28 00:12:16 -04:00
734ab84054 chore(doc/context): clarify context finalization limit error message 2025-10-28 00:10:22 -04:00
c8a3408fdd fix(extractors/filesystem-device-match): clarify when to use fallback for empty criteria 2025-10-28 00:09:11 -04:00
deeda650a7 fix(autoconfigure/linux): remove debug line 2025-10-28 00:06:02 -04:00
268a2cb28b fix(media-loader): improve safety in the event protocol interface install fails 2025-10-27 23:56:12 -04:00
0b6523906d fix(doc): filesystem-device-match will not return a filesystem when criteria is not provided 2025-10-27 23:39:55 -04:00
3acd0ec7d8 chore(doc): document media loader safety 2025-10-27 23:24:35 -04:00
fe593efa8c chore(autoconfigure/docs): clarify why we append / to a device root 2025-10-27 23:15:14 -04:00
3058abab23 fix(menu): check for timeout duration overflow 2025-10-27 23:10:05 -04:00
5df717de6d chore(filesystem-device-match): extract partition uuid fetch to function 2025-10-27 23:05:57 -04:00
011e133455 chore(autoconfigure-linux): clarify variable shadowing for initramfs matching 2025-10-27 23:00:55 -04:00
ccd1a8f498 chore(menu): clarify that we do not need to free the key event 2025-10-27 22:59:00 -04:00
11 changed files with 131 additions and 68 deletions

2
Cargo.lock generated
View File

@@ -116,7 +116,7 @@ dependencies = [
[[package]] [[package]]
name = "edera-sprout" name = "edera-sprout"
version = "0.0.13" version = "0.0.14"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"image", "image",

View File

@@ -2,7 +2,7 @@
name = "edera-sprout" name = "edera-sprout"
description = "Modern UEFI bootloader" description = "Modern UEFI bootloader"
license = "Apache-2.0" license = "Apache-2.0"
version = "0.0.13" version = "0.0.14"
homepage = "https://sprout.edera.dev" homepage = "https://sprout.edera.dev"
repository = "https://github.com/edera-dev/sprout" repository = "https://github.com/edera-dev/sprout"
edition = "2024" edition = "2024"

View File

@@ -18,6 +18,9 @@ existing UEFI bootloader or booted by the hardware directly.
Sprout is licensed under Apache 2.0 and is open to modifications and contributions. Sprout is licensed under Apache 2.0 and is open to modifications and contributions.
**IMPORTANT WARNING**: Sprout does not support UEFI Secure Boot yet.
See [this issue](https://github.com/edera-dev/sprout/issues/20) for updates.
## Background ## Background
At [Edera] we make compute isolation technology for a wide variety of environments, often ones we do not fully control. At [Edera] we make compute isolation technology for a wide variety of environments, often ones we do not fully control.
@@ -55,7 +58,7 @@ The boot menu mechanism is very rudimentary.
### Current ### Current
- [x] Loadable driver support - [x] Loadable driver support
- [x] [Bootloader specification (BLS)](https://uapi-group.org/specifications/specs/boot_loader_specification/) support - [x] Basic [Bootloader specification (BLS)](https://uapi-group.org/specifications/specs/boot_loader_specification/) support
- [x] Chainload support - [x] Chainload support
- [x] Linux boot support via EFI stub - [x] Linux boot support via EFI stub
- [x] Windows boot support via chainload - [x] Windows boot support via chainload
@@ -65,15 +68,18 @@ The boot menu mechanism is very rudimentary.
### Roadmap ### Roadmap
- [ ] Full-featured boot menu - [ ] [Bootloader interface support](https://github.com/edera-dev/sprout/issues/21)
- [ ] Secure Boot support: work in progress - [ ] [BLS specification conformance](https://github.com/edera-dev/sprout/issues/2)
- [ ] UKI support: partial - [ ] [Full-featured boot menu](https://github.com/edera-dev/sprout/issues/1)
- [ ] multiboot2 support - [ ] [Secure Boot support](https://github.com/edera-dev/sprout/issues/20): work in progress
- [ ] Linux boot protocol (boot without EFI stub) - [ ] [UKI support](https://github.com/edera-dev/sprout/issues/6): partial
- [ ] [multiboot2 support](https://github.com/edera-dev/sprout/issues/7)
- [ ] [Linux boot protocol (boot without EFI stub)](https://github.com/edera-dev/sprout/issues/7)
## Concepts ## Concepts
- drivers: loadable EFI modules that can add functionality to the EFI system. - drivers: loadable EFI modules that can add functionality to the EFI system.
- autoconfiguration: code that can automatically generate sprout.toml based on the EFI environment.
- actions: executable code with a configuration that can be run by various other sprout concepts. - actions: executable code with a configuration that can be run by various other sprout concepts.
- generators: code that can generate boot entries based on inputs or runtime code. - generators: code that can generate boot entries based on inputs or runtime code.
- extractors: code that can extract values from the EFI environment. - extractors: code that can extract values from the EFI environment.

View File

@@ -31,7 +31,7 @@ pub fn scan(
.to_string(DisplayOnly(false), AllowShortcuts(false)) .to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")? .context("unable to convert device root to string")?
.to_string(); .to_string();
// Add a trailing slash to the root to ensure the path is valid. // Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/'); root.push('/');
// Generate a unique hash of the root path. // Generate a unique hash of the root path.

View File

@@ -6,7 +6,6 @@ use crate::generators::GeneratorDeclaration;
use crate::generators::list::ListConfiguration; use crate::generators::list::ListConfiguration;
use crate::utils; use crate::utils;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use log::info;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use uefi::CString16; use uefi::CString16;
use uefi::fs::{FileSystem, Path}; use uefi::fs::{FileSystem, Path};
@@ -89,7 +88,7 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
// Find a matching initramfs, if any. // Find a matching initramfs, if any.
let mut initramfs_prefix_iter = INITRAMFS_PREFIXES.iter(); let mut initramfs_prefix_iter = INITRAMFS_PREFIXES.iter();
let initramfs = loop { let matched_initramfs_path = loop {
let Some(prefix) = initramfs_prefix_iter.next() else { let Some(prefix) = initramfs_prefix_iter.next() else {
break None; break None;
}; };
@@ -112,7 +111,7 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
let mut kernel = path.clone(); let mut kernel = path.clone();
kernel.push(Path::new(&item.file_name())); kernel.push(Path::new(&item.file_name()));
let kernel = kernel.to_string(); let kernel = kernel.to_string();
let initramfs = initramfs.map(|initramfs| initramfs.to_string()); let initramfs = matched_initramfs_path.map(|initramfs_path| initramfs_path.to_string());
// Produce a kernel pair. // Produce a kernel pair.
let pair = KernelPair { kernel, initramfs }; let pair = KernelPair { kernel, initramfs };
@@ -135,7 +134,7 @@ pub fn scan(
.to_string(DisplayOnly(false), AllowShortcuts(false)) .to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")? .context("unable to convert device root to string")?
.to_string(); .to_string();
// Add a trailing slash to the root to ensure the path is valid. // Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/'); root.push('/');
// Generate a unique hash of the root path. // Generate a unique hash of the root path.
@@ -215,8 +214,6 @@ pub fn scan(
}, },
); );
info!("{:?}", config);
// We had a Linux kernel, so return true to indicate something was found. // We had a Linux kernel, so return true to indicate something was found.
Ok(true) Ok(true)
} }

View File

@@ -39,7 +39,7 @@ pub fn scan(
.to_string(DisplayOnly(false), AllowShortcuts(false)) .to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")? .context("unable to convert device root to string")?
.to_string(); .to_string();
// Add a trailing slash to the root to ensure the path is valid. // Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/'); root.push('/');
// Generate a unique hash of the root path. // Generate a unique hash of the root path.

View File

@@ -168,13 +168,13 @@ impl SproutContext {
let mut current_values = self.all_values(); let mut current_values = self.all_values();
// To ensure that there is no possible infinite loop, we need to check // To ensure that there is no possible infinite loop, we need to check
// the number of iterations. If it exceeds 100, we bail. // the number of iterations. If it exceeds CONTEXT_FINALIZE_ITERATION_LIMIT, we bail.
let mut iterations: usize = 0; let mut iterations: usize = 0;
loop { loop {
iterations += 1; iterations += 1;
if iterations > CONTEXT_FINALIZE_ITERATION_LIMIT { if iterations > CONTEXT_FINALIZE_ITERATION_LIMIT {
bail!("infinite loop detected in context finalization"); bail!("maximum number of replacement iterations reached while finalizing context");
} }
let mut did_change = false; let mut did_change = false;

View File

@@ -10,16 +10,17 @@ use uefi::proto::device_path::DevicePath;
use uefi::proto::media::file::{File, FileSystemVolumeLabel}; use uefi::proto::media::file::{File, FileSystemVolumeLabel};
use uefi::proto::media::fs::SimpleFileSystem; use uefi::proto::media::fs::SimpleFileSystem;
use uefi::proto::media::partition::PartitionInfo; use uefi::proto::media::partition::PartitionInfo;
use uefi::{CString16, Guid}; use uefi::{CString16, Guid, Handle};
use uefi_raw::Status; use uefi_raw::Status;
/// The filesystem device match extractor. /// The filesystem device match extractor.
/// This extractor finds a filesystem using some search criteria and returns /// This extractor finds a filesystem using some search criteria and returns
/// the device root path that can concatenated with subpaths to access files /// the device root path that can concatenated with subpaths to access files
/// on a particular filesystem. /// on a particular filesystem.
/// The fallback value can be used to provide a value if no match is found.
/// ///
/// This function only requires all the criteria to match. /// This extractor requires all the criteria to match. If no criteria is provided,
/// The fallback value can be used to provide a value if none is found. /// an error is returned.
#[derive(Serialize, Deserialize, Debug, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct FilesystemDeviceMatchExtractor { pub struct FilesystemDeviceMatchExtractor {
/// Matches a filesystem that has the specified label. /// Matches a filesystem that has the specified label.
@@ -40,6 +41,48 @@ pub struct FilesystemDeviceMatchExtractor {
pub fallback: Option<String>, pub fallback: Option<String>,
} }
/// Represents the partition UUIDs for a filesystem.
struct PartitionIds {
/// The UUID of the partition.
partition_uuid: Guid,
/// The type UUID of the partition.
type_uuid: Guid,
}
/// Fetches the partition UUIDs for the specified filesystem handle.
fn fetch_partition_uuids(handle: Handle) -> Result<Option<PartitionIds>> {
// Open the partition info protocol for this handle.
let partition_info = uefi::boot::open_protocol_exclusive::<PartitionInfo>(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;
Ok(Some(PartitionIds {
partition_uuid: uuid,
type_uuid: type_uuid.0,
}))
} else {
Ok(None)
}
}
Err(error) => {
// If the filesystem does not have a partition, that is okay.
if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED {
Ok(None)
} else {
// We should still handle other errors gracefully.
Err(error).context("unable to open filesystem partition info")?;
unreachable!()
}
}
}
}
/// Extract a filesystem device path using the specified `context` and `extractor` configuration. /// Extract a filesystem device path using the specified `context` and `extractor` configuration.
pub fn extract( pub fn extract(
context: Rc<SproutContext>, context: Rc<SproutContext>,
@@ -56,56 +99,28 @@ pub fn extract(
// Extract the partition info for this filesystem. // Extract the partition info for this filesystem.
// There is no guarantee that the filesystem has a partition. // There is no guarantee that the filesystem has a partition.
let partition_info = { let partition_info =
// Open the partition info protocol for this handle. fetch_partition_uuids(handle).context("unable to fetch partition info")?;
let partition_info = uefi::boot::open_protocol_exclusive::<PartitionInfo>(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;
Some((uuid, type_uuid.0))
} else {
None
}
}
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")?;
unreachable!()
}
}
}
};
// Check if the partition info matches partition uuid criteria. // Check if the partition info matches partition uuid criteria.
if let Some((partition_uuid, _partition_type_guid)) = partition_info if let Some(ref partition_info) = partition_info
&& let Some(ref has_partition_uuid) = extractor.has_partition_uuid && let Some(ref has_partition_uuid) = extractor.has_partition_uuid
{ {
let parsed_uuid = Guid::from_str(has_partition_uuid) let parsed_uuid = Guid::from_str(has_partition_uuid)
.map_err(|e| anyhow!("unable to parse has-partition-uuid: {}", e))?; .map_err(|e| anyhow!("unable to parse has-partition-uuid: {}", e))?;
if partition_uuid != parsed_uuid { if partition_info.partition_uuid != parsed_uuid {
continue; continue;
} }
has_match = true; has_match = true;
} }
// Check if the partition info matches partition type uuid criteria. // Check if the partition info matches partition type uuid criteria.
if let Some((_partition_uuid, partition_type_guid)) = partition_info if let Some(ref partition_info) = partition_info
&& let Some(ref has_partition_type_uuid) = extractor.has_partition_type_uuid && let Some(ref has_partition_type_uuid) = extractor.has_partition_type_uuid
{ {
let parsed_uuid = Guid::from_str(has_partition_type_uuid) let parsed_uuid = Guid::from_str(has_partition_type_uuid)
.map_err(|e| anyhow!("unable to parse has-partition-type-uuid: {}", e))?; .map_err(|e| anyhow!("unable to parse has-partition-type-uuid: {}", e))?;
if partition_type_guid != parsed_uuid { if partition_info.type_uuid != parsed_uuid {
continue; continue;
} }
has_match = true; has_match = true;

View File

@@ -41,7 +41,8 @@ impl FromStr for BlsEntry {
continue; continue;
} }
// Split the line once by whitespace. // Split the line once by whitespace. This technically includes newlines but since
// the lines iterator is used, there should never be a newline here.
let Some((key, value)) = line.split_once(char::is_whitespace) else { let Some((key, value)) = line.split_once(char::is_whitespace) else {
continue; continue;
}; };

View File

@@ -40,8 +40,17 @@ fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
uefi::boot::create_event_ex(EventType::TIMER, Tpl::CALLBACK, None, None, None) uefi::boot::create_event_ex(EventType::TIMER, Tpl::CALLBACK, None, None, None)
.context("unable to create timer event")? .context("unable to create timer event")?
}; };
// The timeout is in increments of 100 nanoseconds. // The timeout is in increments of 100 nanoseconds.
let trigger = TimerTrigger::Relative(timeout.as_nanos() as u64 / 100); let timeout_hundred_nanos = timeout.as_nanos() / 100;
// Check if the timeout is too large to fit into an u64.
if timeout_hundred_nanos > u64::MAX as u128 {
bail!("timeout duration overflow");
}
// Set a timer to trigger after the specified duration.
let trigger = TimerTrigger::Relative(timeout_hundred_nanos as u64);
uefi::boot::set_timer(&timer_event, trigger).context("unable to set timeout timer")?; uefi::boot::set_timer(&timer_event, trigger).context("unable to set timeout timer")?;
let mut events = vec![timer_event, key_event]; let mut events = vec![timer_event, key_event];
@@ -50,6 +59,7 @@ fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
.context("unable to wait for event")?; .context("unable to wait for event")?;
// Close the timer event that we acquired. // Close the timer event that we acquired.
// We don't need to close the key event because it is owned globally.
if let Some(timer_event) = events.into_iter().next() { if let Some(timer_event) = events.into_iter().next() {
uefi::boot::close_event(timer_event).context("unable to close timer event")?; uefi::boot::close_event(timer_event).context("unable to close timer event")?;
} }

View File

@@ -51,6 +51,11 @@ impl MediaLoaderHandle {
/// The next call will pass a buffer of the right size, and we should copy /// The next call will pass a buffer of the right size, and we should copy
/// data into that buffer, checking whether it is safe to copy based on /// data into that buffer, checking whether it is safe to copy based on
/// the buffer size. /// the buffer size.
///
/// SAFETY: `this.address` and `this.length` are set by leaking a Box<[u8]>, so we can
/// be sure their pointers are valid when this is called. The caller must call this function
/// while inside UEFI boot services to ensure pointers are valid. Copying to `buffer` is
/// assumed valid because the caller must ensure `buffer` is valid by function contract.
unsafe extern "efiapi" fn load_file( unsafe extern "efiapi" fn load_file(
this: *mut MediaLoaderProtocol, this: *mut MediaLoaderProtocol,
file_path: *const DevicePathProtocol, file_path: *const DevicePathProtocol,
@@ -155,7 +160,7 @@ impl MediaLoaderHandle {
// Install a protocol interface for the device path. // Install a protocol interface for the device path.
// This ensures it can be located by other EFI programs. // This ensures it can be located by other EFI programs.
let mut handle = unsafe { let primary_handle = unsafe {
uefi::boot::install_protocol_interface( uefi::boot::install_protocol_interface(
None, None,
&DevicePathProtocol::GUID, &DevicePathProtocol::GUID,
@@ -178,25 +183,54 @@ impl MediaLoaderHandle {
let protocol = Box::leak(protocol); let protocol = Box::leak(protocol);
// Install a protocol interface for the load file protocol for the media loader protocol. // Install a protocol interface for the load file protocol for the media loader protocol.
handle = unsafe { let secondary_handle = unsafe {
uefi::boot::install_protocol_interface( uefi::boot::install_protocol_interface(
Some(handle), Some(primary_handle),
&LoadFile2Protocol::GUID, &LoadFile2Protocol::GUID,
protocol as *mut _ as *mut c_void, // The UEFI API expects an opaque pointer here.
protocol as *mut MediaLoaderProtocol as *mut c_void,
) )
} };
.context("unable to install media loader load file handle")?;
// Check if the media loader is registered. // If installing the second protocol interface failed, we need to clean up after ourselves.
// If it is not, we can't continue safely because something went wrong. if secondary_handle.is_err() {
if !Self::already_registered(guid)? { // Uninstall the protocol interface for the device path protocol.
bail!("media loader not registered when expected to be registered"); // SAFETY: If we have reached this point, we know that the protocol is registered.
// If this fails, we have no choice but to leak memory. The error will be shown
// to the user, so at least they can see it. In most cases, catching this error
// will exit, so leaking is safe.
unsafe {
uefi::boot::uninstall_protocol_interface(
primary_handle,
&DevicePathProtocol::GUID,
path.as_ffi_ptr() as *mut c_void,
)
.context(
"unable to uninstall media loader device path handle, this will leak memory",
)?;
}
// SAFETY: We know that the protocol is leaked, so we can safely take a reference to it.
let protocol = unsafe { Box::from_raw(protocol) };
// SAFETY: We know that the data is leaked, so we can safely take a reference to it.
let data = unsafe { Box::from_raw(data) };
// SAFETY: We know that the path is leaked, so we can safely take a reference to it.
let path = unsafe { Box::from_raw(path) };
// Drop all the allocations explicitly to clarify the lifetime.
drop(protocol);
drop(data);
drop(path);
} }
// If installing the second protocol interface failed, this will return the error.
// We should have already cleaned up after ourselves, so this is safe.
secondary_handle.context("unable to install media loader load file handle")?;
// Return a handle to the media loader. // Return a handle to the media loader.
Ok(Self { Ok(Self {
guid, guid,
handle, handle: primary_handle,
protocol, protocol,
path, path,
}) })