diff --git a/Cargo.toml b/Cargo.toml index 66d96d2..99c5f1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["placeholder"] +members = ["patina_partition", "placeholder"] [workspace.package] version = "0.0.1" diff --git a/patina_partition/Cargo.toml b/patina_partition/Cargo.toml new file mode 100644 index 0000000..c243fd2 --- /dev/null +++ b/patina_partition/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "patina_partition" +version = "0.1.0" +repository.workspace = true +license.workspace = true +edition.workspace = true +description = "Generic UEFI partition I/O helpers for Patina firmware (BlockIo, SimpleFileSystem)." + +[lints] +workspace = true + +[dependencies] +log = { version = "0.4", default-features = false } +# patina is pulled from feature/patina-boot until a patina release containing the required +# APIs (DevicePathBuf, etc.) is published to crates.io. +patina = { git = "https://github.com/OpenDevicePartnership/patina", branch = "feature/patina-boot", features = ["unstable-device-path"] } +r-efi = { version = "5", default-features = false } + +[dev-dependencies] +patina = { git = "https://github.com/OpenDevicePartnership/patina", branch = "feature/patina-boot", features = ["mockall", "unstable-device-path"] } + +[features] +default = [] +doc = [] +std = [] diff --git a/patina_partition/src/block_io.rs b/patina_partition/src/block_io.rs new file mode 100644 index 0000000..5116123 --- /dev/null +++ b/patina_partition/src/block_io.rs @@ -0,0 +1,274 @@ +//! Raw block-level writes via `EFI_BLOCK_IO_PROTOCOL`. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! +extern crate alloc; + +use alloc::vec::Vec; + +use patina::{ + boot_services::BootServices, + device_path::paths::DevicePathBuf, + error::{EfiError, Result}, +}; +use r_efi::{efi, protocols::block_io}; + +/// Write `data` to the partition addressed by `device_path` starting at LBA 0. +/// +/// Resolves the partition handle via `locate_device_path` against `EFI_BLOCK_IO_PROTOCOL`, +/// then issues `WriteBlocks` followed by `FlushBlocks`. If `data.len()` is not a multiple of +/// the media block size, the trailing partial block is zero-padded. +/// +/// Empty `data` is a no-op (returns `Ok(())` without touching the device). +/// +/// # Arguments +/// +/// * `boot_services` - Boot services interface +/// * `device_path` - Device path resolving to a partition or block device exposing BlockIo +/// * `data` - Bytes to write at LBA 0; must fit within the partition (`data.len() <= (last_block + 1) * block_size`) +/// +/// # Returns +/// +/// Returns `Ok(())` once `WriteBlocks` and `FlushBlocks` both succeed. Returns an error if +/// the device path doesn't resolve to a BlockIo handle, the media is read-only or absent, or +/// either of the write/flush calls fails. +pub fn write_partition_raw(boot_services: &B, device_path: &DevicePathBuf, data: &[u8]) -> Result<()> { + if data.is_empty() { + return Ok(()); + } + + let mut path_ptr = device_path.as_ref() as *const _ as *mut efi::protocols::device_path::Protocol; + + // SAFETY: path_ptr points into a valid DevicePathBuf for the duration of this call. + let handle = + unsafe { boot_services.locate_device_path(&block_io::PROTOCOL_GUID, &mut path_ptr) }.map_err(EfiError::from)?; + + // SAFETY: handle was returned by locate_device_path for the BlockIo GUID. + let protocol = unsafe { boot_services.handle_protocol::(handle).map_err(EfiError::from)? }; + + // SAFETY: `protocol` is a valid reference returned by handle_protocol; lifetime is bound by the controller. + unsafe { write_partition_raw_inner(protocol, data) } +} + +/// Issue `WriteBlocks` + `FlushBlocks` against the supplied BlockIo protocol. +/// +/// Separated from `write_partition_raw` because it dereferences the protocol's `media` raw +/// pointer and invokes function pointers directly. Tests use mock function pointers to +/// exercise the dispatch path. +/// +/// # Safety +/// +/// `protocol.media` must point to a valid `Media` instance owned by the controller, and the +/// `write_blocks`/`flush_blocks` function pointers must remain valid for the duration of the call. +unsafe fn write_partition_raw_inner(protocol: &mut block_io::Protocol, data: &[u8]) -> Result<()> { + // SAFETY: protocol.media is set by the controller when BlockIo is published; non-null per spec. + let media = unsafe { &*protocol.media }; + + if !media.media_present { + return Err(EfiError::from(efi::Status::NO_MEDIA)); + } + if media.read_only { + return Err(EfiError::from(efi::Status::WRITE_PROTECTED)); + } + + let block_size = media.block_size as usize; + if block_size == 0 { + return Err(EfiError::from(efi::Status::DEVICE_ERROR)); + } + + let aligned_size = data.len().div_ceil(block_size) * block_size; + let mut buffer = Vec::with_capacity(aligned_size); + buffer.extend_from_slice(data); + buffer.resize(aligned_size, 0); + + let media_id = media.media_id; + let write_blocks = protocol.write_blocks; + let flush_blocks = protocol.flush_blocks; + + let status = write_blocks(protocol, media_id, 0, aligned_size, buffer.as_mut_ptr() as *mut core::ffi::c_void); + if status != efi::Status::SUCCESS { + return Err(EfiError::from(status)); + } + + let status = flush_blocks(protocol); + if status != efi::Status::SUCCESS { + return Err(EfiError::from(status)); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + extern crate std; + + use core::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; + + use patina::{ + boot_services::MockBootServices, + device_path::{node_defs::EndEntire, paths::DevicePathBuf}, + }; + + use super::*; + + fn create_test_device_path() -> DevicePathBuf { + DevicePathBuf::from_device_path_node_iter(core::iter::once(EndEntire)) + } + + #[test] + fn test_write_partition_raw_empty_data_is_noop() { + let device_path = create_test_device_path(); + // No mock expectations: function must short-circuit before touching boot_services. + let mock = MockBootServices::new(); + + let result = write_partition_raw(&mock, &device_path, &[]); + assert!(result.is_ok(), "empty data must be a no-op"); + } + + #[test] + fn test_write_partition_raw_locate_failure() { + let device_path = create_test_device_path(); + let mut mock = MockBootServices::new(); + + mock.expect_locate_device_path().returning(|_, _| Err(efi::Status::NOT_FOUND)); + + let result = write_partition_raw(&mock, &device_path, &[0xAB; 16]); + assert!(result.is_err(), "missing BlockIo on path must surface as Err"); + } + + #[test] + fn test_write_partition_raw_handle_protocol_failure() { + let device_path = create_test_device_path(); + let mut mock = MockBootServices::new(); + + let handle_addr: usize = 1; + mock.expect_locate_device_path().returning(move |_, _| Ok(handle_addr as efi::Handle)); + mock.expect_handle_protocol::().returning(|_| Err(efi::Status::UNSUPPORTED)); + + let result = write_partition_raw(&mock, &device_path, &[0xAB; 16]); + assert!(result.is_err(), "missing BlockIo interface on located handle must surface as Err"); + } + + // Tests for write_partition_raw_inner — exercise the unsafe FFI dispatch path with mock + // BlockIo function pointers. + + static CAPTURED_WRITE_MEDIA_ID: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); + static CAPTURED_WRITE_LBA: AtomicU64 = AtomicU64::new(0xFFFF_FFFF_FFFF_FFFF); + static CAPTURED_WRITE_SIZE: AtomicUsize = AtomicUsize::new(0); + static FLUSH_BLOCKS_INVOKED: AtomicBool = AtomicBool::new(false); + + fn make_test_media(read_only: bool, present: bool) -> block_io::Media { + block_io::Media { + media_id: 0xCAFEBABE, + removable_media: false, + media_present: present, + logical_partition: false, + read_only, + write_caching: false, + block_size: 512, + io_align: 0, + last_block: 1024, + lowest_aligned_lba: 0, + logical_blocks_per_physical_block: 1, + optimal_transfer_length_granularity: 0, + } + } + + extern "efiapi" fn mock_block_io_reset(_this: *mut block_io::Protocol, _ext: efi::Boolean) -> efi::Status { + efi::Status::SUCCESS + } + + extern "efiapi" fn mock_block_io_read_blocks( + _this: *mut block_io::Protocol, + _media_id: u32, + _lba: efi::Lba, + _buffer_size: usize, + _buffer: *mut core::ffi::c_void, + ) -> efi::Status { + efi::Status::UNSUPPORTED + } + + extern "efiapi" fn mock_write_blocks_capture( + _this: *mut block_io::Protocol, + media_id: u32, + lba: efi::Lba, + buffer_size: usize, + _buffer: *mut core::ffi::c_void, + ) -> efi::Status { + CAPTURED_WRITE_MEDIA_ID.store(media_id, Ordering::SeqCst); + CAPTURED_WRITE_LBA.store(lba, Ordering::SeqCst); + CAPTURED_WRITE_SIZE.store(buffer_size, Ordering::SeqCst); + efi::Status::SUCCESS + } + + extern "efiapi" fn mock_write_blocks_returns_error( + _this: *mut block_io::Protocol, + _media_id: u32, + _lba: efi::Lba, + _buffer_size: usize, + _buffer: *mut core::ffi::c_void, + ) -> efi::Status { + efi::Status::DEVICE_ERROR + } + + extern "efiapi" fn mock_flush_blocks_success(_this: *mut block_io::Protocol) -> efi::Status { + FLUSH_BLOCKS_INVOKED.store(true, Ordering::SeqCst); + efi::Status::SUCCESS + } + + fn make_test_protocol( + media: *const block_io::Media, + write_blocks: block_io::ProtocolWriteBlocks, + ) -> block_io::Protocol { + block_io::Protocol { + revision: block_io::REVISION, + media, + reset: mock_block_io_reset, + read_blocks: mock_block_io_read_blocks, + write_blocks, + flush_blocks: mock_flush_blocks_success, + } + } + + #[test] + fn test_write_partition_raw_inner_writes_to_lba_zero_with_aligned_size() { + FLUSH_BLOCKS_INVOKED.store(false, Ordering::SeqCst); + let media = make_test_media(false, true); + let mut protocol = make_test_protocol(&media, mock_write_blocks_capture); + + // Data size 600 should round up to 1024 (two 512-byte blocks). + let data = [0xAB; 600]; + // SAFETY: protocol and media are valid for the lifetime of the call. + let result = unsafe { write_partition_raw_inner(&mut protocol, &data) }; + + assert!(result.is_ok(), "successful write+flush must produce Ok"); + assert_eq!(CAPTURED_WRITE_MEDIA_ID.load(Ordering::SeqCst), 0xCAFEBABE, "media_id forwarded"); + assert_eq!(CAPTURED_WRITE_LBA.load(Ordering::SeqCst), 0, "writes start at LBA 0"); + assert_eq!(CAPTURED_WRITE_SIZE.load(Ordering::SeqCst), 1024, "600 bytes round up to 2 blocks of 512"); + assert!(FLUSH_BLOCKS_INVOKED.load(Ordering::SeqCst), "FlushBlocks must run after WriteBlocks"); + } + + #[test] + fn test_write_partition_raw_inner_read_only_rejected() { + let media = make_test_media(true, true); + let mut protocol = make_test_protocol(&media, mock_write_blocks_capture); + + // SAFETY: protocol and media are valid for the lifetime of the call. + let result = unsafe { write_partition_raw_inner(&mut protocol, &[0xAB; 16]) }; + assert!(result.is_err(), "read-only media must surface as Err before WriteBlocks runs"); + } + + #[test] + fn test_write_partition_raw_inner_writeblocks_failure_propagates() { + let media = make_test_media(false, true); + let mut protocol = make_test_protocol(&media, mock_write_blocks_returns_error); + + // SAFETY: protocol and media are valid for the lifetime of the call. + let result = unsafe { write_partition_raw_inner(&mut protocol, &[0xAB; 16]) }; + assert!(result.is_err(), "WriteBlocks DEVICE_ERROR must surface as Err"); + } +} diff --git a/patina_partition/src/file_system.rs b/patina_partition/src/file_system.rs new file mode 100644 index 0000000..67f651e --- /dev/null +++ b/patina_partition/src/file_system.rs @@ -0,0 +1,205 @@ +//! File reads via `EFI_SIMPLE_FILE_SYSTEM_PROTOCOL`. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! +extern crate alloc; + +use alloc::vec::Vec; +use core::ptr; + +use patina::{ + boot_services::BootServices, + device_path::paths::DevicePathBuf, + error::{EfiError, Result}, +}; +use r_efi::{ + efi, + protocols::{file, simple_file_system}, +}; + +/// Read the entire contents of `file_path` from the partition addressed by `device_path`. +/// +/// Resolves the partition handle via `locate_device_path` against `EFI_SIMPLE_FILE_SYSTEM_PROTOCOL`, +/// opens the volume, opens the file at `file_path` for reading, slurps the file into a `Vec`, +/// and closes the handles before returning. +/// +/// `file_path` is interpreted as a partition-relative UEFI file path (e.g. `\EFI\BOOT\BOOTX64.EFI`). +/// It is converted to UCS-2 with a trailing null and passed to the protocol's open call. +/// +/// # Arguments +/// +/// * `boot_services` - Boot services interface +/// * `device_path` - Device path resolving to a partition exposing SimpleFileSystem +/// * `file_path` - Partition-relative file path +/// +/// # Returns +/// +/// Returns `Ok(Vec)` containing the file contents on success. Returns an error if the path +/// doesn't resolve to a SimpleFileSystem handle, the volume can't be opened, the file can't be +/// opened, or any read fails. +pub fn read_partition_file( + boot_services: &B, + device_path: &DevicePathBuf, + file_path: &str, +) -> Result> { + let mut path_ptr = device_path.as_ref() as *const _ as *mut efi::protocols::device_path::Protocol; + + // SAFETY: path_ptr points into a valid DevicePathBuf for the duration of this call. + let handle = unsafe { boot_services.locate_device_path(&simple_file_system::PROTOCOL_GUID, &mut path_ptr) } + .map_err(EfiError::from)?; + + // SAFETY: handle was returned by locate_device_path for the SimpleFileSystem GUID. + let fs = unsafe { boot_services.handle_protocol::(handle).map_err(EfiError::from)? }; + + let path_utf16 = encode_ucs2_null_terminated(file_path); + + // SAFETY: fs is valid for the lifetime of the call; path_utf16 is owned by us and outlives the call. + unsafe { read_partition_file_inner(fs, &path_utf16) } +} + +/// Encode a Rust `&str` as a null-terminated UCS-2 buffer suitable for UEFI file-protocol calls. +fn encode_ucs2_null_terminated(s: &str) -> Vec { + let mut out: Vec = s.encode_utf16().collect(); + out.push(0); + out +} + +/// Open the volume, open the file, slurp it into a `Vec`, and clean up. +/// +/// Separated from `read_partition_file` because it dereferences the protocol's function-pointer +/// fields directly. Tests use mock function pointers to exercise the dispatch path. +/// +/// # Safety +/// +/// `fs` must remain valid for the duration of the call. `path_utf16` must be a null-terminated +/// UCS-2 buffer. +unsafe fn read_partition_file_inner(fs: &mut simple_file_system::Protocol, path_utf16: &[u16]) -> Result> { + let mut root: *mut file::Protocol = ptr::null_mut(); + // SAFETY: fs is valid; root pointer is written by the protocol on success. + let status = (fs.open_volume)(fs, &mut root); + if status != efi::Status::SUCCESS { + return Err(EfiError::from(status)); + } + + let mut handle: *mut file::Protocol = ptr::null_mut(); + // SAFETY: root is valid from open_volume; path is null-terminated. + let status = unsafe { ((*root).open)(root, &mut handle, path_utf16.as_ptr() as *mut u16, file::MODE_READ, 0) }; + if status != efi::Status::SUCCESS { + // SAFETY: root is valid; close consumes it. + let _ = unsafe { ((*root).close)(root) }; + return Err(EfiError::from(status)); + } + + // Helper closure: close both handles, used on every exit path. + // SAFETY: handle and root are both valid for the lifetime of this fn; close consumes them. + let close_both = || unsafe { + let _ = ((*handle).close)(handle); + let _ = ((*root).close)(root); + }; + + // Determine file size by seeking to end + reading position. + // SAFETY: handle is valid from open. + let status = unsafe { ((*handle).set_position)(handle, u64::MAX) }; + if status != efi::Status::SUCCESS { + close_both(); + return Err(EfiError::from(status)); + } + + let mut size: u64 = 0; + // SAFETY: handle is valid from open. + let status = unsafe { ((*handle).get_position)(handle, &mut size) }; + if status != efi::Status::SUCCESS { + close_both(); + return Err(EfiError::from(status)); + } + + // Reset position to start of file before reading. + // SAFETY: handle is valid from open. + let status = unsafe { ((*handle).set_position)(handle, 0) }; + if status != efi::Status::SUCCESS { + close_both(); + return Err(EfiError::from(status)); + } + + let size_usize = size as usize; + let mut buffer: Vec = alloc::vec![0u8; size_usize]; + + let mut buffer_size = size_usize; + // SAFETY: handle is valid; buffer outlives the call. + let status = unsafe { ((*handle).read)(handle, &mut buffer_size, buffer.as_mut_ptr() as *mut core::ffi::c_void) }; + if status != efi::Status::SUCCESS { + close_both(); + return Err(EfiError::from(status)); + } + + buffer.truncate(buffer_size); + + close_both(); + + Ok(buffer) +} + +#[cfg(test)] +mod tests { + extern crate std; + + use patina::{ + boot_services::MockBootServices, + device_path::{node_defs::EndEntire, paths::DevicePathBuf}, + }; + + use super::*; + + fn create_test_device_path() -> DevicePathBuf { + DevicePathBuf::from_device_path_node_iter(core::iter::once(EndEntire)) + } + + #[test] + fn test_read_partition_file_locate_failure() { + let device_path = create_test_device_path(); + let mut mock = MockBootServices::new(); + + mock.expect_locate_device_path().returning(|_, _| Err(efi::Status::NOT_FOUND)); + + let result = read_partition_file(&mock, &device_path, r"\EFI\BOOT\BOOTX64.EFI"); + assert!(result.is_err(), "missing SimpleFileSystem on path must surface as Err"); + } + + #[test] + fn test_read_partition_file_handle_protocol_failure() { + let device_path = create_test_device_path(); + let mut mock = MockBootServices::new(); + + let handle_addr: usize = 1; + mock.expect_locate_device_path().returning(move |_, _| Ok(handle_addr as efi::Handle)); + mock.expect_handle_protocol::().returning(|_| Err(efi::Status::UNSUPPORTED)); + + let result = read_partition_file(&mock, &device_path, r"\EFI\BOOT\BOOTX64.EFI"); + assert!(result.is_err(), "missing SimpleFileSystem interface on located handle must surface as Err"); + } + + #[test] + fn test_encode_ucs2_null_terminated_basic() { + let encoded = encode_ucs2_null_terminated("AB"); + assert_eq!(encoded, alloc::vec![b'A' as u16, b'B' as u16, 0]); + } + + #[test] + fn test_encode_ucs2_null_terminated_path() { + // \EFI\BOOT\BOOTX64.EFI — backslash 0x5C, characters as ASCII, trailing null. + let encoded = encode_ucs2_null_terminated(r"\EFI\BOOT\BOOTX64.EFI"); + assert_eq!(encoded.len(), 22, "21 chars + null terminator"); + assert_eq!(encoded[0], 0x5C, "starts with backslash"); + assert_eq!(*encoded.last().unwrap(), 0, "null-terminated"); + } + + #[test] + fn test_encode_ucs2_null_terminated_empty() { + let encoded = encode_ucs2_null_terminated(""); + assert_eq!(encoded, alloc::vec![0u16]); + } +} diff --git a/patina_partition/src/lib.rs b/patina_partition/src/lib.rs new file mode 100644 index 0000000..c54b754 --- /dev/null +++ b/patina_partition/src/lib.rs @@ -0,0 +1,26 @@ +//! Generic UEFI partition I/O helpers for Patina firmware. +//! +//! Two protocol-shaped modules: +//! +//! - [`block_io`] — raw block-level writes via `EFI_BLOCK_IO_PROTOCOL`. +//! - [`file_system`] — file reads via `EFI_SIMPLE_FILE_SYSTEM_PROTOCOL`. +//! +//! All helpers operate on partitions identified by a [`patina::device_path::paths::DevicePathBuf`] +//! and use UEFI protocols already published by the platform's storage stack. They are intended +//! for orchestrators implementing custom boot or recovery flows. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +pub mod block_io; +pub mod file_system; + +pub use block_io::write_partition_raw; +pub use file_system::read_partition_file;