diff --git a/Cargo.lock b/Cargo.lock index 427cc83813..495bcd3840 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3058,6 +3058,7 @@ dependencies = [ "memory_range", "nix 0.30.1", "open_enum", + "openhcl_cpuid_features", "pal", "parking_lot", "safe_intrinsics", @@ -5095,6 +5096,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "openhcl_cpuid_features" +version = "0.0.0" +dependencies = [ + "hvdef", + "safe_intrinsics", + "x86defs", +] + [[package]] name = "openhcl_dma_manager" version = "0.0.0" @@ -8388,13 +8398,13 @@ dependencies = [ "nvme_resources", "nvme_spec", "openhcl_attestation_protocol", + "openhcl_cpuid_features", "openhcl_dma_manager", "pal", "pal_async", "pal_uring", "parking_lot", "profiler_worker", - "safe_intrinsics", "scsi_buffers", "scsi_core", "scsidisk", @@ -8461,7 +8471,6 @@ dependencies = [ "vpci_relay", "watchdog_core", "watchdog_vmgs_format", - "x86defs", "zerocopy", ] @@ -9019,6 +9028,7 @@ dependencies = [ "memory_range", "mesh", "minircu", + "openhcl_cpuid_features", "pal", "pal_async", "pal_uring", diff --git a/Cargo.toml b/Cargo.toml index 2fc5b515c9..dbafc0ff01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -195,6 +195,7 @@ minimal_rt_build = { path = "openhcl/minimal_rt_build" } minimal_rt_reloc = { path = "openhcl/minimal_rt_reloc" } mem_profile_tracing = { path = "openhcl/mem_profile_tracing" } openhcl_dma_manager = { path = "openhcl/openhcl_dma_manager" } +openhcl_cpuid_features = { path = "openhcl/openhcl_cpuid_features" } sidecar_client = { path = "openhcl/sidecar_client" } sidecar_defs = { path = "openhcl/sidecar_defs" } tee_call = { path = "openhcl/tee_call" } diff --git a/openhcl/hcl/Cargo.toml b/openhcl/hcl/Cargo.toml index 83c09df290..188779816e 100644 --- a/openhcl/hcl/Cargo.toml +++ b/openhcl/hcl/Cargo.toml @@ -12,6 +12,7 @@ hv1_structs.workspace = true hvdef.workspace = true pal.workspace = true memory_range.workspace = true +openhcl_cpuid_features.workspace = true sidecar_client.workspace = true tdcall = { workspace = true, features = ["tracing"] } x86defs.workspace = true diff --git a/openhcl/hcl/src/ioctl.rs b/openhcl/hcl/src/ioctl.rs index 8e56b9269c..26bd9d4667 100755 --- a/openhcl/hcl/src/ioctl.rs +++ b/openhcl/hcl/src/ioctl.rs @@ -1346,6 +1346,7 @@ pub struct Hcl { isolation: IsolationType, snp_register_bitmap: [u8; 64], sidecar: Option, + cpuid_features: openhcl_cpuid_features::CpuidFeatures, } /// The isolation type for a partition. @@ -1383,6 +1384,11 @@ impl Hcl { pub fn supports_lower_vtl_timer_virt(&self) -> bool { self.supports_lower_vtl_timer_virt } + + /// Returns cached host CPUID feature information. + pub fn cpuid_features(&self) -> &openhcl_cpuid_features::CpuidFeatures { + &self.cpuid_features + } } #[derive(Debug)] @@ -1727,7 +1733,12 @@ impl<'a, T: Backing<'a>> ProcessorRunner<'a, T> { impl Hcl { /// Returns a new HCL instance. - pub fn new(isolation: IsolationType, sidecar: Option) -> Result { + #[cfg(guest_arch = "x86_64")] // xtask-fmt allow-target-arch cpu-intrinsic + pub fn new( + isolation: IsolationType, + sidecar: Option, + cpuid_features: openhcl_cpuid_features::CpuidFeatures, + ) -> Result { static SIGNAL_HANDLER_INIT: Once = Once::new(); // SAFETY: The signal handler does not perform any actions that are forbidden // for signal handlers to perform, as it performs nothing. @@ -1784,10 +1795,16 @@ impl Hcl { let dr6_shared = mshv_fd.check_extension(HCL_CAP_DR6_SHARED)?; let supports_lower_vtl_timer_virt = mshv_fd.check_extension(HCL_CAP_LOWER_VTL_TIMER_VIRT)?; + + // Use CPUID features from caller + let supports_lower_vtl_snp_guest_request = + cpuid_features.supports_lower_vtl_guest_request(); + tracing::debug!( supports_vtl_ret_action, supports_register_page, supports_lower_vtl_timer_virt, + supports_lower_vtl_snp_guest_request, "HCL capabilities", ); @@ -1815,6 +1832,7 @@ impl Hcl { isolation, snp_register_bitmap, sidecar, + cpuid_features, }) } diff --git a/openhcl/openhcl_cpuid_features/Cargo.toml b/openhcl/openhcl_cpuid_features/Cargo.toml new file mode 100644 index 0000000000..5a38be119a --- /dev/null +++ b/openhcl/openhcl_cpuid_features/Cargo.toml @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[package] +name = "openhcl_cpuid_features" +edition.workspace = true +rust-version.workspace = true + +[target.'cfg(target_arch = "x86_64")'.dependencies] +hvdef.workspace = true +x86defs.workspace = true +safe_intrinsics.workspace = true + +[lints] +workspace = true diff --git a/openhcl/openhcl_cpuid_features/src/lib.rs b/openhcl/openhcl_cpuid_features/src/lib.rs new file mode 100644 index 0000000000..998351de0c --- /dev/null +++ b/openhcl/openhcl_cpuid_features/src/lib.rs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Shared CPUID feature detection for OpenHCL components. + +// xtask-fmt allow-target-arch cpu-intrinsic +#![cfg(target_arch = "x86_64")] + +use hvdef::HvEnlightenmentInformation; +use x86defs::cpuid::VersionAndFeaturesEcx; + +/// Cached CPUID feature detection results. +#[derive(Clone, Copy, Debug)] +pub struct CpuidFeatures { + enlightenment_info: HvEnlightenmentInformation, + version_features: VersionAndFeaturesEcx, +} + +impl CpuidFeatures { + /// Queries host CPUID and builds a snapshot of relevant feature leaves. + pub fn new() -> Self { + let result = + safe_intrinsics::cpuid(hvdef::HV_CPUID_FUNCTION_MS_HV_ENLIGHTENMENT_INFORMATION, 0); + let enlightenment_info = HvEnlightenmentInformation::from_cpuid([ + result.eax, result.ebx, result.ecx, result.edx, + ]); + + let result = safe_intrinsics::cpuid(x86defs::cpuid::CpuidFunction::VersionAndFeatures.0, 0); + let version_features = VersionAndFeaturesEcx::from(result.ecx); + + Self { + enlightenment_info, + version_features, + } + } + + /// Returns whether Hyper-V recommends MMIO access hypercalls. + pub fn use_hypercall_for_mmio_access(&self) -> bool { + self.enlightenment_info.use_hypercall_for_mmio_access() + } + + /// Returns whether x2APIC is supported by the host CPU. + pub fn x2_apic_supported(&self) -> bool { + self.version_features.x2_apic() + } + + /// Returns whether lower VTL guest request interception is supported. + pub fn supports_lower_vtl_guest_request(&self) -> bool { + self.enlightenment_info.lower_vtl_guest_request_support() + } + + /// Returns whether restore partition time on resume is supported. + pub fn supports_restore_partition_time(&self) -> bool { + self.enlightenment_info.restore_time_on_resume() + } +} + +impl Default for CpuidFeatures { + fn default() -> Self { + Self::new() + } +} diff --git a/openhcl/underhill_core/Cargo.toml b/openhcl/underhill_core/Cargo.toml index bfb96bd36f..a41d2c2e49 100644 --- a/openhcl/underhill_core/Cargo.toml +++ b/openhcl/underhill_core/Cargo.toml @@ -78,6 +78,7 @@ netvsp.workspace = true nvme_driver.workspace = true nvme_resources.workspace = true openhcl_dma_manager.workspace = true +openhcl_cpuid_features.workspace = true scsi_core.workspace = true scsidisk.workspace = true scsidisk_resources.workspace = true @@ -133,9 +134,7 @@ vmgs = { workspace = true, features = ["encryption", "save_restore"] } vmgs_broker = { workspace = true, features = ["encryption"] } vmgs_format.workspace = true vmgs_resources.workspace = true -x86defs.workspace = true -safe_intrinsics.workspace = true debug_ptr.workspace = true guid.workspace = true crypto = { workspace = true, features = ["vendored", "openssl"] } diff --git a/openhcl/underhill_core/src/worker.rs b/openhcl/underhill_core/src/worker.rs index df511c89e5..6d27761359 100644 --- a/openhcl/underhill_core/src/worker.rs +++ b/openhcl/underhill_core/src/worker.rs @@ -1591,21 +1591,15 @@ async fn new_underhill_vm( let uevent_listener = Arc::new(UeventListener::new(tp.driver(0)).context("failed to start uevent listener")?); - let use_mmio_hypercalls = dps.general.always_relay_host_mmio; - // TODO: Centralize cpuid based feature determination. + // Initialize CPUID features once at worker startup. + #[cfg(guest_arch = "x86_64")] // xtask-fmt allow-target-arch cpu-intrinsic + let cpuid_features = openhcl_cpuid_features::CpuidFeatures::new(); + #[cfg(guest_arch = "x86_64")] - let use_mmio_hypercalls = use_mmio_hypercalls - || hardware_isolated && { - let result = - safe_intrinsics::cpuid(hvdef::HV_CPUID_FUNCTION_MS_HV_ENLIGHTENMENT_INFORMATION, 0); - hvdef::HvEnlightenmentInformation::from( - result.eax as u128 - | (result.ebx as u128) << 32 - | (result.ecx as u128) << 64 - | (result.edx as u128) << 96, - ) - .use_hypercall_for_mmio_access() - }; + let use_mmio_hypercalls = dps.general.always_relay_host_mmio + || (hardware_isolated && cpuid_features.use_hypercall_for_mmio_access()); + #[cfg(not(guest_arch = "x86_64"))] + let use_mmio_hypercalls = dps.general.always_relay_host_mmio; let boot_info = runtime_params.parsed_openhcl_boot(); @@ -1713,21 +1707,14 @@ async fn new_underhill_vm( // boot rather than allowing the host to make that decision. This would // just require Underhill setting the apicbase register on the VPs // before start. - // - // TODO: centralize cpuid querying logic. #[cfg(guest_arch = "x86_64")] let x2apic = if isolation.is_hardware_isolated() && !hide_isolation { // For hardware CVMs, always enable x2apic support at boot. vm_topology::processor::x86::X2ApicState::Enabled + } else if cpuid_features.x2_apic_supported() { + vm_topology::processor::x86::X2ApicState::Supported } else { - let features = x86defs::cpuid::VersionAndFeaturesEcx::from( - safe_intrinsics::cpuid(x86defs::cpuid::CpuidFunction::VersionAndFeatures.0, 0).ecx, - ); - if features.x2_apic() { - vm_topology::processor::x86::X2ApicState::Supported - } else { - vm_topology::processor::x86::X2ApicState::Unsupported - } + vm_topology::processor::x86::X2ApicState::Unsupported }; #[cfg(guest_arch = "x86_64")] @@ -1871,6 +1858,8 @@ async fn new_underhill_vm( hide_isolation, disable_proxy_redirect: env_cfg.disable_proxy_redirect, disable_lower_vtl_timer_virt: env_cfg.disable_lower_vtl_timer_virt, + #[cfg(guest_arch = "x86_64")] // xtask-fmt allow-target-arch cpu-intrinsic + cpuid_features, }; let proto_partition = UhProtoPartition::new(params, |cpu| tp.driver(cpu).clone()) diff --git a/openhcl/virt_mshv_vtl/Cargo.toml b/openhcl/virt_mshv_vtl/Cargo.toml index 0be8fcf3af..b44c92c7a2 100644 --- a/openhcl/virt_mshv_vtl/Cargo.toml +++ b/openhcl/virt_mshv_vtl/Cargo.toml @@ -11,6 +11,7 @@ gdb = [] [target.'cfg(target_os = "linux")'.dependencies] hcl.workspace = true +openhcl_cpuid_features.workspace = true virt.workspace = true virt_support_apic.workspace = true virt_support_x86emu.workspace = true diff --git a/openhcl/virt_mshv_vtl/src/lib.rs b/openhcl/virt_mshv_vtl/src/lib.rs index 60ffabb807..e1144bbfe0 100755 --- a/openhcl/virt_mshv_vtl/src/lib.rs +++ b/openhcl/virt_mshv_vtl/src/lib.rs @@ -1440,6 +1440,9 @@ pub struct UhPartitionNewParams<'a> { pub disable_proxy_redirect: bool, /// Disable lower VTL timer virtualization. pub disable_lower_vtl_timer_virt: bool, + /// Cached CPUID features queried once at worker startup. + #[cfg(guest_arch = "x86_64")] // xtask-fmt allow-target-arch cpu-intrinsic + pub cpuid_features: openhcl_cpuid_features::CpuidFeatures, } /// Parameters to [`UhProtoPartition::build`]. @@ -1610,6 +1613,9 @@ impl<'a> UhProtoPartition<'a> { // Try to open the sidecar device, if it is present. let sidecar = sidecar_client::SidecarClient::new(driver).map_err(Error::Sidecar)?; + #[cfg(guest_arch = "x86_64")] // xtask-fmt allow-target-arch cpu-intrinsic + let hcl = Hcl::new(hcl_isolation, sidecar, params.cpuid_features).map_err(Error::Hcl)?; + #[cfg(not(guest_arch = "x86_64"))] // xtask-fmt allow-target-arch cpu-intrinsic let hcl = Hcl::new(hcl_isolation, sidecar).map_err(Error::Hcl)?; // Set the hypercalls that this process will use. diff --git a/openhcl/virt_mshv_vtl/src/processor/snp/mod.rs b/openhcl/virt_mshv_vtl/src/processor/snp/mod.rs index 58c2ea54a3..35ae89eb94 100755 --- a/openhcl/virt_mshv_vtl/src/processor/snp/mod.rs +++ b/openhcl/virt_mshv_vtl/src/processor/snp/mod.rs @@ -73,6 +73,9 @@ use vmcore::vmtime::VmTimeAccess; use x86defs::RFlags; use x86defs::apic::X2APIC_MSR_BASE; use x86defs::cpuid::CpuidFunction; +use x86defs::snp::SNP_NUM_VMPCKS; +use x86defs::snp::SNP_SECRETS_VMPCK0_OFFSET; +use x86defs::snp::SNP_VMPCK_KEY_SIZE; use x86defs::snp::SevAvicIncompleteIpiInfo1; use x86defs::snp::SevAvicIncompleteIpiInfo2; use x86defs::snp::SevAvicNoAccelInfo; @@ -427,11 +430,15 @@ pub struct SnpBackedShared { #[inspect(skip)] guest_timer: hardware_cvm::VmTimeGuestTimer, secure_avic: bool, + /// VMPCK keys extracted from the SNP secrets page at partition initialization. + /// Indexed by VMPCK index (0-3), where each key is [`SNP_VMPCK_KEY_SIZE`] bytes. + #[inspect(skip)] + vmpck_keys: [[u8; SNP_VMPCK_KEY_SIZE]; SNP_NUM_VMPCKS], } impl SnpBackedShared { pub(crate) fn new( - _partition_params: &UhPartitionNewParams<'_>, + partition_params: &UhPartitionNewParams<'_>, params: BackingSharedParams<'_>, ) -> Result { let cvm = params.cvm_state.unwrap(); @@ -460,6 +467,18 @@ impl SnpBackedShared { // Configure timer interface for lower VTLs. let guest_timer = hardware_cvm::VmTimeGuestTimer; + // Extract the four VMPCK keys from the SNP secrets page. + let vmpck_keys = if let Some(secrets) = partition_params.snp_secrets { + let mut keys = [[0u8; SNP_VMPCK_KEY_SIZE]; SNP_NUM_VMPCKS]; + for (i, key) in keys.iter_mut().enumerate() { + let offset = SNP_SECRETS_VMPCK0_OFFSET + i * SNP_VMPCK_KEY_SIZE; + key.copy_from_slice(&secrets[offset..offset + SNP_VMPCK_KEY_SIZE]); + } + keys + } else { + [[0u8; SNP_VMPCK_KEY_SIZE]; SNP_NUM_VMPCKS] + }; + Ok(Self { sev_status, invlpgb_count_max, @@ -467,6 +486,7 @@ impl SnpBackedShared { secure_avic, cvm, guest_timer, + vmpck_keys, }) } } @@ -954,6 +974,7 @@ impl UhHypercallHandler<'_, '_, SnpBacked> { hv1_hypercall::HvSendSyntheticClusterIpiEx, hv1_hypercall::HvInstallIntercept, hv1_hypercall::HvAssertVirtualInterrupt, + hv1_hypercall::HvGetSnpVmpck, ], ); @@ -3123,6 +3144,29 @@ impl TlbFlushLockAccess for SnpTlbLockFlushAccess<'_> { } } +impl hv1_hypercall::GetSnpVmpck for UhHypercallHandler<'_, '_, SnpBacked> { + fn get_snp_vmpck(&mut self) -> hvdef::HvResult { + if !self + .vp + .partition + .hcl + .cpuid_features() + .supports_lower_vtl_guest_request() + { + return Err(HvError::AccessDenied); + } + + // The VMPCK index corresponds to the VMPL of the calling VTL: + // VTL0 runs at VMPL2, VTL1 runs at VMPL1. + let index = match self.intercepted_vtl { + GuestVtl::Vtl0 => 2, + GuestVtl::Vtl1 => 1, + }; + let vmpck_key = self.vp.shared.vmpck_keys[index]; + Ok(hvdef::hypercall::GetSnpVmpckOutput { vmpck_key }) + } +} + mod save_restore { use super::SnpBacked; use super::UhProcessor; diff --git a/vm/hv1/hv1_hypercall/src/imp.rs b/vm/hv1/hv1_hypercall/src/imp.rs index 7a8a61c001..3701e1cdfb 100644 --- a/vm/hv1/hv1_hypercall/src/imp.rs +++ b/vm/hv1/hv1_hypercall/src/imp.rs @@ -389,6 +389,22 @@ impl HypercallDispatch; + +/// Implements the `HvGetSnpVmpck` hypercall. +pub trait GetSnpVmpck { + /// Returns the VMPCK (VM Platform Communication Key) for the calling VTL. + fn get_snp_vmpck(&mut self) -> HvResult; +} + +impl HypercallDispatch for T { + fn dispatch(&mut self, params: HypercallParameters<'_>) -> HypercallOutput { + HvGetSnpVmpck::run(params, |()| self.get_snp_vmpck()) + } +} + /// Defines the `HvGetVpRegisters` hypercall. pub type HvGetVpRegisters = RepHypercall< defs::GetSetVpRegisters, diff --git a/vm/hv1/hvdef/src/lib.rs b/vm/hv1/hvdef/src/lib.rs index 8e3f1ab21b..543f164614 100755 --- a/vm/hv1/hvdef/src/lib.rs +++ b/vm/hv1/hvdef/src/lib.rs @@ -621,13 +621,20 @@ pub struct HvEnlightenmentInformation { pub use_hypercall_for_mmio_access: bool, pub use_gpa_pinning_hypercall: bool, pub wake_vps: bool, - _reserved: u8, + pub proxy_interrupt_doorbell_support: bool, + pub memory_type_locking_support: bool, + pub map_partition_event_log_buffer: bool, + pub lower_vtl_guest_request_support: bool, + pub heat_hint_beneficial_support: bool, + pub ring_buffer_message_port_support: bool, + _reserved1: bool, + _reserved2: bool, pub long_spin_wait_count: u32, #[bits(7)] pub implemented_physical_address_bits: u32, #[bits(25)] - _reserved1: u32, - _reserved2: u32, + _reserved3: u32, + _reserved4: u32, } impl HvEnlightenmentInformation { @@ -745,6 +752,7 @@ open_enum! { HvCallPinGpaPageRanges = 0x0112, HvCallUnpinGpaPageRanges = 0x0113, HvCallQuerySparseGpaPageHostVisibility = 0x011C, + HvCallGetSnpVmpck = 0x0134, // Extended hypercalls. HvExtCallQueryCapabilities = 0x8001, @@ -2487,6 +2495,15 @@ pub mod hypercall { pub reference_time_in_100_ns: u64, pub tsc: u64, } + + /// The size of a VMPCK key in bytes. + pub const SNP_VMPCK_KEY_SIZE: usize = 0x20; + + #[repr(C)] + #[derive(Copy, Clone, IntoBytes, Immutable, KnownLayout, FromBytes)] + pub struct GetSnpVmpckOutput { + pub vmpck_key: [u8; SNP_VMPCK_KEY_SIZE], + } } macro_rules! registers { diff --git a/vm/x86/x86defs/src/snp.rs b/vm/x86/x86defs/src/snp.rs index f389541dfb..f91f06c2ba 100644 --- a/vm/x86/x86defs/src/snp.rs +++ b/vm/x86/x86defs/src/snp.rs @@ -21,6 +21,15 @@ pub const SEV_INTR_TYPE_SW: u32 = 4; pub const REG_TWEAK_BITMAP_OFFSET: usize = 0x100; pub const REG_TWEAK_BITMAP_SIZE: usize = 0x40; +// VMPCK key offsets within the SNP secrets page. +// See AMD SEV-SNP Firmware ABI Specification, Table 11 (Guest-Secrets Page Layout). +pub const SNP_NUM_VMPCKS: usize = 4; +pub const SNP_SECRETS_VMPCK0_OFFSET: usize = 0x20; +pub const SNP_SECRETS_VMPCK1_OFFSET: usize = 0x40; +pub const SNP_SECRETS_VMPCK2_OFFSET: usize = 0x60; +pub const SNP_SECRETS_VMPCK3_OFFSET: usize = 0x80; +pub const SNP_VMPCK_KEY_SIZE: usize = 0x20; + /// Value for the `msg_version` member in [`SNP_GUEST_REQ_MSG_VERSION`]. /// Use 1 for now. pub const SNP_GUEST_REQ_MSG_VERSION: u32 = 1;