diff --git a/Cargo.toml b/Cargo.toml index f2b05c911..e96a84075 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["components/*", "core/*", "sdk/*", "patina_dxe_core"] +members = ["components/*", "core/*", "sdk/*", "patina_dxe_core", "patina_mm_supervisor_core", "patina_mm_user_core"] [workspace.package] version = "21.0.1" @@ -38,15 +38,19 @@ patina_ffs_extractors = { version = "21.0.1", path = "sdk/patina_ffs_extractors" patina_internal_collections = { version = "21.0.1", path = "core/patina_internal_collections", default-features = false } patina_internal_cpu = { version = "21.0.1", path = "core/patina_internal_cpu" } patina_internal_depex = { version = "21.0.1", path = "core/patina_internal_depex" } +patina_internal_mm_common = { version = "21.0.0", path = "core/patina_internal_mm_common" } patina_lzma_rs = { version = "0.3.1", default-features = false } patina_macro = { version = "21.0.1", path = "sdk/patina_macro" } patina_mm = { version = "21.0.1", path = "components/patina_mm" } patina_mtrr = { version = "^1.1.6" } +patina_mm_policy = { version = "21.0.0", path = "components/patina_mm_policy" } +patina_mm_supervisor_core = { version = "21.0.0", path = "patina_mm_supervisor_core" } patina_paging = { version = "11.0.2" } patina_performance = { version = "21.0.1", path = "components/patina_performance" } patina_smbios = { version = "21.0.1", path = "components/patina_smbios" } patina_stacktrace = { version = "21.0.1", path = "core/patina_stacktrace" } patina_test = { version = "21.0.1", path = "components/patina_test" } +patina_adv_logger = { version = "21.0.0", path = "components/patina_adv_logger" } proc-macro2 = { version = "1" } quote = { version = "1" } r-efi = { version = "5.0.0", default-features = false } @@ -76,3 +80,6 @@ lzma-rs = { version = "0.3" } [workspace.lints.clippy] undocumented_unsafe_blocks = "warn" + +[patch.crates-io] +patina_paging = { path = 'D:\Repos\patina-paging' } diff --git a/components/patina_mm_policy/Cargo.toml b/components/patina_mm_policy/Cargo.toml new file mode 100644 index 000000000..5ac9481da --- /dev/null +++ b/components/patina_mm_policy/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "patina_mm_policy" +resolver = "2" +version.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +readme = "README.md" +description = "MM Supervisor secure policy library for Rust UEFI environments." + +# Metadata to tell docs.rs how to build the documentation when uploading +[package.metadata.docs.rs] +features = ["doc"] + +[dependencies] +log = { workspace = true } +r-efi = { workspace = true } +spin = { workspace = true } + +[dev-dependencies] + +[features] +default = [] +std = [] +doc = [] diff --git a/components/patina_mm_policy/README.md b/components/patina_mm_policy/README.md new file mode 100644 index 000000000..efad5522d --- /dev/null +++ b/components/patina_mm_policy/README.md @@ -0,0 +1,44 @@ +# patina_mm_policy + +MM Supervisor secure policy library for Rust UEFI environments. + +This crate provides a comprehensive policy management library for the MM Supervisor, +including policy data structures, access validation (policy gate), and helper utilities. + +## Features + +### Policy Gate +Initialize with a policy buffer pointer, then query whether operations are allowed: +- `is_io_allowed()` - Check I/O port access +- `is_msr_allowed()` - Check MSR access +- `is_instruction_allowed()` - Check privileged instruction execution +- `is_save_state_read_allowed()` - Check save state read access + +### Helper Functions +- `dump_policy()` - Print policy contents for debugging +- `compare_policies()` - Compare two policies (order-independent) +- `populate_memory_policy_from_page_table()` - Walk page tables to generate memory policy + +## Usage + +```rust,ignore +use patina_mm_policy::{PolicyGate, AccessType, IoWidth}; + +// Initialize the policy gate with a policy buffer +let gate = unsafe { PolicyGate::new(policy_ptr) }?; + +// Check if I/O access is allowed +let result = gate.is_io_allowed(0x3F8, IoWidth::Byte, AccessType::Read); +if result.is_ok() { + // Access allowed +} + +// Dump policy for debugging +gate.dump_policy(); +``` + +## License + +Copyright (c) Microsoft Corporation. + +SPDX-License-Identifier: Apache-2.0 diff --git a/components/patina_mm_policy/src/gate.rs b/components/patina_mm_policy/src/gate.rs new file mode 100644 index 000000000..e766c3ee1 --- /dev/null +++ b/components/patina_mm_policy/src/gate.rs @@ -0,0 +1,683 @@ +//! Policy Gate - Runtime access validation +//! +//! This module provides the `PolicyGate` struct that wraps a policy buffer +//! and provides methods to check if various operations are allowed. + +use crate::helpers::{IsInsideMmramFn, walk_page_table}; +use crate::types::*; +use spin::Once; + +/// Errors that can occur during policy gate operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PolicyError { + /// The policy pointer is null. + NullPointer, + /// Invalid policy version. + InvalidVersion, + /// Invalid access mask specified. + InvalidAccessMask, + /// Invalid I/O width specified. + InvalidIoWidth, + /// Invalid I/O address (out of 16-bit range). + InvalidIoAddress, + /// Invalid I/O address range (overflow). + InvalidIoRange, + /// Invalid instruction index. + InvalidInstructionIndex, + /// Invalid save state map field. + InvalidSaveStateField, + /// Policy root not found for the requested type. + PolicyRootNotFound, + /// Access denied by policy. + AccessDenied, + /// Internal error during policy evaluation. + InternalError, +} + +/// Policy gate for runtime access validation. +/// +/// This struct wraps a policy buffer and provides methods to check if +/// various operations (I/O, MSR, instruction, save state) are allowed. +/// +/// ## Example +/// +/// ```rust,ignore +/// use patina_mm_policy::{PolicyGate, AccessType, IoWidth}; +/// +/// let gate = unsafe { PolicyGate::new(policy_ptr) }?; +/// +/// // Check I/O access +/// if gate.is_io_allowed(0x3F8, IoWidth::Byte, AccessType::Read).is_ok() { +/// // Access allowed +/// } +/// ``` +pub struct PolicyGate { + /// Pointer to the firmware policy data (static, read-only). + policy_ptr: *const u8, + /// Memory policy buffer (written by `walk_page_table` during snapshot). + memory_policy_buffer: *mut MemDescriptorV1_0, + /// Maximum number of `MemDescriptorV1_0` entries the memory policy buffer can hold. + memory_policy_max_count: usize, + /// Number of descriptors stored in the snapshot buffer. + /// + /// `None` means the ready-to-lock transition has **not** occurred. + /// `Some(count)` means a snapshot was taken with `count` entries. + snapshot_count: Once, +} + +// SAFETY: PolicyGate only holds a pointer to read-only policy data. +unsafe impl Send for PolicyGate {} +unsafe impl Sync for PolicyGate {} + +impl PolicyGate { + /// Creates a new policy gate from a policy buffer pointer. + /// + /// # Safety + /// + /// The caller must ensure that `policy_ptr` points to a valid policy buffer + /// that remains valid for the lifetime of this PolicyGate. + /// + /// # Returns + /// + /// Returns `Ok(PolicyGate)` if the policy is valid, or an error otherwise. + pub unsafe fn new(policy_ptr: *const u8) -> Result { + if policy_ptr.is_null() { + return Err(PolicyError::NullPointer); + } + + let policy = unsafe { &*(policy_ptr as *const SecurePolicyDataV1_0) }; + if !policy.is_valid_version() { + return Err(PolicyError::InvalidVersion); + } + + Ok(Self { + policy_ptr, + memory_policy_buffer: core::ptr::null_mut(), + memory_policy_max_count: 0, + snapshot_count: Once::new(), + }) + } + + /// Sets the memory policy buffer for page-table-derived snapshots. + /// + /// Must be called before [`take_snapshot`](Self::take_snapshot). Typically + /// the buffer address and size come from the PassDown HOB. + /// + /// # Safety + /// + /// # Safety Contract (deferred) + /// + /// The caller must ensure that `buffer` points to a valid memory region + /// that can hold at least `max_count` `MemDescriptorV1_0` entries and that + /// this memory remains valid for the lifetime of the `PolicyGate`. + /// + /// Storing the pointer is safe; the contract is enforced when the buffer + /// is later dereferenced by [`take_snapshot`], [`verify_snapshot`], or + /// [`fetch_n_update_policy`]. + pub fn set_memory_policy_buffer(&mut self, buffer: *mut MemDescriptorV1_0, max_count: usize) { + self.memory_policy_buffer = buffer; + self.memory_policy_max_count = max_count; + } + + /// Gets a reference to the policy header. + fn policy(&self) -> &SecurePolicyDataV1_0 { + // SAFETY: Constructor validated the pointer + unsafe { &*(self.policy_ptr as *const SecurePolicyDataV1_0) } + } + + /// Finds a policy root by type. + fn find_policy_root(&self, policy_type: u32) -> Option<&PolicyRootV1> { + let policy = self.policy(); + // SAFETY: Constructor validated the policy + let roots = unsafe { policy.get_policy_roots() }; + roots.iter().find(|r| r.policy_type == policy_type) + } + + /// Checks if I/O access is allowed. + /// + /// # Arguments + /// + /// * `io_address` - The I/O port address (must be <= 0xFFFF) + /// * `width` - The access width + /// * `access_type` - Read or Write access + /// + /// # Returns + /// + /// Returns `Ok(())` if access is allowed, or `Err(PolicyError)` otherwise. + pub fn is_io_allowed(&self, io_address: u32, width: IoWidth, access_type: AccessType) -> Result<(), PolicyError> { + // Validate access type (must be read or write, not execute) + if access_type == AccessType::Execute { + return Err(PolicyError::InvalidAccessMask); + } + + let io_size = width.size(); + + // Validate I/O address range (16-bit port space) + if io_address > u16::MAX as u32 { + return Err(PolicyError::InvalidIoAddress); + } + + // Check for overflow (MAX_UINT16 + 1 is valid for end address) + if io_address.saturating_add(io_size) > (u16::MAX as u32) + 1 { + return Err(PolicyError::InvalidIoRange); + } + + let policy_root = match self.find_policy_root(TYPE_IO) { + Some(root) => root, + None => { + log::warn!("Could not find IO policy root, denying access to be safe."); + return Err(PolicyError::PolicyRootNotFound); + } + }; + + // SAFETY: We validated the policy in the constructor + let descriptors = unsafe { policy_root.get_io_descriptors(self.policy_ptr) }; + let access_mask = access_type.as_attr_mask(); + + let mut found_match = false; + + for desc in descriptors { + let desc_start = desc.io_address as u32; + let desc_size = desc.length_or_width as u32; + let is_strict_width = (desc.attributes as u32 & RESOURCE_ATTR_STRICT_WIDTH) != 0; + + if is_strict_width { + // Strict width: address and size must match exactly + if io_address == desc_start && io_size == desc_size { + // Check if the access type matches + if (desc.attributes as u32 & access_mask) != 0 { + found_match = true; + break; + } + } + } else { + // Non-strict: check if our range is covered by this descriptor + let desc_end = desc_start.saturating_add(desc_size); + let our_end = io_address.saturating_add(io_size); + + if io_address >= desc_start && our_end <= desc_end { + // Check if the access type matches + if (desc.attributes as u32 & access_mask) != 0 { + found_match = true; + break; + } + } + } + } + + // Evaluate based on allow/deny list semantics + if (found_match && policy_root.access_attr == ACCESS_ATTR_DENY) + || (!found_match && policy_root.access_attr == ACCESS_ATTR_ALLOW) + { + log::debug!("Rejecting IO access: port=0x{:x}, width={}, type={:?}", io_address, io_size, access_type); + return Err(PolicyError::AccessDenied); + } + + Ok(()) + } + + /// Checks if MSR access is allowed. + /// + /// # Arguments + /// + /// * `msr_address` - The MSR address + /// * `access_type` - Read or Write access + /// + /// # Returns + /// + /// Returns `Ok(())` if access is allowed, or `Err(PolicyError)` otherwise. + pub fn is_msr_allowed(&self, msr_address: u32, access_type: AccessType) -> Result<(), PolicyError> { + // Validate access type + if access_type == AccessType::Execute { + return Err(PolicyError::InvalidAccessMask); + } + + let policy_root = match self.find_policy_root(TYPE_MSR) { + Some(root) => root, + None => { + log::warn!("Could not find MSR policy root, denying access to be safe."); + return Err(PolicyError::PolicyRootNotFound); + } + }; + + // SAFETY: We validated the policy in the constructor + let descriptors = unsafe { policy_root.get_msr_descriptors(self.policy_ptr) }; + let access_mask = access_type.as_attr_mask(); + + let mut found_match = false; + + for desc in descriptors { + let desc_start = desc.msr_address; + let desc_end = desc_start.saturating_add(desc.length as u32); + + if msr_address >= desc_start && msr_address < desc_end { + if (desc.attributes as u32 & access_mask) != 0 { + found_match = true; + break; + } + } + } + + // Evaluate based on allow/deny list semantics + if (found_match && policy_root.access_attr == ACCESS_ATTR_DENY) + || (!found_match && policy_root.access_attr == ACCESS_ATTR_ALLOW) + { + log::debug!("Rejecting MSR access: address=0x{:x}, type={:?}", msr_address, access_type); + return Err(PolicyError::AccessDenied); + } + + Ok(()) + } + + /// Checks if instruction execution is allowed. + /// + /// # Arguments + /// + /// * `instruction` - The instruction to check + /// + /// # Returns + /// + /// Returns `Ok(())` if execution is allowed, or `Err(PolicyError)` otherwise. + pub fn is_instruction_allowed(&self, instruction: Instruction) -> Result<(), PolicyError> { + let instruction_index = instruction.as_index(); + + if instruction_index >= INSTRUCTION_COUNT { + return Err(PolicyError::InvalidInstructionIndex); + } + + let policy_root = match self.find_policy_root(TYPE_INSTRUCTION) { + Some(root) => root, + None => { + log::warn!("Could not find Instruction policy root, denying access to be safe."); + return Err(PolicyError::PolicyRootNotFound); + } + }; + + // SAFETY: We validated the policy in the constructor + let descriptors = unsafe { policy_root.get_instruction_descriptors(self.policy_ptr) }; + + let mut found_match = false; + + for desc in descriptors { + if instruction_index == desc.instruction_index { + if (desc.attributes as u32 & RESOURCE_ATTR_EXECUTE) != 0 { + found_match = true; + break; + } + } + } + + // Evaluate based on allow/deny list semantics + if (found_match && policy_root.access_attr == ACCESS_ATTR_DENY) + || (!found_match && policy_root.access_attr == ACCESS_ATTR_ALLOW) + { + log::debug!("Rejecting instruction execution: {:?}", instruction); + return Err(PolicyError::AccessDenied); + } + + Ok(()) + } + + /// Checks if save state read access is allowed. + /// + /// # Arguments + /// + /// * `field` - The save state field to read + /// * `width` - The access width in bytes + /// * `current_condition` - The current I/O trap condition (if applicable) + /// + /// # Returns + /// + /// Returns `Ok(())` if access is allowed, or `Err(PolicyError)` otherwise. + pub fn is_save_state_read_allowed( + &self, + field: SaveStateField, + width: usize, + current_condition: Option, + ) -> Result<(), PolicyError> { + let policy_root = match self.find_policy_root(TYPE_SAVE_STATE) { + Some(root) => root, + None => { + // No save state policy = level 20, allow all reads + log::debug!("No save state policy root found, allowing read (level 20 policy)."); + return Ok(()); + } + }; + + // SAFETY: We validated the policy in the constructor + let descriptors = unsafe { policy_root.get_save_state_descriptors(self.policy_ptr) }; + + let mut found_match = false; + + for desc in descriptors { + if desc.map_field == field.as_index() { + // Check if this is a read-allowed policy + let is_read = (desc.attributes & RESOURCE_ATTR_READ) != 0; + let is_cond_read = (desc.attributes & RESOURCE_ATTR_COND_READ) != 0; + + if is_read || is_cond_read { + // Check condition if this is conditional read + if is_cond_read { + if let Some(current) = current_condition { + if desc.access_condition == current as u32 { + found_match = true; + break; + } + } + // Condition doesn't match, continue looking + } else { + // Unconditional read + if desc.access_condition == SVST_UNCONDITIONAL { + found_match = true; + break; + } + } + } + } + } + + // Evaluate based on allow/deny list semantics + if (found_match && policy_root.access_attr == ACCESS_ATTR_DENY) + || (!found_match && policy_root.access_attr == ACCESS_ATTR_ALLOW) + { + log::debug!("Rejecting save state read: field={:?}, width={}", field, width); + return Err(PolicyError::AccessDenied); + } + + Ok(()) + } + + /// Gets the raw policy pointer. + pub fn as_ptr(&self) -> *const u8 { + self.policy_ptr + } + + /// Returns `true` if the ready-to-lock snapshot has been taken. + pub fn is_locked(&self) -> bool { + self.snapshot_count.get().is_some() + } + + /// Returns the snapshot descriptor count, or `None` if not yet locked. + pub fn snapshot_count(&self) -> Option { + self.snapshot_count.get().copied() + } + + /// Returns the firmware policy blob size (from `SecurePolicyDataV1_0::size`). + /// + /// Returns `0` if the policy pointer is null (should not happen after construction). + pub fn firmware_policy_size(&self) -> usize { + self.policy().size as usize + } + + /// Returns the raw memory policy buffer pointer. + /// + /// After [`take_snapshot`](Self::take_snapshot) this buffer contains the + /// snapshot descriptors. + pub fn memory_policy_buffer(&self) -> *const MemDescriptorV1_0 { + self.memory_policy_buffer as *const MemDescriptorV1_0 + } + + /// Returns the memory policy buffer capacity in descriptor count. + pub fn memory_policy_max_count(&self) -> usize { + self.memory_policy_max_count + } + + /// Takes a page-table memory policy snapshot and transitions to the locked + /// state. + /// + /// Walks the active page table, writes the resulting descriptors into the + /// memory policy buffer, and atomically saves the descriptor count. After + /// this call, [`is_locked`](Self::is_locked) returns `true`. + /// + /// If the gate is already locked, the snapshot is **not** re-taken and the + /// existing descriptor count is returned. + /// + /// # Safety + /// + /// * `cr3` must point to a valid, stable PML4 table. + /// * The memory policy buffer (set via [`set_memory_policy_buffer`]) + /// must still be valid and large enough. + pub unsafe fn take_snapshot(&self, cr3: u64, is_inside_mmram: IsInsideMmramFn) -> Result { + // Idempotent: if already locked, return the saved count. + if let Some(&count) = self.snapshot_count.get() { + return Ok(count); + } + + if self.memory_policy_buffer.is_null() || self.memory_policy_max_count == 0 { + log::error!("take_snapshot: memory policy buffer not configured"); + return Err(PolicyError::InternalError); + } + + // SAFETY: The caller guarantees that `cr3` points to a valid PML4 and + // that the memory policy buffer (set via `set_memory_policy_buffer`) is + // valid and can hold `memory_policy_max_count` descriptors. + let count = + unsafe { walk_page_table(cr3, self.memory_policy_buffer, self.memory_policy_max_count, is_inside_mmram) } + .map_err(|e| { + log::error!("take_snapshot: walk_page_table failed: {:?}", e); + PolicyError::InternalError + })?; + + self.snapshot_count.call_once(|| count); + log::info!("Policy snapshot taken: {} descriptors, ready-to-lock is now TRUE", count); + Ok(count) + } + + /// Verifies that the current page table still matches the saved snapshot. + /// + /// The caller must provide a scratch buffer (typically allocated from the + /// page allocator) large enough to hold the walk results. This avoids + /// overwriting the saved snapshot during comparison. + /// + /// Returns `Ok(())` if the tables match, or `Err(PolicyError::AccessDenied)` + /// if they differ ("security violation"). + /// + /// # Safety + /// + /// * `cr3` must point to a valid, stable PML4 table. + /// * `scratch` must point to a buffer of at least `scratch_max_count` + /// `MemDescriptorV1_0` entries. + pub unsafe fn verify_snapshot( + &self, + cr3: u64, + is_inside_mmram: IsInsideMmramFn, + scratch: *mut MemDescriptorV1_0, + scratch_max_count: usize, + ) -> Result<(), PolicyError> { + let saved_count = match self.snapshot_count.get() { + Some(&c) => c, + None => { + log::warn!("verify_snapshot: no snapshot available, skipping verification"); + return Ok(()); + } + }; + + // SAFETY: The caller guarantees that `cr3` points to a valid PML4 and + // that `scratch` can hold `scratch_max_count` descriptors. The saved + // snapshot buffer (`self.memory_policy_buffer`) was populated by a + // prior `take_snapshot` call with `saved_count` entries. + unsafe { + let fresh_count = walk_page_table(cr3, scratch, scratch_max_count, is_inside_mmram).map_err(|e| { + log::error!("verify_snapshot: walk_page_table failed: {:?}", e); + PolicyError::InternalError + })?; + + if fresh_count != saved_count { + log::error!( + "verify_snapshot: descriptor count mismatch (saved={}, fresh={})", + saved_count, + fresh_count, + ); + return Err(PolicyError::AccessDenied); + } + + let snapshot_ptr = self.memory_policy_buffer as *const MemDescriptorV1_0; + for i in 0..saved_count { + let saved = core::ptr::read(snapshot_ptr.add(i)); + let fresh = core::ptr::read(scratch.add(i)); + if saved != fresh { + log::error!( + "verify_snapshot: descriptor {} mismatch - \ + saved=(base=0x{:x}, size=0x{:x}, attrs=0x{:x}) vs \ + fresh=(base=0x{:x}, size=0x{:x}, attrs=0x{:x})", + i, + saved.base_address, + saved.size, + saved.mem_attributes, + fresh.base_address, + fresh.size, + fresh.mem_attributes, + ); + return Err(PolicyError::AccessDenied); + } + } + } + + log::info!("verify_snapshot: page table matches saved snapshot ({} descriptors)", saved_count,); + Ok(()) + } + + /// Writes the merged firmware + memory policy into `dest`. + /// + /// Mirrors the C `FetchNUpdateSecurityPolicy` function. The caller is + /// responsible for ensuring the snapshot has been taken first (via + /// [`take_snapshot`](Self::take_snapshot)). + /// + /// ## Layout written to `dest` + /// + /// ```text + /// |--------------------------------------| + /// | SecurePolicyDataV1_0 + payload | <- firmware policy blob (copied first) + /// |--------------------------------------| + /// | MemDescriptorV1_0[0..N] | <- memory policy snapshot (appended) + /// |--------------------------------------| + /// ``` + /// + /// After the copy the function patches the header in-place: + /// + /// * The `TYPE_MEM` policy root's `offset` → `fw_size` and `count` → snapshot count + /// * The header's `size` → `fw_size + mem_policy_bytes` + /// * The legacy `memory_policy_count` field is zeroed (unused with root-based layout) + /// + /// Note: the caller is responsible for writing/reserving any request header + /// *before* the region pointed to by `dest`. + /// + /// # Arguments + /// + /// * `dest` - Destination buffer for the merged policy. + /// * `dest_size` - Available bytes at `dest`. + /// + /// # Returns + /// + /// The total number of bytes written to `dest`. + /// + /// # Safety + /// + /// * `dest` must point to a writable buffer of at least `dest_size` bytes. + pub unsafe fn fetch_n_update_policy(&self, dest: *mut u8, dest_size: usize) -> Result { + let count = self.snapshot_count.get().copied().ok_or_else(|| { + log::error!("fetch_n_update_policy: no snapshot taken"); + PolicyError::InternalError + })?; + + let desc_size = core::mem::size_of::(); + let mem_policy_bytes = count.checked_mul(desc_size).ok_or_else(|| { + log::error!("fetch_n_update_policy: descriptor count overflow"); + PolicyError::InternalError + })?; + + let fw_size = self.firmware_policy_size(); + if fw_size == 0 { + log::error!("fetch_n_update_policy: firmware policy size is 0"); + return Err(PolicyError::InternalError); + } + + let total_bytes = fw_size.checked_add(mem_policy_bytes).ok_or_else(|| { + log::error!("fetch_n_update_policy: total size overflow"); + PolicyError::InternalError + })?; + + if dest_size < total_bytes { + log::error!("fetch_n_update_policy: buffer too small ({} bytes, need {})", dest_size, total_bytes,); + return Err(PolicyError::InternalError); + } + + // SAFETY: The caller guarantees that `dest` is writable for at least + // `dest_size` bytes (verified >= `total_bytes` above). + // `self.policy_ptr` points to a valid firmware policy blob of `fw_size` + // bytes (validated at construction). The memory policy buffer holds + // `count` valid descriptors from a prior `take_snapshot` call. + unsafe { + // 1. Copy the firmware policy blob first (header + payload). + core::ptr::copy_nonoverlapping(self.policy_ptr, dest, fw_size); + + // 2. Append memory policy descriptors after the firmware blob. + if mem_policy_bytes > 0 { + let mem_dest = dest.add(fw_size); + let src = self.memory_policy_buffer as *const u8; + core::ptr::copy_nonoverlapping(src, mem_dest, mem_policy_bytes); + } + + // 3. Fix up the copied header to reflect the appended memory policy. + let header = &mut *(dest as *mut SecurePolicyDataV1_0); + + // Find the TYPE_MEM policy root and patch its offset/count. + let roots_ptr = (dest as *mut u8).add(header.policy_root_offset as usize) as *mut PolicyRootV1; + let mut found_mem_root = false; + for i in 0..header.policy_root_count as usize { + let root = &mut *roots_ptr.add(i); + if root.policy_type == TYPE_MEM { + root.access_attr = ACCESS_ATTR_ALLOW; + root.offset = fw_size as u32; + root.count = count as u32; + found_mem_root = true; + break; + } + } + + if !found_mem_root { + log::error!("fetch_n_update_policy: firmware policy has no TYPE_MEM policy root"); + return Err(PolicyError::PolicyRootNotFound); + } + + // Update the total size and clear the legacy memory_policy_count. + header.size = total_bytes as u32; + header.memory_policy_count = 0; + } + + log::info!( + "fetch_n_update_policy: wrote {} bytes (fw_policy={}, mem_policy={} ({} descs))", + total_bytes, + fw_size, + mem_policy_bytes, + count, + ); + Ok(total_bytes) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_io_width() { + assert_eq!(IoWidth::Byte.size(), 1); + assert_eq!(IoWidth::Word.size(), 2); + assert_eq!(IoWidth::Dword.size(), 4); + } + + #[test] + fn test_access_type_mask() { + assert_eq!(AccessType::Read.as_attr_mask(), RESOURCE_ATTR_READ); + assert_eq!(AccessType::Write.as_attr_mask(), RESOURCE_ATTR_WRITE); + assert_eq!(AccessType::Execute.as_attr_mask(), RESOURCE_ATTR_EXECUTE); + } + + #[test] + fn test_instruction_conversion() { + assert_eq!(Instruction::Cli.as_index(), 0); + assert_eq!(Instruction::from_index(0), Some(Instruction::Cli)); + assert_eq!(Instruction::from_index(99), None); + } +} diff --git a/components/patina_mm_policy/src/helpers.rs b/components/patina_mm_policy/src/helpers.rs new file mode 100644 index 000000000..8c35b5219 --- /dev/null +++ b/components/patina_mm_policy/src/helpers.rs @@ -0,0 +1,1015 @@ +//! Policy Helper Functions +//! +//! This module provides utility functions for policy manipulation: +//! - Dump/print policy for debugging +//! - Compare two policies (order-independent) +//! - Page table walking to generate memory policy + +use crate::types::*; +use core::mem::size_of; + +/// Dumps a single memory policy entry for debugging. +pub fn dump_mem_policy_entry(desc: &MemDescriptorV1_0) { + let r = if (desc.mem_attributes & RESOURCE_ATTR_READ) != 0 { "R" } else { "." }; + let w = if (desc.mem_attributes & RESOURCE_ATTR_WRITE) != 0 { "W" } else { "." }; + let x = if (desc.mem_attributes & RESOURCE_ATTR_EXECUTE) != 0 { "X" } else { "." }; + + log::info!( + " MEM: [0x{:016x}-0x{:016x}] {}{}{}", + desc.base_address, + desc.base_address.saturating_add(desc.size).saturating_sub(1), + r, + w, + x + ); +} + +/// Dumps policy data for debugging (like `DumpSmmPolicyData`). +/// +/// # Safety +/// +/// The caller must ensure that `policy_ptr` points to a valid policy buffer. +pub unsafe fn dump_policy(policy_ptr: *const u8) { + if policy_ptr.is_null() { + log::error!("dump_policy: null pointer"); + return; + } + + let policy = unsafe { &*(policy_ptr as *const SecurePolicyDataV1_0) }; + + log::info!("SMM_SUPV_SECURE_POLICY_DATA_V1_0:"); + log::info!(" Version: {}.{}", policy.version_major, policy.version_minor); + log::info!(" Size: 0x{:x}", policy.size); + log::info!(" MemoryPolicyOffset: 0x{:x}", policy.memory_policy_offset); + log::info!(" MemoryPolicyCount: 0x{:x}", policy.memory_policy_count); + log::info!(" Flags: 0x{:x}", policy.flags); + log::info!(" Capabilities: 0x{:x}", policy.capabilities); + log::info!(" PolicyRootOffset: 0x{:x}", policy.policy_root_offset); + log::info!(" PolicyRootCount: 0x{:x}", policy.policy_root_count); + + let policy_roots = unsafe { policy.get_policy_roots() }; + + for (i, root) in policy_roots.iter().enumerate() { + log::info!("Policy Root {}:", i); + log::info!(" Version: {}", root.version); + log::info!(" PolicyRootSize: {}", root.policy_root_size); + log::info!(" Type: {}", root.policy_type); + log::info!(" Offset: 0x{:x}", root.offset); + log::info!(" Count: {}", root.count); + log::info!(" AccessAttr: {}", if root.access_attr == ACCESS_ATTR_ALLOW { "ALLOW" } else { "DENY" }); + + match root.policy_type { + TYPE_MEM => { + let descriptors = unsafe { root.get_mem_descriptors(policy_ptr) }; + for desc in descriptors { + dump_mem_policy_entry(desc); + } + } + TYPE_IO => { + let descriptors = unsafe { root.get_io_descriptors(policy_ptr) }; + for desc in descriptors { + let r = if (desc.attributes as u32 & RESOURCE_ATTR_READ) != 0 { "R" } else { "." }; + let w = if (desc.attributes as u32 & RESOURCE_ATTR_WRITE) != 0 { "W" } else { "." }; + log::info!( + " IO: [0x{:04x}-0x{:04x}] {}{}", + desc.io_address, + (desc.io_address as u32).saturating_add(desc.length_or_width as u32).saturating_sub(1), + r, + w + ); + } + } + TYPE_MSR => { + let descriptors = unsafe { root.get_msr_descriptors(policy_ptr) }; + for desc in descriptors { + let r = if (desc.attributes as u32 & RESOURCE_ATTR_READ) != 0 { "R" } else { "." }; + let w = if (desc.attributes as u32 & RESOURCE_ATTR_WRITE) != 0 { "W" } else { "." }; + log::info!( + " MSR: [0x{:08x}-0x{:08x}] {}{}", + desc.msr_address, + desc.msr_address.saturating_add(desc.length as u32).saturating_sub(1), + r, + w + ); + } + } + TYPE_INSTRUCTION => { + let descriptors = unsafe { root.get_instruction_descriptors(policy_ptr) }; + for desc in descriptors { + let name = match desc.instruction_index { + 0 => "CLI", + 1 => "WBINVD", + 2 => "HLT", + _ => "UNKNOWN", + }; + let x = if (desc.attributes as u32 & RESOURCE_ATTR_EXECUTE) != 0 { "X" } else { "." }; + log::info!(" INSTRUCTION: {} {}", name, x); + } + } + TYPE_SAVE_STATE => { + let descriptors = unsafe { root.get_save_state_descriptors(policy_ptr) }; + for desc in descriptors { + let field = match desc.map_field { + 0 => "RAX", + 1 => "IO_TRAP", + _ => "UNKNOWN", + }; + let condition = match desc.access_condition { + 0 => "Unconditional", + 1 => "IoRead", + 2 => "IoWrite", + _ => "Unknown", + }; + log::info!(" SAVESTATE: {} attr=0x{:x} cond={}", field, desc.attributes, condition); + } + } + _ => { + log::error!(" Unknown policy type: {}", root.policy_type); + } + } + } +} + +/// Compares two policies of a given type (order-independent). +/// +/// # Safety +/// +/// The caller must ensure that both policy pointers are valid. +pub unsafe fn compare_policy_with_type(policy1_ptr: *const u8, policy2_ptr: *const u8, policy_type: u32) -> bool { + if policy1_ptr.is_null() || policy2_ptr.is_null() { + return false; + } + + let policy1 = unsafe { &*(policy1_ptr as *const SecurePolicyDataV1_0) }; + let policy2 = unsafe { &*(policy2_ptr as *const SecurePolicyDataV1_0) }; + + // Find policy roots for the given type + let roots1 = unsafe { policy1.get_policy_roots() }; + let roots2 = unsafe { policy2.get_policy_roots() }; + + let root1 = roots1.iter().find(|r| r.policy_type == policy_type); + let root2 = roots2.iter().find(|r| r.policy_type == policy_type); + + match (root1, root2) { + (None, None) => true, // Neither has this type + (Some(_), None) | (None, Some(_)) => false, // Only one has this type + (Some(r1), Some(r2)) => { + // Both have this type, compare + if r1.count != r2.count || r1.access_attr != r2.access_attr { + return false; + } + + // Compare descriptors (order-independent) + match policy_type { + TYPE_MEM => { + let descs1 = unsafe { r1.get_mem_descriptors(policy1_ptr) }; + let descs2 = unsafe { r2.get_mem_descriptors(policy2_ptr) }; + compare_mem_descriptors(descs1, descs2) + } + TYPE_IO => { + let descs1 = unsafe { r1.get_io_descriptors(policy1_ptr) }; + let descs2 = unsafe { r2.get_io_descriptors(policy2_ptr) }; + compare_io_descriptors(descs1, descs2) + } + TYPE_MSR => { + let descs1 = unsafe { r1.get_msr_descriptors(policy1_ptr) }; + let descs2 = unsafe { r2.get_msr_descriptors(policy2_ptr) }; + compare_msr_descriptors(descs1, descs2) + } + TYPE_INSTRUCTION => { + let descs1 = unsafe { r1.get_instruction_descriptors(policy1_ptr) }; + let descs2 = unsafe { r2.get_instruction_descriptors(policy2_ptr) }; + compare_instruction_descriptors(descs1, descs2) + } + TYPE_SAVE_STATE => { + let descs1 = unsafe { r1.get_save_state_descriptors(policy1_ptr) }; + let descs2 = unsafe { r2.get_save_state_descriptors(policy2_ptr) }; + compare_save_state_descriptors(descs1, descs2) + } + _ => false, + } + } + } +} + +/// Compares memory policies (convenience wrapper). +/// +/// # Safety +/// +/// The caller must ensure that both policy pointers are valid. +pub unsafe fn compare_memory_policy(policy1_ptr: *const u8, policy2_ptr: *const u8) -> bool { + unsafe { compare_policy_with_type(policy1_ptr, policy2_ptr, TYPE_MEM) } +} + +// Helper functions for order-independent comparison + +fn compare_mem_descriptors(descs1: &[MemDescriptorV1_0], descs2: &[MemDescriptorV1_0]) -> bool { + if descs1.len() != descs2.len() { + return false; + } + + // For each descriptor in descs1, check if it exists in descs2 + for d1 in descs1 { + let found = descs2.iter().any(|d2| { + d1.base_address == d2.base_address && d1.size == d2.size && d1.mem_attributes == d2.mem_attributes + }); + if !found { + return false; + } + } + true +} + +fn compare_io_descriptors(descs1: &[IoDescriptorV1_0], descs2: &[IoDescriptorV1_0]) -> bool { + if descs1.len() != descs2.len() { + return false; + } + + for d1 in descs1 { + let found = descs2.iter().any(|d2| { + d1.io_address == d2.io_address && d1.length_or_width == d2.length_or_width && d1.attributes == d2.attributes + }); + if !found { + return false; + } + } + true +} + +fn compare_msr_descriptors(descs1: &[MsrDescriptorV1_0], descs2: &[MsrDescriptorV1_0]) -> bool { + if descs1.len() != descs2.len() { + return false; + } + + for d1 in descs1 { + let found = descs2 + .iter() + .any(|d2| d1.msr_address == d2.msr_address && d1.length == d2.length && d1.attributes == d2.attributes); + if !found { + return false; + } + } + true +} + +fn compare_instruction_descriptors(descs1: &[InstructionDescriptorV1_0], descs2: &[InstructionDescriptorV1_0]) -> bool { + if descs1.len() != descs2.len() { + return false; + } + + for d1 in descs1 { + let found = + descs2.iter().any(|d2| d1.instruction_index == d2.instruction_index && d1.attributes == d2.attributes); + if !found { + return false; + } + } + true +} + +fn compare_save_state_descriptors(descs1: &[SaveStateDescriptorV1_0], descs2: &[SaveStateDescriptorV1_0]) -> bool { + if descs1.len() != descs2.len() { + return false; + } + + for d1 in descs1 { + let found = descs2.iter().any(|d2| { + d1.map_field == d2.map_field && d1.attributes == d2.attributes && d1.access_condition == d2.access_condition + }); + if !found { + return false; + } + } + true +} + +/// Errors that can occur during policy validation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PolicyCheckError { + /// The policy pointer is null. + NullPointer, + /// Invalid policy version. + InvalidVersion { major: u16, minor: u16 }, + /// A reserved field contains non-zero data. + InvalidReservedField { policy_type: u32, entry_index: usize }, + /// The same policy type appears multiple times. + DuplicatePolicyType { policy_type: u32 }, + /// Overlapping entries detected. + OverlappingEntries { policy_type: u32, entry1: usize, entry2: usize }, + /// Duplicate entries detected. + DuplicateEntries { policy_type: u32, entry1: usize, entry2: usize }, + /// Size mismatch. + SizeMismatch { expected: usize, declared: usize }, + /// Unrecognized policy type. + UnrecognizedPolicyType { policy_type: u32 }, + /// Unrecognized header bits. + UnrecognizedHeaderBits, + /// Unsupported attribute. + UnsupportedAttribute { policy_type: u32, entry_index: usize, attributes: u32 }, + /// Conflicting condition. + ConflictingCondition { entry_index: usize }, + /// Legacy memory policy detected. + LegacyMemoryPolicyDetected, + /// Range overflow. + RangeOverflow, +} + +/// Performs comprehensive security policy validation. +/// +/// # Safety +/// +/// The caller must ensure that `policy_ptr` points to a valid policy buffer. +pub unsafe fn security_policy_check(policy_ptr: *const u8) -> Result<(), PolicyCheckError> { + if policy_ptr.is_null() { + return Err(PolicyCheckError::NullPointer); + } + + let policy = unsafe { &*(policy_ptr as *const SecurePolicyDataV1_0) }; + + log::info!("Security policy check entry..."); + + // Version check + if !policy.is_valid_version() { + return Err(PolicyCheckError::InvalidVersion { major: policy.version_major, minor: policy.version_minor }); + } + + // Check for unrecognized header bits + if policy.reserved != 0 || policy.flags != 0 || policy.capabilities != 0 { + return Err(PolicyCheckError::UnrecognizedHeaderBits); + } + + let mut total_scanned_size = size_of::(); + let mut type_flags: u64 = 0; + + let policy_roots = unsafe { policy.get_policy_roots() }; + + for root in policy_roots.iter() { + let type_bit = 1u64 << root.policy_type; + + if (type_flags & type_bit) != 0 { + return Err(PolicyCheckError::DuplicatePolicyType { policy_type: root.policy_type }); + } + type_flags |= type_bit; + + if !root.has_valid_reserved() { + return Err(PolicyCheckError::InvalidReservedField { policy_type: root.policy_type, entry_index: 0 }); + } + + match root.policy_type { + TYPE_IO => { + unsafe { validate_io_policy(policy_ptr, root)? }; + total_scanned_size += (root.count as usize) * size_of::(); + } + TYPE_MEM => { + unsafe { validate_mem_policy(policy_ptr, root)? }; + total_scanned_size += (root.count as usize) * size_of::(); + } + TYPE_MSR => { + unsafe { validate_msr_policy(policy_ptr, root)? }; + total_scanned_size += (root.count as usize) * size_of::(); + } + TYPE_INSTRUCTION => { + unsafe { validate_instruction_policy(policy_ptr, root)? }; + total_scanned_size += (root.count as usize) * size_of::(); + } + TYPE_SAVE_STATE => { + unsafe { validate_save_state_policy(policy_ptr, root)? }; + total_scanned_size += (root.count as usize) * size_of::(); + } + _ => { + return Err(PolicyCheckError::UnrecognizedPolicyType { policy_type: root.policy_type }); + } + } + + total_scanned_size += size_of::(); + } + + if policy.memory_policy_count != 0 { + return Err(PolicyCheckError::LegacyMemoryPolicyDetected); + } + + if total_scanned_size != policy.size as usize { + return Err(PolicyCheckError::SizeMismatch { expected: total_scanned_size, declared: policy.size as usize }); + } + + log::info!("Security policy check passed."); + Ok(()) +} + +// Validation helper functions + +unsafe fn validate_io_policy(policy_base: *const u8, root: &PolicyRootV1) -> Result<(), PolicyCheckError> { + let descriptors = unsafe { root.get_io_descriptors(policy_base) }; + + for (i, desc) in descriptors.iter().enumerate() { + if desc.reserved != 0 { + return Err(PolicyCheckError::InvalidReservedField { policy_type: TYPE_IO, entry_index: i }); + } + } + Ok(()) +} + +unsafe fn validate_mem_policy(policy_base: *const u8, root: &PolicyRootV1) -> Result<(), PolicyCheckError> { + let descriptors = unsafe { root.get_mem_descriptors(policy_base) }; + + for (i, desc) in descriptors.iter().enumerate() { + if desc.reserved != 0 { + return Err(PolicyCheckError::InvalidReservedField { policy_type: TYPE_MEM, entry_index: i }); + } + } + Ok(()) +} + +unsafe fn validate_msr_policy(_policy_base: *const u8, _root: &PolicyRootV1) -> Result<(), PolicyCheckError> { + // MSR descriptors don't have reserved fields + Ok(()) +} + +unsafe fn validate_instruction_policy(policy_base: *const u8, root: &PolicyRootV1) -> Result<(), PolicyCheckError> { + let descriptors = unsafe { root.get_instruction_descriptors(policy_base) }; + + for (i, desc) in descriptors.iter().enumerate() { + if desc.reserved != 0 { + return Err(PolicyCheckError::InvalidReservedField { policy_type: TYPE_INSTRUCTION, entry_index: i }); + } + } + Ok(()) +} + +unsafe fn validate_save_state_policy(policy_base: *const u8, root: &PolicyRootV1) -> Result<(), PolicyCheckError> { + let descriptors = unsafe { root.get_save_state_descriptors(policy_base) }; + + for (i, desc) in descriptors.iter().enumerate() { + // Check for unsupported write attributes + if (desc.attributes & (RESOURCE_ATTR_WRITE | RESOURCE_ATTR_COND_WRITE)) != 0 { + return Err(PolicyCheckError::UnsupportedAttribute { + policy_type: TYPE_SAVE_STATE, + entry_index: i, + attributes: desc.attributes, + }); + } + + // Check for conflicting conditions + if (desc.attributes & RESOURCE_ATTR_COND_READ) == 0 && desc.access_condition != SVST_UNCONDITIONAL { + return Err(PolicyCheckError::ConflictingCondition { entry_index: i }); + } + + if desc.reserved != 0 { + return Err(PolicyCheckError::InvalidReservedField { policy_type: TYPE_SAVE_STATE, entry_index: i }); + } + } + Ok(()) +} + +/// Memory policy builder for collecting memory descriptors from page table walking. +/// +/// This is used to generate memory policy from page table entries. +pub struct MemoryPolicyBuilder { + /// Current descriptor being built + current: Option, + /// Maximum number of descriptors we can store + max_count: usize, + /// Buffer for descriptors + buffer_ptr: *mut MemDescriptorV1_0, + /// Current count of descriptors + count: usize, +} + +impl MemoryPolicyBuilder { + /// Creates a new memory policy builder. + /// + /// # Safety + /// + /// The caller must ensure that `buffer_ptr` points to a valid buffer + /// with space for at least `max_count` descriptors. + pub unsafe fn new(buffer_ptr: *mut MemDescriptorV1_0, max_count: usize) -> Self { + Self { current: None, max_count, buffer_ptr, count: 0 } + } + + /// Adds a memory region to the policy. + /// + /// Adjacent regions with the same attributes will be coalesced. + /// + /// # Returns + /// + /// Returns `Ok(())` if successful, or `Err(())` if the buffer is full. + pub fn add_region(&mut self, base: u64, size: u64, attributes: u32) -> Result<(), ()> { + let new_desc = MemDescriptorV1_0 { base_address: base, size, mem_attributes: attributes, reserved: 0 }; + + if let Some(ref mut current) = self.current { + // Check if we can coalesce with current + let current_end = current.base_address.saturating_add(current.size); + if base == current_end && attributes == current.mem_attributes { + // Coalesce + current.size = current.size.saturating_add(size); + return Ok(()); + } else { + // Flush current and start new + self.flush_current()?; + } + } + + self.current = Some(new_desc); + Ok(()) + } + + /// Flushes the current descriptor to the buffer. + fn flush_current(&mut self) -> Result<(), ()> { + if let Some(desc) = self.current.take() { + if self.count >= self.max_count { + return Err(()); + } + + // SAFETY: We checked bounds + unsafe { + *self.buffer_ptr.add(self.count) = desc; + } + self.count += 1; + } + Ok(()) + } + + /// Finishes building and returns the count of descriptors. + pub fn finish(mut self) -> Result { + self.flush_current()?; + Ok(self.count) + } + + /// Gets the current count of descriptors. + pub fn count(&self) -> usize { + self.count + if self.current.is_some() { 1 } else { 0 } + } +} + +/// Page table entry for x86_64 4-level paging. +/// +/// This structure represents entries in PML4, PDPE, PDE, and PTE tables. +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct PageTableEntry { + /// Raw 64-bit value of the page table entry. + pub value: u64, +} + +impl PageTableEntry { + /// Present bit (bit 0): Entry is valid if set. + pub const PRESENT: u64 = 1 << 0; + /// Read/Write bit (bit 1): Writable if set. + pub const READ_WRITE: u64 = 1 << 1; + /// User/Supervisor bit (bit 2): User-mode accessible if set. + pub const USER_SUPERVISOR: u64 = 1 << 2; + /// Page Size bit (bit 7): Large page (2MB or 1GB) if set. + pub const PAGE_SIZE: u64 = 1 << 7; + /// No Execute bit (bit 63): Not executable if set. + pub const NO_EXECUTE: u64 = 1 << 63; + + /// Address mask for 4KB page table base addresses. + pub const ADDR_MASK_4K: u64 = 0x000F_FFFF_FFFF_F000; + /// Address mask for 2MB large page addresses. + pub const ADDR_MASK_2M: u64 = 0x000F_FFFF_FFE0_0000; + /// Address mask for 1GB huge page addresses. + pub const ADDR_MASK_1G: u64 = 0x000F_FFFF_C000_0000; + + /// Size of a 4KB page. + pub const SIZE_4K: u64 = 0x1000; + /// Size of a 2MB large page. + pub const SIZE_2M: u64 = 0x20_0000; + /// Size of a 1GB huge page. + pub const SIZE_1G: u64 = 0x4000_0000; + + /// Creates a new page table entry from a raw value. + #[inline] + pub const fn new(value: u64) -> Self { + Self { value } + } + + /// Returns true if the entry is present. + #[inline] + pub const fn is_present(&self) -> bool { + (self.value & Self::PRESENT) != 0 + } + + /// Returns true if the entry is writable. + #[inline] + pub const fn is_writable(&self) -> bool { + (self.value & Self::READ_WRITE) != 0 + } + + /// Returns true if the entry is user-mode accessible. + #[inline] + pub const fn is_user(&self) -> bool { + (self.value & Self::USER_SUPERVISOR) != 0 + } + + /// Returns true if this is a large/huge page (PS bit set). + #[inline] + pub const fn is_large_page(&self) -> bool { + (self.value & Self::PAGE_SIZE) != 0 + } + + /// Returns true if the page is executable (NX bit NOT set). + #[inline] + pub const fn is_executable(&self) -> bool { + (self.value & Self::NO_EXECUTE) == 0 + } + + /// Gets the physical address of the next-level page table (4KB aligned). + #[inline] + pub const fn next_table_addr(&self) -> u64 { + self.value & Self::ADDR_MASK_4K + } + + /// Gets the physical address of a 2MB large page. + #[inline] + pub const fn large_page_addr(&self) -> u64 { + self.value & Self::ADDR_MASK_2M + } + + /// Gets the physical address of a 1GB huge page. + #[inline] + pub const fn huge_page_addr(&self) -> u64 { + self.value & Self::ADDR_MASK_1G + } + + /// Gets the physical address of a 4KB page. + #[inline] + pub const fn page_addr(&self) -> u64 { + self.value & Self::ADDR_MASK_4K + } + + /// Converts page table entry attributes to policy memory attributes. + /// + /// Inherits R/W/X permissions from upper-level entries. + #[inline] + pub fn to_policy_attrs(&self, inherited_attrs: u32) -> u32 { + if !self.is_present() { + return 0; + } + + let mut attrs = RESOURCE_ATTR_READ; + + if self.is_writable() { + attrs |= RESOURCE_ATTR_WRITE; + } + + if self.is_executable() { + attrs |= RESOURCE_ATTR_EXECUTE; + } + + // Inherit restrictions from upper-level tables + attrs & inherited_attrs + } +} + +/// Errors that can occur during page table walking. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PageTableWalkError { + /// Buffer is full, cannot add more descriptors. + BufferFull, + /// The CR3 value is invalid (null). + InvalidCr3, + /// Paging is not in the expected mode. + UnsupportedPagingMode, +} + +/// Callback type for checking if a buffer is inside MMRAM. +/// +/// Returns `true` if the buffer `[base, base + size)` is fully inside MMRAM. +pub type IsInsideMmramFn = fn(base: u64, size: u64) -> bool; + +/// Walks x86_64 4-level page tables and generates memory policy descriptors. +/// +/// This function traverses the page table hierarchy starting from the PML4 +/// table (pointed to by CR3), and for each mapped page, generates a memory +/// policy descriptor with the effective R/W/X attributes. +/// +/// Adjacent pages with the same attributes are coalesced into single descriptors. +/// +/// # Arguments +/// +/// * `cr3` - The CR3 register value (physical address of PML4 table) +/// * `buffer` - Buffer to store the generated memory descriptors +/// * `max_count` - Maximum number of descriptors the buffer can hold +/// * `is_inside_mmram` - Callback to check if a region is inside MMRAM +/// (regions fully inside MMRAM are skipped) +/// +/// # Returns +/// +/// The number of memory descriptors generated, or an error. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `cr3` points to a valid PML4 table +/// - `buffer` has space for at least `max_count` descriptors +/// - The page table memory is accessible and won't change during the walk +/// +/// # Example +/// +/// ```rust,ignore +/// use patina_mm_policy::{walk_page_table, MemDescriptorV1_0, default_mmram_check}; +/// +/// let cr3 = read_cr3(); // Read from hardware +/// let mut buffer = [MemDescriptorV1_0::default(); 1024]; +/// +/// let count = unsafe { +/// walk_page_table(cr3, buffer.as_mut_ptr(), buffer.len(), default_mmram_check)? +/// }; +/// +/// println!("Generated {} memory policy descriptors", count); +/// ``` +pub unsafe fn walk_page_table( + cr3: u64, + buffer: *mut MemDescriptorV1_0, + max_count: usize, + is_inside_mmram: IsInsideMmramFn, +) -> Result { + if cr3 == 0 || buffer.is_null() { + return Err(PageTableWalkError::InvalidCr3); + } + + let pml4_base = cr3 & PageTableEntry::ADDR_MASK_4K; + let mut builder = unsafe { MemoryPolicyBuilder::new(buffer, max_count) }; + + // Walk PML4 (512 entries) + let pml4_table = pml4_base as *const PageTableEntry; + + for i in 0..512u64 { + let pml4e = unsafe { *pml4_table.add(i as usize) }; + if !pml4e.is_present() { + continue; + } + + // Calculate inherited attributes from PML4 entry + let pml4_attrs = pml4e.to_policy_attrs(RESOURCE_ATTR_READ | RESOURCE_ATTR_WRITE | RESOURCE_ATTR_EXECUTE); + + // Walk PDPE (512 entries) + let pdpe_table = pml4e.next_table_addr() as *const PageTableEntry; + + for j in 0..512u64 { + let pdpe = unsafe { *pdpe_table.add(j as usize) }; + if !pdpe.is_present() { + continue; + } + + let pdpe_attrs = pdpe.to_policy_attrs(pml4_attrs); + + // Check for 1GB huge page + if pdpe.is_large_page() { + let page_addr = pdpe.huge_page_addr(); + let page_size = PageTableEntry::SIZE_1G; + + // Skip if fully inside MMRAM + if is_inside_mmram(page_addr, page_size) { + continue; + } + + if builder.add_region(page_addr, page_size, pdpe_attrs).is_err() { + return Err(PageTableWalkError::BufferFull); + } + continue; + } + + // Walk PDE (512 entries) + let pde_table = pdpe.next_table_addr() as *const PageTableEntry; + + for k in 0..512u64 { + let pde = unsafe { *pde_table.add(k as usize) }; + if !pde.is_present() { + continue; + } + + let pde_attrs = pde.to_policy_attrs(pdpe_attrs); + + // Check for 2MB large page + if pde.is_large_page() { + let page_addr = pde.large_page_addr(); + let page_size = PageTableEntry::SIZE_2M; + + // Skip if fully inside MMRAM + if is_inside_mmram(page_addr, page_size) { + continue; + } + + if builder.add_region(page_addr, page_size, pde_attrs).is_err() { + return Err(PageTableWalkError::BufferFull); + } + continue; + } + + // Walk PTE (512 entries) + let pte_table = pde.next_table_addr() as *const PageTableEntry; + + for l in 0..512u64 { + let pte = unsafe { *pte_table.add(l as usize) }; + if !pte.is_present() { + continue; + } + + let page_addr = pte.page_addr(); + let page_size = PageTableEntry::SIZE_4K; + + // Skip if fully inside MMRAM + if is_inside_mmram(page_addr, page_size) { + continue; + } + + let pte_attrs = pte.to_policy_attrs(pde_attrs); + + if builder.add_region(page_addr, page_size, pte_attrs).is_err() { + return Err(PageTableWalkError::BufferFull); + } + } + } + } + } + + builder.finish().map_err(|()| PageTableWalkError::BufferFull) +} + +/// Statistics from page table walking. +#[derive(Debug, Clone, Copy, Default)] +pub struct PageTableWalkStats { + /// Number of PML4 entries traversed. + pub pml4_entries: usize, + /// Number of PDPE entries traversed. + pub pdpe_entries: usize, + /// Number of PDE entries traversed. + pub pde_entries: usize, + /// Number of PTE entries traversed. + pub pte_entries: usize, + /// Number of 1GB huge pages found. + pub huge_pages_1g: usize, + /// Number of 2MB large pages found. + pub large_pages_2m: usize, + /// Number of 4KB pages found. + pub pages_4k: usize, + /// Number of pages skipped (inside MMRAM). + pub skipped_mmram: usize, +} + +/// Walks x86_64 4-level page tables with statistics. +/// +/// This is the same as [`walk_page_table`] but also returns statistics +/// about the page table structure. +/// +/// # Safety +/// +/// Same requirements as [`walk_page_table`]. +pub unsafe fn walk_page_table_with_stats( + cr3: u64, + buffer: *mut MemDescriptorV1_0, + max_count: usize, + is_inside_mmram: IsInsideMmramFn, +) -> Result<(usize, PageTableWalkStats), PageTableWalkError> { + if cr3 == 0 || buffer.is_null() { + return Err(PageTableWalkError::InvalidCr3); + } + + let pml4_base = cr3 & PageTableEntry::ADDR_MASK_4K; + let mut builder = unsafe { MemoryPolicyBuilder::new(buffer, max_count) }; + let mut stats = PageTableWalkStats::default(); + + // Walk PML4 (512 entries) + let pml4_table = pml4_base as *const PageTableEntry; + + for i in 0..512u64 { + let pml4e = unsafe { *pml4_table.add(i as usize) }; + if !pml4e.is_present() { + continue; + } + + stats.pml4_entries += 1; + + let pml4_attrs = pml4e.to_policy_attrs(RESOURCE_ATTR_READ | RESOURCE_ATTR_WRITE | RESOURCE_ATTR_EXECUTE); + + // Walk PDPE + let pdpe_table = pml4e.next_table_addr() as *const PageTableEntry; + + for j in 0..512u64 { + let pdpe = unsafe { *pdpe_table.add(j as usize) }; + if !pdpe.is_present() { + continue; + } + + stats.pdpe_entries += 1; + let pdpe_attrs = pdpe.to_policy_attrs(pml4_attrs); + + // 1GB huge page + if pdpe.is_large_page() { + let page_addr = pdpe.huge_page_addr(); + let page_size = PageTableEntry::SIZE_1G; + + if is_inside_mmram(page_addr, page_size) { + stats.skipped_mmram += 1; + continue; + } + + stats.huge_pages_1g += 1; + if builder.add_region(page_addr, page_size, pdpe_attrs).is_err() { + return Err(PageTableWalkError::BufferFull); + } + continue; + } + + // Walk PDE + let pde_table = pdpe.next_table_addr() as *const PageTableEntry; + + for k in 0..512u64 { + let pde = unsafe { *pde_table.add(k as usize) }; + if !pde.is_present() { + continue; + } + + stats.pde_entries += 1; + let pde_attrs = pde.to_policy_attrs(pdpe_attrs); + + // 2MB large page + if pde.is_large_page() { + let page_addr = pde.large_page_addr(); + let page_size = PageTableEntry::SIZE_2M; + + if is_inside_mmram(page_addr, page_size) { + stats.skipped_mmram += 1; + continue; + } + + stats.large_pages_2m += 1; + if builder.add_region(page_addr, page_size, pde_attrs).is_err() { + return Err(PageTableWalkError::BufferFull); + } + continue; + } + + // Walk PTE + let pte_table = pde.next_table_addr() as *const PageTableEntry; + + for l in 0..512u64 { + let pte = unsafe { *pte_table.add(l as usize) }; + if !pte.is_present() { + continue; + } + + stats.pte_entries += 1; + let page_addr = pte.page_addr(); + let page_size = PageTableEntry::SIZE_4K; + + if is_inside_mmram(page_addr, page_size) { + stats.skipped_mmram += 1; + continue; + } + + stats.pages_4k += 1; + let pte_attrs = pte.to_policy_attrs(pde_attrs); + + if builder.add_region(page_addr, page_size, pte_attrs).is_err() { + return Err(PageTableWalkError::BufferFull); + } + } + } + } + } + + let count = builder.finish().map_err(|()| PageTableWalkError::BufferFull)?; + Ok((count, stats)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dump_mem_policy_entry() { + // Just verify it doesn't panic + let desc = MemDescriptorV1_0 { + base_address: 0x1000, + size: 0x1000, + mem_attributes: RESOURCE_ATTR_READ | RESOURCE_ATTR_WRITE, + reserved: 0, + }; + dump_mem_policy_entry(&desc); + } + + #[test] + fn test_compare_mem_descriptors() { + let descs1 = [ + MemDescriptorV1_0 { base_address: 0x1000, size: 0x1000, mem_attributes: 1, reserved: 0 }, + MemDescriptorV1_0 { base_address: 0x2000, size: 0x1000, mem_attributes: 2, reserved: 0 }, + ]; + let descs2 = [ + MemDescriptorV1_0 { base_address: 0x2000, size: 0x1000, mem_attributes: 2, reserved: 0 }, + MemDescriptorV1_0 { base_address: 0x1000, size: 0x1000, mem_attributes: 1, reserved: 0 }, + ]; + + // Order-independent comparison should succeed + assert!(compare_mem_descriptors(&descs1, &descs2)); + } + + #[test] + fn test_compare_mem_descriptors_mismatch() { + let descs1 = [MemDescriptorV1_0 { base_address: 0x1000, size: 0x1000, mem_attributes: 1, reserved: 0 }]; + let descs2 = [MemDescriptorV1_0 { base_address: 0x1000, size: 0x2000, mem_attributes: 1, reserved: 0 }]; + + assert!(!compare_mem_descriptors(&descs1, &descs2)); + } +} diff --git a/components/patina_mm_policy/src/lib.rs b/components/patina_mm_policy/src/lib.rs new file mode 100644 index 000000000..30430e1bb --- /dev/null +++ b/components/patina_mm_policy/src/lib.rs @@ -0,0 +1,45 @@ +//! MM Supervisor Secure Policy Library +//! +//! This crate provides a comprehensive policy management library for the MM Supervisor, +//! including policy data structures, access validation (policy gate), and helper utilities. +//! +//! ## Overview +//! +//! The MM Supervisor uses security policies to control what resources user-mode MM drivers +//! can access. This crate provides: +//! +//! - **Data Structures**: Rust definitions matching the C structures in `SmmSecurePolicy.h` +//! - **Policy Gate**: Runtime access validation for I/O, MSR, instruction, and save state +//! - **Helpers**: Dump, compare, and page table walking utilities +//! +//! ## Example +//! +//! ```rust,ignore +//! use patina_mm_policy::{PolicyGate, AccessType, IoWidth}; +//! +//! // Initialize the policy gate with a policy buffer +//! let gate = unsafe { PolicyGate::new(policy_ptr) }?; +//! +//! // Check if I/O access is allowed +//! if gate.is_io_allowed(0x3F8, IoWidth::Byte, AccessType::Read).is_ok() { +//! // Perform the I/O operation +//! } +//! ``` +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +#![cfg_attr(all(not(feature = "std"), not(test)), no_std)] +#![allow(dead_code)] + +mod gate; +mod helpers; +mod types; + +pub use gate::*; +pub use helpers::*; +pub use types::*; diff --git a/components/patina_mm_policy/src/types.rs b/components/patina_mm_policy/src/types.rs new file mode 100644 index 000000000..b56051637 --- /dev/null +++ b/components/patina_mm_policy/src/types.rs @@ -0,0 +1,397 @@ +//! MM Supervisor Secure Policy Data Structures +//! +//! Rust definitions matching the C structures in `SmmSecurePolicy.h`. + +use core::slice; + +/// Memory policy descriptor type. +pub const TYPE_MEM: u32 = 1; +/// I/O policy descriptor type. +pub const TYPE_IO: u32 = 2; +/// MSR policy descriptor type. +pub const TYPE_MSR: u32 = 3; +/// Instruction policy descriptor type. +pub const TYPE_INSTRUCTION: u32 = 4; +/// Save state policy descriptor type. +pub const TYPE_SAVE_STATE: u32 = 5; + +/// Access attribute: Allow access to resources described by this policy root. +pub const ACCESS_ATTR_ALLOW: u8 = 0; +/// Access attribute: Deny access to resources described by this policy root. +pub const ACCESS_ATTR_DENY: u8 = 1; + +/// Resource attribute: Read access. +pub const RESOURCE_ATTR_READ: u32 = 0x01; +/// Resource attribute: Write access. +pub const RESOURCE_ATTR_WRITE: u32 = 0x02; +/// Resource attribute: Execute access. +pub const RESOURCE_ATTR_EXECUTE: u32 = 0x04; +/// Resource attribute: Strict width (for I/O - must match exact width). +pub const RESOURCE_ATTR_STRICT_WIDTH: u32 = 0x08; +/// Resource attribute: Conditional read access. +pub const RESOURCE_ATTR_COND_READ: u32 = 0x10; +/// Resource attribute: Conditional write access. +pub const RESOURCE_ATTR_COND_WRITE: u32 = 0x20; + +/// Instruction index for CLI. +pub const INSTRUCTION_CLI: u16 = 0; +/// Instruction index for WBINVD. +pub const INSTRUCTION_WBINVD: u16 = 1; +/// Instruction index for HLT. +pub const INSTRUCTION_HLT: u16 = 2; +/// Total count of privileged instructions tracked. +pub const INSTRUCTION_COUNT: u16 = 3; + +/// Privileged instruction types. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum Instruction { + /// CLI - Clear Interrupt Flag + Cli = 0, + /// WBINVD - Write Back and Invalidate Cache + Wbinvd = 1, + /// HLT - Halt + Hlt = 2, +} + +impl Instruction { + /// Convert to instruction index. + pub fn as_index(self) -> u16 { + self as u16 + } + + /// Create from instruction index. + pub fn from_index(index: u16) -> Option { + match index { + 0 => Some(Self::Cli), + 1 => Some(Self::Wbinvd), + 2 => Some(Self::Hlt), + _ => None, + } + } +} + +/// Save state field: RAX register. +pub const SVST_RAX: u32 = 0; +/// Save state field: I/O trap information. +pub const SVST_IO_TRAP: u32 = 1; +/// Total count of save state fields tracked. +pub const SVST_COUNT: u32 = 2; + +/// Save state map fields. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum SaveStateField { + /// RAX register + Rax = 0, + /// I/O trap information + IoTrap = 1, +} + +impl SaveStateField { + /// Convert to field index. + pub fn as_index(self) -> u32 { + self as u32 + } + + /// Create from field index. + pub fn from_index(index: u32) -> Option { + match index { + 0 => Some(Self::Rax), + 1 => Some(Self::IoTrap), + _ => None, + } + } +} + +/// Save state access is unconditional. +pub const SVST_UNCONDITIONAL: u32 = 0; +/// Save state access is conditional on I/O read trap. +pub const SVST_CONDITION_IO_RD: u32 = 1; +/// Save state access is conditional on I/O write trap. +pub const SVST_CONDITION_IO_WR: u32 = 2; +/// Total count of save state conditions. +pub const SVST_CONDITION_COUNT: u32 = 3; + +/// Save state access conditions. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum SaveStateCondition { + /// Unconditional access + Unconditional = 0, + /// Conditional on I/O read trap + IoRead = 1, + /// Conditional on I/O write trap + IoWrite = 2, +} + +/// Type of access being requested. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AccessType { + /// Read access + Read, + /// Write access + Write, + /// Execute access (for instructions) + Execute, +} + +impl AccessType { + /// Convert to resource attribute mask. + pub fn as_attr_mask(self) -> u32 { + match self { + AccessType::Read => RESOURCE_ATTR_READ, + AccessType::Write => RESOURCE_ATTR_WRITE, + AccessType::Execute => RESOURCE_ATTR_EXECUTE, + } + } +} + +/// I/O access width. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum IoWidth { + /// 8-bit (1 byte) access + Byte = 1, + /// 16-bit (2 byte) access + Word = 2, + /// 32-bit (4 byte) access + Dword = 4, +} + +impl IoWidth { + /// Get the size in bytes. + pub fn size(self) -> u32 { + self as u32 + } + + /// Create from size in bytes. + pub fn from_size(size: u32) -> Option { + match size { + 1 => Some(Self::Byte), + 2 => Some(Self::Word), + 4 => Some(Self::Dword), + _ => None, + } + } +} + +/// Memory policy descriptor (V1.0). +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct MemDescriptorV1_0 { + /// Base address of memory region. + pub base_address: u64, + /// Size of memory region in bytes. + pub size: u64, + /// Memory attributes (combination of `RESOURCE_ATTR_*`). + pub mem_attributes: u32, + /// Reserved, must be 0. + pub reserved: u32, +} + +/// I/O policy descriptor (V1.0). +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct IoDescriptorV1_0 { + /// Base I/O port address. + pub io_address: u16, + /// Length or width of the I/O range. + pub length_or_width: u16, + /// I/O attributes (combination of `RESOURCE_ATTR_*`). + pub attributes: u16, + /// Reserved, must be 0. + pub reserved: u16, +} + +/// MSR policy descriptor (V1.0). +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct MsrDescriptorV1_0 { + /// Base MSR address. + pub msr_address: u32, + /// Length of MSR range. + pub length: u16, + /// MSR attributes (combination of `RESOURCE_ATTR_*`). + pub attributes: u16, +} + +/// Instruction policy descriptor (V1.0). +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct InstructionDescriptorV1_0 { + /// Instruction index (one of `INSTRUCTION_*` constants). + pub instruction_index: u16, + /// Instruction attributes (combination of `RESOURCE_ATTR_*`). + pub attributes: u16, + /// Reserved, must be 0. + pub reserved: u32, +} + +/// Save state policy descriptor (V1.0). +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct SaveStateDescriptorV1_0 { + /// Save state map field (one of `SVST_*` constants). + pub map_field: u32, + /// Save state attributes (combination of `RESOURCE_ATTR_*`). + pub attributes: u32, + /// Access condition (one of `SVST_CONDITION_*` constants). + pub access_condition: u32, + /// Reserved, must be 0. + pub reserved: u32, +} + +/// Policy root structure (V1). +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct PolicyRootV1 { + /// Version of this policy root structure. + pub version: u32, + /// Size of this policy root structure in bytes. + pub policy_root_size: u32, + /// Type of descriptors (one of `TYPE_*` constants). + pub policy_type: u32, + /// Offset in bytes from policy data start to the descriptors. + pub offset: u32, + /// Number of descriptor entries. + pub count: u32, + /// Access attribute (one of `ACCESS_ATTR_*` constants). + pub access_attr: u8, + /// Reserved, must be all zeros. + pub reserved: [u8; 3], +} + +/// Secure policy data header (V1.0). +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct SecurePolicyDataV1_0 { + /// Minor version (should be 0x0000). + pub version_minor: u16, + /// Major version (should be 0x0001). + pub version_major: u16, + /// Total size in bytes of the entire policy block. + pub size: u32, + /// Offset to legacy memory policy (0 if not supported). + pub memory_policy_offset: u32, + /// Count of legacy memory policy entries (0 if not supported). + pub memory_policy_count: u32, + /// Flag field indicating supervisor status. + pub flags: u32, + /// Capability field indicating features supported by supervisor. + pub capabilities: u32, + /// Reserved, must be 0. + pub reserved: u64, + /// Offset from this structure to the policy root array. + pub policy_root_offset: u32, + /// Number of policy roots. + pub policy_root_count: u32, +} + +impl SecurePolicyDataV1_0 { + /// Returns true if this is a valid V1.0 policy header. + pub fn is_valid_version(&self) -> bool { + self.version_major == 1 && self.version_minor == 0 + } + + /// Gets a pointer to the policy root array. + /// + /// # Safety + /// + /// The caller must ensure that this structure is part of a valid policy buffer. + pub unsafe fn get_policy_roots_ptr(&self) -> *const PolicyRootV1 { + let base = self as *const Self as *const u8; + unsafe { base.add(self.policy_root_offset as usize) as *const PolicyRootV1 } + } + + /// Gets a slice of policy roots. + /// + /// # Safety + /// + /// The caller must ensure that this structure is part of a valid policy buffer. + pub unsafe fn get_policy_roots(&self) -> &[PolicyRootV1] { + unsafe { slice::from_raw_parts(self.get_policy_roots_ptr(), self.policy_root_count as usize) } + } +} + +impl PolicyRootV1 { + /// Returns true if the reserved fields are all zeros. + pub fn has_valid_reserved(&self) -> bool { + self.reserved == [0, 0, 0] + } + + /// Gets a pointer to the descriptors for this policy root. + /// + /// # Safety + /// + /// The caller must ensure that `policy_base` points to a valid policy buffer. + pub unsafe fn get_descriptors_ptr(&self, policy_base: *const u8) -> *const T { + unsafe { policy_base.add(self.offset as usize) as *const T } + } + + /// Gets memory descriptors from this policy root. + /// + /// # Safety + /// + /// Caller must ensure this policy root has `policy_type == TYPE_MEM`. + pub unsafe fn get_mem_descriptors(&self, policy_base: *const u8) -> &[MemDescriptorV1_0] { + unsafe { + slice::from_raw_parts(self.get_descriptors_ptr::(policy_base), self.count as usize) + } + } + + /// Gets I/O descriptors from this policy root. + /// + /// # Safety + /// + /// Caller must ensure this policy root has `policy_type == TYPE_IO`. + pub unsafe fn get_io_descriptors(&self, policy_base: *const u8) -> &[IoDescriptorV1_0] { + unsafe { slice::from_raw_parts(self.get_descriptors_ptr::(policy_base), self.count as usize) } + } + + /// Gets MSR descriptors from this policy root. + /// + /// # Safety + /// + /// Caller must ensure this policy root has `policy_type == TYPE_MSR`. + pub unsafe fn get_msr_descriptors(&self, policy_base: *const u8) -> &[MsrDescriptorV1_0] { + unsafe { + slice::from_raw_parts(self.get_descriptors_ptr::(policy_base), self.count as usize) + } + } + + /// Gets instruction descriptors from this policy root. + /// + /// # Safety + /// + /// Caller must ensure this policy root has `policy_type == TYPE_INSTRUCTION`. + pub unsafe fn get_instruction_descriptors(&self, policy_base: *const u8) -> &[InstructionDescriptorV1_0] { + unsafe { + slice::from_raw_parts( + self.get_descriptors_ptr::(policy_base), + self.count as usize, + ) + } + } + + /// Gets save state descriptors from this policy root. + /// + /// # Safety + /// + /// Caller must ensure this policy root has `policy_type == TYPE_SAVE_STATE`. + pub unsafe fn get_save_state_descriptors(&self, policy_base: *const u8) -> &[SaveStateDescriptorV1_0] { + unsafe { + slice::from_raw_parts(self.get_descriptors_ptr::(policy_base), self.count as usize) + } + } +} + +const _: () = { + assert!(core::mem::size_of::() == 24); + assert!(core::mem::size_of::() == 8); + assert!(core::mem::size_of::() == 8); + assert!(core::mem::size_of::() == 8); + assert!(core::mem::size_of::() == 16); + assert!(core::mem::size_of::() == 24); + assert!(core::mem::size_of::() == 40); +}; diff --git a/core/patina_internal_cpu/Cargo.toml b/core/patina_internal_cpu/Cargo.toml index 4a435653a..aa3efcff6 100644 --- a/core/patina_internal_cpu/Cargo.toml +++ b/core/patina_internal_cpu/Cargo.toml @@ -46,3 +46,6 @@ serial_test = { workspace = true } default = [] std = [] doc = [] +alloc = [] +save_state_intel = [] +save_state_amd = [] diff --git a/core/patina_internal_cpu/src/lib.rs b/core/patina_internal_cpu/src/lib.rs index e3b6a7586..d4a6214a0 100644 --- a/core/patina_internal_cpu/src/lib.rs +++ b/core/patina_internal_cpu/src/lib.rs @@ -15,3 +15,4 @@ pub mod cpu; pub mod interrupts; pub mod paging; +pub mod save_state; diff --git a/core/patina_internal_cpu/src/save_state/amd.rs b/core/patina_internal_cpu/src/save_state/amd.rs new file mode 100644 index 000000000..b1a919b8d --- /dev/null +++ b/core/patina_internal_cpu/src/save_state/amd.rs @@ -0,0 +1,346 @@ +//! AMD64 SMRAM Save State Map +//! +//! Register-to-offset lookup table for the AMD 64-bit SMRAM save state +//! layout (`AMD_SMRAM_SAVE_STATE_MAP64`). The save state area starts at +//! `SMBASE + 0xFC00` (the address stored in `CpuSaveState[CpuIndex]`). +//! All offsets are relative to that base. +//! +//! Reference: AMD64 Architecture Programmer's Manual Vol 2, Table 10-2; +//! MdePkg `AmdSmramSaveStateMap.h`. +//! +//! ## Key Differences from Intel +//! +//! - Segment selectors are 2 bytes (UINT16) vs Intel's 4-byte fields. +//! - GDT, IDT, and LDT limits **are** supported (Intel returns `None`). +//! - CR4 is 8 bytes (Intel stores only 4 bytes). +//! - All 8-byte registers are stored contiguously (Intel splits DT bases). +//! - IO information uses `IO_DWord` at offset 0x2C0 with a different bit +//! layout from Intel's `IOMisc`. +//! - AMD64 always operates in 64-bit mode during SMM, so the LMA +//! pseudo-register always returns 64-bit without checking EFER.LMA. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 + +use super::{ + IO_TYPE_INPUT, IO_TYPE_OUTPUT, IO_WIDTH_UINT8, IO_WIDTH_UINT16, IO_WIDTH_UINT32, MmSaveStateRegister, ParsedIoInfo, + RegisterInfo, VendorConstants, +}; + +/// AMD-specific offsets and behaviour constants. +pub static VENDOR_CONSTANTS: VendorConstants = VendorConstants { + smmrevid_offset: 0x02FC, + io_info_offset: 0x02C0, + efer_offset: 0x02D0, + rax_offset: 0x03F8, + min_rev_id_io: 0x30064, + lma_always_64: true, +}; + +/// IO_DWord bit 0: direction (0 = WRITE/OUT, 1 = READ/IN). +const IO_DIRECTION_IN: u32 = 1; +//const IO_DIRECTION_OUT: u32 = 0; + +/// IO_DWord bits \[5:4\]: data size encoding. +const IO_SIZE_BYTE: u32 = 0; +const IO_SIZE_WORD: u32 = 1; +//const IO_SIZE_RESERVED: u32 = 2; +const IO_SIZE_DWORD: u32 = 3; + +/// +/// AMD save state struct layout (from SMBASE + 0xFC00): +/// +/// +0x000 – 0x1FF: Reserved (padding). +/// +0x200 – 0x25F: Segment descriptors (ES, CS, SS, DS, FS, GS) — 16 bytes each. +/// +0x260 – 0x29F: System descriptors (GDTR, IDTR, LDTR, TR) — 16 bytes each. +/// +0x2A0 – 0x2BF: MSRs (KernelGsBase, STAR, LSTAR, CSTAR). +/// +0x2C0: IO_DWord (4 bytes). +/// +0x2D0: EFER (8 bytes). +/// +0x2FC: SMMRevId (4 bytes). +/// +0x338 – 0x3FF: Registers (DR7, DR6, CR4, CR3, CR0, RFLAGS, RIP, R15..R8, +/// RBP, RSP, RBX, RDI, RSI, RDX, RCX, RAX). +/// +/// Each segment descriptor is 16 bytes: +/// +0: Selector (UINT16) +/// +2: Attributes (UINT16) +/// +4: Limit (UINT32) +/// +8: BaseLoDword (UINT32) +/// +C: BaseHiDword (UINT32) + +/// +/// Looks up the AMD64 save state register info for a PI register. +/// +/// Returns `None` for pseudo-registers (IO, LMA, ProcessorId) and for +/// `LdtInfo` (not supported in AMD's save state map). +pub fn register_info(reg: MmSaveStateRegister) -> Option { + match reg { + MmSaveStateRegister::GdtBase => Some(RegisterInfo { lo_offset: 0x0268, hi_offset: 0x026C, native_width: 8 }), + MmSaveStateRegister::IdtBase => Some(RegisterInfo { lo_offset: 0x0278, hi_offset: 0x027C, native_width: 8 }), + // NOTE: The C reference code has a copy-paste bug where both lo and + // hi point to `_LDTRBaseLoDword` (0x288). The correct hi offset is + // `_LDTRBaseHiDword` (0x28C). + MmSaveStateRegister::LdtBase => Some(RegisterInfo { lo_offset: 0x0288, hi_offset: 0x028C, native_width: 8 }), + + // GDT and IDT limits are architecturally 16-bit, stored in UINT32 + // fields. Only the lower 2 bytes are meaningful. + MmSaveStateRegister::GdtLimit => Some(RegisterInfo { lo_offset: 0x0264, hi_offset: 0, native_width: 2 }), + MmSaveStateRegister::IdtLimit => Some(RegisterInfo { lo_offset: 0x0274, hi_offset: 0, native_width: 2 }), + // LDT limit is a system-segment limit (up to 32 bits in long mode). + MmSaveStateRegister::LdtLimit => Some(RegisterInfo { lo_offset: 0x0284, hi_offset: 0, native_width: 4 }), + + // LdtInfo is not supported. + MmSaveStateRegister::LdtInfo => None, + + MmSaveStateRegister::Es => Some(RegisterInfo { lo_offset: 0x0200, hi_offset: 0, native_width: 2 }), + MmSaveStateRegister::Cs => Some(RegisterInfo { lo_offset: 0x0210, hi_offset: 0, native_width: 2 }), + MmSaveStateRegister::Ss => Some(RegisterInfo { lo_offset: 0x0220, hi_offset: 0, native_width: 2 }), + MmSaveStateRegister::Ds => Some(RegisterInfo { lo_offset: 0x0230, hi_offset: 0, native_width: 2 }), + MmSaveStateRegister::Fs => Some(RegisterInfo { lo_offset: 0x0240, hi_offset: 0, native_width: 2 }), + MmSaveStateRegister::Gs => Some(RegisterInfo { lo_offset: 0x0250, hi_offset: 0, native_width: 2 }), + MmSaveStateRegister::LdtrSel => Some(RegisterInfo { lo_offset: 0x0280, hi_offset: 0, native_width: 2 }), + MmSaveStateRegister::TrSel => Some(RegisterInfo { lo_offset: 0x0290, hi_offset: 0, native_width: 2 }), + MmSaveStateRegister::Dr7 => Some(RegisterInfo { lo_offset: 0x0338, hi_offset: 0x033C, native_width: 8 }), + MmSaveStateRegister::Dr6 => Some(RegisterInfo { lo_offset: 0x0340, hi_offset: 0x0344, native_width: 8 }), + MmSaveStateRegister::R8 => Some(RegisterInfo { lo_offset: 0x03B8, hi_offset: 0x03BC, native_width: 8 }), + MmSaveStateRegister::R9 => Some(RegisterInfo { lo_offset: 0x03B0, hi_offset: 0x03B4, native_width: 8 }), + MmSaveStateRegister::R10 => Some(RegisterInfo { lo_offset: 0x03A8, hi_offset: 0x03AC, native_width: 8 }), + MmSaveStateRegister::R11 => Some(RegisterInfo { lo_offset: 0x03A0, hi_offset: 0x03A4, native_width: 8 }), + MmSaveStateRegister::R12 => Some(RegisterInfo { lo_offset: 0x0398, hi_offset: 0x039C, native_width: 8 }), + MmSaveStateRegister::R13 => Some(RegisterInfo { lo_offset: 0x0390, hi_offset: 0x0394, native_width: 8 }), + MmSaveStateRegister::R14 => Some(RegisterInfo { lo_offset: 0x0388, hi_offset: 0x038C, native_width: 8 }), + MmSaveStateRegister::R15 => Some(RegisterInfo { lo_offset: 0x0380, hi_offset: 0x0384, native_width: 8 }), + MmSaveStateRegister::Rax => Some(RegisterInfo { lo_offset: 0x03F8, hi_offset: 0x03FC, native_width: 8 }), + MmSaveStateRegister::Rbx => Some(RegisterInfo { lo_offset: 0x03D0, hi_offset: 0x03D4, native_width: 8 }), + MmSaveStateRegister::Rcx => Some(RegisterInfo { lo_offset: 0x03F0, hi_offset: 0x03F4, native_width: 8 }), + MmSaveStateRegister::Rdx => Some(RegisterInfo { lo_offset: 0x03E8, hi_offset: 0x03EC, native_width: 8 }), + MmSaveStateRegister::Rsp => Some(RegisterInfo { lo_offset: 0x03C8, hi_offset: 0x03CC, native_width: 8 }), + MmSaveStateRegister::Rbp => Some(RegisterInfo { lo_offset: 0x03C0, hi_offset: 0x03C4, native_width: 8 }), + MmSaveStateRegister::Rsi => Some(RegisterInfo { lo_offset: 0x03E0, hi_offset: 0x03E4, native_width: 8 }), + MmSaveStateRegister::Rdi => Some(RegisterInfo { lo_offset: 0x03D8, hi_offset: 0x03DC, native_width: 8 }), + MmSaveStateRegister::Rip => Some(RegisterInfo { lo_offset: 0x0378, hi_offset: 0x037C, native_width: 8 }), + // Flags and Control Registers + MmSaveStateRegister::Rflags => Some(RegisterInfo { lo_offset: 0x0370, hi_offset: 0x0374, native_width: 8 }), + MmSaveStateRegister::Cr0 => Some(RegisterInfo { lo_offset: 0x0358, hi_offset: 0x035C, native_width: 8 }), + MmSaveStateRegister::Cr3 => Some(RegisterInfo { lo_offset: 0x0350, hi_offset: 0x0354, native_width: 8 }), + // CR4 is 8 bytes on AMD (vs 4 bytes on Intel). + MmSaveStateRegister::Cr4 => Some(RegisterInfo { lo_offset: 0x0348, hi_offset: 0x034C, native_width: 8 }), + + // Pseudo-registers are not in the architectural register map. + MmSaveStateRegister::Io | MmSaveStateRegister::Lma | MmSaveStateRegister::ProcessorId => None, + } +} + +/// Parses AMD's `IO_DWord` field from the SMRAM save state. +/// +/// AMD `IO_DWord` bit layout: +/// - Bit 0: Direction — 0 = WRITE (OUT), 1 = READ (IN). +/// - Bits \[3:1\]: Reserved. +/// - Bits \[5:4\]: Data size — 0 = byte, 1 = word, 3 = dword. +/// - Bits \[15:6\]: Reserved. +/// - Bits \[31:16\]: I/O port address. +/// +/// Returns `None` if the data-size encoding is invalid (value 2 is reserved). +pub fn parse_io_field(io_field: u32) -> Option { + let direction = io_field & 1; + let size_enc = (io_field >> 4) & 0x3; + let port = (io_field >> 16) & 0xFFFF; + + let io_type = if direction == IO_DIRECTION_IN { IO_TYPE_INPUT } else { IO_TYPE_OUTPUT }; + + let (io_width, byte_count) = match size_enc { + IO_SIZE_BYTE => (IO_WIDTH_UINT8, 1usize), + IO_SIZE_WORD => (IO_WIDTH_UINT16, 2usize), + IO_SIZE_DWORD => (IO_WIDTH_UINT32, 4usize), + _ => return None, // Reserved encoding. + }; + + Some(ParsedIoInfo { io_type, io_width, byte_count, io_port: port }) +} + +#[cfg(test)] +mod tests { + use super::*; + + // ---------------------------------------------------------------- + // Register map tests + // ---------------------------------------------------------------- + + #[test] + fn test_gpr_offsets() { + let rax = register_info(MmSaveStateRegister::Rax).unwrap(); + assert_eq!(rax.lo_offset, 0x03F8); + assert_eq!(rax.hi_offset, 0x03FC); + assert_eq!(rax.native_width, 8); + + let rcx = register_info(MmSaveStateRegister::Rcx).unwrap(); + assert_eq!(rcx.lo_offset, 0x03F0); + assert_eq!(rcx.hi_offset, 0x03F4); + } + + #[test] + fn test_segment_selectors_are_2_bytes() { + let cs = register_info(MmSaveStateRegister::Cs).unwrap(); + assert_eq!(cs.lo_offset, 0x0210); + assert_eq!(cs.native_width, 2); + assert_eq!(cs.hi_offset, 0); + } + + #[test] + fn test_descriptor_table_bases_contiguous() { + let gdt = register_info(MmSaveStateRegister::GdtBase).unwrap(); + assert_eq!(gdt.lo_offset, 0x0268); + assert_eq!(gdt.hi_offset, 0x026C); + assert_eq!(gdt.native_width, 8); + // AMD uses contiguous lo/hi (unlike Intel's split layout). + assert_eq!(gdt.hi_offset, gdt.lo_offset + 4); + } + + #[test] + fn test_limits_supported_on_amd() { + let gdt_limit = register_info(MmSaveStateRegister::GdtLimit).unwrap(); + assert_eq!(gdt_limit.native_width, 2); + assert_eq!(gdt_limit.lo_offset, 0x0264); + + let idt_limit = register_info(MmSaveStateRegister::IdtLimit).unwrap(); + assert_eq!(idt_limit.native_width, 2); + assert_eq!(idt_limit.lo_offset, 0x0274); + + let ldt_limit = register_info(MmSaveStateRegister::LdtLimit).unwrap(); + assert_eq!(ldt_limit.native_width, 4); + assert_eq!(ldt_limit.lo_offset, 0x0284); + } + + #[test] + fn test_cr4_is_8_bytes_on_amd() { + let cr4 = register_info(MmSaveStateRegister::Cr4).unwrap(); + assert_eq!(cr4.native_width, 8); + assert_eq!(cr4.lo_offset, 0x0348); + assert_eq!(cr4.hi_offset, 0x034C); + } + + #[test] + fn test_ldt_info_unsupported() { + assert!(register_info(MmSaveStateRegister::LdtInfo).is_none()); + } + + #[test] + fn test_pseudo_registers_return_none() { + assert!(register_info(MmSaveStateRegister::Io).is_none()); + assert!(register_info(MmSaveStateRegister::Lma).is_none()); + assert!(register_info(MmSaveStateRegister::ProcessorId).is_none()); + } + + #[test] + fn test_register_coverage() { + // All architectural registers except LdtInfo should be supported. + let supported_regs = [ + MmSaveStateRegister::GdtBase, + MmSaveStateRegister::IdtBase, + MmSaveStateRegister::LdtBase, + MmSaveStateRegister::GdtLimit, + MmSaveStateRegister::IdtLimit, + MmSaveStateRegister::LdtLimit, + MmSaveStateRegister::Es, + MmSaveStateRegister::Cs, + MmSaveStateRegister::Ss, + MmSaveStateRegister::Ds, + MmSaveStateRegister::Fs, + MmSaveStateRegister::Gs, + MmSaveStateRegister::LdtrSel, + MmSaveStateRegister::TrSel, + MmSaveStateRegister::Dr7, + MmSaveStateRegister::Dr6, + MmSaveStateRegister::R8, + MmSaveStateRegister::R9, + MmSaveStateRegister::R10, + MmSaveStateRegister::R11, + MmSaveStateRegister::R12, + MmSaveStateRegister::R13, + MmSaveStateRegister::R14, + MmSaveStateRegister::R15, + MmSaveStateRegister::Rax, + MmSaveStateRegister::Rbx, + MmSaveStateRegister::Rcx, + MmSaveStateRegister::Rdx, + MmSaveStateRegister::Rsp, + MmSaveStateRegister::Rbp, + MmSaveStateRegister::Rsi, + MmSaveStateRegister::Rdi, + MmSaveStateRegister::Rip, + MmSaveStateRegister::Rflags, + MmSaveStateRegister::Cr0, + MmSaveStateRegister::Cr3, + MmSaveStateRegister::Cr4, + ]; + + for reg in &supported_regs { + assert!(register_info(*reg).is_some(), "Missing AMD lookup for {:?}", reg); + } + } + + // ---------------------------------------------------------------- + // IO_DWord parsing tests + // ---------------------------------------------------------------- + + #[test] + fn test_parse_io_field_in_byte() { + // Direction=1 (IN), Size=0 (byte), Port=0x80 + let io_field: u32 = (0x0080 << 16) | (IO_SIZE_BYTE << 4) | IO_DIRECTION_IN; + let parsed = parse_io_field(io_field).unwrap(); + assert_eq!(parsed.io_type, IO_TYPE_INPUT); + assert_eq!(parsed.io_width, IO_WIDTH_UINT8); + assert_eq!(parsed.byte_count, 1); + assert_eq!(parsed.io_port, 0x80); + } + + #[test] + fn test_parse_io_field_out_dword() { + // Direction=0 (OUT), Size=3 (dword), Port=0xCF8 + let io_field: u32 = (0x0CF8 << 16) | (IO_SIZE_DWORD << 4) | 0; + let parsed = parse_io_field(io_field).unwrap(); + assert_eq!(parsed.io_type, IO_TYPE_OUTPUT); + assert_eq!(parsed.io_width, IO_WIDTH_UINT32); + assert_eq!(parsed.byte_count, 4); + assert_eq!(parsed.io_port, 0x0CF8); + } + + #[test] + fn test_parse_io_field_in_word() { + // Direction=1 (IN), Size=1 (word), Port=0x3F8 + let io_field: u32 = (0x03F8 << 16) | (IO_SIZE_WORD << 4) | IO_DIRECTION_IN; + let parsed = parse_io_field(io_field).unwrap(); + assert_eq!(parsed.io_type, IO_TYPE_INPUT); + assert_eq!(parsed.io_width, IO_WIDTH_UINT16); + assert_eq!(parsed.byte_count, 2); + assert_eq!(parsed.io_port, 0x03F8); + } + + #[test] + fn test_parse_io_field_reserved_size() { + // Direction=0, Size=2 (reserved) → None + let io_field: u32 = (0x0080 << 16) | (2 << 4) | 0; + assert!(parse_io_field(io_field).is_none()); + } + + #[test] + fn test_idt_base_hi_is_correct() { + // Verify we use the correct hi offset (0x27C), not the buggy + // C code value (0x278 = lo offset repeated). + let idt = register_info(MmSaveStateRegister::IdtBase).unwrap(); + assert_eq!(idt.lo_offset, 0x0278); + assert_eq!(idt.hi_offset, 0x027C); + assert_ne!(idt.lo_offset, idt.hi_offset, "hi must differ from lo"); + } + + #[test] + fn test_ldt_base_hi_is_correct() { + // Same bug fix for LdtBase: hi should be 0x28C, not 0x288. + let ldt = register_info(MmSaveStateRegister::LdtBase).unwrap(); + assert_eq!(ldt.lo_offset, 0x0288); + assert_eq!(ldt.hi_offset, 0x028C); + assert_ne!(ldt.lo_offset, ldt.hi_offset, "hi must differ from lo"); + } +} diff --git a/core/patina_internal_cpu/src/save_state/intel.rs b/core/patina_internal_cpu/src/save_state/intel.rs new file mode 100644 index 000000000..4fb055386 --- /dev/null +++ b/core/patina_internal_cpu/src/save_state/intel.rs @@ -0,0 +1,275 @@ +//! Intel x64 SMRAM Save State Map +//! +//! Register-to-offset lookup table for the Intel 64-bit SMRAM save state +//! layout (`SMRAM_SAVE_STATE_MAP64`). The save state area starts at +//! `SMBASE + 0x7C00` (the address stored in `CpuSaveState[CpuIndex]`). +//! +//! Reference: Intel SDM Vol 3C, Table 31-3; MdePkg `SmramSaveStateMap.h`. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 + +use super::{ + IO_TYPE_INPUT, IO_TYPE_OUTPUT, IO_WIDTH_UINT8, IO_WIDTH_UINT16, IO_WIDTH_UINT32, MmSaveStateRegister, ParsedIoInfo, + RegisterInfo, VendorConstants, +}; + +/// Intel-specific offsets and behaviour constants. +pub static VENDOR_CONSTANTS: VendorConstants = VendorConstants { + smmrevid_offset: 0x02FC, + io_info_offset: 0x03A4, + efer_offset: 0x03E0, + rax_offset: 0x035C, + min_rev_id_io: 0x30004, + lma_always_64: false, +}; + +/// IOMisc Type field value: OUT instruction. +const IOMISC_TYPE_OUT: u32 = 0; +/// IOMisc Type field value: IN instruction. +const IOMISC_TYPE_IN: u32 = 1; + +/// Looks up the Intel x64 save state register info for a PI register. +/// +/// Returns `None` for pseudo-registers (IO, LMA, ProcessorId) — those are +/// handled separately — and for unsupported registers (limits, LdtInfo). +pub fn register_info(reg: MmSaveStateRegister) -> Option { + match reg { + // Descriptor table bases (split hi/lo dwords — non-contiguous on Intel) + MmSaveStateRegister::GdtBase => Some(RegisterInfo { lo_offset: 0x028C, hi_offset: 0x01D0, native_width: 8 }), + MmSaveStateRegister::IdtBase => Some(RegisterInfo { lo_offset: 0x0294, hi_offset: 0x01D8, native_width: 8 }), + MmSaveStateRegister::LdtBase => Some(RegisterInfo { lo_offset: 0x029C, hi_offset: 0x01D4, native_width: 8 }), + + // Limits / LdtInfo — not supported in Intel 64-bit save state map. + MmSaveStateRegister::GdtLimit + | MmSaveStateRegister::IdtLimit + | MmSaveStateRegister::LdtLimit + | MmSaveStateRegister::LdtInfo => None, + + // Segment selectors (4-byte fields on Intel) + MmSaveStateRegister::Es => Some(RegisterInfo { lo_offset: 0x03A8, hi_offset: 0, native_width: 4 }), + MmSaveStateRegister::Cs => Some(RegisterInfo { lo_offset: 0x03AC, hi_offset: 0, native_width: 4 }), + MmSaveStateRegister::Ss => Some(RegisterInfo { lo_offset: 0x03B0, hi_offset: 0, native_width: 4 }), + MmSaveStateRegister::Ds => Some(RegisterInfo { lo_offset: 0x03B4, hi_offset: 0, native_width: 4 }), + MmSaveStateRegister::Fs => Some(RegisterInfo { lo_offset: 0x03B8, hi_offset: 0, native_width: 4 }), + MmSaveStateRegister::Gs => Some(RegisterInfo { lo_offset: 0x03BC, hi_offset: 0, native_width: 4 }), + MmSaveStateRegister::LdtrSel => Some(RegisterInfo { lo_offset: 0x03C0, hi_offset: 0, native_width: 4 }), + MmSaveStateRegister::TrSel => Some(RegisterInfo { lo_offset: 0x03C4, hi_offset: 0, native_width: 4 }), + + // Debug registers (8-byte, contiguous) + MmSaveStateRegister::Dr7 => Some(RegisterInfo { lo_offset: 0x03C8, hi_offset: 0x03CC, native_width: 8 }), + MmSaveStateRegister::Dr6 => Some(RegisterInfo { lo_offset: 0x03D0, hi_offset: 0x03D4, native_width: 8 }), + + // Extended registers R8–R15 (8-byte, contiguous, descending addresses) + MmSaveStateRegister::R8 => Some(RegisterInfo { lo_offset: 0x0354, hi_offset: 0x0358, native_width: 8 }), + MmSaveStateRegister::R9 => Some(RegisterInfo { lo_offset: 0x034C, hi_offset: 0x0350, native_width: 8 }), + MmSaveStateRegister::R10 => Some(RegisterInfo { lo_offset: 0x0344, hi_offset: 0x0348, native_width: 8 }), + MmSaveStateRegister::R11 => Some(RegisterInfo { lo_offset: 0x033C, hi_offset: 0x0340, native_width: 8 }), + MmSaveStateRegister::R12 => Some(RegisterInfo { lo_offset: 0x0334, hi_offset: 0x0338, native_width: 8 }), + MmSaveStateRegister::R13 => Some(RegisterInfo { lo_offset: 0x032C, hi_offset: 0x0330, native_width: 8 }), + MmSaveStateRegister::R14 => Some(RegisterInfo { lo_offset: 0x0324, hi_offset: 0x0328, native_width: 8 }), + MmSaveStateRegister::R15 => Some(RegisterInfo { lo_offset: 0x031C, hi_offset: 0x0320, native_width: 8 }), + + // General-purpose registers (8-byte, contiguous) + MmSaveStateRegister::Rax => Some(RegisterInfo { lo_offset: 0x035C, hi_offset: 0x0360, native_width: 8 }), + MmSaveStateRegister::Rbx => Some(RegisterInfo { lo_offset: 0x0374, hi_offset: 0x0378, native_width: 8 }), + MmSaveStateRegister::Rcx => Some(RegisterInfo { lo_offset: 0x0364, hi_offset: 0x0368, native_width: 8 }), + MmSaveStateRegister::Rdx => Some(RegisterInfo { lo_offset: 0x036C, hi_offset: 0x0370, native_width: 8 }), + MmSaveStateRegister::Rsp => Some(RegisterInfo { lo_offset: 0x037C, hi_offset: 0x0380, native_width: 8 }), + MmSaveStateRegister::Rbp => Some(RegisterInfo { lo_offset: 0x0384, hi_offset: 0x0388, native_width: 8 }), + MmSaveStateRegister::Rsi => Some(RegisterInfo { lo_offset: 0x038C, hi_offset: 0x0390, native_width: 8 }), + MmSaveStateRegister::Rdi => Some(RegisterInfo { lo_offset: 0x0394, hi_offset: 0x0398, native_width: 8 }), + MmSaveStateRegister::Rip => Some(RegisterInfo { lo_offset: 0x03D8, hi_offset: 0x03DC, native_width: 8 }), + + // Flags and control registers + MmSaveStateRegister::Rflags => Some(RegisterInfo { lo_offset: 0x03E8, hi_offset: 0x03EC, native_width: 8 }), + MmSaveStateRegister::Cr0 => Some(RegisterInfo { lo_offset: 0x03F8, hi_offset: 0x03FC, native_width: 8 }), + MmSaveStateRegister::Cr3 => Some(RegisterInfo { lo_offset: 0x03F0, hi_offset: 0x03F4, native_width: 8 }), + // CR4 is only 4 bytes in the Intel x64 save state map. + MmSaveStateRegister::Cr4 => Some(RegisterInfo { lo_offset: 0x0240, hi_offset: 0, native_width: 4 }), + + // Pseudo-registers are not in the architectural register map. + MmSaveStateRegister::Io | MmSaveStateRegister::Lma | MmSaveStateRegister::ProcessorId => None, + } +} + +/// Parses Intel's `IOMisc` field from the SMRAM save state. +/// +/// Intel IOMisc bit layout: +/// - Bit 0: `SmiFlag` — 1 if the SMI was caused by an I/O instruction. +/// - Bits \[3:1\]: `Length` — I/O width in bytes (1, 2, or 4). +/// - Bits \[7:4\]: `Type` — 0 = OUT, 1 = IN. +/// - Bits \[31:16\]: `Port` — I/O port address. +/// +/// Returns `None` if `SmiFlag` is 0 (SMI was not caused by I/O) or the I/O +/// type is not a simple IN or OUT (e.g. string / REP I/O). +pub fn parse_io_field(io_field: u32) -> Option { + // Check SmiFlag. + let smi_flag = io_field & 1; + if smi_flag == 0 { + return None; + } + + let length = (io_field >> 1) & 0x7; + let io_type_raw = (io_field >> 4) & 0xF; + let port = (io_field >> 16) & 0xFFFF; + + // Only simple IN/OUT are supported. + let io_type = match io_type_raw { + IOMISC_TYPE_OUT => IO_TYPE_OUTPUT, + IOMISC_TYPE_IN => IO_TYPE_INPUT, + _ => return None, + }; + + // Map length to IO width enum and byte count. + let (io_width, byte_count) = match length { + 1 => (IO_WIDTH_UINT8, 1usize), + 2 => (IO_WIDTH_UINT16, 2usize), + 4 => (IO_WIDTH_UINT32, 4usize), + _ => return None, + }; + + Some(ParsedIoInfo { io_type, io_width, byte_count, io_port: port }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_gpr_offsets() { + let rax = register_info(MmSaveStateRegister::Rax).unwrap(); + assert_eq!(rax.lo_offset, 0x035C); + assert_eq!(rax.hi_offset, 0x0360); + assert_eq!(rax.native_width, 8); + + let r15 = register_info(MmSaveStateRegister::R15).unwrap(); + assert_eq!(r15.lo_offset, 0x031C); + assert_eq!(r15.hi_offset, 0x0320); + assert_eq!(r15.native_width, 8); + } + + #[test] + fn test_segment_selectors() { + let es = register_info(MmSaveStateRegister::Es).unwrap(); + assert_eq!(es.lo_offset, 0x03A8); + assert_eq!(es.hi_offset, 0); + assert_eq!(es.native_width, 4); + } + + #[test] + fn test_descriptor_table_bases_are_split() { + let gdt = register_info(MmSaveStateRegister::GdtBase).unwrap(); + assert_eq!(gdt.lo_offset, 0x028C); + assert_eq!(gdt.hi_offset, 0x01D0); + assert_eq!(gdt.native_width, 8); + // Verify they are non-contiguous on Intel. + assert_ne!(gdt.hi_offset, gdt.lo_offset + 4); + } + + #[test] + fn test_limits_unsupported_on_intel() { + assert!(register_info(MmSaveStateRegister::GdtLimit).is_none()); + assert!(register_info(MmSaveStateRegister::IdtLimit).is_none()); + assert!(register_info(MmSaveStateRegister::LdtLimit).is_none()); + assert!(register_info(MmSaveStateRegister::LdtInfo).is_none()); + } + + #[test] + fn test_cr4_is_4_bytes_on_intel() { + let cr4 = register_info(MmSaveStateRegister::Cr4).unwrap(); + assert_eq!(cr4.native_width, 4); + assert_eq!(cr4.hi_offset, 0); + } + + #[test] + fn test_pseudo_registers_return_none() { + assert!(register_info(MmSaveStateRegister::Io).is_none()); + assert!(register_info(MmSaveStateRegister::Lma).is_none()); + assert!(register_info(MmSaveStateRegister::ProcessorId).is_none()); + } + + #[test] + fn test_parse_io_field_in() { + // SmiFlag=1, Length=1 (byte), Type=1 (IN), Port=0x80 + // Bits: Port(31:16)=0x0080, Type(7:4)=1, Length(3:1)=1, SmiFlag(0)=1 + let io_field: u32 = (0x0080 << 16) | (1 << 4) | (1 << 1) | 1; + let parsed = parse_io_field(io_field).unwrap(); + assert_eq!(parsed.io_type, IO_TYPE_INPUT); + assert_eq!(parsed.io_width, IO_WIDTH_UINT8); + assert_eq!(parsed.byte_count, 1); + assert_eq!(parsed.io_port, 0x80); + } + + #[test] + fn test_parse_io_field_out() { + // SmiFlag=1, Length=4 (dword), Type=0 (OUT), Port=0xCF8 + let io_field: u32 = (0x0CF8 << 16) | (0 << 4) | (4 << 1) | 1; + let parsed = parse_io_field(io_field).unwrap(); + assert_eq!(parsed.io_type, IO_TYPE_OUTPUT); + assert_eq!(parsed.io_width, IO_WIDTH_UINT32); + assert_eq!(parsed.byte_count, 4); + assert_eq!(parsed.io_port, 0x0CF8); + } + + #[test] + fn test_parse_io_field_no_smi_flag() { + // SmiFlag=0 → should return None + let io_field: u32 = (0x0080 << 16) | (1 << 4) | (1 << 1) | 0; + assert!(parse_io_field(io_field).is_none()); + } + + #[test] + fn test_parse_io_field_string_io() { + // SmiFlag=1, Length=1, Type=4 (string, not IN/OUT) → None + let io_field: u32 = (0x0080 << 16) | (4 << 4) | (1 << 1) | 1; + assert!(parse_io_field(io_field).is_none()); + } + + #[test] + fn test_register_coverage() { + let architectural_regs = [ + MmSaveStateRegister::GdtBase, + MmSaveStateRegister::IdtBase, + MmSaveStateRegister::LdtBase, + MmSaveStateRegister::Es, + MmSaveStateRegister::Cs, + MmSaveStateRegister::Ss, + MmSaveStateRegister::Ds, + MmSaveStateRegister::Fs, + MmSaveStateRegister::Gs, + MmSaveStateRegister::LdtrSel, + MmSaveStateRegister::TrSel, + MmSaveStateRegister::Dr7, + MmSaveStateRegister::Dr6, + MmSaveStateRegister::R8, + MmSaveStateRegister::R9, + MmSaveStateRegister::R10, + MmSaveStateRegister::R11, + MmSaveStateRegister::R12, + MmSaveStateRegister::R13, + MmSaveStateRegister::R14, + MmSaveStateRegister::R15, + MmSaveStateRegister::Rax, + MmSaveStateRegister::Rbx, + MmSaveStateRegister::Rcx, + MmSaveStateRegister::Rdx, + MmSaveStateRegister::Rsp, + MmSaveStateRegister::Rbp, + MmSaveStateRegister::Rsi, + MmSaveStateRegister::Rdi, + MmSaveStateRegister::Rip, + MmSaveStateRegister::Rflags, + MmSaveStateRegister::Cr0, + MmSaveStateRegister::Cr3, + MmSaveStateRegister::Cr4, + ]; + + for reg in &architectural_regs { + assert!(register_info(*reg).is_some(), "Missing Intel lookup for {:?}", reg); + } + } +} diff --git a/core/patina_internal_cpu/src/save_state/mod.rs b/core/patina_internal_cpu/src/save_state/mod.rs new file mode 100644 index 000000000..19c45ff80 --- /dev/null +++ b/core/patina_internal_cpu/src/save_state/mod.rs @@ -0,0 +1,355 @@ +//! SMRAM Save State Architecture Definitions +//! +//! Provides platform-independent types and feature-gated vendor-specific +//! register maps for reading the SMRAM save state area. +//! +//! ## Vendor Selection +//! +//! The active vendor is selected at **build time** via Cargo features on the +//! `patina_internal_cpu` crate: +//! +//! - `save_state_intel` — Intel x64 SMRAM save state map. +//! - `save_state_amd` — AMD64 SMRAM save state map. +//! +//! Exactly one must be enabled. Re-exported items are available through the +//! `register_info()` function, which resolves to the active vendor's lookup at +//! compile time. +//! +//! ## Overview +//! +//! The common types (`MmSaveStateRegister`, `RegisterInfo`, etc.) are always +//! available. The vendor modules contain only the register-to-offset lookup +//! table and any vendor-specific constants (e.g. IO trap field layout). +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 + +// Vendor-specific submodules — exactly one is compiled in. +#[cfg(feature = "save_state_intel")] +pub mod intel; + +#[cfg(feature = "save_state_amd")] +pub mod amd; + +/// `EFI_MM_SAVE_STATE_REGISTER` values from the PI Specification. +/// +/// These correspond to the registers that can be read from the SMRAM save +/// state area via `EFI_MM_CPU_PROTOCOL.ReadSaveState()`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u64)] +pub enum MmSaveStateRegister { + // Descriptor table bases + /// GDT base address. + GdtBase = 4, + /// IDT base address. + IdtBase = 5, + /// LDT base address. + LdtBase = 6, + /// GDT limit. + GdtLimit = 7, + /// IDT limit. + IdtLimit = 8, + /// LDT limit. + LdtLimit = 9, + /// LDT information. + LdtInfo = 10, + + // Segment selectors + /// ES selector. + Es = 20, + /// CS selector. + Cs = 21, + /// SS selector. + Ss = 22, + /// DS selector. + Ds = 23, + /// FS selector. + Fs = 24, + /// GS selector. + Gs = 25, + /// LDTR selector. + LdtrSel = 26, + /// TR selector. + TrSel = 27, + + // Debug registers + /// DR7. + Dr7 = 28, + /// DR6. + Dr6 = 29, + + // Extended general-purpose registers (x86_64 only) + /// R8. + R8 = 30, + /// R9. + R9 = 31, + /// R10. + R10 = 32, + /// R11. + R11 = 33, + /// R12. + R12 = 34, + /// R13. + R13 = 35, + /// R14. + R14 = 36, + /// R15. + R15 = 37, + + // General-purpose registers + /// RAX. + Rax = 38, + /// RBX. + Rbx = 39, + /// RCX. + Rcx = 40, + /// RDX. + Rdx = 41, + /// RSP. + Rsp = 42, + /// RBP. + Rbp = 43, + /// RSI. + Rsi = 44, + /// RDI. + Rdi = 45, + /// RIP. + Rip = 46, + + // Flags and control registers + /// RFLAGS. + Rflags = 51, + /// CR0. + Cr0 = 52, + /// CR3. + Cr3 = 53, + /// CR4. + Cr4 = 54, + + // Pseudo-registers + /// I/O operation information. + Io = 512, + /// Long Mode Active indicator. + Lma = 513, + /// Processor identifier (APIC ID). + ProcessorId = 514, +} + +impl MmSaveStateRegister { + /// Creates a register from a raw `EFI_MM_SAVE_STATE_REGISTER` value. + pub fn from_u64(value: u64) -> Option { + match value { + 4 => Some(Self::GdtBase), + 5 => Some(Self::IdtBase), + 6 => Some(Self::LdtBase), + 7 => Some(Self::GdtLimit), + 8 => Some(Self::IdtLimit), + 9 => Some(Self::LdtLimit), + 10 => Some(Self::LdtInfo), + 20 => Some(Self::Es), + 21 => Some(Self::Cs), + 22 => Some(Self::Ss), + 23 => Some(Self::Ds), + 24 => Some(Self::Fs), + 25 => Some(Self::Gs), + 26 => Some(Self::LdtrSel), + 27 => Some(Self::TrSel), + 28 => Some(Self::Dr7), + 29 => Some(Self::Dr6), + 30 => Some(Self::R8), + 31 => Some(Self::R9), + 32 => Some(Self::R10), + 33 => Some(Self::R11), + 34 => Some(Self::R12), + 35 => Some(Self::R13), + 36 => Some(Self::R14), + 37 => Some(Self::R15), + 38 => Some(Self::Rax), + 39 => Some(Self::Rbx), + 40 => Some(Self::Rcx), + 41 => Some(Self::Rdx), + 42 => Some(Self::Rsp), + 43 => Some(Self::Rbp), + 44 => Some(Self::Rsi), + 45 => Some(Self::Rdi), + 46 => Some(Self::Rip), + 51 => Some(Self::Rflags), + 52 => Some(Self::Cr0), + 53 => Some(Self::Cr3), + 54 => Some(Self::Cr4), + 512 => Some(Self::Io), + 513 => Some(Self::Lma), + 514 => Some(Self::ProcessorId), + _ => None, + } + } +} + +/// Layout descriptor for a register in the SMRAM save state map. +/// +/// For 8-byte registers the value may be stored as two dwords at potentially +/// non-contiguous offsets (e.g. Intel GDT/IDT/LDT base). `lo_offset` gives +/// the low dword; `hi_offset` gives the high dword. +/// +/// For registers narrower than 8 bytes, `hi_offset` is 0 and only the low +/// dword location is used. +#[derive(Debug, Clone, Copy)] +pub struct RegisterInfo { + /// Offset of the low (or only) dword from the save state base. + pub lo_offset: u16, + /// Offset of the high dword. 0 for registers < 8 bytes. + pub hi_offset: u16, + /// Native width in bytes as stored in the save state (1, 2, 4, or 8). + pub native_width: u8, +} + +/// Vendor-specific save state field offsets and behaviour that differ between +/// Intel and AMD. Provided by the active vendor module. +pub struct VendorConstants { + /// Offset of `SMMRevId` within the save state map. + pub smmrevid_offset: u16, + /// Offset of the IO information field (Intel `IOMisc`, AMD `IO_DWord`). + pub io_info_offset: u16, + /// Offset of `IA32_EFER` / `EFER`. + pub efer_offset: u16, + /// Offset of `_RAX` (used for IO data reads). + pub rax_offset: u16, + /// Minimum `SMMRevId` that supports IO information. + pub min_rev_id_io: u32, + /// Whether the LMA pseudo-register always returns 64-bit. + /// + /// AMD64 processors always operate in 64-bit mode during SMM, so + /// they skip the EFER.LMA check and always return LMA_64BIT. + pub lma_always_64: bool, +} + +/// Parsed I/O trap information extracted from the vendor-specific IO field. +/// +/// Returned by [`parse_io_field`] after decoding the vendor's raw IO bits. +#[derive(Debug, Clone, Copy)] +pub struct ParsedIoInfo { + /// I/O type: [`IO_TYPE_INPUT`] (1) or [`IO_TYPE_OUTPUT`] (2). + pub io_type: u32, + /// I/O width: [`IO_WIDTH_UINT8`], [`IO_WIDTH_UINT16`], or [`IO_WIDTH_UINT32`]. + pub io_width: u32, + /// Number of bytes transferred (1, 2, or 4). + pub byte_count: usize, + /// I/O port address. + pub io_port: u32, +} + +/// Returns the [`VendorConstants`] for the active vendor (selected at build time). +#[cfg(feature = "save_state_intel")] +pub fn vendor_constants() -> &'static VendorConstants { + &intel::VENDOR_CONSTANTS +} + +/// Returns the [`VendorConstants`] for the active vendor (selected at build time). +#[cfg(feature = "save_state_amd")] +pub fn vendor_constants() -> &'static VendorConstants { + &amd::VENDOR_CONSTANTS +} + +/// Returns the [`RegisterInfo`] for a given register in the active vendor's +/// save state map. +/// +/// Returns `None` for pseudo-registers (IO, LMA, ProcessorId) and for +/// registers not supported by the active vendor's 64-bit save state layout. +#[cfg(feature = "save_state_intel")] +pub fn register_info(reg: MmSaveStateRegister) -> Option { + intel::register_info(reg) +} + +/// Returns the [`RegisterInfo`] for a given register in the active vendor's +/// save state map. +#[cfg(feature = "save_state_amd")] +pub fn register_info(reg: MmSaveStateRegister) -> Option { + amd::register_info(reg) +} + +/// Parses the vendor-specific I/O information field from the save state. +/// +/// Returns `None` if the field indicates no I/O instruction triggered the SMI +/// (Intel: `SmiFlag` not set) or if the I/O type/width is not a simple IN/OUT. +#[cfg(feature = "save_state_intel")] +pub fn parse_io_field(io_field: u32) -> Option { + intel::parse_io_field(io_field) +} + +/// Parses the vendor-specific I/O information field from the save state. +#[cfg(feature = "save_state_amd")] +pub fn parse_io_field(io_field: u32) -> Option { + amd::parse_io_field(io_field) +} + +/// `EFI_MM_SAVE_STATE_IO_INFO` — written to the user buffer when reading +/// the IO pseudo-register. +#[repr(C)] +#[derive(Debug, Clone, Copy, Default)] +pub struct MmSaveStateIoInfo { + /// I/O data value (from RAX, zero-extended). + pub io_data: u64, + /// I/O port address. + pub io_port: u64, + /// I/O width enum (`EFI_MM_SAVE_STATE_IO_WIDTH`). + pub io_width: u32, + /// I/O type enum (`EFI_MM_SAVE_STATE_IO_TYPE`). + pub io_type: u32, +} + +/// Size of [`MmSaveStateIoInfo`] in bytes. +pub const IO_INFO_SIZE: usize = 24; + +const _: () = assert!(core::mem::size_of::() == IO_INFO_SIZE); + +/// `EFI_MM_SAVE_STATE_IO_TYPE_INPUT` (1). +pub const IO_TYPE_INPUT: u32 = 1; +/// `EFI_MM_SAVE_STATE_IO_TYPE_OUTPUT` (2). +pub const IO_TYPE_OUTPUT: u32 = 2; + +/// `EFI_MM_SAVE_STATE_IO_WIDTH_UINT8` (0). +pub const IO_WIDTH_UINT8: u32 = 0; +/// `EFI_MM_SAVE_STATE_IO_WIDTH_UINT16` (1). +pub const IO_WIDTH_UINT16: u32 = 1; +/// `EFI_MM_SAVE_STATE_IO_WIDTH_UINT32` (2). +pub const IO_WIDTH_UINT32: u32 = 2; + +/// IA32_EFER.LMA bit (bit 10). +pub const IA32_EFER_LMA: u64 = 1 << 10; + +/// LMA value: processor was in 32-bit mode. +pub const LMA_32BIT: u64 = 32; +/// LMA value: processor was in 64-bit mode. +pub const LMA_64BIT: u64 = 64; + +/// Size of one `EFI_PROCESSOR_INFORMATION` entry (PI 1.7+). +/// +/// Layout: `ProcessorId`(8) + `StatusFlag`(4) + `CpuPhysicalLocation`(12) + +/// `ExtendedProcessorInformation`(24) = 48 bytes. +pub const PROCESSOR_INFO_ENTRY_SIZE: usize = 48; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_register_from_u64() { + assert_eq!(MmSaveStateRegister::from_u64(38), Some(MmSaveStateRegister::Rax)); + assert_eq!(MmSaveStateRegister::from_u64(512), Some(MmSaveStateRegister::Io)); + assert_eq!(MmSaveStateRegister::from_u64(514), Some(MmSaveStateRegister::ProcessorId)); + assert_eq!(MmSaveStateRegister::from_u64(999), None); + assert_eq!(MmSaveStateRegister::from_u64(0), None); + } + + #[test] + fn test_io_info_struct_layout() { + assert_eq!(core::mem::size_of::(), 24); + assert_eq!(core::mem::offset_of!(MmSaveStateIoInfo, io_data), 0); + assert_eq!(core::mem::offset_of!(MmSaveStateIoInfo, io_port), 8); + assert_eq!(core::mem::offset_of!(MmSaveStateIoInfo, io_width), 16); + assert_eq!(core::mem::offset_of!(MmSaveStateIoInfo, io_type), 20); + } +} diff --git a/core/patina_internal_mm_common/Cargo.toml b/core/patina_internal_mm_common/Cargo.toml new file mode 100644 index 000000000..90a65b131 --- /dev/null +++ b/core/patina_internal_mm_common/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "patina_internal_mm_common" +version.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +readme = "README.md" +description = "Shared type definitions for MM supervisor and user cores." + +[lints] +workspace = true + +[dependencies] +r-efi = { workspace = true } diff --git a/core/patina_internal_mm_common/README.md b/core/patina_internal_mm_common/README.md new file mode 100644 index 000000000..2c0ac22b0 --- /dev/null +++ b/core/patina_internal_mm_common/README.md @@ -0,0 +1,10 @@ +# patina_internal_mm_common + +Shared type definitions used by both the MM Supervisor Core and MM User Core. + +This crate provides the communication structures and enumerations that define +the ABI between the supervisor (ring 0) and user (ring 3) MM modules, +including: + +- `UserCommandType` — Supervisor-to-user command enumeration +- `MM_COMM_BUFFER_HOB_GUID` — Shared GUID for the communication buffer HOB diff --git a/core/patina_internal_mm_common/src/lib.rs b/core/patina_internal_mm_common/src/lib.rs new file mode 100644 index 000000000..a074253bc --- /dev/null +++ b/core/patina_internal_mm_common/src/lib.rs @@ -0,0 +1,112 @@ +//! Shared type definitions for MM supervisor and user cores. +//! +//! This crate provides the communication structures and enumerations that define +//! the ABI between the supervisor (ring 0) and user (ring 3) MM modules. + +#![no_std] + +/// Command types passed from the supervisor to the user core via `invoke_demoted_routine`. +/// +/// Discriminant values are part of the supervisor↔user ABI and must not change. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u64)] +pub enum UserCommandType { + /// Initialize the user core: walk HOBs, discover drivers, dispatch. + StartUserCore = 0, + /// Handle a runtime MMI request: parse communication buffer and dispatch handlers. + UserRequest = 1, + /// Execute a procedure on an AP. + UserApProcedure = 2, +} + +impl TryFrom for UserCommandType { + type Error = u64; + + fn try_from(value: u64) -> Result { + match value { + 0 => Ok(UserCommandType::StartUserCore), + 1 => Ok(UserCommandType::UserRequest), + 2 => Ok(UserCommandType::UserApProcedure), + other => Err(other), + } + } +} + +/// Syscall indices for the MM Supervisor ↔ User Core syscall interface. +/// +/// These match the definitions in SysCallLib.h and define the ABI used when +/// Ring 3 code issues a `syscall` instruction to the Ring 0 supervisor. +/// +/// ## ABI +/// +/// - RAX = call index ([`SyscallIndex`]) +/// - RDX = arg1 +/// - R8 = arg2 +/// - R9 = arg3 +/// +/// On return: +/// - RAX = result value +/// - RDX = status (EFI_STATUS) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u64)] +pub enum SyscallIndex { + /// Read MSR - Arg1: MSR index, Returns: MSR value + RdMsr = 0x0000, + /// Write MSR - Arg1: MSR index, Arg2: value + WrMsr = 0x0001, + /// CLI - Clear interrupts + Cli = 0x0002, + /// IO Read - Arg1: port, Arg2: width + IoRead = 0x0003, + /// IO Write - Arg1: port, Arg2: width, Arg3: value + IoWrite = 0x0004, + /// WBINVD - Write back and invalidate cache + Wbinvd = 0x0005, + /// HLT - Halt processor + Hlt = 0x0006, + /// Save State Read - Arg1: register, Arg2: CPU index + SaveStateRead = 0x0007, + /// Maximum value for legacy syscall indices + LegacyMax = 0xFFFF, + /// Allocate Pages - Arg1: alloc_type, Arg2: mem_type, Arg3: page_count + AllocPage = 0x10004, + /// Free Pages - Arg1: address, Arg2: page_count + FreePage = 0x10005, + /// Start AP Procedure - Arg1: procedure, Arg2: CPU index, Arg3: argument + StartApProc = 0x10006, + /// Save state read with extended support - Arg1: width, Arg2: buffer pointer + SaveStateRead2 = 0x10021, + /// MM memory unblocked - Arg1: address, Arg2: size + MmMemoryUnblocked = 0x10022, + /// MM is communication buffer - Arg1: address, Arg2: size + MmIsCommBuffer = 0x10023, +} + +impl SyscallIndex { + /// Creates a `SyscallIndex` from a raw `u64` value. + pub fn from_u64(value: u64) -> Option { + match value { + 0x0000 => Some(Self::RdMsr), + 0x0001 => Some(Self::WrMsr), + 0x0002 => Some(Self::Cli), + 0x0003 => Some(Self::IoRead), + 0x0004 => Some(Self::IoWrite), + 0x0005 => Some(Self::Wbinvd), + 0x0006 => Some(Self::Hlt), + 0x0007 => Some(Self::SaveStateRead), + 0xFFFF => Some(Self::LegacyMax), + 0x10004 => Some(Self::AllocPage), + 0x10005 => Some(Self::FreePage), + 0x10006 => Some(Self::StartApProc), + 0x10021 => Some(Self::SaveStateRead2), + 0x10022 => Some(Self::MmMemoryUnblocked), + 0x10023 => Some(Self::MmIsCommBuffer), + _ => None, + } + } + + /// Returns the raw `u64` value of this syscall index. + pub fn as_u64(self) -> u64 { + self as u64 + } +} diff --git a/patina_mm_supervisor_core/Cargo.toml b/patina_mm_supervisor_core/Cargo.toml new file mode 100644 index 000000000..b2868e714 --- /dev/null +++ b/patina_mm_supervisor_core/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "patina_mm_supervisor_core" +resolver = "2" +version.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +readme = "README.md" +description = "A pure Rust implementation of the MM Supervisor Core for standalone MM mode environments." + +# Metadata to tell docs.rs how to build the documentation when uploading +[package.metadata.docs.rs] +features = ["doc"] + +[dependencies] +linkme = { workspace = true } +log = { workspace = true } +patina = { workspace = true } +patina_internal_cpu = { workspace = true } +patina_internal_mm_common = { workspace = true } +patina_mm_policy = { workspace = true } +patina_paging = { workspace = true, features = [ + "mm_supv" +] } +patina_stacktrace = { workspace = true } +r-efi = { workspace = true } +spin = { workspace = true } +x86_64 = { workspace = true, features = [ + "instructions", + "abi_x86_interrupt", +] } + +[dev-dependencies] +mockall = { workspace = true } +serial_test = { workspace = true } + +[features] +default = [] +std = [] +doc = [] +save_state_intel = ["patina_internal_cpu/save_state_intel"] +save_state_amd = ["patina_internal_cpu/save_state_amd"] diff --git a/patina_mm_supervisor_core/README.md b/patina_mm_supervisor_core/README.md new file mode 100644 index 000000000..f6c4183c2 --- /dev/null +++ b/patina_mm_supervisor_core/README.md @@ -0,0 +1,177 @@ +# Patina MM Supervisor Core + +A pure Rust implementation of the MM Supervisor Core for standalone MM mode environments. + +## Overview + +This crate provides the core functionality for the MM (Management Mode) Supervisor in a standalone MM environment. It is designed to run on x64 systems where: + +- Page tables are already set up by the pre-MM phase +- All images are loaded and ready to execute +- The BSP (Bootstrap Processor) orchestrates incoming requests +- APs (Application Processors) wait in a holding pen, checking a mailbox for work + +## Memory Model + +**This is a core component that does not use heap allocation.** All data structures use fixed-size arrays with compile-time constants provided via const generics in the `PlatformInfo` trait: + +- `MAX_CPU_COUNT` - Maximum number of CPUs supported +- `MAX_HANDLERS` - Maximum number of request handlers + +This allows the entire supervisor to be instantiated as a `static` with no runtime allocation. + +## Building a PE/COFF Binary + +### Prerequisites + +1. Install the Rust UEFI target: + ```bash + rustup target add x86_64-unknown-uefi + ``` + +2. Ensure you have the nightly toolchain (required for `#![feature(...)]`): + ```bash + rustup override set nightly + ``` + +### Build Command + +Build the example MM Supervisor binary: + +```bash +cargo build --release --target x86_64-unknown-uefi --bin example_mm_supervisor +``` + +The output PE/COFF binary will be at: +``` +target/x86_64-unknown-uefi/release/example_mm_supervisor.efi +``` + +### Entry Point + +The MM Supervisor exports `MmSupervisorMain` as its entry point, matching the EDK2 convention: + +```rust +#[unsafe(export_name = "MmSupervisorMain")] +pub extern "efiapi" fn mm_supervisor_main(hob_list: *const c_void) -> ! { + SUPERVISOR.entry_point(hob_list) +} +``` + +The MM IPL (Initial Program Loader) calls this entry point on **all processors** after: +1. Loading the supervisor image into MMRAM +2. Setting up page tables +3. Constructing the HOB list with MMRAM ranges + +## Architecture + +### Entry Point Model + +The entry point is executed on all cores simultaneously: + +1. **BSP (Bootstrap Processor)**: + - First CPU to arrive (determined by atomic counter) + - Performs one-time initialization + - Sets up the request handling infrastructure + - Enters the main request serving loop + +2. **APs (Application Processors)**: + - All other CPUs + - Wait for BSP initialization to complete + - Enter a holding pen and poll mailboxes for commands + +### Mailbox System + +The mailbox system provides inter-processor communication: + +- Each AP has a dedicated mailbox (cache-line aligned to avoid false sharing) +- BSP sends commands to APs via mailboxes +- APs respond with results through the same mailbox +- Supports synchronization primitives for coordinated operations + +## Usage + +### Basic Platform Implementation + +```rust +#![no_std] +#![no_main] + +use core::{ffi::c_void, panic::PanicInfo}; +use patina_mm_supervisor_core::*; + +struct MyPlatform; + +impl CpuInfo for MyPlatform { + fn ap_poll_timeout_us() -> u64 { 1000 } +} + +impl PlatformInfo for MyPlatform { + type CpuInfo = Self; + const MAX_CPU_COUNT: usize = 8; + const MAX_HANDLERS: usize = 32; +} + +// Static instance - no heap allocation required +static SUPERVISOR: MmSupervisorCore = MmSupervisorCore::new(); + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop { core::hint::spin_loop(); } +} + +#[unsafe(export_name = "MmSupervisorMain")] +pub extern "efiapi" fn mm_supervisor_main(hob_list: *const c_void) -> ! { + SUPERVISOR.entry_point(hob_list) +} +``` + +### Registering Request Handlers + +Handlers must be defined as static references: + +```rust +use patina_mm_supervisor_core::*; + +struct MyHandler; + +impl RequestHandler for MyHandler { + fn guid(&self) -> r_efi::efi::Guid { + // Your handler's GUID + r_efi::efi::Guid::from_fields(0x12345678, 0x1234, 0x5678, 0x12, 0x34, &[0; 6]) + } + + fn handle(&self, context: &mut RequestContext) -> RequestResult { + // Handle the request + RequestResult::Success + } + + fn name(&self) -> &'static str { + "MyHandler" + } +} + +static MY_HANDLER: MyHandler = MyHandler; + +// Register before calling entry_point, or during BSP initialization +SUPERVISOR.register_handler(&MY_HANDLER); +``` + +### Integration with MM IPL + +The MM IPL (from EDK2/MmSupervisorPkg) loads this binary and calls the entry point. The HOB list passed contains: + +- `gEfiMmPeiMmramMemoryReserveGuid` - MMRAM ranges +- `gMmCommBufferHobGuid` - Communication buffer information +- `gMmCommonRegionHobGuid` - Common memory regions +- FV HOBs for MM driver firmware volumes + +## Example Binary + +See [bin/example_mm_supervisor.rs](bin/example_mm_supervisor.rs) for a complete example platform implementation. + +## License + +Copyright (c) Microsoft Corporation. + +SPDX-License-Identifier: Apache-2.0 diff --git a/patina_mm_supervisor_core/src/cpu.rs b/patina_mm_supervisor_core/src/cpu.rs new file mode 100644 index 000000000..8f03ba47c --- /dev/null +++ b/patina_mm_supervisor_core/src/cpu.rs @@ -0,0 +1,532 @@ +//! CPU Management Module +//! +//! This module provides CPU identification and management for the MM Supervisor Core. +//! It handles BSP/AP detection, CPU registration, and state tracking. +//! +//! ## Memory Model +//! +//! This module does not perform heap allocation. All structures use fixed-size arrays +//! with compile-time constants provided via const generics. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use core::arch::{x86_64, x86_64::CpuidResult}; +use core::sync::atomic::{AtomicU8, AtomicU32, Ordering}; + +/// A trait to be implemented by the platform to provide CPU-related configuration. +/// +/// ## Example +/// +/// ```rust,ignore +/// use patina_mm_supervisor_core::CpuInfo; +/// +/// struct ExamplePlatform; +/// +/// impl CpuInfo for ExamplePlatform { +/// fn ap_poll_timeout_us() -> u64 { 500 } +/// } +/// ``` +#[cfg_attr(test, mockall::automock)] +pub trait CpuInfo { + /// Returns the timeout in microseconds for AP mailbox polling. + /// + /// By default, this returns 1000 (1ms) which is a reasonable polling interval. + #[inline(always)] + fn ap_poll_timeout_us() -> u64 { + 1000 + } + + /// Returns the performance counter frequency in Hz, if known by the platform. + /// + /// For example, on QEMU Q35 the platform can calibrate the TSC frequency + /// from the ACPI PM Timer and return it here. + /// + /// If `None` is returned (the default), the supervisor will attempt + /// auto-detection via CPUID. + fn perf_timer_frequency() -> Option { + None + } +} + +/// The state of an Application Processor (AP). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum ApState { + /// The AP has not been registered yet. + NotPresent = 0, + /// The AP is in the holding pen, waiting for work. + InHoldingPen = 1, + /// The AP is currently executing a task. + Busy = 2, + /// The AP has been halted. + Halted = 3, +} + +impl From for ApState { + fn from(value: u8) -> Self { + match value { + 0 => ApState::NotPresent, + 1 => ApState::InHoldingPen, + 2 => ApState::Busy, + 3 => ApState::Halted, + _ => ApState::NotPresent, + } + } +} + +/// Information about a registered CPU stored in a fixed-size slot. +#[repr(C)] +struct CpuSlot { + /// The CPU's APIC ID. u32::MAX means slot is unused. + cpu_id: AtomicU32, + /// Whether this CPU is the BSP (0 = AP, 1 = BSP). + is_bsp: AtomicU8, + /// Current state (for APs only). + state: AtomicU8, + /// Padding for alignment. + _padding: [u8; 2], +} + +impl CpuSlot { + /// Creates a new empty CPU slot. + const fn new() -> Self { + Self { + cpu_id: AtomicU32::new(u32::MAX), + is_bsp: AtomicU8::new(0), + state: AtomicU8::new(ApState::NotPresent as u8), + _padding: [0; 2], + } + } + + /// Checks if this slot is in use. + fn is_used(&self) -> bool { + self.cpu_id.load(Ordering::Acquire) != u32::MAX + } + + /// Gets the CPU ID if the slot is used. + fn get_cpu_id(&self) -> Option { + let id = self.cpu_id.load(Ordering::Acquire); + if id == u32::MAX { None } else { Some(id) } + } +} + +/// Manager for CPU-related operations. +/// +/// Tracks registered CPUs and their states using fixed-size arrays. +/// +/// ## Const Generic Parameters +/// +/// * `MAX_CPUS` - The maximum number of CPUs that can be registered. +pub struct CpuManager { + /// CPU slots - fixed size array. + slots: [CpuSlot; MAX_CPUS], + /// Number of CPUs currently registered. + registered_count: AtomicU32, + /// The APIC ID of the BSP. + bsp_id: AtomicU32, +} + +impl CpuManager { + /// Creates a new CPU manager. + /// + /// This is a const fn and performs no heap allocation. + pub const fn new() -> Self { + Self { + slots: [const { CpuSlot::new() }; MAX_CPUS], + registered_count: AtomicU32::new(0), + bsp_id: AtomicU32::new(u32::MAX), + } + } + + /// Registers a CPU with the manager. + /// + /// # Arguments + /// + /// * `cpu_id` - The CPU's APIC ID. + /// * `is_bsp` - Whether this CPU is the BSP. + /// + /// # Returns + /// + /// The index of the registered CPU, or `None` if max CPUs reached or already registered. + pub fn register_cpu(&self, cpu_id: u32, is_bsp: bool) -> Option { + // Check if already registered + for slot in &self.slots { + if slot.get_cpu_id() == Some(cpu_id) { + log::warn!("CPU {} already registered", cpu_id); + return None; + } + } + + // Find an empty slot + for (index, slot) in self.slots.iter().enumerate() { + // Try to claim this slot using compare-exchange + let result = slot.cpu_id.compare_exchange(u32::MAX, cpu_id, Ordering::AcqRel, Ordering::Acquire); + + if result.is_ok() { + // Successfully claimed the slot + slot.is_bsp.store(if is_bsp { 1 } else { 0 }, Ordering::Release); + slot.state + .store(if is_bsp { ApState::Busy as u8 } else { ApState::InHoldingPen as u8 }, Ordering::Release); + + self.registered_count.fetch_add(1, Ordering::SeqCst); + + if is_bsp { + self.bsp_id.store(cpu_id, Ordering::SeqCst); + log::info!("Registered BSP with APIC ID {}", cpu_id); + } else { + log::trace!("Registered AP with APIC ID {} at index {}", cpu_id, index); + } + + return Some(index); + } + } + + log::warn!("Maximum CPU count ({}) reached, cannot register CPU {}", MAX_CPUS, cpu_id); + None + } + + /// Gets the number of registered CPUs. + pub fn registered_count(&self) -> usize { + self.registered_count.load(Ordering::SeqCst) as usize + } + + /// Gets the maximum number of CPUs supported. + pub const fn max_cpus(&self) -> usize { + MAX_CPUS + } + + /// Gets the APIC ID of the BSP. + pub fn bsp_id(&self) -> Option { + let id = self.bsp_id.load(Ordering::SeqCst); + if id == u32::MAX { None } else { Some(id) } + } + + /// Checks if the given CPU ID is the BSP. + pub fn is_bsp(&self, cpu_id: u32) -> bool { + self.bsp_id() == Some(cpu_id) + } + + /// Finds the slot index for a given CPU ID (APIC ID). + fn find_slot(&self, cpu_id: u32) -> Option { + for (index, slot) in self.slots.iter().enumerate() { + if slot.get_cpu_id() == Some(cpu_id) { + return Some(index); + } + } + None + } + + /// Finds the slot index for a given CPU ID (public wrapper). + pub fn find_cpu_index(&self, cpu_id: u32) -> Option { + self.find_slot(cpu_id) + } + + /// Gets the APIC ID of the CPU at the given slot index. + /// + /// Returns `None` if the index is out of range or the slot is unused. + pub fn get_cpu_id_by_index(&self, index: usize) -> Option { + if index >= MAX_CPUS { + return None; + } + self.slots[index].get_cpu_id() + } + + /// Gets the AP state by slot index. + /// + /// Returns `None` if the index is out of range or the slot is unused. + pub fn get_ap_state_by_index(&self, index: usize) -> Option { + if index >= MAX_CPUS { + return None; + } + let slot = &self.slots[index]; + if slot.is_used() { Some(ApState::from(slot.state.load(Ordering::Acquire))) } else { None } + } + + /// Gets the state of an AP. + pub fn get_ap_state(&self, cpu_id: u32) -> Option { + let index = self.find_slot(cpu_id)?; + Some(ApState::from(self.slots[index].state.load(Ordering::Acquire))) + } + + /// Sets the state of an AP. + pub fn set_ap_state(&self, cpu_id: u32, state: ApState) -> bool { + let index = match self.find_slot(cpu_id) { + Some(idx) => idx, + None => return false, + }; + + let slot = &self.slots[index]; + + // Don't allow changing BSP state + if slot.is_bsp.load(Ordering::Acquire) != 0 { + log::warn!("Attempted to change BSP state, ignoring"); + return false; + } + + slot.state.store(state as u8, Ordering::Release); + true + } + + /// Iterates over all registered AP IDs. + /// + /// Calls the provided closure for each registered AP. + pub fn for_each_ap(&self, mut f: F) { + for slot in &self.slots { + if let Some(cpu_id) = slot.get_cpu_id() { + if slot.is_bsp.load(Ordering::Acquire) == 0 { + f(cpu_id); + } + } + } + } + + /// Counts APs in a specific state. + pub fn count_aps_in_state(&self, state: ApState) -> usize { + let mut count = 0; + for slot in &self.slots { + if slot.is_used() + && slot.is_bsp.load(Ordering::Acquire) == 0 + && slot.state.load(Ordering::Acquire) == state as u8 + { + count += 1; + } + } + count + } +} + +impl Default for CpuManager { + fn default() -> Self { + Self::new() + } +} + +/// Gets the current CPU's APIC ID. +/// +/// On x86_64, this reads the APIC ID from the Local APIC or CPUID. +#[cfg(target_arch = "x86_64")] +pub fn get_current_cpu_id() -> u32 { + // Use CPUID to get the initial APIC ID + // CPUID function 0x01, EBX[31:24] contains the initial APIC ID + + // SAFETY: CPUID is always available on x86_64 and reading it is safe. + let CpuidResult { ebx, .. } = x86_64::__cpuid(0x01); + let cpuid_result = (ebx >> 24) & 0xff; + cpuid_result +} + +/// Gets the current CPU's APIC ID (stub for non-x86_64). +#[cfg(not(target_arch = "x86_64"))] +pub fn get_current_cpu_id() -> u32 { + 0 +} + +/// MSR index for IA32_APIC_BASE. +const IA32_APIC_BASE_MSR_INDEX: u32 = 0x1B; + +/// BSP flag bit in IA32_APIC_BASE MSR (bit 8). +const IA32_APIC_BSP: u64 = 1 << 8; + +/// MSR index for SMM Base Address (SMBASE). +pub const IA32_MSR_SMBASE: u32 = 0x9E; + +/// Reads a Model-Specific Register (MSR) by index. +/// +/// # Safety +/// +/// The caller must ensure the MSR index is valid and readable on the current platform. +#[cfg(target_arch = "x86_64")] +pub unsafe fn read_msr(msr: u32) -> Result { + let lo: u32; + let hi: u32; + unsafe { + core::arch::asm!( + "rdmsr", + in("ecx") msr, + out("eax") lo, + out("edx") hi, + options(nomem, nostack), + ); + } + Ok(((hi as u64) << 32) | (lo as u64)) +} + +/// Reads a Model-Specific Register (stub for non-x86_64). +#[cfg(not(target_arch = "x86_64"))] +pub unsafe fn read_msr(_msr: u32) -> Result { + Err("rdmsr not supported on this architecture") +} + +/// Writes a 64-bit value to a Model-Specific Register (MSR). +/// +/// # Safety +/// +/// The caller must ensure the MSR index is valid and writable on the current platform. +#[cfg(target_arch = "x86_64")] +pub unsafe fn write_msr(msr: u32, value: u64) -> Result<(), &'static str> { + let lo = value as u32; + let hi = (value >> 32) as u32; + unsafe { + core::arch::asm!( + "wrmsr", + in("ecx") msr, + in("eax") lo, + in("edx") hi, + options(nomem, nostack), + ); + } + Ok(()) +} + +/// Writes a Model-Specific Register (stub for non-x86_64). +#[cfg(not(target_arch = "x86_64"))] +pub unsafe fn write_msr(_msr: u32, _value: u64) -> Result<(), &'static str> { + Err("wrmsr not supported on this architecture") +} + +/// Checks if the current processor is the Bootstrap Processor (BSP). +/// +/// This reads the IA32_APIC_BASE MSR and checks the BSP flag (bit 8). +/// The BSP flag is set by hardware during reset and indicates which +/// processor is the bootstrap processor. +/// +/// # Returns +/// +/// `true` if this is the BSP, `false` if this is an AP. +#[cfg(target_arch = "x86_64")] +pub fn is_bsp() -> bool { + // SAFETY: Reading the IA32_APIC_BASE MSR is safe on x86_64. + let apic_base_lo: u32; + let apic_base_hi: u32; + unsafe { + core::arch::asm!( + "rdmsr", + in("ecx") IA32_APIC_BASE_MSR_INDEX, + out("eax") apic_base_lo, + out("edx") apic_base_hi, + ); + } + + let apic_base = ((apic_base_hi as u64) << 32) | (apic_base_lo as u64); + (apic_base & IA32_APIC_BSP) != 0 +} + +/// Checks if the current processor is the BSP (stub for non-x86_64). +#[cfg(not(target_arch = "x86_64"))] +pub fn is_bsp() -> bool { + true // Assume BSP on non-x86_64 platforms +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cpu_manager_creation() { + let manager: CpuManager<4> = CpuManager::new(); + assert_eq!(manager.registered_count(), 0); + assert!(manager.bsp_id().is_none()); + assert_eq!(manager.max_cpus(), 4); + } + + #[test] + fn test_cpu_manager_is_const() { + // Verify we can create a static instance + static _MANAGER: CpuManager<8> = CpuManager::new(); + } + + #[test] + fn test_cpu_registration() { + let manager: CpuManager<4> = CpuManager::new(); + + // Register BSP + let bsp_idx = manager.register_cpu(0, true); + assert_eq!(bsp_idx, Some(0)); + assert_eq!(manager.bsp_id(), Some(0)); + assert!(manager.is_bsp(0)); + + // Register APs + let ap1_idx = manager.register_cpu(1, false); + assert_eq!(ap1_idx, Some(1)); + assert!(!manager.is_bsp(1)); + + let ap2_idx = manager.register_cpu(2, false); + assert_eq!(ap2_idx, Some(2)); + + assert_eq!(manager.registered_count(), 3); + } + + #[test] + fn test_duplicate_registration() { + let manager: CpuManager<4> = CpuManager::new(); + + assert!(manager.register_cpu(1, false).is_some()); + assert!(manager.register_cpu(1, false).is_none()); // Should fail - duplicate + } + + #[test] + fn test_ap_state_management() { + let manager: CpuManager<4> = CpuManager::new(); + manager.register_cpu(0, true); + manager.register_cpu(1, false); + + // Check initial state + assert_eq!(manager.get_ap_state(1), Some(ApState::InHoldingPen)); + + // Change state + assert!(manager.set_ap_state(1, ApState::Busy)); + assert_eq!(manager.get_ap_state(1), Some(ApState::Busy)); + + // Cannot change BSP state + assert!(!manager.set_ap_state(0, ApState::Halted)); + } + + #[test] + fn test_for_each_ap() { + let manager: CpuManager<4> = CpuManager::new(); + manager.register_cpu(0, true); + manager.register_cpu(1, false); + manager.register_cpu(2, false); + + let mut ap_ids = [0u32; 4]; + let mut count = 0; + manager.for_each_ap(|id| { + if count < 4 { + ap_ids[count] = id; + count += 1; + } + }); + + assert_eq!(count, 2); + assert!(ap_ids[..count].contains(&1)); + assert!(ap_ids[..count].contains(&2)); + } + + #[test] + fn test_max_cpu_limit() { + let manager: CpuManager<2> = CpuManager::new(); + assert!(manager.register_cpu(0, true).is_some()); + assert!(manager.register_cpu(1, false).is_some()); + assert!(manager.register_cpu(2, false).is_none()); // Should fail + } + + #[test] + fn test_count_aps_in_state() { + let manager: CpuManager<4> = CpuManager::new(); + manager.register_cpu(0, true); + manager.register_cpu(1, false); + manager.register_cpu(2, false); + + assert_eq!(manager.count_aps_in_state(ApState::InHoldingPen), 2); + assert_eq!(manager.count_aps_in_state(ApState::Busy), 0); + + manager.set_ap_state(1, ApState::Busy); + assert_eq!(manager.count_aps_in_state(ApState::InHoldingPen), 1); + assert_eq!(manager.count_aps_in_state(ApState::Busy), 1); + } +} diff --git a/patina_mm_supervisor_core/src/entry_point.asm b/patina_mm_supervisor_core/src/entry_point.asm new file mode 100644 index 000000000..3b9ad2681 --- /dev/null +++ b/patina_mm_supervisor_core/src/entry_point.asm @@ -0,0 +1,19 @@ +# +# Exception entry point logic for X64. +# +# Copyright (c) Microsoft Corporation. +# +# SPDX-License-Identifier: Apache-2.0 +# + +.section .data + +.section .text +.global rust_main +.global efi_main + +.align 8 +# Shim layer that redefines the contract between runtime module and init. +efi_main: + + jmp rust_main diff --git a/patina_mm_supervisor_core/src/lib.rs b/patina_mm_supervisor_core/src/lib.rs new file mode 100644 index 000000000..d1a9d79e2 --- /dev/null +++ b/patina_mm_supervisor_core/src/lib.rs @@ -0,0 +1,2191 @@ +//! MM Supervisor Core +//! +//! A pure Rust implementation of the MM Supervisor Core for standalone MM mode environments. +//! +//! This crate provides the core functionality for running a supervisor in MM (Management Mode) +//! that orchestrates incoming requests on the BSP while APs wait in a holding pen. +//! +//! ## Architecture +//! +//! The entry point is executed on all cores: +//! - **BSP**: Performs one-time initialization and enters the request serving loop +//! - **APs**: Enter a holding pen and poll mailboxes for commands from BSP +//! +//! ## Memory Model +//! +//! This is a core component that manages its own memory. It does **not** use heap allocation. +//! All structures use fixed-size arrays with compile-time constants provided via const generics. +//! +//! ## Example +//! +//! ```rust,ignore +//! use patina_mm_supervisor_core::*; +//! +//! struct MyPlatform; +//! +//! impl PlatformInfo for MyPlatform { +//! type CpuInfo = Self; +//! const MAX_CPU_COUNT: usize = 8; +//! } +//! +//! impl CpuInfo for MyPlatform { +//! fn ap_poll_timeout_us() -> u64 { 1000 } +//! } +//! +//! static SUPERVISOR: MmSupervisorCore = MmSupervisorCore::new(); +//! ``` +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! +#![cfg_attr(all(not(feature = "std"), not(test)), no_std)] +#![cfg(target_arch = "x86_64")] +#![feature(coverage_attribute)] +#![allow(incomplete_features)] +#![feature(generic_const_exprs)] + +mod cpu; +mod mailbox; +pub mod mm_mem; +pub mod paging_allocator; +pub mod perf_timer; +pub mod privilege_mgmt; +pub mod save_state; +pub mod supervisor_handlers; +pub mod unblock_memory; + +pub use cpu::{ApState, CpuInfo, CpuManager, get_current_cpu_id, is_bsp}; +pub use mailbox::{ApCommand, ApMailbox, ApResponse, MailboxManager}; +pub use mm_mem::{ + AllocationType, MM_PEI_MMRAM_MEMORY_RESERVE_GUID, PAGE_ALLOCATOR, PageAllocError, PageAllocator, + SMM_SMRAM_MEMORY_GUID, SmramDescriptor, +}; +pub use paging_allocator::{ + DEFAULT_PAGING_POOL_PAGES, PAGING_ALLOCATOR, PagingAllocError, PagingPoolAllocator, SharedPagingAllocator, +}; +pub use privilege_mgmt::{SyscallInterface, invoke_demoted_routine}; +pub use supervisor_handlers::{SUPERVISOR_MMI_HANDLERS, SupervisorMmiHandler}; +pub use unblock_memory::{UNBLOCKED_MEMORY_TRACKER, UnblockError, UnblockedMemoryEntry, UnblockedMemoryTracker}; + +pub use patina_internal_mm_common::UserCommandType; + +use core::{ + arch::{asm, global_asm}, + ffi::c_void, + num::NonZeroUsize, + panic, + ptr::NonNull, + sync::atomic::{AtomicBool, AtomicU32, Ordering}, +}; + +use x86_64::structures::DescriptorTablePointer; + +use patina::base::UEFI_PAGE_SIZE; +use patina::management_mode::MmCommBufferStatus; +use patina::management_mode::comm_buffer_hob::{MM_COMM_BUFFER_HOB_GUID, MmCommonBufferHobData}; +use patina::pi::hob::{self, Hob, PhaseHandoffInformationTable}; +use patina::pi::mm_cis::EfiMmEntryContext; +use patina::pi::protocols::communication::EfiMmCommunicateHeader; +use patina_paging::{ + MemoryAttributes, PageTable, PagingType, + x64::{X64PageTable, disable_write_protection, enable_write_protection}, +}; +use r_efi::efi; + +use patina_mm_policy::{MemDescriptorV1_0, walk_page_table}; + +// GUID for gMmSupervisorHobMemoryAllocModuleGuid +// { 0x3efafe72, 0x3dbf, 0x4341, { 0xad, 0x04, 0x1c, 0xb6, 0xe8, 0xb6, 0x8e, 0x5e }} +/// GUID used in MemoryAllocationModule HOBs to identify MM Supervisor module allocations. +pub const MM_SUPERVISOR_HOB_MEMORY_ALLOC_MODULE_GUID: efi::Guid = + efi::Guid::from_fields(0x3efafe72, 0x3dbf, 0x4341, 0xad, 0x04, &[0x1c, 0xb6, 0xe8, 0xb6, 0x8e, 0x5e]); + +// GUID for gMmSupervisorUserGuid +// { 0x30d1cc3f, 0xc1db, 0x41ed, { 0xb1, 0x13, 0xab, 0xce, 0x21, 0xb0, 0x2b, 0xce }} +/// GUID identifying the MM Supervisor User module. +pub const MM_SUPERVISOR_USER_GUID: efi::Guid = + efi::Guid::from_fields(0x30d1cc3f, 0xc1db, 0x41ed, 0xb1, 0x13, &[0xab, 0xce, 0x21, 0xb0, 0x2b, 0xce]); + +// GUID for gMmCommonRegionHobGuid +// { 0xd4ffc718, 0xfb82, 0x4274, { 0x9a, 0xfc, 0xaa, 0x8b, 0x1e, 0xef, 0x52, 0x93 } } +pub const MM_COMMON_REGION_HOB_GUID: efi::Guid = + efi::Guid::from_fields(0xd4ffc718, 0xfb82, 0x4274, 0x9a, 0xfc, &[0xaa, 0x8b, 0x1e, 0xef, 0x52, 0x93]); + +/// MM Common Region HOB Data Structure +/// +/// This structure contains information about the common memory region used by the MM Supervisor. +#[repr(C, packed)] +#[derive(Debug, Clone, Copy)] +pub struct MmCommonRegionHobData { + /// Type of the common region, must be 0 to represent the MM Supervisor communication buffer region + pub region_type: u64, + /// Base address of the supervisor communication buffer region + pub addr: u64, + /// Number of pages in the supervisor communication buffer region + pub number_of_pages: u64, +} + +// GUID for gMmSupervisorPassDownHobGuid +// { 0x3f2d2d1a, 0x7c6a, 0x4e2e, { 0x91, 0x2e, 0x5c, 0x4f, 0x5b, 0x8c, 0x2a, 0x9d } } +/// GUID for the MM Supervisor PassDown HOB. +pub const MM_SUPV_PASS_DOWN_HOB_GUID: efi::Guid = + efi::Guid::from_fields(0x3f2d2d1a, 0x7c6a, 0x4e2e, 0x91, 0x2e, &[0x5c, 0x4f, 0x5b, 0x8c, 0x2a, 0x9d]); + +/// MM Supervisor PassDown HOB Revision +pub const MM_SUPV_PASS_DOWN_HOB_REVISION: u32 = 1; + +/// MM Supervisor PassDown HOB Data Structure +/// +/// This structure contains various buffer pointers and sizes passed from +/// the PEI phase to the MM Supervisor. +#[repr(C, packed)] +#[derive(Debug, Clone, Copy)] +pub struct MmSupvPassDownHobData { + /// Revision of this HOB structure + pub revision: u32, + /// Reserved for future use + pub reserved: u32, + /// Base address of CPL3 stack for MM Supervisor + pub mm_supervisor_cpl3_stack_base: u64, + /// Per-CPU stack size for CPL3 + pub mm_supervisor_cpl3_per_core_stack_size: u64, + /// MM Supervisor CPU private data base address + pub mm_supv_cpu_private: u64, + /// Size of MM Supervisor CPU private data + pub mm_supv_cpu_private_size: u64, + /// MM Initialized buffer base address + pub mm_initialized_buffer: u64, + /// MM Supervisor firmware policy buffer base address + pub mm_supv_firmware_policy_buffer: u64, + /// Size of MM Supervisor firmware policy buffer + pub mm_supv_firmware_policy_buffer_size: u64, + /// Size of the MMI entry point structure (for validating against expected size in supervisor) + pub mmi_entrypoint_size: u64, + /// Base address of the BSP MM + pub bsp_mm_base_address: u64, +} + +/// Errors that can occur during policy initialization. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PolicyInitError { + /// The HOB list pointer is null. + NullHobList, + /// Some HOB not found. + HobNotFound, + /// Invalid PassDown HOB revision. + InvalidRevision { found: u32, expected: u32 }, + /// Firmware policy buffer is null or empty. + NullFirmwarePolicyBuffer, + /// Invalid policy data. + InvalidPolicyData, + /// Memory allocation failed for policy buffers. + MemoryAllocationFailed, + /// One or more communication buffers are not properly initialized. + MissingCommunicationBuffer, +} + +use patina_internal_cpu::interrupts::Interrupts; +use spin::{Mutex, Once}; + +global_asm!(include_str!("entry_point.asm")); + +/// A trait to be implemented by the platform to provide configuration values and types to be used +/// by the MM Supervisor Core. +/// +/// ## Example +/// +/// ```rust,ignore +/// use patina_mm_supervisor_core::*; +/// +/// struct ExamplePlatform; +/// +/// impl CpuInfo for ExamplePlatform { +/// fn ap_poll_timeout_us() -> u64 { 1000 } +/// } +/// +/// impl PlatformInfo for ExamplePlatform { +/// type CpuInfo = Self; +/// const MAX_CPU_COUNT: usize = 8; +/// } +/// ``` +#[cfg_attr(test, mockall::automock(type CpuInfo = MockCpuInfo;))] +pub trait PlatformInfo: 'static { + /// The platform's CPU information and configuration. + type CpuInfo: CpuInfo; + + /// Maximum number of CPUs supported by the platform. + /// This is used to size the CPU manager and mailbox arrays. + const MAX_CPU_COUNT: usize; +} + +/// Static reference to the MM Supervisor Core instance. +/// +/// This is set during the `entry_point` call and provides global access to the supervisor. +static __SUPERVISOR: Once = Once::new(); + +/// Flag indicating that BSP one-time initialization is complete. +static BSP_INIT_COMPLETE: AtomicBool = AtomicBool::new(false); + +/// Pointer to the per-core initialized buffer from the PassDown HOB. +/// Each core has a 64-bit slot at `buffer_base + (cpu_index * 8)`. +/// A non-zero value indicates the core has completed initialization. +static MM_INITIALIZED_BUFFER: Once = Once::new(); + +/// Counter for tracking how many cores have completed their per-core init. +static PER_CORE_INIT_COUNT: AtomicU32 = AtomicU32::new(0); + +/// The policy object is initialized once during BSP initialization and provides access to the security policy +/// for the MM Supervisor. It is stored in a static variable for global access. +/// The policy gate is initialized from the firmware policy buffer provided in the PassDown HOB. +pub(crate) static POLICY_GATE: Once = Once::new(); + +/// Global page table instance for managing page attributes. +/// +/// Initialized during BSP init from the active CR3 register. This allows the supervisor +/// to modify page table attributes (e.g., marking supervisor pages as R/W + NX) when +/// allocating memory. +pub(crate) static PAGE_TABLE: Mutex>> = Mutex::new(None); + +/// Result of a page table ownership query. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum PageOwnership { + /// The page is user-accessible (U/S = 1, SpecialPurpose clear). + User, + /// The page is supervisor-only (U/S = 0, SpecialPurpose set). + Supervisor, +} + +/// Aligns an address and size to page boundaries for page table queries. +/// +/// Rounds the address down to the nearest page boundary and adjusts the size +/// upward so the entire original range `[address, address+size)` is covered. +/// +/// Returns `(page_aligned_address, page_aligned_size)`. +#[inline] +fn page_align_range(address: u64, size: u64) -> (u64, u64) { + const PAGE_MASK: u64 = (UEFI_PAGE_SIZE as u64) - 1; + let aligned_start = address & !PAGE_MASK; + let end = address.saturating_add(size); + let aligned_end = end.saturating_add(PAGE_MASK) & !PAGE_MASK; + (aligned_start, aligned_end.saturating_sub(aligned_start)) +} + +/// Queries the page table to determine the ownership (user vs supervisor) of an address. +/// +/// The address and size are page-aligned before querying (rounded down / up respectively). +/// +/// Checks the `Supervisor` attribute which maps to the U/S bit on X64: +/// - `Supervisor` set => `PageOwnership::Supervisor` (U/S = 0) +/// - `Supervisor` clear => `PageOwnership::User` (U/S = 1) +/// +/// Returns `None` if the page table is not initialized or the address is unmapped. +pub(crate) fn query_address_ownership(address: u64, size: u64) -> Option { + let (aligned_addr, aligned_size) = page_align_range(address, size); + let page_table = PAGE_TABLE.lock(); + let pt = page_table.as_ref()?; + let attrs = pt.query_memory_region(aligned_addr, aligned_size).ok()?; + log::info!( + "Queried page ownership for address range 0x{:016x}-0x{:016x}: attributes={:?}", + aligned_addr, + aligned_addr + aligned_size, + attrs + ); + if attrs.contains(MemoryAttributes::Supervisor) { + Some(PageOwnership::Supervisor) + } else { + Some(PageOwnership::User) + } +} + +/// Communication buffer configuration extracted from PassDown HOB. +#[derive(Debug, Clone, Copy, Default)] +pub struct CommBufferConfig { + /// MM Supervisor communication buffer (external interface). + pub supv_comm_buffer: u64, + /// MM Supervisor internal communication buffer. + pub supv_comm_buffer_internal: u64, + /// Size of supervisor communication buffer. + pub supv_comm_buffer_size: u64, + /// MM User communication buffer (external interface). + pub user_comm_buffer: u64, + /// MM User internal communication buffer. + pub user_comm_buffer_internal: u64, + /// Size of user communication buffer. + pub user_comm_buffer_size: u64, + /// MM Supervisor status buffer (indicates target: supervisor or user). + pub status_buffer: u64, + /// MM Supervisor to User buffer. + pub supv_to_user_buffer: u64, + /// Size of Supervisor to User buffer. + pub supv_to_user_buffer_size: u64, +} + +/// Communication buffer configuration initialized from PassDown HOB. +pub(crate) static COMM_BUFFER_CONFIG: Once = Once::new(); + +/// User module entry point discovered from HOB list. +static USER_ENTRY_POINT: Once = Once::new(); + +/// Pointer to the SMM_CPU_PRIVATE_DATA structure from the PassDown HOB. +/// This is used to access the SmmCoreEntryContext for user request dispatch. +static SMM_CPU_PRIVATE: Once = Once::new(); + +/// Type-erased function pointer for AP startup dispatch. +/// +/// This is set during [`MmSupervisorCore`] initialization. The function is monomorphized +/// for the concrete `PlatformInfo` type so the syscall dispatcher can invoke it without +/// knowing the platform's const-generic parameters. +/// +/// Signature: `fn(cpu_index: u64, procedure: u64, argument: u64) -> u64` +/// +/// Returns 0 on success, or an EFI status code on failure. +pub(crate) static AP_STARTUP_FN: Once u64> = Once::new(); + +/// EFI_SMM_RESERVED_SMRAM_REGION structure. +/// +/// Describes a reserved SMRAM region that cannot be used for the SMRAM heap. +/// Matches the C structure from PI specification. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct EfiSmmReservedSmramRegion { + /// Starting address of the reserved SMRAM area. + pub smram_reserved_start: u64, + /// Number of bytes occupied by the reserved SMRAM area. + pub smram_reserved_size: u64, +} + +/// SMM_CPU_PRIVATE_DATA structure. +/// +/// Private structure for the SMM CPU module, passed from PEI via the PassDown HOB. +/// Matches the C `SMM_CPU_PRIVATE_DATA` layout from MpService.h. +/// +/// Layout (x86_64): +/// ```text +/// Offset Field +/// 0x00 signature (UINTN) +/// 0x08 smm_cpu_handle (EFI_HANDLE) +/// 0x10 processor_info (ptr) +/// 0x18 cpu_save_state_size (ptr) +/// 0x20 cpu_save_state (ptr) +/// 0x28 smm_reserved_smram_region[1] (16 bytes) +/// 0x38 smm_core_entry_context (40 bytes, inline) +/// 0x60 smm_core_entry (fn ptr) +/// 0x68 smm_user_entry (fn ptr) +/// 0x70 ap_wrapper_func (ptr) +/// 0x78 token_list (ptr) +/// 0x80 first_free_token (ptr) +/// Total: 0x88 bytes +/// ``` +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct SmmCpuPrivateData { + /// Signature ('scpu'). + pub signature: u64, + /// SMM CPU handle. + pub smm_cpu_handle: u64, + /// Pointer to processor information array. + pub processor_info: u64, + /// Pointer to per-CPU save state size array. + pub cpu_save_state_size: u64, + /// Pointer to per-CPU save state pointer array. + pub cpu_save_state: u64, + /// Reserved SMRAM region descriptor (single element array). + pub smm_reserved_smram_region: EfiSmmReservedSmramRegion, + /// Inline entry context structure (40 bytes). + pub smm_core_entry_context: EfiMmEntryContext, + /// Supervisor core entry point function pointer. + pub smm_core_entry: u64, + /// User core entry point function pointer. + pub smm_user_entry: u64, + /// AP wrapper function pointer. + pub ap_wrapper_func: u64, + /// Token list pointer. + pub token_list: u64, + /// First free token pointer. + pub first_free_token: u64, +} + +/// Request target derived from MM_COMM_BUFFER_STATUS. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RequestTarget { + /// No pending request (buffer not valid). + None, + /// Request targets the Supervisor. + Supervisor, + /// Request targets the User module. + User, +} + +impl From<&MmCommBufferStatus> for RequestTarget { + fn from(status: &MmCommBufferStatus) -> Self { + if status.is_comm_buffer_valid == 0 { + RequestTarget::User + } else if status.talk_to_supervisor != 0 { + RequestTarget::Supervisor + } else { + RequestTarget::User + } + } +} + +/// The MM Supervisor Core responsible for managing the standalone MM environment. +/// +/// This struct is generic over the [`PlatformInfo`] trait, which provides platform-specific +/// configuration including compile-time constants for array sizes. +/// +/// The supervisor manages: +/// - BSP initialization and request handling +/// - AP management through the holding pen and mailbox system +/// - Request dispatching and response handling +/// +/// ## Memory Model +/// +/// This struct does not perform heap allocation. All internal structures use fixed-size +/// arrays based on the `MAX_CPU_COUNT` constant from [`PlatformInfo`]. +/// +/// ## Usage +/// +/// Create a static instance of the supervisor and call `entry_point` from all cores: +/// +/// ```rust,ignore +/// use patina_mm_supervisor_core::*; +/// +/// static SUPERVISOR: MmSupervisorCore = MmSupervisorCore::new(); +/// +/// #[no_mangle] +/// pub extern "efiapi" fn mm_entry(hob_list: *const c_void) -> ! { +/// SUPERVISOR.entry_point(hob_list) +/// } +/// ``` +pub struct MmSupervisorCore +where + [(); P::MAX_CPU_COUNT]:, +{ + /// Manager for CPU-related operations. + cpu_manager: CpuManager<{ P::MAX_CPU_COUNT }>, + /// Manager for AP mailboxes. + mailbox_manager: MailboxManager<{ P::MAX_CPU_COUNT }, P::CpuInfo>, + /// Syscall interface for privilege transitions. + syscall_interface: SyscallInterface<{ P::MAX_CPU_COUNT }>, + /// Flag indicating if the core has been initialized. + initialized: AtomicBool, + /// Phantom data for the platform type. + _phantom: core::marker::PhantomData

, +} + +// SAFETY: The MmSupervisorCore is designed to be shared across threads with proper synchronization. +unsafe impl Send for MmSupervisorCore

where [(); P::MAX_CPU_COUNT]: {} +unsafe impl Sync for MmSupervisorCore

where [(); P::MAX_CPU_COUNT]: {} + +pub(crate) fn is_buffer_inside_mmram(base: u64, size: u64) -> bool { + // we will go over the page allocator to see if this region falls inside any of the MMRAM regions + mm_mem::PAGE_ALLOCATOR.is_region_inside_mmram(base, size) +} + +/// Read CR3 register. +pub(crate) fn read_cr3() -> u64 { + let mut _value = 0u64; + + #[cfg(all(not(test), target_arch = "x86_64"))] + { + // SAFETY: inline asm is inherently unsafe because Rust can't reason about it. + // In this case we are reading the CR3 register, which is a safe operation. + unsafe { + asm!("mov {}, cr3", out(reg) _value, options(nostack, preserves_flags)); + } + } + + _value +} + +/// Offset from SMBASE where the SMI handler code is located. +const SMM_HANDLER_OFFSET: u64 = 0x8000; + +/// Index into the Fixup64 array for the SMI handler IDTR pointer. +const FIXUP64_SMI_HANDLER_IDTR: usize = 5; + +/// Per-core MMI entry structure header. +/// +/// This packed structure is embedded at the end of the SMI handler binary template. +/// It contains offsets (relative to the header start) to fixup arrays that the +/// relocation code uses to patch per-CPU values into the binary. +/// +/// Layout matches the C `PER_CORE_MMI_ENTRY_STRUCT_HDR` from SeaResponder.h. +#[repr(C, packed)] +#[derive(Debug, Clone, Copy)] +struct PerCoreMmiEntryStructHdr { + /// Header version (4 for version 4). + header_version: u32, + /// Offset from header start to FixUpStruct array. + fixup_struct_offset: u8, + /// Number of FixUpStruct array entries. + fixup_struct_num: u8, + /// Offset from header start to Fixup64 array. + fixup64_offset: u8, + /// Number of Fixup64 array entries. + fixup64_num: u8, + /// Offset from header start to Fixup32 array. + fixup32_offset: u8, + /// Number of Fixup32 array entries. + fixup32_num: u8, + /// Offset from header start to Fixup8 array. + fixup8_offset: u8, + /// Number of Fixup8 array entries. + fixup8_num: u8, + /// SMI entry binary version. + binary_version: u16, + /// SPL value for SMI entry binary. + spl_value: u32, + /// Reserved for future use. + reserved: u32, +} + +/// Read the current IDT Register (IDTR) via the `SIDT` instruction. +/// +/// Returns a [`DescriptorTablePointer`] containing the IDT base and limit. +fn read_idtr() -> DescriptorTablePointer { + let mut descriptor = DescriptorTablePointer { limit: 0, base: x86_64::VirtAddr::zero() }; + + #[cfg(all(not(test), target_arch = "x86_64"))] + { + // SAFETY: SIDT stores the 10-byte IDTR pseudo-descriptor to the specified + // memory location. This is a read-only operation on CPU state. + unsafe { + asm!( + "sidt [{}]", + in(reg) &mut descriptor as *mut DescriptorTablePointer, + options(nostack, preserves_flags) + ); + } + } + + descriptor +} + +/// Checks if a specific core has completed initialization. +/// +/// Reads the 64-bit slot at `mm_initialized_buffer + (cpu_index * 8)`. +/// A non-zero value indicates the core has completed initialization. +fn is_core_initialized(cpu_index: usize) -> bool { + if let Some(&buffer_base) = MM_INITIALIZED_BUFFER.get() { + if buffer_base == 0 { + return false; + } + let slot_ptr = (buffer_base as usize + cpu_index) as *const u8; + // SAFETY: The buffer is provided by the MM IPL and is guaranteed to be valid. + // Each core only reads its own slot or slots of other cores. + let value = unsafe { core::ptr::read_volatile(slot_ptr) }; + value != 0 + } else { + false + } +} + +/// Marks a specific core as initialized. +/// +/// Writes a non-zero value to the 64-bit slot at `mm_initialized_buffer + (cpu_index * 8)`. +fn mark_core_initialized(cpu_index: usize) { + if let Some(&buffer_base) = MM_INITIALIZED_BUFFER.get() { + if buffer_base == 0 { + log::error!("MM initialized buffer is null, cannot mark core {} as initialized", cpu_index); + return; + } + let slot_ptr = (buffer_base as usize + cpu_index) as *mut u8; + // SAFETY: The buffer is provided by the MM IPL and is guaranteed to be valid. + // Each core writes only to its own slot. + unsafe { core::ptr::write_volatile(slot_ptr, 1) }; + log::trace!("Core {} marked as initialized at 0x{:016x}", cpu_index, slot_ptr as u64); + } else { + log::error!("MM initialized buffer not set, cannot mark core {} as initialized", cpu_index); + } +} + +/// Helper function to disable the SMAP bit in EFLAGS to allow supervisor code to access user memory when needed. +pub fn disable_smap() { + #[cfg(all(not(test), target_arch = "x86_64"))] + unsafe { + asm!( + "stac", // Set AC flag to enable access to user memory + options(nostack, preserves_flags) + ); + } +} + +/// Helper function to re-enable the SMAP bit in EFLAGS after accessing user memory. +pub fn enable_smap() { + #[cfg(all(not(test), target_arch = "x86_64"))] + unsafe { + asm!( + "clac", // Clear AC flag to re-enable SMAP protections + options(nostack, preserves_flags) + ); + } +} + +#[coverage(off)] +impl MmSupervisorCore

+where + [(); P::MAX_CPU_COUNT]:, +{ + /// Creates a new instance of the MM Supervisor Core. + /// + /// This is a const fn that performs no heap allocation. + pub const fn new() -> Self { + Self { + cpu_manager: CpuManager::new(), + mailbox_manager: MailboxManager::new(), + syscall_interface: SyscallInterface::new(), + initialized: AtomicBool::new(false), + _phantom: core::marker::PhantomData, + } + } + + /// Sets the static supervisor instance for global access. + /// + /// Returns true if the address was successfully stored, false if already set. + /// Also registers the type-erased AP startup function pointer. + #[must_use] + fn set_instance(&'static self) -> bool { + let physical_address = NonNull::from_ref(self).expose_provenance(); + let stored = &physical_address == __SUPERVISOR.call_once(|| physical_address); + if stored { + // Register the monomorphized AP startup function for this platform + AP_STARTUP_FN.call_once(|| Self::start_ap_procedure_trampoline); + } + stored + } + + /// Gets the static MM Supervisor Core instance for global access. + #[allow(unused)] + pub(crate) fn instance<'a>() -> &'a Self { + // SAFETY: The pointer is guaranteed to be valid as set_instance ensures single initialization. + unsafe { + NonNull::::with_exposed_provenance( + *__SUPERVISOR.get().expect("MM Supervisor Core is not initialized."), + ) + .as_ref() + } + } + + /// The entry point for the MM Supervisor Core. + /// + /// This function is called on all cores (BSP and APs). The BSP performs initialization + /// and enters the request serving loop, while APs enter the holding pen. + /// + /// # Arguments + /// + /// * `hob_list` - Pointer to the HOB (Hand-Off Block) list passed from the pre-MM phase. + /// + /// # Panics + /// + /// Panics if: + /// - The supervisor instance was already set + /// - The HOB list pointer is null + /// + /// # Returns + /// + /// On the first call (initialization phase), this function returns after init is complete. + /// On subsequent calls, BSP enters the request loop and APs enter the holding pen (neither returns). + pub fn entry_point(&'static self, cpu_index: usize, hob_list: *const c_void) { + // Get the current CPU's APIC ID + let cpu_id = cpu::get_current_cpu_id(); + + // Determine if we're BSP by checking IA32_APIC_BASE MSR + let is_bsp = cpu::is_bsp(); + + // Check if this core has already completed initialization (per-core check) + if is_core_initialized(cpu_index) { + // Subsequent entry: go directly to request loop or holding pen (does not return) + self.enter_runtime(cpu_id); + + return; + } + + // First entry: initialization phase + if is_bsp { + // BSP path: Initialize the supervisor + assert!(self.set_instance(), "MM Supervisor Core instance was already set!"); + assert!(!hob_list.is_null(), "MM Supervisor Core requires a non-null HOB list pointer."); + + log::info!("MM Supervisor Core v{}", env!("CARGO_PKG_VERSION")); + log::info!("BSP (CPU {}, index {}) starting one-time initialization...", cpu_id, cpu_index); + + // Register BSP with CPU manager + self.cpu_manager.register_cpu(cpu_id, true); + + // Perform BSP-only one-time initialization (this sets up MM_INITIALIZED_BUFFER) + self.bsp_init(hob_list); + + // Dispatch to the user level entry point discovered from the HOB list (if found) + let user_entry = match USER_ENTRY_POINT.get() { + Some(&entry) if entry != 0 => entry, + _ => { + log::error!("User entry point not configured, cannot demote"); + return; + } + }; + + let cpl3_stack = match self.syscall_interface.get_cpl3_stack(cpu_index) { + Ok(stack) => stack, + Err(e) => { + log::error!("Failed to get CPL3 stack for CPU {}: {:?}", cpu_index, e); + return; + } + }; + + // SAFETY: We are transitioning from the supervisor (CPL0) to the user module (CPL3) for the first time. + // The entry point and stack have been validated and set up during initialization, and the user module is + // will be responsible for validating any further inputs. + let ret = unsafe { + invoke_demoted_routine( + cpu_index, + user_entry, + cpl3_stack, + 3, + UserCommandType::StartUserCore as u64, + hob_list, + 0, + ) + }; + log::info!("Returned from user entry point with value: 0x{:016x}", ret); + + // Mark BSP init as complete so APs can proceed + self.initialized.store(true, Ordering::Release); + BSP_INIT_COMPLETE.store(true, Ordering::Release); + + log::info!("BSP one-time initialization complete."); + } else { + // AP path: Wait for BSP to complete one-time initialization + log::trace!("AP (CPU {}, index {}) waiting for BSP initialization...", cpu_id, cpu_index); + + // Spin until BSP completes initialization + while !BSP_INIT_COMPLETE.load(Ordering::Acquire) { + core::hint::spin_loop(); + } + + // Register this AP with the CPU manager + self.cpu_manager.register_cpu(cpu_id, false); + } + + // All cores perform per-core initialization + self.per_core_init(cpu_id, is_bsp); + + // Mark this core as initialized in the per-core buffer + mark_core_initialized(cpu_index); + + // Track that this core has completed per-core init + let init_count = PER_CORE_INIT_COUNT.fetch_add(1, Ordering::SeqCst) + 1; + log::trace!("CPU {} (index {}) completed per-core init ({} cores initialized)", cpu_id, cpu_index, init_count); + + // BSP waits for all registered CPUs to complete per-core init before returning + if is_bsp { + let expected_cpus = self.cpu_manager.registered_count(); + while PER_CORE_INIT_COUNT.load(Ordering::Acquire) < expected_cpus as u32 { + core::hint::spin_loop(); + } + + log::info!("All {} cores completed initialization, returning to caller.", expected_cpus); + } + + // First entry returns to caller after init is complete + // (Each core has already marked itself as initialized via mark_core_initialized) + } + + /// BSP-specific initialization. + /// + /// This is called only on the BSP after basic setup is complete. + fn bsp_init(&'static self, hob_list: *const c_void) { + log::info!("BSP performing one-time initialization..."); + + let mut interrupt_manager = Interrupts::new(); + interrupt_manager.initialize().unwrap_or_else(|err| { + panic!("Failed to initialize Interrupt Manager: {:?}", err); + }); + + // Initialize the page allocator from the HOB list + // This finds all SMRAM regions and sets up memory tracking + // SAFETY: hob_list is provided by the MM IPL and is guaranteed to be valid + unsafe { + if let Err(e) = mm_mem::PAGE_ALLOCATOR.init_from_hob_list(hob_list) { + log::error!("Failed to initialize page allocator: {:?}", e); + } + } + + // Reserve pages from the page allocator for paging structures. + // This is done before paging is initialized to avoid circular dependency. + unsafe { + match mm_mem::PAGE_ALLOCATOR.allocate_pages(paging_allocator::DEFAULT_PAGING_POOL_PAGES) { + Ok(paging_pool_base) => { + log::info!( + "Reserved {} pages at 0x{:016x} for paging structures", + paging_allocator::DEFAULT_PAGING_POOL_PAGES, + paging_pool_base + ); + // Initialize the paging allocator with the reserved pool + if let Err(e) = paging_allocator::PAGING_ALLOCATOR + .init(paging_pool_base, paging_allocator::DEFAULT_PAGING_POOL_PAGES) + { + log::error!("Failed to initialize paging allocator: {:?}", e); + } + } + Err(e) => { + log::error!("Failed to reserve pages for paging structures: {:?}", e); + } + } + } + + // Initialize the global page table from the active CR3. + // This allows the supervisor to modify page attributes on newly allocated pages. + let cr3 = read_cr3(); + let paging_alloc = paging_allocator::SharedPagingAllocator::new(&paging_allocator::PAGING_ALLOCATOR); + let page_table = unsafe { X64PageTable::from_existing(cr3, paging_alloc, PagingType::Paging4Level) } + .expect("Failed to create page table from active CR3"); + *PAGE_TABLE.lock() = Some(page_table); + log::info!("Page table initialized from CR3=0x{:016x}", cr3); + + // Discover the MM Supervisor User module entry point from the HOB list. + // We look for EFI_HOB_TYPE_MEMORY_ALLOCATION HOBs that have: + // - MemoryAllocationHeader.Name == gMmSupervisorHobMemoryAllocModuleGuid + // - ModuleName == gMmSupervisorUserGuid + // SAFETY: hob_list is provided by the MM IPL and is guaranteed to be valid + let user_entry_point = unsafe { self.discover_user_module_entry(hob_list) }; + if let Some(entry) = user_entry_point { + log::info!("Discovered MM User module entry point: 0x{:016x}", entry); + // Store entry point in static for use during request processing + USER_ENTRY_POINT.call_once(|| entry); + } else { + log::warn!("MM User module entry point not found in HOB list"); + } + + // Initialize the policy gate from the PassDown HOB. + // This discovers the firmware policy buffer and initializes the policy gate. + // SAFETY: hob_list is provided by the MM IPL and is guaranteed to be valid + unsafe { + if let Err(e) = self.init_policy_from_hob_list(hob_list) { + log::error!("Failed to initialize policy gate: {:?}", e); + } + } + + // Okay, we have all the hob content used, will map this to user level and don't need to keep + // the HOB list pointer anymore. + // + // Walk the HOB list to compute its total size (the list may not start + // with a PHIT, so we cannot use `end_of_hob_list`). Each HOB has a + // generic header with `{ type: u16, length: u16, ... }`. We advance + // by `length` until we hit `END_OF_HOB_LIST`, then include that + // terminal header in the total size — mirroring the C `GetHobListSize`. + // + // After computing the size, page-align the range and remap as + // user-accessible + read-only + non-executable so the demoted user + // core can walk the HOBs during `StartUserCore`. + let hob_base = hob_list as u64; + let hob_list_size = { + let mut cursor = hob_list as *const u8; + loop { + // SAFETY: cursor is within the HOB list buffer; the header is at + // least `size_of::()` bytes (8). + let hdr = unsafe { &*(cursor as *const hob::header::Hob) }; + if hdr.r#type == hob::END_OF_HOB_LIST { + // Include the END_OF_HOB_LIST header itself. + break (cursor as u64 - hob_base) + core::mem::size_of::() as u64; + } + if hdr.length == 0 { + log::error!("HOB with zero length at 0x{:016x}, aborting walk", cursor as u64); + break cursor as u64 - hob_base; + } + cursor = unsafe { cursor.add(hdr.length as usize) }; + } + }; + + let page_mask: u64 = (UEFI_PAGE_SIZE as u64) - 1; + let aligned_base = hob_base & !page_mask; + let aligned_end = (hob_base + hob_list_size + page_mask) & !page_mask; + let hob_region_size = aligned_end.saturating_sub(aligned_base); + log::info!( + "HOB list at 0x{:016x} size 0x{:x}, aligned region 0x{:016x}-0x{:016x} (0x{:x} bytes)", + hob_base, + hob_list_size, + aligned_base, + aligned_end, + hob_region_size + ); + if hob_region_size > 0 { + let attrs = MemoryAttributes::ReadOnly | MemoryAttributes::ExecuteProtect; + let mut pt_guard = PAGE_TABLE.lock(); + if let Some(ref mut pt) = *pt_guard { + if let Err(e) = pt.map_memory_region(aligned_base, hob_region_size, attrs) { + log::error!( + "Failed to remap HOB list to user level at 0x{:016x} (0x{:x} bytes): {:?}", + aligned_base, + hob_region_size, + e + ); + panic!("Cannot continue without user access to HOB list"); + } else { + log::info!("Remapped HOB list 0x{:016x}-0x{:016x} as user read-only", aligned_base, aligned_end); + } + } else { + log::error!("Page table not initialized, cannot remap HOB list to user level"); + panic!("Cannot continue without user access to HOB list"); + } + } + + log::trace!("BSP one-time initialization complete."); + } + + /// Patches the SMI handler's IDT descriptor to point to Rust interrupt handlers. + /// + /// Navigates the per-core MMI entry fixup structure embedded at the end of the + /// SMI handler binary to locate the `gSmiHandlerIdtr` pointer, then overwrites + /// it with the current IDT descriptor (base + limit). + /// + /// # Arguments + /// + /// * `mmi_entry_size` - Size of the MMI entry binary from the PassDown HOB. + fn patch_smi_handler_idt(mmbase: u64, mmi_entry_size: u64) { + if mmi_entry_size == 0 { + log::warn!("MMI entry size is 0 in PassDown HOB, cannot navigate fixup structure"); + return; + } + + // SAFETY: Reading SMBASE MSR is safe during BSP init in SMM context. + let mut smbase = unsafe { cpu::read_msr(cpu::IA32_MSR_SMBASE) }.unwrap_or_else(|err| { + panic!("Failed to read IA32_MSR_SMBASE: {:?}", err); + }); + + if smbase == 0 { + smbase = mmbase; + } + + let mmi_entry_base = smbase + SMM_HANDLER_OFFSET; + log::info!("MMI entry at 0x{:016x} with size 0x{:x}", mmi_entry_base, mmi_entry_size); + + // The last u32 in the MMI entry binary is the total fixup structure size. + let whole_struct_size_addr = mmi_entry_base + mmi_entry_size - 4; + // SAFETY: whole_struct_size_addr points into the SMI handler template in SMRAM. + let whole_struct_size = unsafe { core::ptr::read_unaligned(whole_struct_size_addr as *const u32) }; + + // The structure header starts before the trailing size field. + let hdr_addr = + (mmi_entry_base + mmi_entry_size - 4 - whole_struct_size as u64) as *const PerCoreMmiEntryStructHdr; + // SAFETY: hdr_addr points to the packed fixup header within the SMI handler binary. + let hdr = unsafe { core::ptr::read_unaligned(hdr_addr) }; + + let hdr_version = hdr.header_version; + let f64_offset = hdr.fixup64_offset; + let f64_num = hdr.fixup64_num; + log::trace!( + "Fixup header at 0x{:016x}: version={}, fixup64_offset={}, fixup64_num={}", + hdr_addr as u64, + hdr_version, + f64_offset, + f64_num + ); + + // Validate the Fixup64 array has the IDTR entry. + if (FIXUP64_SMI_HANDLER_IDTR as u8) >= f64_num { + log::error!( + "Fixup64 array too small: need index {} but only {} entries", + FIXUP64_SMI_HANDLER_IDTR, + f64_num + ); + return; + } + + // Navigate to the Fixup64 array and read the IDTR entry. + let fixup64_base = (hdr_addr as u64 + f64_offset as u64) as *const u64; + // SAFETY: fixup64_base + index is within the fixup array in the SMI handler binary. + let idt_desc_addr = unsafe { core::ptr::read_unaligned(fixup64_base.add(FIXUP64_SMI_HANDLER_IDTR)) }; + + if idt_desc_addr == 0 { + log::warn!("Fixup64[{}] (SMI_HANDLER_IDTR) is null", FIXUP64_SMI_HANDLER_IDTR); + return; + } + + // Overwrite the IA32_DESCRIPTOR with our Rust IDT's base/limit. + let idt_desc_ptr = idt_desc_addr as *mut DescriptorTablePointer; + let idtr = read_idtr(); + + // SAFETY: idt_desc_ptr points to an IA32_DESCRIPTOR allocated by the C relocation + // code via AllocateCodePages(1). Both DescriptorTablePointer (packed(2)) and + // IA32_DESCRIPTOR (packed(1)) have the same 10-byte {u16, u64} layout. + unsafe { core::ptr::write_unaligned(idt_desc_ptr, idtr) }; + + log::info!( + "Patched SMI handler IDT descriptor at 0x{:016x}: base=0x{:016x}, limit=0x{:04x}", + idt_desc_addr, + idtr.base.as_u64(), + idtr.limit + ); + } + + /// Per-core initialization. + /// + /// This is called on every core (BSP and APs) during the first entry. + /// Use this for setting up per-CPU state like syscall MSRs, GS base, etc. + fn per_core_init(&'static self, cpu_id: u32, is_bsp: bool) { + let core_type = if is_bsp { "BSP" } else { "AP" }; + log::trace!("{} (CPU {}) performing per-core initialization...", core_type, cpu_id); + + // TODO: Set up per-CPU GDT/TSS if needed + // TODO: Set up per-CPU interrupt stacks + // TODO: Initialize per-CPU data structures + + log::trace!("{} (CPU {}) per-core initialization complete.", core_type, cpu_id); + } + + /// Enter runtime mode (called on subsequent entries after init is complete). + /// + /// Implements the MP synchronization protocol: + /// 1. APs check in by setting their state to `InHoldingPen` and entering the holding pen + /// 2. BSP waits (with timeout) for all registered APs to arrive + /// 3. BSP processes the pending request via `bsp_request_loop` + /// 4. BSP broadcasts `Return` to all APs so they exit the holding pen + /// 5. BSP waits for all AP responses before returning + fn enter_runtime(&'static self, cpu_id: u32) { + let is_bsp = cpu::is_bsp(); + + if is_bsp { + log::trace!("BSP (CPU {}) waiting for APs to arrive...", cpu_id); + + // Wait for all registered APs to check in (set state to InHoldingPen) + let expected_aps = self.cpu_manager.registered_count().saturating_sub(1) as usize; + self.wait_for_ap_arrival(expected_aps); + + // All APs (or timeout) - proceed with request processing + log::trace!("BSP (CPU {}) entering request serving routine...", cpu_id); + self.bsp_request_loop(cpu_id as usize); + + // BSP is done handling the request - broadcast Return to all APs + log::trace!("BSP (CPU {}) broadcasting Return to all APs...", cpu_id); + let sent = self.mailbox_manager.broadcast_command(ApCommand::Return); + log::trace!("BSP (CPU {}) sent Return to {} APs, waiting for acknowledgement...", cpu_id, sent); + + // TODO: Wait for all APs to acknowledge the Return command + const RETURN_TIMEOUT_US: u64 = 100_000; // 100 ms + let responded = self.mailbox_manager.wait_all_responses(RETURN_TIMEOUT_US); + log::trace!("BSP (CPU {}) done: {}/{} APs acknowledged Return", cpu_id, responded, sent); + } else { + // AP: check in by marking state, then enter holding pen + self.cpu_manager.set_ap_state(cpu_id, cpu::ApState::InHoldingPen); + log::trace!("AP (CPU {}) checked in, entering holding pen...", cpu_id); + self.ap_holding_pen(cpu_id); + } + } + + /// Waits for APs to arrive with a timeout. + /// + /// Spins until the expected number of APs have set their state to `InHoldingPen`, + /// or the timeout expires (whichever comes first). + fn wait_for_ap_arrival(&self, expected_aps: usize) { + if expected_aps == 0 { + return; + } + + const AP_ARRIVAL_TIMEOUT_US: u64 = 100_000; // 100 ms + + let all_arrived = perf_timer::spin_until::(AP_ARRIVAL_TIMEOUT_US, || { + self.cpu_manager.count_aps_in_state(cpu::ApState::InHoldingPen) >= expected_aps + }); + + if all_arrived { + log::trace!("All {} APs arrived", expected_aps); + } else { + let arrived = self.cpu_manager.count_aps_in_state(cpu::ApState::InHoldingPen); + log::warn!("AP arrival timeout: {}/{} APs arrived, proceeding with available cores", arrived, expected_aps); + } + } + + /// Discovers the MM Supervisor User module entry point from the HOB list. + /// + /// This function iterates through the HOB list looking for `MemoryAllocationModule` HOBs + /// that match the MM Supervisor memory allocation module GUID and have the MM Supervisor + /// User GUID as their module name. + /// + /// # Safety + /// + /// The caller must ensure that `hob_list` points to a valid HOB list. + /// + /// # Returns + /// + /// The entry point address of the user module if found, or `None` otherwise. + unsafe fn discover_user_module_entry(&self, hob_list: *const c_void) -> Option { + if hob_list.is_null() { + return None; + } + + // Get the HOB list header + let hob_list_info = unsafe { (hob_list as *const PhaseHandoffInformationTable).as_ref()? }; + + let hob = Hob::Handoff(hob_list_info); + + // Iterate through the HOB list looking for MemoryAllocationModule HOBs + for current_hob in &hob { + if let Hob::MemoryAllocationModule(mem_alloc_mod) = current_hob { + // Check if this is an MM Supervisor module allocation + // (MemoryAllocationHeader.Name == gMmSupervisorHobMemoryAllocModuleGuid) + if mem_alloc_mod.alloc_descriptor.name == MM_SUPERVISOR_HOB_MEMORY_ALLOC_MODULE_GUID { + log::debug!( + "Found MM Supervisor module HOB: module_name={:?}, entry_point=0x{:016x}", + mem_alloc_mod.module_name, + mem_alloc_mod.entry_point + ); + + // Check if this is the User module (ModuleName == gMmSupervisorUserGuid) + if mem_alloc_mod.module_name == MM_SUPERVISOR_USER_GUID { + log::info!( + "Found MM User module: entry_point=0x{:016x}, base=0x{:016x}, size=0x{:x}", + mem_alloc_mod.entry_point, + mem_alloc_mod.alloc_descriptor.memory_base_address, + mem_alloc_mod.alloc_descriptor.memory_length + ); + return Some(mem_alloc_mod.entry_point); + } + } + } + } + + None + } + + /// Initializes the policy gate from the PassDown HOB. + /// + /// This function iterates through the HOB list looking for the PassDown HOB, + /// extracts the firmware policy buffer pointer, and initializes the policy gate. + /// + /// # Safety + /// + /// The caller must ensure that `hob_list` points to a valid HOB list. + /// + /// # Returns + /// + /// `Ok(())` if the policy gate was successfully initialized, or an error otherwise. + /// TODO: Remove the passdown hob eventually!!!!! + unsafe fn init_policy_from_hob_list(&self, hob_list: *const c_void) -> Result<(), PolicyInitError> { + if hob_list.is_null() { + return Err(PolicyInitError::NullHobList); + } + + // Get the HOB list header + let hob_list_info = + unsafe { (hob_list as *const PhaseHandoffInformationTable).as_ref().ok_or(PolicyInitError::NullHobList)? }; + + let mut supv_comm_buffer = 0 as u64; + let mut supv_comm_buffer_size = 0 as u64; + let mut supv_comm_buffer_internal = 0 as u64; + let mut user_comm_buffer = 0 as u64; + let mut user_comm_buffer_size = 0 as u64; + let mut user_comm_buffer_internal = 0 as u64; + let mut status_buffer = 0 as u64; + + let hob = Hob::Handoff(hob_list_info); + + // Walk through HOBs to find the PassDown HOB + for current_hob in &hob { + if let Hob::GuidHob(guid_hob, data) = current_hob { + if guid_hob.name == MM_SUPV_PASS_DOWN_HOB_GUID { + log::info!("Found MM Supervisor PassDown HOB"); + + // Verify data size + if data.len() < core::mem::size_of::() { + log::error!( + "PassDown HOB data too small: {} < {}", + data.len(), + core::mem::size_of::() + ); + return Err(PolicyInitError::InvalidPolicyData); + } + + // Cast to PassDown HOB data structure + let pass_down = unsafe { &*(data.as_ptr() as *const MmSupvPassDownHobData) }; + + // Copy packed struct fields to local variables to avoid unaligned access + // SAFETY: Direct access to read the addresses from the hob data. + let revision = unsafe { core::ptr::addr_of!(pass_down.revision).read() }; + let mm_initialized_buffer = unsafe { core::ptr::addr_of!(pass_down.mm_initialized_buffer).read() }; + let firmware_policy_buffer = + unsafe { core::ptr::addr_of!(pass_down.mm_supv_firmware_policy_buffer).read() }; + let firmware_policy_buffer_size = + unsafe { core::ptr::addr_of!(pass_down.mm_supv_firmware_policy_buffer_size).read() }; + + // Extract communication buffer pointers + let cpl3_stack_buffer = + unsafe { core::ptr::addr_of!(pass_down.mm_supervisor_cpl3_stack_base).read() }; + let cpl3_stack_buffer_size = + unsafe { core::ptr::addr_of!(pass_down.mm_supervisor_cpl3_per_core_stack_size).read() }; + let mmi_entry_size = unsafe { core::ptr::addr_of!(pass_down.mmi_entrypoint_size).read() }; + let mmbase = unsafe { core::ptr::addr_of!(pass_down.bsp_mm_base_address).read() }; + + // Patch the SMI entry IDT descriptor to point to our interrupt handlers. + // The C relocation code patches each CPU's SMI handler template with fixup + // arrays. The Fixup64 array at index FIXUP64_SMI_HANDLER_IDTR contains the + // address of an IA32_DESCRIPTOR (gSmiHandlerIdtr). On SMI entry, the assembly + // loads that address and does `lidt [rax]`. We navigate the fixup structure + // in the BSP's SMI handler to find this pointer, then overwrite the descriptor + // with our Rust IDT's base/limit. + Self::patch_smi_handler_idt(mmbase, mmi_entry_size); + + // Extract CPU private data pointer + let cpu_private = unsafe { core::ptr::addr_of!(pass_down.mm_supv_cpu_private).read() }; + + // Validate revision + if revision != MM_SUPV_PASS_DOWN_HOB_REVISION { + log::error!( + "Invalid PassDown HOB revision: {} (expected {})", + revision, + MM_SUPV_PASS_DOWN_HOB_REVISION + ); + return Err(PolicyInitError::InvalidRevision { + found: revision, + expected: MM_SUPV_PASS_DOWN_HOB_REVISION, + }); + } + + // Store the per-core initialized buffer address for use by all cores + if mm_initialized_buffer != 0 { + MM_INITIALIZED_BUFFER.call_once(|| mm_initialized_buffer); + log::info!("MM Initialized buffer set to 0x{:016x}", mm_initialized_buffer); + } else { + log::warn!("MM Initialized buffer is null in PassDown HOB"); + } + + // Store CPU private data pointer for SmmCoreEntryContext access + if cpu_private != 0 { + SMM_CPU_PRIVATE.call_once(|| cpu_private); + log::info!("SMM CPU Private data at 0x{:016x}", cpu_private); + } else { + log::warn!("SMM CPU Private data pointer is null in PassDown HOB"); + } + + log::info!( + "PassDown HOB: FirmwarePolicyBuffer=0x{:x}, Size=0x{:x}", + firmware_policy_buffer, + firmware_policy_buffer_size + ); + + // Validate firmware policy buffer + if firmware_policy_buffer == 0 || firmware_policy_buffer_size == 0 { + log::error!("Firmware policy buffer is null or empty"); + return Err(PolicyInitError::NullFirmwarePolicyBuffer); + } + + // Initialize the policy gate with the firmware policy buffer + let policy_ptr = firmware_policy_buffer as *const u8; + // allocate one page for the memory policy buffer which will be filled in by walk_page_table + let memory_policy_buffer = mm_mem::PAGE_ALLOCATOR.allocate_pages(1).map_err(|e| { + log::error!("Failed to allocate page for memory policy buffer: {:?}", e); + PolicyInitError::MemoryAllocationFailed + })?; + // SAFETY: We validated that policy_ptr is non-null above and comes from + // the PassDown HOB which is set up by the MM IPL. + match unsafe { patina_mm_policy::PolicyGate::new(policy_ptr) } { + Ok(mut gate) => { + log::info!("Policy gate initialized successfully"); + // SAFETY: policy_ptr points to valid policy data as validated above. + unsafe { patina_mm_policy::dump_policy(policy_ptr) }; + + // Configure the memory policy buffer on the gate so that + // take_snapshot / verify_snapshot / fetch_n_update_policy + // can use it. + let mem_policy_max_count = + UEFI_PAGE_SIZE as usize / core::mem::size_of::(); + gate.set_memory_policy_buffer( + memory_policy_buffer as *mut MemDescriptorV1_0, + mem_policy_max_count, + ); + + // Store the initialized policy gate in the static variable for global access + POLICY_GATE.call_once(|| gate); + } + Err(e) => { + log::error!("Failed to create policy gate: {:?}", e); + return Err(PolicyInitError::InvalidPolicyData); + } + } + + // Init syscall interface + self.syscall_interface + .init( + self.cpu_manager.max_cpus(), + cpl3_stack_buffer, + cpl3_stack_buffer_size + .try_into() + .unwrap_or_else(|err| panic!("Invalid CPL3 stack buffer size: {:?}", err)), + ) + .unwrap_or_else(|err| { + panic!("Failed to initialize syscall interface: {:?}", err); + }); + + // Read CR3 from hardware + let cr3: u64 = read_cr3(); + + // Walk page table and generate memory policy + let count = unsafe { + walk_page_table( + cr3, + memory_policy_buffer as *mut MemDescriptorV1_0, + UEFI_PAGE_SIZE as usize, + |base, size| is_buffer_inside_mmram(base, size), // Your MMRAM check + ) + }; + + if let Ok(descriptor_count) = count { + log::info!("Successfully generated {} memory policy descriptors", descriptor_count); + + // Initialize the unblocked memory tracker from the generated descriptors + // SAFETY: memory_policy_buffer points to valid MemDescriptorV1_0 array + // with descriptor_count entries, as we just filled it via walk_page_table + if let Err(e) = unsafe { + unblock_memory::UNBLOCKED_MEMORY_TRACKER + .init_from_buffer(memory_policy_buffer as *const MemDescriptorV1_0, descriptor_count) + } { + log::error!("Failed to initialize unblocked memory tracker: {:?}", e); + } else { + log::info!("Unblocked memory tracker initialized"); + // Dump regions for debugging + unblock_memory::UNBLOCKED_MEMORY_TRACKER.dump_regions(); + } + } else { + log::error!("Failed to generate memory policy descriptors: {:?}", count.err()); + } + + log::info!("Generated {} memory policy descriptors", count.unwrap_or(0)); + } else if guid_hob.name == MM_COMMON_REGION_HOB_GUID { + // This is a hob describing the supervisor communication region (user goes through a different one now) + log::info!("Found MM Common Region HOB"); + + // Cast to comm buffer HOB data structure + let supv_buffer_hob = unsafe { &*(data.as_ptr() as *const MmCommonRegionHobData) }; + supv_comm_buffer = unsafe { core::ptr::addr_of!(supv_buffer_hob.addr).read() }; + + let supv_comm_buffer_pages = unsafe { core::ptr::addr_of!(supv_buffer_hob.number_of_pages).read() }; + // safe multiplication with checked arithmetic to prevent overflow + supv_comm_buffer_size = + supv_comm_buffer_pages.checked_mul(UEFI_PAGE_SIZE as u64).unwrap_or_else(|| { + panic!( + "Invalid supervisor common buffer size: {} pages * {} page size overflows", + supv_comm_buffer_pages, UEFI_PAGE_SIZE + ); + }); + + // Check to see if this region is outside of MMRAM and has the supervisor/read/write attribute + if !is_buffer_inside_mmram(supv_comm_buffer, supv_comm_buffer_size) { + match query_address_ownership(supv_comm_buffer, supv_comm_buffer_size) { + Some(PageOwnership::User) => { + panic!( + "Supervisor common buffer at 0x{:016x}-0x{:016x} is not marked as supervisor-owned", + supv_comm_buffer, + supv_comm_buffer + supv_comm_buffer_size + ); + } + Some(PageOwnership::Supervisor) => { + // Do nothing + } + None => { + panic!( + "Failed to query page ownership for supervisor common buffer at 0x{:016x}", + supv_comm_buffer + ); + } + }; + } + + // All checked out, make a copy of this supervisor to be used when handling incoming requests + supv_comm_buffer_internal = mm_mem::PAGE_ALLOCATOR + .allocate_pages_with_type(supv_comm_buffer_pages as usize, mm_mem::AllocationType::Supervisor) + .map_err(|e| { + log::error!("Failed to allocate internal supervisor common buffer: {:?}", e); + PolicyInitError::MemoryAllocationFailed + })?; + } else if guid_hob.name == MM_COMM_BUFFER_HOB_GUID { + // This is a hob describing the communication buffer + log::info!("Found MM Communication Buffer HOB"); + + // Cast to comm buffer HOB data structure + let comm_buffer_hob = data.as_ptr() as *mut MmCommonBufferHobData; + user_comm_buffer = unsafe { core::ptr::addr_of!((*comm_buffer_hob).physical_start).read() }; + + let user_comm_buffer_pages = + unsafe { core::ptr::addr_of!((*comm_buffer_hob).number_of_pages).read() }; + // safe multiplication with checked arithmetic to prevent overflow + user_comm_buffer_size = + user_comm_buffer_pages.checked_mul(UEFI_PAGE_SIZE as u64).unwrap_or_else(|| { + panic!( + "Invalid user common buffer size: {} pages * {} page size overflows", + user_comm_buffer_pages, UEFI_PAGE_SIZE + ); + }); + + // Check to see if this region is outside of MMRAM and has the supervisor/read/write attribute + if !is_buffer_inside_mmram(user_comm_buffer, user_comm_buffer_size) { + match query_address_ownership(user_comm_buffer, user_comm_buffer_size) { + Some(PageOwnership::User) => { + panic!( + "User common buffer at 0x{:016x}-0x{:016x} is not marked as user-owned", + user_comm_buffer, + user_comm_buffer + user_comm_buffer_size + ); + } + Some(PageOwnership::Supervisor) => { + // Do nothing + } + None => { + panic!( + "Failed to query page ownership for user common buffer at 0x{:016x}", + user_comm_buffer + ); + } + }; + } + + status_buffer = unsafe { core::ptr::addr_of!((*comm_buffer_hob).status_buffer).read() }; + + // Validate that the status buffer is also within the supervisor common buffer region (so that the user do not have direct access) + if !is_buffer_inside_mmram(status_buffer, core::mem::size_of::() as u64) { + match query_address_ownership(status_buffer, core::mem::size_of::() as u64) + { + Some(PageOwnership::User) => { + panic!( + "User common buffer at 0x{:016x}-0x{:016x} is not marked as supervisor-exposed", + user_comm_buffer, + user_comm_buffer + user_comm_buffer_size + ); + } + Some(PageOwnership::Supervisor) => { + // Do nothing + } + None => { + panic!("Failed to query page ownership for status buffer at 0x{:016x}", status_buffer); + } + }; + } + + // All checked out, make a copy of this buffer to be used when handling incoming requests + user_comm_buffer_internal = mm_mem::PAGE_ALLOCATOR + .allocate_pages_with_type(user_comm_buffer_pages as usize, mm_mem::AllocationType::User) + .map_err(|e| { + log::error!("Failed to allocate internal user common buffer: {:?}", e); + PolicyInitError::MemoryAllocationFailed + })?; + + // TODO: HACKHACK: this updates the hob passed to user module with the internal buffer address, which is a bit gross but it works for now. + // SAFETY: We have exclusive access to the HOB data structure at this point during initialization, and we're just updating the physical_start field to point to our internal buffer copy. + unsafe { + // Disable page protection to allow writing to the HOB data structure if needed + let original_cr0 = disable_write_protection(); + + core::ptr::write_volatile( + core::ptr::addr_of_mut!((*comm_buffer_hob).physical_start), + user_comm_buffer_internal, + ); + + // Restore original CR0 value to re-enable page protection + enable_write_protection(original_cr0); + } + } + } + } + + // allocate one page for the buffer that the supervisor will use to send messages to the user module + let supv_to_user_buffer = + mm_mem::PAGE_ALLOCATOR.allocate_pages_with_type(1, mm_mem::AllocationType::User).map_err(|e| { + log::error!("Failed to allocate page for supervisor-to-user buffer: {:?}", e); + PolicyInitError::MemoryAllocationFailed + })?; + + // At this point, none of the following buffers may be zero. + if supv_comm_buffer == 0 || user_comm_buffer == 0 || status_buffer == 0 || supv_to_user_buffer == 0 { + log::error!("One or more communication buffers are not properly initialized"); + return Err(PolicyInitError::MissingCommunicationBuffer); + } + + // Store communication buffer configuration. + COMM_BUFFER_CONFIG.call_once(|| CommBufferConfig { + supv_comm_buffer, + supv_comm_buffer_internal, + supv_comm_buffer_size, + user_comm_buffer, + user_comm_buffer_internal, + user_comm_buffer_size, + status_buffer, + supv_to_user_buffer, + supv_to_user_buffer_size: UEFI_PAGE_SIZE as u64, + }); + log::info!( + "Comm buffers: supv=0x{:x}/0x{:x} size=0x{:x}, user=0x{:x}/0x{:x} size=0x{:x}, status=0x{:x}", + supv_comm_buffer, + supv_comm_buffer_internal, + supv_comm_buffer_size, + user_comm_buffer, + user_comm_buffer_internal, + user_comm_buffer_size, + status_buffer + ); + + Ok(()) + } + + /// The main request serving loop for the BSP. + /// It manages other CPUs and processes pending requests from the communication buffer. + /// + /// This function reads the MM_COMM_BUFFER_STATUS structure to determine if there's a pending request + /// and whether it targets the Supervisor or User module. + /// + /// - If targeting User: copies user comm buffer to internal, then demotes to user entry point + /// - If targeting Supervisor: dispatches to the request dispatcher + fn bsp_request_loop(&self, cpu_index: usize) { + // Get communication buffer configuration + let config = match COMM_BUFFER_CONFIG.get() { + Some(c) => c, + None => { + // Not yet initialized, nothing to process + return; + } + }; + + // Check status buffer for pending request + if config.status_buffer == 0 { + return; + } + + // Read the MM_COMM_BUFFER_STATUS structure + // SAFETY: status_buffer is provided by MM IPL and is guaranteed valid + let status = unsafe { core::ptr::read_volatile(config.status_buffer as *const MmCommBufferStatus) }; + let target = RequestTarget::from(&status); + + log::trace!( + "Processing request: valid={}, talk_to_supervisor={}, target={:?}", + status.is_comm_buffer_valid, + status.talk_to_supervisor, + target + ); + + match target { + RequestTarget::None => { + // No pending request + } + RequestTarget::User => { + // Request targets the User module + self.process_user_request(config, &status, cpu_index); + } + RequestTarget::Supervisor => { + // Request targets the Supervisor + self.process_supervisor_request(config, &status, cpu_index); + } + } + } + + /// Process a request targeting the User module. + /// + /// This function implements the user-mode MMI dispatch pathway: + /// 1. Builds a fresh `EfiMmEntryContext` with the current CPU index and CPU count + /// 2. Copies the `EfiMmEntryContext` into the supervisor-to-user data buffer + /// 3. Appends the `MmCommBufferStatus` immediately after the context + /// 4. For synchronous MMIs, copies the user comm buffer to the internal copy + /// 5. Demotes to the user entry point via `invoke_demoted_routine` + /// 6. On return, copies back the user comm buffer and reads the updated status + fn process_user_request(&self, config: &CommBufferConfig, status: &MmCommBufferStatus, cpu_index: usize) { + log::trace!("Processing User request..."); + + // Validate buffers + if config.user_comm_buffer == 0 || config.user_comm_buffer_internal == 0 { + log::error!("User communication buffer not configured"); + return; + } + + if config.supv_to_user_buffer == 0 { + log::error!("Supervisor-to-user data buffer not configured"); + return; + } + + // Get user entry point + let user_entry = match USER_ENTRY_POINT.get() { + Some(&entry) if entry != 0 => entry, + _ => { + log::error!("User entry point not configured, cannot demote"); + return; + } + }; + + // Demote to user entry point to process the request + let cpl3_stack = match self.syscall_interface.get_cpl3_stack(cpu_index) { + Ok(stack) => stack, + Err(e) => { + log::error!("Failed to get CPL3 stack for CPU {}: {:?}", cpu_index, e); + return; + } + }; + + // Build a fresh EfiMmEntryContext with only the fields the user actually needs. + // The legacy C structure carried pointers (mm_startup_this_ap, cpu_save_state, + // cpu_save_state_size) that are meaningless in the Rust supervisor model — the + // user module accesses those services through syscalls instead. + let entry_context = EfiMmEntryContext { + mm_startup_this_ap: 0, + currently_executing_cpu: cpu_index as u64, + number_of_cpus: self.cpu_manager.registered_count() as u64, + cpu_save_state_size: 0, + cpu_save_state: 0, + }; + + // Copy the EfiMmEntryContext + MmCommBufferStatus into the supervisor-to-user + // data buffer so the user can read processor information after demotion. + let context_size = core::mem::size_of::(); + let status_size = core::mem::size_of::(); + + // Validate the supervisor-to-user buffer is large enough for context + status + if (config.supv_to_user_buffer_size as usize) < context_size + status_size { + log::error!( + "Supervisor-to-user buffer too small: {} < {} (context) + {} (status)", + config.supv_to_user_buffer_size, + context_size, + status_size + ); + return; + } + + // SAFETY: supv_to_user_buffer is valid and large enough, verified above. + unsafe { + // First disable SMAP to allow the supervisor to write to the user buffer + disable_smap(); + // Copy the EfiMmEntryContext to the start of the supervisor-to-user buffer + core::ptr::copy_nonoverlapping( + &entry_context as *const EfiMmEntryContext as *const u8, + config.supv_to_user_buffer as *mut u8, + context_size, + ); + + // Copy the MmCommBufferStatus right after the context + core::ptr::copy_nonoverlapping( + status as *const MmCommBufferStatus as *const u8, + (config.supv_to_user_buffer as *mut u8).add(context_size), + status_size, + ); + // Re-enable SMAP after writing to the user buffer + enable_smap(); + } + + // Determine whether this is synchronous or asynchronous request + let sync_mmi = status.is_comm_buffer_valid; + + if sync_mmi != 0 { + // Copy user buffer to user internal buffer for processing in Ring 3 + // SAFETY: Buffers are provided by MM IPL and are guaranteed valid + unsafe { + // Disable SMAP to allow copying from the user buffer + disable_smap(); + core::ptr::copy_nonoverlapping( + config.user_comm_buffer as *const u8, + config.user_comm_buffer_internal as *mut u8, + config.user_comm_buffer_size as usize, + ); + // Re-enable SMAP after copying from the user buffer + enable_smap(); + } + log::trace!( + "Copied {} bytes from user buffer 0x{:x} to internal 0x{:x}", + config.user_comm_buffer_size, + config.user_comm_buffer, + config.user_comm_buffer_internal + ); + } + + // Invoke the demoted user entry point with: + // arg1: UserCommandType::UserRequest (command type) + // arg2: supv_to_user_buffer (pointer to EfiMmEntryContext + MmCommBufferStatus) + // arg3: sizeof(EfiMmEntryContext) (size of the context portion) + let ret = unsafe { + invoke_demoted_routine( + cpu_index, + user_entry, + cpl3_stack, + 3, + UserCommandType::UserRequest as u64, + config.supv_to_user_buffer, + context_size as u64, + ) + }; + log::trace!("Returned from user request with value: 0x{}", ret); + + // Copy the response from the internal buffer back to the user buffer + // SAFETY: Buffers are provided by MM IPL and are guaranteed valid + if sync_mmi != 0 { + unsafe { + // Disable SMAP to allow copying back to the user buffer + disable_smap(); + core::ptr::copy_nonoverlapping( + config.user_comm_buffer_internal as *const u8, + config.user_comm_buffer as *mut u8, + config.user_comm_buffer_size as usize, + ); + // Re-enable SMAP after copying back to the user buffer + enable_smap(); + } + } + + // Read the updated MmCommBufferStatus back from the supervisor-to-user buffer + // (the user may have modified return_status and return_buffer_size) + // SAFETY: supv_to_user_buffer is valid and the status is at offset context_size + let returned_status = unsafe { + // Again, disable SMAP to allow reading from the user buffer + disable_smap(); + let status = core::ptr::read( + (config.supv_to_user_buffer as *const u8).add(context_size) as *const MmCommBufferStatus + ); + // Re-enable SMAP after reading from the user buffer + enable_smap(); + status + }; + + // Write the returned status back to the supervisor's status buffer, clearing + // is_comm_buffer_valid to indicate processing is complete + // SAFETY: status_buffer is valid and writable + unsafe { + let status_ptr = config.status_buffer as *mut MmCommBufferStatus; + let mut final_status = returned_status; + final_status.is_comm_buffer_valid = 0; + core::ptr::write_volatile(status_ptr, final_status); + } + } + + /// Process a request targeting the Supervisor. + /// + /// Parses the [`EfiMmCommunicateHeader`] from the supervisor communication buffer, + /// matches the header GUID against the [`SUPERVISOR_MMI_HANDLERS`] distributed slice, + /// and invokes the first matching handler. Handlers are registered at build time, + /// allowing platforms to link in additional handlers without modifying the core. + /// + /// ## Dispatch Flow + /// + /// 1. Zero the internal buffer and copy the external supervisor buffer into it + /// 2. Parse the `EfiMmCommunicateHeader` (GUID + message length) from the internal buffer + /// 3. Validate message length does not exceed the buffer size + /// 4. Iterate [`SUPERVISOR_MMI_HANDLERS`] to find a handler matching the header GUID + /// 5. Call the handler with a pointer to the data payload and mutable size + /// 6. Update the status buffer with return status and total response size + /// 7. Copy the internal buffer back to the external buffer + fn process_supervisor_request(&self, config: &CommBufferConfig, status: &MmCommBufferStatus, cpu_index: usize) { + log::trace!("Processing Supervisor request on CPU {}...", cpu_index); + + // Validate buffers + if config.supv_comm_buffer == 0 || config.supv_comm_buffer_internal == 0 { + log::error!("Supervisor communication buffer not configured"); + return; + } + + let buffer_size = config.supv_comm_buffer_size as usize; + + // Zero the internal buffer then copy the external supervisor buffer into it + // SAFETY: Buffers are provided by MM IPL and are guaranteed valid and non-overlapping + unsafe { + core::ptr::write_bytes(config.supv_comm_buffer_internal as *mut u8, 0, buffer_size); + core::ptr::copy_nonoverlapping( + config.supv_comm_buffer as *const u8, + config.supv_comm_buffer_internal as *mut u8, + buffer_size, + ); + } + + // Parse the EfiMmCommunicateHeader from the internal buffer + if buffer_size < EfiMmCommunicateHeader::size() { + log::error!( + "Supervisor buffer too small for communicate header: {} < {}", + buffer_size, + EfiMmCommunicateHeader::size() + ); + self.write_supv_status(config, status, efi::Status::BAD_BUFFER_SIZE, 0); + return; + } + + // SAFETY: We verified the buffer is large enough for the header. + // The header is packed so we use read_unaligned. + let header = + unsafe { core::ptr::read_unaligned(config.supv_comm_buffer_internal as *const EfiMmCommunicateHeader) }; + + let message_length = header.message_length(); + + // Validate message length doesn't exceed the buffer + if message_length > buffer_size.saturating_sub(EfiMmCommunicateHeader::size()) { + log::error!( + "Message length 0x{:x} exceeds available buffer space 0x{:x}", + message_length, + buffer_size - EfiMmCommunicateHeader::size() + ); + self.write_supv_status(config, status, efi::Status::BAD_BUFFER_SIZE, 0); + return; + } + + // Compute pointer to the data payload (after the header) + let data_ptr = unsafe { (config.supv_comm_buffer_internal as *mut u8).add(EfiMmCommunicateHeader::size()) }; + let mut data_size = message_length; + + // Dispatch: iterate the SUPERVISOR_MMI_HANDLERS distributed slice to find a match + let handler_guid = header.header_guid(); + let mut dispatch_status = efi::Status::NOT_FOUND; + + for handler in SUPERVISOR_MMI_HANDLERS.iter() { + if patina::Guid::from_ref(&handler.handler_guid) == handler_guid { + log::trace!( + "Dispatching supervisor request to handler '{}' (GUID: {:?})", + handler.name, + handler.handler_guid + ); + dispatch_status = (handler.handle)(data_ptr, &mut data_size); + break; + } + } + + if dispatch_status == efi::Status::NOT_FOUND { + log::warn!("No handler found for supervisor request GUID: {:?}", handler_guid); + } + + // Compute the total response size (header + data) for the copy-back + let total_response_size = data_size + EfiMmCommunicateHeader::size(); + + // Copy the (possibly modified) internal buffer back to the external buffer + if total_response_size <= buffer_size { + // SAFETY: Both buffers are valid and total_response_size is within bounds + unsafe { + core::ptr::copy_nonoverlapping( + config.supv_comm_buffer_internal as *const u8, + config.supv_comm_buffer as *mut u8, + total_response_size, + ); + } + } else { + log::error!("Response size 0x{:x} exceeds buffer capacity 0x{:x}", total_response_size, buffer_size); + } + log::trace!( + "Copied {} bytes from internal buffer 0x{:x} back to external 0x{:x}", + total_response_size, + config.supv_comm_buffer_internal, + config.supv_comm_buffer + ); + + // Update the status buffer with return status and response size + let return_status = + if dispatch_status == efi::Status::SUCCESS { efi::Status::SUCCESS } else { efi::Status::NOT_FOUND }; + self.write_supv_status(config, status, return_status, total_response_size as u64); + } + + /// Write the supervisor status buffer after processing a supervisor request. + /// + /// Clears `is_comm_buffer_valid` and `talk_to_supervisor`, sets return status and size. + fn write_supv_status( + &self, + config: &CommBufferConfig, + _status: &MmCommBufferStatus, + return_status: efi::Status, + return_buffer_size: u64, + ) { + // SAFETY: status_buffer is valid and writable, set up by MM IPL + unsafe { + let status_ptr = config.status_buffer as *mut MmCommBufferStatus; + let updated = MmCommBufferStatus { + is_comm_buffer_valid: 0, + talk_to_supervisor: 0, + _padding: [0; 6], + return_status: return_status.as_usize() as u64, + return_buffer_size, + }; + core::ptr::write_volatile(status_ptr, updated); + } + } + + /// The holding pen for APs. + /// + /// APs wait here, polling their mailbox for commands from the BSP. + /// The loop exits when the AP receives a `Return` command. + fn ap_holding_pen(&'static self, cpu_id: u32) { + log::trace!("AP (CPU {}) in holding pen, polling mailbox...", cpu_id); + + loop { + // Check mailbox for commands + if let Some(command) = self.mailbox_manager.check_mailbox(cpu_id) { + log::trace!("AP (CPU {}) received command: {:?}", cpu_id, command); + + // Execute the command + let response = self.execute_ap_command(cpu_id, &command); + + // Post the response + self.mailbox_manager.post_response(cpu_id, response); + + // Break out of the holding pen on Return + if matches!(command, ApCommand::Return) { + log::trace!("AP (CPU {}) exiting holding pen", cpu_id); + break; + } + } + } + } + + /// Execute a command received by an AP. + fn execute_ap_command(&self, cpu_id: u32, command: &ApCommand) -> ApResponse { + match *command { + ApCommand::RunProcedure { procedure, argument } => self.run_procedure_on_ap(cpu_id, procedure, argument), + ApCommand::Return => { + log::trace!("AP (CPU {}) received return command", cpu_id); + ApResponse::Success + } + } + } + + /// Run a procedure on an AP, demoting to user mode if the procedure is in user-owned range. + /// + /// This is the AP-side handler for `ApCommand::RunProcedure`. It mirrors the C + /// `ProcedureWrapper` logic: inspects the procedure pointer ownership and either + /// calls it directly (supervisor-owned) or demotes to Ring 3 (user-owned). + fn run_procedure_on_ap(&self, cpu_id: u32, procedure: u64, argument: u64) -> ApResponse { + log::trace!("AP (CPU {}) running procedure 0x{:x} with arg 0x{:x}", cpu_id, procedure, argument); + + // Determine if the procedure is in user-owned (Ring 3) range by querying the + // page table via the centralized helper. + let is_user_range = match query_address_ownership(procedure, core::mem::size_of::() as u64) { + Some(PageOwnership::User) => true, + Some(PageOwnership::Supervisor) => false, + None => { + log::error!( + "AP (CPU {}) failed to query ownership for 0x{:x} (unmapped or page table not ready)", + cpu_id, + procedure + ); + return ApResponse::Error(efi::Status::DEVICE_ERROR.as_usize() as u32); + } + }; + + if is_user_range { + // Resolve the cpu_index (slot index) for this APIC ID + let cpu_index = match self.cpu_manager.find_cpu_index(cpu_id) { + Some(idx) => idx, + None => { + log::error!("AP (CPU {}) has no registered slot, cannot demote", cpu_id); + return ApResponse::Error(efi::Status::DEVICE_ERROR.as_usize() as u32); + } + }; + + // Get the CPL3 stack for this CPU + let cpl3_stack = match self.syscall_interface.get_cpl3_stack(cpu_index) { + Ok(stack) => stack, + Err(e) => { + log::error!("AP (CPU {}) failed to get CPL3 stack: {:?}", cpu_id, e); + return ApResponse::Error(efi::Status::DEVICE_ERROR.as_usize() as u32); + } + }; + + let user_entry = match USER_ENTRY_POINT.get() { + Some(&entry) if entry != 0 => entry, + _ => { + log::error!("User entry point not configured, cannot demote AP (CPU {})", cpu_id); + return ApResponse::Error(efi::Status::DEVICE_ERROR.as_usize() as u32); + } + }; + + // Demote to user mode and call the procedure + // The procedure signature is: void (EFIAPI *)(void *ProcedureArgument) + log::trace!( + "AP (CPU {}) demoting to user: proc=0x{:x}, stack=0x{:x}, arg=0x{:x}", + cpu_id, + procedure, + cpl3_stack, + argument + ); + + let _ret = unsafe { + invoke_demoted_routine( + cpu_index, + user_entry, + cpl3_stack, + 3, + UserCommandType::UserApProcedure as u64, + procedure, + argument, + ) + }; + + log::trace!("AP (CPU {}) returned from demoted procedure: 0x{:x}", cpu_id, _ret); + ApResponse::Success + } else { + // Supervisor-owned: call directly in Ring 0 + log::trace!("AP (CPU {}) calling supervisor procedure directly at 0x{:x}", cpu_id, procedure); + + // SAFETY: The BSP validated the procedure pointer before dispatching. + // The procedure follows the EFI AP_PROCEDURE calling convention. + type EfiApProcedure = unsafe extern "efiapi" fn(*mut core::ffi::c_void); + let proc_fn: EfiApProcedure = unsafe { core::mem::transmute(procedure) }; + unsafe { proc_fn(argument as *mut core::ffi::c_void) }; + + ApResponse::Success + } + } + + /// Type-erased trampoline for AP startup, called from the syscall dispatcher. + /// + /// This function is monomorphized for the concrete `P: PlatformInfo` type + /// and stored as a `fn(u64, u64, u64) -> u64` in [`AP_STARTUP_FN`]. + /// + /// # Arguments + /// + /// * `cpu_index` - The EFI processor index (slot index) of the target AP + /// * `procedure` - The procedure function pointer to execute on the AP + /// * `argument` - The argument to pass to the procedure + /// + /// # Returns + /// + /// 0 on success, or an EFI status code (as u64) on failure. + fn start_ap_procedure_trampoline(cpu_index: u64, procedure: u64, argument: u64) -> u64 { + let core = Self::instance(); + core.start_ap_procedure(cpu_index, procedure, argument) + } + + /// Validate and dispatch a procedure to a specific AP. + /// + /// Performs validation checks similar to the C `InternalSmmStartupThisAp`: + /// 1. CPU index is within range of registered CPUs + /// 2. CPU at that index is present (registered) + /// 3. CPU is not the BSP + /// 4. Procedure pointer is non-null + /// 5. Sends the command via the mailbox (fails if AP is busy) + /// 6. Waits for the AP to complete (blocking) + fn start_ap_procedure(&self, cpu_index: u64, procedure: u64, argument: u64) -> u64 { + let cpu_index = cpu_index as usize; + + // 1. Validate CPU index is within registered count + let registered = self.cpu_manager.registered_count(); + if cpu_index >= registered { + log::error!("START_AP: CpuIndex({}) >= registered_count({})", cpu_index, registered); + return efi::Status::INVALID_PARAMETER.as_usize() as u64; + } + + // 2. Look up the APIC ID for this index + let cpu_id = match self.cpu_manager.get_cpu_id_by_index(cpu_index) { + Some(id) => id, + None => { + log::error!("START_AP: CpuIndex({}) has no registered CPU", cpu_index); + return efi::Status::INVALID_PARAMETER.as_usize() as u64; + } + }; + + // 3. Check that the target is not the BSP + if self.cpu_manager.is_bsp(cpu_id) { + log::error!("START_AP: CpuIndex({}) is the BSP, cannot start as AP", cpu_index); + return efi::Status::INVALID_PARAMETER.as_usize() as u64; + } + + // 4. Validate procedure pointer is non-null + if procedure == 0 { + log::error!("START_AP: Null procedure pointer"); + return efi::Status::INVALID_PARAMETER.as_usize() as u64; + } + + // 5. Send the RunProcedure command to the AP via mailbox + // This will fail if the AP's mailbox is not empty (AP is busy). + let command = ApCommand::RunProcedure { procedure, argument }; + if let Err(()) = self.mailbox_manager.send_command(cpu_id, command) { + log::error!("START_AP: AP (CPU {}, index {}) is busy or mailbox unavailable", cpu_id, cpu_index); + return efi::Status::INVALID_PARAMETER.as_usize() as u64; + } + + log::trace!( + "START_AP: Dispatched proc=0x{:x} arg=0x{:x} to CPU {} (index {})", + procedure, + argument, + cpu_id, + cpu_index + ); + + // 6. Wait for the AP to complete (blocking mode) + // Use a generous timeout (10 seconds = 10_000_000 microseconds) + const AP_TIMEOUT_US: u64 = 10_000_000; + match self.mailbox_manager.wait_response(cpu_id, AP_TIMEOUT_US) { + Some(ApResponse::Success) => { + log::trace!("START_AP: AP (CPU {}) completed successfully", cpu_id); + efi::Status::SUCCESS.as_usize() as u64 + } + Some(ApResponse::Error(code)) => { + log::error!("START_AP: AP (CPU {}) returned error: 0x{:x}", cpu_id, code); + code as u64 + } + Some(ApResponse::Busy) => { + log::error!("START_AP: AP (CPU {}) reported busy", cpu_id); + efi::Status::NOT_READY.as_usize() as u64 + } + Some(ApResponse::None) | None => { + log::error!("START_AP: AP (CPU {}) timed out or no response", cpu_id); + efi::Status::TIMEOUT.as_usize() as u64 + } + } + } + + /// Get the CPU manager. + pub fn cpu_manager(&self) -> &CpuManager<{ P::MAX_CPU_COUNT }> { + &self.cpu_manager + } + + /// Get the mailbox manager. + pub fn mailbox_manager(&self) -> &MailboxManager<{ P::MAX_CPU_COUNT }, P::CpuInfo> { + &self.mailbox_manager + } + + /// Send a command to a specific AP. + /// + /// Returns `Ok(())` if the command was successfully posted to the mailbox, + /// or `Err(())` if the AP is not available or the mailbox is full. + pub fn send_ap_command(&self, cpu_id: u32, command: ApCommand) -> Result<(), ()> { + self.mailbox_manager.send_command(cpu_id, command) + } + + /// Wait for a response from a specific AP. + /// + /// Returns the response from the AP, or `None` if timeout or error. + pub fn wait_ap_response(&self, cpu_id: u32, timeout_us: u64) -> Option { + self.mailbox_manager.wait_response(cpu_id, timeout_us) + } + + /// Check if the supervisor has been initialized. + pub fn is_initialized(&self) -> bool { + self.initialized.load(Ordering::Acquire) + } +} + +impl Default for MmSupervisorCore

+where + [(); P::MAX_CPU_COUNT]:, +{ + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct TestPlatform; + + impl CpuInfo for TestPlatform {} + + impl PlatformInfo for TestPlatform { + type CpuInfo = Self; + const MAX_CPU_COUNT: usize = 4; + } + + #[test] + fn test_cpu_info_defaults() { + assert_eq!(::ap_poll_timeout_us(), 1000); + } + + #[test] + fn test_supervisor_creation() { + let _supervisor: MmSupervisorCore = MmSupervisorCore::new(); + // Just verify it compiles and creates without panic + } + + #[test] + fn test_supervisor_is_const() { + // Verify we can create a static instance (no heap allocation) + static _SUPERVISOR: MmSupervisorCore = MmSupervisorCore::new(); + } +} diff --git a/patina_mm_supervisor_core/src/mailbox.rs b/patina_mm_supervisor_core/src/mailbox.rs new file mode 100644 index 000000000..aaf3ad580 --- /dev/null +++ b/patina_mm_supervisor_core/src/mailbox.rs @@ -0,0 +1,627 @@ +//! Mailbox Module +//! +//! This module provides the mailbox infrastructure for BSP-AP communication. +//! Each AP has a dedicated mailbox that the BSP uses to send commands and receive responses. +//! +//! ## Architecture +//! +//! The mailbox system uses a simple producer-consumer model: +//! - BSP writes commands to AP mailboxes +//! - APs poll their mailboxes for commands +//! - APs write responses back +//! - BSP reads responses when ready +//! +//! ## Memory Model +//! +//! This module does not perform heap allocation. All structures use fixed-size arrays +//! with compile-time constants provided via const generics. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use core::sync::atomic::{AtomicU32, AtomicU64, Ordering}; + +use crate::{CpuInfo, perf_timer}; + +/// Commands that can be sent from BSP to APs via the mailbox. +/// +/// APs sit in a holding pen polling for commands. When no command is pending +/// the AP simply keeps spinning - there is no explicit "no-op" variant. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ApCommand { + /// Run a procedure on the AP, with potential demotion to user mode. + /// + /// The AP checks buffer ownership and demotes to Ring 3 if the procedure + /// lives in user-owned memory, otherwise calls it directly in Ring 0. + RunProcedure { + /// The procedure function pointer. + procedure: u64, + /// The argument to pass to the procedure. + argument: u64, + }, + /// Exit the holding pen and return to the caller. + Return, +} + +impl ApCommand { + /// Converts the command to a u64 tag for atomic storage. + fn to_u64(self) -> u64 { + match self { + ApCommand::RunProcedure { .. } => 1, + ApCommand::Return => 2, + } + } + + /// Converts a u64 tag back to a command. + fn from_u64(tag: u64, procedure: u64, argument: u64) -> Option { + match tag & 0xFF { + 1 => Some(ApCommand::RunProcedure { procedure, argument }), + 2 => Some(ApCommand::Return), + _ => None, + } + } +} + +/// Responses from APs to the BSP. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ApResponse { + /// No response yet (mailbox empty). + None, + /// Command executed successfully. + Success, + /// Command failed with an error code. + Error(u32), + /// AP is busy and cannot accept commands. + Busy, +} + +impl ApResponse { + /// Converts the response to a u64 for atomic storage. + fn to_u64(self) -> u64 { + match self { + ApResponse::None => 0, + ApResponse::Success => 1, + ApResponse::Error(code) => 2 | ((code as u64) << 32), + ApResponse::Busy => 3, + } + } + + /// Converts a u64 back to a response. + fn from_u64(value: u64) -> Self { + let resp_type = value & 0xFF; + match resp_type { + 0 => ApResponse::None, + 1 => ApResponse::Success, + 2 => { + let code = (value >> 32) as u32; + ApResponse::Error(code) + } + 3 => ApResponse::Busy, + _ => ApResponse::None, + } + } +} + +/// Mailbox state flags. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +enum MailboxState { + /// Mailbox is empty, no pending command. + Empty = 0, + /// Mailbox has a command waiting to be processed. + CommandPending = 1, + /// Command is being processed. + Processing = 2, + /// Response is ready for BSP to read. + ResponseReady = 3, +} + +impl From for MailboxState { + fn from(value: u32) -> Self { + match value { + 0 => MailboxState::Empty, + 1 => MailboxState::CommandPending, + 2 => MailboxState::Processing, + 3 => MailboxState::ResponseReady, + _ => MailboxState::Empty, + } + } +} + +/// A single AP's mailbox for communication with the BSP. +#[repr(C, align(64))] // Cache-line aligned to avoid false sharing +pub struct ApMailbox { + /// Current state of the mailbox. + state: AtomicU32, + /// The command tag (discriminant packed into u64). + command: AtomicU64, + /// The procedure function pointer (for RunProcedure). + procedure: AtomicU64, + /// The argument to pass to the procedure (for RunProcedure). + argument: AtomicU64, + /// The response data (packed into u64). + response: AtomicU64, + /// The CPU ID this mailbox is assigned to (u32::MAX = unassigned). + assigned_cpu: AtomicU32, + /// Padding to ensure cache-line alignment. + _padding: [u8; 12], +} + +impl ApMailbox { + /// Creates a new empty mailbox. + pub const fn new() -> Self { + Self { + state: AtomicU32::new(MailboxState::Empty as u32), + command: AtomicU64::new(0), + procedure: AtomicU64::new(0), + argument: AtomicU64::new(0), + response: AtomicU64::new(0), + assigned_cpu: AtomicU32::new(u32::MAX), + _padding: [0; 12], + } + } + + /// Gets the current state of the mailbox. + fn state(&self) -> MailboxState { + self.state.load(Ordering::Acquire).into() + } + + /// Gets the assigned CPU ID, if any. + pub fn assigned_cpu(&self) -> Option { + let cpu = self.assigned_cpu.load(Ordering::Acquire); + if cpu == u32::MAX { None } else { Some(cpu) } + } + + /// Assigns this mailbox to a CPU. + /// + /// Returns true if assignment succeeded, false if already assigned. + fn assign(&self, cpu_id: u32) -> bool { + self.assigned_cpu.compare_exchange(u32::MAX, cpu_id, Ordering::AcqRel, Ordering::Acquire).is_ok() + } + + /// Checks if a command is pending (called by AP). + pub fn has_pending_command(&self) -> bool { + self.state() == MailboxState::CommandPending + } + + /// Gets the pending command (called by AP). + /// + /// Returns `Some(command)` if a command is pending, `None` otherwise. + /// This also transitions the mailbox to the Processing state. + pub fn take_command(&self) -> Option { + // Try to transition from CommandPending to Processing + let result = self.state.compare_exchange( + MailboxState::CommandPending as u32, + MailboxState::Processing as u32, + Ordering::AcqRel, + Ordering::Acquire, + ); + + if result.is_ok() { + let tag = self.command.load(Ordering::Acquire); + let proc = self.procedure.load(Ordering::Acquire); + let arg = self.argument.load(Ordering::Acquire); + ApCommand::from_u64(tag, proc, arg) + } else { + None + } + } + + /// Posts a response (called by AP). + pub fn post_response(&self, response: ApResponse) { + self.response.store(response.to_u64(), Ordering::Release); + self.state.store(MailboxState::ResponseReady as u32, Ordering::Release); + } + + /// Sends a command to this mailbox (called by BSP). + /// + /// Returns `true` if the command was successfully posted, `false` if the mailbox is busy. + /// + /// The payload (command tag, procedure, argument) is written first with `Relaxed` + /// ordering, then `state` is set to `CommandPending` with `Release` ordering. + /// The AP acquires `state`, which guarantees it sees the fully-written payload. + pub fn send_command(&self, command: ApCommand) -> bool { + // Only allow sending if mailbox is empty + let result = self.state.compare_exchange( + MailboxState::Empty as u32, + MailboxState::Empty as u32, // keep Empty while we fill the payload + Ordering::AcqRel, + Ordering::Acquire, + ); + + if result.is_ok() { + // Write all payload fields before publishing. + // Relaxed is fine here — the Release store to `state` below + // will fence all prior writes. + match command { + ApCommand::RunProcedure { procedure, argument } => { + self.procedure.store(procedure, Ordering::Relaxed); + self.argument.store(argument, Ordering::Relaxed); + } + ApCommand::Return => { + self.procedure.store(0, Ordering::Relaxed); + self.argument.store(0, Ordering::Relaxed); + } + } + self.command.store(command.to_u64(), Ordering::Relaxed); + + // Publish: the AP polls on `state` with Acquire, so this + // Release ensures it sees the payload written above. + self.state.store(MailboxState::CommandPending as u32, Ordering::Release); + true + } else { + false + } + } + + /// Gets the response from this mailbox (called by BSP). + /// + /// Returns the response and clears the mailbox if a response is ready. + pub fn get_response(&self) -> Option { + // Only read if response is ready + let result = self.state.compare_exchange( + MailboxState::ResponseReady as u32, + MailboxState::Empty as u32, + Ordering::AcqRel, + Ordering::Acquire, + ); + + if result.is_ok() { + let resp = self.response.load(Ordering::Acquire); + Some(ApResponse::from_u64(resp)) + } else { + None + } + } + + /// Spins until the mailbox reaches the `Empty` state, draining any pending response. + /// + /// This is analogous to the C code's `WaitForAllAPsNotBusy(TRUE)` which + /// acquires+releases each AP's Busy spinlock, blocking until the AP is done. + /// + /// If the mailbox is in `ResponseReady`, the response is consumed to transition + /// it back to `Empty`. If it is in `CommandPending` or `Processing`, this spins + /// until the AP finishes and posts a response (which is then drained). + fn drain_to_empty(&self) { + loop { + match self.state() { + MailboxState::Empty => return, + MailboxState::ResponseReady => { + // Consume the response to transition back to Empty. + let _ = self.get_response(); + } + _ => { + // CommandPending or Processing — AP is still working. + core::hint::spin_loop(); + } + } + } + } + + /// Checks if the mailbox is empty (no pending work). + pub fn is_empty(&self) -> bool { + self.state() == MailboxState::Empty + } + + /// Checks if a response is ready. + pub fn has_response(&self) -> bool { + self.state() == MailboxState::ResponseReady + } +} + +impl Default for ApMailbox { + fn default() -> Self { + Self::new() + } +} + +/// Manager for all AP mailboxes. +/// +/// Uses fixed-size arrays with const generic for maximum AP count. +/// +/// ## Const Generic Parameters +/// +/// * `MAX_APS` - The maximum number of APs that can be managed. +pub struct MailboxManager { + /// Mailboxes - fixed size array. + mailboxes: [ApMailbox; MAX_APS], + /// Number of assigned mailboxes. + assigned_count: AtomicU32, + /// Phantom data for the CpuInfo type. + _cpu_info: core::marker::PhantomData, +} + +impl MailboxManager { + /// Creates a new mailbox manager. + /// + /// This is a const fn and performs no heap allocation. + pub const fn new() -> Self { + Self { + mailboxes: [const { ApMailbox::new() }; MAX_APS], + assigned_count: AtomicU32::new(0), + _cpu_info: core::marker::PhantomData, + } + } + + /// Finds or allocates a mailbox for the specified CPU ID. + fn get_or_assign_mailbox(&self, cpu_id: u32) -> Option<&ApMailbox> { + // First, check if already assigned + for mailbox in &self.mailboxes { + if mailbox.assigned_cpu() == Some(cpu_id) { + return Some(mailbox); + } + } + + // Find an unassigned mailbox + for mailbox in &self.mailboxes { + if mailbox.assign(cpu_id) { + self.assigned_count.fetch_add(1, Ordering::SeqCst); + log::trace!("Assigned mailbox to CPU {}", cpu_id); + return Some(mailbox); + } + } + + log::warn!("No available mailbox for CPU {}", cpu_id); + None + } + + /// Gets the mailbox for the specified CPU ID. + fn get_mailbox(&self, cpu_id: u32) -> Option<&ApMailbox> { + for mailbox in &self.mailboxes { + if mailbox.assigned_cpu() == Some(cpu_id) { + return Some(mailbox); + } + } + None + } + + /// Sends a command to a specific AP. + pub fn send_command(&self, cpu_id: u32, command: ApCommand) -> Result<(), ()> { + let mailbox = self.get_or_assign_mailbox(cpu_id).ok_or(())?; + if mailbox.send_command(command) { Ok(()) } else { Err(()) } + } + + /// Checks for a pending command (called by AP). + pub fn check_mailbox(&self, cpu_id: u32) -> Option { + let mailbox = self.get_or_assign_mailbox(cpu_id)?; + mailbox.take_command() + } + + /// Posts a response (called by AP). + pub fn post_response(&self, cpu_id: u32, response: ApResponse) { + if let Some(mailbox) = self.get_mailbox(cpu_id) { + mailbox.post_response(response); + } + } + + /// Waits for a response from an AP with timeout. + /// + /// Returns the response, or `None` if timeout. + pub fn wait_response(&self, cpu_id: u32, timeout_us: u64) -> Option { + let mailbox = self.get_mailbox(cpu_id)?; + let mut result = None; + + perf_timer::spin_until::(timeout_us, || { + if let Some(response) = mailbox.get_response() { + result = Some(response); + true + } else { + false + } + }); + + result + } + + /// Broadcasts a command to all assigned APs. + /// + /// For each assigned mailbox, this first drains any pending response + /// (spinning until the mailbox is `Empty`), then sends the command. + /// This mirrors the C code's `WaitForAllAPsNotBusy(TRUE)` followed + /// by `ReleaseAllAPs()`, ensuring no AP is ever skipped. + /// + /// Returns the number of APs that received the command. + pub fn broadcast_command(&self, command: ApCommand) -> usize { + let mut success_count = 0; + + for mailbox in &self.mailboxes { + if let Some(cpu_id) = mailbox.assigned_cpu() { + // Drain any in-flight work so the mailbox is Empty. + mailbox.drain_to_empty(); + + // Mailbox is now guaranteed Empty — send_command must succeed. + let sent = mailbox.send_command(command); + debug_assert!(sent, "send_command failed after drain_to_empty for CPU {}", cpu_id); + success_count += 1; + log::trace!("Broadcast command to CPU {}", cpu_id); + } + } + + success_count + } + + /// Waits for all assigned APs to post responses, with a timeout. + /// + /// Returns the number of APs that responded within the timeout. + pub fn wait_all_responses(&self, timeout_us: u64) -> usize { + let total = self.assigned_count(); + + perf_timer::spin_until::(timeout_us, || { + let mut responded = 0; + for mailbox in &self.mailboxes { + if mailbox.assigned_cpu().is_some() { + // Count APs that have already been consumed (Empty) or have response ready + if mailbox.is_empty() || mailbox.has_response() { + responded += 1; + } + } + } + responded >= total + }); + + // Drain all pending responses and count + let mut responded = 0; + for mailbox in &self.mailboxes { + if mailbox.assigned_cpu().is_some() { + if mailbox.has_response() { + let _ = mailbox.get_response(); + responded += 1; + } else if mailbox.is_empty() { + responded += 1; + } + } + } + + responded + } + + /// Gets the number of assigned mailboxes. + pub fn assigned_count(&self) -> usize { + self.assigned_count.load(Ordering::SeqCst) as usize + } + + /// Gets the maximum number of mailboxes. + pub const fn max_mailboxes(&self) -> usize { + MAX_APS + } + + /// Iterates over assigned mailboxes, calling the closure for each. + pub fn for_each_assigned(&self, mut f: F) { + for mailbox in &self.mailboxes { + if let Some(cpu_id) = mailbox.assigned_cpu() { + f(cpu_id, mailbox); + } + } + } +} + +impl Default for MailboxManager { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mailbox_creation() { + let mailbox = ApMailbox::new(); + assert!(mailbox.is_empty()); + assert!(!mailbox.has_pending_command()); + assert!(!mailbox.has_response()); + assert!(mailbox.assigned_cpu().is_none()); + } + + #[test] + fn test_mailbox_is_const() { + // Verify we can create a static mailbox + static _MAILBOX: ApMailbox = ApMailbox::new(); + } + + #[test] + fn test_command_send_receive() { + let mailbox = ApMailbox::new(); + mailbox.assign(1); + + // Send a command + assert!(mailbox.send_command(ApCommand::Return)); + assert!(mailbox.has_pending_command()); + + // Cannot send another while one is pending + assert!(!mailbox.send_command(ApCommand::Return)); + + // Take the command + let cmd = mailbox.take_command(); + assert_eq!(cmd, Some(ApCommand::Return)); + assert!(!mailbox.has_pending_command()); + + // Post response + mailbox.post_response(ApResponse::Success); + assert!(mailbox.has_response()); + + // Get response + let resp = mailbox.get_response(); + assert_eq!(resp, Some(ApResponse::Success)); + assert!(mailbox.is_empty()); + } + + #[test] + fn test_run_procedure_command() { + let mailbox = ApMailbox::new(); + mailbox.assign(1); + + let cmd = ApCommand::RunProcedure { procedure: 0xDEAD_BEEF, argument: 0x12345678 }; + + assert!(mailbox.send_command(cmd)); + let received = mailbox.take_command(); + assert!(matches!(received, Some(ApCommand::RunProcedure { procedure: 0xDEAD_BEEF, argument: 0x12345678 }))); + } + + #[test] + fn test_mailbox_manager_is_const() { + // Verify we can create a static manager + static _MANAGER: MailboxManager<8> = MailboxManager::new(); + } + + #[test] + fn test_mailbox_manager() { + let manager: MailboxManager<4> = MailboxManager::new(); + + // Send command (implicitly assigns mailbox) + assert!(manager.send_command(1, ApCommand::Return).is_ok()); + assert_eq!(manager.assigned_count(), 1); + + // Check mailbox + let cmd = manager.check_mailbox(1); + assert_eq!(cmd, Some(ApCommand::Return)); + + // Post response + manager.post_response(1, ApResponse::Success); + + // Wait for response + let resp = manager.wait_response(1, 1000); + assert_eq!(resp, Some(ApResponse::Success)); + } + + #[test] + fn test_broadcast() { + let manager: MailboxManager<4> = MailboxManager::new(); + + // Assign mailboxes for multiple APs + manager.send_command(1, ApCommand::Return).ok(); + manager.check_mailbox(1); // Clear command + manager.post_response(1, ApResponse::Success); + manager.wait_response(1, 1); + + manager.send_command(2, ApCommand::Return).ok(); + manager.check_mailbox(2); + manager.post_response(2, ApResponse::Success); + manager.wait_response(2, 1); + + manager.send_command(3, ApCommand::Return).ok(); + manager.check_mailbox(3); + manager.post_response(3, ApResponse::Success); + manager.wait_response(3, 1); + + // Broadcast + let count = manager.broadcast_command(ApCommand::Return); + assert_eq!(count, 3); + } + + #[test] + fn test_response_encoding() { + let responses = [ApResponse::None, ApResponse::Success, ApResponse::Error(42), ApResponse::Busy]; + + for resp in responses { + let encoded = resp.to_u64(); + let decoded = ApResponse::from_u64(encoded); + assert_eq!(resp, decoded); + } + } +} diff --git a/patina_mm_supervisor_core/src/mm_mem.rs b/patina_mm_supervisor_core/src/mm_mem.rs new file mode 100644 index 000000000..975773c4f --- /dev/null +++ b/patina_mm_supervisor_core/src/mm_mem.rs @@ -0,0 +1,912 @@ +//! MM Supervisor Core Page and Pool Allocators +//! +//! Provides a page-granularity memory allocator and a pool allocator for the MM Supervisor Core. +//! +//! ## Page Allocator +//! +//! When the one-time initialization routine is called, it will mark the blocks reported under +//! `gEfiSmmSmramMemoryGuid` or `gEfiMmPeiMmramMemoryReserveGuid` in the HOB list accordingly. +//! Blocks that have the `EFI_ALLOCATED` bit set in the `RegionState` field will be marked as allocated, +//! indicating they are in use. All other blocks will be marked as free. +//! +//! The page allocator is fully dynamic: +//! - No fixed limit on number of SMRAM regions +//! - No fixed limit on pages per region (supports up to 4GB per region) +//! - Bookkeeping is stored in SMRAM itself +//! +//! The page allocator provides: +//! - `allocate_pages(num_pages)` - Allocate contiguous pages +//! - `free_pages(addr, num_pages)` - Free previously allocated pages +//! +//! ## Pool Allocator +//! +//! Built on top of the page allocator, the pool allocator provides smaller-granularity allocations. +//! It allocates pages from the page allocator and subdivides them for pool allocations. +//! When a pool page is exhausted, more pages are allocated as needed. +//! +//! The pool allocator implements the `GlobalAlloc` trait for use as a global allocator. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use core::{ + cell::UnsafeCell, + ffi::c_void, + mem::size_of, + ptr, slice, + sync::atomic::{AtomicBool, Ordering}, +}; + +use patina::base::UEFI_PAGE_SIZE; +use patina::pi::hob::{Hob, PhaseHandoffInformationTable}; +use patina_paging::{MemoryAttributes, PageTable}; +use r_efi::efi; +use spin::Mutex; + +/// Errors that can occur during page allocation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PageAllocError { + /// The allocator has not been initialized. + NotInitialized, + /// No free pages available to satisfy the request. + OutOfMemory, + /// The requested address is not aligned to page boundary. + NotAligned, + /// The address is not within any known SMRAM region. + InvalidAddress, + /// The address was not previously allocated. + NotAllocated, + /// Too many regions to track. + TooManyRegions, +} + +/// Bits per byte. +const BITS_PER_BYTE: usize = 8; + +/// EFI_ALLOCATED bit in RegionState. +pub const EFI_ALLOCATED: u64 = 0x0000000000000010; + +// GUID for gEfiSmmSmramMemoryGuid +// { 0x6dadf1d1, 0xd4cc, 0x4910, { 0xbb, 0x6e, 0x82, 0xb1, 0xfd, 0x80, 0xff, 0x3d }} +pub const SMM_SMRAM_MEMORY_GUID: efi::Guid = + efi::Guid::from_fields(0x6dadf1d1, 0xd4cc, 0x4910, 0xbb, 0x6e, &[0x82, 0xb1, 0xfd, 0x80, 0xff, 0x3d]); + +// GUID for gEfiMmPeiMmramMemoryReserveGuid +// { 0x0703f912, 0xbf8d, 0x4e2a, { 0xbe, 0x07, 0xab, 0x27, 0x25, 0x25, 0xc5, 0x92 }} +pub const MM_PEI_MMRAM_MEMORY_RESERVE_GUID: efi::Guid = + efi::Guid::from_fields(0x0703f912, 0xbf8d, 0x4e2a, 0xbe, 0x07, &[0xab, 0x27, 0x25, 0x25, 0xc5, 0x92]); + +/// Type of memory allocation - distinguishes supervisor-internal vs user/driver allocations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum AllocationType { + /// Supervisor-internal allocation (e.g., for core data structures). + /// These are typically never freed and may have stricter protections. + Supervisor = 0, + /// User/driver allocation (e.g., for MM driver requests). + /// These can be allocated and freed by external code. + User = 1, +} + +/// SMRAM descriptor structure matching EFI_SMRAM_DESCRIPTOR. +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct SmramDescriptor { + /// Physical start address of the SMRAM region. + pub physical_start: efi::PhysicalAddress, + /// CPU start address (may differ from physical for remapping). + pub cpu_start: efi::PhysicalAddress, + /// Size of the SMRAM region in bytes. + pub physical_size: u64, + /// Region state flags (EFI_ALLOCATED, etc.). + pub region_state: u64, +} + +/// SMRAM reserve descriptor count structure. +/// This is the data that immediately follows a GuidHob with SMM_SMRAM_MEMORY_GUID +/// or MM_PEI_MMRAM_MEMORY_RESERVE_GUID. +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct SmramReserveHobData { + /// Number of SMRAM descriptors that follow. + pub number_of_smram_regions: u32, + /// Reserved for alignment. + pub reserved: u32, + // SmramDescriptor array follows immediately after +} + +/// Metadata for a single SMRAM region. +/// This struct is stored in the bookkeeping pages, not statically. +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct RegionInfo { + /// Base physical address of the region. + pub base: u64, + /// Total number of pages in this region. + pub total_pages: usize, + /// Starting bit index in the global allocation bitmap. + pub bitmap_start_bit: usize, +} + +/// Internal state for the page allocator, stored in bookkeeping pages. +#[repr(C)] +struct AllocatorState { + /// Number of regions. + region_count: usize, + /// Total number of pages across all regions. + total_pages: usize, + /// Number of pages used for bookkeeping. + bookkeeping_pages: usize, + /// Base address of bookkeeping memory. + bookkeeping_base: u64, + // Followed by: + // - RegionInfo array (region_count entries) + // - Allocation bitmap (total_pages bits, rounded up to bytes) + // - Type bitmap (total_pages bits, rounded up to bytes) +} + +/// Page-granularity allocator for SMRAM memory. +/// +/// This allocator is fully dynamic: +/// - No fixed limit on number of SMRAM regions +/// - No fixed limit on pages per region (supports up to 4GB per region) +/// - Bookkeeping data structures are allocated from SMRAM itself +/// +/// ## Initialization +/// +/// During initialization, the allocator: +/// 1. Scans HOBs to count regions and total pages +/// 2. Calculates bookkeeping space needed +/// 3. Reserves pages from the first available region for bookkeeping +/// 4. Initializes bitmaps in the reserved pages +pub struct PageAllocator { + /// Pointer to the allocator state (stored in SMRAM). + state: UnsafeCell<*mut AllocatorState>, + /// Lock for thread safety. + lock: Mutex<()>, + /// Whether the allocator has been initialized. + initialized: AtomicBool, +} + +// SAFETY: The PageAllocator uses internal locking for thread safety. +unsafe impl Send for PageAllocator {} +unsafe impl Sync for PageAllocator {} + +impl PageAllocator { + /// Creates a new uninitialized page allocator. + pub const fn new() -> Self { + Self { state: UnsafeCell::new(ptr::null_mut()), lock: Mutex::new(()), initialized: AtomicBool::new(false) } + } + + /// Calculates the number of pages needed for bookkeeping. + /// + /// Bookkeeping includes: + /// - AllocatorState header + /// - RegionInfo array + /// - Allocation bitmap (1 bit per page) + /// - Type bitmap (1 bit per page) + fn calculate_bookkeeping_pages(region_count: usize, total_pages: usize) -> usize { + let header_size = size_of::(); + let regions_size = region_count * size_of::(); + let bitmap_bytes = (total_pages + BITS_PER_BYTE - 1) / BITS_PER_BYTE; + let total_bytes = header_size + regions_size + bitmap_bytes * 2; // alloc + type bitmaps + (total_bytes + UEFI_PAGE_SIZE - 1) / UEFI_PAGE_SIZE + } + + /// Gets the regions array from the state. + unsafe fn get_regions(&self) -> &[RegionInfo] { + unsafe { + let state = *self.state.get(); + if state.is_null() { + return &[]; + } + let region_count = (*state).region_count; + let regions_ptr = (state as *const u8).add(size_of::()) as *const RegionInfo; + slice::from_raw_parts(regions_ptr, region_count) + } + } + + /// Gets the regions array mutably from the state. + unsafe fn get_regions_mut(&self) -> &mut [RegionInfo] { + unsafe { + let state = *self.state.get(); + if state.is_null() { + return &mut []; + } + let region_count = (*state).region_count; + let regions_ptr = (state as *mut u8).add(size_of::()) as *mut RegionInfo; + slice::from_raw_parts_mut(regions_ptr, region_count) + } + } + + /// Gets the allocation bitmap from the state. + unsafe fn get_alloc_bitmap(&self) -> &[u8] { + unsafe { + let state = *self.state.get(); + if state.is_null() { + return &[]; + } + let region_count = (*state).region_count; + let total_pages = (*state).total_pages; + let bitmap_bytes = (total_pages + BITS_PER_BYTE - 1) / BITS_PER_BYTE; + let bitmap_ptr = + (state as *const u8).add(size_of::()).add(region_count * size_of::()); + slice::from_raw_parts(bitmap_ptr, bitmap_bytes) + } + } + + /// Gets the allocation bitmap mutably from the state. + unsafe fn get_alloc_bitmap_mut(&self) -> &mut [u8] { + unsafe { + let state = *self.state.get(); + if state.is_null() { + return &mut []; + } + let region_count = (*state).region_count; + let total_pages = (*state).total_pages; + let bitmap_bytes = (total_pages + BITS_PER_BYTE - 1) / BITS_PER_BYTE; + let bitmap_ptr = + (state as *mut u8).add(size_of::()).add(region_count * size_of::()); + slice::from_raw_parts_mut(bitmap_ptr, bitmap_bytes) + } + } + + /// Gets the type bitmap from the state. + unsafe fn get_type_bitmap(&self) -> &[u8] { + unsafe { + let state = *self.state.get(); + if state.is_null() { + return &[]; + } + let region_count = (*state).region_count; + let total_pages = (*state).total_pages; + let bitmap_bytes = (total_pages + BITS_PER_BYTE - 1) / BITS_PER_BYTE; + let type_bitmap_ptr = (state as *const u8) + .add(size_of::()) + .add(region_count * size_of::()) + .add(bitmap_bytes); + slice::from_raw_parts(type_bitmap_ptr, bitmap_bytes) + } + } + + /// Gets the type bitmap mutably from the state. + unsafe fn get_type_bitmap_mut(&self) -> &mut [u8] { + unsafe { + let state = *self.state.get(); + if state.is_null() { + return &mut []; + } + let region_count = (*state).region_count; + let total_pages = (*state).total_pages; + let bitmap_bytes = (total_pages + BITS_PER_BYTE - 1) / BITS_PER_BYTE; + let type_bitmap_ptr = (state as *mut u8) + .add(size_of::()) + .add(region_count * size_of::()) + .add(bitmap_bytes); + slice::from_raw_parts_mut(type_bitmap_ptr, bitmap_bytes) + } + } + + /// Checks if a global bit index is allocated. + unsafe fn is_bit_allocated(&self, bit_index: usize) -> bool { + unsafe { + let bitmap = self.get_alloc_bitmap(); + let byte_index = bit_index / BITS_PER_BYTE; + let bit_offset = bit_index % BITS_PER_BYTE; + if byte_index >= bitmap.len() { + return true; // Out of bounds = allocated + } + (bitmap[byte_index] & (1 << bit_offset)) != 0 + } + } + + /// Gets the allocation type for a global bit index. + unsafe fn get_bit_type(&self, bit_index: usize) -> AllocationType { + unsafe { + let bitmap = self.get_type_bitmap(); + let byte_index = bit_index / BITS_PER_BYTE; + let bit_offset = bit_index % BITS_PER_BYTE; + if byte_index >= bitmap.len() { + return AllocationType::Supervisor; + } + if (bitmap[byte_index] & (1 << bit_offset)) != 0 { + AllocationType::User + } else { + AllocationType::Supervisor + } + } + } + + /// Sets a bit as allocated with the given type. + unsafe fn set_bit_allocated(&self, bit_index: usize, alloc_type: AllocationType) { + unsafe { + let alloc_bitmap = self.get_alloc_bitmap_mut(); + let type_bitmap = self.get_type_bitmap_mut(); + let byte_index = bit_index / BITS_PER_BYTE; + let bit_offset = bit_index % BITS_PER_BYTE; + + if byte_index < alloc_bitmap.len() { + alloc_bitmap[byte_index] |= 1 << bit_offset; + match alloc_type { + AllocationType::User => { + type_bitmap[byte_index] |= 1 << bit_offset; + } + AllocationType::Supervisor => { + type_bitmap[byte_index] &= !(1 << bit_offset); + } + } + } + } + } + + /// Clears a bit (marks as free). + unsafe fn set_bit_free(&self, bit_index: usize) { + unsafe { + let alloc_bitmap = self.get_alloc_bitmap_mut(); + let type_bitmap = self.get_type_bitmap_mut(); + let byte_index = bit_index / BITS_PER_BYTE; + let bit_offset = bit_index % BITS_PER_BYTE; + + if byte_index < alloc_bitmap.len() { + alloc_bitmap[byte_index] &= !(1 << bit_offset); + type_bitmap[byte_index] &= !(1 << bit_offset); + } + } + } + + /// Finds which region contains an address and returns (region_index, page_index_in_region). + unsafe fn find_region_for_address(&self, addr: u64) -> Option<(usize, usize)> { + unsafe { + let regions = self.get_regions(); + for (i, region) in regions.iter().enumerate() { + let region_end = region.base + (region.total_pages as u64 * UEFI_PAGE_SIZE as u64); + if addr >= region.base && addr < region_end { + let page_in_region = ((addr - region.base) / UEFI_PAGE_SIZE as u64) as usize; + return Some((i, page_in_region)); + } + } + None + } + } + + /// Converts a region index and page-in-region to a global bit index. + unsafe fn region_page_to_bit(&self, region_index: usize, page_in_region: usize) -> usize { + unsafe { + let regions = self.get_regions(); + if region_index < regions.len() { regions[region_index].bitmap_start_bit + page_in_region } else { 0 } + } + } + + /// Initializes the page allocator from the HOB list. + /// + /// This function: + /// 1. Scans HOBs to count regions and total pages + /// 2. Finds the first non-allocated region for bookkeeping + /// 3. Reserves pages for bookkeeping structures + /// 4. Initializes the bitmaps + /// + /// # Safety + /// + /// The caller must ensure that `hob_list` points to a valid HOB list. + pub unsafe fn init_from_hob_list(&self, hob_list: *const c_void) -> Result<(), PageAllocError> { + if hob_list.is_null() { + return Err(PageAllocError::NotInitialized); + } + + let _guard = self.lock.lock(); + + // Get the HOB list iterator + let hob_list_info = unsafe { + (hob_list as *const PhaseHandoffInformationTable).as_ref().ok_or(PageAllocError::NotInitialized)? + }; + + let hob = Hob::Handoff(hob_list_info); + + // First pass: count regions and total pages + let mut region_count = 0usize; + let mut total_pages = 0usize; + let mut first_free_region_base: Option = None; + let mut first_free_region_size: u64 = 0; + + // Temporary storage for region info (we'll copy to SMRAM later) + // Using a reasonable stack limit - actual regions stored in SMRAM + const MAX_TEMP_REGIONS: usize = 256; + let mut temp_regions: [(u64, u64, bool); MAX_TEMP_REGIONS] = [(0, 0, false); MAX_TEMP_REGIONS]; + + for current_hob in &hob { + if let Hob::GuidHob(guid_hob, data) = current_hob { + if guid_hob.name == SMM_SMRAM_MEMORY_GUID || guid_hob.name == MM_PEI_MMRAM_MEMORY_RESERVE_GUID { + log::info!("Found SMRAM memory HOB with GUID {:?}", guid_hob.name); + + if data.len() < size_of::() { + continue; + } + + let reserve_data = unsafe { &*(data.as_ptr() as *const SmramReserveHobData) }; + let descriptor_count = reserve_data.number_of_smram_regions as usize; + + let descriptors_ptr = + unsafe { data.as_ptr().add(size_of::()) as *const SmramDescriptor }; + + for i in 0..descriptor_count { + if region_count >= MAX_TEMP_REGIONS { + log::warn!("Too many SMRAM regions for temp storage, increase MAX_TEMP_REGIONS"); + break; + } + + let descriptor = unsafe { &*descriptors_ptr.add(i) }; + let pre_allocated = (descriptor.region_state & EFI_ALLOCATED) != 0; + let pages = (descriptor.physical_size as usize) / UEFI_PAGE_SIZE; + + log::info!( + "SMRAM Region {}: base=0x{:016x}, size=0x{:x}, pages={}, state=0x{:x}, allocated={}", + region_count, + descriptor.physical_start, + descriptor.physical_size, + pages, + descriptor.region_state, + pre_allocated + ); + + temp_regions[region_count] = + (descriptor.physical_start, descriptor.physical_size, pre_allocated); + + // Track first non-allocated region for bookkeeping + if first_free_region_base.is_none() && !pre_allocated { + first_free_region_base = Some(descriptor.physical_start); + first_free_region_size = descriptor.physical_size; + } + + total_pages += pages; + region_count += 1; + } + } + } + } + + if region_count == 0 { + log::error!("No SMRAM regions found in HOB list"); + return Err(PageAllocError::NotInitialized); + } + + // Calculate bookkeeping space needed + let bookkeeping_pages = Self::calculate_bookkeeping_pages(region_count, total_pages); + + log::info!( + "Allocator needs {} pages for bookkeeping ({} regions, {} total pages)", + bookkeeping_pages, + region_count, + total_pages + ); + + // Find space for bookkeeping + let bookkeeping_base = first_free_region_base.ok_or_else(|| { + log::error!("No free SMRAM region available for bookkeeping"); + PageAllocError::OutOfMemory + })?; + + if (bookkeeping_pages * UEFI_PAGE_SIZE) as u64 > first_free_region_size { + log::error!("First free region too small for bookkeeping"); + return Err(PageAllocError::OutOfMemory); + } + + log::info!("Using 0x{:016x} for bookkeeping ({} pages)", bookkeeping_base, bookkeeping_pages); + + // Initialize the state structure in SMRAM + let state_ptr = bookkeeping_base as *mut AllocatorState; + unsafe { + // Zero the bookkeeping pages first + ptr::write_bytes(bookkeeping_base as *mut u8, 0, bookkeeping_pages * UEFI_PAGE_SIZE); + + // Write the header + (*state_ptr).region_count = region_count; + (*state_ptr).total_pages = total_pages; + (*state_ptr).bookkeeping_pages = bookkeeping_pages; + (*state_ptr).bookkeeping_base = bookkeeping_base; + + // Store state pointer + *self.state.get() = state_ptr; + } + + // Initialize region info + let mut bitmap_start_bit = 0usize; + { + let regions = unsafe { self.get_regions_mut() }; + for (i, region) in regions.iter_mut().enumerate() { + let (base, size, _) = temp_regions[i]; + let pages = (size as usize) / UEFI_PAGE_SIZE; + region.base = base; + region.total_pages = pages; + region.bitmap_start_bit = bitmap_start_bit; + bitmap_start_bit += pages; + } + } + + // Mark pre-allocated regions and bookkeeping pages as allocated + for i in 0..region_count { + let (base, size, pre_allocated) = temp_regions[i]; + let pages = (size as usize) / UEFI_PAGE_SIZE; + + if pre_allocated { + // Mark entire region as allocated (supervisor) + let regions = unsafe { self.get_regions() }; + let start_bit = regions[i].bitmap_start_bit; + for p in 0..pages { + unsafe { self.set_bit_allocated(start_bit + p, AllocationType::Supervisor) }; + } + } else if base == bookkeeping_base { + // Mark bookkeeping pages as allocated (supervisor) + let regions = unsafe { self.get_regions() }; + let start_bit = regions[i].bitmap_start_bit; + for p in 0..bookkeeping_pages { + unsafe { self.set_bit_allocated(start_bit + p, AllocationType::Supervisor) }; + } + } + } + + self.initialized.store(true, Ordering::Release); + + log::info!( + "Page allocator initialized: {} region(s), {} total pages, {} free pages", + region_count, + total_pages, + self.free_page_count() + ); + + Ok(()) + } + + /// Allocates contiguous pages from SMRAM for supervisor use. + pub fn allocate_pages(&self, num_pages: usize) -> Result { + self.allocate_pages_with_type(num_pages, AllocationType::Supervisor) + } + + /// Allocates contiguous pages from SMRAM with the specified allocation type. + /// + /// For `Supervisor` allocations, the allocated region is marked as supervisor-owned + /// data pages (R/W, non-executable) in the page table. + pub fn allocate_pages_with_type( + &self, + num_pages: usize, + alloc_type: AllocationType, + ) -> Result { + if !self.initialized.load(Ordering::Acquire) { + return Err(PageAllocError::NotInitialized); + } + + if num_pages == 0 { + return Err(PageAllocError::OutOfMemory); + } + + let allocated_addr = { + let _guard = self.lock.lock(); + + // SAFETY: We have exclusive access via the lock + unsafe { + let regions = self.get_regions(); + + let mut found: Option = None; + // Try each region + 'outer: for region in regions.iter() { + // First-fit search for contiguous pages + let mut run_start = 0usize; + let mut run_length = 0usize; + + for page_in_region in 0..region.total_pages { + let bit_index = region.bitmap_start_bit + page_in_region; + if self.is_bit_allocated(bit_index) { + run_start = page_in_region + 1; + run_length = 0; + } else { + run_length += 1; + if run_length == num_pages { + // Found a suitable run, allocate it + for p in run_start..run_start + num_pages { + let bit = region.bitmap_start_bit + p; + self.set_bit_allocated(bit, alloc_type); + } + let addr = region.base + (run_start as u64 * UEFI_PAGE_SIZE as u64); + log::trace!("Allocated {} {:?} page(s) at 0x{:016x}", num_pages, alloc_type, addr); + found = Some(addr); + break 'outer; + } + } + } + } + + found + } + }; // lock is dropped here + + let addr = allocated_addr.ok_or(PageAllocError::OutOfMemory)?; + + // For supervisor allocations, update page table attributes to mark as + // supervisor-owned data pages (R/W/NX/S), otherwise they would + // default to user data (R/W/NX/U). + self.apply_data_page_attributes(addr, num_pages, alloc_type); + + Ok(addr) + } + + /// Applies supervisor page table attributes to a newly allocated region. + /// + /// Marks pages as supervisor-owned data pages: Read/Write + Non-Executable (NX). + /// This ensures supervisor data cannot be executed, providing W^X enforcement. + /// + /// If the global page table is not yet initialized (e.g., during early boot), + /// this is a no-op with a warning. + fn apply_data_page_attributes(&self, addr: u64, num_pages: usize, _alloc_type: AllocationType) { + let size = (num_pages * UEFI_PAGE_SIZE) as u64; + let mut pt_guard = crate::PAGE_TABLE.lock(); + if let Some(ref mut pt) = *pt_guard { + // Data pages: R/W (no ReadOnly) + NX (ExecuteProtect) + let mut attributes = MemoryAttributes::ExecuteProtect; + + if _alloc_type == AllocationType::Supervisor { + // For Supervisor allocations, we additionally want the U/S bit cleared (Supervisor-only). + attributes = attributes | MemoryAttributes::Supervisor; // Ensure not writable by user code + } + + if let Err(e) = pt.map_memory_region(addr, size, attributes) { + log::error!( + "Failed to set supervisor page attributes for 0x{:016x} ({} pages): {:?}", + addr, + num_pages, + e + ); + } else { + log::trace!("Marked 0x{:016x} ({} pages) as supervisor R/W+NX", addr, num_pages,); + } + } else { + log::warn!("Page table not initialized, skipping attribute update for 0x{:016x}", addr); + } + } + + /// Applies restrictive page table attributes to freed pages. + /// + /// Marks pages as completely inaccessible: Supervisor + ReadProtect + ExecuteProtect (NX). + /// This prevents any read, write, or execute access to freed memory, mitigating + /// use-after-free vulnerabilities. + /// + /// If the global page table is not yet initialized (e.g., during early boot), + /// this is a no-op with a warning. + fn apply_freed_page_attributes(&self, addr: u64, num_pages: usize) { + let size = (num_pages * UEFI_PAGE_SIZE) as u64; + let mut pt_guard = crate::PAGE_TABLE.lock(); + if let Some(ref mut pt) = *pt_guard { + // Freed pages: ReadProtect (not present) + NX (no execute) + ReadOnly (no write) + // This makes the pages completely inaccessible. + if let Err(e) = pt.unmap_memory_region(addr, size) { + log::error!("Failed to set freed page attributes for 0x{:016x} ({} pages): {:?}", addr, num_pages, e); + } else { + log::trace!("Marked 0x{:016x} ({} pages) as inaccessible (RP+NX+RO+S)", addr, num_pages,); + } + } else { + log::warn!("Page table not initialized, skipping freed page attribute update for 0x{:016x}", addr); + } + } + + /// Frees previously allocated pages. + /// + /// After freeing, the pages are marked as inaccessible in the page table + /// (Supervisor + ReadProtect + ExecuteProtect) to prevent use-after-free. + pub fn free_pages(&self, addr: u64, num_pages: usize) -> Result<(), PageAllocError> { + if !self.initialized.load(Ordering::Acquire) { + return Err(PageAllocError::NotInitialized); + } + + if addr % UEFI_PAGE_SIZE as u64 != 0 { + return Err(PageAllocError::NotAligned); + } + + { + let _guard = self.lock.lock(); + + unsafe { + let (region_index, page_in_region) = + self.find_region_for_address(addr).ok_or(PageAllocError::InvalidAddress)?; + + let regions = self.get_regions(); + let region = ®ions[region_index]; + + // Verify all pages are allocated + for p in 0..num_pages { + let bit = region.bitmap_start_bit + page_in_region + p; + if !self.is_bit_allocated(bit) { + return Err(PageAllocError::NotAllocated); + } + } + + // Free the pages + for p in 0..num_pages { + let bit = region.bitmap_start_bit + page_in_region + p; + self.set_bit_free(bit); + } + + log::trace!("Freed {} page(s) at 0x{:016x}", num_pages, addr); + } + } // lock is dropped here + + // Mark freed pages as inaccessible in the page table. + self.apply_freed_page_attributes(addr, num_pages); + + Ok(()) + } + + /// Frees previously allocated pages, verifying the allocation type matches. + /// + /// After freeing, the pages are marked as inaccessible in the page table + /// (Supervisor + ReadProtect + ExecuteProtect) to prevent use-after-free. + pub fn free_pages_checked( + &self, + addr: u64, + num_pages: usize, + expected_type: AllocationType, + ) -> Result<(), PageAllocError> { + if !self.initialized.load(Ordering::Acquire) { + return Err(PageAllocError::NotInitialized); + } + + if addr % UEFI_PAGE_SIZE as u64 != 0 { + return Err(PageAllocError::NotAligned); + } + + { + let _guard = self.lock.lock(); + + unsafe { + let (region_index, page_in_region) = + self.find_region_for_address(addr).ok_or(PageAllocError::InvalidAddress)?; + + let regions = self.get_regions(); + let region = ®ions[region_index]; + + // Verify all pages are allocated with expected type + for p in 0..num_pages { + let bit = region.bitmap_start_bit + page_in_region + p; + if !self.is_bit_allocated(bit) { + return Err(PageAllocError::NotAllocated); + } + if self.get_bit_type(bit) != expected_type { + log::warn!( + "Type mismatch at 0x{:016x}: expected {:?}, got {:?}", + addr + (p as u64 * UEFI_PAGE_SIZE as u64), + expected_type, + self.get_bit_type(bit) + ); + return Err(PageAllocError::InvalidAddress); + } + } + + // Free the pages + for p in 0..num_pages { + let bit = region.bitmap_start_bit + page_in_region + p; + self.set_bit_free(bit); + } + + log::trace!("Freed {} {:?} page(s) at 0x{:016x}", num_pages, expected_type, addr); + } + } // lock is dropped here + + // Mark freed pages as inaccessible in the page table. + self.apply_freed_page_attributes(addr, num_pages); + + Ok(()) + } + + /// Returns the total number of free pages across all regions. + pub fn free_page_count(&self) -> usize { + if !self.initialized.load(Ordering::Acquire) { + return 0; + } + + unsafe { + let state = *self.state.get(); + if state.is_null() { + return 0; + } + let total_pages = (*state).total_pages; + let mut free = 0; + for bit in 0..total_pages { + if !self.is_bit_allocated(bit) { + free += 1; + } + } + free + } + } + + /// Returns the number of pages allocated for a specific type. + pub fn allocated_page_count(&self, alloc_type: AllocationType) -> usize { + if !self.initialized.load(Ordering::Acquire) { + return 0; + } + + let _guard = self.lock.lock(); + + unsafe { + let state = *self.state.get(); + if state.is_null() { + return 0; + } + let total_pages = (*state).total_pages; + let mut count = 0; + for bit in 0..total_pages { + if self.is_bit_allocated(bit) && self.get_bit_type(bit) == alloc_type { + count += 1; + } + } + count + } + } + + /// Returns the allocation type for a given address. + pub fn get_allocation_type(&self, addr: u64) -> Option { + if !self.initialized.load(Ordering::Acquire) { + return None; + } + + let _guard = self.lock.lock(); + + unsafe { + let (region_index, page_in_region) = self.find_region_for_address(addr)?; + let bit = self.region_page_to_bit(region_index, page_in_region); + if self.is_bit_allocated(bit) { Some(self.get_bit_type(bit)) } else { None } + } + } + + /// Returns whether the allocator has been initialized. + pub fn is_initialized(&self) -> bool { + self.initialized.load(Ordering::Acquire) + } + + /// Returns the total number of pages across all regions. + pub fn total_page_count(&self) -> usize { + if !self.initialized.load(Ordering::Acquire) { + return 0; + } + unsafe { + let state = *self.state.get(); + if state.is_null() { 0 } else { (*state).total_pages } + } + } + + /// Returns the number of regions. + pub fn region_count(&self) -> usize { + if !self.initialized.load(Ordering::Acquire) { + return 0; + } + unsafe { + let state = *self.state.get(); + if state.is_null() { 0 } else { (*state).region_count } + } + } + + pub fn is_region_inside_mmram(&self, addr: u64, size: u64) -> bool { + if !self.initialized.load(Ordering::Acquire) { + return false; + } + + let _guard = self.lock.lock(); + + unsafe { + let regions = self.get_regions(); + for region in regions.iter() { + let region_end = region.base + (region.total_pages as u64 * UEFI_PAGE_SIZE as u64); + if addr >= region.base && (addr + size) <= region_end { + return true; + } + } + false + } + } +} + +/// Global page allocator instance. +/// +/// This must be initialized via `init_from_hob_list` before use. +pub static PAGE_ALLOCATOR: PageAllocator = PageAllocator::new(); diff --git a/patina_mm_supervisor_core/src/paging_allocator.rs b/patina_mm_supervisor_core/src/paging_allocator.rs new file mode 100644 index 000000000..99cec3cc9 --- /dev/null +++ b/patina_mm_supervisor_core/src/paging_allocator.rs @@ -0,0 +1,401 @@ +//! Paging Page Allocator +//! +//! A dedicated page allocator for the paging subsystem that allocates pages for +//! page table structures (PML4, PDPT, PD, PT entries). +//! +//! ## Design +//! +//! This allocator is separate from the generic PageAllocator for two reasons: +//! +//! 1. **Bootstrap problem**: The paging subsystem needs to allocate pages for page +//! tables, but the generic PageAllocator wants to call into paging to set page +//! attributes for newly allocated pages. This creates a circular dependency. +//! +//! 2. **Security**: Page table pages require special attributes (Supervisor, RW, +//! non-executable) and should be tracked separately from general allocations. +//! +//! ## Initialization +//! +//! The paging allocator is initialized with a reserved memory region from SMRAM. +//! This region is exclusively used for page table allocations. +//! +//! ## Integration with Paging +//! +//! After the paging subsystem is fully initialized, the generic PageAllocator can +//! optionally register a callback to apply page table attributes to newly allocated +//! pages via the paging instance. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use core::{ + cell::UnsafeCell, + sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}, +}; + +use patina::base::UEFI_PAGE_SIZE; +use patina_paging::{PtError, page_allocator::PageAllocator as PagingPageAllocator}; +use spin::Mutex; + +/// Default number of pages to reserve for page table allocations. +/// This should be sufficient for most MM environments (128 pages = 512KB). +pub const DEFAULT_PAGING_POOL_PAGES: usize = 128; + +/// Errors that can occur during paging allocator operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PagingAllocError { + /// The allocator has not been initialized. + NotInitialized, + /// Already initialized. + AlreadyInitialized, + /// No free pages available to satisfy the request. + OutOfMemory, + /// Invalid alignment requested. + InvalidAlignment, + /// The pool region is too small. + PoolTooSmall, +} + +/// A dedicated page allocator for the paging subsystem. +/// +/// This allocator uses a simple bump allocator from a reserved pool of pages. +/// It implements the `patina_paging::PageAllocator` trait to be used directly +/// by the paging crate for allocating page table structures. +/// +/// ## Thread Safety +/// +/// This allocator is thread-safe and can be used from multiple CPUs. +/// +/// ## Example +/// +/// ```rust,ignore +/// use patina_mm_supervisor_core::paging_allocator::{PagingPageAllocator, DEFAULT_PAGING_POOL_PAGES}; +/// +/// // During early init, reserve a region from SMRAM for page tables +/// let pool_base = 0x8000_0000u64; // Example base address +/// let pool_pages = DEFAULT_PAGING_POOL_PAGES; +/// +/// // Initialize the allocator +/// unsafe { +/// PAGING_ALLOCATOR.init(pool_base, pool_pages)?; +/// } +/// +/// // The allocator can now be used by the paging crate +/// ``` +pub struct PagingPoolAllocator { + /// Base address of the pool. + pool_base: AtomicU64, + /// Total number of pages in the pool. + pool_pages: AtomicUsize, + /// Current allocation offset (bump pointer) in bytes. + current_offset: AtomicUsize, + /// Number of pages allocated. + allocated_pages: AtomicUsize, + /// Whether the allocator has been initialized. + initialized: AtomicBool, + /// Lock for thread safety during allocation. + lock: Mutex<()>, +} + +// SAFETY: The PagingPoolAllocator uses internal locking for thread safety. +unsafe impl Send for PagingPoolAllocator {} +unsafe impl Sync for PagingPoolAllocator {} + +impl PagingPoolAllocator { + /// Creates a new uninitialized paging page allocator. + pub const fn new() -> Self { + Self { + pool_base: AtomicU64::new(0), + pool_pages: AtomicUsize::new(0), + current_offset: AtomicUsize::new(0), + allocated_pages: AtomicUsize::new(0), + initialized: AtomicBool::new(false), + lock: Mutex::new(()), + } + } + + /// Initializes the paging allocator with a reserved memory region. + /// + /// # Arguments + /// + /// * `pool_base` - Base physical address of the reserved pool (must be page-aligned) + /// * `pool_pages` - Number of pages in the pool + /// + /// # Safety + /// + /// The caller must ensure that: + /// - `pool_base` points to a valid memory region in SMRAM + /// - The region is not used by any other allocator + /// - The region has at least `pool_pages * UEFI_PAGE_SIZE` bytes available + /// + /// # Errors + /// + /// Returns an error if already initialized or if parameters are invalid. + pub unsafe fn init(&self, pool_base: u64, pool_pages: usize) -> Result<(), PagingAllocError> { + if self.initialized.load(Ordering::Acquire) { + return Err(PagingAllocError::AlreadyInitialized); + } + + if pool_base == 0 || pool_pages == 0 { + return Err(PagingAllocError::PoolTooSmall); + } + + if pool_base % UEFI_PAGE_SIZE as u64 != 0 { + return Err(PagingAllocError::InvalidAlignment); + } + + let _guard = self.lock.lock(); + + // Zero the pool region + unsafe { + core::ptr::write_bytes(pool_base as *mut u8, 0, pool_pages * UEFI_PAGE_SIZE); + } + + self.pool_base.store(pool_base, Ordering::Release); + self.pool_pages.store(pool_pages, Ordering::Release); + self.current_offset.store(0, Ordering::Release); + self.allocated_pages.store(0, Ordering::Release); + self.initialized.store(true, Ordering::Release); + + log::info!( + "Paging allocator initialized: base=0x{:016x}, pages={} ({} KB)", + pool_base, + pool_pages, + pool_pages * UEFI_PAGE_SIZE / 1024 + ); + + Ok(()) + } + + /// Allocates a page for page table structures. + /// + /// # Arguments + /// + /// * `align` - Required alignment in bytes (must be a power of 2 and >= UEFI_PAGE_SIZE) + /// * `size` - Size in bytes (must be >= UEFI_PAGE_SIZE) + /// * `is_root` - Whether this is a root page table (e.g., PML4) + /// + /// # Returns + /// + /// The physical address of the allocated page, or an error. + pub fn allocate_page_internal(&self, align: u64, size: u64, _is_root: bool) -> Result { + if !self.initialized.load(Ordering::Acquire) { + return Err(PagingAllocError::NotInitialized); + } + + // Validate alignment (must be at least UEFI_PAGE_SIZE and power of 2) + let align = align.max(UEFI_PAGE_SIZE as u64); + if !align.is_power_of_two() { + return Err(PagingAllocError::InvalidAlignment); + } + + // Validate size (must be at least UEFI_PAGE_SIZE) + let size = size.max(UEFI_PAGE_SIZE as u64); + let pages_needed = ((size as usize) + UEFI_PAGE_SIZE - 1) / UEFI_PAGE_SIZE; + + let _guard = self.lock.lock(); + + let pool_base = self.pool_base.load(Ordering::Acquire); + let pool_pages = self.pool_pages.load(Ordering::Acquire); + let current_offset = self.current_offset.load(Ordering::Acquire); + + // Calculate the aligned address + let current_addr = pool_base + current_offset as u64; + let aligned_addr = (current_addr + align - 1) & !(align - 1); + let padding = (aligned_addr - current_addr) as usize; + let total_bytes = padding + (pages_needed * UEFI_PAGE_SIZE); + + // Check if we have enough space + if current_offset + total_bytes > pool_pages * UEFI_PAGE_SIZE { + log::error!( + "Paging allocator out of memory: need {} bytes, have {} bytes remaining", + total_bytes, + pool_pages * UEFI_PAGE_SIZE - current_offset + ); + return Err(PagingAllocError::OutOfMemory); + } + + // Update the offset + self.current_offset.store(current_offset + total_bytes, Ordering::Release); + self.allocated_pages.fetch_add(pages_needed, Ordering::Release); + + log::trace!( + "Paging allocator: allocated {} page(s) at 0x{:016x} (align=0x{:x})", + pages_needed, + aligned_addr, + align + ); + + Ok(aligned_addr) + } + + /// Returns whether the allocator has been initialized. + pub fn is_initialized(&self) -> bool { + self.initialized.load(Ordering::Acquire) + } + + /// Returns the number of pages allocated. + pub fn allocated_page_count(&self) -> usize { + self.allocated_pages.load(Ordering::Acquire) + } + + /// Returns the number of free pages remaining. + pub fn free_page_count(&self) -> usize { + if !self.initialized.load(Ordering::Acquire) { + return 0; + } + + let pool_pages = self.pool_pages.load(Ordering::Acquire); + let current_offset = self.current_offset.load(Ordering::Acquire); + let used_pages = (current_offset + UEFI_PAGE_SIZE - 1) / UEFI_PAGE_SIZE; + pool_pages.saturating_sub(used_pages) + } + + /// Returns the base address of the pool. + pub fn pool_base(&self) -> u64 { + self.pool_base.load(Ordering::Acquire) + } + + /// Returns the total size of the pool in bytes. + pub fn pool_size(&self) -> usize { + self.pool_pages.load(Ordering::Acquire) * UEFI_PAGE_SIZE + } +} + +impl PagingPageAllocator for PagingPoolAllocator { + /// Allocates a page for page table structures. + /// + /// This implements the `patina_paging::PageAllocator` trait. + fn allocate_page(&mut self, align: u64, size: u64, is_root: bool) -> Result { + self.allocate_page_internal(align, size, is_root).map_err(|e| { + log::error!("Paging allocator error: {:?}", e); + match e { + PagingAllocError::NotInitialized => PtError::InvalidParameter, + PagingAllocError::AlreadyInitialized => PtError::InvalidParameter, + PagingAllocError::OutOfMemory => PtError::OutOfResources, + PagingAllocError::InvalidAlignment => PtError::InvalidParameter, + PagingAllocError::PoolTooSmall => PtError::InvalidParameter, + } + }) + } +} + +/// A wrapper around PagingPoolAllocator that allows shared (non-mutable) access +/// while still implementing the PageAllocator trait. +/// +/// This is needed because the `patina_paging::PageAllocator` trait requires `&mut self`, +/// but we want to use a global static allocator with interior mutability. +pub struct SharedPagingAllocator { + /// The underlying allocator. + inner: UnsafeCell<&'static PagingPoolAllocator>, +} + +// SAFETY: The PagingPoolAllocator uses internal locking for thread safety. +unsafe impl Send for SharedPagingAllocator {} +unsafe impl Sync for SharedPagingAllocator {} + +impl SharedPagingAllocator { + /// Creates a new shared paging allocator wrapper. + pub const fn new(allocator: &'static PagingPoolAllocator) -> Self { + Self { inner: UnsafeCell::new(allocator) } + } +} + +impl PagingPageAllocator for SharedPagingAllocator { + fn allocate_page(&mut self, align: u64, size: u64, is_root: bool) -> Result { + // SAFETY: The underlying PagingPoolAllocator uses internal locking + let allocator = unsafe { *self.inner.get() }; + allocator.allocate_page_internal(align, size, is_root).map_err(|e| { + log::error!("Paging allocator error: {:?}", e); + match e { + PagingAllocError::NotInitialized => PtError::InvalidParameter, + PagingAllocError::AlreadyInitialized => PtError::InvalidParameter, + PagingAllocError::OutOfMemory => PtError::OutOfResources, + PagingAllocError::InvalidAlignment => PtError::InvalidParameter, + PagingAllocError::PoolTooSmall => PtError::InvalidParameter, + } + }) + } +} + +/// Global paging page allocator instance. +/// +/// This must be initialized via `init()` before use. +pub static PAGING_ALLOCATOR: PagingPoolAllocator = PagingPoolAllocator::new(); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_paging_allocator_not_initialized() { + let allocator = PagingPoolAllocator::new(); + assert!(!allocator.is_initialized()); + assert_eq!(allocator.free_page_count(), 0); + assert_eq!(allocator.allocated_page_count(), 0); + } + + #[test] + fn test_paging_allocator_init() { + let allocator = PagingPoolAllocator::new(); + + // Create a test buffer + let mut buffer = vec![0u8; 16 * UEFI_PAGE_SIZE]; + let base = buffer.as_mut_ptr() as u64; + // Align to page boundary + let aligned_base = (base + UEFI_PAGE_SIZE as u64 - 1) & !(UEFI_PAGE_SIZE as u64 - 1); + + unsafe { + assert!(allocator.init(aligned_base, 8).is_ok()); + } + + assert!(allocator.is_initialized()); + assert_eq!(allocator.free_page_count(), 8); + assert_eq!(allocator.allocated_page_count(), 0); + } + + #[test] + fn test_paging_allocator_double_init() { + let allocator = PagingPoolAllocator::new(); + + let mut buffer = vec![0u8; 16 * UEFI_PAGE_SIZE]; + let base = buffer.as_mut_ptr() as u64; + let aligned_base = (base + UEFI_PAGE_SIZE as u64 - 1) & !(UEFI_PAGE_SIZE as u64 - 1); + + unsafe { + assert!(allocator.init(aligned_base, 8).is_ok()); + assert_eq!(allocator.init(aligned_base, 8), Err(PagingAllocError::AlreadyInitialized)); + } + } + + #[test] + fn test_paging_allocator_allocate() { + let allocator = PagingPoolAllocator::new(); + + let mut buffer = vec![0u8; 32 * UEFI_PAGE_SIZE]; + let base = buffer.as_mut_ptr() as u64; + let aligned_base = (base + UEFI_PAGE_SIZE as u64 - 1) & !(UEFI_PAGE_SIZE as u64 - 1); + + unsafe { + allocator.init(aligned_base, 16).unwrap(); + } + + // Allocate a page + let result = allocator.allocate_page_internal(UEFI_PAGE_SIZE as u64, UEFI_PAGE_SIZE as u64, false); + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr, aligned_base); + assert_eq!(allocator.allocated_page_count(), 1); + + // Allocate another page + let result2 = allocator.allocate_page_internal(UEFI_PAGE_SIZE as u64, UEFI_PAGE_SIZE as u64, false); + assert!(result2.is_ok()); + let addr2 = result2.unwrap(); + assert_eq!(addr2, aligned_base + UEFI_PAGE_SIZE as u64); + assert_eq!(allocator.allocated_page_count(), 2); + } +} diff --git a/patina_mm_supervisor_core/src/perf_timer.rs b/patina_mm_supervisor_core/src/perf_timer.rs new file mode 100644 index 000000000..9d4123ec8 --- /dev/null +++ b/patina_mm_supervisor_core/src/perf_timer.rs @@ -0,0 +1,103 @@ +//! Performance Timer for the MM Supervisor Core +//! +//! Provides real-time, TSC-based timing helpers used by mailbox timeouts and +//! AP-arrival polling. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use core::arch::x86_64; + +use crate::CpuInfo; + +// TODO: This is copied from perf_timer.rs in patina_dxe_core +/// Returns the current CPU count using architecture-specific methods. +/// +/// Skip coverage as any value could be valid, including 0. +#[coverage(off)] +fn ticks() -> u64 { + // SAFETY: _rdtsc only reads the TSC on x86_64. No invariants are required for safety. + unsafe { x86_64::_rdtsc() } +} + +/// Returns the frequency in Hz from the platform's CpuInfo, or `0` if +/// not determinable. +#[inline] +pub fn frequency() -> u64 { + C::perf_timer_frequency().unwrap_or(0) +} + +/// Converts a duration in microseconds to the equivalent tick count using +/// the given CpuInfo's frequency. +/// +/// Returns `None` if the frequency is unknown (0). +#[inline] +pub fn us_to_ticks(us: u64) -> Option { + let freq = frequency::(); + if freq == 0 { + return None; + } + Some(((freq as u128 * us as u128) / 1_000_000) as u64) +} + +/// Spins until at least `timeout_us` microseconds have elapsed. +/// +/// Returns `true` when the provided `condition` closure returns `true` +/// before the deadline, or `false` on timeout. +/// +/// If the performance frequency is unknown, falls back to a conservative +/// iteration-count heuristic (`timeout_us * 10` loops). +pub fn spin_until(timeout_us: u64, mut condition: F) -> bool +where + F: FnMut() -> bool, +{ + if let Some(deadline_ticks) = us_to_ticks::(timeout_us) { + let start = ticks(); + loop { + if condition() { + return true; + } + if ticks().wrapping_sub(start) >= deadline_ticks { + return false; + } + core::hint::spin_loop(); + } + } else { + // Fallback: iteration-count approximation. + let iterations = timeout_us.saturating_mul(10); + for _ in 0..iterations { + if condition() { + return true; + } + core::hint::spin_loop(); + } + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct TestCpu; + impl CpuInfo for TestCpu { + fn perf_timer_frequency() -> Option { + None + } + } + + #[test] + fn test_us_to_ticks_basic() { + assert_eq!(us_to_ticks::(1000), None); + } + + #[test] + fn test_spin_until_immediate_true() { + let result = spin_until::(1_000, || true); + assert!(result); + } +} diff --git a/patina_mm_supervisor_core/src/privilege_mgmt/call_gate.rs b/patina_mm_supervisor_core/src/privilege_mgmt/call_gate.rs new file mode 100644 index 000000000..c2529f377 --- /dev/null +++ b/patina_mm_supervisor_core/src/privilege_mgmt/call_gate.rs @@ -0,0 +1,208 @@ +//! Call Gate and TSS Management +//! +//! This module manages call gates and Task State Segment (TSS) descriptors +//! for privilege level transitions. Call gates provide an alternative mechanism +//! (besides syscall/sysret) for Ring 3 code to transition back to Ring 0. +//! +//! ## Call Gate Usage +//! +//! 1. When invoking a demoted routine, the supervisor sets up a call gate +//! pointing to the return address. +//! +//! 2. The demoted routine in Ring 3 can return to Ring 0 by doing a far call +//! to the call gate selector. +//! +//! 3. The CPU automatically transitions to Ring 0 and jumps to the address +//! in the call gate descriptor. +//! +//! ## TSS Usage +//! +//! The TSS is used to specify the Ring 0 stack pointer (RSP0) that the CPU +//! will load when transitioning from Ring 3 to Ring 0 via an interrupt or +//! call gate. +//! + +#![allow(unsafe_op_in_unsafe_fn)] + +use super::{CALL_GATE_OFFSET, LONG_CS_R0, TSS_DESC_OFFSET, TSS_SEL_OFFSET}; +use core::arch::{asm, global_asm}; +use x86_64::{VirtAddr, structures::tss::TaskStateSegment}; + +global_asm!(include_str!("call_gate_transfer.asm")); + +/// 64-bit Call Gate Descriptor. +/// +/// A call gate allows privilege level transitions through a far call instruction. +#[repr(C, packed)] +#[derive(Debug, Clone, Copy, Default)] +pub struct CallGateDescriptor { + /// Offset bits 15:0 + pub offset_low: u16, + /// Target code segment selector + pub selector: u16, + /// Reserved (must be 0) and IST (bits 2:0) + pub ist: u8, + /// Type (0xC = 64-bit call gate) and DPL + pub type_attr: u8, + /// Offset bits 31:16 + pub offset_mid: u16, + /// Offset bits 63:32 + pub offset_high: u32, + /// Reserved (must be 0) + pub reserved: u32, +} + +impl CallGateDescriptor { + /// Sets the target offset in the descriptor. + pub fn set_offset(&mut self, offset: u64) { + self.offset_low = (offset & 0xFFFF) as u16; + self.offset_mid = ((offset >> 16) & 0xFFFF) as u16; + self.offset_high = ((offset >> 32) & 0xFFFFFFFF) as u32; + } +} + +/// 64-bit TSS Descriptor (16 bytes in 64-bit mode). +#[repr(C, packed)] +#[derive(Debug, Clone, Copy, Default)] +pub struct TssDescriptor { + /// Limit bits 15:0 + pub limit_low: u16, + /// Base bits 15:0 + pub base_low: u16, + /// Base bits 23:16 + pub base_mid_low: u8, + /// Type and attributes + pub type_attr: u8, + /// Limit bits 19:16 and flags + pub limit_flags: u8, + /// Base bits 31:24 + pub base_mid_high: u8, + /// Base bits 63:32 + pub base_high: u32, + /// Reserved + pub reserved: u32, +} + +impl TssDescriptor { + /// Sets the base address in the descriptor. + pub fn set_base(&mut self, base: u64) { + self.base_low = (base & 0xFFFF) as u16; + self.base_mid_low = ((base >> 16) & 0xFF) as u8; + self.base_mid_high = ((base >> 24) & 0xFF) as u8; + self.base_high = ((base >> 32) & 0xFFFFFFFF) as u32; + } +} + +/// GDTR (GDT Register) structure. +#[repr(C, packed)] +#[derive(Debug, Clone, Copy, Default)] +pub struct GdtRegister { + /// Size of the GDT minus 1 + pub limit: u16, + /// Linear address of the GDT + pub base: u64, +} + +/// Gets the current GDT base address by reading the GDTR register. +/// # Safety +/// This function is safe to call as it only reads the GDTR register and does not modify +/// any state. However, it is marked unsafe because it uses inline assembly. +#[cfg(target_arch = "x86_64")] +pub unsafe fn get_current_gdt_base() -> u64 { + // Get current GDT base + let mut gdtr = GdtRegister::default(); + core::arch::asm!( + "sgdt [{}]", + in(reg) &mut gdtr, + options(nostack, preserves_flags) + ); + let gdt_base = gdtr.base; + gdt_base +} + +/// Sets up the call gate for returning from a demoted routine. +/// This function is called from assembly code (InvokeDemotedRoutine). +/// +/// # Arguments +/// +/// * `return_pointer` - Address to jump to when the call gate is invoked +/// +/// # Safety +/// +/// This modifies the GDT. +#[unsafe(no_mangle)] +#[cfg(target_arch = "x86_64")] +pub unsafe extern "efiapi" fn setup_call_gate(return_pointer: u64, cpl0_stack_ptr: u64) { + // Get current GDT base + let gdt_base = get_current_gdt_base(); + + let call_gate_addr = gdt_base + CALL_GATE_OFFSET as u64; + + let tss_desc_addr = gdt_base + TSS_SEL_OFFSET as u64; + let tss_addr = gdt_base + TSS_DESC_OFFSET as u64; + + // Mask page protection on GDT to allow writing to the call gate descriptor + let mut cr4: u64; + // SAFETY: This is safe because we are temporarily disabling page protection + // on the GDT to update the call gate descriptor, which is necessary for the + // call gate setup. We will restore protections after updating. + unsafe { + asm!("mov {}, cr4", out(reg) cr4); + asm!("mov cr4, {}", in(reg) cr4 & !(1 << 7)); // Clear PGE to disable page protection on GDT + }; + + // Now program the call gate descriptor for the return address + let call_gate = call_gate_addr as *mut CallGateDescriptor; + + // Update the call gate offset + let mut desc = core::ptr::read_volatile(call_gate); + desc.set_offset(return_pointer); + desc.selector = LONG_CS_R0; + // Type = 0xC (64-bit call gate), P = 1, DPL = 3 (Ring 3 can call) + desc.type_attr = 0xEC; + core::ptr::write_volatile(call_gate, desc); + + // Then program the TSS descriptor to point to the TSS (which contains the stack pointer for Ring 0) + let tss_desc = tss_desc_addr as *mut TssDescriptor; + let tss = tss_addr as *mut TaskStateSegment; + + // Update TSS descriptor to point to the TSS + let mut desc = core::ptr::read_volatile(tss_desc); + desc.set_base(tss_addr); + core::ptr::write_volatile(tss_desc, desc); + + // Update RSP0 in the TSS + let mut tss_data = core::ptr::read_volatile(tss); + tss_data.privilege_stack_table[0] = VirtAddr::new(cpl0_stack_ptr); + core::ptr::write_volatile(tss, tss_data); + + // Restore GDT read-only protection + unsafe { + asm!("mov cr4, {}", in(reg) cr4); + } + + log::trace!("Call gate set to 0x{:016x}, CPL0 stack pointer set to 0x{:016x}", return_pointer, cpl0_stack_ptr); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_call_gate_descriptor_offset() { + let mut desc = CallGateDescriptor::new(0x12345678_9ABCDEF0, LONG_CS_R0, 3); + assert_eq!(desc.get_offset(), 0x12345678_9ABCDEF0); + + desc.set_offset(0xFEDCBA98_76543210); + assert_eq!(desc.get_offset(), 0xFEDCBA98_76543210); + } + + #[test] + fn test_tss_descriptor_base() { + let mut desc = TssDescriptor::new(0x12345678_9ABCDEF0, 0x1000); + assert_eq!(desc.get_base(), 0x12345678_9ABCDEF0); + + desc.set_base(0xFEDCBA98_76543210); + assert_eq!(desc.get_base(), 0xFEDCBA98_76543210); + } +} diff --git a/patina_mm_supervisor_core/src/privilege_mgmt/call_gate_transfer.asm b/patina_mm_supervisor_core/src/privilege_mgmt/call_gate_transfer.asm new file mode 100644 index 000000000..2458e0801 --- /dev/null +++ b/patina_mm_supervisor_core/src/privilege_mgmt/call_gate_transfer.asm @@ -0,0 +1,323 @@ +#------------------------------------------------------------------------------ +# Copyright 2008 - 2020 ADVANCED MICRO DEVICES, INC. All Rights Reserved. +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +# Module Name: +# +# WriteTr.nasm +# +# Abstract: +# +# Write TR register +# +# Notes: +# +#------------------------------------------------------------------------------ + +.section .data + +.global invoke_demoted_routine +.global setup_call_gate +.global syscall_center + +# .global SetupCpl0MsrStar +# .global RestoreCpl0MsrStar + +.section .text +.align 8 + +# Segments defined in SmiException.nasm +.equ PROTECTED_DS, 0x20 +.equ LONG_CS_R0, 0x38 +.equ LONG_DS_R0, 0x40 +.equ LONG_CS_R3_PH, 0x4B +.equ LONG_DS_R3, 0x53 +.equ LONG_CS_R3, 0x5B +.equ CALL_GATE_OFFSET, 0x63 + +# MSR constants +.equ MSR_IA32_EFER, 0xC0000080 +.equ MSR_IA32_EFER_SCE_MASK, 0x00000001 + +.equ MSR_IA32_STAR, 0xC0000081 +.equ MSR_IA32_LSTAR, 0xC0000082 +.equ MSR_IA32_GS_BASE, 0xC0000101 +.equ MSR_IA32_KERNEL_GS_BASE, 0xC0000102 + + +.macro CHECK_RAX + cmp rax, 0 + jz 4f +.endm + +#------------------------------------------------------------------------------ +# /** +# Invoke specified routine on specified core in CPL 3. +# +# @param[in] CpuIndex CpuIndex value of intended core, cannot be +# greater than mNumberOfCpus. +# @param[in] Cpl3Routine Function pointer to demoted routine. +# @param[in] ArgCount Number of arguments needed by Cpl3Routine. +# @param ... The variable argument list whose count is defined by +# ArgCount. Its contented will be accessed and populated +# to the registers and/or CPL3 stack areas per EFIAPI +# calling convention. +# +# @retval EFI_SUCCESS The demoted routine returns successfully. +# @retval Others Errors caught by subroutines during ring transitioning +# or error code returned from demoted routine. +# **/ +# EFI_STATUS +# EFIAPI +# InvokeDemotedRoutine ( +# IN UINTN CpuIndex, +# IN EFI_PHYSICAL_ADDRESS Cpl3Routine, +# IN EFI_PHYSICAL_ADDRESS Cpl3Stack, +# IN UINTN ArgCount, +# ... +# ); +# Calling convention: Arg0 in RCX, Arg1 in RDX, Arg2 in R8, Arg3 in R9, more on the stack +#------------------------------------------------------------------------------ +invoke_demoted_routine: + #Preserve input parameters onto reg parameter stack area for later usage + mov [rsp + 0x20], r9 + mov [rsp + 0x18], r8 + mov [rsp + 0x10], rdx + mov [rsp + 0x08], rcx + + #Preserve nonvolatile registers, in case demoted routines mess with them + push rbp + mov rbp, rsp + #Clear the lowest 16 bit after saving rsp, to make sure the stack pointer 16byte aligned + and rsp, -16 + + push rbx + push rdi + push rsi + push r12 + push r13 + push r14 + push r15 + + #Preserve the updated rbp as we need them on return + push rbp + + mov r15, r8 + and r15, -16 + + # Set up the MSR STAR, LSTAR, EFER, GS_BASE and KERNEL_GS_BASE, in situ + mov rcx, MSR_IA32_STAR + rdmsr + push rdx + push rax + + mov edx, LONG_CS_R3_PH + shl edx, 16 + add edx, LONG_CS_R0 + wrmsr + + mov rcx, MSR_IA32_LSTAR + rdmsr + push rdx + push rax + + lea rax, syscall_center + lea rdx, syscall_center + shr rdx, 32 + wrmsr + + mov rcx, MSR_IA32_EFER + rdmsr + push rdx + push rax + + or rax, MSR_IA32_EFER_SCE_MASK + wrmsr + + mov rcx, MSR_IA32_GS_BASE + rdmsr + push rdx + push rax + + xor rdx, rdx + xor rax, rax + wrmsr + + mov rcx, MSR_IA32_KERNEL_GS_BASE + rdmsr + push rdx + push rax + + mov eax, esp + sub eax, 16 + mov rdx, rsp + sub rdx, 16 + shr rdx, 32 + wrmsr + + # This is to do the GS trick upon syscall entry + sub rsp, 8 + + # This is to do the GS trick upon syscall entry + mov rdx, rsp + sub rdx, 8 + push rdx + + # Now the stack will look like + # Current RSP <- Incoming calls will operate on top of this + # 0 <- Will be used for user stack saving + # KERNEL_GS_BASE * 2 <- Will be restored on return + # GS_BASE * 2 <- Will be restored on return + # EFER <- Will be restored on return + # LSTAR * 2 <- Will be restored on return + # STAR * 2 <- Will be restored on return + # One version of RBP <- Value after we pushed NV registers + # r15 + # r14 + # r13 + # r12 + # rsi + # rdi + # rbx + # ? <- Potential buffer for unaligned incoming caller + # Original RBP + # --------------- <- RSP When the caller invokes this + # rcx + # rdx + # r8 + # r9 + + #Setup call gate for return + lea rcx, [rip + 5f] + mov rdx, rsp + sub rsp, 0x20 + call setup_call_gate + add rsp, 0x20 + + #Same level far return to apply GDT change + xor rcx, rcx + mov rcx, cs + push rcx #prepare cs on the stack + lea rax, [rip + 2f] + push rax #prepare return rip on the stack + retfq + +2: + #Prepare for ds, es, fs, gs + xor rax, rax + mov ax, LONG_DS_R3 + mov ds, ax + mov es, ax + mov fs, ax + mov gs, ax + + #Prepare input arguments + mov rax, [rbp + 0x28] #Get ArgCount from stack + CHECK_RAX + mov rcx, [rbp + 0x30] #First input argument for demoted routine + dec rax + CHECK_RAX + mov rdx, [rbp + 0x38] #Second input argument for demoted routine + dec rax + CHECK_RAX + mov r8, [rbp + 0x40] #Third input argument for demoted routine + dec rax + CHECK_RAX + mov r9, [rbp + 0x48] #Forth input argument for demoted routine + dec rax + CHECK_RAX + #For further input arguments, they will be put on the stack + xor rbx, rbx #rbx=0 + mov r14, rax + shl r14, 3 #r14=8*rax + sub r15, r14 #r15-=r14, offset the stack for remainder of input arguments + sub r15, 0x20 #r15-=0x20, 4 stack parameters + and r15, -16 #finally we worry about the stack alignment in CPL3 +3: + mov r14, [rbp + 0x48 + rbx] #r14=*(rbp+0x48+rbx) + mov [r15 + 0x20 + rbx], r14 #*(r15+0x20+rbx)=r14 + add rbx, 0x08 #rbx+=0x08 + dec rax + CHECK_RAX + jmp 3b + +4: + #Demote to CPL3 by far return, it will take care of cs and ss + #Note: we did more pushes on the way, so need to compensate the calculation when grabbing earlier pushed values + sub r15, 0x08 #dummy r15 displacement, to mimic the return pointer on the stack + push LONG_DS_R3 #prepare ss on the stack + mov rax, r15 #grab Cpl3StackPtr from r15 + push rax #prepare CPL3 stack pointer on the stack + push LONG_CS_R3 #prepare cs on the stack + mov rax, [rbp + 0x18] #grab routine pointer from stack + push rax #prepare routine pointer on the stack + + mov r15, CALL_GATE_OFFSET #This is our way to come back, do not mess it up + shl r15, 32 #Call gate on call far stack should be CS:rIP + + retfq + + #2000 years later... + +5: + #First offset the return far related 4 pushes (we have 0 count of arguments): + #PUSH.v old_SS // #SS on this or next pushes use SS.sel as error code + #PUSH.v old_RSP + #PUSH.v old_CS + #PUSH.v next_RIP + add rsp, 0x20 + + #Demoted routine is responsible for returning to this point by invoking call gate + #Return status should still be in rax, save it before calling other functions + push rax + + add rsp, 24 + + pop rax + pop rdx + mov rcx, MSR_IA32_KERNEL_GS_BASE + wrmsr + + pop rax + pop rdx + mov rcx, MSR_IA32_GS_BASE + wrmsr + + pop rax + pop rdx + mov rcx, MSR_IA32_EFER + wrmsr + + pop rax + pop rdx + mov rcx, MSR_IA32_LSTAR + wrmsr + + pop rax + pop rdx + mov rcx, MSR_IA32_STAR + wrmsr + + mov rax, [rsp - 13 * 8] + + xor rcx, rcx + mov cx, LONG_DS_R0 + mov ds, cx + mov es, cx + mov fs, cx + mov gs, cx + + add rsp, 0x08 #Unwind the rbp from the last net-push + #Unwind the rest of the pushes + pop r15 + pop r14 + pop r13 + pop r12 + pop rsi + pop rdi + pop rbx + mov rsp, rbp + pop rbp + + ret diff --git a/patina_mm_supervisor_core/src/privilege_mgmt/mod.rs b/patina_mm_supervisor_core/src/privilege_mgmt/mod.rs new file mode 100644 index 000000000..80a780c8c --- /dev/null +++ b/patina_mm_supervisor_core/src/privilege_mgmt/mod.rs @@ -0,0 +1,213 @@ +//! Privilege Management for MM Supervisor Core +//! +//! This module manages the privilege level transitions between Ring 0 (supervisor) +//! and Ring 3 (user) in the MM environment. It provides: +//! +//! - One-time initialization of syscall/sysret MSRs +//! - Demotion of code execution to Ring 3 via `InvokeDemotedRoutine` +//! - Handling of syscall requests from Ring 3 code +//! - Call gate and TSS descriptor management for privilege transitions +//! +//! ## Architecture +//! +//! The privilege management follows the x86_64 syscall/sysret model: +//! +//! 1. **Initialization**: Configure MSR_IA32_STAR, MSR_IA32_LSTAR, MSR_IA32_EFER +//! to set up syscall entry points and segment selectors. +//! +//! 2. **Demotion**: Use `InvokeDemotedRoutine` to transition from Ring 0 to Ring 3. +//! This sets up call gates for return and prepares the Ring 3 stack. +//! +//! 3. **Syscall Entry**: When Ring 3 code executes `syscall`, the CPU jumps to +//! the address in MSR_IA32_LSTAR (our `SyscallCenter`), which dispatches +//! to the appropriate handler. +//! +//! 4. **Return**: Ring 3 code returns via call gate or syscall dispatcher returns +//! via `sysret`. +//! +//! ## Segment Layout (from SmiException.nasm) +//! +//! ```text +//! PROTECTED_DS = 0x20 +//! LONG_CS_R0 = 0x38 (Ring 0 code segment) +//! LONG_DS_R0 = 0x40 (Ring 0 data segment) +//! LONG_CS_R3_PH = 0x4B (Ring 3 code segment placeholder) +//! LONG_DS_R3 = 0x53 (Ring 3 data segment) +//! LONG_CS_R3 = 0x5B (Ring 3 code segment) +//! CALL_GATE_OFFSET = 0x60 (Call gate descriptor offset) +//! TSS_SEL_OFFSET = 0x70 (TSS selector offset) +//! TSS_DESC_OFFSET = 0x80 (TSS descriptor offset) +//! ``` +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +mod call_gate; +mod syscall_dispatcher; +mod syscall_setup; + +pub use syscall_dispatcher::{SyscallDispatcher, SyscallIndex, SyscallResult}; +pub use syscall_setup::{SyscallInterface, SyscallSetupError}; + +unsafe extern "efiapi" { + /// Invokes a specified routine in CPL 3 (Ring 3). + /// + /// This function transitions from Ring 0 to Ring 3, executes the demoted routine, + /// and returns back to Ring 0 through a call gate. + /// + /// # Arguments + /// + /// * `cpu_index` - CPU index value of the intended core + /// * `cpl3_routine` - Function pointer to the demoted routine + /// * `cpl3_stack` - Stack pointer for Ring 3 execution + /// * `arg_count` - Number of arguments needed by the demoted routine + /// * `...` - Variable argument list (count defined by `arg_count`), populated + /// to registers and/or CPL3 stack areas per EFIAPI calling convention + /// + /// # Returns + /// + /// * `EFI_SUCCESS` - The demoted routine returned successfully + /// * Other values - Errors from ring transitioning or the demoted routine + /// + /// # Safety + /// + /// This function modifies privilege levels and stack pointers. Callers must ensure: + /// - Valid function pointer for `cpl3_routine` + /// - Valid stack pointer for `cpl3_stack` + /// - Correct `arg_count` matching the actual arguments + pub fn invoke_demoted_routine(cpu_index: usize, cpl3_routine: u64, cpl3_stack: u64, arg_count: usize, ...) + -> usize; +} + +/// Protected mode data segment selector. +pub const PROTECTED_DS: u16 = 0x20; + +/// Long mode Ring 0 code segment selector. +pub const LONG_CS_R0: u16 = 0x38; + +/// Long mode Ring 0 data segment selector. +pub const LONG_DS_R0: u16 = 0x40; + +/// Long mode Ring 3 code segment placeholder (for STAR MSR). +pub const LONG_CS_R3_PH: u16 = 0x4B; + +/// Long mode Ring 3 data segment selector. +pub const LONG_DS_R3: u16 = 0x53; + +/// Long mode Ring 3 code segment selector. +pub const LONG_CS_R3: u16 = 0x5B; + +/// Call gate descriptor offset in GDT. +pub const CALL_GATE_OFFSET: u16 = 0x60; + +/// TSS selector offset in GDT. +pub const TSS_SEL_OFFSET: u16 = 0x70; + +/// TSS descriptor offset in GDT. +pub const TSS_DESC_OFFSET: u16 = 0x80; + +/// MSR_IA32_STAR - System Call Target Address Register +/// Contains the segment selectors for syscall/sysret. +pub const MSR_IA32_STAR: u32 = 0xC000_0081; + +/// MSR_IA32_LSTAR - Long Mode System Call Target Address Register +/// Contains the RIP for 64-bit syscall entry. +pub const MSR_IA32_LSTAR: u32 = 0xC000_0082; + +/// MSR_IA32_CSTAR - Compatibility Mode System Call Target Address Register +/// Contains the RIP for compatibility mode syscall (not used in pure 64-bit). +pub const MSR_IA32_CSTAR: u32 = 0xC000_0083; + +/// MSR_IA32_SFMASK - System Call Flag Mask +/// Contains flags to be cleared on syscall. +pub const MSR_IA32_SFMASK: u32 = 0xC000_0084; + +/// MSR_IA32_EFER - Extended Feature Enable Register +pub const MSR_IA32_EFER: u32 = 0xC000_0080; + +/// MSR_IA32_GS_BASE - GS Base Address Register +pub const MSR_IA32_GS_BASE: u32 = 0xC000_0101; + +/// MSR_IA32_KERNEL_GS_BASE - Kernel GS Base Address Register (swapped on swapgs) +pub const MSR_IA32_KERNEL_GS_BASE: u32 = 0xC000_0102; + +/// EFER.SCE - System Call Enable bit +pub const EFER_SCE: u64 = 1 << 0; + +/// EFER.LME - Long Mode Enable bit +pub const EFER_LME: u64 = 1 << 8; + +/// EFER.LMA - Long Mode Active bit +pub const EFER_LMA: u64 = 1 << 10; + +/// EFER.NXE - No-Execute Enable bit +pub const EFER_NXE: u64 = 1 << 11; + +/// Current privilege level. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum PrivilegeLevel { + /// Ring 0 - Supervisor/Kernel mode + Ring0 = 0, + /// Ring 3 - User mode + Ring3 = 3, +} + +impl PrivilegeLevel { + /// Returns the numeric value of the privilege level. + pub fn as_u8(self) -> u8 { + self as u8 + } + + /// Creates a PrivilegeLevel from a numeric value. + pub fn from_u8(value: u8) -> Option { + match value { + 0 => Some(Self::Ring0), + 3 => Some(Self::Ring3), + _ => None, + } + } +} + +/// Result type for privilege management operations. +pub type PrivilegeResult = Result; + +/// Errors that can occur during privilege management operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PrivilegeError { + /// The privilege management system has not been initialized. + NotInitialized, + /// Already initialized (cannot re-initialize). + AlreadyInitialized, + /// Invalid CPU index. + InvalidCpuIndex, + /// Out of resources (memory allocation failed). + OutOfResources, + /// The operation is not ready (missing prerequisite). + NotReady, + /// Invalid parameter provided. + InvalidParameter, + /// Security violation detected. + SecurityViolation, + /// The syscall index is not supported. + UnsupportedSyscall, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_privilege_level() { + assert_eq!(PrivilegeLevel::Ring0.as_u8(), 0); + assert_eq!(PrivilegeLevel::Ring3.as_u8(), 3); + assert_eq!(PrivilegeLevel::from_u8(0), Some(PrivilegeLevel::Ring0)); + assert_eq!(PrivilegeLevel::from_u8(3), Some(PrivilegeLevel::Ring3)); + assert_eq!(PrivilegeLevel::from_u8(1), None); + assert_eq!(PrivilegeLevel::from_u8(2), None); + } +} diff --git a/patina_mm_supervisor_core/src/privilege_mgmt/syscall_dispatcher.rs b/patina_mm_supervisor_core/src/privilege_mgmt/syscall_dispatcher.rs new file mode 100644 index 000000000..025308028 --- /dev/null +++ b/patina_mm_supervisor_core/src/privilege_mgmt/syscall_dispatcher.rs @@ -0,0 +1,849 @@ +//! Syscall Dispatcher +//! +//! This module handles syscall requests from Ring 3 code. When Ring 3 code +//! executes the `syscall` instruction, the CPU jumps to the address in +//! MSR_IA32_LSTAR (our SyscallCenter assembly stub), which then calls into +//! this dispatcher. +//! +//! ## Syscall Interface +//! +//! The syscall uses a custom calling convention: +//! - RAX: Call index (SyscallIndex) +//! - RDX: Argument 1 +//! - R8: Argument 2 +//! - R9: Argument 3 +//! - RCX: Caller return address (set by syscall instruction) +//! - R11: RFLAGS (set by syscall instruction) +//! +//! The dispatcher validates the request and dispatches to the appropriate handler. +//! +//! ## Security +//! +//! All syscall handlers must validate their arguments and check that any +//! memory pointers are within valid user-accessible regions. +//! + +use core::arch::{asm, global_asm}; +use core::sync::atomic::{AtomicBool, Ordering}; +use r_efi::efi::{ALLOCATE_ANY_PAGES, AllocateType, MemoryType, RUNTIME_SERVICES_DATA}; + +use patina::base::UEFI_PAGE_SIZE; +use patina_mm_policy::{AccessType, Instruction, IoWidth}; + +use super::{PrivilegeError, PrivilegeResult}; +use crate::{COMM_BUFFER_CONFIG, POLICY_GATE, PageOwnership, UNBLOCKED_MEMORY_TRACKER, query_address_ownership}; + +global_asm!(include_str!("syscall_entry.asm")); + +/// MM_IO_UINT8 - 8-bit I/O access width. +const MM_IO_UINT8: u64 = 0; +/// MM_IO_UINT16 - 16-bit I/O access width. +const MM_IO_UINT16: u64 = 1; +/// MM_IO_UINT32 - 32-bit I/O access width. +const MM_IO_UINT32: u64 = 2; + +/// Converts an EFI_MM_IO_WIDTH enum value to our [`IoWidth`] type. +/// +/// The EFI spec defines: MM_IO_UINT8=0, MM_IO_UINT16=1, MM_IO_UINT32=2. +fn efi_io_width_to_io_width(width: u64) -> Option { + match width { + MM_IO_UINT8 => Some(IoWidth::Byte), + MM_IO_UINT16 => Some(IoWidth::Word), + MM_IO_UINT32 => Some(IoWidth::Dword), + _ => None, + } +} + +/// Reads an 8-bit value from an I/O port. +/// +/// # Safety +/// +/// The caller must ensure the port address is valid and access is allowed by policy. +#[inline] +unsafe fn io_read_u8(port: u16) -> u8 { + let value: u8; + unsafe { + asm!("in al, dx", out("al") value, in("dx") port, options(nomem, nostack)); + } + value +} + +/// Reads a 16-bit value from an I/O port. +/// +/// # Safety +/// +/// The caller must ensure the port address is valid and access is allowed by policy. +#[inline] +unsafe fn io_read_u16(port: u16) -> u16 { + let value: u16; + unsafe { + asm!("in ax, dx", out("ax") value, in("dx") port, options(nomem, nostack)); + } + value +} + +/// Reads a 32-bit value from an I/O port. +/// +/// # Safety +/// +/// The caller must ensure the port address is valid and access is allowed by policy. +#[inline] +unsafe fn io_read_u32(port: u16) -> u32 { + let value: u32; + unsafe { + asm!("in eax, dx", out("eax") value, in("dx") port, options(nomem, nostack)); + } + value +} + +/// Writes an 8-bit value to an I/O port. +/// +/// # Safety +/// +/// The caller must ensure the port address is valid and access is allowed by policy. +#[inline] +unsafe fn io_write_u8(port: u16, value: u8) { + unsafe { + asm!("out dx, al", in("dx") port, in("al") value, options(nomem, nostack)); + } +} + +/// Writes a 16-bit value to an I/O port. +/// +/// # Safety +/// +/// The caller must ensure the port address is valid and access is allowed by policy. +#[inline] +unsafe fn io_write_u16(port: u16, value: u16) { + unsafe { + asm!("out dx, ax", in("dx") port, in("ax") value, options(nomem, nostack)); + } +} + +/// Writes a 32-bit value to an I/O port. +/// +/// # Safety +/// +/// The caller must ensure the port address is valid and access is allowed by policy. +#[inline] +unsafe fn io_write_u32(port: u16, value: u32) { + unsafe { + asm!("out dx, eax", in("dx") port, in("eax") value, options(nomem, nostack)); + } +} + +pub use patina_internal_mm_common::SyscallIndex; + +/// Result of a syscall operation. +#[derive(Debug, Clone, Copy)] +pub struct SyscallResult { + /// Return value (in RAX on return to Ring 3). + pub value: u64, + /// Status code (EFI_STATUS compatible). + pub status: u64, +} + +impl SyscallResult { + /// Creates a successful result with a value. + pub const fn success(value: u64) -> Self { + Self { value, status: 0 } + } + + /// Creates an error result. + pub const fn error(status: u64) -> Self { + Self { value: 0, status } + } + + /// EFI_SUCCESS + pub const EFI_SUCCESS: u64 = 0; + /// EFI_INVALID_PARAMETER + pub const EFI_INVALID_PARAMETER: u64 = 0x8000_0000_0000_0002; + /// EFI_UNSUPPORTED + pub const EFI_UNSUPPORTED: u64 = 0x8000_0000_0000_0003; + /// EFI_ACCESS_DENIED + pub const EFI_ACCESS_DENIED: u64 = 0x8000_0000_0000_000F; + /// EFI_NOT_READY + pub const EFI_NOT_READY: u64 = 0x8000_0000_0000_0006; + /// EFI_OUT_OF_RESOURCES + pub const EFI_OUT_OF_RESOURCES: u64 = 0x8000_0000_0000_0009; + /// EFI_SECURITY_VIOLATION + pub const EFI_SECURITY_VIOLATION: u64 = 0x8000_0000_0000_001A; +} + +/// Context for a syscall invocation. +#[derive(Debug, Clone, Copy)] +pub struct SyscallContext { + /// The syscall index (from RAX). + pub call_index: u64, + /// First argument (from RDX). + pub arg1: u64, + /// Second argument (from R8). + pub arg2: u64, + /// Third argument (from R9). + pub arg3: u64, + /// Caller return address (from RCX, set by syscall instruction). + pub caller_addr: u64, + /// Ring 3 stack pointer at syscall entry. + pub ring3_stack_ptr: u64, +} + +/// The syscall dispatcher handles incoming syscalls from Ring 3. +pub struct SyscallDispatcher { + /// Whether the dispatcher has been initialized. + initialized: AtomicBool, +} + +impl SyscallDispatcher { + /// Creates a new syscall dispatcher. + pub const fn new() -> Self { + Self { initialized: AtomicBool::new(false) } + } + + /// Initializes the syscall dispatcher. + pub fn init(&self) -> PrivilegeResult<()> { + if self.initialized.swap(true, Ordering::SeqCst) { + return Err(PrivilegeError::AlreadyInitialized); + } + + log::info!("SyscallDispatcher initialized"); + Ok(()) + } + + /// Checks if the dispatcher is initialized. + pub fn is_initialized(&self) -> bool { + self.initialized.load(Ordering::Acquire) + } + + /// Dispatches a syscall. + /// + /// This is the main entry point called from the assembly syscall handler. + /// It validates the syscall index and dispatches to the appropriate handler. + /// + /// # Arguments + /// + /// * `ctx` - The syscall context containing all arguments + /// + /// # Returns + /// + /// The result to be returned to Ring 3 in RAX. + pub fn dispatch(&self, ctx: &SyscallContext) -> SyscallResult { + // Parse the syscall index + let index = match SyscallIndex::from_u64(ctx.call_index) { + Some(idx) => idx, + None => { + log::warn!("Unknown syscall index: 0x{:x}", ctx.call_index); + return SyscallResult::error(SyscallResult::EFI_UNSUPPORTED); + } + }; + + log::trace!( + "Syscall: {:?} (0x{:x}), args: 0x{:x}, 0x{:x}, 0x{:x}", + index, + ctx.call_index, + ctx.arg1, + ctx.arg2, + ctx.arg3 + ); + + // Dispatch to the appropriate handler + match index { + SyscallIndex::RdMsr => self.handle_rdmsr(ctx), + SyscallIndex::WrMsr => self.handle_wrmsr(ctx), + SyscallIndex::Cli => self.handle_cli(ctx), + SyscallIndex::IoRead => self.handle_io_read(ctx), + SyscallIndex::IoWrite => self.handle_io_write(ctx), + SyscallIndex::Wbinvd => self.handle_wbinvd(ctx), + SyscallIndex::Hlt => self.handle_hlt(ctx), + SyscallIndex::SaveStateRead => self.handle_save_state_read(ctx), + SyscallIndex::LegacyMax => panic!("Invalid syscall index: LegacyMax is not a real syscall"), + SyscallIndex::AllocPage => self.handle_alloc_page(ctx), + SyscallIndex::FreePage => self.handle_free_page(ctx), + SyscallIndex::StartApProc => self.handle_start_ap_proc(ctx), + SyscallIndex::SaveStateRead2 => self.handle_save_state_read2(ctx), + SyscallIndex::MmMemoryUnblocked => self.handle_mm_memory_unblocked(ctx), + SyscallIndex::MmIsCommBuffer => self.handle_mm_is_comm_buffer(ctx), + } + } + + /// Handles MSR read syscall. + /// + /// Validates the MSR read against firmware policy, then executes `rdmsr`. + /// - Arg1: MSR index + /// - Returns: MSR value in result.value + fn handle_rdmsr(&self, ctx: &SyscallContext) -> SyscallResult { + let msr_index = ctx.arg1 as u32; + log::trace!("RDMSR: msr=0x{:x}", msr_index); + + // Validate against policy + let gate = match POLICY_GATE.get() { + Some(g) => g, + None => { + log::error!("RDMSR: Policy gate not initialized"); + return SyscallResult::error(SyscallResult::EFI_NOT_READY); + } + }; + + if let Err(e) = gate.is_msr_allowed(msr_index, AccessType::Read) { + log::error!("RDMSR: MSR 0x{:x} blocked by policy: {:?}", msr_index, e); + return SyscallResult::error(SyscallResult::EFI_ACCESS_DENIED); + } + + // Policy allows - execute the MSR read + let value = unsafe { crate::cpu::read_msr(msr_index) }.unwrap_or_else(|e| { + log::error!("RDMSR: rdmsr failed: {}", e); + 0 + }); + log::debug!("RDMSR: MSR 0x{:x} = 0x{:x}", msr_index, value); + SyscallResult::success(value) + } + + /// Handles MSR write syscall. + /// + /// Validates the MSR write against firmware policy, then executes `wrmsr`. + /// - Arg1: MSR index + /// - Arg2: Value to write + fn handle_wrmsr(&self, ctx: &SyscallContext) -> SyscallResult { + let msr_index = ctx.arg1 as u32; + let value = ctx.arg2; + log::trace!("WRMSR: msr=0x{:x}, value=0x{:x}", msr_index, value); + + // Validate against policy + let gate = match POLICY_GATE.get() { + Some(g) => g, + None => { + log::error!("WRMSR: Policy gate not initialized"); + return SyscallResult::error(SyscallResult::EFI_NOT_READY); + } + }; + + if let Err(e) = gate.is_msr_allowed(msr_index, AccessType::Write) { + log::error!("WRMSR: MSR 0x{:x} blocked by policy: {:?}", msr_index, e); + return SyscallResult::error(SyscallResult::EFI_ACCESS_DENIED); + } + + // Policy allows - execute the MSR write + if let Err(e) = unsafe { crate::cpu::write_msr(msr_index, value) } { + log::error!("WRMSR: wrmsr failed: {}", e); + return SyscallResult::error(SyscallResult::EFI_UNSUPPORTED); + } + log::debug!("WRMSR: MSR 0x{:x} written with 0x{:x}", msr_index, value); + SyscallResult::success(0) + } + + /// Handles CLI (clear interrupt flag) syscall. + /// + /// Validates the CLI instruction against firmware policy, then executes `cli`. + fn handle_cli(&self, _ctx: &SyscallContext) -> SyscallResult { + log::trace!("CLI"); + + // Validate against policy + let gate = match POLICY_GATE.get() { + Some(g) => g, + None => { + log::error!("CLI: Policy gate not initialized"); + return SyscallResult::error(SyscallResult::EFI_NOT_READY); + } + }; + + if let Err(e) = gate.is_instruction_allowed(Instruction::Cli) { + log::error!("CLI: Instruction blocked by policy: {:?}", e); + return SyscallResult::error(SyscallResult::EFI_ACCESS_DENIED); + } + + // Policy allows - disable interrupts + unsafe { asm!("cli", options(nomem, nostack)) }; + log::debug!("CLI: Interrupts disabled"); + SyscallResult::success(0) + } + + /// Handles I/O port read syscall. + /// + /// Validates the I/O read against firmware policy, then executes the `in` instruction. + /// - Arg1: I/O port address + /// - Arg2: EFI_MM_IO_WIDTH (0=UINT8, 1=UINT16, 2=UINT32) + /// - Returns: Value read from the port in result.value + fn handle_io_read(&self, ctx: &SyscallContext) -> SyscallResult { + let port = ctx.arg1; + let efi_width = ctx.arg2; + log::trace!("IO_READ: port=0x{:x}, width={}", port, efi_width); + + // Convert EFI_MM_IO_WIDTH to IoWidth + let io_width = match efi_io_width_to_io_width(efi_width) { + Some(w) => w, + None => { + log::error!("IO_READ: Invalid IO width: {}", efi_width); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + }; + + // Validate against policy + let gate = match POLICY_GATE.get() { + Some(g) => g, + None => { + log::error!("IO_READ: Policy gate not initialized"); + return SyscallResult::error(SyscallResult::EFI_NOT_READY); + } + }; + + if let Err(e) = gate.is_io_allowed(port as u32, io_width, AccessType::Read) { + log::error!("IO_READ: Port 0x{:x} width {:?} blocked by policy: {:?}", port, io_width, e); + return SyscallResult::error(SyscallResult::EFI_ACCESS_DENIED); + } + + // Policy allows - execute the I/O read + let port_addr = port as u16; + let value: u64 = unsafe { + match efi_width { + MM_IO_UINT8 => io_read_u8(port_addr) as u64, + MM_IO_UINT16 => io_read_u16(port_addr) as u64, + MM_IO_UINT32 => io_read_u32(port_addr) as u64, + _ => unreachable!(), // Already validated above + } + }; + + log::debug!("IO_READ: port=0x{:x} => 0x{:x}", port, value); + SyscallResult::success(value) + } + + /// Handles I/O port write syscall. + /// + /// Validates the I/O write against firmware policy, then executes the `out` instruction. + /// - Arg1: I/O port address + /// - Arg2: EFI_MM_IO_WIDTH (0=UINT8, 1=UINT16, 2=UINT32) + /// - Arg3: Value to write + fn handle_io_write(&self, ctx: &SyscallContext) -> SyscallResult { + let port = ctx.arg1; + let efi_width = ctx.arg2; + let value = ctx.arg3; + log::trace!("IO_WRITE: port=0x{:x}, width={}, value=0x{:x}", port, efi_width, value); + + // Convert EFI_MM_IO_WIDTH to IoWidth + let io_width = match efi_io_width_to_io_width(efi_width) { + Some(w) => w, + None => { + log::error!("IO_WRITE: Invalid IO width: {}", efi_width); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + }; + + // Validate against policy + let gate = match POLICY_GATE.get() { + Some(g) => g, + None => { + log::error!("IO_WRITE: Policy gate not initialized"); + return SyscallResult::error(SyscallResult::EFI_NOT_READY); + } + }; + + if let Err(e) = gate.is_io_allowed(port as u32, io_width, AccessType::Write) { + log::error!("IO_WRITE: Port 0x{:x} width {:?} blocked by policy: {:?}", port, io_width, e); + return SyscallResult::error(SyscallResult::EFI_ACCESS_DENIED); + } + + // Policy allows - execute the I/O write + let port_addr = port as u16; + unsafe { + match efi_width { + MM_IO_UINT8 => io_write_u8(port_addr, value as u8), + MM_IO_UINT16 => io_write_u16(port_addr, value as u16), + MM_IO_UINT32 => io_write_u32(port_addr, value as u32), + _ => unreachable!(), // Already validated above + } + } + + log::debug!("IO_WRITE: port=0x{:x} <= 0x{:x}", port, value); + SyscallResult::success(0) + } + + /// Handles WBINVD (write-back and invalidate cache) syscall. + /// + /// Validates the WBINVD instruction against firmware policy, then executes `wbinvd`. + fn handle_wbinvd(&self, _ctx: &SyscallContext) -> SyscallResult { + log::trace!("WBINVD"); + + // Validate against policy + let gate = match POLICY_GATE.get() { + Some(g) => g, + None => { + log::error!("WBINVD: Policy gate not initialized"); + return SyscallResult::error(SyscallResult::EFI_NOT_READY); + } + }; + + if let Err(e) = gate.is_instruction_allowed(Instruction::Wbinvd) { + log::error!("WBINVD: Instruction blocked by policy: {:?}", e); + return SyscallResult::error(SyscallResult::EFI_ACCESS_DENIED); + } + + // Policy allows - write back and invalidate cache + unsafe { asm!("wbinvd", options(nomem, nostack)) }; + log::debug!("WBINVD: Cache written back and invalidated"); + SyscallResult::success(0) + } + + /// Handles HLT (halt processor) syscall. + /// + /// Validates the HLT instruction against firmware policy, then executes `hlt`. + fn handle_hlt(&self, _ctx: &SyscallContext) -> SyscallResult { + log::trace!("HLT"); + + // Validate against policy + let gate = match POLICY_GATE.get() { + Some(g) => g, + None => { + log::error!("HLT: Policy gate not initialized"); + return SyscallResult::error(SyscallResult::EFI_NOT_READY); + } + }; + + if let Err(e) = gate.is_instruction_allowed(Instruction::Hlt) { + log::error!("HLT: Instruction blocked by policy: {:?}", e); + return SyscallResult::error(SyscallResult::EFI_ACCESS_DENIED); + } + + // Policy allows - halt processor (sleep until next interrupt) + unsafe { asm!("hlt", options(nomem, nostack)) }; + log::debug!("HLT: Processor halted and resumed"); + SyscallResult::success(0) + } + + /// Handles save state read syscall (legacy). + /// + /// - Arg1: User MM CPU protocol pointer + /// - Arg2: Register to be read (`EFI_MM_SAVE_STATE_REGISTER`) + /// - Arg3: CPU index to read from + fn handle_save_state_read(&self, ctx: &SyscallContext) -> SyscallResult { + log::trace!("SAVE_STATE_READ: protocol=0x{:x}, register={}, cpu={}", ctx.arg1, ctx.arg2, ctx.arg3); + + // Validate parameters + if ctx.arg1 == 0 { + log::error!("SAVE_STATE_READ: Null protocol pointer"); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + + // Delegate to save state module Phase 1 + crate::save_state::save_state_read_phase1(ctx.arg1, ctx.arg2, ctx.arg3) + } + + /// Handles page allocation syscall. + /// + /// - Arg1: Allocate type (EFI_ALLOCATE_TYPE) + /// - Arg2: Memory type (must be EfiRuntimeServicesData) + /// - Arg3: Page count + /// - Returns: Allocated physical address in result.value + fn handle_alloc_page(&self, ctx: &SyscallContext) -> SyscallResult { + let alloc_type = ctx.arg1 as AllocateType; + let mem_type = ctx.arg2 as MemoryType; + let page_count = ctx.arg3; + log::trace!("ALLOC_PAGE: alloc_type={}, mem_type={}, count={}", alloc_type, mem_type, page_count); + + // Only BSP can allocate pages (AP allocating involves page table updates) + if !crate::is_bsp() { + log::error!("ALLOC_PAGE: AP cannot allocate pages"); + return SyscallResult::error(SyscallResult::EFI_ACCESS_DENIED); + } + + if mem_type != RUNTIME_SERVICES_DATA { + log::error!("ALLOC_PAGE: Invalid memory type: {}", mem_type); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + + // Currently only AllocateAnyPages is supported by our page allocator + if alloc_type != ALLOCATE_ANY_PAGES { + log::error!("ALLOC_PAGE: Only AllocateAnyPages (0) is supported, got {}", alloc_type); + return SyscallResult::error(SyscallResult::EFI_UNSUPPORTED); + } + + if page_count == 0 { + log::error!("ALLOC_PAGE: Zero page count"); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + + // Allocate pages as User type (Ring 3 driver request) + match crate::PAGE_ALLOCATOR.allocate_pages_with_type(page_count as usize, crate::mm_mem::AllocationType::User) { + Ok(addr) => { + log::trace!("ALLOC_PAGE: Allocated {} page(s) at 0x{:x}", page_count, addr); + SyscallResult::success(addr) + } + Err(e) => { + log::error!("ALLOC_PAGE: Allocation failed: {:?}", e); + SyscallResult::error(SyscallResult::EFI_OUT_OF_RESOURCES) + } + } + } + + /// Handles page free syscall. + /// + /// Mirrors the C implementation's `SMM_FREE_PAGE` case. + /// - Arg1: Physical address to free + /// - Arg2: Number of pages + fn handle_free_page(&self, ctx: &SyscallContext) -> SyscallResult { + let addr = ctx.arg1; + let page_count = ctx.arg2; + log::trace!("FREE_PAGE: addr=0x{:x}, count={}", addr, page_count); + + if page_count == 0 { + log::error!("FREE_PAGE: Zero page count"); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + + // Validate the address is page-aligned + if addr % UEFI_PAGE_SIZE as u64 != 0 { + log::error!("FREE_PAGE: Address 0x{:x} is not page-aligned", addr); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + + // Verify the range was allocated as User type (Ring 3 code should only free its own memory) + // This prevents user code from freeing supervisor-internal allocations. + match crate::PAGE_ALLOCATOR.get_allocation_type(addr) { + Some(crate::mm_mem::AllocationType::User) => { + // Good - this is user-owned memory + } + Some(crate::mm_mem::AllocationType::Supervisor) => { + log::error!("FREE_PAGE: Address 0x{:x} is a supervisor allocation - access denied", addr); + return SyscallResult::error(SyscallResult::EFI_SECURITY_VIOLATION); + } + None => { + log::error!("FREE_PAGE: Address 0x{:x} is not allocated", addr); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + } + + // Free the pages, verifying they are all User allocations + match crate::PAGE_ALLOCATOR.free_pages_checked(addr, page_count as usize, crate::mm_mem::AllocationType::User) { + Ok(()) => { + log::debug!("FREE_PAGE: Freed {} page(s) at 0x{:x}", page_count, addr); + SyscallResult::success(0) + } + Err(e) => { + log::error!("FREE_PAGE: Free failed: {:?}", e); + SyscallResult::error(SyscallResult::EFI_SECURITY_VIOLATION) + } + } + } + + /// Handles start AP procedure syscall. + /// + /// Validates the request and delegates to the platform-specific AP startup + /// function registered during [`MmSupervisorCore`] initialization. + /// + /// Checks performed before dispatch: + /// - Procedure pointer is non-null + /// - Procedure pointer is within user-accessible memory (unblocked region) + /// - Argument pointer (if non-null) is within user-accessible memory + /// + /// The remaining validation (CPU index range, BSP check, AP busy check) and + /// the actual dispatch are handled by the registered AP startup function, + /// which has access to the CPU manager and mailbox manager. + /// + /// - Arg1: Procedure function pointer + /// - Arg2: CPU index + /// - Arg3: Argument pointer + fn handle_start_ap_proc(&self, ctx: &SyscallContext) -> SyscallResult { + let procedure = ctx.arg1; + let cpu_index = ctx.arg2; + let argument = ctx.arg3; + + log::info!("START_AP_PROC: proc=0x{:x}, cpu={}, arg=0x{:x}", procedure, cpu_index, argument); + + // 1. Validate procedure pointer is non-null + if procedure == 0 { + log::error!("START_AP_PROC: Null procedure pointer"); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + + // 2. Validate procedure pointer is within mapped memory via page table query + if crate::query_address_ownership(procedure, core::mem::size_of::() as u64).is_none() { + log::error!("START_AP_PROC: Procedure 0x{:x} not in mapped memory", procedure); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + + // 3. Validate argument pointer (if non-null) is within mapped memory + if argument != 0 { + if crate::query_address_ownership(argument, core::mem::size_of::() as u64).is_none() { + log::error!("START_AP_PROC: Argument 0x{:x} not in mapped memory", argument); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + } + + // 4. Delegate to the registered AP startup function + match crate::AP_STARTUP_FN.get() { + Some(start_fn) => { + log::info!( + "START_AP_PROC: Dispatching to AP startup function at {:p} for CPU {}", + *start_fn as *const (), + cpu_index + ); + let status = start_fn(cpu_index, procedure, argument); + if status == 0 { SyscallResult::success(0) } else { SyscallResult::error(status) } + } + None => { + log::error!("START_AP_PROC: AP startup not initialized"); + SyscallResult::error(SyscallResult::EFI_NOT_READY) + } + } + } + + /// Handles extended save state read syscall. + /// + /// - Arg1: User MM CPU protocol pointer + /// - Arg2: Width of buffer to read in bytes + /// - Arg3: User buffer to hold return data + fn handle_save_state_read2(&self, ctx: &SyscallContext) -> SyscallResult { + log::trace!("SAVE_STATE_READ2: protocol=0x{:x}, width={}, buffer=0x{:x}", ctx.arg1, ctx.arg2, ctx.arg3); + + // Validate parameters + if ctx.arg1 == 0 { + log::error!("SAVE_STATE_READ2: Null protocol pointer"); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + + // Delegate to save state module Phase 2 + crate::save_state::save_state_read_phase2(ctx.arg1, ctx.arg2, ctx.arg3) + } + + /// Handles MM memory unblocked check syscall. + /// + /// Checks if a memory range is outside MMRAM and valid (unblocked), AND + /// is within user-owned space. + /// - Arg1: Physical address + /// - Arg2: Size in bytes + /// - Returns: 1 (TRUE) if valid, 0 (FALSE) otherwise + fn handle_mm_memory_unblocked(&self, ctx: &SyscallContext) -> SyscallResult { + let addr = ctx.arg1; + let size = ctx.arg2; + log::trace!("MM_MEMORY_UNBLOCKED: addr=0x{:x}, size=0x{:x}", addr, size); + + // Check if the buffer is within an unblocked memory region + let is_valid = UNBLOCKED_MEMORY_TRACKER.is_within_unblocked_region(addr, size); + + if !is_valid { + log::trace!("MM_MEMORY_UNBLOCKED: addr=0x{:x} size=0x{:x} not in unblocked region", addr, size); + return SyscallResult::success(0); // FALSE + } + + // Additional check - verify buffer is in user-owned space + match query_address_ownership(addr, size) { + Some(owner) => { + if owner != PageOwnership::User { + log::trace!( + "MM_MEMORY_UNBLOCKED: addr=0x{:x} size=0x{:x} owned by {:?} - not valid", + addr, + size, + owner + ); + return SyscallResult::success(0); // FALSE + } + } + None => { + log::trace!("MM_MEMORY_UNBLOCKED: addr=0x{:x} size=0x{:x} not in mapped memory", addr, size); + return SyscallResult::success(0); // FALSE + } + } + + log::trace!("MM_MEMORY_UNBLOCKED: addr=0x{:x} size=0x{:x} is valid", addr, size); + SyscallResult::success(1) // TRUE + } + + /// Handles MM is communication buffer check syscall. + /// + /// Verifies that a given memory range is a valid communication buffer. + /// - Arg1: Buffer address + /// - Arg2: Buffer size + /// - Returns: 1 (TRUE) if valid comm buffer, 0 (FALSE) otherwise + fn handle_mm_is_comm_buffer(&self, ctx: &SyscallContext) -> SyscallResult { + let address = ctx.arg1; + let size = ctx.arg2; + log::trace!("MM_IS_COMM_BUFFER: addr=0x{:x}, size=0x{:x}", address, size); + + let config = match COMM_BUFFER_CONFIG.get() { + Some(c) => c, + None => { + log::error!("MM_IS_COMM_BUFFER: Comm buffer config not initialized"); + return SyscallResult::success(0); // FALSE + } + }; + + let buf_start = config.user_comm_buffer_internal; + let buf_end = buf_start.saturating_add(config.user_comm_buffer_size); + let range_end = address.saturating_add(size); + + // Check that the range is non-empty and falls entirely within the user comm buffer. + let is_valid = size > 0 && address >= buf_start && range_end <= buf_end; + + log::debug!("MM_IS_COMM_BUFFER: addr=0x{:x} size=0x{:x} => {}", address, size, is_valid); + SyscallResult::success(if is_valid { 1 } else { 0 }) + } +} + +/// Global syscall dispatcher instance. +pub static SYSCALL_DISPATCHER: SyscallDispatcher = SyscallDispatcher::new(); + +/// C-compatible syscall dispatcher entry point. +/// +/// This function is called from the assembly syscall entry stub (SyscallCenter). +/// +/// # Arguments +/// +/// * `call_index` - Syscall index (from RAX) +/// * `arg1` - First argument (from RDX) +/// * `arg2` - Second argument (from R8) +/// * `arg3` - Third argument (from R9) +/// * `caller_addr` - Caller return address (from RCX) +/// * `ring3_stack_ptr` - Ring 3 stack pointer +/// +/// # Returns +/// +/// The value to return in RAX. +#[unsafe(no_mangle)] +pub extern "efiapi" fn syscall_dispatcher( + call_index: u64, + arg1: u64, + arg2: u64, + arg3: u64, + caller_addr: u64, + ring3_stack_ptr: u64, +) -> u64 { + let ctx = SyscallContext { call_index, arg1, arg2, arg3, caller_addr, ring3_stack_ptr }; + + let result = SYSCALL_DISPATCHER.dispatch(&ctx); + + // For now, just return the value. In the future, we may need to handle + // error codes differently. + if result.status != 0 { + panic!("Syscall error: status=0x{:x}", result.status); + } else { + result.value + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_syscall_index_roundtrip() { + for idx in + [SyscallIndex::RdMsr, SyscallIndex::WrMsr, SyscallIndex::Cli, SyscallIndex::IoRead, SyscallIndex::IoWrite] + { + assert_eq!(SyscallIndex::from_u64(idx.as_u64()), Some(idx)); + } + } + + #[test] + fn test_unknown_syscall_index() { + assert_eq!(SyscallIndex::from_u64(0xFFFF), None); + assert_eq!(SyscallIndex::from_u64(0), None); + } + + #[test] + fn test_syscall_result() { + let success = SyscallResult::success(42); + assert_eq!(success.value, 42); + assert_eq!(success.status, 0); + + let error = SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + assert_eq!(error.value, 0); + assert_eq!(error.status, SyscallResult::EFI_INVALID_PARAMETER); + } +} diff --git a/patina_mm_supervisor_core/src/privilege_mgmt/syscall_entry.asm b/patina_mm_supervisor_core/src/privilege_mgmt/syscall_entry.asm new file mode 100644 index 000000000..9ba1e5cf2 --- /dev/null +++ b/patina_mm_supervisor_core/src/privilege_mgmt/syscall_entry.asm @@ -0,0 +1,159 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2020, AMD Incorporated. All rights reserved.
+# Copyright (c) 2017, Intel Corporation. All rights reserved.
+# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +# Module Name: +# +# WriteTr.nasm +# +# Abstract: +# +# Write TR register +# +# Notes: +# +#------------------------------------------------------------------------------ + +.section .data +.global syscall_center +.global syscall_dispatcher + +.section .text +.align 8 + +# Segments defined in SmiException.nasm +.equ LONG_DS_R0, 0x40 +.equ LONG_DS_R3, 0x53 + +# This should be OFFSET_OF (MM_SUPV_SYSCALL_CACHE, MmSupvRsp) +.equ MM_SUPV_RSP, 0x00 +# This should be OFFSET_OF (MM_SUPV_SYSCALL_CACHE, SavedUserRsp) +.equ SAVED_USER_RSP, 0x08 + +#------------------------------------------------------------------------------ +# Caller Interface: +# UINT64 +# EFIAPI +# SysCall ( +# UINTN CallIndex, +# UINTN Arg1, +# UINTN Arg2, +# UINTN Arg3 +# ); +# +# Backend Interface: +# /// C-compatible syscall dispatcher entry point. +# /// +# /// This function is called from the assembly syscall entry stub (SyscallCenter). +# /// +# /// # Arguments +# /// +# /// * `call_index` - Syscall index (from RAX) +# /// * `arg1` - First argument (from RDX) +# /// * `arg2` - Second argument (from R8) +# /// * `arg3` - Third argument (from R9) +# /// * `caller_addr` - Caller return address (from RCX) +# /// * `ring3_stack_ptr` - Ring 3 stack pointer +# /// +# /// # Returns +# /// +# /// The value to return in RAX. +# #[unsafe(no_mangle)] +# pub extern "efiapi" fn syscall_dispatcher( +# call_index: u64, +# arg1: u64, +# arg2: u64, +# arg3: u64, +# caller_addr: u64, +# ring3_stack_ptr: u64, +# ) -> u64; +#------------------------------------------------------------------------------ +syscall_center: +# Calling convention: CallIndex in RAX, Arg1 in RDX, Arg2 in R8, Arg3 in R9 from SysCallLib +# Architectural definition: CallerAddr in RCX, rFLAGs in R11 from x64 syscall instruction +# push CallIndex stored at top of stack + + swapgs # get kernel pointer, save user GSbase + mov gs:[SAVED_USER_RSP], rsp # save user's stack pointer + mov rsp, gs:[MM_SUPV_RSP] # set up kernel stack + + #Preserve all registers in CPL3 + push rax + push rcx + push rbp + push rdx + push r8 + push r9 + push rsi + push r12 + push rdi + push rbx + push r11 + push r10 + push r13 + push r14 + push r15 + + mov rbp, rsp + and rsp, -16 + + ## FX_SAVE_STATE_X64 FxSaveState# + sub rsp, 512 + mov rdi, rsp + .byte 0x0f, 0xae, 0x07 #fxsave [rdi] + + #Prepare for ds, es, fs, gs + xor rbx, rbx + mov bx, LONG_DS_R0 + mov ds, bx + mov es, bx + mov fs, bx + + mov rsi, gs:[SAVED_USER_RSP] # Save Ring 3 stack to RSI + push rsi # Push Ring 3 stack as Ring3Stack for syscall_dispatcher + push rcx # Push return address on stack as CallerAddr for syscall_dispatcher + mov rcx, rax + sub rsp, 0x20 + + call syscall_dispatcher + + add rsp, 0x20 + pop rcx # Restore SP to avoid stack overflow + pop rsi # Restore SI to avoid stack overflow + + #Prepare for ds, es, fs, gs + xor rbx, rbx + mov bx, LONG_DS_R3 + mov ds, bx + mov es, bx + mov fs, bx + + mov rsi, rsp + .byte 0x0f, 0xae, 0x0e # fxrstor [rsi] + add rsp, 512 + + mov rsp, rbp + + #restore registers from CPL3 stack + pop r15 + pop r14 + pop r13 + pop r10 + pop r11 + pop rbx + pop rdi + pop r12 + pop rsi + pop r9 + pop r8 + pop rdx + pop rbp + pop rcx # return rcx from stack + + add rsp, 8 # return rsp to original position + mov rsp, gs:[SAVED_USER_RSP] # restore user RSP + swapgs # restore user GS, save kernel pointer + .byte 0x48 # return to the long mode + sysret # RAX contains return value diff --git a/patina_mm_supervisor_core/src/privilege_mgmt/syscall_setup.rs b/patina_mm_supervisor_core/src/privilege_mgmt/syscall_setup.rs new file mode 100644 index 000000000..dc0a5b7f2 --- /dev/null +++ b/patina_mm_supervisor_core/src/privilege_mgmt/syscall_setup.rs @@ -0,0 +1,230 @@ +//! Syscall Interface Setup +//! +//! This module handles the initialization and configuration of syscall/sysret MSRs +//! for privilege level transitions. It manages per-CPU storage for MSR values and +//! the syscall cache structure used during ring transitions. +//! +//! ## MSR Configuration +//! +//! - **MSR_IA32_STAR**: Contains segment selectors for syscall/sysret +//! - Bits 47:32 = SYSRET CS and SS (LONG_CS_R3_PH << 16) +//! - Bits 31:16 = SYSCALL CS and SS (LONG_CS_R0) +//! +//! - **MSR_IA32_LSTAR**: Contains the 64-bit RIP for syscall entry (SyscallCenter) +//! +//! - **MSR_IA32_EFER**: Extended Feature Enable Register +//! - Bit 0 (SCE) must be set to enable syscall/sysret +//! +//! - **MSR_IA32_KERNEL_GS_BASE**: Used with swapgs to switch between user and kernel +//! GS base addresses, allowing access to per-CPU data in the syscall handler. +//! + +#![allow(unsafe_op_in_unsafe_fn)] + +use core::sync::atomic::{AtomicBool, Ordering}; +use spin::Mutex; + +use super::PrivilegeError; + +/// Errors specific to syscall setup operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SyscallSetupError { + /// The syscall interface has not been initialized. + NotInitialized, + /// Already initialized. + AlreadyInitialized, + /// Invalid CPU index (exceeds configured CPU count). + InvalidCpuIndex, + /// Out of resources. + OutOfResources, + /// MSR stores are not ready. + NotReady, +} + +impl From for PrivilegeError { + fn from(e: SyscallSetupError) -> Self { + match e { + SyscallSetupError::NotInitialized => PrivilegeError::NotInitialized, + SyscallSetupError::AlreadyInitialized => PrivilegeError::AlreadyInitialized, + SyscallSetupError::InvalidCpuIndex => PrivilegeError::InvalidCpuIndex, + SyscallSetupError::OutOfResources => PrivilegeError::OutOfResources, + SyscallSetupError::NotReady => PrivilegeError::NotReady, + } + } +} + +/// Internal state for the syscall interface. +/// +/// ## Const Generic Parameters +/// +/// * `MAX_CPUS` - The maximum number of CPUs that can be supported. +struct SyscallInterfaceState { + /// Number of CPUs configured. + num_cpus: usize, + /// CPL3 stack array base address. + cpl3_stack_base: u64, + /// Per-CPU stack size. + stack_size: usize, +} + +impl SyscallInterfaceState { + const fn new() -> Self { + Self { num_cpus: 0, cpl3_stack_base: 0, stack_size: 0 } + } +} + +/// Syscall interface manager. +/// +/// Manages the syscall/sysret MSR configuration for all CPUs and provides +/// the infrastructure for Ring 0 ↔ Ring 3 transitions. +/// +/// ## Const Generic Parameters +/// +/// * `MAX_CPUS` - The maximum number of CPUs that can be supported. +/// This should match `PlatformInfo::MAX_CPU_COUNT`. +pub struct SyscallInterface { + /// Whether the interface has been initialized. + initialized: AtomicBool, + /// Internal state protected by mutex. + state: Mutex>, +} + +impl SyscallInterface { + /// Creates a new syscall interface. + pub const fn new() -> Self { + Self { initialized: AtomicBool::new(false), state: Mutex::new(SyscallInterfaceState::new()) } + } + + /// Returns the maximum number of CPUs this interface supports. + pub const fn max_cpus(&self) -> usize { + MAX_CPUS + } + + /// Initializes the syscall interface. + /// + /// This should be called once during BSP initialization. + /// + /// # Arguments + /// + /// * `num_cpus` - Total number of CPUs to support (must be <= MAX_CPUS) + /// * `cpl3_stack_base` - Base address of the CPL3 stack array + /// * `stack_size` - Per-CPU stack size + /// + /// # Returns + /// + /// `Ok(())` if initialization succeeded, error otherwise. + pub fn init(&self, num_cpus: usize, cpl3_stack_base: u64, stack_size: usize) -> Result<(), SyscallSetupError> { + // Check if already initialized + if self.initialized.swap(true, Ordering::SeqCst) { + return Err(SyscallSetupError::AlreadyInitialized); + } + + if num_cpus == 0 || num_cpus > MAX_CPUS { + self.initialized.store(false, Ordering::SeqCst); + return Err(SyscallSetupError::InvalidCpuIndex); + } + + let mut state = self.state.lock(); + state.num_cpus = num_cpus; + state.cpl3_stack_base = cpl3_stack_base; + state.stack_size = stack_size; + + log::info!( + "SyscallInterface<{}> initialized: {} CPUs, cpl3_stack=0x{:016x}, stack_size=0x{:x}", + MAX_CPUS, + num_cpus, + cpl3_stack_base, + stack_size + ); + + Ok(()) + } + + /// Checks if the syscall interface is initialized. + pub fn is_initialized(&self) -> bool { + self.initialized.load(Ordering::Acquire) + } + + /// Gets the CPL3 stack pointer for a specific CPU. + /// + /// The stack pointer is calculated as: + /// `cpl3_stack_base + stack_size * (cpu_index + 1) - sizeof(usize)` + /// + /// This gives the top of the stack for the CPU (stacks grow downward). + /// TODO: Might just want to allocate a page on the fly before demotion and free the pointer + /// upon returning instead of messing with the pre-allocated stack array. + pub fn get_cpl3_stack(&self, cpu_index: usize) -> Result { + if !self.is_initialized() { + log::error!("SyscallInterface not initialized"); + return Err(SyscallSetupError::NotInitialized); + } + + let state = self.state.lock(); + if cpu_index >= state.num_cpus { + log::error!("Invalid CPU index {}: exceeds configured CPU count {}", cpu_index, state.num_cpus); + return Err(SyscallSetupError::InvalidCpuIndex); + } + + // Calculate stack top: base + size * (index + 1) - sizeof(usize) + let stack_top = state + .cpl3_stack_base + .wrapping_add((state.stack_size as u64) * ((cpu_index as u64) + 1)) + .wrapping_sub(core::mem::size_of::() as u64); + + Ok(stack_top) + } +} + +impl Default for SyscallInterface { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cpl3_stack_calculation() { + let interface: SyscallInterface<8> = SyscallInterface::new(); + + // Initialize with known values: num_cpus=4, entry=0x1000, cpl3_stack_base=0x10000, stack_size=0x4000 + interface.init(4, 0x1000, 0x10000, 0x4000).unwrap(); + + // CPU 0: base + 0x4000 * 1 - 8 = 0x10000 + 0x4000 - 8 = 0x13FF8 + assert_eq!(interface.get_cpl3_stack(0).unwrap(), 0x13FF8); + + // CPU 1: base + 0x4000 * 2 - 8 = 0x10000 + 0x8000 - 8 = 0x17FF8 + assert_eq!(interface.get_cpl3_stack(1).unwrap(), 0x17FF8); + } + + #[test] + fn test_init_twice_fails() { + let interface: SyscallInterface<8> = SyscallInterface::new(); + assert!(interface.init(4, 0x1000, 0x10000, 0x4000).is_ok()); + assert_eq!(interface.init(4, 0x1000, 0x10000, 0x4000), Err(SyscallSetupError::AlreadyInitialized)); + } + + #[test] + fn test_invalid_cpu_index() { + let interface: SyscallInterface<8> = SyscallInterface::new(); + interface.init(4, 0x1000, 0x10000, 0x4000).unwrap(); + + assert_eq!(interface.get_cpl3_stack(4), Err(SyscallSetupError::InvalidCpuIndex)); + assert_eq!(interface.get_cpl3_stack(100), Err(SyscallSetupError::InvalidCpuIndex)); + } + + #[test] + fn test_max_cpus_exceeded() { + let interface: SyscallInterface<4> = SyscallInterface::new(); + // Try to init with more CPUs than the const generic allows + assert_eq!(interface.init(8, 0x1000, 0x10000, 0x4000), Err(SyscallSetupError::InvalidCpuIndex)); + } + + #[test] + fn test_max_cpus_accessor() { + let interface: SyscallInterface<16> = SyscallInterface::new(); + assert_eq!(interface.max_cpus(), 16); + } +} diff --git a/patina_mm_supervisor_core/src/save_state.rs b/patina_mm_supervisor_core/src/save_state.rs new file mode 100644 index 000000000..75650345a --- /dev/null +++ b/patina_mm_supervisor_core/src/save_state.rs @@ -0,0 +1,619 @@ +//! Save State Read Operations for the MM Supervisor Syscall Dispatcher +//! +//! Implements the two-phase save state read protocol used by the +//! `EFI_MM_CPU_PROTOCOL.ReadSaveState()` user-space API. +//! +//! **Phase 1** (`SyscallIndex::SaveStateRead`): stores the requested register +//! and CPU index in a per-BSP holder. +//! +//! **Phase 2** (`SyscallIndex::SaveStateRead2`): validates the request against +//! the MM security policy, reads the register value from the CPU's SMRAM save +//! state area, and copies the result into the caller-supplied buffer. +//! +//! ## Security Model +//! +//! - User buffer addresses are validated via page-table ownership queries. +//! - Policy-gated registers (RAX, IO) are checked through +//! [`PolicyGate::is_save_state_read_allowed`](patina_mm_policy::PolicyGate::is_save_state_read_allowed). +//! - `PROCESSOR_ID` is always allowed (informational, not security-sensitive). +//! - Other architectural registers pass through without policy gating, matching +//! the C reference implementation's allow-list semantics. +//! +//! ## Vendor Selection +//! +//! The SMRAM save state layout (Intel vs AMD) is selected **at build time** +//! via Cargo features on the `patina` crate (`save_state_intel` or +//! `save_state_amd`). All vendor-specific register offsets and I/O field +//! parsing live in the SDK; this module is vendor-agnostic. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 + +use spin::Mutex; + +use patina_internal_cpu::save_state::{ + self, IA32_EFER_LMA, IO_INFO_SIZE, IO_TYPE_INPUT, IO_TYPE_OUTPUT, LMA_32BIT, LMA_64BIT, MmSaveStateIoInfo, + MmSaveStateRegister, PROCESSOR_INFO_ENTRY_SIZE, +}; +use patina_mm_policy::{SaveStateCondition, SaveStateField}; + +use crate::privilege_mgmt::SyscallResult; +use crate::{POLICY_GATE, PageOwnership, SMM_CPU_PRIVATE, SmmCpuPrivateData, query_address_ownership}; + +/// Maps a save state register to a policy-gated [`SaveStateField`], if any. +/// +/// Only RAX and IO are subject to policy gating. All other registers are +/// either always allowed or have special handling (PROCESSOR_ID). +fn to_policy_field(reg: MmSaveStateRegister) -> Option { + match reg { + MmSaveStateRegister::Rax => Some(SaveStateField::Rax), + MmSaveStateRegister::Io => Some(SaveStateField::IoTrap), + _ => None, + } +} + +/// Holds the parameters from Phase 1 until Phase 2 completes the read. +struct SaveStateAccessHolder { + /// User protocol pointer (must match across both phases). + user_protocol: u64, + /// Register to read. + register: MmSaveStateRegister, + /// CPU index to read from. + cpu_index: u64, +} + +/// Global holder for the in-flight two-phase save state read. +/// +/// Only one read can be in flight at a time (enforced by the single-threaded +/// BSP syscall dispatch model). +static SAVE_STATE_ACCESS: Mutex> = Mutex::new(None); + +/// Processes Phase 1 of the save state read syscall. +/// +/// Validates and stores the register and CPU index for the subsequent Phase 2 +/// call. +/// +/// # Arguments +/// +/// * `protocol` - User MM CPU protocol pointer (for consistency check in Phase 2) +/// * `register_raw` - Raw `EFI_MM_SAVE_STATE_REGISTER` value +/// * `cpu_index` - CPU index to read the save state from +pub fn save_state_read_phase1(protocol: u64, register_raw: u64, cpu_index: u64) -> SyscallResult { + // Validate register + let register = match MmSaveStateRegister::from_u64(register_raw) { + Some(r) => r, + None => { + log::error!("SAVE_STATE_READ: Unknown register value: {}", register_raw); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + }; + + // Validate CPU index against NumberOfCpus + let num_cpus = match get_number_of_cpus() { + Ok(n) => n, + Err(status) => return SyscallResult::error(status), + }; + + if cpu_index >= num_cpus { + log::error!("SAVE_STATE_READ: CPU index {} >= NumberOfCpus {}", cpu_index, num_cpus); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + + // Store for Phase 2 + let mut access = SAVE_STATE_ACCESS.lock(); + *access = Some(SaveStateAccessHolder { user_protocol: protocol, register, cpu_index }); + + log::debug!("SAVE_STATE_READ: Stored register={:?}, cpu_index={} for Phase 2", register, cpu_index); + SyscallResult::success(0) +} + +/// Processes Phase 2 of the save state read syscall. +/// +/// Validates the request against the MM security policy, reads the register +/// from the CPU's SMRAM save state area, and copies the result into the user +/// buffer. +/// +/// # Arguments +/// +/// * `protocol` - User MM CPU protocol pointer (must match Phase 1) +/// * `width` - Width of the read in bytes +/// * `buffer` - User buffer to receive the register value +pub fn save_state_read_phase2(protocol: u64, width: u64, buffer: u64) -> SyscallResult { + // Retrieve and consume the Phase 1 state + let holder = { + let mut access = SAVE_STATE_ACCESS.lock(); + match access.take() { + Some(h) => h, + None => { + log::error!("SAVE_STATE_READ2: Phase 1 not completed"); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + } + }; + + // Verify protocol matches Phase 1 + if holder.user_protocol != protocol { + log::error!("SAVE_STATE_READ2: Protocol mismatch: expected 0x{:x}, got 0x{:x}", holder.user_protocol, protocol); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + + // Validate width and buffer + if width == 0 || buffer == 0 { + log::error!("SAVE_STATE_READ2: Invalid width ({}) or null buffer", width); + return SyscallResult::error(SyscallResult::EFI_INVALID_PARAMETER); + } + + let register = holder.register; + let cpu_index = holder.cpu_index; + + // Determine the actual number of bytes we'll write + let write_size = actual_write_size(register, width); + if write_size == 0 { + log::error!("SAVE_STATE_READ2: Unsupported width {} for register {:?}", width, register); + return SyscallResult::error(SyscallResult::EFI_UNSUPPORTED); + } + + // Validate buffer is in user-owned memory + match query_address_ownership(buffer, write_size as u64) { + Some(PageOwnership::User) => {} + Some(owner) => { + log::error!("SAVE_STATE_READ2: Buffer 0x{:x} owned by {:?}, expected User", buffer, owner); + return SyscallResult::error(SyscallResult::EFI_ACCESS_DENIED); + } + None => { + log::error!("SAVE_STATE_READ2: Buffer 0x{:x} not in mapped memory", buffer); + return SyscallResult::error(SyscallResult::EFI_ACCESS_DENIED); + } + } + + // Special case: PROCESSOR_ID — always allowed, no policy check + if register == MmSaveStateRegister::ProcessorId { + return read_processor_id(cpu_index, buffer); + } + + // Policy check for gated registers (RAX, IO) + if let Some(policy_field) = to_policy_field(register) { + let condition = inspect_io_condition(cpu_index); + let gate = match POLICY_GATE.get() { + Some(g) => g, + None => { + log::error!("SAVE_STATE_READ2: Policy gate not initialized"); + return SyscallResult::error(SyscallResult::EFI_NOT_READY); + } + }; + + if let Err(e) = gate.is_save_state_read_allowed(policy_field, width as usize, condition) { + log::error!("SAVE_STATE_READ2: Policy denied read of {:?}: {:?}", register, e); + return SyscallResult::error(SyscallResult::EFI_ACCESS_DENIED); + } + } + + // Get the save state base pointer for this CPU + let save_state_base = match get_save_state_base(cpu_index) { + Ok(base) => base, + Err(status) => return SyscallResult::error(status), + }; + + // Dispatch to the appropriate read handler. + // + // SAFETY: `save_state_base` points to a valid SMRAM save state region + // (obtained from SMM_CPU_PRIVATE which is set up by PiSmmCpuDxeSmm). + // `buffer` has been validated as a user-owned region of sufficient size. + let status = match register { + MmSaveStateRegister::Io => unsafe { read_io_register(save_state_base, buffer as *mut u8) }, + MmSaveStateRegister::Lma => unsafe { read_lma_register(save_state_base, width, buffer as *mut u8) }, + _ => unsafe { read_architectural_register(save_state_base, register, width, buffer as *mut u8) }, + }; + + if status == SyscallResult::EFI_SUCCESS { + log::debug!("SAVE_STATE_READ2: Read {:?} (cpu={}, width={}) successfully", register, cpu_index, width); + SyscallResult::success(0) + } else { + SyscallResult::error(status) + } +} + +/// Returns the number of CPUs from the SMM CPU private data. +fn get_number_of_cpus() -> Result { + let cpu_private_addr = match SMM_CPU_PRIVATE.get() { + Some(&addr) if addr != 0 => addr, + _ => { + log::error!("SMM CPU Private data not initialized"); + return Err(SyscallResult::EFI_NOT_READY); + } + }; + + // SAFETY: cpu_private_addr is provided by MM IPL via PassDown HOB and validated + // during initialization to point to a valid SmmCpuPrivateData in SMRAM. + let cpu_private = unsafe { &*(cpu_private_addr as *const SmmCpuPrivateData) }; + Ok(cpu_private.smm_core_entry_context.number_of_cpus) +} + +/// Returns the save state base pointer for a given CPU index. +/// +/// The pointer comes from `SmmCpuPrivateData.cpu_save_state[cpu_index]`, +/// which points to SMBASE + 0x7C00 for that CPU's save state area. +fn get_save_state_base(cpu_index: u64) -> Result<*const u8, u64> { + let cpu_private_addr = match SMM_CPU_PRIVATE.get() { + Some(&addr) if addr != 0 => addr, + _ => { + log::error!("SMM CPU Private data not initialized for save state read"); + return Err(SyscallResult::EFI_NOT_READY); + } + }; + + // SAFETY: cpu_private_addr points to valid SmmCpuPrivateData in SMRAM. + let cpu_private = unsafe { &*(cpu_private_addr as *const SmmCpuPrivateData) }; + let save_state_array = cpu_private.cpu_save_state; + if save_state_array == 0 { + log::error!("CpuSaveState array pointer is null"); + return Err(SyscallResult::EFI_NOT_READY); + } + + // Read the per-CPU save state pointer from the array. + // SAFETY: cpu_save_state points to a valid array of VOID* pointers in SMRAM, + // and cpu_index has been validated against NumberOfCpus. + let save_state_ptr = unsafe { + let array_ptr = save_state_array as *const u64; + *array_ptr.add(cpu_index as usize) + }; + + if save_state_ptr == 0 { + log::error!("CpuSaveState[{}] is null", cpu_index); + return Err(SyscallResult::EFI_INVALID_PARAMETER); + } + + Ok(save_state_ptr as *const u8) +} + +/// Determines the actual number of bytes that will be written to the user buffer. +/// +/// Returns 0 if the width is not supported for the given register. +fn actual_write_size(register: MmSaveStateRegister, width: u64) -> usize { + match register { + MmSaveStateRegister::Io => IO_INFO_SIZE, + MmSaveStateRegister::ProcessorId => 8, + MmSaveStateRegister::Lma => { + if width == 4 || width == 8 { + width as usize + } else { + 0 + } + } + _ => { + if let Some(info) = save_state::register_info(register) { + if width == 2 && info.native_width >= 2 { + 2 + } else if width == 4 && info.native_width >= 4 { + 4 + } else if width == 8 && info.native_width == 8 { + 8 + } else { + 0 + } + } else { + 0 + } + } + } +} + +/// Reads the PROCESSOR_ID for a given CPU and writes it to the user buffer. +/// +/// The ProcessorId (APIC ID) is read from the `EFI_PROCESSOR_INFORMATION` array +/// that was set up by PiSmmCpuDxeSmm and passed through the PassDown HOB. +fn read_processor_id(cpu_index: u64, buffer: u64) -> SyscallResult { + let cpu_private_addr = match SMM_CPU_PRIVATE.get() { + Some(&addr) if addr != 0 => addr, + _ => return SyscallResult::error(SyscallResult::EFI_NOT_READY), + }; + + // SAFETY: validated during initialization. + let cpu_private = unsafe { &*(cpu_private_addr as *const SmmCpuPrivateData) }; + + if cpu_private.processor_info == 0 { + log::error!("PROCESSOR_ID: ProcessorInfo array is null"); + return SyscallResult::error(SyscallResult::EFI_NOT_READY); + } + + // Read ProcessorId (first field, offset 0) from the EFI_PROCESSOR_INFORMATION + // entry at the given CPU index. + // + // SAFETY: processor_info points to a valid array of EFI_PROCESSOR_INFORMATION + // entries in firmware memory, and cpu_index has been validated. + let processor_id: u64 = unsafe { + let base = cpu_private.processor_info as *const u8; + let entry_ptr = base.add(cpu_index as usize * PROCESSOR_INFO_ENTRY_SIZE); + core::ptr::read_unaligned(entry_ptr as *const u64) + }; + + // Write the 8-byte ProcessorId to the user buffer. + // + // SAFETY: buffer is validated as user-owned with sufficient size (8 bytes). + unsafe { + core::ptr::write_unaligned(buffer as *mut u64, processor_id); + } + + log::debug!("PROCESSOR_ID: CPU {} = 0x{:x}", cpu_index, processor_id); + SyscallResult::success(0) +} + +/// Inspects the I/O condition (IN vs OUT) from the save state for policy checking. +/// +/// Reads the vendor-specific IO field from the CPU's save state and uses the +/// SDK's [`save_state::parse_io_field`] to determine whether the I/O trap was +/// caused by an IN or OUT instruction. +fn inspect_io_condition(cpu_index: u64) -> Option { + log::info!("Inspecting I/O condition for CPU {}: retrieving save state base", cpu_index); + let save_state_base = match get_save_state_base(cpu_index) { + Ok(base) => base, + Err(e) => { + log::error!("Failed to get save state base for CPU {} due to {}", cpu_index, e); + return None; + } + }; + + let vc = save_state::vendor_constants(); + + // Verify the save state revision supports IO info before reading the field. + // + // SAFETY: save_state_base is valid and smmrevid_offset is within the + // save state map region. + let smm_rev_id: u32 = + unsafe { core::ptr::read_volatile(save_state_base.add(vc.smmrevid_offset as usize) as *const u32) }; + + if smm_rev_id < vc.min_rev_id_io { + log::warn!( + "inspect_io_condition: SMMRevId 0x{:x} < 0x{:x}, IO info not available for CPU {}", + smm_rev_id, + vc.min_rev_id_io, + cpu_index + ); + return None; + } + + // Read the vendor-specific IO information field (u32). + // + // SAFETY: save_state_base is valid and io_info_offset is within the + // save state map region. + let io_field: u32 = + unsafe { core::ptr::read_volatile(save_state_base.add(vc.io_info_offset as usize) as *const u32) }; + + log::info!("Inspecting IO condition for CPU {}: IO field = 0x{:x}", cpu_index, io_field); + + // Use the SDK's vendor-specific parser. + match save_state::parse_io_field(io_field) { + Some(parsed) => match parsed.io_type { + IO_TYPE_INPUT => Some(SaveStateCondition::IoRead), + IO_TYPE_OUTPUT => Some(SaveStateCondition::IoWrite), + _ => Some(SaveStateCondition::IoWrite), + }, + None => { + // No valid IO info (Intel: SmiFlag not set, AMD: reserved size) — + // default to write condition (matching C behaviour). + Some(SaveStateCondition::IoWrite) + } + } +} + +/// Reads an architectural register from the Intel x64 save state map. +/// +/// # Safety +/// +/// - `save_state_base` must point to a valid `SMRAM_SAVE_STATE_MAP64` region. +/// - `buffer` must point to a user-owned region with at least `width` bytes. +unsafe fn read_architectural_register( + save_state_base: *const u8, + register: MmSaveStateRegister, + width: u64, + buffer: *mut u8, +) -> u64 { + let info = match save_state::register_info(register) { + Some(i) => i, + None => { + log::error!("Register {:?} not found in save state map", register); + return SyscallResult::EFI_UNSUPPORTED; + } + }; + + if width == 2 { + if info.native_width < 2 { + return SyscallResult::EFI_UNSUPPORTED; + } + // Read the low 2 bytes (AMD segment selectors, DT limits). + let val = unsafe { core::ptr::read_volatile(save_state_base.add(info.lo_offset as usize) as *const u16) }; + unsafe { + core::ptr::write_unaligned(buffer as *mut u16, val); + } + } else if width == 4 { + if info.native_width < 4 { + return SyscallResult::EFI_UNSUPPORTED; + } + // Read the low 4 bytes. + let lo = unsafe { core::ptr::read_volatile(save_state_base.add(info.lo_offset as usize) as *const u32) }; + unsafe { + core::ptr::write_unaligned(buffer as *mut u32, lo); + } + } else if width == 8 { + if info.native_width != 8 { + return SyscallResult::EFI_UNSUPPORTED; + } + // Read lo dword then hi dword (handles both contiguous and split layouts). + let lo = unsafe { core::ptr::read_volatile(save_state_base.add(info.lo_offset as usize) as *const u32) }; + let hi = unsafe { core::ptr::read_volatile(save_state_base.add(info.hi_offset as usize) as *const u32) }; + // Write as two adjacent dwords (matching C split-register behaviour). + unsafe { + core::ptr::write_unaligned(buffer as *mut u32, lo); + core::ptr::write_unaligned((buffer as *mut u32).add(1), hi); + } + } else { + return SyscallResult::EFI_INVALID_PARAMETER; + } + + SyscallResult::EFI_SUCCESS +} + +/// Reads the IO pseudo-register and writes an `EFI_MM_SAVE_STATE_IO_INFO` +/// structure to the user buffer. +/// +/// The IO pseudo-register provides information about the I/O instruction that +/// triggered the SMI, including the port, width, direction, and data value. +/// +/// # Safety +/// +/// - `save_state_base` must point to a valid `SMRAM_SAVE_STATE_MAP64` region. +/// - `buffer` must point to a user-owned region with at least [`IO_INFO_SIZE`] bytes. +unsafe fn read_io_register(save_state_base: *const u8, buffer: *mut u8) -> u64 { + let vc = save_state::vendor_constants(); + + // 1. Read SMMRevId to verify IO info is available. + let smm_rev_id = + unsafe { core::ptr::read_volatile(save_state_base.add(vc.smmrevid_offset as usize) as *const u32) }; + + if smm_rev_id < vc.min_rev_id_io { + log::error!("IO_READ: SMMRevId 0x{:x} < 0x{:x}, IO info not supported", smm_rev_id, vc.min_rev_id_io); + return SyscallResult::EFI_UNSUPPORTED; + } + + // 2. Read the vendor-specific IO information field and parse it. + let io_field = unsafe { core::ptr::read_volatile(save_state_base.add(vc.io_info_offset as usize) as *const u32) }; + + let parsed = match save_state::parse_io_field(io_field) { + Some(p) => p, + None => { + log::debug!("IO_READ: IO field 0x{:x} did not indicate a valid I/O trap", io_field); + return SyscallResult::EFI_UNSUPPORTED; + } + }; + + // 3. Read I/O data from RAX (only the significant bytes). + let io_data: u64 = unsafe { + let rax_ptr = save_state_base.add(vc.rax_offset as usize); + match parsed.byte_count { + 1 => core::ptr::read_volatile(rax_ptr as *const u8) as u64, + 2 => core::ptr::read_volatile(rax_ptr as *const u16) as u64, + 4 => core::ptr::read_volatile(rax_ptr as *const u32) as u64, + _ => 0, + } + }; + + // 4. Compose the EFI_MM_SAVE_STATE_IO_INFO structure and copy to user buffer. + let io_info = MmSaveStateIoInfo { + io_data, + io_port: parsed.io_port as u64, + io_width: parsed.io_width, + io_type: parsed.io_type, + }; + + unsafe { + core::ptr::copy_nonoverlapping(&io_info as *const MmSaveStateIoInfo as *const u8, buffer, IO_INFO_SIZE); + } + + SyscallResult::EFI_SUCCESS +} + +/// Reads the LMA pseudo-register (processor Long Mode Active state). +/// +/// Returns `LMA_32BIT` (32) or `LMA_64BIT` (64) depending on the IA32_EFER.LMA +/// bit in the save state. +/// +/// # Safety +/// +/// - `save_state_base` must point to a valid `SMRAM_SAVE_STATE_MAP64` region. +/// - `buffer` must point to a user-owned region with at least `width` bytes. +unsafe fn read_lma_register(save_state_base: *const u8, width: u64, buffer: *mut u8) -> u64 { + let vc = save_state::vendor_constants(); + + // AMD64 always operates in 64-bit mode during SMM. + let lma_value = if vc.lma_always_64 { + LMA_64BIT + } else { + // Read IA32_EFER from the save state. + let efer = unsafe { core::ptr::read_volatile(save_state_base.add(vc.efer_offset as usize) as *const u64) }; + if (efer & IA32_EFER_LMA) != 0 { LMA_64BIT } else { LMA_32BIT } + }; + + if width == 4 { + unsafe { + core::ptr::write_unaligned(buffer as *mut u32, lma_value as u32); + } + } else if width == 8 { + unsafe { + core::ptr::write_unaligned(buffer as *mut u64, lma_value); + } + } else { + return SyscallResult::EFI_INVALID_PARAMETER; + } + + SyscallResult::EFI_SUCCESS +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_register_from_u64() { + assert_eq!(MmSaveStateRegister::from_u64(38), Some(MmSaveStateRegister::Rax)); + assert_eq!(MmSaveStateRegister::from_u64(512), Some(MmSaveStateRegister::Io)); + assert_eq!(MmSaveStateRegister::from_u64(514), Some(MmSaveStateRegister::ProcessorId)); + assert_eq!(MmSaveStateRegister::from_u64(999), None); + assert_eq!(MmSaveStateRegister::from_u64(0), None); + } + + #[test] + fn test_to_policy_field() { + assert_eq!(to_policy_field(MmSaveStateRegister::Rax), Some(SaveStateField::Rax)); + assert_eq!(to_policy_field(MmSaveStateRegister::Io), Some(SaveStateField::IoTrap)); + assert_eq!(to_policy_field(MmSaveStateRegister::Rbx), None); + assert_eq!(to_policy_field(MmSaveStateRegister::ProcessorId), None); + } + + #[test] + fn test_actual_write_size() { + // IO always writes IO_INFO_SIZE + assert_eq!(actual_write_size(MmSaveStateRegister::Io, 4), IO_INFO_SIZE); + assert_eq!(actual_write_size(MmSaveStateRegister::Io, 24), IO_INFO_SIZE); + + // PROCESSOR_ID always writes 8 + assert_eq!(actual_write_size(MmSaveStateRegister::ProcessorId, 8), 8); + + // LMA supports 4 and 8 + assert_eq!(actual_write_size(MmSaveStateRegister::Lma, 4), 4); + assert_eq!(actual_write_size(MmSaveStateRegister::Lma, 8), 8); + assert_eq!(actual_write_size(MmSaveStateRegister::Lma, 3), 0); + + // RAX (native 8): supports Width=2, 4, and 8 + assert_eq!(actual_write_size(MmSaveStateRegister::Rax, 2), 2); + assert_eq!(actual_write_size(MmSaveStateRegister::Rax, 4), 4); + assert_eq!(actual_write_size(MmSaveStateRegister::Rax, 8), 8); + assert_eq!(actual_write_size(MmSaveStateRegister::Rax, 16), 0); + } + + #[test] + fn test_save_state_access_holder() { + // Test that the mutex works correctly for Phase 1/Phase 2. + { + let mut access = SAVE_STATE_ACCESS.lock(); + assert!(access.is_none()); + *access = + Some(SaveStateAccessHolder { user_protocol: 0xDEAD, register: MmSaveStateRegister::Rax, cpu_index: 0 }); + } + + { + let mut access = SAVE_STATE_ACCESS.lock(); + let holder = access.take().unwrap(); + assert_eq!(holder.user_protocol, 0xDEAD); + assert_eq!(holder.register, MmSaveStateRegister::Rax); + assert_eq!(holder.cpu_index, 0); + } + + { + let access = SAVE_STATE_ACCESS.lock(); + assert!(access.is_none()); + } + } +} diff --git a/patina_mm_supervisor_core/src/supervisor_handlers.rs b/patina_mm_supervisor_core/src/supervisor_handlers.rs new file mode 100644 index 000000000..a4d507e8f --- /dev/null +++ b/patina_mm_supervisor_core/src/supervisor_handlers.rs @@ -0,0 +1,698 @@ +//! Supervisor MMI Handler Registry +//! +//! This module provides the build-time handler registration mechanism for the MM Supervisor Core. +//! Handlers are registered at link time using `linkme::distributed_slice`, allowing platforms +//! to add custom supervisor handlers without modifying the core. +//! +//! ## Architecture +//! +//! The [`SUPERVISOR_MMI_HANDLERS`] distributed slice collects all handler entries across the +//! final binary. Each entry is a [`SupervisorMmiHandler`] that specifies a GUID and handler +//! function. During supervisor request processing, the core iterates the slice to find a +//! handler matching the communicate header GUID. +//! +//! ## Adding Platform-Specific Handlers +//! +//! To register a handler from a platform crate: +//! +//! ```rust,ignore +//! use patina_mm_supervisor_core::{SupervisorMmiHandler, SUPERVISOR_MMI_HANDLERS}; +//! use r_efi::efi; +//! +//! fn my_handler(comm_buffer: *mut u8, comm_buffer_size: &mut usize) -> efi::Status { +//! // Handle the request... +//! efi::Status::SUCCESS +//! } +//! +//! #[linkme::distributed_slice(SUPERVISOR_MMI_HANDLERS)] +//! static MY_HANDLER: SupervisorMmiHandler = SupervisorMmiHandler { +//! name: "MyPlatformHandler", +//! handler_guid: efi::Guid::from_fields( +//! 0x12345678, 0xabcd, 0xef01, +//! 0x23, 0x45, &[0x67, 0x89, 0xab, 0xcd, 0xef, 0x01] +//! ), +//! handle: my_handler, +//! }; +//! ``` +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use r_efi::efi; + +use patina::base::UEFI_PAGE_SIZE; +use patina_paging::{MemoryAttributes, PageTable, PtError}; + +use crate::mm_mem::PAGE_ALLOCATOR; +use crate::unblock_memory::{UNBLOCKED_MEMORY_TRACKER, UnblockError}; +use crate::{POLICY_GATE, is_buffer_inside_mmram, read_cr3}; + +use patina::management_mode::protocol::mm_supervisor_request::{ + MM_SUPERVISOR_REQUEST_HANDLER_GUID, MmSupervisorRequestHeader, MmSupervisorUnblockMemoryParams, + MmSupervisorVersionInfo, REVISION, RequestType, SIGNATURE, +}; + +use patina_mm_policy::{MemDescriptorV1_0, PolicyError}; + +/// A build-time registered supervisor MMI handler. +/// +/// Each entry represents a handler that the supervisor core will consider when dispatching +/// supervisor-channel requests. Handlers are matched by comparing the +/// [`EfiMmCommunicateHeader::header_guid`](crate::EfiMmCommunicateHeader::header_guid) +/// against [`handler_guid`](SupervisorMmiHandler::handler_guid). +/// +/// ## Handler Function Signature +/// +/// The [`handle`](SupervisorMmiHandler::handle) function receives: +/// - `comm_buffer`: Pointer to the data portion of the communicate buffer (after the header). +/// - `comm_buffer_size`: On input, the message length. On output, the response data length. +/// +/// The handler should return an [`efi::Status`] code. +#[derive(Debug)] +pub struct SupervisorMmiHandler { + /// Human-readable name for logging and debugging. + pub name: &'static str, + /// GUID identifying the request type this handler processes. + pub handler_guid: efi::Guid, + /// The handler function. + /// + /// # Arguments + /// + /// * `comm_buffer` - Pointer to the data payload (after `EfiMmCommunicateHeader`). + /// * `comm_buffer_size` - On input, the data size; on output, the response data size. + /// + /// # Returns + /// + /// An EFI status code indicating the result of the handler. + pub handle: fn(comm_buffer: *mut u8, comm_buffer_size: &mut usize) -> efi::Status, +} + +// SAFETY: SupervisorMmiHandler contains only a &'static str, a Guid (plain data), and a fn pointer. +// All of these are inherently Sync. +unsafe impl Sync for SupervisorMmiHandler {} + +/// The global distributed slice collecting all supervisor MMI handlers. +/// +/// Handlers from the core and from platform crates are collected here at link time. +/// The supervisor dispatch loop iterates this slice to find a matching handler for +/// each incoming supervisor-channel request. +/// +/// ## Usage +/// +/// ```rust,ignore +/// use patina_mm_supervisor_core::{SupervisorMmiHandler, SUPERVISOR_MMI_HANDLERS}; +/// +/// #[linkme::distributed_slice(SUPERVISOR_MMI_HANDLERS)] +/// static MY_HANDLER: SupervisorMmiHandler = SupervisorMmiHandler { +/// name: "MyHandler", +/// handler_guid: MY_GUID, +/// handle: my_handler_fn, +/// }; +/// ``` +#[linkme::distributed_slice] +pub static SUPERVISOR_MMI_HANDLERS: [SupervisorMmiHandler]; + +// GUID for gEfiDxeMmReadyToLockProtocolGuid +// { 0x60ff8964, 0xe906, 0x41d0, { 0xaf, 0xed, 0xf2, 0x41, 0xe9, 0x74, 0xe0, 0x8e } } +/// GUID for the DXE MM Ready To Lock protocol. +pub const EFI_DXE_MM_READY_TO_LOCK_PROTOCOL_GUID: efi::Guid = + efi::Guid::from_fields(0x60ff8964, 0xe906, 0x41d0, 0xaf, 0xed, &[0xf2, 0x41, 0xe9, 0x74, 0xe0, 0x8e]); + +/// Ready-to-lock handler. +/// +/// Triggered from the non-MM environment upon DxeMmReadyToLock event. +/// After this handler runs, certain features (e.g., unblock memory) are no longer available. +#[linkme::distributed_slice(SUPERVISOR_MMI_HANDLERS)] +static READY_TO_LOCK_HANDLER: SupervisorMmiHandler = SupervisorMmiHandler { + name: "MmReadyToLock", + handler_guid: EFI_DXE_MM_READY_TO_LOCK_PROTOCOL_GUID, + handle: mm_ready_to_lock_handler, +}; + +/// Supervisor request handler. +/// +/// Handles general supervisor requests such as unblock memory, fetch policy, +/// version info, and communication buffer updates. +#[linkme::distributed_slice(SUPERVISOR_MMI_HANDLERS)] +static SUPV_REQUEST_HANDLER: SupervisorMmiHandler = SupervisorMmiHandler { + name: "MmSupvRequest", + handler_guid: MM_SUPERVISOR_REQUEST_HANDLER_GUID.into_inner(), + handle: mm_supv_request_handler, +}; + +/// MmReadyToLock handler implementation. +/// +/// Called when the DXE phase signals that MM should transition to a locked state. +/// After this runs, no new memory regions can be unblocked and the memory policy +/// snapshot stored inside `PolicyGate` is considered the reference baseline. +fn mm_ready_to_lock_handler(_comm_buffer: *mut u8, _comm_buffer_size: &mut usize) -> efi::Status { + log::info!("MmReadyToLockHandler invoked"); + + let gate = match POLICY_GATE.get() { + Some(g) => g, + None => { + log::error!("MmReadyToLock: POLICY_GATE not initialized"); + return efi::Status::NOT_READY; + } + }; + + // If already locked, this is a no-op (idempotent). + if gate.is_locked() { + log::warn!("MmReadyToLock: already locked, ignoring duplicate"); + return efi::Status::SUCCESS; + } + + // Take a snapshot and mark as locked. + let cr3 = read_cr3(); + // SAFETY: cr3 points to the active PML4 table inside SMM, + // and the memory policy buffer was configured during init. + if let Err(e) = unsafe { gate.take_snapshot(cr3, is_buffer_inside_mmram) } { + log::error!("MmReadyToLock: take_snapshot failed: {:?}", e); + return efi::Status::DEVICE_ERROR; + } + + // And mark the unblock memory tracker as locked as well since unblock memory is no longer allowed after this point. + UNBLOCKED_MEMORY_TRACKER.set_core_init_complete(); + + efi::Status::SUCCESS +} + +/// Supervisor version. Encodes major.minor as (major << 16) | minor. +pub const VERSION: u32 = 0x00130008; + +/// Supervisor patch level. +pub const PATCH_LEVEL: u32 = 0x00010001; + +/// MM Supervisor request handler implementation. +/// +/// Handles structured requests from the non-MM environment, such as: +/// - [`RequestType::UnblockMem`]: Unblock memory regions +/// - [`RequestType::FetchPolicy`]: Fetch security policy +/// - [`RequestType::VersionInfo`]: Query supervisor version information +/// - [`RequestType::CommUpdate`]: Update communication buffer configuration +/// +/// The buffer is expected to contain an [`MmSupervisorRequestHeader`] at the start. +/// On return, the header's `result` field is set and any response payload follows +/// immediately after the header. +fn mm_supv_request_handler(comm_buffer: *mut u8, comm_buffer_size: &mut usize) -> efi::Status { + log::info!("MmSupvRequestHandler invoked (buffer_size={})", *comm_buffer_size); + + if comm_buffer.is_null() || *comm_buffer_size < MmSupervisorRequestHeader::SIZE { + log::error!( + "MmSupvRequestHandler: buffer too small ({} bytes, need at least {})", + *comm_buffer_size, + MmSupervisorRequestHeader::SIZE, + ); + return efi::Status::INVALID_PARAMETER; + } + + // SAFETY: We verified the buffer is non-null and large enough for the header. + let header = unsafe { &*(comm_buffer as *const MmSupervisorRequestHeader) }; + + // Validate signature + if header.signature != SIGNATURE { + log::error!("MmSupvRequestHandler: invalid signature 0x{:08X}, expected 0x{:08X}", header.signature, SIGNATURE,); + return efi::Status::INVALID_PARAMETER; + } + + // Validate revision + if header.revision > REVISION { + log::error!("MmSupvRequestHandler: unsupported revision {}, max supported {}", header.revision, REVISION,); + return efi::Status::UNSUPPORTED; + } + + // Dispatch by request type + let status = match RequestType::try_from(header.request) { + Ok(RequestType::VersionInfo) => { + log::info!("Processing VERSION_INFO request"); + handle_version_info(comm_buffer, comm_buffer_size) + } + Ok(RequestType::FetchPolicy) => { + log::info!("Processing FETCH_POLICY request"); + handle_fetch_policy(comm_buffer, comm_buffer_size) + } + Ok(RequestType::CommUpdate) => { + log::info!("Processing COMM_UPDATE request"); + handle_comm_update(comm_buffer, comm_buffer_size) + } + Ok(RequestType::UnblockMem) => { + log::info!("Processing UNBLOCK_MEM request"); + handle_unblock_mem(comm_buffer, comm_buffer_size) + } + Err(unknown) => { + log::warn!("MmSupvRequestHandler: unsupported request type 0x{:08X}", unknown); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + efi::Status::UNSUPPORTED + } + }; + + // Write the final status into the request header's result field. + write_request_result(comm_buffer, status); + + // The handler's return value is only for indicating communication-level errors + // (e.g., interrupt is being handled or not), in this case we handled the request successfully. + efi::Status::SUCCESS +} + +/// Write an [`efi::Status`] into the request header's `result` field. +/// +/// The status is stored as its raw `usize` representation cast to `u64`, +/// matching the C `MM_SUPERVISOR_REQUEST_HEADER.Result` convention. +/// +/// # Safety +/// +/// `comm_buffer` must point to at least `MmSupervisorRequestHeader::SIZE` bytes of writable memory. +fn write_request_result(comm_buffer: *mut u8, status: efi::Status) { + // SAFETY: caller guarantees buffer is large enough for the header. + unsafe { + let header = &mut *(comm_buffer as *mut MmSupervisorRequestHeader); + header.result = status.as_usize() as u64; + } +} + +/// Handle a VERSION_INFO request. +/// +/// Writes back the response header followed by [`MmSupervisorVersionInfo`]. +fn handle_version_info(comm_buffer: *mut u8, comm_buffer_size: &mut usize) -> efi::Status { + let response_size = MmSupervisorRequestHeader::SIZE + MmSupervisorVersionInfo::SIZE; + + if *comm_buffer_size < response_size { + log::error!( + "VERSION_INFO: buffer too small for response ({} bytes, need {})", + *comm_buffer_size, + response_size, + ); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::BUFFER_TOO_SMALL; + } + + // Write version info payload after the header + let version_info = MmSupervisorVersionInfo { + version: VERSION, + patch_level: PATCH_LEVEL, + max_supervisor_request_level: RequestType::MAX_REQUEST_TYPE, + }; + + // SAFETY: We verified the buffer is large enough for header + version info. + unsafe { + let payload_ptr = comm_buffer.add(MmSupervisorRequestHeader::SIZE) as *mut MmSupervisorVersionInfo; + core::ptr::write(payload_ptr, version_info); + } + + *comm_buffer_size = response_size; + log::info!( + "VERSION_INFO response: version=0x{:08X}, patch=0x{:08X}, max_level={}", + VERSION, + PATCH_LEVEL, + RequestType::MAX_REQUEST_TYPE, + ); + + efi::Status::SUCCESS +} + +/// Handle a FETCH_POLICY request. +/// +/// Returns the merged memory + firmware policy to the caller. +/// +/// ## Behaviour +/// +/// 1. **First-time call (before lock):** takes a memory policy snapshot, saves it, +/// and sets the ready-to-lock flag (whichever of `MmReadyToLock` or `FETCH_POLICY` +/// fires first performs this). +/// 2. **Subsequent calls (after lock):** re-walks the page table and compares the +/// fresh result against the saved snapshot. Any discrepancy is a security +/// violation. +/// 3. **Merges** the memory policy snapshot with the static firmware policy blob +/// from `POLICY_GATE` and writes the combined result into `comm_buffer`. +/// +/// ## Response layout +/// +/// ```text +/// |----------------------------------| +/// | MmSupervisorRequestHeader (24 B) | +/// |----------------------------------| +/// | MemDescriptorV1_0[0..N] | <- memory policy snapshot +/// |----------------------------------| +/// | SecurePolicyDataV1_0 + payload | <- firmware policy blob (raw copy) +/// |----------------------------------| +/// ``` +fn handle_fetch_policy(comm_buffer: *mut u8, comm_buffer_size: &mut usize) -> efi::Status { + log::info!("FETCH_POLICY request"); + + // -- 0. Obtain the PolicyGate ------------------------------------- + let gate = match POLICY_GATE.get() { + Some(g) => g, + None => { + log::error!("FETCH_POLICY: POLICY_GATE not initialized"); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::NOT_READY; + } + }; + + let cr3 = read_cr3(); + + // -- 1. Ensure we have a snapshot (lock if not yet locked) ------------ + if !gate.is_locked() { + // Policy requested prior to ready to lock - enforce lock now. + log::info!("FETCH_POLICY: not yet locked - taking snapshot and locking now"); + // SAFETY: cr3 is valid and the memory policy buffer was configured during init. + if let Err(e) = unsafe { gate.take_snapshot(cr3, is_buffer_inside_mmram) } { + log::error!("FETCH_POLICY: take_snapshot failed: {:?}", e); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::DEVICE_ERROR; + } + } else { + // -- 2. Already locked - verify that current page table matches snapshot + if let Err(status) = verify_policy_snapshot(gate, cr3) { + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return status; + } + } + + // -- 3. Write the merged policy into the comm buffer (after the header) - + let payload_capacity = match comm_buffer_size.checked_sub(MmSupervisorRequestHeader::SIZE) { + Some(c) => c, + None => { + log::error!("FETCH_POLICY: comm_buffer_size too small for header"); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::BUFFER_TOO_SMALL; + } + }; + + // SAFETY: comm_buffer + header offset is valid writable memory. + let dest = unsafe { comm_buffer.add(MmSupervisorRequestHeader::SIZE) }; + let payload_written = match unsafe { gate.fetch_n_update_policy(dest, payload_capacity) } { + Ok(n) => n, + Err(PolicyError::InternalError) => { + // Could be buffer-too-small, size overflow, or missing snapshot. + log::error!("FETCH_POLICY: fetch_n_update_policy failed"); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::BUFFER_TOO_SMALL; + } + Err(e) => { + log::error!("FETCH_POLICY: fetch_n_update_policy unexpected error: {:?}", e); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::DEVICE_ERROR; + } + }; + + let total_response = MmSupervisorRequestHeader::SIZE + payload_written; + *comm_buffer_size = total_response; + log::info!( + "FETCH_POLICY: response {} bytes (header={}, payload={})", + total_response, + MmSupervisorRequestHeader::SIZE, + payload_written + ); + + efi::Status::SUCCESS +} + +/// Walks the page table and compares the result against the saved snapshot +/// inside `PolicyGate`. Allocates a temporary scratch buffer from the page +/// allocator for the fresh walk. +/// +/// Returns `Ok(())` if the tables match, or an `efi::Status` error on mismatch +/// or allocation failure. +fn verify_policy_snapshot(gate: &patina_mm_policy::PolicyGate, cr3: u64) -> Result<(), efi::Status> { + let saved_count = match gate.snapshot_count() { + Some(c) => c, + None => { + log::warn!("verify_policy_snapshot: no snapshot available, skipping"); + return Ok(()); + } + }; + + let desc_size = core::mem::size_of::(); + let needed_bytes = saved_count.checked_mul(desc_size).ok_or_else(|| { + log::error!("verify_policy_snapshot: descriptor count overflow"); + efi::Status::DEVICE_ERROR + })?; + let needed_pages = (needed_bytes + 0xFFF) / 0x1000; + + let scratch_base = PAGE_ALLOCATOR.allocate_pages(needed_pages).map_err(|e| { + log::error!("verify_policy_snapshot: failed to allocate scratch buffer: {:?}", e); + efi::Status::OUT_OF_RESOURCES + })?; + + let scratch_ptr = scratch_base as *mut MemDescriptorV1_0; + let scratch_max_count = (needed_pages * 0x1000) / desc_size; + + // SAFETY: scratch_ptr was just allocated and scratch_max_count is correct. + let result = unsafe { gate.verify_snapshot(cr3, is_buffer_inside_mmram, scratch_ptr, scratch_max_count) }; + + // Free the scratch buffer regardless of the result. + let _ = PAGE_ALLOCATOR.free_pages(scratch_base, needed_pages); + + result.map_err(|e| { + log::error!("verify_policy_snapshot: snapshot verification failed: {:?}", e); + efi::Status::SECURITY_VIOLATION + }) +} + +/// Handle a COMM_UPDATE request. +/// +/// Updates the communication buffer address for future SMI entries. +fn handle_comm_update(_comm_buffer: *mut u8, comm_buffer_size: &mut usize) -> efi::Status { + log::info!("COMM_UPDATE request"); + + // We do not support dynamic communication buffer updates in this implementation, because + // we expect the runtime allocation will fall into PEI memory bin. + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + + efi::Status::ACCESS_DENIED +} + +/// Handle an UNBLOCK_MEM request. +/// +/// Unblocks a memory region so that user-mode MM drivers can access it. +/// +/// ## Validation (stricter than the C `ProcessUnblockPages` implementation) +/// +/// 1. **Ready-to-lock check** - reject if core init is complete (post-lock state). +/// 2. **Buffer size** - must hold header + [`MmSupervisorUnblockMemoryParams`]. +/// 3. **Zero-GUID** - the identifier GUID must be non-zero. +/// 4. **Page alignment** - `PhysicalStart` must be 4 KiB aligned. +/// 5. **Non-zero page count** - `NumberOfPages` must be > 0. +/// 6. **Overflow** - `NumberOfPages * UEFI_PAGE_SIZE` and `PhysicalStart + size` must not overflow. +/// 7. **MMRAM overlap** - region must not overlap supervisor RAM. +/// 8. **Duplicate / conflict** - checked by the [`UNBLOCKED_MEMORY_TRACKER`]. +/// 9. **Page attributes** - pages must be not-present (RP set) and not read-only. +/// 10. **Page table update** - make pages present, R/W, NX; optionally supervisor-only (SP). +fn handle_unblock_mem(comm_buffer: *mut u8, comm_buffer_size: &mut usize) -> efi::Status { + log::info!("UNBLOCK_MEM request"); + + // 1. Ready-to-lock check + // After core initialization is complete, unblock requests are rejected. + // This mirrors the C `mMmReadyToLockDone` guard. + if UNBLOCKED_MEMORY_TRACKER.is_core_init_complete() { + log::error!("UNBLOCK_MEM: rejected - core initialization already complete (post ready-to-lock)"); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::ACCESS_DENIED; + } + + // 2. Buffer size check + let min_size = MmSupervisorRequestHeader::SIZE + MmSupervisorUnblockMemoryParams::SIZE; + if *comm_buffer_size < min_size { + log::error!("UNBLOCK_MEM: buffer too small ({} bytes, need at least {})", *comm_buffer_size, min_size,); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::BUFFER_TOO_SMALL; + } + + // 3. Parse the payload + // SAFETY: We verified the buffer is large enough for header + params. + let params = + unsafe { &*(comm_buffer.add(MmSupervisorRequestHeader::SIZE) as *const MmSupervisorUnblockMemoryParams) }; + + let physical_start = params.memory_descriptor.physical_start; + let number_of_pages = params.memory_descriptor.number_of_pages; + let attribute = params.memory_descriptor.attribute; + let identifier_guid = params.identifier_guid; + + log::info!( + "UNBLOCK_MEM: request from {:?} - PhysicalStart=0x{:016x}, Pages={}, Attr=0x{:x}", + identifier_guid, + physical_start, + number_of_pages, + attribute, + ); + + // 4. Zero-GUID check + if *identifier_guid.as_bytes() == [0u8; 16] { + log::error!("UNBLOCK_MEM: identifier GUID is zero"); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::INVALID_PARAMETER; + } + + // 5. Page alignment check (stricter than C) + if physical_start & (UEFI_PAGE_SIZE as u64 - 1) != 0 { + log::error!("UNBLOCK_MEM: PhysicalStart 0x{:016x} is not page-aligned", physical_start,); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::INVALID_PARAMETER; + } + + // 6. Non-zero page count + if number_of_pages == 0 { + log::error!("UNBLOCK_MEM: NumberOfPages is 0"); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::INVALID_PARAMETER; + } + + // 7. Overflow checks + let region_size = match number_of_pages.checked_mul(UEFI_PAGE_SIZE as u64) { + Some(s) => s, + None => { + log::error!("UNBLOCK_MEM: NumberOfPages ({}) * UEFI_PAGE_SIZE overflows u64", number_of_pages,); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::INVALID_PARAMETER; + } + }; + + if physical_start.checked_add(region_size).is_none() { + log::error!("UNBLOCK_MEM: address range 0x{:016x} + 0x{:x} overflows", physical_start, region_size,); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::INVALID_PARAMETER; + } + + // 8. MMRAM overlap check + if PAGE_ALLOCATOR.is_region_inside_mmram(physical_start, region_size) { + log::error!( + "UNBLOCK_MEM: region 0x{:016x}-0x{:016x} overlaps with MMRAM", + physical_start, + physical_start + region_size, + ); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::SECURITY_VIOLATION; + } + + // 9. Duplicate / conflict check via tracker + // We use the tracker's region count to distinguish newly-added vs idempotent. + // For newly-added regions we must additionally verify page attributes and + // apply page table changes. For idempotent (exact duplicate) requests we + // can short-circuit with SUCCESS. + let is_supervisor_page = (attribute & efi::MEMORY_SP) != 0; + let track_attributes: u32 = if is_supervisor_page { + patina_mm_policy::RESOURCE_ATTR_READ | patina_mm_policy::RESOURCE_ATTR_WRITE | 0x80000000 // high bit tag for supervisor-only tracking + } else { + patina_mm_policy::RESOURCE_ATTR_READ | patina_mm_policy::RESOURCE_ATTR_WRITE + }; + + let count_before = UNBLOCKED_MEMORY_TRACKER.region_count(); + match UNBLOCKED_MEMORY_TRACKER.unblock_memory(physical_start, region_size, track_attributes) { + Ok(()) => { + let count_after = UNBLOCKED_MEMORY_TRACKER.region_count(); + if count_after == count_before { + // Idempotent - already tracked with same attributes, nothing more to do. + log::info!( + "UNBLOCK_MEM: region 0x{:016x}-0x{:016x} already unblocked (idempotent)", + physical_start, + physical_start + region_size, + ); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::SUCCESS; + } + // Newly added - continue to verify page attributes and update page table. + } + Err(UnblockError::ConflictingAttributes) => { + log::error!( + "UNBLOCK_MEM: region 0x{:016x}-0x{:016x} conflicts with existing entry", + physical_start, + physical_start + region_size, + ); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::SECURITY_VIOLATION; + } + Err(e) => { + log::error!( + "UNBLOCK_MEM: tracker rejected request for 0x{:016x}-0x{:016x}: {:?}", + physical_start, + physical_start + region_size, + e, + ); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::INVALID_PARAMETER; + } + } + + // 10. Verify current page attributes + // Pages must be not-present (ReadProtect) and NOT read-only. This ensures + // we only unblock pages that were explicitly guarded, matching the C + // `VerifyUnblockRequest` logic with an additional RO check. + { + let pt_guard = crate::PAGE_TABLE.lock(); + if let Some(ref pt) = *pt_guard { + match pt.query_memory_region(physical_start, region_size) { + Ok(current_attrs) => { + log::error!( + "UNBLOCK_MEM: pages at 0x{:016x} are already present (attrs: {:?}). \ + Only not-present pages may be unblocked.", + physical_start, + current_attrs, + ); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::SECURITY_VIOLATION; + } + Err(PtError::NoMapping) => { + // Expected case - pages are currently not present, so we can unblock them. + } + Err(e) => { + log::error!( + "UNBLOCK_MEM: failed to query page attributes for 0x{:016x}-0x{:016x}: {:?}", + physical_start, + physical_start + region_size, + e, + ); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::DEVICE_ERROR; + } + } + } else { + log::error!("UNBLOCK_MEM: page table not initialized"); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::NOT_READY; + } + } + + // 11. Apply page table changes + // Make the region: + // - Present (clear ReadProtect) + // - Read/Write (clear ReadOnly) + // - Non-executable (set ExecuteProtect) - data pages must be W^X + // - Optionally Supervisor-only (set Supervisor) if EFI_MEMORY_SP requested + { + let mut pt_guard = crate::PAGE_TABLE.lock(); + if let Some(ref mut pt) = *pt_guard { + let mut new_attrs = MemoryAttributes::ExecuteProtect; // NX - data pages are non-executable + if is_supervisor_page { + new_attrs = new_attrs | MemoryAttributes::Supervisor; // Supervisor-only (U/S=0) + } + + if let Err(e) = pt.map_memory_region(physical_start, region_size, new_attrs) { + log::error!( + "UNBLOCK_MEM: failed to update page table for 0x{:016x}-0x{:016x}: {:?}", + physical_start, + physical_start + region_size, + e, + ); + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + return efi::Status::DEVICE_ERROR; + } + } + // If page table is None, we already returned NOT_READY above. + } + + log::info!( + "UNBLOCK_MEM: SUCCESS - unblocked 0x{:016x}-0x{:016x} ({} pages, {})", + physical_start, + physical_start + region_size, + number_of_pages, + if is_supervisor_page { "supervisor-only" } else { "user-accessible" }, + ); + + *comm_buffer_size = MmSupervisorRequestHeader::SIZE; + efi::Status::SUCCESS +} diff --git a/patina_mm_supervisor_core/src/unblock_memory.rs b/patina_mm_supervisor_core/src/unblock_memory.rs new file mode 100644 index 000000000..4b60d56c5 --- /dev/null +++ b/patina_mm_supervisor_core/src/unblock_memory.rs @@ -0,0 +1,635 @@ +//! Unblocked Memory Region Management +//! +//! This module provides functionality to track and manage memory regions that have been +//! unblocked for access in the MM (Management Mode) environment, similar to `UnblockMemory.c`. +//! +//! ## Overview +//! +//! The MM Supervisor maintains a list of memory regions that have been explicitly unblocked +//! for access. By default, all memory outside MMRAM is blocked. Drivers and handlers can +//! request specific regions to be unblocked via the `unblock_memory` interface. +//! +//! ## Design +//! +//! - The unblocked region tracker is initialized from memory policy descriptors +//! - Regions can be dynamically added via `unblock_memory()` +//! - Access checks use `is_memory_blocked()` to validate memory access requests +//! - Duplicate unblock requests with identical attributes are allowed (idempotent) +//! - Overlapping requests with different attributes are rejected +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use core::sync::atomic::{AtomicBool, Ordering}; +use spin::Mutex; + +use patina_mm_policy::{MemDescriptorV1_0, RESOURCE_ATTR_EXECUTE, RESOURCE_ATTR_READ, RESOURCE_ATTR_WRITE}; + +use crate::mm_mem::PAGE_ALLOCATOR; + +/// Maximum number of unblocked memory regions that can be tracked. +/// This is a conservative limit; in practice, most systems will have far fewer. +const MAX_UNBLOCKED_REGIONS: usize = 256; + +/// EFI_MEMORY_SP attribute bit - Supervisor page (kernel-only access). +pub const EFI_MEMORY_SP: u64 = 0x0000000000040000; + +/// Errors that can occur during unblock memory operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UnblockError { + /// The region tracker has not been initialized. + NotInitialized, + /// Already initialized (cannot re-initialize). + AlreadyInitialized, + /// Too many regions to track (exceeded MAX_UNBLOCKED_REGIONS). + TooManyRegions, + /// The requested region overlaps with MMRAM. + OverlapsWithMmram, + /// The requested region overlaps with an existing unblocked region + /// but has different attributes. + ConflictingAttributes, + /// The requested region is already unblocked (identical request). + AlreadyUnblocked, + /// Invalid parameters (null pointer, zero length, etc.). + InvalidParameter, + /// The region's address + size would overflow. + AddressOverflow, +} + +/// A single entry in the unblocked memory region list. +#[derive(Clone, Copy, Debug, Default)] +pub struct UnblockedMemoryEntry { + /// Base address of the unblocked region. + pub base_address: u64, + /// Size of the unblocked region in bytes. + pub size: u64, + /// Memory attributes (combination of `RESOURCE_ATTR_*`). + pub attributes: u32, + /// Whether this entry is valid (in use). + pub valid: bool, +} + +impl UnblockedMemoryEntry { + /// Creates a new empty (invalid) entry. + pub const fn empty() -> Self { + Self { base_address: 0, size: 0, attributes: 0, valid: false } + } + + /// Creates a new valid entry from base, size, and attributes. + pub const fn new(base_address: u64, size: u64, attributes: u32) -> Self { + Self { base_address, size, attributes, valid: true } + } + + /// Returns the end address (exclusive) of this region. + pub fn end_address(&self) -> u64 { + self.base_address.saturating_add(self.size) + } + + /// Checks if the given range [base, base + size) is fully contained within this entry. + pub fn contains(&self, base: u64, size: u64) -> bool { + if !self.valid || size == 0 { + return false; + } + let query_end = base.saturating_add(size); + base >= self.base_address && query_end <= self.end_address() + } + + /// Checks if the given range [base, base + size) overlaps with this entry. + pub fn overlaps(&self, base: u64, size: u64) -> bool { + if !self.valid || size == 0 { + return false; + } + let query_end = base.saturating_add(size); + let entry_end = self.end_address(); + + // Two ranges overlap if: start1 < end2 && start2 < end1 + base < entry_end && self.base_address < query_end + } + + /// Checks if this entry has identical base, size, and attributes as the query. + pub fn is_identical(&self, base: u64, size: u64, attributes: u32) -> bool { + self.valid && self.base_address == base && self.size == size && self.attributes == attributes + } +} + +/// Internal state for the unblocked memory tracker. +struct UnblockedMemoryState { + /// Array of unblocked memory entries. + entries: [UnblockedMemoryEntry; MAX_UNBLOCKED_REGIONS], + /// Number of valid entries in the array. + count: usize, +} + +impl UnblockedMemoryState { + /// Creates a new empty state. + const fn new() -> Self { + Self { entries: [UnblockedMemoryEntry::empty(); MAX_UNBLOCKED_REGIONS], count: 0 } + } + + /// Finds an entry that exactly matches the given base and size. + fn find_exact_match(&self, base: u64, size: u64) -> Option<&UnblockedMemoryEntry> { + self.entries[..self.count].iter().find(|e| e.valid && e.base_address == base && e.size == size) + } + + /// Finds all entries that overlap with the given range. + #[allow(dead_code)] + fn find_overlapping(&self, base: u64, size: u64) -> impl Iterator { + self.entries[..self.count].iter().filter(move |e| e.overlaps(base, size)) + } + + /// Adds a new entry if there's space. + fn add_entry(&mut self, base: u64, size: u64, attributes: u32) -> Result<(), UnblockError> { + if self.count >= MAX_UNBLOCKED_REGIONS { + return Err(UnblockError::TooManyRegions); + } + + self.entries[self.count] = UnblockedMemoryEntry::new(base, size, attributes); + self.count += 1; + Ok(()) + } +} + +/// Global unblocked memory region tracker. +/// +/// This struct manages a list of memory regions that have been unblocked for +/// access within the MM environment. It provides thread-safe access to the +/// region list through internal locking. +pub struct UnblockedMemoryTracker { + /// Whether the tracker has been initialized. + initialized: AtomicBool, + /// Flag indicating if core initialization is complete (after which we enforce checks). + core_init_complete: AtomicBool, + /// Internal state protected by a mutex. + state: Mutex, +} + +impl UnblockedMemoryTracker { + /// Creates a new unblocked memory tracker. + pub const fn new() -> Self { + Self { + initialized: AtomicBool::new(false), + core_init_complete: AtomicBool::new(false), + state: Mutex::new(UnblockedMemoryState::new()), + } + } + + /// Initializes the tracker from an array of memory policy descriptors. + /// + /// This should be called once during BSP initialization after the memory + /// policy has been generated from the page table walk. + /// + /// # Arguments + /// + /// * `descriptors` - Slice of memory policy descriptors from page table walk. + /// These represent the initial "unblocked" regions. + /// + /// # Returns + /// + /// `Ok(())` if initialization succeeded, or an error if: + /// - Already initialized + /// - Too many descriptors to track + pub fn init_from_descriptors(&self, descriptors: &[MemDescriptorV1_0]) -> Result<(), UnblockError> { + // Check if already initialized + if self.initialized.swap(true, Ordering::SeqCst) { + return Err(UnblockError::AlreadyInitialized); + } + + let mut state = self.state.lock(); + + // Add each descriptor as an unblocked region + for desc in descriptors { + if desc.size == 0 { + continue; // Skip zero-size entries + } + + // Skip regions inside MMRAM - those are supervisor-controlled, not "unblocked" + if PAGE_ALLOCATOR.is_region_inside_mmram(desc.base_address, desc.size) { + log::trace!( + "Skipping MMRAM region during unblock init: 0x{:016x} - 0x{:016x}", + desc.base_address, + desc.base_address.saturating_add(desc.size) + ); + continue; + } + + state.add_entry(desc.base_address, desc.size, desc.mem_attributes)?; + } + + log::info!("UnblockedMemoryTracker initialized with {} regions", state.count); + + Ok(()) + } + + /// Initializes the tracker from a raw buffer of memory policy descriptors. + /// + /// # Safety + /// + /// The caller must ensure: + /// - `buffer` points to a valid array of `MemDescriptorV1_0` structures + /// - `count` is the number of valid entries in the buffer + pub unsafe fn init_from_buffer(&self, buffer: *const MemDescriptorV1_0, count: usize) -> Result<(), UnblockError> { + if buffer.is_null() || count == 0 { + // Empty initialization is valid + if self.initialized.swap(true, Ordering::SeqCst) { + return Err(UnblockError::AlreadyInitialized); + } + log::info!("UnblockedMemoryTracker initialized with 0 regions (empty)"); + return Ok(()); + } + + // SAFETY: Caller guarantees buffer is valid for count entries + let descriptors = unsafe { core::slice::from_raw_parts(buffer, count) }; + self.init_from_descriptors(descriptors) + } + + /// Marks core initialization as complete. + /// + /// After this is called, memory access checks will be enforced. + /// Before this, all memory is considered accessible (for bootstrap). + pub fn set_core_init_complete(&self) { + self.core_init_complete.store(true, Ordering::Release); + log::info!("UnblockedMemoryTracker: Core initialization complete, enforcing checks"); + } + + /// Checks if core initialization is complete. + pub fn is_core_init_complete(&self) -> bool { + self.core_init_complete.load(Ordering::Acquire) + } + + /// Unblocks a memory region for access. + /// + /// This adds a new region to the unblocked list after validating: + /// - The region does not overlap with MMRAM + /// - The region is not already unblocked with different attributes + /// - Identical unblock requests are allowed (idempotent) + /// + /// # Arguments + /// + /// * `base` - Base address of the region to unblock + /// * `size` - Size of the region in bytes + /// * `attributes` - Memory attributes (RESOURCE_ATTR_READ | WRITE | EXECUTE) + /// + /// # Returns + /// + /// `Ok(())` if the region was successfully unblocked or already unblocked with same attributes. + /// `Err(UnblockError)` if the request is invalid or conflicts with existing regions. + pub fn unblock_memory(&self, base: u64, size: u64, attributes: u32) -> Result<(), UnblockError> { + // Validate parameters + if size == 0 { + return Err(UnblockError::InvalidParameter); + } + + // Check for address overflow + if base.checked_add(size).is_none() { + return Err(UnblockError::AddressOverflow); + } + + // Check if the region overlaps with MMRAM + if PAGE_ALLOCATOR.is_region_inside_mmram(base, size) { + log::error!( + "unblock_memory: Region 0x{:016x} - 0x{:016x} overlaps with MMRAM", + base, + base.saturating_add(size) + ); + return Err(UnblockError::OverlapsWithMmram); + } + + let mut state = self.state.lock(); + + // Check for existing entries that might conflict + // First, check for exact match (idempotent unblock) + if let Some(existing) = state.find_exact_match(base, size) { + if existing.attributes == attributes { + // Identical request - this is allowed (idempotent) + log::debug!( + "unblock_memory: Region 0x{:016x} - 0x{:016x} already unblocked with same attributes", + base, + base.saturating_add(size) + ); + return Ok(()); + } else { + // Same base/size but different attributes - conflict + log::error!( + "unblock_memory: Region 0x{:016x} - 0x{:016x} already unblocked with different attributes (existing: 0x{:x}, requested: 0x{:x})", + base, + base.saturating_add(size), + existing.attributes, + attributes + ); + return Err(UnblockError::ConflictingAttributes); + } + } + + // Check for partial overlaps (not allowed) + // We iterate directly without collecting to avoid heap allocation + let mut has_overlap = false; + for entry in &state.entries[..state.count] { + if entry.overlaps(base, size) { + log::error!( + "unblock_memory: Region 0x{:016x} - 0x{:016x} overlaps with existing region 0x{:016x} - 0x{:016x}", + base, + base.saturating_add(size), + entry.base_address, + entry.end_address() + ); + has_overlap = true; + // Continue to log all overlaps for debugging + } + } + + if has_overlap { + return Err(UnblockError::ConflictingAttributes); + } + + // No conflicts - add the new entry + state.add_entry(base, size, attributes)?; + + log::info!( + "unblock_memory: Unblocked region 0x{:016x} - 0x{:016x} with attributes 0x{:x}", + base, + base.saturating_add(size), + attributes + ); + + Ok(()) + } + + /// Checks if a memory region is blocked (i.e., NOT in the unblocked list). + /// + /// This is the inverse of checking if memory is accessible - a blocked region + /// should not be accessed by MM handlers. + /// + /// # Arguments + /// + /// * `base` - Base address of the region to check + /// * `size` - Size of the region in bytes + /// + /// # Returns + /// + /// `true` if the region is blocked (not accessible), `false` if unblocked. + /// + /// # Note + /// + /// Before core initialization is complete, this always returns `false` + /// (everything is accessible during bootstrap). + pub fn is_memory_blocked(&self, base: u64, size: u64) -> bool { + // During initialization, everything is accessible + if !self.core_init_complete.load(Ordering::Acquire) { + return false; + } + + // Zero-size queries are invalid + if size == 0 { + log::warn!("is_memory_blocked: Zero-size query for address 0x{:016x}", base); + return true; // Invalid query = blocked + } + + // Check for address overflow + if base.checked_add(size).is_none() { + log::warn!("is_memory_blocked: Address overflow for 0x{:016x} + 0x{:x}", base, size); + return true; // Invalid query = blocked + } + + let state = self.state.lock(); + + // Check if the queried region is fully contained within any unblocked entry + for entry in &state.entries[..state.count] { + if entry.contains(base, size) { + log::trace!( + "is_memory_blocked: Region 0x{:016x} - 0x{:016x} is within unblocked region 0x{:016x} - 0x{:016x}", + base, + base.saturating_add(size), + entry.base_address, + entry.end_address() + ); + return false; // Found within unblocked region + } + } + + log::trace!( + "is_memory_blocked: Region 0x{:016x} - 0x{:016x} is NOT within any unblocked region", + base, + base.saturating_add(size) + ); + + true // Not found in any unblocked region = blocked + } + + /// Checks if a memory region is within unblocked regions (the inverse of `is_memory_blocked`). + /// + /// This is a convenience method that returns `true` if the region is accessible. + #[inline] + pub fn is_within_unblocked_region(&self, base: u64, size: u64) -> bool { + !self.is_memory_blocked(base, size) + } + + /// Gets the current count of unblocked regions. + pub fn region_count(&self) -> usize { + self.state.lock().count + } + + /// Collects unblocked regions into a provided buffer. + /// + /// This is useful for reporting or serializing the unblocked region list. + /// + /// # Arguments + /// + /// * `start_index` - Starting index in the region list + /// * `buffer` - Buffer to fill with region descriptors + /// + /// # Returns + /// + /// The number of entries actually copied to the buffer. + pub fn collect_regions(&self, start_index: usize, buffer: &mut [MemDescriptorV1_0]) -> usize { + let state = self.state.lock(); + + if start_index >= state.count || buffer.is_empty() { + return 0; + } + + let mut copied = 0; + for (i, entry) in state.entries[start_index..state.count].iter().enumerate() { + if i >= buffer.len() { + break; + } + if entry.valid { + buffer[i] = MemDescriptorV1_0 { + base_address: entry.base_address, + size: entry.size, + mem_attributes: entry.attributes, + reserved: 0, + }; + copied += 1; + } + } + + copied + } + + /// Dumps the unblocked regions for debugging. + pub fn dump_regions(&self) { + let state = self.state.lock(); + + log::info!("UnblockedMemoryTracker: {} regions", state.count); + for (i, entry) in state.entries[..state.count].iter().enumerate() { + if entry.valid { + let r = if (entry.attributes & RESOURCE_ATTR_READ) != 0 { "R" } else { "." }; + let w = if (entry.attributes & RESOURCE_ATTR_WRITE) != 0 { "W" } else { "." }; + let x = if (entry.attributes & RESOURCE_ATTR_EXECUTE) != 0 { "X" } else { "." }; + log::info!(" [{}] 0x{:016x} - 0x{:016x} {}{}{}", i, entry.base_address, entry.end_address(), r, w, x); + } + } + } +} + +/// Global unblocked memory tracker instance. +/// +/// This is the singleton that manages all unblocked memory regions for the +/// MM Supervisor. It should be initialized during BSP initialization and +/// used for all memory access validation. +pub static UNBLOCKED_MEMORY_TRACKER: UnblockedMemoryTracker = UnblockedMemoryTracker::new(); + +#[cfg(test)] +mod tests { + use super::*; + + fn create_test_tracker() -> UnblockedMemoryTracker { + UnblockedMemoryTracker::new() + } + + #[test] + fn test_empty_entry() { + let entry = UnblockedMemoryEntry::empty(); + assert!(!entry.valid); + assert_eq!(entry.base_address, 0); + assert_eq!(entry.size, 0); + } + + #[test] + fn test_entry_contains() { + let entry = UnblockedMemoryEntry::new(0x1000, 0x1000, RESOURCE_ATTR_READ); + + // Fully contained + assert!(entry.contains(0x1000, 0x1000)); + assert!(entry.contains(0x1000, 0x800)); + assert!(entry.contains(0x1800, 0x800)); + + // Partially outside + assert!(!entry.contains(0x0800, 0x1000)); // Starts before + assert!(!entry.contains(0x1800, 0x1000)); // Ends after + + // Completely outside + assert!(!entry.contains(0x3000, 0x1000)); + } + + #[test] + fn test_entry_overlaps() { + let entry = UnblockedMemoryEntry::new(0x1000, 0x1000, RESOURCE_ATTR_READ); + + // Overlapping cases + assert!(entry.overlaps(0x1000, 0x1000)); // Exact match + assert!(entry.overlaps(0x0800, 0x1000)); // Starts before, ends inside + assert!(entry.overlaps(0x1800, 0x1000)); // Starts inside, ends after + assert!(entry.overlaps(0x0800, 0x2000)); // Completely contains entry + + // Non-overlapping + assert!(!entry.overlaps(0x2000, 0x1000)); // Immediately after + assert!(!entry.overlaps(0x0000, 0x1000)); // Immediately before + assert!(!entry.overlaps(0x3000, 0x1000)); // Far after + } + + #[test] + fn test_tracker_before_init_complete() { + let tracker = create_test_tracker(); + + // Before core init complete, nothing is blocked + assert!(!tracker.is_memory_blocked(0x1000, 0x1000)); + assert!(!tracker.is_memory_blocked(0x0, 0x100000)); + } + + #[test] + fn test_tracker_after_init_complete_empty() { + let tracker = create_test_tracker(); + tracker.set_core_init_complete(); + + // After init complete with no regions, everything is blocked + assert!(tracker.is_memory_blocked(0x1000, 0x1000)); + } + + #[test] + fn test_unblock_memory() { + let tracker = create_test_tracker(); + + // Unblock a region + assert!(tracker.unblock_memory(0x1000, 0x1000, RESOURCE_ATTR_READ | RESOURCE_ATTR_WRITE).is_ok()); + + tracker.set_core_init_complete(); + + // Region should be accessible + assert!(!tracker.is_memory_blocked(0x1000, 0x1000)); + assert!(!tracker.is_memory_blocked(0x1000, 0x800)); + + // Outside region should be blocked + assert!(tracker.is_memory_blocked(0x3000, 0x1000)); + } + + #[test] + fn test_idempotent_unblock() { + let tracker = create_test_tracker(); + + // First unblock + assert!(tracker.unblock_memory(0x1000, 0x1000, RESOURCE_ATTR_READ).is_ok()); + + // Identical unblock should succeed + assert!(tracker.unblock_memory(0x1000, 0x1000, RESOURCE_ATTR_READ).is_ok()); + + // Same region with different attributes should fail + assert_eq!( + tracker.unblock_memory(0x1000, 0x1000, RESOURCE_ATTR_READ | RESOURCE_ATTR_WRITE), + Err(UnblockError::ConflictingAttributes) + ); + } + + #[test] + fn test_overlapping_unblock_fails() { + let tracker = create_test_tracker(); + + // First unblock + assert!(tracker.unblock_memory(0x1000, 0x1000, RESOURCE_ATTR_READ).is_ok()); + + // Overlapping unblock should fail + assert_eq!( + tracker.unblock_memory(0x1800, 0x1000, RESOURCE_ATTR_READ), + Err(UnblockError::ConflictingAttributes) + ); + } + + #[test] + fn test_invalid_parameters() { + let tracker = create_test_tracker(); + tracker.set_core_init_complete(); + + // Zero size + assert_eq!(tracker.unblock_memory(0x1000, 0, RESOURCE_ATTR_READ), Err(UnblockError::InvalidParameter)); + + // Overflow + assert_eq!(tracker.unblock_memory(u64::MAX, 0x1000, RESOURCE_ATTR_READ), Err(UnblockError::AddressOverflow)); + } + + #[test] + fn test_region_count() { + let tracker = create_test_tracker(); + + assert_eq!(tracker.region_count(), 0); + + tracker.unblock_memory(0x1000, 0x1000, RESOURCE_ATTR_READ).unwrap(); + assert_eq!(tracker.region_count(), 1); + + tracker.unblock_memory(0x3000, 0x1000, RESOURCE_ATTR_WRITE).unwrap(); + assert_eq!(tracker.region_count(), 2); + } +} diff --git a/patina_mm_user_core/Cargo.toml b/patina_mm_user_core/Cargo.toml new file mode 100644 index 000000000..317e3273d --- /dev/null +++ b/patina_mm_user_core/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "patina_mm_user_core" +version.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +readme = "README.md" +description = "A pure Rust implementation of the MM User Core for standalone MM mode environments." + +# Metadata to tell docs.rs how to build the documentation when uploading +[package.metadata.docs.rs] +features = ["doc"] + +[dependencies] +goblin = { workspace = true, features = ["pe32", "pe64"] } +log = { workspace = true } +patina = { workspace = true } +patina_internal_depex = { workspace = true } +patina_internal_mm_common = { workspace = true } +patina_adv_logger = { workspace = true } +r-efi = { workspace = true } +spin = { workspace = true } +uuid = { workspace = true } + +[dev-dependencies] +mockall = { workspace = true } +serial_test = { workspace = true } + +[features] +default = [] +std = [] +doc = [] diff --git a/patina_mm_user_core/README.md b/patina_mm_user_core/README.md new file mode 100644 index 000000000..4697f3e16 --- /dev/null +++ b/patina_mm_user_core/README.md @@ -0,0 +1,168 @@ +# Patina MM Supervisor Core + +A pure Rust implementation of the MM Supervisor Core for standalone MM mode environments. + +## Overview + +This crate provides the core functionality for the MM (Management Mode) Supervisor in a standalone MM environment. It is designed to run on x64 systems where: + +- Page tables are already set up by the pre-MM phase +- All images are loaded and ready to execute +- The BSP (Bootstrap Processor) orchestrates incoming requests +- APs (Application Processors) wait in a holding pen, checking a mailbox for work + +## Memory Model + +The core can be instantiated as a `static` with no runtime allocation required for core data structures. + +## Building a PE/COFF Binary + +### Prerequisites + +1. Install the Rust UEFI target: + ```bash + rustup target add x86_64-unknown-uefi + ``` + +2. Ensure you have the nightly toolchain (required for `#![feature(...)]`): + ```bash + rustup override set nightly + ``` + +### Build Command + +Build the example MM Supervisor binary: + +```bash +cargo build --release --target x86_64-unknown-uefi --bin example_mm_supervisor +``` + +The output PE/COFF binary will be at: +``` +target/x86_64-unknown-uefi/release/example_mm_supervisor.efi +``` + +### Entry Point + +The MM Supervisor exports `MmSupervisorMain` as its entry point, matching the EDK2 convention: + +```rust +#[unsafe(export_name = "MmSupervisorMain")] +pub extern "efiapi" fn mm_supervisor_main(hob_list: *const c_void) -> ! { + SUPERVISOR.entry_point(hob_list) +} +``` + +The MM IPL (Initial Program Loader) calls this entry point on **all processors** after: +1. Loading the supervisor image into MMRAM +2. Setting up page tables +3. Constructing the HOB list with MMRAM ranges + +## Architecture + +### Entry Point Model + +The entry point is executed on all cores simultaneously: + +1. **BSP (Bootstrap Processor)**: + - First CPU to arrive (determined by atomic counter) + - Performs one-time initialization + - Sets up the request handling infrastructure + - Enters the main request serving loop + +2. **APs (Application Processors)**: + - All other CPUs + - Wait for BSP initialization to complete + - Enter a holding pen and poll mailboxes for commands + +### Mailbox System + +The mailbox system provides inter-processor communication: + +- Each AP has a dedicated mailbox (cache-line aligned to avoid false sharing) +- BSP sends commands to APs via mailboxes +- APs respond with results through the same mailbox +- Supports synchronization primitives for coordinated operations + +## Usage + +### Basic Platform Implementation + +```rust +#![no_std] +#![no_main] + +use core::{ffi::c_void, panic::PanicInfo}; +use patina_mm_supervisor_core::*; + +struct MyPlatform; + +impl CpuInfo for MyPlatform { + fn ap_poll_timeout_us() -> u64 { 1000 } +} + + + +// Static instance - no heap allocation required +static SUPERVISOR: MmSupervisorCore = MmSupervisorCore::new(); + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop { core::hint::spin_loop(); } +} + +#[unsafe(export_name = "MmSupervisorMain")] +pub extern "efiapi" fn mm_supervisor_main(hob_list: *const c_void) -> ! { + SUPERVISOR.entry_point(hob_list) +} +``` + +### Registering Request Handlers + +Handlers must be defined as static references: + +```rust +use patina_mm_supervisor_core::*; + +struct MyHandler; + +impl RequestHandler for MyHandler { + fn guid(&self) -> r_efi::efi::Guid { + // Your handler's GUID + r_efi::efi::Guid::from_fields(0x12345678, 0x1234, 0x5678, 0x12, 0x34, &[0; 6]) + } + + fn handle(&self, context: &mut RequestContext) -> RequestResult { + // Handle the request + RequestResult::Success + } + + fn name(&self) -> &'static str { + "MyHandler" + } +} + +static MY_HANDLER: MyHandler = MyHandler; + +// Register before calling entry_point, or during BSP initialization +SUPERVISOR.register_handler(&MY_HANDLER); +``` + +### Integration with MM IPL + +The MM IPL (from EDK2/MmSupervisorPkg) loads this binary and calls the entry point. The HOB list passed contains: + +- `gEfiMmPeiMmramMemoryReserveGuid` - MMRAM ranges +- `gMmCommBufferHobGuid` - Communication buffer information +- `gMmCommonRegionHobGuid` - Common memory regions +- FV HOBs for MM driver firmware volumes + +## Example Binary + +See [bin/example_mm_supervisor.rs](bin/example_mm_supervisor.rs) for a complete example platform implementation. + +## License + +Copyright (c) Microsoft Corporation. + +SPDX-License-Identifier: Apache-2.0 diff --git a/patina_mm_user_core/src/config_table.rs b/patina_mm_user_core/src/config_table.rs new file mode 100644 index 000000000..30661b784 --- /dev/null +++ b/patina_mm_user_core/src/config_table.rs @@ -0,0 +1,188 @@ +//! Configuration Table Management for MM System Table +//! +//! Implements `MmInstallConfigurationTable` — the ability to add, modify, or +//! delete (GUID, pointer) pairs stored in the MM System Table's configuration +//! table array. +//! +//! ## Semantics +//! +//! The configuration table is an array of `EFI_CONFIGURATION_TABLE` entries +//! exposed through `EfiMmSystemTable.mm_configuration_table`. Drivers use +//! this to publish well-known data (e.g. the HOB list) that other drivers +//! can discover by iterating the table. +//! +//! Operations: +//! - **Add**: GUID not present, table pointer non-null. +//! - **Modify**: GUID already present, table pointer non-null → update pointer. +//! - **Delete**: GUID already present, table pointer null → remove entry. +//! - **Error**: GUID not present, table pointer null → `NOT_FOUND`. +//! +//! After every modification the MM System Table's `mm_configuration_table` +//! pointer and `number_of_table_entries` count are updated so the change is +//! immediately visible to all consumers. +//! +//! ## Thread Safety +//! +//! All access is serialized through a `spin::Mutex`. The configuration table +//! array pointer stored in the system table is replaced atomically (pointer- +//! sized write) so readers always see a consistent snapshot even without +//! holding the lock. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +extern crate alloc; + +use alloc::boxed::Box; +use alloc::vec::Vec; +use core::ffi::c_void; + +use r_efi::efi; +use spin::Mutex; + +use crate::mm_services::get_mm_system_table; + +/// Global configuration table database used by the MM System Table. +pub static GLOBAL_CONFIG_TABLE_DB: MmConfigurationTableDb = MmConfigurationTableDb::new(); + +/// Configuration table database for the MM System Table. +/// +/// Maintains the canonical list of `(GUID, Pointer)` pairs. After each +/// mutation the MM System Table's pointer and count are updated so all +/// consumers see the change immediately. +pub struct MmConfigurationTableDb { + inner: Mutex, +} + +struct ConfigTableInner { + /// The authoritative list of entries. + entries: Vec, + /// Raw pointer to the most recently leaked boxed‐slice that the system + /// table currently points to. `null` when no allocation exists. + leaked_ptr: *mut efi::ConfigurationTable, + /// Length of the leaked allocation (for reclaim). + leaked_len: usize, +} + +// SAFETY: All access is synchronized by the Mutex. +unsafe impl Send for MmConfigurationTableDb {} +unsafe impl Sync for MmConfigurationTableDb {} + +impl MmConfigurationTableDb { + /// Create a new, empty configuration table database. + pub const fn new() -> Self { + Self { + inner: Mutex::new(ConfigTableInner { + entries: Vec::new(), + leaked_ptr: core::ptr::null_mut(), + leaked_len: 0, + }), + } + } + + /// Install, modify, or delete a configuration table entry. + /// + /// Semantics match the PI Specification `EFI_MM_INSTALL_CONFIGURATION_TABLE`: + /// + /// | Existing? | `table` | Action | + /// |-----------|---------|----------| + /// | Yes | non-null| Modify | + /// | Yes | null | Delete | + /// | No | non-null| Add | + /// | No | null | NOT_FOUND| + pub fn install_configuration_table(&self, guid: &efi::Guid, table: *mut c_void) -> efi::Status { + let mut inner = self.inner.lock(); + + // Search for an existing entry with the same GUID. + let existing_idx = inner.entries.iter().position(|e| e.vendor_guid == *guid); + + match (existing_idx, table.is_null()) { + // Match found, table non-null → modify + (Some(idx), false) => { + inner.entries[idx].vendor_table = table; + log::debug!("MmInstallConfigurationTable: modified {:?}", guid); + } + // Match found, table null → delete + (Some(idx), true) => { + inner.entries.remove(idx); + log::debug!("MmInstallConfigurationTable: deleted {:?}", guid); + } + // No match, table non-null → add + (None, false) => { + inner.entries.push(efi::ConfigurationTable { vendor_guid: *guid, vendor_table: table }); + log::debug!("MmInstallConfigurationTable: added {:?}", guid); + } + // No match, table null → error + (None, true) => { + return efi::Status::NOT_FOUND; + } + } + + // Publish the updated table to the MM System Table. + Self::publish_to_system_table(&mut inner); + + efi::Status::SUCCESS + } + + /// Look up a configuration table entry by GUID. + /// + /// Returns the `vendor_table` pointer if found, or `None`. + #[allow(dead_code)] + pub fn get_configuration_table(&self, guid: &efi::Guid) -> Option<*mut c_void> { + let inner = self.inner.lock(); + inner.entries.iter().find(|e| e.vendor_guid == *guid).map(|e| e.vendor_table) + } + + // ------------------------------------------------------------------------- + // Internal helpers + // ------------------------------------------------------------------------- + + /// Re‐publish the current entry list to the MM System Table. + /// + /// Allocates a new boxed slice, updates the system table pointer, then + /// reclaims the previous allocation. + fn publish_to_system_table(inner: &mut ConfigTableInner) { + let mmst = get_mm_system_table(); + if mmst.is_null() { + return; + } + + // --- Reclaim the previous allocation --------------------------------- + if !inner.leaked_ptr.is_null() { + // SAFETY: `leaked_ptr` / `leaked_len` were produced by + // `Box::into_raw` in a prior call to this function. + unsafe { + let _ = Box::from_raw(core::ptr::slice_from_raw_parts_mut(inner.leaked_ptr, inner.leaked_len)); + } + inner.leaked_ptr = core::ptr::null_mut(); + inner.leaked_len = 0; + } + + // --- Produce the new allocation and patch the MMST ------------------- + if inner.entries.is_empty() { + // SAFETY: We hold the Mutex, and `mmst` was initialised by + // `init_mm_system_table`. + unsafe { + (*mmst).number_of_table_entries = 0; + (*mmst).mm_configuration_table = core::ptr::null_mut(); + } + } else { + let boxed: Box<[efi::ConfigurationTable]> = inner.entries.clone().into_boxed_slice(); + let len = boxed.len(); + let ptr = Box::into_raw(boxed) as *mut efi::ConfigurationTable; + + inner.leaked_ptr = ptr; + inner.leaked_len = len; + + // SAFETY: Same as above. + unsafe { + (*mmst).number_of_table_entries = len; + (*mmst).mm_configuration_table = ptr; + } + } + } +} diff --git a/patina_mm_user_core/src/core_handlers.rs b/patina_mm_user_core/src/core_handlers.rs new file mode 100644 index 000000000..cc5493ce8 --- /dev/null +++ b/patina_mm_user_core/src/core_handlers.rs @@ -0,0 +1,280 @@ +//! MM Core Internal MMI Handlers +//! +//! These are the MMI handlers registered by the MM Core itself to handle +//! lifecycle events forwarded from the DXE phase. They mirror the C +//! `mMmCoreMmiHandlers[]` table in `StandaloneMmCore.c`. +//! +//! Each handler is registered with [`MmiDatabase::register_internal_handler`] +//! during startup and dispatched when the supervisor forwards the corresponding +//! GUID-tagged MMI through the communication buffer. +//! +//! ## Lifecycle Events +//! +//! | GUID | Handler | Description | +//! |------|---------|-------------| +//! | `MM_DISPATCH_EVENT` | [`mm_driver_dispatch_handler`] | Re-triggers driver dispatch | +//! | `MM_DXE_READY_TO_LOCK_PROTOCOL` | [`mm_ready_to_lock_handler`] | Unregisters one-shot handlers, installs lock protocol | +//! | `MM_END_OF_PEI_PROTOCOL` | [`mm_end_of_pei_handler`] | Installs end-of-PEI protocol | +//! | `EVENT_GROUP_END_OF_DXE` | [`mm_end_of_dxe_handler`] | Installs end-of-DXE protocol | +//! | `EVENT_EXIT_BOOT_SERVICES` | [`mm_exit_boot_service_handler`] | Installs exit-boot-services protocol | +//! | `EVENT_READY_TO_BOOT` | [`mm_ready_to_boot_handler`] | Installs ready-to-boot protocol | +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use core::ffi::c_void; + +use r_efi::efi; +use spin::Mutex; + +use crate::mmi::InternalMmiHandler; +use patina::{BinaryGuid, guids}; + +/// Description of a core MMI handler to be registered at startup. +struct CoreMmiHandler { + /// The handler function (native Rust signature). + handler: InternalMmiHandler, + /// The GUID that triggers this handler. + handler_type: &'static BinaryGuid, + /// Whether this handler should be unregistered during ready-to-lock. + unregister_on_lock: bool, +} + +/// Table of MMI handlers registered by the MM Core, mirroring the C `mMmCoreMmiHandlers[]`. +static CORE_MMI_HANDLERS: &[CoreMmiHandler] = &[ + CoreMmiHandler { + handler: mm_driver_dispatch_handler, + handler_type: &guids::MM_DISPATCH_EVENT, + unregister_on_lock: false, + }, + CoreMmiHandler { + handler: mm_ready_to_lock_handler, + handler_type: &guids::MM_DXE_READY_TO_LOCK_PROTOCOL, + unregister_on_lock: true, + }, + CoreMmiHandler { + handler: mm_end_of_pei_handler, + handler_type: &guids::MM_END_OF_PEI_PROTOCOL, + unregister_on_lock: false, + }, + CoreMmiHandler { + handler: mm_end_of_dxe_handler, + handler_type: &guids::EVENT_GROUP_END_OF_DXE, + unregister_on_lock: false, + }, + CoreMmiHandler { + handler: mm_exit_boot_service_handler, + handler_type: &guids::EVENT_EXIT_BOOT_SERVICES, + unregister_on_lock: false, + }, + CoreMmiHandler { + handler: mm_ready_to_boot_handler, + handler_type: &guids::EVENT_READY_TO_BOOT, + unregister_on_lock: false, + }, +]; + +/// Newtype wrapper around `efi::Handle` so it can be stored in a `static Mutex`. +/// +/// `efi::Handle` is `*mut c_void` which is `!Send`. The dispatch handles are only +/// written by the BSP during single-threaded init and read during the ready-to-lock +/// handler (also on the BSP), so it is safe to share them. +#[derive(Clone, Copy)] +struct SendHandle(efi::Handle); +unsafe impl Send for SendHandle {} +unsafe impl Sync for SendHandle {} + +impl SendHandle { + const NULL: Self = Self(core::ptr::null_mut()); +} + +/// Dispatch handles returned from `register_internal_handler` for each core handler. +/// +/// Index matches the `CORE_MMI_HANDLERS` table. Populated by [`register_core_mmi_handlers`]. +static DISPATCH_HANDLES: Mutex<[SendHandle; 6]> = Mutex::new([SendHandle::NULL; 6]); + +/// Register all core MMI handlers with the global MMI database. +/// +/// This must be called after driver dispatch (matching the C `StandaloneMmMain` +/// ordering where handlers are registered after `MmDispatchFvs`). +pub fn register_core_mmi_handlers() { + let mut handles = DISPATCH_HANDLES.lock(); + + for (i, entry) in CORE_MMI_HANDLERS.iter().enumerate() { + match crate::mm_services::GLOBAL_MMI_DB.register_internal_handler(entry.handler, Some(entry.handler_type)) { + Ok(handle) => { + handles[i] = SendHandle(handle); + log::info!("Registered core MMI handler [{}] for {:?}", i, entry.handler_type,); + } + Err(status) => { + log::error!("Failed to register core MMI handler [{}] for {:?}: {:?}", i, entry.handler_type, status,); + } + } + } +} + +/// Install a protocol with a NULL interface on a new handle. +/// +/// This mirrors the C pattern used in lifecycle handlers: +/// ```c +/// MmHandle = NULL; +/// Status = MmInstallProtocolInterface(&MmHandle, &guid, EFI_NATIVE_INTERFACE, NULL); +/// ``` +fn install_lifecycle_protocol(guid: &efi::Guid) -> efi::Status { + let (handle, pending_notifies) = match crate::mm_services::GLOBAL_PROTOCOL_DB.install_protocol( + core::ptr::null_mut(), // NULL handle → allocate new + guid, + core::ptr::null_mut(), // NULL interface + ) { + Ok(result) => result, + Err(status) => return status, + }; + + log::info!("Installed lifecycle protocol {:?} on handle {:p}", guid, handle,); + + // Fire pending notify callbacks outside the protocol DB lock. + for notify in pending_notifies { + unsafe { + (notify.function)(¬ify.guid as *const efi::Guid, notify.interface, notify.handle); + } + } + + efi::Status::SUCCESS +} + +/// MM Driver Dispatch Handler. +/// +/// Re-triggers driver dispatch for any previously discovered but not-yet-dispatched +/// drivers. Once dispatch completes, the handler unregisters itself (it is a +/// one-shot handler). +/// +/// Corresponds to the C `MmDriverDispatchHandler`. +fn mm_driver_dispatch_handler( + _handler_type: &efi::Guid, + _comm_buffer: *mut c_void, + _comm_buffer_size: *mut usize, +) -> efi::Status { + log::info!("MmDriverDispatchHandler"); + + // TODO: Re-dispatch any remaining undispatched drivers. + // Currently all drivers are dispatched during StartUserCore, so this is a no-op. + + // Self-unregister (one-shot). + let handles = DISPATCH_HANDLES.lock(); + let dispatch_handle = handles[0].0; + drop(handles); + + if !dispatch_handle.is_null() { + let _ = crate::mm_services::GLOBAL_MMI_DB.mmi_handler_unregister(dispatch_handle); + } + + log::info!("MmDriverDispatchHandler done"); + + efi::Status::SUCCESS +} + +/// MM Ready To Lock Handler. +/// +/// Called when `gEfiDxeMmReadyToLockProtocolGuid` MMI is received. This: +/// 1. Unregisters handlers marked with `unregister_on_lock` (including itself). +/// 2. Installs the `gEfiMmReadyToLockProtocolGuid` protocol to notify MM drivers. +/// +/// Corresponds to the C `MmReadyToLockHandler`. +fn mm_ready_to_lock_handler( + _handler_type: &efi::Guid, + _comm_buffer: *mut c_void, + _comm_buffer_size: *mut usize, +) -> efi::Status { + log::info!("MmReadyToLockHandler"); + + // Unregister handlers that are no longer needed after MM lock. + let handles = DISPATCH_HANDLES.lock(); + for (i, entry) in CORE_MMI_HANDLERS.iter().enumerate() { + if entry.unregister_on_lock && !handles[i].0.is_null() { + let _ = crate::mm_services::GLOBAL_MMI_DB.mmi_handler_unregister(handles[i].0); + } + } + drop(handles); + + // Install the MM Ready To Lock Protocol. + let status = install_lifecycle_protocol(&guids::MM_READY_TO_LOCK_PROTOCOL); + if status != efi::Status::SUCCESS { + log::error!("Failed to install MM Ready To Lock Protocol: {:?}", status); + } + + status +} + +/// MM End of PEI Handler. +/// +/// Installs the `gEfiMmEndOfPeiProtocol` protocol. +/// +/// Corresponds to the C `MmEndOfPeiHandler`. +fn mm_end_of_pei_handler( + _handler_type: &efi::Guid, + _comm_buffer: *mut c_void, + _comm_buffer_size: *mut usize, +) -> efi::Status { + log::info!("MmEndOfPeiHandler"); + + install_lifecycle_protocol(&guids::MM_END_OF_PEI_PROTOCOL) +} + +/// MM End of DXE Handler. +/// +/// Installs the `gEfiMmEndOfDxeProtocolGuid` protocol. +/// +/// Corresponds to the C `MmEndOfDxeHandler`. +fn mm_end_of_dxe_handler( + _handler_type: &efi::Guid, + _comm_buffer: *mut c_void, + _comm_buffer_size: *mut usize, +) -> efi::Status { + log::info!("MmEndOfDxeHandler"); + + install_lifecycle_protocol(&guids::MM_END_OF_DXE_PROTOCOL) +} + +/// MM Exit Boot Service Handler. +/// +/// Installs the `gEfiEventExitBootServicesGuid` protocol (once). +/// +/// Corresponds to the C `MmExitBootServiceHandler`. +fn mm_exit_boot_service_handler( + _handler_type: &efi::Guid, + _comm_buffer: *mut c_void, + _comm_buffer_size: *mut usize, +) -> efi::Status { + static FIRED: spin::Once<()> = spin::Once::new(); + let mut status = efi::Status::SUCCESS; + + FIRED.call_once(|| { + status = install_lifecycle_protocol(&guids::EVENT_EXIT_BOOT_SERVICES); + }); + + status +} + +/// MM Ready To Boot Handler. +/// +/// Installs the `gEfiEventReadyToBootGuid` protocol (once). +/// +/// Corresponds to the C `MmReadyToBootHandler`. +fn mm_ready_to_boot_handler( + _handler_type: &efi::Guid, + _comm_buffer: *mut c_void, + _comm_buffer_size: *mut usize, +) -> efi::Status { + static FIRED: spin::Once<()> = spin::Once::new(); + let mut status = efi::Status::SUCCESS; + + FIRED.call_once(|| { + status = install_lifecycle_protocol(&guids::EVENT_READY_TO_BOOT); + }); + + status +} diff --git a/patina_mm_user_core/src/entry_point.asm b/patina_mm_user_core/src/entry_point.asm new file mode 100644 index 000000000..2d5c4a3e1 --- /dev/null +++ b/patina_mm_user_core/src/entry_point.asm @@ -0,0 +1,33 @@ +# +# Entry point to a Standalone MM driver. +# +# Copyright (c), Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent +# + +.section .data + +.section .text +.global user_core_main +.global efi_main + + +.align 8 +# Shim layer that redefines the contract between runtime module and init. +efi_main: + + #By the time we are here, it should be everything CPL3 already + sub rsp, 0x28 + + #To boot strap this driver, we directly call the entry point worker + call user_core_main + + #Restore the stack pointer + add rsp, 0x28 + + # Once returned, we will get returned status in rax, don't touch it, if you can help + # r15 contains call gate selector that was planned ahead + push r15 # New selector to be used, which is set to call gate by the supervisor + .byte 0xff, 0x1c, 0x24 # call far qword [rsp]# return to ring 0 via call gate m16:32 +1: + jmp 1b # Code should not reach here diff --git a/patina_mm_user_core/src/lib.rs b/patina_mm_user_core/src/lib.rs new file mode 100644 index 000000000..3e9d9cadb --- /dev/null +++ b/patina_mm_user_core/src/lib.rs @@ -0,0 +1,529 @@ +//! MM User Core +//! +//! A pure Rust implementation of the MM User Core for standalone MM mode environments. +//! +//! This crate provides the core functionality for a user-mode (Ring 3) MM module that is +//! invoked by the MM Supervisor Core via privilege demotion. It implements the equivalent +//! functionality of the C `StandaloneMmCore` — discovering drivers from HOBs, evaluating +//! dependency expressions, dispatching drivers, and managing MMI handlers. +//! +//! ## Architecture +//! +//! The user core is invoked by the supervisor with three command types: +//! - **StartUserCore**: One-time initialization. Walk HOBs to discover drivers and dispatch them. +//! - **UserRequest**: Runtime MMI dispatch. Parse the communication buffer and invoke registered handlers. +//! - **UserApProcedure**: Execute a procedure on behalf of an AP. +//! +//! ## Entry Protocol +//! +//! The supervisor calls the user core entry point with three arguments: +//! - `arg1` (`u64`): Command type (0 = StartUserCore, 1 = UserRequest, 2 = UserApProcedure) +//! - `arg2` (`u64`): Command-specific data pointer +//! - `arg3` (`u64`): Command-specific size or auxiliary data +//! +//! ## Memory Model +//! +//! This crate runs in Ring 3 (user mode). It does not have direct access to supervisor +//! resources. All supervisor services are accessed through syscalls. +//! +//! ## Example +//! +//! ```rust,ignore +//! use patina_mm_user_core::*; +//! +//! static USER_CORE: MmUserCore = MmUserCore::new(); +//! ``` +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! +#![cfg_attr(all(not(feature = "std"), not(test)), no_std)] +#![cfg(target_arch = "x86_64")] + +extern crate alloc; + +pub mod config_table; +pub mod core_handlers; +pub mod mm_dispatcher; +pub mod mm_mem; +pub mod mm_services; +pub mod mmi; +pub mod pool_allocator; +pub mod protocol_db; + +use core::{ + ffi::c_void, + mem, + num::NonZeroUsize, + ptr::NonNull, + sync::atomic::{AtomicBool, AtomicU64, Ordering}, +}; + +use patina::pi::hob::{Hob, PhaseHandoffInformationTable}; +use r_efi::efi; +use spin::Once; + +use crate::{mm_dispatcher::MmDispatcher, mmi::MmiDatabase, protocol_db::ProtocolDatabase}; + +use core::arch::global_asm; + +global_asm!(include_str!("entry_point.asm")); + +/// GUID used in `MemoryAllocationModule` HOBs to identify MM Supervisor module allocations. +/// +/// `gMmSupervisorHobMemoryAllocModuleGuid` +pub const MM_SUPERVISOR_HOB_MEMORY_ALLOC_MODULE_GUID: efi::Guid = + efi::Guid::from_fields(0x3efafe72, 0x3dbf, 0x4341, 0xad, 0x04, &[0x1c, 0xb6, 0xe8, 0xb6, 0x8e, 0x5e]); + +/// GUID identifying the MM User Core module itself. +/// +/// `gMmSupervisorUserGuid` +pub const MM_SUPERVISOR_USER_GUID: efi::Guid = + efi::Guid::from_fields(0x30d1cc3f, 0xc1db, 0x41ed, 0xb1, 0x13, &[0xab, 0xce, 0x21, 0xb0, 0x2b, 0xce]); + +/// GUID identifying the MM Supervisor Core module (to be skipped during driver discovery). +/// +/// `gMmSupervisorCoreGuid` +pub const MM_SUPERVISOR_CORE_GUID: efi::Guid = + efi::Guid::from_fields(0x4e4c89dc, 0xa452, 0x4b6b, 0xb1, 0x83, &[0xf1, 0x6a, 0x2a, 0x22, 0x37, 0x33]); + +/// GUID for depex data HOBs paired with driver `MemoryAllocationModule` HOBs. +/// +/// `gMmSupervisorDepexHobGuid` +pub const MM_SUPERVISOR_DEPEX_HOB_GUID: efi::Guid = + efi::Guid::from_fields(0xb17f0049, 0xaffd, 0x4530, 0xac, 0xd6, &[0xe2, 0x45, 0xe1, 0x9d, 0xea, 0xf1]); + +/// Mirrors the MM_SUPV_DEPEX_HOB_DATA structure defined in the supervisor. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct DepexHobData { + pub name: efi::Guid, // Protocol GUID + pub depex_expression_size: u64, + pub depex_expression: [u8; 0], +} + +use patina::management_mode::MmCommBufferStatus; +use patina::management_mode::comm_buffer_hob::{MM_COMM_BUFFER_HOB_GUID, MmCommonBufferHobData}; +use patina::pi::mm_cis::EfiMmEntryContext; +use patina::pi::protocols::communication::EfiMmCommunicateHeader; +/// GUID for the MM communication buffer HOB. +/// +/// `gMmCommBufferHobGuid` +use patina_internal_mm_common::UserCommandType; + +/// Base address of the user communication buffer (discovered from HOBs). +/// +/// The supervisor rewrites the HOB's `physical_start` to point to the internal +/// (MMRAM-resident, user-accessible) copy of the communication buffer before +/// invoking `StartUserCore`. +static COMM_BUFFER_BASE: AtomicU64 = AtomicU64::new(0); + +/// Size in bytes of the user communication buffer. +static COMM_BUFFER_SIZE: AtomicU64 = AtomicU64::new(0); + +/// Static reference to the user core instance. +static __USER_CORE: Once = Once::new(); + +/// Useful for offline inspection (like debugging) to determine core version. +#[used] +static MM_USER_CORE_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// The MM User Core responsible for driver dispatch and MMI handling in user mode. +/// +/// Create a static instance and call [`entry_point_worker`](MmUserCore::entry_point_worker) +/// from the binary entry point. +/// +/// ## Example +/// +/// ```rust,ignore +/// static USER_CORE: MmUserCore = MmUserCore::new(); +/// +/// #[unsafe(export_name = "efi_main")] +/// pub extern "efiapi" fn _start(arg1: u64, arg2: u64, arg3: u64) -> u64 { +/// USER_CORE.entry_point_worker(arg1, arg2, arg3) +/// } +/// ``` +pub struct MmUserCore { + /// The MMI handler database. + pub mmi_db: MmiDatabase, + /// The protocol/handle database (for depex evaluation and driver services). + pub protocol_db: ProtocolDatabase, + /// The driver dispatcher. + pub dispatcher: MmDispatcher, + /// Whether the core has completed initialization. + initialized: AtomicBool, +} + +// SAFETY: MmUserCore is designed to be shared across threads with proper synchronization. +unsafe impl Send for MmUserCore {} +unsafe impl Sync for MmUserCore {} + +impl MmUserCore { + /// Creates a new instance of the MM User Core. + pub const fn new() -> Self { + Self { + mmi_db: MmiDatabase::new(), + protocol_db: ProtocolDatabase::new(), + dispatcher: MmDispatcher::new(), + initialized: AtomicBool::new(false), + } + } + + /// Sets the static user core instance for global access. + /// + /// Returns true if the address was successfully stored, false if already set. + #[must_use] + fn set_instance(&'static self) -> bool { + let physical_address = NonNull::from_ref(self).expose_provenance(); + &physical_address == __USER_CORE.call_once(|| physical_address) + } + + /// Gets the static MM User Core instance for global access. + #[allow(unused)] + pub fn instance<'a>() -> &'a Self { + // SAFETY: The pointer is guaranteed to be valid as set_instance ensures single initialization. + unsafe { + NonNull::::with_exposed_provenance(*__USER_CORE.get().expect("MM User Core is not initialized.")) + .as_ref() + } + } + + /// Main entry point for the MM User Core. + /// + /// This is called by the supervisor via `invoke_demoted_routine`. The arguments + /// correspond to the three parameters passed by the supervisor: + /// + /// - `arg1`: Command type ([`UserCommandType`]) + /// - `arg2`: Command-specific data pointer + /// - `arg3`: Command-specific size or auxiliary data + /// + /// Returns 0 on success, or a non-zero status on failure. + pub fn entry_point_worker(&'static self, op_code: u64, arg1: u64, arg2: u64) -> u64 { + let command = match UserCommandType::try_from(op_code) { + Ok(cmd) => cmd, + Err(unknown) => { + log::error!("Unknown command type: {}", unknown); + return efi::Status::INVALID_PARAMETER.as_usize() as u64; + } + }; + + match command { + UserCommandType::StartUserCore => self.handle_start_user_core(arg1 as *const c_void), + UserCommandType::UserRequest => self.handle_user_request(arg1, arg2), + UserCommandType::UserApProcedure => self.handle_user_ap_procedure(arg1, arg2), + } + } + + /// Handle the `StartUserCore` command. + /// + /// This is called once during initialization. The supervisor passes the HOB list + /// pointer as `arg2`. We: + /// 1. Set the static instance + /// 2. Walk HOBs to discover the communication buffer + /// 3. Walk HOBs to discover MM drivers (MemoryAllocationModule HOBs) + /// 4. Read paired depex GuidHobs for each driver + /// 5. Evaluate dependency expressions and dispatch drivers in order + fn handle_start_user_core(&'static self, hob_list: *const c_void) -> u64 { + if !self.set_instance() { + log::warn!("MM User Core instance was already set, skipping re-initialization."); + return efi::Status::ALREADY_STARTED.as_usize() as u64; + } + + if hob_list.is_null() { + log::error!("HOB list pointer is null."); + return efi::Status::INVALID_PARAMETER.as_usize() as u64; + } + + log::info!("MM User Core v{} starting initialization...", env!("CARGO_PKG_VERSION")); + + // Enable the heap (syscall page allocator) before doing anything that + // requires dynamic allocation (driver discovery, depex parsing, etc.). + mm_mem::SYSCALL_PAGE_ALLOCATOR.set_initialized(); + + // Parse the HOB list + let hob_list_info = unsafe { + match (hob_list as *const PhaseHandoffInformationTable).as_ref() { + Some(info) => info, + None => { + log::error!("Failed to read HOB list header."); + return efi::Status::INVALID_PARAMETER.as_usize() as u64; + } + } + }; + + let hob = Hob::Handoff(hob_list_info); + + // Discover communication buffer from HOBs + self.discover_comm_buffer(&hob); + + // Initialize the MM System Table (heap-allocated, function pointers + // route to the global protocol DB and MMI DB in mm_services). + let mm_system_table = mm_services::init_mm_system_table(); + log::info!("MM System Table initialized at {:p}", mm_system_table); + + // Publish the HOB list as a configuration table entry so dispatched + // drivers can locate it via the system table (mirrors the C + // `MmInstallConfigurationTable(&gMmCoreMmst, &gEfiHobListGuid, ...)` + // call in `InitializeMmHobList`). + let status = config_table::GLOBAL_CONFIG_TABLE_DB + .install_configuration_table(&patina::guids::HOB_LIST, hob_list as *mut c_void); + if status != efi::Status::SUCCESS { + log::error!("Failed to install HOB list configuration table: {:?}", status); + } + + // Discover and dispatch MM drivers from HOBs + let dispatch_result = self.dispatcher.discover_and_dispatch_drivers( + &hob, + &self.mmi_db, + &self.protocol_db, + mm_system_table as *const _ as *const core::ffi::c_void, + ); + + match dispatch_result { + Ok(count) => { + log::info!("Successfully dispatched {} MM driver(s).", count); + } + Err(status) => { + log::error!("Driver dispatch failed: {:?}", status); + return status.as_usize() as u64; + } + } + + // Register core MMI handlers (lifecycle events like ready-to-lock, + // end-of-DXE, exit-boot-services, etc.). Matches the C ordering + // where handlers are registered after `MmDispatchFvs()`. + core_handlers::register_core_mmi_handlers(); + + self.initialized.store(true, Ordering::Release); + log::info!("MM User Core initialization complete."); + + efi::Status::SUCCESS.as_usize() as u64 + } + + /// Handle the `UserRequest` command (runtime MMI dispatch). + /// + /// The supervisor passes a pointer to a buffer containing: + /// - `EfiMmEntryContext` (at offset 0) + /// - `MmCommBufferStatus` (at offset `context_size`) + /// + /// For synchronous MMIs the supervisor has already copied the external + /// communication buffer into an internal (user-accessible) region. We: + /// 1. Validate the buffer via the `MmIsCommBuffer` syscall + /// 2. Parse the `EfiMmCommunicateHeader` to extract the handler GUID and data + /// 3. Dispatch via `mmi_manage` with the GUID and data pointer + /// + /// Asynchronous MMIs (timer, etc.) are always dispatched as root-only + /// (`mmi_manage(None, …)`). + /// + /// Mirrors the C `MmEntryPoint` flow in `StandaloneMmCore.c`. + fn handle_user_request(&self, supv_to_user_buffer: u64, context_size: u64) -> u64 { + if supv_to_user_buffer == 0 { + log::error!("Supervisor-to-user buffer is null."); + return efi::Status::INVALID_PARAMETER.as_usize() as u64; + } + + // Read the EfiMmEntryContext + let entry_context = unsafe { core::ptr::read(supv_to_user_buffer as *const EfiMmEntryContext) }; + + mm_services::update_cpu_context( + entry_context.currently_executing_cpu as usize, + entry_context.number_of_cpus as usize, + ); + + // Read MmCommBufferStatus (immediately after the context) + let comm_status = unsafe { + core::ptr::read((supv_to_user_buffer as *const u8).add(context_size as usize) as *const MmCommBufferStatus) + }; + + // ---- Synchronous MMI dispatch ---- + let mut sync_status = efi::Status::NOT_FOUND; + let mut return_buffer_size: u64 = 0; + + let comm_buffer_base = COMM_BUFFER_BASE.load(Ordering::Acquire); + let comm_buffer_size = COMM_BUFFER_SIZE.load(Ordering::Acquire); + + if comm_buffer_base != 0 && comm_status.is_comm_buffer_valid != 0 { + // Validate the communication buffer via a supervisor syscall. + if !mm_mem::is_comm_buffer(comm_buffer_base, comm_buffer_size) { + log::error!("MmIsCommBuffer rejected buffer at 0x{:x} size 0x{:x}", comm_buffer_base, comm_buffer_size); + } else { + sync_status = + self.dispatch_synchronous_mmi(comm_buffer_base, comm_buffer_size, &mut return_buffer_size); + } + } + + // ---- Asynchronous MMI dispatch (always runs) ---- + mm_services::GLOBAL_MMI_DB.mmi_manage(None, core::ptr::null(), core::ptr::null_mut(), core::ptr::null_mut()); + + // Write back the updated status to the supervisor-to-user buffer + let updated_status = MmCommBufferStatus { + is_comm_buffer_valid: 0, + talk_to_supervisor: 0, + _padding: [0; 6], + return_status: if sync_status == efi::Status::SUCCESS { + efi::Status::SUCCESS.as_usize() as u64 + } else { + efi::Status::NOT_FOUND.as_usize() as u64 + }, + return_buffer_size, + }; + + unsafe { + core::ptr::write( + (supv_to_user_buffer as *mut u8).add(context_size as usize) as *mut MmCommBufferStatus, + updated_status, + ); + } + + efi::Status::SUCCESS.as_usize() as u64 + } + + /// Parse the `EfiMmCommunicateHeader` from the communication buffer and + /// dispatch the appropriate GUID-specific MMI handler. + /// + /// Returns the dispatch status and updates `return_buffer_size` with the + /// total response size (header + data). + fn dispatch_synchronous_mmi( + &self, + comm_buffer_base: u64, + comm_buffer_size: u64, + return_buffer_size: &mut u64, + ) -> efi::Status { + let buffer_size = comm_buffer_size as usize; + + // The buffer must be large enough for at least the communicate header. + if buffer_size < EfiMmCommunicateHeader::size() { + log::error!( + "Communication buffer too small for header: {} < {}", + buffer_size, + EfiMmCommunicateHeader::size() + ); + return efi::Status::BAD_BUFFER_SIZE; + } + + // SAFETY: We verified the buffer is large enough for the header. + let header = unsafe { core::ptr::read_unaligned(comm_buffer_base as *const EfiMmCommunicateHeader) }; + + // Determine header layout: check for V3 signature first, then fall + // back to the legacy `EfiMmCommunicateHeader`. + let (comm_guid_ptr, comm_header_size, mut data_size) = if header.header_guid() + == patina::Guid::from_ref(&patina::pi::protocols::communication3::COMMUNICATE_HEADER_V3_GUID) + { + // V3 header + let v3 = unsafe { + core::ptr::read_unaligned( + comm_buffer_base as *const patina::pi::protocols::communication3::EfiMmCommunicateHeader, + ) + }; + let header_size = mem::size_of::(); + let total = v3.buffer_size as usize; + if total > buffer_size { + log::error!("V3 buffer_size 0x{:x} exceeds available 0x{:x}", total, buffer_size); + return efi::Status::BAD_BUFFER_SIZE; + } + // GUID to dispatch is `message_guid` in V3 + let guid_offset = + core::mem::offset_of!(patina::pi::protocols::communication3::EfiMmCommunicateHeader, message_guid); + let guid_ptr = (comm_buffer_base as *const u8).wrapping_add(guid_offset) as *const efi::Guid; + (guid_ptr, header_size, total.saturating_sub(header_size)) + } else { + // Legacy header + let message_length = header.message_length(); + let total = EfiMmCommunicateHeader::size() + message_length; + if total > buffer_size { + log::error!( + "Legacy message_length 0x{:x} exceeds available 0x{:x}", + message_length, + buffer_size.saturating_sub(EfiMmCommunicateHeader::size()) + ); + return efi::Status::BAD_BUFFER_SIZE; + } + // GUID to dispatch is `header_guid` in legacy + let guid_ptr = comm_buffer_base as *const efi::Guid; + (guid_ptr, EfiMmCommunicateHeader::size(), message_length) + }; + + // Zero the remainder of the buffer past the message (matches C behaviour). + let used = comm_header_size + data_size; + if used < buffer_size { + unsafe { + core::ptr::write_bytes((comm_buffer_base as *mut u8).add(used), 0, buffer_size - used); + } + } + + // Dispatch the GUID-specific handler. + let comm_data_ptr = unsafe { (comm_buffer_base as *mut u8).add(comm_header_size) as *mut c_void }; + + let status = mm_services::GLOBAL_MMI_DB.mmi_manage( + Some(unsafe { &*comm_guid_ptr }), + core::ptr::null(), + comm_data_ptr, + &mut data_size as *mut usize, + ); + + *return_buffer_size = (data_size + comm_header_size) as u64; + status + } + + /// Handle the `UserApProcedure` command. + /// + /// The supervisor passes the procedure pointer and argument. We call the procedure + /// directly since we're already in user mode. + fn handle_user_ap_procedure(&self, procedure: u64, argument: u64) -> u64 { + if procedure == 0 { + log::error!("AP procedure pointer is null."); + return efi::Status::INVALID_PARAMETER.as_usize() as u64; + } + + log::trace!("Executing AP procedure at 0x{:016x} with arg 0x{:016x}", procedure, argument); + + // SAFETY: The supervisor has validated the procedure pointer before dispatching. + // The procedure follows the EFI AP_PROCEDURE calling convention. + type EfiApProcedure = unsafe extern "efiapi" fn(*mut c_void); + let proc_fn: EfiApProcedure = unsafe { core::mem::transmute(procedure) }; + unsafe { proc_fn(argument as *mut c_void) }; + + efi::Status::SUCCESS.as_usize() as u64 + } + + /// Discover the communication buffer address from HOBs and store it for + /// later use in `handle_user_request`. + /// + /// The supervisor rewrites the HOB's `physical_start` field to point to + /// the internal (user-accessible) copy of the buffer before invoking + /// `StartUserCore`, so the address we read here is the one we should + /// read from at runtime. + fn discover_comm_buffer(&self, hob: &Hob<'_>) { + for current_hob in hob { + if let Hob::GuidHob(guid_hob, data) = current_hob { + if guid_hob.name == MM_COMM_BUFFER_HOB_GUID { + if data.len() >= mem::size_of::() { + let buffer_data = unsafe { &*(data.as_ptr() as *const MmCommonBufferHobData) }; + let physical_start = + unsafe { core::ptr::addr_of!(buffer_data.physical_start).read_unaligned() }; + let number_of_pages = + unsafe { core::ptr::addr_of!(buffer_data.number_of_pages).read_unaligned() }; + + let buffer_size = number_of_pages.saturating_mul(4096); + + COMM_BUFFER_BASE.store(physical_start, Ordering::Release); + COMM_BUFFER_SIZE.store(buffer_size, Ordering::Release); + + log::info!( + "Found MM communication buffer: base=0x{:016x}, pages={}, size=0x{:x}", + physical_start, + number_of_pages, + buffer_size, + ); + return; + } + } + } + } + + log::warn!("No MM communication buffer HOB found — only root MMI handlers will be supported."); + } +} diff --git a/patina_mm_user_core/src/mm_dispatcher.rs b/patina_mm_user_core/src/mm_dispatcher.rs new file mode 100644 index 000000000..a3bdb4341 --- /dev/null +++ b/patina_mm_user_core/src/mm_dispatcher.rs @@ -0,0 +1,333 @@ +//! MM Driver Dispatcher +//! +//! This module is responsible for discovering MM drivers from HOBs and dispatching them +//! in dependency order. It follows the same pattern as the C `StandaloneMmCore` dispatcher +//! in `FwVol.c` and `Dispatcher.c`, and the Rust DXE Core's `pi_dispatcher.rs`. +//! +//! ## Driver Discovery +//! +//! MM drivers are discovered from `MemoryAllocationModule` HOBs in the HOB list. Each driver +//! HOB is identified by having `alloc_descriptor.name == MM_SUPERVISOR_HOB_MEMORY_ALLOC_MODULE_GUID`. +//! The HOB's `module_name` provides the driver GUID, and `entry_point` provides the address to call. +//! +//! Drivers that are the supervisor core or user core themselves are skipped. +//! +//! ## Depex Evaluation +//! +//! Each driver's `MemoryAllocationModule` HOB is followed by a `GuidHob` with +//! `name == MM_SUPERVISOR_DEPEX_HOB_GUID` containing the raw dependency expression bytes. +//! The depex is parsed and evaluated against the protocol database. +//! +//! ## Dispatch Order +//! +//! Drivers with satisfied dependencies (or `TRUE`/empty depex) are dispatched first. +//! `BEFORE`/`AFTER` associations are respected: if driver A has `BEFORE(B)`, A is +//! dispatched immediately before B. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use alloc::{collections::BTreeMap, vec::Vec}; +use core::{cmp::Ordering, ffi::c_void}; + +use patina::{boot_services::c_ptr::CPtr, pi::hob::Hob}; +use patina_internal_depex::{AssociatedDependency, Depex}; +use r_efi::efi; +use spin::Mutex; + +use crate::{ + DepexHobData, MM_SUPERVISOR_CORE_GUID, MM_SUPERVISOR_DEPEX_HOB_GUID, MM_SUPERVISOR_HOB_MEMORY_ALLOC_MODULE_GUID, + MM_SUPERVISOR_USER_GUID, mmi::MmiDatabase, protocol_db::ProtocolDatabase, +}; + +/// Represents a discovered MM driver pending dispatch. +#[derive(Debug)] +struct DriverEntry { + /// The GUID identifying this driver (from `MemoryAllocationModule.module_name`). + file_name: efi::Guid, + /// The entry point address of the driver. + entry_point: u64, + /// The base address of the driver image in memory. + _image_base: u64, + /// The size of the driver image in memory. + _image_size: u64, + /// The parsed dependency expression, if any. + depex: Option, +} + +/// Wrapper for `efi::Guid` that implements `Ord` for use in `BTreeMap`. +#[derive(Debug, Eq, PartialEq)] +struct OrdGuid(efi::Guid); + +impl PartialOrd for OrdGuid { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for OrdGuid { + fn cmp(&self, other: &Self) -> Ordering { + self.0.as_bytes().cmp(other.0.as_bytes()) + } +} + +/// The MM Driver Dispatcher. +/// +/// Discovers drivers from HOBs at initialization time, evaluates their dependency +/// expressions, and dispatches them by calling their entry points. +pub struct MmDispatcher { + /// Tracks whether the dispatcher is currently executing (prevents re-entrancy). + executing: Mutex, +} + +impl MmDispatcher { + /// Creates a new `MmDispatcher`. + pub const fn new() -> Self { + Self { executing: Mutex::new(false) } + } + + /// Discover drivers from HOBs and dispatch them. + /// + /// This is the main entry point called during `StartUserCore`. It: + /// 1. Walks the HOB list to find `MemoryAllocationModule` HOBs with the supervisor alloc GUID + /// 2. Skips the supervisor core and user core modules + /// 3. Reads the paired depex `GuidHob` that follows each driver HOB + /// 4. Evaluates dependencies and dispatches in order + /// + /// Returns the number of drivers successfully dispatched, or an error status. + pub fn discover_and_dispatch_drivers( + &self, + hob: &Hob<'_>, + _mmi_db: &MmiDatabase, + protocol_db: &ProtocolDatabase, + mm_system_table: *const c_void, + ) -> Result { + let mut is_executing = self.executing.lock(); + if *is_executing { + return Err(efi::Status::ALREADY_STARTED); + } + *is_executing = true; + drop(is_executing); + + let drivers = self.discover_drivers(hob); + log::info!("Discovered {} MM driver(s) from HOBs.", drivers.len()); + + let dispatched = self.dispatch_drivers(drivers, protocol_db, mm_system_table); + + *self.executing.lock() = false; + Ok(dispatched) + } + + /// Walk the HOB list and collect driver entries. + /// + /// For each `MemoryAllocationModule` HOB with the supervisor allocation GUID: + /// - Skip if the module is the supervisor core or user core + /// - Look at the next HOB for a depex `GuidHob` with `MM_SUPERVISOR_DEPEX_HOB_GUID` + /// - Create a `DriverEntry` with the parsed depex + fn discover_drivers(&self, hob: &Hob<'_>) -> Vec { + let mut drivers = Vec::new(); + + // Collect all HOBs into a vec for indexed access (we need to look ahead for depex) + let all_hobs: Vec> = hob.into_iter().collect(); + + for (index, current_hob) in all_hobs.iter().enumerate() { + if let Hob::MemoryAllocationModule(mem_alloc_mod) = current_hob { + // Check if this is an MM Supervisor module allocation + if mem_alloc_mod.alloc_descriptor.name != MM_SUPERVISOR_HOB_MEMORY_ALLOC_MODULE_GUID { + continue; + } + + let module_name = mem_alloc_mod.module_name; + + // Skip the supervisor core and user core modules + if module_name == MM_SUPERVISOR_CORE_GUID || module_name == MM_SUPERVISOR_USER_GUID { + log::info!("Skipping core module: {:?}", module_name); + continue; + } + + log::info!( + "Found MM driver: name={:?}, entry=0x{:016x}, base=0x{:016x}, size=0x{:x}", + module_name, + mem_alloc_mod.entry_point, + mem_alloc_mod.alloc_descriptor.memory_base_address, + mem_alloc_mod.alloc_descriptor.memory_length, + ); + + // Look for a paired depex GuidHob in the next HOB + let depex: Option = if let Some(next_hob) = all_hobs.get(index + 1) { + if let Hob::GuidHob(guid_hob, data) = next_hob { + if guid_hob.name == MM_SUPERVISOR_DEPEX_HOB_GUID { + log::debug!(" Found depex HOB ({} bytes)", data.len()); + if data.is_empty() { + None + } else { + // Check to make sure the name matches the expected depex HOB GUID before parsing + let depex_hob_data = data.as_ref() as *const [u8] as *const DepexHobData; + // SAFETY: We trust that the supervisor correctly formats the depex HOB data + let depex_hob_data = unsafe { &*depex_hob_data }; + if depex_hob_data.name != module_name { + panic!( + "Depex HOB module name {:?} does not match driver module name {:?}", + depex_hob_data.name, module_name + ); + } + // print depex_hob_data.depex_expression pointer and length + log::info!( + " Parsed depex HOB {:#x?} for driver {:?} at {:#x?}: expression length = {}", + depex_hob_data.as_ptr(), + module_name, + depex_hob_data.depex_expression.as_ptr(), + depex_hob_data.depex_expression_size + ); + // SAFETY: depex_expression is a zero-length array (flexible array member). + // The actual bytes follow the struct in memory; use from_raw_parts with the real size. + let depex_bytes = unsafe { + core::slice::from_raw_parts( + depex_hob_data.depex_expression.as_ptr(), + depex_hob_data.depex_expression_size as usize, + ) + }; + Some(Depex::from(depex_bytes)) + } + } else { + log::debug!(" No depex HOB (next HOB has different GUID)"); + None + } + } else { + log::debug!(" No depex HOB (next HOB is not GuidHob)"); + None + } + } else { + log::debug!(" No depex HOB (no next HOB)"); + None + }; + + log::info!(" Driver {:?} has depex: {:?}", module_name, depex); + + drivers.push(DriverEntry { + file_name: module_name.into_inner(), + entry_point: mem_alloc_mod.entry_point, + _image_base: mem_alloc_mod.alloc_descriptor.memory_base_address, + _image_size: mem_alloc_mod.alloc_descriptor.memory_length, + depex, + }); + } + } + + drivers + } + + /// Dispatch drivers in dependency order. + /// + /// This implements a multi-pass dispatch loop similar to the DXE Core's `PiDispatcher`: + /// 1. Evaluate each pending driver's depex against the current protocol database + /// 2. Drivers with satisfied (or absent) depexes are scheduled + /// 3. Before/After associations are handled by reordering the scheduled list + /// 4. Each scheduled driver's entry point is called + /// 5. Repeat until no more drivers can be dispatched + fn dispatch_drivers( + &self, + mut pending: Vec, + protocol_db: &ProtocolDatabase, + mm_system_table: *const c_void, + ) -> usize { + let mut total_dispatched = 0; + + loop { + // Merge protocols from both the original protocol_db (for depex evaluation) + // and the global protocol DB (populated by drivers via the MM System Table). + let mut registered_protocols = protocol_db.registered_protocols(); + registered_protocols.extend(crate::mm_services::GLOBAL_PROTOCOL_DB.registered_protocols()); + let mut scheduled = Vec::new(); + let mut still_pending = Vec::new(); + let mut associated_before: BTreeMap> = BTreeMap::new(); + let mut associated_after: BTreeMap> = BTreeMap::new(); + + for mut driver in pending.drain(..) { + let depex_satisfied = match driver.depex { + Some(ref mut depex) => depex.eval(®istered_protocols), + // No depex means the driver can be dispatched immediately + None => true, + }; + + if depex_satisfied { + scheduled.push(driver); + } else { + // Check for Before/After associations + match driver.depex.as_ref().map(|d| d.is_associated()) { + Some(Some(AssociatedDependency::Before(guid))) => { + associated_before.entry(OrdGuid(guid)).or_default().push(driver); + } + Some(Some(AssociatedDependency::After(guid))) => { + associated_after.entry(OrdGuid(guid)).or_default().push(driver); + } + _ => { + still_pending.push(driver); + } + } + } + } + + if scheduled.is_empty() { + // No more drivers can be dispatched; move remaining to pending for logging + pending = still_pending; + break; + } + + // Build the final dispatch order respecting Before/After associations + let ordered: Vec = scheduled + .into_iter() + .flat_map(|driver| { + let filename = OrdGuid(driver.file_name); + let mut list = associated_before.remove(&filename).unwrap_or_default(); + let mut after_list = associated_after.remove(&filename).unwrap_or_default(); + list.push(driver); + list.append(&mut after_list); + list + }) + .collect(); + + // Dispatch each scheduled driver + for driver in ordered { + log::info!("Dispatching MM driver {:?} at entry 0x{:016x}", driver.file_name, driver.entry_point,); + + // Call the driver's entry point. + // MM driver entry signature: EFI_STATUS EFIAPI DriverEntry(EFI_HANDLE ImageHandle, EFI_MM_SYSTEM_TABLE *MmSystemTable) + // We pass a null image handle and the system table pointer. + type MmDriverEntryPoint = unsafe extern "efiapi" fn(efi::Handle, *const c_void) -> efi::Status; + let entry_fn: MmDriverEntryPoint = unsafe { core::mem::transmute(driver.entry_point) }; + + let status = unsafe { entry_fn(core::ptr::null_mut(), mm_system_table) }; + + if status == efi::Status::SUCCESS { + log::info!(" Driver {:?} returned SUCCESS.", driver.file_name); + total_dispatched += 1; + } else { + log::warn!(" Driver {:?} returned status: 0x{:x}", driver.file_name, status.as_usize(),); + } + } + + // Remaining unmatched Before/After drivers go back to pending + for (_guid, drivers) in associated_before { + still_pending.extend(drivers); + } + + for (_guid, drivers) in associated_after { + still_pending.extend(drivers); + } + + pending = still_pending; + } + + // Log any undispatched drivers + for driver in &pending { + log::warn!("Driver {:?} discovered but not dispatched (unsatisfied depex).", driver.file_name,); + } + + total_dispatched + } +} diff --git a/patina_mm_user_core/src/mm_mem.rs b/patina_mm_user_core/src/mm_mem.rs new file mode 100644 index 000000000..01c589958 --- /dev/null +++ b/patina_mm_user_core/src/mm_mem.rs @@ -0,0 +1,199 @@ +//! MM User Core Memory Allocator +//! +//! Provides a [`SyscallPageAllocator`] that implements [`PageAllocatorBackend`] +//! by issuing `syscall` instructions to the MM Supervisor for page allocation +//! and deallocation. +//! +//! The pool allocator, `PoolAllocator`, is wired up as the `#[global_allocator]`. +//! +//! ## Syscall ABI +//! +//! The MM Supervisor exposes page allocation via the following syscall indices +//! (defined in SysCallLib.h / `SyscallIndex` enum in the supervisor): +//! +//! | Syscall | RAX | RDX (arg1) | R8 (arg2) | R9 (arg3) | +//! |-------------|-----------|----------------|----------------|-------------| +//! | AllocPage | `0x10004` | alloc_type (0) | mem_type (6) | page_count | +//! | FreePage | `0x10005` | address | page_count | 0 | +//! +//! The supervisor returns: +//! - RAX: result value (allocated address for AllocPage, 0 for FreePage) +//! - RDX: EFI status (0 = success) +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use core::sync::atomic::{AtomicBool, Ordering}; + +use crate::pool_allocator::{PageAllocError, PageAllocatorBackend, PoolAllocator}; +use patina_internal_mm_common::SyscallIndex; + +/// `AllocateAnyPages` — allocate any available pages. +const ALLOCATE_ANY_PAGES: u64 = 0; + +/// `EfiRuntimeServicesData` — the memory type used for MM pool allocations. +const RUNTIME_SERVICES_DATA: u64 = 6; + +/// Result of a raw syscall to the supervisor. +#[derive(Debug, Clone, Copy)] +struct RawSyscallResult { + /// Value returned in RAX (e.g., allocated address). + value: u64, + /// Status returned in RDX (EFI_STATUS). + status: u64, +} + +/// Issue a `syscall` instruction to the MM Supervisor. +/// +/// # ABI +/// +/// - RAX = call_index +/// - RDX = arg1 +/// - R8 = arg2 +/// - R9 = arg3 +/// +/// On return: +/// - RAX = result value +/// - RDX = status +/// +/// # Safety +/// +/// This is inherently unsafe — it transfers control to the supervisor and +/// the arguments must be valid for the specific syscall index. +#[cfg(target_arch = "x86_64")] +unsafe fn raw_syscall(call_index: u64, arg1: u64, arg2: u64, arg3: u64) -> RawSyscallResult { + let value: u64; + let status: u64; + + // The `syscall` instruction uses: + // RAX = syscall number + // RCX = return address (set by CPU on syscall entry, clobbered) + // R11 = RFLAGS (set by CPU on syscall entry, clobbered) + // RDX = arg1 (also used for status return) + // R8 = arg2 + // R9 = arg3 + // + // On return from the supervisor: + // RAX = result value + // RDX = status + unsafe { + core::arch::asm!( + "syscall", + inlateout("rax") call_index => value, + inlateout("rdx") arg1 => status, + in("r8") arg2, + in("r9") arg3, + // RCX and R11 are clobbered by the `syscall` instruction. + lateout("rcx") _, + lateout("r11") _, + options(nostack), + ); + } + + RawSyscallResult { value, status } +} + +/// Validate that a given memory range is a valid MM communication buffer by +/// issuing the `MmIsCommBuffer` syscall to the supervisor. +/// +/// Returns `true` if the supervisor confirms the range falls entirely within +/// the user communication buffer region. +pub fn is_comm_buffer(address: u64, size: u64) -> bool { + let result = unsafe { raw_syscall(SyscallIndex::MmIsCommBuffer.as_u64(), address, size, 0) }; + result.value != 0 +} + +/// A page allocator backend that issues `syscall` instructions to the MM Supervisor. +/// +/// This is used as the [`PageAllocatorBackend`] for the MM User Core's +/// [`PoolAllocator`] and global allocator. +/// +/// ## Initialization +/// +/// Call [`SyscallPageAllocator::set_initialized`] after the user core has been +/// set up and is ready to issue syscalls (i.e., during `StartUserCore` handling, +/// before driver dispatch begins). +pub struct SyscallPageAllocator { + /// Whether the allocator has been activated. Before this is set, all + /// allocations will fail immediately. This prevents accidental allocations + /// before the syscall interface is ready. + initialized: AtomicBool, +} + +// SAFETY: SyscallPageAllocator uses an atomic flag and the syscall interface is +// re-entrant from the BSP. +unsafe impl Send for SyscallPageAllocator {} +unsafe impl Sync for SyscallPageAllocator {} + +impl SyscallPageAllocator { + /// Creates a new uninitialized syscall page allocator. + pub const fn new() -> Self { + Self { initialized: AtomicBool::new(false) } + } + + /// Marks the allocator as ready. Must be called after the syscall interface + /// is available (i.e., early in `StartUserCore` handling). + pub fn set_initialized(&self) { + self.initialized.store(true, Ordering::Release); + log::info!("SyscallPageAllocator initialized — heap is now available."); + } +} + +impl PageAllocatorBackend for SyscallPageAllocator { + fn allocate_pages(&self, num_pages: usize) -> Result { + if !self.initialized.load(Ordering::Acquire) { + return Err(PageAllocError::NotInitialized); + } + + if num_pages == 0 { + return Err(PageAllocError::OutOfMemory); + } + + let result = unsafe { + raw_syscall(SyscallIndex::AllocPage.as_u64(), ALLOCATE_ANY_PAGES, RUNTIME_SERVICES_DATA, num_pages as u64) + }; + + if result.status != 0 { + log::warn!("SyscallPageAllocator: AllocPage({} pages) failed with status 0x{:x}", num_pages, result.status); + return Err(PageAllocError::SyscallFailed(result.status)); + } + + log::trace!("SyscallPageAllocator: allocated {} page(s) at 0x{:016x}", num_pages, result.value); + + Ok(result.value) + } + + fn free_pages(&self, addr: u64, num_pages: usize) -> Result<(), PageAllocError> { + if !self.initialized.load(Ordering::Acquire) { + return Err(PageAllocError::NotInitialized); + } + + unsafe { raw_syscall(SyscallIndex::FreePage.as_u64(), addr, num_pages as u64, 0) }; + + log::trace!("SyscallPageAllocator: freed {} page(s) at 0x{:016x}", num_pages, addr); + + Ok(()) + } + + fn is_initialized(&self) -> bool { + self.initialized.load(Ordering::Acquire) + } +} + +/// Global page allocator instance for the user core. +/// +/// This issues syscalls to the supervisor for actual page allocation. +/// Call [`SYSCALL_PAGE_ALLOCATOR.set_initialized()`] during `StartUserCore` +/// to enable the heap. +pub static SYSCALL_PAGE_ALLOCATOR: SyscallPageAllocator = SyscallPageAllocator::new(); + +/// Global pool allocator instance. +/// +/// Uses the shared [`PoolAllocator`] from `pool_allocator`, +/// backed by [`SyscallPageAllocator`] for page allocation via syscalls. +#[global_allocator] +static GLOBAL_ALLOCATOR: PoolAllocator = PoolAllocator::new(&SYSCALL_PAGE_ALLOCATOR); diff --git a/patina_mm_user_core/src/mm_services.rs b/patina_mm_user_core/src/mm_services.rs new file mode 100644 index 000000000..c2dff4fda --- /dev/null +++ b/patina_mm_user_core/src/mm_services.rs @@ -0,0 +1,689 @@ +//! MM System Table (MMST) Construction — User Core Implementation +//! +//! This module builds the concrete `EfiMmSystemTable` instance that is passed +//! to dispatched MM drivers. The *type definitions* (`EfiMmSystemTable`, +//! `MmServices` trait, `StandardMmServices`, etc.) live in the Patina SDK at +//! [`patina::mm_services`] — this module only provides the `extern "efiapi"` +//! thunk functions, the global databases they route to, and the one-time +//! `init_mm_system_table()` entry point. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +extern crate alloc; + +use alloc::{boxed::Box, vec::Vec}; +use core::ffi::c_void; + +use r_efi::efi; +use spin::Once; + +use crate::mmi::MmiDatabase; +use crate::pool_allocator::PageAllocatorBackend; +use patina::pi::mm_cis::{ + EfiMmSystemTable, MM_MMST_SIGNATURE, MM_SYSTEM_TABLE_REVISION, MmCpuIoAccess, MmCpuIoProtocol, MmiHandlerEntryPoint, +}; + +/// Wrapper around a raw pointer so it can live in a `static Once<>`. +struct SendSyncPtr(*mut EfiMmSystemTable); + +// SAFETY: The pointer is only written once (in `init_mm_system_table`) and read +// immutably afterwards. All mutable state behind it is protected by locks. +unsafe impl Send for SendSyncPtr {} +unsafe impl Sync for SendSyncPtr {} + +/// The heap-allocated MM System Table. Initialized once in [`init_mm_system_table`]. +static MM_SYSTEM_TABLE: Once = Once::new(); + +/// Global MMI handler database used by the system table services. +pub static GLOBAL_MMI_DB: MmiDatabase = MmiDatabase::new(); + +/// Global protocol database used by the system table services. +pub static GLOBAL_PROTOCOL_DB: MmProtocolDatabase = MmProtocolDatabase::new(); + +/// Initialize the MM System Table. +/// +/// Allocates the table on the heap and populates it with service function pointers +/// that route to the user core's databases. Must be called once during +/// `StartUserCore`, **after** the heap is available. +/// +/// Returns a raw pointer to the table suitable for passing to driver entry points. +pub fn init_mm_system_table() -> *mut EfiMmSystemTable { + MM_SYSTEM_TABLE + .call_once(|| { + let table = EfiMmSystemTable { + hdr: efi::TableHeader { + signature: MM_MMST_SIGNATURE as u64, + revision: MM_SYSTEM_TABLE_REVISION, + header_size: core::mem::size_of::() as u32, + crc32: 0, + reserved: 0, + }, + mm_firmware_vendor: core::ptr::null_mut(), + mm_firmware_revision: 0, + + mm_install_configuration_table: mm_install_configuration_table_impl, + + mm_io: MmCpuIoProtocol { + mem: MmCpuIoAccess { read: mm_io_not_available, write: mm_io_not_available }, + io: MmCpuIoAccess { read: mm_io_not_available, write: mm_io_not_available }, + }, + + mm_allocate_pool: mm_allocate_pool_impl, + mm_free_pool: mm_free_pool_impl, + mm_allocate_pages: mm_allocate_pages_impl, + mm_free_pages: mm_free_pages_impl, + + mm_startup_this_ap: mm_startup_this_ap_not_available, + + currently_executing_cpu: 0, + number_of_cpus: 0, + cpu_save_state_size: core::ptr::null_mut(), + cpu_save_state: core::ptr::null_mut(), + + number_of_table_entries: 0, + mm_configuration_table: core::ptr::null_mut(), + + mm_install_protocol_interface: mm_install_protocol_interface_impl, + mm_uninstall_protocol_interface: mm_uninstall_protocol_interface_impl, + mm_handle_protocol: mm_handle_protocol_impl, + mm_register_protocol_notify: mm_register_protocol_notify_impl, + mm_locate_handle: mm_locate_handle_impl, + mm_locate_protocol: mm_locate_protocol_impl, + + mmi_manage: mmi_manage_impl, + mmi_handler_register: mmi_handler_register_impl, + mmi_handler_unregister: mmi_handler_unregister_impl, + }; + + let ptr = Box::into_raw(Box::new(table)); + log::info!("MM System Table allocated at {:p}", ptr); + SendSyncPtr(ptr) + }) + .0 +} + +/// Returns the MM System Table pointer, or null if not yet initialized. +pub fn get_mm_system_table() -> *mut EfiMmSystemTable { + MM_SYSTEM_TABLE.get().map(|p| p.0).unwrap_or(core::ptr::null_mut()) +} + +unsafe extern "efiapi" fn mm_io_not_available( + _this: *const MmCpuIoAccess, + _width: usize, + _address: u64, + _count: usize, + _buffer: *mut c_void, +) -> efi::Status { + efi::Status::UNSUPPORTED +} + +unsafe extern "efiapi" fn mm_install_configuration_table_impl( + _system_table: *const EfiMmSystemTable, + guid: *const efi::Guid, + table: *mut c_void, + _table_size: usize, +) -> efi::Status { + if guid.is_null() { + return efi::Status::INVALID_PARAMETER; + } + + let guid = unsafe { &*guid }; + crate::config_table::GLOBAL_CONFIG_TABLE_DB.install_configuration_table(guid, table) +} + +extern "efiapi" fn mm_allocate_pool_impl( + _pool_type: efi::MemoryType, + size: usize, + buffer: *mut *mut c_void, +) -> efi::Status { + if buffer.is_null() || size == 0 { + return efi::Status::INVALID_PARAMETER; + } + + let layout = match core::alloc::Layout::from_size_align(size, 8) { + Ok(l) => l, + Err(_) => return efi::Status::INVALID_PARAMETER, + }; + + let ptr = unsafe { alloc::alloc::alloc(layout) }; + if ptr.is_null() { + return efi::Status::OUT_OF_RESOURCES; + } + + unsafe { *buffer = ptr as *mut c_void }; + efi::Status::SUCCESS +} + +extern "efiapi" fn mm_free_pool_impl(buffer: *mut c_void) -> efi::Status { + if buffer.is_null() { + return efi::Status::INVALID_PARAMETER; + } + + let layout = unsafe { core::alloc::Layout::from_size_align_unchecked(1, 1) }; + unsafe { alloc::alloc::dealloc(buffer as *mut u8, layout) }; + efi::Status::SUCCESS +} + +extern "efiapi" fn mm_allocate_pages_impl( + _alloc_type: efi::AllocateType, + _memory_type: efi::MemoryType, + pages: usize, + memory: *mut efi::PhysicalAddress, +) -> efi::Status { + if memory.is_null() || pages == 0 { + return efi::Status::INVALID_PARAMETER; + } + + match crate::mm_mem::SYSCALL_PAGE_ALLOCATOR.allocate_pages(pages) { + Ok(addr) => { + unsafe { *memory = addr }; + efi::Status::SUCCESS + } + Err(_) => efi::Status::OUT_OF_RESOURCES, + } +} + +extern "efiapi" fn mm_free_pages_impl(memory: efi::PhysicalAddress, pages: usize) -> efi::Status { + if memory == 0 || pages == 0 { + return efi::Status::INVALID_PARAMETER; + } + + match crate::mm_mem::SYSCALL_PAGE_ALLOCATOR.free_pages(memory, pages) { + Ok(()) => efi::Status::SUCCESS, + Err(_) => efi::Status::INVALID_PARAMETER, + } +} + +unsafe extern "efiapi" fn mm_startup_this_ap_not_available( + _procedure: usize, + _cpu_number: usize, + _proc_arguments: *mut c_void, +) -> efi::Status { + efi::Status::UNSUPPORTED +} + +extern "efiapi" fn mm_install_protocol_interface_impl( + handle: *mut efi::Handle, + protocol: *mut efi::Guid, + _interface_type: efi::InterfaceType, + interface: *mut c_void, +) -> efi::Status { + if handle.is_null() || protocol.is_null() { + return efi::Status::INVALID_PARAMETER; + } + + let guid = unsafe { &*protocol }; + let caller_handle = unsafe { *handle }; + + match GLOBAL_PROTOCOL_DB.install_protocol(caller_handle, guid, interface) { + Ok((new_handle, pending_notifies)) => { + unsafe { *handle = new_handle }; + // Fire notifications outside the DB lock. + for notify in pending_notifies { + unsafe { + (notify.function)(¬ify.guid as *const efi::Guid, notify.interface, notify.handle); + } + } + efi::Status::SUCCESS + } + Err(status) => status, + } +} + +extern "efiapi" fn mm_uninstall_protocol_interface_impl( + handle: efi::Handle, + protocol: *mut efi::Guid, + interface: *mut c_void, +) -> efi::Status { + if handle.is_null() || protocol.is_null() { + return efi::Status::INVALID_PARAMETER; + } + + let guid = unsafe { &*protocol }; + + match GLOBAL_PROTOCOL_DB.uninstall_protocol(handle, guid, interface) { + Ok(()) => efi::Status::SUCCESS, + Err(status) => status, + } +} + +extern "efiapi" fn mm_handle_protocol_impl( + handle: efi::Handle, + protocol: *mut efi::Guid, + interface: *mut *mut c_void, +) -> efi::Status { + if protocol.is_null() { + return efi::Status::INVALID_PARAMETER; + } + + if interface.is_null() { + return efi::Status::INVALID_PARAMETER; + } + // C reference: *Interface = NULL before lookup. + unsafe { *interface = core::ptr::null_mut() }; + + if handle.is_null() { + return efi::Status::INVALID_PARAMETER; + } + + let guid = unsafe { &*protocol }; + + match GLOBAL_PROTOCOL_DB.handle_protocol(handle, guid) { + Some(iface) => { + unsafe { *interface = iface }; + efi::Status::SUCCESS + } + None => efi::Status::UNSUPPORTED, + } +} + +extern "efiapi" fn mm_register_protocol_notify_impl( + protocol: *const efi::Guid, + function: usize, + registration: *mut *mut c_void, +) -> efi::Status { + if protocol.is_null() || registration.is_null() { + return efi::Status::INVALID_PARAMETER; + } + + let guid = unsafe { &*protocol }; + + if function == 0 { + // Function is NULL → unregister the notification identified by *Registration. + let reg = unsafe { *registration }; + match GLOBAL_PROTOCOL_DB.unregister_protocol_notify(guid, reg) { + Ok(()) => efi::Status::SUCCESS, + Err(status) => status, + } + } else { + // Register a new notification. + // SAFETY: function is an `EFI_MM_NOTIFY_FN` function pointer passed as usize. + let notify_fn: MmNotifyFn = unsafe { core::mem::transmute(function) }; + let token = GLOBAL_PROTOCOL_DB.register_protocol_notify(guid, notify_fn); + unsafe { *registration = token }; + efi::Status::SUCCESS + } +} + +extern "efiapi" fn mm_locate_handle_impl( + search_type: efi::LocateSearchType, + protocol: *mut efi::Guid, + _search_key: *mut c_void, + buffer_size: *mut usize, + buffer: *mut efi::Handle, +) -> efi::Status { + if buffer_size.is_null() { + return efi::Status::INVALID_PARAMETER; + } + + let handles = match search_type { + efi::ALL_HANDLES => GLOBAL_PROTOCOL_DB.all_handles(), + efi::BY_PROTOCOL => { + if protocol.is_null() { + return efi::Status::INVALID_PARAMETER; + } + let guid = unsafe { &*protocol }; + GLOBAL_PROTOCOL_DB.locate_handle_by_protocol(guid) + } + _ => { + log::warn!("MmLocateHandle: search type {} not yet supported", search_type); + return efi::Status::UNSUPPORTED; + } + }; + + if handles.is_empty() { + return efi::Status::NOT_FOUND; + } + + let required_size = handles.len() * core::mem::size_of::(); + let caller_size = unsafe { *buffer_size }; + unsafe { *buffer_size = required_size }; + + if caller_size < required_size { + return efi::Status::BUFFER_TOO_SMALL; + } + + if buffer.is_null() { + return efi::Status::INVALID_PARAMETER; + } + + unsafe { + core::ptr::copy_nonoverlapping(handles.as_ptr(), buffer, handles.len()); + } + efi::Status::SUCCESS +} + +extern "efiapi" fn mm_locate_protocol_impl( + protocol: *mut efi::Guid, + _registration: *mut c_void, + interface: *mut *mut c_void, +) -> efi::Status { + if protocol.is_null() || interface.is_null() { + return efi::Status::INVALID_PARAMETER; + } + + let guid = unsafe { &*protocol }; + + match GLOBAL_PROTOCOL_DB.locate_protocol(guid) { + Some(iface) => { + unsafe { *interface = iface }; + efi::Status::SUCCESS + } + None => efi::Status::NOT_FOUND, + } +} + +unsafe extern "efiapi" fn mmi_manage_impl( + handler_type: *const efi::Guid, + context: *const c_void, + comm_buffer: *mut c_void, + comm_buffer_size: *mut usize, +) -> efi::Status { + let guid = if handler_type.is_null() { None } else { Some(unsafe { &*handler_type }) }; + + GLOBAL_MMI_DB.mmi_manage(guid, context, comm_buffer, comm_buffer_size) +} + +unsafe extern "efiapi" fn mmi_handler_register_impl( + handler: MmiHandlerEntryPoint, + handler_type: *const efi::Guid, + dispatch_handle: *mut efi::Handle, +) -> efi::Status { + if dispatch_handle.is_null() { + return efi::Status::INVALID_PARAMETER; + } + + let guid = if handler_type.is_null() { None } else { Some(unsafe { &*handler_type }) }; + + match GLOBAL_MMI_DB.mmi_handler_register(handler, guid) { + Ok(handle) => { + unsafe { *dispatch_handle = handle }; + efi::Status::SUCCESS + } + Err(status) => status, + } +} + +unsafe extern "efiapi" fn mmi_handler_unregister_impl(dispatch_handle: efi::Handle) -> efi::Status { + match GLOBAL_MMI_DB.mmi_handler_unregister(dispatch_handle) { + Ok(()) => efi::Status::SUCCESS, + Err(status) => status, + } +} + +/// `EFI_MM_NOTIFY_FN` — callback invoked when a protocol is installed. +/// +/// ```c +/// typedef EFI_STATUS (EFIAPI *EFI_MM_NOTIFY_FN)( +/// IN CONST EFI_GUID *Protocol, +/// IN VOID *Interface, +/// IN EFI_HANDLE Handle +/// ); +/// ``` +type MmNotifyFn = unsafe extern "efiapi" fn(*const efi::Guid, *mut c_void, efi::Handle) -> efi::Status; + +/// Per-handle state: all protocol interfaces installed on one handle. +struct MmHandle { + protocols: Vec<(efi::Guid, *mut c_void)>, +} + +/// A registered protocol notification. +struct ProtocolNotifyEntry { + guid: efi::Guid, + function: MmNotifyFn, + /// Unique token returned via `*Registration`. + token: usize, +} + +/// Info collected under the lock, fired after the lock is released. +pub struct PendingNotify { + pub function: MmNotifyFn, + pub guid: efi::Guid, + pub interface: *mut c_void, + pub handle: efi::Handle, +} + +struct MmProtocolDatabaseInner { + /// All handles: (opaque id, per-handle data). + handles: Vec<(usize, MmHandle)>, + /// Registered protocol notifications. + notifications: Vec, + /// Next monotonic id for handle allocation (starts at 1 to avoid null). + next_handle_id: usize, + /// Next monotonic id for registration tokens (starts at 1 to avoid null). + next_registration_id: usize, +} + +/// Handle-aware protocol database with notification support. +/// +/// Mirrors the C `StandaloneMmCore` handle/protocol infrastructure +/// (`IHANDLE`, `PROTOCOL_ENTRY`, `PROTOCOL_INTERFACE`, `PROTOCOL_NOTIFY`). +pub struct MmProtocolDatabase { + inner: spin::Mutex, +} + +// SAFETY: All mutable state is behind a spin::Mutex. +unsafe impl Send for MmProtocolDatabase {} +unsafe impl Sync for MmProtocolDatabase {} + +impl MmProtocolDatabase { + pub const fn new() -> Self { + Self { + inner: spin::Mutex::new(MmProtocolDatabaseInner { + handles: Vec::new(), + notifications: Vec::new(), + next_handle_id: 1, + next_registration_id: 1, + }), + } + } + + // ----------------------------------------------------------------- + // Install / Uninstall + // ----------------------------------------------------------------- + + /// Install a protocol interface onto a handle. + /// + /// If `handle` is null a new handle is allocated. Returns the + /// (possibly new) handle and any pending notify callbacks that must + /// be invoked **after** the caller has released the lock. + pub fn install_protocol( + &self, + handle: efi::Handle, + guid: &efi::Guid, + interface: *mut c_void, + ) -> Result<(efi::Handle, Vec), efi::Status> { + let mut inner = self.inner.lock(); + + let handle_id = if handle.is_null() { + // Allocate a new handle. + let id = inner.next_handle_id; + inner.next_handle_id += 1; + inner.handles.push((id, MmHandle { protocols: Vec::new() })); + id + } else { + let id = handle as usize; + // Validate the handle exists. + if !inner.handles.iter().any(|(h, _)| *h == id) { + return Err(efi::Status::INVALID_PARAMETER); + } + // Reject duplicate: same protocol already on this handle. + let mm_handle = &inner.handles.iter().find(|(h, _)| *h == id).unwrap().1; + if mm_handle.protocols.iter().any(|(g, _)| g == guid) { + return Err(efi::Status::INVALID_PARAMETER); + } + id + }; + + // Add the protocol interface to the handle. + let mm_handle = &mut inner.handles.iter_mut().find(|(h, _)| *h == handle_id).unwrap().1; + mm_handle.protocols.push((*guid, interface)); + + // Collect pending notifications. + let actual_handle = handle_id as efi::Handle; + let notifies: Vec = inner + .notifications + .iter() + .filter(|n| n.guid == *guid) + .map(|n| PendingNotify { function: n.function, guid: *guid, interface, handle: actual_handle }) + .collect(); + + log::debug!("MmInstallProtocolInterface: {:?} on handle {:p}", guid, actual_handle); + Ok((actual_handle, notifies)) + } + + /// Uninstall a protocol interface from a handle. + /// + /// If the handle has no remaining protocols it is removed from the + /// database (matching the C `MmUninstallProtocolInterface` behaviour). + pub fn uninstall_protocol( + &self, + handle: efi::Handle, + guid: &efi::Guid, + interface: *mut c_void, + ) -> Result<(), efi::Status> { + let mut inner = self.inner.lock(); + let id = handle as usize; + + let mm_handle = match inner.handles.iter_mut().find(|(h, _)| *h == id) { + Some((_, h)) => h, + None => return Err(efi::Status::INVALID_PARAMETER), + }; + + if let Some(pos) = mm_handle.protocols.iter().position(|(g, i)| g == guid && *i == interface) { + mm_handle.protocols.remove(pos); + } else { + return Err(efi::Status::NOT_FOUND); + } + + // Remove the handle entirely when it has no more protocols. + if inner.handles.iter().find(|(h, _)| *h == id).unwrap().1.protocols.is_empty() { + inner.handles.retain(|(h, _)| *h != id); + } + + Ok(()) + } + + // ----------------------------------------------------------------- + // Lookup + // ----------------------------------------------------------------- + + /// Look up a specific protocol on a specific handle (`HandleProtocol`). + pub fn handle_protocol(&self, handle: efi::Handle, guid: &efi::Guid) -> Option<*mut c_void> { + let inner = self.inner.lock(); + let id = handle as usize; + let mm_handle = &inner.handles.iter().find(|(h, _)| *h == id)?.1; + mm_handle.protocols.iter().find(|(g, _)| g == guid).map(|(_, i)| *i) + } + + /// Locate the first installed interface for a GUID across all handles. + pub fn locate_protocol(&self, guid: &efi::Guid) -> Option<*mut c_void> { + let inner = self.inner.lock(); + for (_, mm_handle) in &inner.handles { + if let Some((_, iface)) = mm_handle.protocols.iter().find(|(g, _)| g == guid) { + return Some(*iface); + } + } + None + } + + /// Return all handles that support a given protocol. + pub fn locate_handle_by_protocol(&self, guid: &efi::Guid) -> Vec { + let inner = self.inner.lock(); + inner + .handles + .iter() + .filter(|(_, mm_handle)| mm_handle.protocols.iter().any(|(g, _)| g == guid)) + .map(|(id, _)| *id as efi::Handle) + .collect() + } + + /// Return all handles in the database. + pub fn all_handles(&self) -> Vec { + let inner = self.inner.lock(); + inner.handles.iter().map(|(id, _)| *id as efi::Handle).collect() + } + + // ----------------------------------------------------------------- + // Notify + // ----------------------------------------------------------------- + + /// Register a notification callback for a protocol GUID. + /// + /// If an identical `(GUID, function)` pair is already registered the + /// existing token is returned (matching the C implementation). + pub fn register_protocol_notify(&self, guid: &efi::Guid, function: MmNotifyFn) -> *mut c_void { + let mut inner = self.inner.lock(); + let fn_addr = function as usize; + + // De-duplicate: same GUID + same function pointer. + if let Some(existing) = inner.notifications.iter().find(|n| n.guid == *guid && (n.function as usize) == fn_addr) + { + return existing.token as *mut c_void; + } + + let token = inner.next_registration_id; + inner.next_registration_id += 1; + inner.notifications.push(ProtocolNotifyEntry { guid: *guid, function, token }); + + token as *mut c_void + } + + /// Unregister a notification by its registration token. + pub fn unregister_protocol_notify(&self, guid: &efi::Guid, registration: *mut c_void) -> Result<(), efi::Status> { + let mut inner = self.inner.lock(); + let token = registration as usize; + + if let Some(pos) = inner.notifications.iter().position(|n| n.guid == *guid && n.token == token) { + inner.notifications.remove(pos); + Ok(()) + } else { + Err(efi::Status::NOT_FOUND) + } + } + + // ----------------------------------------------------------------- + // Depex helpers (backward-compatible public API) + // ----------------------------------------------------------------- + + /// Check if a protocol GUID is installed on any handle. + pub fn is_protocol_installed(&self, guid: &efi::Guid) -> bool { + let inner = self.inner.lock(); + inner.handles.iter().any(|(_, mm_handle)| mm_handle.protocols.iter().any(|(g, _)| g == guid)) + } + + /// Return all unique installed protocol GUIDs. + pub fn registered_protocols(&self) -> Vec { + let inner = self.inner.lock(); + let mut guids = Vec::new(); + for (_, mm_handle) in &inner.handles { + for (g, _) in &mm_handle.protocols { + if !guids.contains(g) { + guids.push(*g); + } + } + } + guids + } +} + +/// Update the system table's CPU information from a new `EfiMmEntryContext`. +/// +/// Called at the start of each `UserRequest` handling to reflect the current +/// processor state. +pub fn update_cpu_context(currently_executing_cpu: usize, number_of_cpus: usize) { + let ptr = get_mm_system_table(); + if ptr.is_null() { + return; + } + // SAFETY: The table is heap-allocated and we are the only writer of these fields. + unsafe { + (*ptr).currently_executing_cpu = currently_executing_cpu; + (*ptr).number_of_cpus = number_of_cpus; + } +} diff --git a/patina_mm_user_core/src/mmi.rs b/patina_mm_user_core/src/mmi.rs new file mode 100644 index 000000000..ef921632d --- /dev/null +++ b/patina_mm_user_core/src/mmi.rs @@ -0,0 +1,370 @@ +//! MMI (Management Mode Interrupt) Handler Database +//! +//! This module manages the registration and dispatch of MMI handlers, following the +//! same patterns as the C `Mmi.c` in `StandaloneMmPkg/Core`. +//! +//! ## Handler Types +//! +//! - **Root handlers**: Registered with `handler_type = None`. Called on every MMI regardless +//! of the communication buffer contents. Used for hardware-level interrupt sources. +//! - **GUID-specific handlers**: Registered with a specific GUID. Called only when an MMI +//! communication targets that GUID. +//! +//! ## External vs Internal Handlers +//! +//! The database supports two calling conventions: +//! - **External** (`MmiHandlerEntryPoint`): `unsafe extern "efiapi" fn` — used by drivers +//! registering through the MMST `MmiHandlerRegister` service. +//! - **Internal** (`InternalMmiHandler`): Safe Rust `fn` — used by the core's own lifecycle +//! handlers (ready-to-lock, end-of-DXE, etc.) without going through the C ABI. +//! +//! ## Dispatch Flow +//! +//! [`MmiDatabase::mmi_manage`] is the main dispatch entry point: +//! 1. If `handler_type` is `None`, iterate root handlers +//! 2. If `handler_type` is `Some(guid)`, find the `MmiEntry` for that GUID and iterate its handlers +//! 3. Each handler returns a status that determines whether dispatch continues +//! +//! **Lock safety**: The database lock is released before calling handlers and +//! re-acquired afterwards, so handlers may safely call `mmi_handler_register` or +//! `mmi_handler_unregister` without deadlocking. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use alloc::{vec, vec::Vec}; +use core::ffi::c_void; + +use r_efi::efi; +use spin::Mutex; + +/// MMI handler entry point signature (external / C ABI). +/// +/// Re-exported from [`patina::pi::mm_cis::MmiHandlerEntryPoint`]. +pub use patina::pi::mm_cis::MmiHandlerEntryPoint; + +/// EFI_WARN_INTERRUPT_SOURCE_QUIESCED — PI spec warning status code. +/// Indicates an interrupt source was quiesced. +const WARN_INTERRUPT_SOURCE_QUIESCED: efi::Status = efi::Status::from_usize(3); + +/// EFI_INTERRUPT_PENDING — PI spec status for pending interrupts. +const INTERRUPT_PENDING: efi::Status = efi::Status::from_usize(0x80000000 | 0x00000004); + +/// Signature for internal (Rust-native) MMI handlers. +/// +/// These are registered by the core itself for lifecycle events and do not go +/// through the `unsafe extern "efiapi"` calling convention. +/// +/// ## Parameters +/// +/// - `handler_type` — The GUID that triggered this handler (same as the registration GUID). +/// - `comm_buffer` — Pointer to the communication data (may be null for async MMIs). +/// - `comm_buffer_size` — Mutable pointer to the communication buffer size. +/// +/// ## Returns +/// +/// An [`efi::Status`] following the standard `MmiManage` return protocol. +pub type InternalMmiHandler = + fn(handler_type: &efi::Guid, comm_buffer: *mut c_void, comm_buffer_size: *mut usize) -> efi::Status; + +/// An MMI handler callback — either an external (C ABI) or internal (Rust) function. +#[derive(Clone, Copy)] +enum HandlerKind { + /// External handler registered by a driver through the MMST. + External(MmiHandlerEntryPoint), + /// Internal handler registered by the MM Core directly. + Internal(InternalMmiHandler), +} + +/// An MMI entry groups all handlers registered for a specific GUID. +#[derive(Clone)] +struct MmiEntry { + /// The handler type GUID. + handler_type: efi::Guid, + /// All handlers registered for this GUID. + handlers: Vec, +} + +/// A registered MMI handler. +#[derive(Clone, Copy)] +struct MmiHandler { + /// The handler callback. + kind: HandlerKind, + /// Monotonic ID used as the dispatch handle for unregistration. + id: usize, + /// Whether this handler is marked for removal (deferred removal during dispatch). + to_remove: bool, +} + +/// The MMI handler database. +/// +/// Manages root handlers (called for all MMIs) and GUID-specific handlers. +/// Thread-safe via internal `Mutex`. +pub struct MmiDatabase { + /// Internal state protected by a mutex. + inner: Mutex, +} + +struct MmiDatabaseInner { + /// Root MMI handlers (called for every MMI, regardless of GUID). + root_handlers: Vec, + /// GUID-specific MMI entries. + entries: Vec, + /// Re-entrance depth counter for `mmi_manage`. + manage_calling_depth: usize, + /// Monotonic ID counter for handler dispatch handles. + next_id: usize, +} + +impl MmiDatabase { + /// Creates a new empty `MmiDatabase`. + pub const fn new() -> Self { + Self { + inner: Mutex::new(MmiDatabaseInner { + root_handlers: Vec::new(), + entries: Vec::new(), + manage_calling_depth: 0, + next_id: 1, + }), + } + } + + /// Register an external (C ABI) MMI handler. + /// + /// If `handler_type` is `None`, the handler is registered as a root handler. + /// If `handler_type` is `Some(guid)`, the handler is registered for that specific GUID. + /// + /// Returns `Ok(dispatch_handle)` on success, where `dispatch_handle` is an opaque handle + /// that can be used to unregister the handler. + pub fn mmi_handler_register( + &self, + handler: MmiHandlerEntryPoint, + handler_type: Option<&efi::Guid>, + ) -> Result { + let mut inner = self.inner.lock(); + let id = inner.next_id; + inner.next_id += 1; + + let mmi_handler = MmiHandler { kind: HandlerKind::External(handler), id, to_remove: false }; + + Self::insert_handler(&mut inner, handler_type, mmi_handler); + + let handle = id as efi::Handle; + log::debug!("Registered external MMI handler id={} for {:?}", id, handler_type,); + Ok(handle) + } + + /// Register an internal (Rust-native) MMI handler. + /// + /// Works like [`mmi_handler_register`](Self::mmi_handler_register) but takes a safe + /// Rust function pointer instead of an `unsafe extern "efiapi" fn`. + /// + /// Returns the dispatch handle (an opaque `usize`-based ID) on success. + pub fn register_internal_handler( + &self, + handler: InternalMmiHandler, + handler_type: Option<&efi::Guid>, + ) -> Result { + let mut inner = self.inner.lock(); + let id = inner.next_id; + inner.next_id += 1; + + let mmi_handler = MmiHandler { kind: HandlerKind::Internal(handler), id, to_remove: false }; + + Self::insert_handler(&mut inner, handler_type, mmi_handler); + + let handle = id as efi::Handle; + log::debug!("Registered internal MMI handler id={} for {:?}", id, handler_type,); + Ok(handle) + } + + /// Insert a handler into the appropriate list (root or GUID-specific). + fn insert_handler(inner: &mut MmiDatabaseInner, handler_type: Option<&efi::Guid>, handler: MmiHandler) { + match handler_type { + None => { + inner.root_handlers.push(handler); + } + Some(guid) => { + if let Some(entry) = inner.entries.iter_mut().find(|e| e.handler_type == *guid) { + entry.handlers.push(handler); + } else { + inner.entries.push(MmiEntry { handler_type: *guid, handlers: vec![handler] }); + } + } + } + } + + /// Unregister an MMI handler by its dispatch handle. + /// + /// If we are inside a dispatch (`manage_calling_depth > 0`) the handler is + /// marked for deferred removal. Otherwise it is removed immediately. + pub fn mmi_handler_unregister(&self, dispatch_handle: efi::Handle) -> Result<(), efi::Status> { + let target_id = dispatch_handle as usize; + let mut inner = self.inner.lock(); + + // Search root handlers + for handler in inner.root_handlers.iter_mut() { + if handler.id == target_id { + handler.to_remove = true; + log::debug!("Marked root MMI handler id={} for removal.", target_id); + if inner.manage_calling_depth == 0 { + Self::cleanup_removed_handlers(&mut inner); + } + return Ok(()); + } + } + + // Search GUID-specific handlers + for entry in inner.entries.iter_mut() { + for handler in entry.handlers.iter_mut() { + if handler.id == target_id { + handler.to_remove = true; + log::debug!("Marked MMI handler id={} for removal (GUID: {:?}).", target_id, entry.handler_type,); + if inner.manage_calling_depth == 0 { + Self::cleanup_removed_handlers(&mut inner); + } + return Ok(()); + } + } + } + + log::warn!("MMI handler {:?} not found for unregistration.", dispatch_handle); + Err(efi::Status::NOT_FOUND) + } + + /// Manage (dispatch) an MMI. + /// + /// This is the main dispatch function, equivalent to the C `MmiManage`. + /// + /// - If `handler_type` is `None`, root handlers are dispatched. + /// - If `handler_type` is `Some(guid)`, the handlers for that GUID are dispatched. + /// + /// **Lock safety**: The database lock is released before calling any handler + /// and re-acquired afterwards, so handlers may call `mmi_handler_register` / + /// `mmi_handler_unregister` without deadlocking. + /// + /// Returns: + /// - `EFI_SUCCESS` if at least one handler returned success + /// - `EFI_WARN_INTERRUPT_SOURCE_QUIESCED` if a source was quiesced + /// - `EFI_INTERRUPT_PENDING` if a handler indicated the interrupt is still pending + /// - `EFI_NOT_FOUND` if no handlers are registered for the given type + pub fn mmi_manage( + &self, + handler_type: Option<&efi::Guid>, + context: *const c_void, + comm_buffer: *mut c_void, + comm_buffer_size: *mut usize, + ) -> efi::Status { + // ----- Phase 1: snapshot handlers under the lock ----- + let handlers_snapshot = { + let mut inner = self.inner.lock(); + inner.manage_calling_depth += 1; + + match handler_type { + None => inner.root_handlers.iter().filter(|h| !h.to_remove).cloned().collect::>(), + Some(guid) => { + if let Some(entry) = inner.entries.iter().find(|e| e.handler_type == *guid) { + entry.handlers.iter().filter(|h| !h.to_remove).cloned().collect::>() + } else { + Vec::new() + } + } + } + // lock released here + }; + + let short_circuit = handler_type.is_some(); + + // ----- Phase 2: dispatch without the lock held ----- + let return_status = Self::dispatch_handler_snapshot( + &handlers_snapshot, + handler_type, + context, + comm_buffer, + comm_buffer_size, + short_circuit, + ); + + // ----- Phase 3: update depth and clean up under the lock ----- + { + let mut inner = self.inner.lock(); + inner.manage_calling_depth -= 1; + + if inner.manage_calling_depth == 0 { + Self::cleanup_removed_handlers(&mut inner); + } + } + + return_status + } + + /// Dispatch a snapshot of handlers. The database lock is NOT held. + fn dispatch_handler_snapshot( + handlers: &[MmiHandler], + handler_type: Option<&efi::Guid>, + context: *const c_void, + comm_buffer: *mut c_void, + comm_buffer_size: *mut usize, + short_circuit: bool, + ) -> efi::Status { + if handlers.is_empty() { + return efi::Status::NOT_FOUND; + } + + let mut return_status = efi::Status::NOT_FOUND; + + // Provide a dummy GUID for root dispatch (handlers don't use it). + let null_guid = efi::Guid::from_fields(0, 0, 0, 0, 0, &[0; 6]); + let guid_ref = handler_type.unwrap_or(&null_guid); + + for handler in handlers { + let status = match handler.kind { + HandlerKind::External(entry_point) => { + // SAFETY: External handler follows the PI spec efiapi calling convention. + // The dispatch_handle is the monotonic ID cast to a handle. + unsafe { entry_point(handler.id as efi::Handle, context, comm_buffer, comm_buffer_size) } + } + HandlerKind::Internal(fn_ptr) => fn_ptr(guid_ref, comm_buffer, comm_buffer_size), + }; + + match status { + efi::Status::SUCCESS => { + return_status = efi::Status::SUCCESS; + if short_circuit { + break; + } + } + s if s == INTERRUPT_PENDING => { + if short_circuit { + return INTERRUPT_PENDING; + } + if return_status != efi::Status::SUCCESS { + return_status = status; + } + } + s if s == WARN_INTERRUPT_SOURCE_QUIESCED => { + return_status = efi::Status::SUCCESS; + } + _ => { + // Other statuses are ignored per PI spec + } + } + } + + return_status + } + + /// Remove handlers marked with `to_remove` and clean up empty entries. + fn cleanup_removed_handlers(inner: &mut MmiDatabaseInner) { + inner.root_handlers.retain(|h| !h.to_remove); + + inner.entries.retain_mut(|entry| { + entry.handlers.retain(|h| !h.to_remove); + !entry.handlers.is_empty() + }); + } +} diff --git a/patina_mm_user_core/src/pool_allocator.rs b/patina_mm_user_core/src/pool_allocator.rs new file mode 100644 index 000000000..d96f31160 --- /dev/null +++ b/patina_mm_user_core/src/pool_allocator.rs @@ -0,0 +1,271 @@ +//! Pool Allocator +//! +//! This module provides a trait-based page allocator abstraction and a generic pool +//! allocator for the MM User Core. +//! +//! ## Design +//! +//! The [`PageAllocatorBackend`] trait abstracts the page allocation mechanism. +//! The user core implements it by issuing `syscall` instructions that thunk +//! into the supervisor for page allocation. +//! +//! The [`PoolAllocator`] is a bump-allocator built on top of any `PageAllocatorBackend`. +//! It implements [`GlobalAlloc`] so it can be used as `#[global_allocator]`. +//! +//! ## Block Management +//! +//! Block metadata is stored **in-band** at the start of each page allocation, forming +//! an intrusive linked list. This means there is no fixed cap on the number of blocks — +//! the allocator grows dynamically as needed by requesting more pages from the backend. +//! When all allocations within a block are freed, the block is unlinked from the list +//! and the pages are returned to the backend. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use core::{ + alloc::{GlobalAlloc, Layout}, + mem, ptr, +}; + +use spin::Mutex; + +/// Standard UEFI page size (4 KB). +pub const PAGE_SIZE: usize = 4096; + +/// Minimum allocation size for the pool allocator. +const MIN_POOL_ALLOC_SIZE: usize = 16; + +/// Errors that can occur during page allocation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PageAllocError { + /// The allocator has not been initialized. + NotInitialized, + /// No free pages available to satisfy the request. + OutOfMemory, + /// The requested address is not aligned to page boundary. + NotAligned, + /// The address is not within any known SMRAM region. + InvalidAddress, + /// The address was not previously allocated. + NotAllocated, + /// Too many regions to track. + TooManyRegions, + /// A syscall to the supervisor failed. + SyscallFailed(u64), +} + +/// Trait for page-granularity memory allocation. +/// +/// Implementors provide the actual page allocation mechanism. The user core +/// implements this by issuing syscalls to the supervisor. +pub trait PageAllocatorBackend: Send + Sync { + /// Allocates `num_pages` contiguous pages. + /// + /// Returns the physical base address of the allocated region on success. + fn allocate_pages(&self, num_pages: usize) -> Result; + + /// Frees `num_pages` contiguous pages starting at `addr`. + fn free_pages(&self, addr: u64, num_pages: usize) -> Result<(), PageAllocError>; + + /// Returns whether the page allocator has been initialized and is ready for use. + fn is_initialized(&self) -> bool; +} + +/// In-band header stored at the beginning of each pool page block. +/// +/// By placing the metadata inside the allocated pages themselves, we avoid +/// any fixed-size bookkeeping array. Blocks form a singly-linked list so +/// traversal, insertion, and removal are straightforward. +#[repr(C)] +struct PoolBlockHeader { + /// Pointer to the next block in the linked list (`null` if this is the tail). + next: *mut PoolBlockHeader, + /// Number of pages backing this block (includes the header). + num_pages: usize, + /// Current bump offset (in bytes from the block base). Starts just past the header. + offset: usize, + /// Number of live allocations served from this block. + alloc_count: usize, +} + +impl PoolBlockHeader { + /// Base address of this block (== address of the header itself). + fn base(&self) -> usize { + self as *const Self as usize + } + + /// Total usable capacity of this block in bytes. + fn capacity(&self) -> usize { + self.num_pages * PAGE_SIZE + } + + /// Remaining bytes available for bump allocation. + fn remaining(&self) -> usize { + self.capacity().saturating_sub(self.offset) + } + + /// Returns `true` if the given address falls within this block's page range. + fn contains(&self, addr: usize) -> bool { + addr >= self.base() && addr < self.base() + self.capacity() + } + + /// Try to bump-allocate `layout` from this block. + fn try_alloc(&mut self, layout: Layout) -> Option<*mut u8> { + let current_ptr = self.base() + self.offset; + let align = layout.align().max(MIN_POOL_ALLOC_SIZE); + let aligned_ptr = (current_ptr + align - 1) & !(align - 1); + let padding = aligned_ptr - current_ptr; + let total_size = padding + layout.size(); + + if total_size > self.remaining() { + return None; + } + + self.offset += total_size; + self.alloc_count += 1; + + Some(aligned_ptr as *mut u8) + } +} + +/// Pool allocator built on top of a [`PageAllocatorBackend`]. +/// +/// This allocator provides smaller-granularity allocations by requesting +/// full pages from the backend and subdividing them via bump allocation. +pub struct PoolAllocator { + /// Reference to the underlying page allocator. + page_allocator: &'static P, + /// Head of the intrusive linked list of pool blocks. + head: Mutex<*mut PoolBlockHeader>, +} + +// SAFETY: The PoolAllocator uses internal locking (spin::Mutex) for all accesses +// to the block linked list. The raw pointer is only dereferenced under the lock. +unsafe impl Send for PoolAllocator

{} +unsafe impl Sync for PoolAllocator

{} + +impl PoolAllocator

{ + /// Creates a new pool allocator backed by the given page allocator. + pub const fn new(page_allocator: &'static P) -> Self { + Self { page_allocator, head: Mutex::new(ptr::null_mut()) } + } + + /// Allocate a new page block large enough for `min_size` bytes of payload + /// and prepend it to the linked list. + fn allocate_new_block<'a>( + &self, + head: &mut *mut PoolBlockHeader, + min_size: usize, + ) -> Option<&'a mut PoolBlockHeader> { + let header_size = mem::size_of::(); + let needed = min_size + header_size; + let num_pages = ((needed + PAGE_SIZE - 1) / PAGE_SIZE).max(1); + + let base = match self.page_allocator.allocate_pages(num_pages) { + Ok(addr) => addr, + Err(e) => { + log::warn!("Pool allocator: failed to allocate {} pages: {:?}", num_pages, e); + return None; + } + }; + + // SAFETY: `base` is a freshly allocated, page-aligned region of at least + // `num_pages * PAGE_SIZE` bytes. We place our header at offset 0. + let header = unsafe { &mut *(base as *mut PoolBlockHeader) }; + header.next = *head; + header.num_pages = num_pages; + header.offset = header_size; + header.alloc_count = 0; + + *head = header as *mut PoolBlockHeader; + + log::trace!("Pool allocator: new block at {:#018x} ({} pages)", base, num_pages,); + + Some(header) + } +} + +unsafe impl GlobalAlloc for PoolAllocator

{ + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + if !self.page_allocator.is_initialized() { + return ptr::null_mut(); + } + + let mut head = self.head.lock(); + + // Walk the linked list, try to bump-alloc from an existing block. + { + let mut current = *head; + while !current.is_null() { + // SAFETY: `current` was written by us under the same lock. + let block = unsafe { &mut *current }; + if let Some(ptr) = block.try_alloc(layout) { + return ptr; + } + current = block.next; + } + } + + // No existing block had space — allocate a new one and retry. + if let Some(block) = self.allocate_new_block(&mut head, layout.size()) { + if let Some(ptr) = block.try_alloc(layout) { + return ptr; + } + } + + ptr::null_mut() + } + + unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) { + if ptr.is_null() { + return; + } + + let mut head = self.head.lock(); + let addr = ptr as usize; + + let mut prev: *mut PoolBlockHeader = ptr::null_mut(); + let mut current = *head; + + while !current.is_null() { + // SAFETY: `current` was written by us under the same lock. + let block = unsafe { &mut *current }; + + if block.contains(addr) { + block.alloc_count = block.alloc_count.saturating_sub(1); + + // If the block is now empty, unlink it and free the pages. + if block.alloc_count == 0 { + let next = block.next; + let base = block.base() as u64; + let num_pages = block.num_pages; + + if prev.is_null() { + *head = next; + } else { + // SAFETY: `prev` is a valid block we visited earlier. + unsafe { (*prev).next = next }; + } + + if let Err(e) = self.page_allocator.free_pages(base, num_pages) { + log::warn!("Pool allocator: failed to free block at {:#018x}: {:?}", base, e,); + } else { + log::trace!("Pool allocator: freed block at {:#018x} ({} pages)", base, num_pages); + } + } + + return; + } + + prev = current; + current = block.next; + } + + log::warn!("Pool allocator: dealloc called with unknown pointer {:#018x}", addr,); + } +} diff --git a/patina_mm_user_core/src/protocol_db.rs b/patina_mm_user_core/src/protocol_db.rs new file mode 100644 index 000000000..4266b2138 --- /dev/null +++ b/patina_mm_user_core/src/protocol_db.rs @@ -0,0 +1,84 @@ +//! Protocol/Handle Database +//! +//! This module provides a simplified protocol database for the MM User Core. +//! It tracks installed protocols for depex evaluation and driver service use. +//! +//! In the DXE Core, the protocol database is a full handle-protocol mapping +//! (handles can have multiple protocols, protocols can be on multiple handles). +//! The MM User Core simplifies this to a flat set of installed protocol GUIDs +//! since MM drivers primarily need: +//! - `MmInstallProtocolInterface`: Register that a protocol is available +//! - `MmLocateProtocol`: Check if a protocol is available (for depex evaluation) +//! - `registered_protocols()`: Get the list of all installed protocols (for depex eval) +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use alloc::vec::Vec; + +use r_efi::efi; +use spin::Mutex; + +/// A simplified protocol/handle database for the MM User Core. +/// +/// Tracks installed protocol GUIDs for depex evaluation and protocol location. +pub struct ProtocolDatabase { + /// Internal state protected by a mutex. + inner: Mutex, +} + +struct ProtocolDatabaseInner { + /// The set of installed protocol GUIDs. + protocols: Vec, +} + +impl ProtocolDatabase { + /// Creates a new empty `ProtocolDatabase`. + pub const fn new() -> Self { + Self { inner: Mutex::new(ProtocolDatabaseInner { protocols: Vec::new() }) } + } + + /// Install a protocol interface. + /// + /// Registers the given protocol GUID as available. Duplicate GUIDs are allowed + /// (multiple instances of the same protocol can be installed on different handles). + pub fn install_protocol(&self, protocol_guid: &efi::Guid) -> Result<(), efi::Status> { + let mut inner = self.inner.lock(); + inner.protocols.push(*protocol_guid); + log::debug!("Installed protocol: {:?}", protocol_guid); + Ok(()) + } + + /// Uninstall a protocol interface. + /// + /// Removes the first occurrence of the given protocol GUID. + pub fn uninstall_protocol(&self, protocol_guid: &efi::Guid) -> Result<(), efi::Status> { + let mut inner = self.inner.lock(); + if let Some(pos) = inner.protocols.iter().position(|g| g == protocol_guid) { + inner.protocols.remove(pos); + log::debug!("Uninstalled protocol: {:?}", protocol_guid); + Ok(()) + } else { + log::warn!("Protocol {:?} not found for uninstall.", protocol_guid); + Err(efi::Status::NOT_FOUND) + } + } + + /// Check if a protocol is installed. + pub fn is_protocol_installed(&self, protocol_guid: &efi::Guid) -> bool { + let inner = self.inner.lock(); + inner.protocols.contains(protocol_guid) + } + + /// Get the list of all installed protocol GUIDs. + /// + /// This is used by the depex evaluator to determine which dependencies are satisfied. + pub fn registered_protocols(&self) -> Vec { + let inner = self.inner.lock(); + inner.protocols.clone() + } +} diff --git a/sdk/patina/Cargo.toml b/sdk/patina/Cargo.toml index a0d4fff1d..07388f0c0 100644 --- a/sdk/patina/Cargo.toml +++ b/sdk/patina/Cargo.toml @@ -72,7 +72,7 @@ doc = ['alloc'] alloc = ["dep:goblin", "dep:mu_rust_helpers", "dep:fixedbitset"] mockall = ["dep:mockall", "std"] global_allocator = [] -default = ['alloc'] +default = [] serde = ["alloc", "dep:serde", "serde/alloc", "dep:serde_json"] serde-with-yaml = ["serde", "dep:serde_yaml"] diff --git a/sdk/patina/src/lib.rs b/sdk/patina/src/lib.rs index ee0fb7a34..6161e081c 100644 --- a/sdk/patina/src/lib.rs +++ b/sdk/patina/src/lib.rs @@ -51,6 +51,8 @@ pub mod hash; pub mod log; pub mod management_mode; #[cfg(any(test, feature = "alloc"))] +pub mod mm_services; +#[cfg(any(test, feature = "alloc"))] pub mod performance; pub mod pi; #[cfg(any(test, feature = "alloc"))] diff --git a/sdk/patina/src/mm_services.rs b/sdk/patina/src/mm_services.rs new file mode 100644 index 000000000..a37c43d85 --- /dev/null +++ b/sdk/patina/src/mm_services.rs @@ -0,0 +1,319 @@ +//! MM (Management Mode) Services type definitions and trait. +//! +//! This module provides the Rust definitions for the PI `EFI_MM_SYSTEM_TABLE` +//! and an `MmServices` trait that wraps the raw C function-pointer table with +//! safe Rust method signatures, following the same pattern as +//! [`boot_services::BootServices`](crate::boot_services::BootServices). +//! +//! ## Layout +//! +//! * [`EfiMmSystemTable`] — `#[repr(C)]` struct matching the C +//! `_EFI_MM_SYSTEM_TABLE` layout from `PiMmCis.h`. +//! * [`MmServices`] — Safe Rust trait exposing the system-table services. +//! * [`StandardMmServices`] — Concrete wrapper around `*mut EfiMmSystemTable` +//! that implements `MmServices` by calling through the function pointers. +//! +//! Cores (e.g., `patina_mm_user_core`) allocate an `EfiMmSystemTable`, populate +//! its function pointers with their own `extern "efiapi"` thunks, and hand the +//! raw pointer to dispatched MM drivers. Drivers that want safe access can wrap +//! it in a `StandardMmServices`. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use core::ffi::c_void; + +use crate::pi::mm_cis::{EfiMmSystemTable, MmiHandlerEntryPoint}; +use r_efi::efi; +use spin::Once; + +// SAFETY: The system table is allocated once and its pointer is shared read-only +// with dispatched drivers. Internal mutation goes through synchronized databases. +unsafe impl Send for EfiMmSystemTable {} +unsafe impl Sync for EfiMmSystemTable {} + +/// Wrapper around a raw `*mut EfiMmSystemTable` pointer that implements +/// [`MmServices`] by calling through the C function-pointer table. +/// +/// This is the MM equivalent of +/// [`StandardBootServices`](crate::boot_services::StandardBootServices). +pub struct StandardMmServices { + efi_mm_system_table: Once<*mut EfiMmSystemTable>, +} + +// SAFETY: The raw pointer is only written once (protected by `Once`) and the +// underlying table is not expected to change after initialisation. +unsafe impl Sync for StandardMmServices {} +unsafe impl Send for StandardMmServices {} + +impl StandardMmServices { + /// Create a new `StandardMmServices` from an existing system table pointer. + pub fn new(mm_system_table: *mut EfiMmSystemTable) -> Self { + let this = Self::new_uninit(); + this.init(mm_system_table); + this + } + + /// Create an uninitialised instance. + pub const fn new_uninit() -> Self { + Self { efi_mm_system_table: Once::new() } + } + + /// Initialise with the given system table pointer. + pub fn init(&self, mm_system_table: *mut EfiMmSystemTable) { + self.efi_mm_system_table.call_once(|| mm_system_table); + } + + /// Returns `true` if the instance has been initialised. + pub fn is_init(&self) -> bool { + self.efi_mm_system_table.is_completed() + } + + /// Returns the raw system table pointer (panics if uninitialised). + pub fn as_mut_ptr(&self) -> *mut EfiMmSystemTable { + *self.efi_mm_system_table.get().expect("StandardMmServices is not initialized!") + } +} + +impl Clone for StandardMmServices { + fn clone(&self) -> Self { + if let Some(ptr) = self.efi_mm_system_table.get() { + StandardMmServices::new(*ptr) + } else { + StandardMmServices::new_uninit() + } + } +} + +impl core::fmt::Debug for StandardMmServices { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + if !self.is_init() { + return f.debug_struct("StandardMmServices").field("table", &"Not Initialized").finish(); + } + f.debug_struct("StandardMmServices").field("table", &self.as_mut_ptr()).finish() + } +} + +/// Safe Rust interface to the MM System Table services. +/// +/// This is the MM analogue of +/// [`BootServices`](crate::boot_services::BootServices). +/// Each method maps 1:1 to a function pointer in [`EfiMmSystemTable`]. +pub trait MmServices { + // ---- Memory services ------------------------------------------------ + + /// Allocate pool memory. + /// + /// PI Spec: `EFI_MM_SYSTEM_TABLE.MmAllocatePool` + fn allocate_pool(&self, pool_type: efi::MemoryType, size: usize) -> Result<*mut u8, efi::Status>; + + /// Free pool memory. + /// + /// PI Spec: `EFI_MM_SYSTEM_TABLE.MmFreePool` + fn free_pool(&self, buffer: *mut u8) -> Result<(), efi::Status>; + + /// Allocate pages. + /// + /// PI Spec: `EFI_MM_SYSTEM_TABLE.MmAllocatePages` + fn allocate_pages( + &self, + alloc_type: efi::AllocateType, + memory_type: efi::MemoryType, + pages: usize, + ) -> Result; + + /// Free pages. + /// + /// PI Spec: `EFI_MM_SYSTEM_TABLE.MmFreePages` + fn free_pages(&self, memory: u64, pages: usize) -> Result<(), efi::Status>; + + // ---- Protocol services ---------------------------------------------- + + /// Install a protocol interface on a handle. + /// + /// PI Spec: `EFI_MM_SYSTEM_TABLE.MmInstallProtocolInterface` + /// + /// # Safety + /// + /// `interface` must be a valid pointer to the protocol structure or null. + unsafe fn install_protocol_interface( + &self, + handle: *mut efi::Handle, + protocol: &efi::Guid, + interface_type: efi::InterfaceType, + interface: *mut c_void, + ) -> Result<(), efi::Status>; + + /// Uninstall a protocol interface from a handle. + /// + /// PI Spec: `EFI_MM_SYSTEM_TABLE.MmUninstallProtocolInterface` + /// + /// # Safety + /// + /// `interface` must match the pointer that was installed. + unsafe fn uninstall_protocol_interface( + &self, + handle: efi::Handle, + protocol: &efi::Guid, + interface: *mut c_void, + ) -> Result<(), efi::Status>; + + /// Query a handle for a protocol. + /// + /// PI Spec: `EFI_MM_SYSTEM_TABLE.MmHandleProtocol` + /// + /// # Safety + /// + /// The returned pointer must be used carefully to avoid aliasing violations. + unsafe fn handle_protocol(&self, handle: efi::Handle, protocol: &efi::Guid) -> Result<*mut c_void, efi::Status>; + + /// Locate the first device that supports a protocol. + /// + /// PI Spec: `EFI_MM_SYSTEM_TABLE.MmLocateProtocol` + /// + /// # Safety + /// + /// The returned pointer must be used carefully to avoid aliasing violations. + unsafe fn locate_protocol(&self, protocol: &efi::Guid) -> Result<*mut c_void, efi::Status>; + + // ---- MMI management ------------------------------------------------- + + /// Manage (dispatch) an MMI. + /// + /// PI Spec: `EFI_MM_SYSTEM_TABLE.MmiManage` + fn mmi_manage( + &self, + handler_type: Option<&efi::Guid>, + context: *const c_void, + comm_buffer: *mut c_void, + comm_buffer_size: *mut usize, + ) -> efi::Status; + + /// Register an MMI handler. + /// + /// PI Spec: `EFI_MM_SYSTEM_TABLE.MmiHandlerRegister` + fn mmi_handler_register( + &self, + handler: MmiHandlerEntryPoint, + handler_type: Option<&efi::Guid>, + ) -> Result; + + /// Unregister an MMI handler. + /// + /// PI Spec: `EFI_MM_SYSTEM_TABLE.MmiHandlerUnRegister` + fn mmi_handler_unregister(&self, dispatch_handle: efi::Handle) -> Result<(), efi::Status>; +} + +impl MmServices for StandardMmServices { + fn allocate_pool(&self, pool_type: efi::MemoryType, size: usize) -> Result<*mut u8, efi::Status> { + let mmst = unsafe { &*self.as_mut_ptr() }; + let mut buffer: *mut c_void = core::ptr::null_mut(); + let status = (mmst.mm_allocate_pool)(pool_type, size, &mut buffer); + if status == efi::Status::SUCCESS { Ok(buffer as *mut u8) } else { Err(status) } + } + + fn free_pool(&self, buffer: *mut u8) -> Result<(), efi::Status> { + let mmst = unsafe { &*self.as_mut_ptr() }; + let status = (mmst.mm_free_pool)(buffer as *mut c_void); + if status == efi::Status::SUCCESS { Ok(()) } else { Err(status) } + } + + fn allocate_pages( + &self, + alloc_type: efi::AllocateType, + memory_type: efi::MemoryType, + pages: usize, + ) -> Result { + let mmst = unsafe { &*self.as_mut_ptr() }; + let mut memory: efi::PhysicalAddress = 0; + let status = (mmst.mm_allocate_pages)(alloc_type, memory_type, pages, &mut memory); + if status == efi::Status::SUCCESS { Ok(memory) } else { Err(status) } + } + + fn free_pages(&self, memory: u64, pages: usize) -> Result<(), efi::Status> { + let mmst = unsafe { &*self.as_mut_ptr() }; + let status = (mmst.mm_free_pages)(memory, pages); + if status == efi::Status::SUCCESS { Ok(()) } else { Err(status) } + } + + unsafe fn install_protocol_interface( + &self, + handle: *mut efi::Handle, + protocol: &efi::Guid, + interface_type: efi::InterfaceType, + interface: *mut c_void, + ) -> Result<(), efi::Status> { + let mmst = unsafe { &*self.as_mut_ptr() }; + let status = (mmst.mm_install_protocol_interface)( + handle, + protocol as *const efi::Guid as *mut efi::Guid, + interface_type, + interface, + ); + if status == efi::Status::SUCCESS { Ok(()) } else { Err(status) } + } + + unsafe fn uninstall_protocol_interface( + &self, + handle: efi::Handle, + protocol: &efi::Guid, + interface: *mut c_void, + ) -> Result<(), efi::Status> { + let mmst = unsafe { &*self.as_mut_ptr() }; + let status = + (mmst.mm_uninstall_protocol_interface)(handle, protocol as *const efi::Guid as *mut efi::Guid, interface); + if status == efi::Status::SUCCESS { Ok(()) } else { Err(status) } + } + + unsafe fn handle_protocol(&self, handle: efi::Handle, protocol: &efi::Guid) -> Result<*mut c_void, efi::Status> { + let mmst = unsafe { &*self.as_mut_ptr() }; + let mut interface: *mut c_void = core::ptr::null_mut(); + let status = (mmst.mm_handle_protocol)(handle, protocol as *const efi::Guid as *mut efi::Guid, &mut interface); + if status == efi::Status::SUCCESS { Ok(interface) } else { Err(status) } + } + + unsafe fn locate_protocol(&self, protocol: &efi::Guid) -> Result<*mut c_void, efi::Status> { + let mmst = unsafe { &*self.as_mut_ptr() }; + let mut interface: *mut c_void = core::ptr::null_mut(); + let status = (mmst.mm_locate_protocol)( + protocol as *const efi::Guid as *mut efi::Guid, + core::ptr::null_mut(), + &mut interface, + ); + if status == efi::Status::SUCCESS { Ok(interface) } else { Err(status) } + } + + fn mmi_manage( + &self, + handler_type: Option<&efi::Guid>, + context: *const c_void, + comm_buffer: *mut c_void, + comm_buffer_size: *mut usize, + ) -> efi::Status { + let mmst = unsafe { &*self.as_mut_ptr() }; + let guid_ptr = handler_type.map_or(core::ptr::null(), |g| g as *const efi::Guid); + unsafe { (mmst.mmi_manage)(guid_ptr, context, comm_buffer, comm_buffer_size) } + } + + fn mmi_handler_register( + &self, + handler: MmiHandlerEntryPoint, + handler_type: Option<&efi::Guid>, + ) -> Result { + let mmst = unsafe { &*self.as_mut_ptr() }; + let guid_ptr = handler_type.map_or(core::ptr::null(), |g| g as *const efi::Guid); + let mut dispatch_handle: efi::Handle = core::ptr::null_mut(); + let status = unsafe { (mmst.mmi_handler_register)(handler, guid_ptr, &mut dispatch_handle) }; + if status == efi::Status::SUCCESS { Ok(dispatch_handle) } else { Err(status) } + } + + fn mmi_handler_unregister(&self, dispatch_handle: efi::Handle) -> Result<(), efi::Status> { + let mmst = unsafe { &*self.as_mut_ptr() }; + let status = unsafe { (mmst.mmi_handler_unregister)(dispatch_handle) }; + if status == efi::Status::SUCCESS { Ok(()) } else { Err(status) } + } +}