From 20932695e3772b91cbfa659407be71c8c8a9cadb Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Thu, 30 Oct 2025 18:57:26 -0400 Subject: [PATCH] feat(safety): bail if secure boot is enabled early --- src/integrations/bootloader_interface.rs | 88 ++++++++++++---------- src/main.rs | 9 +++ src/secure.rs | 14 ++++ src/utils.rs | 3 + src/utils/variables.rs | 93 ++++++++++++++++++++++++ 5 files changed, 170 insertions(+), 37 deletions(-) create mode 100644 src/secure.rs create mode 100644 src/utils/variables.rs diff --git a/src/integrations/bootloader_interface.rs b/src/integrations/bootloader_interface.rs index d1fd77a..5cbca63 100644 --- a/src/integrations/bootloader_interface.rs +++ b/src/integrations/bootloader_interface.rs @@ -1,9 +1,10 @@ use crate::platform::timer::PlatformTimer; use crate::utils::device_path_subpath; +use crate::utils::variables::{VariableClass, VariableController}; use anyhow::{Context, Result}; use uefi::proto::device_path::DevicePath; -use uefi::{CString16, Guid, guid}; -use uefi_raw::table::runtime::{VariableAttributes, VariableVendor}; +use uefi::{Guid, guid}; +use uefi_raw::table::runtime::VariableVendor; /// The name of the bootloader to tell the system. const LOADER_NAME: &str = "Sprout"; @@ -13,7 +14,9 @@ pub struct BootloaderInterface; impl BootloaderInterface { /// Bootloader Interface GUID from https://systemd.io/BOOT_LOADER_INTERFACE - const VENDOR: VariableVendor = VariableVendor(guid!("4a67b082-0a4c-41cf-b6c7-440b29bb8c4f")); + const VENDOR: VariableController = VariableController::new(VariableVendor(guid!( + "4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" + ))); /// Tell the system that Sprout was initialized at the current time. pub fn mark_init(timer: &PlatformTimer) -> Result<()> { @@ -35,23 +38,39 @@ impl BootloaderInterface { fn mark_time(key: &str, timer: &PlatformTimer) -> Result<()> { // Measure the elapsed time since the hardware timer was started. let elapsed = timer.elapsed_since_lifetime(); - Self::set_cstr16(key, &elapsed.as_micros().to_string()) + Self::VENDOR.set_cstr16( + key, + &elapsed.as_micros().to_string(), + VariableClass::BootAndRuntimeTemporary, + ) } /// Tell the system what loader is being used. pub fn set_loader_info() -> Result<()> { - Self::set_cstr16("LoaderInfo", LOADER_NAME) + Self::VENDOR.set_cstr16( + "LoaderInfo", + LOADER_NAME, + VariableClass::BootAndRuntimeTemporary, + ) } /// Tell the system the relative path to the partition root of the current bootloader. pub fn set_loader_path(path: &DevicePath) -> Result<()> { let subpath = device_path_subpath(path).context("unable to get loader path subpath")?; - Self::set_cstr16("LoaderImageIdentifier", &subpath) + Self::VENDOR.set_cstr16( + "LoaderImageIdentifier", + &subpath, + VariableClass::BootAndRuntimeTemporary, + ) } /// Tell the system what the partition GUID of the ESP Sprout was booted from is. pub fn set_partition_guid(guid: &Guid) -> Result<()> { - Self::set_cstr16("LoaderDevicePartUUID", &guid.to_string()) + Self::VENDOR.set_cstr16( + "LoaderDevicePartUUID", + &guid.to_string(), + VariableClass::BootAndRuntimeTemporary, + ) } /// Tell the system what boot entries are available. @@ -69,17 +88,29 @@ impl BootloaderInterface { // Write the bytes (including the null terminator) into the data buffer. data.extend_from_slice(&encoded); } - Self::set("LoaderEntries", &data) + Self::VENDOR.set( + "LoaderEntries", + &data, + VariableClass::BootAndRuntimeTemporary, + ) } /// Tell the system what the default boot entry is. pub fn set_default_entry(entry: String) -> Result<()> { - Self::set_cstr16("LoaderEntryDefault", &entry) + Self::VENDOR.set_cstr16( + "LoaderEntryDefault", + &entry, + VariableClass::BootAndRuntimeTemporary, + ) } /// Tell the system what the selected boot entry is. pub fn set_selected_entry(entry: String) -> Result<()> { - Self::set_cstr16("LoaderEntrySelected", &entry) + Self::VENDOR.set_cstr16( + "LoaderEntrySelected", + &entry, + VariableClass::BootAndRuntimeTemporary, + ) } /// Tell the system about the UEFI firmware we are running on. @@ -91,35 +122,18 @@ impl BootloaderInterface { uefi::system::firmware_revision() >> 16, uefi::system::firmware_revision() & 0xFFFFF, ); - Self::set_cstr16("LoaderFirmwareInfo", &firmware_info)?; + Self::VENDOR.set_cstr16( + "LoaderFirmwareInfo", + &firmware_info, + VariableClass::BootAndRuntimeTemporary, + )?; // Format the firmware revision into something human-readable. let firmware_type = format!("UEFI {:02}", uefi::system::firmware_revision()); - Self::set_cstr16("LoaderFirmwareType", &firmware_type) - } - - /// The [VariableAttributes] for bootloader interface variables. - fn attributes() -> VariableAttributes { - VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS - } - - /// Set a bootloader interface variable specified by `key` to `value`. - fn set(key: &str, value: &[u8]) -> Result<()> { - let name = - CString16::try_from(key).context("unable to convert variable name to CString16")?; - uefi::runtime::set_variable(&name, &Self::VENDOR, Self::attributes(), value) - .with_context(|| format!("unable to set efi variable {}", key))?; - Ok(()) - } - - /// Set a bootloader interface variable specified by `key` to `value`, converting the value to - /// a [CString16]. - fn set_cstr16(key: &str, value: &str) -> Result<()> { - // Encode the value as a CString16 little endian. - let encoded = value - .encode_utf16() - .flat_map(|c| c.to_le_bytes()) - .collect::>(); - Self::set(key, &encoded) + Self::VENDOR.set_cstr16( + "LoaderFirmwareType", + &firmware_type, + VariableClass::BootAndRuntimeTemporary, + ) } } diff --git a/src/main.rs b/src/main.rs index 7e7daf4..0aa27ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use crate::options::SproutOptions; use crate::options::parser::OptionsRepresentable; use crate::phases::phase; use crate::platform::timer::PlatformTimer; +use crate::secure::SecureBoot; use crate::utils::PartitionGuidForm; use anyhow::{Context, Result, bail}; use log::{error, info}; @@ -57,6 +58,9 @@ pub mod integrations; /// phases: Hooks into specific parts of the boot process. pub mod phases; +/// secure: Secure Boot support. +pub mod secure; + /// setup: Code that initializes the UEFI environment for Sprout. pub mod setup; @@ -68,6 +72,11 @@ pub mod utils; /// Run Sprout, returning an error if one occurs. fn run() -> Result<()> { + // For safety reasons, we will bail early if Secure Boot is enabled. + if SecureBoot::enabled().context("unable to determine Secure Boot status")? { + bail!("Secure Boot is enabled. Sprout does not currently support Secure Boot."); + } + // Start the platform timer. let timer = PlatformTimer::start(); diff --git a/src/secure.rs b/src/secure.rs new file mode 100644 index 0000000..a28e227 --- /dev/null +++ b/src/secure.rs @@ -0,0 +1,14 @@ +use crate::utils::variables::VariableController; +use anyhow::Result; + +/// Secure boot services. +pub struct SecureBoot; + +impl SecureBoot { + /// Checks if Secure Boot is enabled on the system. + /// This might fail if retrieving the variable fails in an irrecoverable way. + pub fn enabled() -> Result { + // The SecureBoot variable will tell us whether Secure Boot is enabled at all. + VariableController::GLOBAL.get_bool("SecureBoot") + } +} diff --git a/src/utils.rs b/src/utils.rs index 936a508..526d61c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -14,6 +14,9 @@ pub mod framebuffer; /// Support code for the media loader protocol. pub mod media_loader; +/// Support code for EFI variables. +pub mod variables; + /// Parses the input `path` as a [DevicePath]. /// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol. pub fn text_to_device_path(path: &str) -> Result { diff --git a/src/utils/variables.rs b/src/utils/variables.rs new file mode 100644 index 0000000..06f171c --- /dev/null +++ b/src/utils/variables.rs @@ -0,0 +1,93 @@ +use anyhow::{Context, Result}; +use uefi::{CString16, guid}; +use uefi_raw::Status; +use uefi_raw::table::runtime::{VariableAttributes, VariableVendor}; + +/// The classification of a variable. +/// This is an abstraction over various variable attributes. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum VariableClass { + /// The variable is available in Boot Services and Runtime Services and is not persistent. + BootAndRuntimeTemporary, +} + +impl VariableClass { + /// The [VariableAttributes] for this classification. + fn attributes(&self) -> VariableAttributes { + match self { + VariableClass::BootAndRuntimeTemporary => { + VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS + } + } + } +} + +/// Provides access to a particular set of vendor variables. +pub struct VariableController { + /// The GUID of the vendor. + vendor: VariableVendor, +} + +impl VariableController { + /// Global variables. + pub const GLOBAL: VariableController = VariableController::new(VariableVendor(guid!( + "8be4df61-93ca-11d2-aa0d-00e098032b8c" + ))); + + /// Create a new [VariableController] for the `vendor`. + pub const fn new(vendor: VariableVendor) -> Self { + Self { vendor } + } + + /// Convert `key` to a variable name as a CString16. + fn name(key: &str) -> Result { + CString16::try_from(key).context("unable to convert variable name to CString16") + } + + /// Retrieve a boolean value specified by the `key`. + pub fn get_bool(&self, key: &str) -> Result { + let name = Self::name(key)?; + + // Retrieve the variable data, handling variable not existing as false. + match uefi::runtime::get_variable_boxed(&name, &self.vendor) { + Ok((data, _)) => { + // If the variable is zero-length, we treat it as false. + if data.is_empty() { + Ok(false) + } else { + // We treat the variable as true if the first byte is non-zero. + Ok(data[0] > 0) + } + } + + Err(error) => { + // If the variable does not exist, we treat it as false. + if error.status() == Status::NOT_FOUND { + Ok(false) + } else { + Err(error).with_context(|| format!("unable to get efi variable {}", key)) + } + } + } + } + + /// Set a variable specified by `key` to `value`. + /// The variable `class` controls the attributes for the variable. + pub fn set(&self, key: &str, value: &[u8], class: VariableClass) -> Result<()> { + let name = Self::name(key)?; + uefi::runtime::set_variable(&name, &self.vendor, class.attributes(), value) + .with_context(|| format!("unable to set efi variable {}", key))?; + Ok(()) + } + + /// Set a variable specified by `key` to `value`, converting the value to + /// a [CString16]. The variable `class` controls the attributes for the variable. + pub fn set_cstr16(&self, key: &str, value: &str, class: VariableClass) -> Result<()> { + // Encode the value as a CString16 little endian. + let encoded = value + .encode_utf16() + .flat_map(|c| c.to_le_bytes()) + .collect::>(); + self.set(key, &encoded, class) + } +}