Skip to content
8 changes: 8 additions & 0 deletions openhcl/hcl/src/ioctl/register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,14 @@ impl Hcl {
)
}

/// Get the current [`hvdef::HvRegisterVsmPartitionConfig`] register.
pub fn get_vtl2_vsm_partition_config(
&self,
) -> Result<hvdef::HvRegisterVsmPartitionConfig, GetRegError> {
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)
Expand Down
28 changes: 28 additions & 0 deletions openhcl/underhill_core/src/emuplat/firmware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<UhPartition>,
}

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) {
Comment on lines +83 to +88
tracelimit::warn_ratelimited!(
CVM_ALLOWED,
error = err.as_ref() as &dyn std::error::Error,
"failed to update zero_memory_on_reset for MOR"
);
}
Comment on lines +88 to +94
}
}
}
4 changes: 4 additions & 0 deletions openhcl/underhill_core/src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 => {}
Expand Down
29 changes: 29 additions & 0 deletions openhcl/virt_mshv_vtl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Comment thread
mebersol marked this conversation as resolved.
}

/// Returns the current hypervisor reference time, in 100ns units.
pub fn reference_time(&self) -> u64 {
if let Some(hv) = self.inner.hv() {
Expand Down
1 change: 1 addition & 0 deletions openvmm/openvmm_core/src/worker/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
57 changes: 57 additions & 0 deletions vm/devices/firmware/firmware_uefi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -143,6 +144,7 @@ pub struct UefiRuntimeDeps<'a> {
pub generation_id_deps: generation_id::GenerationIdRuntimeDeps,
pub vsm_config: Option<Box<dyn VsmConfig>>,
pub time_source: Box<dyn InspectableLocalClock>,
pub mor_config: Option<Box<dyn MorConfig>>,
}

/// The Hyper-V UEFI services chipset device.
Expand All @@ -164,6 +166,11 @@ pub struct UefiDevice {
#[inspect(hex)]
address: u32,

// MOR (Memory Overwrite Request) state
mor_bit_status: bool,
#[inspect(skip)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe worth inspecting with an is_some?

mor_config: Option<Box<dyn MorConfig>>,

// Receiver for watchdog timeout events
#[inspect(skip)]
watchdog_recv: mesh::Receiver<()>,
Expand All @@ -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.
Expand All @@ -194,6 +202,8 @@ impl UefiDevice {
address: 0,
gm,
watchdog_recv,
mor_bit_status: true, // initialized to success, matching legacy behavior
mor_config,
Comment thread
mebersol marked this conversation as resolved.
service: UefiDeviceServices {
nvram: service::nvram::NvramServices::new(
nvram_storage,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -285,10 +299,45 @@ impl UefiDevice {
None,
);
}
UefiCommand::MOR_SET_VARIABLE => block_on(self.handle_mor_set_variable(data as u8)),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can't be a block_on. If it really needs to be async then this device will have to do the whole PollDevice dance

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namely store a future and poll it inside poll_device

_ => 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"
);
}
}
}
Comment on lines +307 to +339

/// Extra inspection fields for the UEFI device.
fn inspect_extra(&mut self, resp: &mut inspect::Response<'_>) {
const USAGE: &str =
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -568,6 +618,8 @@ mod save_restore {
pub time: <TimeServices as SaveRestore>::SavedState,
#[mesh(7)]
pub diagnostics: <DiagnosticsServices as SaveRestore>::SavedState,
#[mesh(8)]
pub mor_bit_status: bool,
}
}

Expand All @@ -580,6 +632,8 @@ mod save_restore {
command_set: _,
gm: _,
watchdog_recv: _,
mor_bit_status,
mor_config: _,
Comment on lines 633 to +636
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mor_bit_status affects guest-visible behavior for MOR_SET_VARIABLE reads, but it is not included in the saved state (it’s ignored during save() and never restored). Consider adding it to SavedState and restoring it so snapshots/migration preserve the MOR status semantics. #Closed

Copilot uses AI. Check for mistakes.
service:
UefiDeviceServices {
nvram,
Expand All @@ -601,6 +655,7 @@ mod save_restore {
generation_id: generation_id.save()?,
time: time.save()?,
diagnostics: diagnostics.save()?,
mor_bit_status: *mor_bit_status,
})
}

Expand All @@ -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)?;
Expand Down
15 changes: 15 additions & 0 deletions vm/devices/firmware/firmware_uefi/src/platform/nvram.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Comment on lines +20 to +33
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds a new guest-visible firmware feature (MOR / memory-overwrite-request support) and a new platform callback (MorConfig). The Guide has a reference page for mu_msvm UEFI + the firmware_uefi device (Guide/src/reference/devices/firmware/mu_msvm_uefi.md), but it doesn't mention MOR. Consider updating that page (or adding a short note) so users know MOR is supported and what behavior to expect on reset.

Copilot uses AI. Check for mistakes.
12 changes: 12 additions & 0 deletions vm/devices/firmware/firmware_uefi/src/service/nvram/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<NvramError>)> {
self.services
.set_variable_ucs2(vendor, name, attr, vec![value])
Comment on lines +107 to +110
.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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +1978 to +1980

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;
Comment on lines +1992 to +1994

assert_eq!(status, EfiStatus::SUCCESS);
assert_eq!(data, Some(vec![0x00]));
}
}
13 changes: 13 additions & 0 deletions vm/devices/firmware/uefi_specs/src/uefi/nvram.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
);
}
4 changes: 4 additions & 0 deletions vmm_core/vmotherboard/src/base_chipset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@ impl<'a> BaseChipsetBuilder<'a> {
watchdog_recv,
vsm_config,
time_source,
mor_config,
}) = deps_hyperv_firmware_uefi
{
builder
Expand Down Expand Up @@ -585,6 +586,7 @@ impl<'a> BaseChipsetBuilder<'a> {
},
vsm_config,
time_source,
mor_config,
};

firmware_uefi::UefiDevice::new(runtime_deps, config, foundation.is_restoring)
Expand Down Expand Up @@ -1387,6 +1389,8 @@ pub mod options {
pub vsm_config: Option<Box<dyn firmware_uefi::platform::nvram::VsmConfig>>,
/// Time source
pub time_source: Box<dyn InspectableLocalClock>,
/// Interface for MOR (Memory Overwrite Request) bit notifications.
pub mor_config: Option<Box<dyn firmware_uefi::platform::nvram::MorConfig>>,
}

/// Hyper-V specific framebuffer device
Expand Down
Loading