diff --git a/Cargo.lock b/Cargo.lock index 085b538f5..b787bfcc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -901,6 +901,7 @@ dependencies = [ "log", "qemu-exit", "uefi", + "uefi-raw", ] [[package]] diff --git a/uefi-test-runner/Cargo.toml b/uefi-test-runner/Cargo.toml index be314c930..f52406776 100644 --- a/uefi-test-runner/Cargo.toml +++ b/uefi-test-runner/Cargo.toml @@ -6,6 +6,7 @@ publish = false edition = "2021" [dependencies] +uefi-raw = { path = "../uefi-raw" } uefi = { path = "../uefi", features = ["alloc", "global_allocator", "panic_handler", "logger", "qemu"] } log.workspace = true diff --git a/uefi-test-runner/src/proto/load.rs b/uefi-test-runner/src/proto/load.rs new file mode 100644 index 000000000..1a694d31c --- /dev/null +++ b/uefi-test-runner/src/proto/load.rs @@ -0,0 +1,144 @@ +use alloc::boxed::Box; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::ffi::c_void; +use core::pin::Pin; +use core::ptr; +use core::ptr::addr_of; +use uefi::prelude::BootServices; +use uefi::proto::device_path::build::DevicePathBuilder; +use uefi::proto::media::load_file::{LoadFile, LoadFile2}; +use uefi::proto::BootPolicy; +use uefi::{Guid, Handle}; +use uefi_raw::protocol::device_path::DevicePathProtocol; +use uefi_raw::protocol::media::{LoadFile2Protocol, LoadFileProtocol}; +use uefi_raw::Status; + +unsafe extern "efiapi" fn raw_load_file( + this: *mut LoadFile2Protocol, + _file_path: *const DevicePathProtocol, + _boot_policy: bool, + buffer_size: *mut usize, + buffer: *mut c_void, +) -> Status { + log::debug!("Called static extern \"efiapi\" `raw_load_file` glue function"); + let this = this.cast::().as_ref().unwrap(); + this.load_file(buffer_size, buffer.cast()) +} + +#[repr(C)] +struct CustomLoadFile2Protocol { + inner: LoadFile2Protocol, + file_data: Vec, +} + +impl CustomLoadFile2Protocol { + fn new(file_data: Vec) -> Pin> { + let inner = Self { + inner: LoadFile2Protocol { + load_file: raw_load_file, + }, + file_data, + }; + Box::pin(inner) + } + + fn load_file(&self, buf_len: *mut usize, buf: *mut c_void) -> Status { + if buf.is_null() || unsafe { *buf_len } < self.file_data.len() { + log::debug!("Returning buffer size"); + unsafe { *buf_len = self.file_data.len() }; + Status::BUFFER_TOO_SMALL + } else { + log::debug!("Writing file content to buffer"); + unsafe { + ptr::copy_nonoverlapping(self.file_data.as_ptr(), buf.cast(), self.file_data.len()); + } + Status::SUCCESS + } + } +} + +unsafe fn install_protocol( + bt: &BootServices, + handle: Handle, + guid: Guid, + protocol: &mut CustomLoadFile2Protocol, +) { + bt.install_protocol_interface(Some(handle), &guid, addr_of!(*protocol).cast()) + .unwrap(); +} + +unsafe fn uninstall_protocol( + bt: &BootServices, + handle: Handle, + guid: Guid, + protocol: &mut CustomLoadFile2Protocol, +) { + bt.uninstall_protocol_interface(handle, &guid, addr_of!(*protocol).cast()) + .unwrap(); +} + +/// This tests the LoadFile and LoadFile2 protocols. As this protocol is not +/// implemented in OVMF for the default handle, we implement it manually using +/// `install_protocol_interface`. Then, we load a file from our custom installed +/// protocol leveraging our protocol abstraction. +/// +/// The way we are implementing the LoadFile(2) protocol is roughly what certain +/// Linux loaders do so that Linux can find its initrd [0, 1]. +/// +/// [0] https://github.com/u-boot/u-boot/commit/ec80b4735a593961fe701cc3a5d717d4739b0fd0#diff-1f940face4d1cf74f9d2324952759404d01ee0a81612b68afdcba6b49803bdbbR171 +/// [1] https://github.com/torvalds/linux/blob/ee9a43b7cfe2d8a3520335fea7d8ce71b8cabd9d/drivers/firmware/efi/libstub/efi-stub-helper.c#L550 +pub fn test(bt: &BootServices) { + let image = bt.image_handle(); + + let load_data_msg = "Example file content."; + let load_data = load_data_msg.to_string().into_bytes(); + let mut proto_load_file = CustomLoadFile2Protocol::new(load_data); + // Get the ptr to the inner value, not the wrapping smart pointer type. + let proto_load_file_ptr = proto_load_file.as_mut().get_mut(); + + // Install our custom protocol implementation as LoadFile and LoadFile2 + // protocol. + unsafe { + install_protocol(bt, image, LoadFileProtocol::GUID, proto_load_file_ptr); + install_protocol(bt, image, LoadFile2Protocol::GUID, proto_load_file_ptr); + } + + let mut dvp_vec = Vec::new(); + let dummy_dvp = DevicePathBuilder::with_vec(&mut dvp_vec); + let dummy_dvp = dummy_dvp.finalize().unwrap(); + + let mut load_file_protocol = bt.open_protocol_exclusive::(image).unwrap(); + let loadfile_file = load_file_protocol + .load_file(dummy_dvp, BootPolicy::BootSelection) + .unwrap(); + let loadfile_file_string = String::from_utf8(loadfile_file.to_vec()).unwrap(); + + let mut load_file2_protocol = bt.open_protocol_exclusive::(image).unwrap(); + let loadfile2_file = load_file2_protocol.load_file(dummy_dvp).unwrap(); + let loadfile2_file_string = String::from_utf8(loadfile2_file.to_vec()).unwrap(); + + assert_eq!(load_data_msg, &loadfile_file_string); + assert_eq!(load_data_msg, &loadfile2_file_string); + + // Cleanup: Uninstall protocols again. + drop(load_file_protocol); + drop(load_file2_protocol); + unsafe { + uninstall_protocol(bt, image, LoadFileProtocol::GUID, proto_load_file_ptr); + uninstall_protocol(bt, image, LoadFile2Protocol::GUID, proto_load_file_ptr); + } + // Ensure protocols have been uninstalled: + assert_eq!( + bt.open_protocol_exclusive::(image) + .map(|_| ()) // make Result Eq'able + .map_err(|e| e.status()), + Err(Status::UNSUPPORTED) + ); + assert_eq!( + bt.open_protocol_exclusive::(image) + .map(|_| ()) // make Result Eq'able + .map_err(|e| e.status()), + Err(Status::UNSUPPORTED) + ); +} diff --git a/uefi-test-runner/src/proto/mod.rs b/uefi-test-runner/src/proto/mod.rs index 0c900d84c..09a9fd5ef 100644 --- a/uefi-test-runner/src/proto/mod.rs +++ b/uefi-test-runner/src/proto/mod.rs @@ -17,6 +17,7 @@ pub fn test(st: &mut SystemTable) { debug::test(bt); device_path::test(bt); driver::test(bt); + load::test(bt); loaded_image::test(bt); media::test(bt); network::test(bt); @@ -78,6 +79,7 @@ mod console; mod debug; mod device_path; mod driver; +mod load; mod loaded_image; mod media; mod misc; diff --git a/uefi/CHANGELOG.md b/uefi/CHANGELOG.md index 8eb093607..afd0113e2 100644 --- a/uefi/CHANGELOG.md +++ b/uefi/CHANGELOG.md @@ -20,6 +20,10 @@ details of the new `system`/`boot`/`runtime` modules, and upcoming deprecations. the implementations `MemoryMapRef`, `MemoryMapRefMut`, and `MemoryMapOwned`. This comes with some changes. Read below. We recommend to directly use the implementations instead of the traits. +- Added `LoadFile` and `LoadFile2` which abstracts over the `LOAD_FILE` and + `LOAD_FILE2` protocols. The UEFI test runner includes an integration test + that shows how Linux loaders can use this to implement the initrd loading + mechanism used in Linux. ## Changed - **Breaking:** `uefi::helpers::init` no longer takes an argument. diff --git a/uefi/src/proto/media/load_file.rs b/uefi/src/proto/media/load_file.rs new file mode 100644 index 000000000..6f5d74634 --- /dev/null +++ b/uefi/src/proto/media/load_file.rs @@ -0,0 +1,160 @@ +//! LoadFile and LoadFile2 protocols. + +use crate::proto::unsafe_protocol; +#[cfg(all(feature = "alloc", feature = "unstable"))] +use alloc::alloc::Global; +use uefi_raw::protocol::media::{LoadFile2Protocol, LoadFileProtocol}; +#[cfg(feature = "alloc")] +use { + crate::{mem::make_boxed, proto::device_path::DevicePath, Result, StatusExt}, + alloc::boxed::Box, + uefi::proto::BootPolicy, +}; + +/// Load File Protocol. +/// +/// Used to obtain files, that are primarily boot options, from arbitrary +/// devices. +/// +/// # UEFI Spec Description +/// The EFI_LOAD_FILE_PROTOCOL is a simple protocol used to obtain files from +/// arbitrary devices. +/// +/// When the firmware is attempting to load a file, it first attempts to use the +/// device’s Simple File System protocol to read the file. If the file system +/// protocol is found, the firmware implements the policy of interpreting the +/// File Path value of the file being loaded. If the device does not support the +/// file system protocol, the firmware then attempts to read the file via the +/// EFI_LOAD_FILE_PROTOCOL and the LoadFile() function. In this case the +/// LoadFile() function implements the policy of interpreting the File Path +/// value. +#[derive(Debug)] +#[repr(transparent)] +#[unsafe_protocol(LoadFileProtocol::GUID)] +pub struct LoadFile(LoadFileProtocol); + +impl LoadFile { + /// Causes the driver to load a specified file. + /// + /// # Parameters + /// - `file_path` The device specific path of the file to load. + /// - `boot_policy` The [`BootPolicy`] to use. + /// + /// # Errors + /// - `uefi::status::EFI_SUCCESS` The file was loaded. + /// - `uefi::status::EFI_UNSUPPORTED` The device does not support the + /// provided BootPolicy. + /// - `uefi::status::EFI_INVALID_PARAMETER` FilePath is not a valid device + /// path, or BufferSize is NULL. + /// - `uefi::status::EFI_NO_MEDIA` No medium was present to load the file. + /// - `uefi::status::EFI_DEVICE_ERROR` The file was not loaded due to a + /// device error. + /// - `uefi::status::EFI_NO_RESPONSE` The remote system did not respond. + /// - `uefi::status::EFI_NOT_FOUND` The file was not found. + /// - `uefi::status::EFI_ABORTED` The file load process was manually + /// cancelled. + /// - `uefi::status::EFI_BUFFER_TOO_SMALL` The BufferSize is too small to + /// read the current directory entry. BufferSize has been updated with the + /// size needed to complete the request. + /// - `uefi::status::EFI_WARN_FILE_SYSTEM` The resulting Buffer contains + /// UEFI-compliant file system. + /// + /// [`BootPolicy`]: uefi::proto::BootPolicy + #[cfg(feature = "alloc")] + #[allow(clippy::extra_unused_lifetimes)] // false positive, it is used + pub fn load_file<'a>( + &mut self, + file_path: &DevicePath, + boot_policy: BootPolicy, + ) -> Result> { + let this = core::ptr::addr_of_mut!(*self).cast(); + + let fetch_data_fn = |buf: &'a mut [u8]| { + let mut size = buf.len(); + let status = unsafe { + (self.0.load_file)( + this, + file_path.as_ffi_ptr().cast(), + boot_policy.into(), + &mut size, + buf.as_mut_ptr().cast(), + ) + }; + status.to_result_with_err(|_| Some(size)).map(|_| buf) + }; + + #[cfg(not(feature = "unstable"))] + let file: Box<[u8]> = make_boxed::<[u8], _>(fetch_data_fn)?; + + #[cfg(feature = "unstable")] + let file = make_boxed::<[u8], _, _>(fetch_data_fn, Global)?; + + Ok(file) + } +} + +/// Load File2 Protocol. +/// +/// The Load File2 protocol is used to obtain files from arbitrary devices that +/// are not boot options. +/// +/// # UEFI Spec Description +/// +/// The EFI_LOAD_FILE2_PROTOCOL is a simple protocol used to obtain files from +/// arbitrary devices that are not boot options. It is used by LoadImage() when +/// its BootOption parameter is FALSE and the FilePath does not have an instance +/// of the EFI_SIMPLE_FILE_SYSTEM_PROTOCOL. +#[derive(Debug)] +#[repr(transparent)] +#[unsafe_protocol(LoadFile2Protocol::GUID)] +pub struct LoadFile2(LoadFile2Protocol); + +impl LoadFile2 { + /// Causes the driver to load a specified file. + /// + /// # Parameters + /// - `file_path` The device specific path of the file to load. + /// + /// # Errors + /// - `uefi::status::EFI_SUCCESS` The file was loaded. + /// - `uefi::status::EFI_UNSUPPORTED` BootPolicy is TRUE. + /// - `uefi::status::EFI_INVALID_PARAMETER` FilePath is not a valid device + /// path, or BufferSize is NULL. + /// - `uefi::status::EFI_NO_MEDIA` No medium was present to load the file. + /// - `uefi::status::EFI_DEVICE_ERROR` The file was not loaded due to a + /// device error. + /// - `uefi::status::EFI_NO_RESPONSE` The remote system did not respond. + /// - `uefi::status::EFI_NOT_FOUND` The file was not found. + /// - `uefi::status::EFI_ABORTED` The file load process was manually + /// cancelled. + /// - `uefi::status::EFI_BUFFER_TOO_SMALL` The BufferSize is too small to + /// read the current directory entry. BufferSize has been updated with the + /// size needed to complete the request. + #[cfg(feature = "alloc")] + #[allow(clippy::extra_unused_lifetimes)] // false positive, it is used + pub fn load_file<'a>(&mut self, file_path: &DevicePath) -> Result> { + let this = core::ptr::addr_of_mut!(*self).cast(); + + let fetch_data_fn = |buf: &'a mut [u8]| { + let mut size = buf.len(); + let status = unsafe { + (self.0.load_file)( + this, + file_path.as_ffi_ptr().cast(), + false, /* always false - see spec */ + &mut size, + buf.as_mut_ptr().cast(), + ) + }; + status.to_result_with_err(|_| Some(size)).map(|_| buf) + }; + + #[cfg(not(feature = "unstable"))] + let file: Box<[u8]> = make_boxed::<[u8], _>(fetch_data_fn)?; + + #[cfg(feature = "unstable")] + let file = make_boxed::<[u8], _, _>(fetch_data_fn, Global)?; + + Ok(file) + } +} diff --git a/uefi/src/proto/media/mod.rs b/uefi/src/proto/media/mod.rs index 6750875a6..cd1473a73 100644 --- a/uefi/src/proto/media/mod.rs +++ b/uefi/src/proto/media/mod.rs @@ -9,4 +9,5 @@ pub mod file; pub mod block; pub mod disk; pub mod fs; +pub mod load_file; pub mod partition; diff --git a/uefi/src/table/boot.rs b/uefi/src/table/boot.rs index eaca7f9fb..666685d18 100644 --- a/uefi/src/table/boot.rs +++ b/uefi/src/table/boot.rs @@ -1383,9 +1383,12 @@ pub enum LoadImageSource<'a> { /// Load an image via the [`SimpleFileSystem`] protocol. If there is /// no instance of that protocol associated with the path then the /// behavior depends on [`BootPolicy`]. If [`BootPolicy::BootSelection`], - /// attempt to load via the `LoadFile` protocol. If - /// [`BootPolicy::ExactMatch`], attempt to load via the `LoadFile2` - /// protocol, then fall back to `LoadFile`. + /// attempt to load via the [`LoadFile`] protocol. If + /// [`BootPolicy::ExactMatch`], attempt to load via the [`LoadFile2`] + /// protocol, then fall back to [`LoadFile`]. + /// + /// [`LoadFile`]: crate::proto::media::load_file::LoadFile + /// [`LoadFile2`]: crate::proto::media::load_file::LoadFile2 FromDevicePath { /// The full device path from which to load the image. ///