Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions openhcl/hcl/src/vmsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ impl<'a, T> VmsaWrapper<'a, T> {

/// Wraps a SEV VMSA structure with the register tweak bitmap to provide safe access methods.
impl<T: Deref<Target = SevVmsa>> VmsaWrapper<'_, T> {
/// Returns the raw VMSA bytes.
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

as_bytes() exposes the complete raw VMSA image (including the register protection nonce and XOR-masked protected fields). Given how sensitive this data is, the doc comment should explicitly call out that callers must treat the returned bytes as highly confidential and should not copy it into host-visible/shared memory unless explicitly intended.

If possible, also consider whether this needs to be a public API vs. restricted to the crates that implement low-level diagnostics.

Suggested change
/// Returns the raw VMSA bytes.
/// Returns the complete raw VMSA image as bytes.
///
/// This includes highly sensitive register-protection state, including the
/// register protection nonce and XOR-masked protected fields. Callers must
/// treat the returned bytes as highly confidential and must not copy them
/// into host-visible or shared memory unless that exposure is explicitly
/// intended (for example, for tightly controlled low-level diagnostics).

Copilot uses AI. Check for mistakes.
pub fn as_bytes(&self) -> &[u8] {
self.vmsa.as_bytes()
}

/// 64 bit register read
fn get_u64(&self, offset: usize) -> u64 {
assert!(offset.is_multiple_of(8));
Expand Down
2 changes: 2 additions & 0 deletions openhcl/virt_mshv_vtl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ pub enum Error {
InvalidDebugConfiguration,
#[error("failed to allocate TLB flush page")]
AllocateTlbFlushPage(#[source] anyhow::Error),
#[error("failed to allocate crash VMSA page")]
AllocateCrashVmsaPage(#[source] anyhow::Error),
#[error("host does not support required cpu capabilities")]
Capabilities(virt::PartitionCapabilitiesError),
#[error("failed to get register")]
Expand Down
66 changes: 66 additions & 0 deletions openhcl/virt_mshv_vtl/src/processor/snp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use crate::devmsr;
use crate::processor::UhHypercallHandler;
use crate::processor::UhProcessor;
use crate::processor::hardware_cvm::apic::ApicBacking;
use core::mem::size_of;
use cvm_tracing::CVM_ALLOWED;
use cvm_tracing::CVM_CONFIDENTIAL;
use hcl::protocol::hcl_intr_offload_flags;
Expand All @@ -51,6 +52,7 @@ use hvdef::hypercall::HypercallOutput;
use inspect::Inspect;
use inspect::InspectMut;
use inspect_counters::Counter;
use user_driver::memory::MemoryBlock;
use virt::EmulatorMonitorSupport;
use virt::Processor;
use virt::VpHaltReason;
Expand Down Expand Up @@ -115,6 +117,10 @@ pub struct SnpBacked {
hv_sint_notifications: u16,
general_stats: VtlArray<GeneralStats, 2>,
exit_stats: VtlArray<ExitStats, 2>,
#[inspect(skip)]
crash_vmsa_page: MemoryBlock,
#[inspect(hex)]
crash_vmsa_page_pa: u64,
#[inspect(flatten)]
cvm: UhCvmVpState,
}
Expand Down Expand Up @@ -174,6 +180,55 @@ impl SnpBacked {
pub fn shared_pages_required_per_cpu() -> u64 {
UhDirectOverlay::Count as u64
}

fn update_vmsa_crash_page(this: &mut UhProcessor<'_, Self>, vtl: GuestVtl) {
let crash_page_pa = this.backing.crash_vmsa_page_pa;
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

This code assumes SevVmsa fits in the single page allocated for crash_vmsa_page. There’s no local assert/clamp to HV_PAGE_SIZE, so a future layout change could make vmsa_size exceed the page and turn this into a partial write/failure.

Add an explicit assert!(size_of::<SevVmsa>() <= HV_PAGE_SIZE as usize) (or clamp the copy length and published size) to make the assumption self-checking.

Suggested change
let crash_page_pa = this.backing.crash_vmsa_page_pa;
let crash_page_pa = this.backing.crash_vmsa_page_pa;
assert!(size_of::<SevVmsa>() <= HV_PAGE_SIZE as usize);

Copilot uses AI. Check for mistakes.
let vmsa_size = size_of::<SevVmsa>() as u64;

// Keep the crash-page register pair pointing to a host-visible page
// so a hypervisor debugger can find the latest VMSA snapshot.
this.crash_reg[3] = crash_page_pa;
this.crash_reg[4] = vmsa_size;

for target_vtl in [GuestVtl::Vtl0, GuestVtl::Vtl1] {
if let Err(err) = this.runner.set_vp_register(
target_vtl,
HvX64RegisterName::GuestCrashP3,
crash_page_pa.into(),
) {
tracelimit::warn_ratelimited!(
CVM_ALLOWED,
?err,
?target_vtl,
"failed to publish crash page address"
);
}

if let Err(err) = this.runner.set_vp_register(
target_vtl,
HvX64RegisterName::GuestCrashP4,
vmsa_size.into(),
) {
tracelimit::warn_ratelimited!(
CVM_ALLOWED,
?err,
?target_vtl,
"failed to publish crash page size"
);
}
}

Comment on lines +186 to +220
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

HV_X64_MSR_GUEST_CRASH_P3/P4 are defined as the crash-message GPA/length (see hvdef::GuestCrashCtl docs), and the VTL crash notification path in processor/mod.rs reads crash_reg[3]/[4] to fetch that message. Overwriting this.crash_reg[3]/[4] here will clobber guest-provided crash message parameters and can cause crash notifications to read from the wrong GPA.

Consider keeping the guest crash registers exclusively guest-owned (only updated on guest MSR writes), and publishing the VMSA snapshot pointer via a separate mechanism/state that doesn’t reuse the crash-message registers.

Suggested change
let vmsa_size = size_of::<SevVmsa>() as u64;
// Keep the crash-page register pair pointing to a host-visible page
// so a hypervisor debugger can find the latest VMSA snapshot.
this.crash_reg[3] = crash_page_pa;
this.crash_reg[4] = vmsa_size;
for target_vtl in [GuestVtl::Vtl0, GuestVtl::Vtl1] {
if let Err(err) = this.runner.set_vp_register(
target_vtl,
HvX64RegisterName::GuestCrashP3,
crash_page_pa.into(),
) {
tracelimit::warn_ratelimited!(
CVM_ALLOWED,
?err,
?target_vtl,
"failed to publish crash page address"
);
}
if let Err(err) = this.runner.set_vp_register(
target_vtl,
HvX64RegisterName::GuestCrashP4,
vmsa_size.into(),
) {
tracelimit::warn_ratelimited!(
CVM_ALLOWED,
?err,
?target_vtl,
"failed to publish crash page size"
);
}
}
// Do not reuse the guest crash-message registers (P3/P4) to publish
// the VMSA snapshot location. Those registers are guest-owned and are
// consumed by the crash notification path as the crash-message GPA and
// length.

Copilot uses AI. Check for mistakes.
let vmsa = this.runner.vmsa(vtl);
let src = vmsa.as_bytes();
if let Err(err) = this.shared.cvm.shared_memory.write_at(crash_page_pa, src) {
tracelimit::warn_ratelimited!(
CVM_ALLOWED,
?err,
crash_page_pa,
"failed to write crash VMSA page"
);
Comment on lines +188 to +229
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

This copies the full VMSA into a shared/host-visible page (“so a hypervisor debugger can find…”). On SNP, that effectively exports guest register state (and the register protection nonce) to the untrusted host/hypervisor, which is a major confidentiality risk.

This should be gated behind an explicit debug/diagnostics opt-in (and ideally compiled/available only in debug builds), or restricted to a crash-only path where the guest has already decided to disclose crash details.

Suggested change
// Keep the crash-page register pair pointing to a host-visible page
// so a hypervisor debugger can find the latest VMSA snapshot.
this.crash_reg[3] = crash_page_pa;
this.crash_reg[4] = vmsa_size;
for target_vtl in [GuestVtl::Vtl0, GuestVtl::Vtl1] {
if let Err(err) = this.runner.set_vp_register(
target_vtl,
HvX64RegisterName::GuestCrashP3,
crash_page_pa.into(),
) {
tracelimit::warn_ratelimited!(
CVM_ALLOWED,
?err,
?target_vtl,
"failed to publish crash page address"
);
}
if let Err(err) = this.runner.set_vp_register(
target_vtl,
HvX64RegisterName::GuestCrashP4,
vmsa_size.into(),
) {
tracelimit::warn_ratelimited!(
CVM_ALLOWED,
?err,
?target_vtl,
"failed to publish crash page size"
);
}
}
let vmsa = this.runner.vmsa(vtl);
let src = vmsa.as_bytes();
if let Err(err) = this.shared.cvm.shared_memory.write_at(crash_page_pa, src) {
tracelimit::warn_ratelimited!(
CVM_ALLOWED,
?err,
crash_page_pa,
"failed to write crash VMSA page"
);
if cfg!(debug_assertions) {
// Only publish a host-visible VMSA snapshot in debug builds.
// In production SNP guests this would disclose sensitive guest
// register state to the untrusted host/hypervisor.
this.crash_reg[3] = crash_page_pa;
this.crash_reg[4] = vmsa_size;
for target_vtl in [GuestVtl::Vtl0, GuestVtl::Vtl1] {
if let Err(err) = this.runner.set_vp_register(
target_vtl,
HvX64RegisterName::GuestCrashP3,
crash_page_pa.into(),
) {
tracelimit::warn_ratelimited!(
CVM_ALLOWED,
?err,
?target_vtl,
"failed to publish crash page address"
);
}
if let Err(err) = this.runner.set_vp_register(
target_vtl,
HvX64RegisterName::GuestCrashP4,
vmsa_size.into(),
) {
tracelimit::warn_ratelimited!(
CVM_ALLOWED,
?err,
?target_vtl,
"failed to publish crash page size"
);
}
}
let vmsa = this.runner.vmsa(vtl);
let src = vmsa.as_bytes();
if let Err(err) = this.shared.cvm.shared_memory.write_at(crash_page_pa, src) {
tracelimit::warn_ratelimited!(
CVM_ALLOWED,
?err,
crash_page_pa,
"failed to write crash VMSA page"
);
}
} else {
// Do not expose a host-visible VMSA page in non-debug builds.
this.crash_reg[3] = 0;
this.crash_reg[4] = 0;
for target_vtl in [GuestVtl::Vtl0, GuestVtl::Vtl1] {
if let Err(err) = this.runner.set_vp_register(
target_vtl,
HvX64RegisterName::GuestCrashP3,
0.into(),
) {
tracelimit::warn_ratelimited!(
CVM_ALLOWED,
?err,
?target_vtl,
"failed to clear crash page address"
);
}
if let Err(err) = this.runner.set_vp_register(
target_vtl,
HvX64RegisterName::GuestCrashP4,
0.into(),
) {
tracelimit::warn_ratelimited!(
CVM_ALLOWED,
?err,
?target_vtl,
"failed to clear crash page size"
);
}
}

Copilot uses AI. Check for mistakes.
}
}
}

impl HardwareIsolatedBacking for SnpBacked {
Expand Down Expand Up @@ -237,6 +292,7 @@ impl HardwareIsolatedBacking for SnpBacked {
}

this.backing.cvm_state_mut().exit_vtl = target_vtl;
Self::update_vmsa_crash_page(this, target_vtl);
}

fn translation_registers(
Expand Down Expand Up @@ -485,10 +541,19 @@ impl BackingPrivate for SnpBacked {
}

fn new(params: BackingParams<'_, '_, Self>, shared: &SnpBackedShared) -> Result<Self, Error> {
let crash_vmsa_page = shared
.cvm
.shared_dma_client
.allocate_dma_buffer(HV_PAGE_SIZE as usize)
.map_err(Error::AllocateCrashVmsaPage)?;
let crash_vmsa_page_pa = crash_vmsa_page.pfns()[0] << hvdef::HV_PAGE_SHIFT;

Ok(Self {
hv_sint_notifications: 0,
general_stats: VtlArray::from_fn(|_| Default::default()),
exit_stats: VtlArray::from_fn(|_| Default::default()),
crash_vmsa_page,
crash_vmsa_page_pa,
cvm: UhCvmVpState::new(
&shared.cvm,
params.partition,
Expand Down Expand Up @@ -1437,6 +1502,7 @@ impl UhProcessor<'_, SnpBacked> {
.map_err(|e| dev.fatal_error(SnpRunVpError(e).into()))?;

let entered_from_vtl = next_vtl;
SnpBacked::update_vmsa_crash_page(self, entered_from_vtl);
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

update_vmsa_crash_page() is called on every VP exit (right after run()), which means an extra ~page-sized copy per exit. On workloads with frequent exits this can become a noticeable overhead.

If this is intended primarily for post-mortem/debugging, consider updating the crash page only when a crash is detected (e.g., on guest crash MSR write), or at a lower frequency / behind a diagnostics flag.

Suggested change
SnpBacked::update_vmsa_crash_page(self, entered_from_vtl);
if has_intercept {
SnpBacked::update_vmsa_crash_page(self, entered_from_vtl);
}

Copilot uses AI. Check for mistakes.
let (avic_page, mut vmsa) = self.runner.secure_avic_page_vmsa_mut(entered_from_vtl);

// TODO SNP: The guest busy bit needs to be tested and set atomically.
Expand Down
Loading