diff --git a/openhcl/hcl/src/ioctl/register.rs b/openhcl/hcl/src/ioctl/register.rs index ed963bb1c1..25ed37ef2e 100644 --- a/openhcl/hcl/src/ioctl/register.rs +++ b/openhcl/hcl/src/ioctl/register.rs @@ -430,6 +430,14 @@ impl Hcl { ) } + /// Get the current [`hvdef::HvRegisterVsmPartitionConfig`] register. + pub fn get_vtl2_vsm_partition_config( + &self, + ) -> Result { + let value = self.get_partition_vtl2_register(HvArchRegisterName::VsmPartitionConfig)?; + Ok(hvdef::HvRegisterVsmPartitionConfig::from(value.as_u64())) + } + /// Configure guest VSM. /// The only configuration attribute currently supported is changing the maximum number of /// guest-visible virtual trust levels for the partition. (VTL 1) diff --git a/openhcl/underhill_core/src/emuplat/firmware.rs b/openhcl/underhill_core/src/emuplat/firmware.rs index 4e94ac894d..8ced753d31 100644 --- a/openhcl/underhill_core/src/emuplat/firmware.rs +++ b/openhcl/underhill_core/src/emuplat/firmware.rs @@ -67,3 +67,31 @@ impl firmware_uefi::platform::nvram::VsmConfig for UnderhillVsmConfig { } } } + +/// MOR (Memory Overwrite Request) configuration for Underhill. +/// +/// When the guest sets the MOR bit, this notifies the hypervisor to ensure +/// memory is scrubbed on the next partition reset by setting the +/// `zero_memory_on_reset` flag in `HvRegisterVsmPartitionConfig`. +#[derive(Debug)] +pub struct UnderhillMorConfig { + pub partition: Weak, +} + +impl firmware_uefi::platform::nvram::MorConfig for UnderhillMorConfig { + fn notify_mor_set(&self, mor_value: u8) { + const MOR_CLEAR_MEMORY_BIT_MASK: u8 = 0x01; + + let clear_memory = (mor_value & MOR_CLEAR_MEMORY_BIT_MASK) != 0; + + if let Some(partition) = self.partition.upgrade() { + if let Err(err) = partition.set_zero_memory_on_reset(clear_memory) { + tracelimit::warn_ratelimited!( + CVM_ALLOWED, + error = err.as_ref() as &dyn std::error::Error, + "failed to update zero_memory_on_reset for MOR" + ); + } + } + } +} diff --git a/openhcl/underhill_core/src/worker.rs b/openhcl/underhill_core/src/worker.rs index df511c89e5..b62a842ae7 100644 --- a/openhcl/underhill_core/src/worker.rs +++ b/openhcl/underhill_core/src/worker.rs @@ -23,6 +23,7 @@ use crate::dispatch::vtl2_settings_worker::disk_from_disk_type; use crate::dispatch::vtl2_settings_worker::wait_for_mana; use crate::emuplat::EmuplatServicing; use crate::emuplat::firmware::UnderhillLogger; +use crate::emuplat::firmware::UnderhillMorConfig; use crate::emuplat::firmware::UnderhillVsmConfig; use crate::emuplat::framebuffer::FramebufferRemoteControl; use crate::emuplat::i440bx_host_pci_bridge::ArcMutexGetBackedAdjustGpaRange; @@ -2612,6 +2613,9 @@ async fn new_underhill_vm( partition: Arc::downgrade(&partition), })), time_source: Box::new(rtc_time_source.new_linked_clock()), + mor_config: Some(Box::new(UnderhillMorConfig { + partition: Arc::downgrade(&partition), + })), }) } FirmwareType::None => {} diff --git a/openhcl/virt_mshv_vtl/src/lib.rs b/openhcl/virt_mshv_vtl/src/lib.rs index 60ffabb807..dfa1e90b49 100755 --- a/openhcl/virt_mshv_vtl/src/lib.rs +++ b/openhcl/virt_mshv_vtl/src/lib.rs @@ -839,6 +839,35 @@ impl UhPartition { Ok(()) } + /// Ensures that guest memory will be zeroed on the next partition reset. + /// + /// This is used to implement TCG MOR (Memory Overwrite Request) by updating + /// `HvRegisterVsmPartitionConfig.zero_memory_on_reset`. + pub fn set_zero_memory_on_reset(&self, zero: bool) -> anyhow::Result<()> { + use anyhow::Context; + + let config = self + .inner + .hcl + .get_vtl2_vsm_partition_config() + .context("failed to get VsmPartitionConfig")?; + + // Skip the hypercall if the flag is already in the desired state. + if config.zero_memory_on_reset() == zero { + return Ok(()); + } + + let config = config.with_zero_memory_on_reset(zero); + + self.inner + .hcl + .set_vtl2_vsm_partition_config(config) + .context("failed to set VsmPartitionConfig")?; + + tracing::debug!(zero, "updated zero_memory_on_reset for MOR"); + Ok(()) + } + /// Returns the current hypervisor reference time, in 100ns units. pub fn reference_time(&self) -> u64 { if let Some(hv) = self.inner.hv() { diff --git a/openvmm/openvmm_core/src/worker/dispatch.rs b/openvmm/openvmm_core/src/worker/dispatch.rs index abce813589..e6e7bc6935 100644 --- a/openvmm/openvmm_core/src/worker/dispatch.rs +++ b/openvmm/openvmm_core/src/worker/dispatch.rs @@ -1216,6 +1216,7 @@ impl InitializedVm { time_source: Box::new(local_clock::SystemTimeClock::new( LocalClockDelta::from_millis(cfg.rtc_delta_milliseconds), )), + mor_config: None, }) } #[cfg(guest_arch = "x86_64")] diff --git a/vm/devices/firmware/firmware_uefi/src/lib.rs b/vm/devices/firmware/firmware_uefi/src/lib.rs index 78e5135487..20d400494a 100644 --- a/vm/devices/firmware/firmware_uefi/src/lib.rs +++ b/vm/devices/firmware/firmware_uefi/src/lib.rs @@ -71,6 +71,7 @@ use inspect::InspectMut; use local_clock::InspectableLocalClock; use pal_async::local::block_on; use platform::logger::UefiLogger; +use platform::nvram::MorConfig; use platform::nvram::VsmConfig; use service::diagnostics::DEFAULT_LOGS_PER_PERIOD; use service::diagnostics::WATCHDOG_LOGS_PER_PERIOD; @@ -143,6 +144,7 @@ pub struct UefiRuntimeDeps<'a> { pub generation_id_deps: generation_id::GenerationIdRuntimeDeps, pub vsm_config: Option>, pub time_source: Box, + pub mor_config: Option>, } /// The Hyper-V UEFI services chipset device. @@ -164,6 +166,11 @@ pub struct UefiDevice { #[inspect(hex)] address: u32, + // MOR (Memory Overwrite Request) state + mor_bit_status: bool, + #[inspect(skip)] + mor_config: Option>, + // Receiver for watchdog timeout events #[inspect(skip)] watchdog_recv: mesh::Receiver<()>, @@ -185,6 +192,7 @@ impl UefiDevice { generation_id_deps, vsm_config, time_source, + mor_config, } = runtime_deps; // Create the UEFI device with the rest of the services. @@ -194,6 +202,8 @@ impl UefiDevice { address: 0, gm, watchdog_recv, + mor_bit_status: true, // initialized to success, matching legacy behavior + mor_config, service: UefiDeviceServices { nvram: service::nvram::NvramServices::new( nvram_storage, @@ -233,6 +243,10 @@ impl UefiDevice { self.handle_watchdog_read(reg) } UefiCommand::NFIT_SIZE => 0, // no NFIT + UefiCommand::MOR_SET_VARIABLE => { + // Return 1 if the last MOR SetVariable succeeded, 0 otherwise. + u32::from(self.mor_bit_status) + } _ => { tracelimit::warn_ratelimited!(?addr, "unknown uefi read"); !0 @@ -285,10 +299,45 @@ impl UefiDevice { None, ); } + UefiCommand::MOR_SET_VARIABLE => block_on(self.handle_mor_set_variable(data as u8)), _ => tracelimit::warn_ratelimited!(addr, data, "unknown uefi write"), } } + /// Handle the MOR_SET_VARIABLE command from the guest. + /// + /// Persists the MOR variable in NVRAM and notifies the platform so it can + /// arrange for memory to be scrubbed on the next reset. + async fn handle_mor_set_variable(&mut self, value: u8) { + use uefi_specs::uefi::nvram::EfiVariableAttributes; + use uefi_specs::uefi::nvram::vars; + + let attr = EfiVariableAttributes::DEFAULT_ATTRIBUTES; + + let result = self + .service + .nvram + .set_mor_variable(vars::MEMORY_OVERWRITE_REQUEST_CONTROL(), value, attr.into()) + .await; + + self.mor_bit_status = result.is_ok(); + + match result { + Ok(_) => { + if let Some(mor_config) = &self.mor_config { + mor_config.notify_mor_set(value); + } + } + Err((status, error)) => { + tracelimit::warn_ratelimited!( + ?status, + ?error, + "failed to set MOR variable in NVRAM" + ); + } + } + } + /// Extra inspection fields for the UEFI device. fn inspect_extra(&mut self, resp: &mut inspect::Response<'_>) { const USAGE: &str = @@ -350,6 +399,7 @@ impl ChangeDeviceState for UefiDevice { async fn reset(&mut self) { self.address = 0; + self.mor_bit_status = true; self.service.nvram.reset(); self.service.event_log.reset(); @@ -568,6 +618,8 @@ mod save_restore { pub time: ::SavedState, #[mesh(7)] pub diagnostics: ::SavedState, + #[mesh(8)] + pub mor_bit_status: bool, } } @@ -580,6 +632,8 @@ mod save_restore { command_set: _, gm: _, watchdog_recv: _, + mor_bit_status, + mor_config: _, service: UefiDeviceServices { nvram, @@ -601,6 +655,7 @@ mod save_restore { generation_id: generation_id.save()?, time: time.save()?, diagnostics: diagnostics.save()?, + mor_bit_status: *mor_bit_status, }) } @@ -614,9 +669,11 @@ mod save_restore { generation_id, time, diagnostics, + mor_bit_status, } = state; self.address = address; + self.mor_bit_status = mor_bit_status; self.service.nvram.restore(nvram)?; self.service.event_log.restore(event_log)?; diff --git a/vm/devices/firmware/firmware_uefi/src/platform/nvram.rs b/vm/devices/firmware/firmware_uefi/src/platform/nvram.rs index b8f910ebf7..904120a993 100644 --- a/vm/devices/firmware/firmware_uefi/src/platform/nvram.rs +++ b/vm/devices/firmware/firmware_uefi/src/platform/nvram.rs @@ -16,3 +16,18 @@ pub use uefi_specs::uefi::time::EFI_TIME; pub trait VsmConfig: Send { fn revoke_guest_vsm(&self); } + +/// Callbacks for MOR (Memory Overwrite Request) bit changes. +/// +/// When the guest sets the MOR bit via the UEFI device, the platform may need +/// to take action to ensure memory is scrubbed on the next reset. In Underhill, +/// this is done by setting the `zero_memory_on_reset` flag in +/// `HvRegisterVsmPartitionConfig`. +pub trait MorConfig: Send { + /// Called when the guest sets the MOR variable. + /// + /// `mor_value` is the raw byte written by the guest. Bit 0 + /// (`MOR_CLEAR_MEMORY_BIT_MASK`) indicates whether memory should be cleared + /// on the next reset. + fn notify_mor_set(&self, mor_value: u8); +} diff --git a/vm/devices/firmware/firmware_uefi/src/service/nvram/mod.rs b/vm/devices/firmware/firmware_uefi/src/service/nvram/mod.rs index d08e745923..799ad50337 100644 --- a/vm/devices/firmware/firmware_uefi/src/service/nvram/mod.rs +++ b/vm/devices/firmware/firmware_uefi/src/service/nvram/mod.rs @@ -99,6 +99,18 @@ impl NvramServices { self.services.prepare_for_boot(); } + /// Set the MOR (Memory Overwrite Request) variable in NVRAM. + pub async fn set_mor_variable( + &mut self, + (vendor, name): (guid::Guid, &ucs2::Ucs2LeSlice), + value: u8, + attr: u32, + ) -> Result<(), (EfiStatus, Option)> { + self.services + .set_variable_ucs2(vendor, name, attr, vec![value]) + .await + } + /// Check if this is the VM's first boot, and if so, inject various /// hard-coded and custom UEFI vars. async fn inject_vars_on_first_boot( diff --git a/vm/devices/firmware/firmware_uefi/src/service/nvram/spec_services/mod.rs b/vm/devices/firmware/firmware_uefi/src/service/nvram/spec_services/mod.rs index 2fe5df92b6..40ce1643c9 100644 --- a/vm/devices/firmware/firmware_uefi/src/service/nvram/spec_services/mod.rs +++ b/vm/devices/firmware/firmware_uefi/src/service/nvram/spec_services/mod.rs @@ -1955,4 +1955,45 @@ mod test { expected.extend_from_slice(&append_data); assert_eq!(data, Some(expected)); } + + #[async_test] + async fn mor_set_variable() { + use uefi_specs::uefi::nvram::vars; + + let nvram_storage = InMemoryNvram::new(); + let mut nvram = NvramSpecServices::new(nvram_storage); + + nvram.prepare_for_boot(); + + let (vendor, name) = vars::MEMORY_OVERWRITE_REQUEST_CONTROL(); + let attr = EfiVariableAttributes::DEFAULT_ATTRIBUTES; + + // Set MOR bit to 1 (request memory clear) + nvram + .set_variable_ucs2(vendor, name, attr.into(), vec![0x01]) + .await + .expect("failed to set MOR variable"); + + // Read the variable back + let NvramResult(data, status, _err) = nvram + .uefi_get_variable(Some(name.as_bytes()), vendor, &mut 0u32, &mut 256u32, false) + .await; + + assert_eq!(status, EfiStatus::SUCCESS); + assert_eq!(data, Some(vec![0x01])); + + // Clear MOR bit + nvram + .set_variable_ucs2(vendor, name, attr.into(), vec![0x00]) + .await + .expect("failed to clear MOR variable"); + + // Read back and verify cleared + let NvramResult(data, status, _err) = nvram + .uefi_get_variable(Some(name.as_bytes()), vendor, &mut 0u32, &mut 256u32, false) + .await; + + assert_eq!(status, EfiStatus::SUCCESS); + assert_eq!(data, Some(vec![0x00])); + } } diff --git a/vm/devices/firmware/uefi_specs/src/uefi/nvram.rs b/vm/devices/firmware/uefi_specs/src/uefi/nvram.rs index 5556ce3909..bc08fd75d5 100644 --- a/vm/devices/firmware/uefi_specs/src/uefi/nvram.rs +++ b/vm/devices/firmware/uefi_specs/src/uefi/nvram.rs @@ -201,4 +201,17 @@ pub mod vars { defn_nvram_var!(DB = (IMAGE_SECURITY_DATABASE_GUID, "db")); defn_nvram_var!(DBX = (IMAGE_SECURITY_DATABASE_GUID, "dbx")); + + /// TCG MOR (Memory Overwrite Request) variable GUID. + /// + /// See "TCG Platform Reset Attack Mitigation Specification". + pub const EFI_MEMORY_OVERWRITE_REQUEST_CONTROL_GUID: Guid = + guid::guid!("e20939be-32d4-41be-a150-897f85d49829"); + + defn_nvram_var!( + MEMORY_OVERWRITE_REQUEST_CONTROL = ( + EFI_MEMORY_OVERWRITE_REQUEST_CONTROL_GUID, + "MemoryOverwriteRequestControl" + ) + ); } diff --git a/vmm_core/vmotherboard/src/base_chipset.rs b/vmm_core/vmotherboard/src/base_chipset.rs index 4c2d602870..c156a5f1db 100644 --- a/vmm_core/vmotherboard/src/base_chipset.rs +++ b/vmm_core/vmotherboard/src/base_chipset.rs @@ -556,6 +556,7 @@ impl<'a> BaseChipsetBuilder<'a> { watchdog_recv, vsm_config, time_source, + mor_config, }) = deps_hyperv_firmware_uefi { builder @@ -585,6 +586,7 @@ impl<'a> BaseChipsetBuilder<'a> { }, vsm_config, time_source, + mor_config, }; firmware_uefi::UefiDevice::new(runtime_deps, config, foundation.is_restoring) @@ -1387,6 +1389,8 @@ pub mod options { pub vsm_config: Option>, /// Time source pub time_source: Box, + /// Interface for MOR (Memory Overwrite Request) bit notifications. + pub mor_config: Option>, } /// Hyper-V specific framebuffer device