diff --git a/.github/workflows/DailyArm64.yml b/.github/workflows/DailyArm64.yml index 25f657a69..e39bca67c 100644 --- a/.github/workflows/DailyArm64.yml +++ b/.github/workflows/DailyArm64.yml @@ -27,6 +27,8 @@ jobs: # Build aarch64 guest binaries once and upload them as artifacts for the # build-test and run-examples jobs to download. build-guests: + # Temporarily disabled while Arm runners are unavailable. + if: ${{ false }} strategy: fail-fast: false matrix: @@ -40,6 +42,8 @@ jobs: # Full aarch64 build-and-test. full_aarch64: "true" re-enables the Miri and # single-driver steps that are skipped on PRs. build-test: + # Temporarily disabled while Arm runners are unavailable. + if: ${{ false }} needs: build-guests permissions: # checkout in the called workflow @@ -60,6 +64,8 @@ jobs: full_aarch64: "true" run-examples: + # Temporarily disabled while Arm runners are unavailable. + if: ${{ false }} needs: build-guests strategy: fail-fast: false @@ -77,6 +83,8 @@ jobs: # limited arm64 runners, and runs here daily instead. The tracing # fuzzers (fuzz_guest_trace, fuzz_guest_estimate_trace_event) are x86_64-only fuzzing: + # Temporarily disabled while Arm runners are unavailable. + if: ${{ false }} needs: build-guests strategy: fail-fast: false diff --git a/.github/workflows/PrimeCaches.yml b/.github/workflows/PrimeCaches.yml index 5dd65c220..035f965da 100644 --- a/.github/workflows/PrimeCaches.yml +++ b/.github/workflows/PrimeCaches.yml @@ -33,7 +33,9 @@ jobs: strategy: fail-fast: false matrix: - arch: [X64, arm64] + # Temporarily disabled while Arm runners are unavailable. + arch: [X64] + # arch: [X64, arm64] config: [debug, release] uses: ./.github/workflows/dep_build_guests.yml secrets: inherit @@ -74,12 +76,13 @@ jobs: - arch: X64 hypervisor: hyperv-ws2025 config: release - - arch: arm64 - hypervisor: kvm - config: debug - - arch: arm64 - hypervisor: kvm - config: release + # Arm cache entries are retained for when Arm CI is re-enabled. + # - arch: arm64 + # hypervisor: kvm + # config: debug + # - arch: arm64 + # hypervisor: kvm + # config: release timeout-minutes: ${{ fromJSON(vars.PRIME_CACHES_JOB_TIMEOUT || '30') }} runs-on: ${{ fromJson(matrix.arch == 'X64' && format('["self-hosted", "{0}", "X64", "1ES.Pool=hld-{1}-amd", "JobId=prime-cache-{2}-{3}-{4}-{5}-{6}"]', diff --git a/.github/workflows/RegenSnapshotGoldens.yml b/.github/workflows/RegenSnapshotGoldens.yml index 5a2f69621..b17826c09 100644 --- a/.github/workflows/RegenSnapshotGoldens.yml +++ b/.github/workflows/RegenSnapshotGoldens.yml @@ -124,7 +124,9 @@ jobs: if: needs.check-published.outputs.needs_publish == 'true' strategy: matrix: - arch: [X64, arm64] + # Temporarily disabled while Arm runners are unavailable. + arch: [X64] + # arch: [X64, arm64] config: [debug, release] uses: ./.github/workflows/dep_build_guests.yml with: @@ -140,7 +142,9 @@ jobs: matrix: hypervisor: [kvm, mshv3, hyperv-ws2025] cpu: [amd, intel, apple] - arch: [X64, arm64] + # Temporarily disabled while Arm runners are unavailable. + arch: [X64] + # arch: [X64, arm64] config: [debug, release] exclude: # aarch64 covers Apple under KVM only. @@ -148,12 +152,13 @@ jobs: hypervisor: mshv3 - cpu: apple hypervisor: hyperv-ws2025 + # Arm exclusions are retained for when Arm CI is re-enabled. + # - cpu: amd + # arch: arm64 + # - cpu: intel + # arch: arm64 - cpu: apple arch: X64 - - cpu: amd - arch: arm64 - - cpu: intel - arch: arm64 runs-on: ${{ fromJson( format('["self-hosted", "{0}", "{1}"{2}]', matrix.hypervisor == 'hyperv-ws2025' && 'Windows' || 'Linux', diff --git a/.github/workflows/ValidatePullRequest.yml b/.github/workflows/ValidatePullRequest.yml index eee3f9717..fa4916633 100644 --- a/.github/workflows/ValidatePullRequest.yml +++ b/.github/workflows/ValidatePullRequest.yml @@ -57,7 +57,9 @@ jobs: strategy: fail-fast: true matrix: - arch: [X64, arm64] + # Temporarily disabled while Arm runners are unavailable. + arch: [X64] + # arch: [X64, arm64] config: [debug, release] uses: ./.github/workflows/dep_build_guests.yml secrets: inherit @@ -147,21 +149,24 @@ jobs: # pull goldens from GHCR in the called workflow packages: read strategy: - fail-fast: true + fail-fast: false # temp will remove matrix: + # Temporarily disabled while Arm runners are unavailable. hypervisor: ['hyperv-ws2025', mshv3, kvm] cpu_vendor: [amd, intel, apple] - arch: [X64, arm64] + arch: [X64] + # arch: [X64, arm64] config: [debug, release] exclude: - cpu_vendor: apple hypervisor: hyperv-ws2025 - cpu_vendor: apple hypervisor: mshv3 - - cpu_vendor: amd - arch: arm64 - - cpu_vendor: intel - arch: arm64 + # Arm exclusions are retained for when Arm CI is re-enabled. + # - cpu_vendor: amd + # arch: arm64 + # - cpu_vendor: intel + # arch: arm64 - cpu_vendor: apple arch: X64 uses: ./.github/workflows/dep_build_test.yml diff --git a/Justfile b/Justfile index 1eb4dd3a0..9e982f0e9 100644 --- a/Justfile +++ b/Justfile @@ -243,6 +243,8 @@ test-isolated target=default-target features="" : {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --test integration_test -- log_message --exact --ignored @# CPU vendor check, gated to known CI runner hardware {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- sandbox::snapshot::file::config::tests::cpu_vendor_current_is_recognized --exact --ignored + @# Slow host-dependent MSR audit. Run once per x86_64 CI profile. + {{ if features == "" { if hyperlight-target-arch == "x86_64" { cargo-cmd + " test --profile=" + (if target == "debug" { "dev" } else { target }) + " " + target-triple-flag + " -p hyperlight-host --lib -- sandbox::initialized_multi_use::tests::msr_tests::test_no_msr_leaks_across_restore_full_window_sweep --exact --ignored --nocapture" } else { "" } } else { "" } }} @# metrics tests {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F function_call_metrics," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- metrics::tests::test_metrics_are_emitted --exact diff --git a/docs/msr.md b/docs/msr.md new file mode 100644 index 000000000..adfd6c755 --- /dev/null +++ b/docs/msr.md @@ -0,0 +1,129 @@ +# MSR state across restore + +## Requirement + +A snapshot restore must remove all model-specific register (MSR) state written +after the snapshot. The guest must observe the MSR values saved with the +restored state. + +## Reset policy + +The reset set contains every MSR whose guest-written value can persist. Each +running snapshot stores values for this set. A snapshot created from a guest +binary has no saved MSR values, so restore uses the baseline captured when the +VM was created. + +`MSR_TABLE` classifies known MSRs: + +* `Stateful` entries hold values that restore must write. +* `WriteOnlyCommand` entries hold no state. Their classification gives allow + requests a clear error. + +The resolved reset set contains the backend core set, required MTRRs, and the +validated allow list. Hyperlight sorts and deduplicates the indices before +capturing the initialization baseline. + +The required invariant is: + +```text +guest-writable retained state => host-readable and host-writable state +``` + +Host-readable state need not be guest-writable. Extra reset entries are safe. +`EFER`, `APIC_BASE`, `FS_BASE`, and `GS_BASE` belong to the special-register +state. + +## Snapshot validation + +Snapshot MSR entries are untrusted. `validate_snapshot` checks their count, +indices, and order against the locally resolved reset set. It also replaces +their classifications with values from `MSR_TABLE`. + +A mismatch rejects the restore. A restore failure poisons the sandbox before +the guest can run. Equivalent allow lists produce the same sorted reset set, +regardless of insertion order. + +## Allow list + +`SandboxConfiguration::allow_msr` and `allow_msrs` add indices to the requested +allow list. They enforce capacity only. VM creation verifies that each index is +stateful and supported by the selected backend. + +KVM requires the index in `KVM_GET_MSR_INDEX_LIST` and a successful host read +and write. MSHV and WHP require a named-register mapping. + +At most 64 distinct MSRs may be requested. KVM also limits the resulting +contiguous filter groups to 16. + +## KVM + +KVM installs a deny filter over the full MSR space. Allowed indices form the +only guest `RDMSR` and `WRMSR` paths through that filter. A denied access exits +to Hyperlight, injects `#GP`, and poisons the sandbox. The denied write stores +no state. + +The KVM reset set contains the allow list plus `KERNEL_GS_BASE` and `TSC`. +`KERNEL_GS_BASE` is required because `WRGSBASE` followed by `SWAPGS` changes it +without `WRMSR`. `TSC` gives restore the same clock semantics on every backend. + +KVM does not filter x2APIC indices `0x800..=0x8FF`. Hyperlight keeps the APIC in +xAPIC mode, where MSR access to that range raises `#GP`. + +## MTRRs + +MSHV and WHP read `IA32_MTRRCAP` when the VM is created. The required set +contains `MTRR_DEF_TYPE`, each variable pair reported by `VCNT`, and all fixed +MTRRs. + +Hyper-V accepts fixed-MTRR writes even when `MTRRCAP.FIX` is clear. All fixed +MTRRs are therefore required. Hyper-V supports at most 16 variable pairs. VM +creation fails when the count is larger or a required MTRR cannot be read. + +## MSHV + +MSHV has no per-MSR filter. Its reset set contains every stateful table entry +that has a Hyper-V register mapping and can be read, plus the allow list. + +`msr_to_hv_reg_name` determines which indices the get and set path can reach. +The enumerated host index list does not classify retained state, so it does not +define the reset set. + +MSHV maps `IA32_XSS` through `MSR_IA32_REGISTER_U_XSS`. It maps `IA32_MPERF` +and `IA32_APERF` to the per-VP `MCount` and `ACount` registers. TSX control and +XFD state enter the reset set when their host-register probes succeed. + +## WHP + +WHP has no per-MSR filter. Its reset set contains every stateful table entry +that has a WHP register name and can be read, plus the allow list. + +WHP uses Germanium compatibility. PMU, architectural LBR, and experimental +`DEBUGCTL` bits remain disabled. FRED is in a processor feature bank that the +WHP API does not expose. + +Each guest MSR write is either captured for restore or unsupported by the +partition. Unsupported writes store no state. + +## TSC + +MSHV and WHP expose `TSC` as a host-writable register. Hyper-V stores `TSC` and +`TSC_ADJUST` independently, so restoring `TSC_ADJUST` cannot undo a guest +`WRMSR(TSC)`. + +Hyper-V does not permit an intercept for its implemented `TSC` MSR. Restore +must therefore write the captured `TSC` value. KVM also restores `TSC` so all +backends rewind guest time with the rest of the snapshot state. + +## Limitations + +KVM's security boundary is structural because its deny filter bounds guest +writes. MSHV and WHP depend on the classified table and exposed processor +features. + +The filterless backend tests run on one CPU model per runner. Model-specific +state absent on that CPU is not exercised. A backend that exposes a new +retained MSR feature needs a matching table entry before Hyperlight can use it +safely. + +The ignored full-window audit probes fixed index ranges with a small set of +values. It cannot prove that every vendor MSR or accepted value is covered. diff --git a/src/hyperlight_host/src/error.rs b/src/hyperlight_host/src/error.rs index c6738374d..f280dfe85 100644 --- a/src/hyperlight_host/src/error.rs +++ b/src/hyperlight_host/src/error.rs @@ -154,6 +154,16 @@ pub enum HyperlightError { #[error("Memory Access Violation at address {0:#x} of type {1}, but memory is marked as {2}")] MemoryAccessViolation(u64, MemoryRegionFlags, MemoryRegionFlags), + /// A denied guest MSR read. + #[cfg(target_arch = "x86_64")] + #[error("Guest read from denied MSR {0:#x}")] + MsrReadViolation(u32), + + /// A denied guest MSR write. + #[cfg(target_arch = "x86_64")] + #[error("Guest write of {1:#x} to denied MSR {0:#x}")] + MsrWriteViolation(u32, u64), + /// Memory Allocation Failed. #[error("Memory Allocation Failed with OS Error {0:?}.")] MemoryAllocationFailed(Option), @@ -352,6 +362,10 @@ impl HyperlightError { // as poisoning here too for defense in depth. | HyperlightError::HyperlightVmError(HyperlightVmError::Restore(_)) => true, + #[cfg(target_arch = "x86_64")] + HyperlightError::MsrReadViolation(_) + | HyperlightError::MsrWriteViolation(_, _) => true, + // These errors poison the sandbox because they can leave // it in an inconsistent state due to snapshot restore // failing partway through diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs index 56f7d635d..3418b9f78 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs @@ -163,6 +163,16 @@ impl DispatchGuestCallError { region_flags, }) => HyperlightError::MemoryAccessViolation(addr, access_type, region_flags), + #[cfg(all(target_arch = "x86_64", any(kvm, target_os = "windows")))] + DispatchGuestCallError::Run(RunVmError::MsrReadViolation(msr_index)) => { + HyperlightError::MsrReadViolation(msr_index) + } + + #[cfg(all(target_arch = "x86_64", any(kvm, target_os = "windows")))] + DispatchGuestCallError::Run(RunVmError::MsrWriteViolation { msr_index, value }) => { + HyperlightError::MsrWriteViolation(msr_index, value) + } + // Leave others as is other => HyperlightVmError::DispatchGuestCall(other).into(), }; @@ -213,6 +223,12 @@ pub enum RunVmError { MmioReadUnmapped(u64), #[error("MMIO WRITE access to unmapped address {0:#x}")] MmioWriteUnmapped(u64), + #[cfg(all(target_arch = "x86_64", any(kvm, target_os = "windows")))] + #[error("Guest read from denied MSR {0:#x}")] + MsrReadViolation(u32), + #[cfg(all(target_arch = "x86_64", any(kvm, target_os = "windows")))] + #[error("Guest write of {value:#x} to denied MSR {msr_index:#x}")] + MsrWriteViolation { msr_index: u32, value: u64 }, #[error("vCPU run failed: {0}")] RunVcpu(#[from] RunVcpuError), #[error("Unexpected VM exit: {0}")] @@ -409,6 +425,9 @@ pub(crate) struct HyperlightVm { pub(super) trace_info: MemTraceInfo, #[cfg(crashdump)] pub(super) rt_cfg: SandboxRuntimeConfig, + /// MSR indices and initialization baseline used by snapshot restore. + #[cfg(target_arch = "x86_64")] + pub(super) msr_reset: Option, } impl HyperlightVm { @@ -732,6 +751,14 @@ impl HyperlightVm { } } } + #[cfg(all(target_arch = "x86_64", any(kvm, target_os = "windows")))] + Ok(VmExit::MsrRead(msr_index)) => { + break Err(RunVmError::MsrReadViolation(msr_index)); + } + #[cfg(all(target_arch = "x86_64", any(kvm, target_os = "windows")))] + Ok(VmExit::MsrWrite { msr_index, value }) => { + break Err(RunVmError::MsrWriteViolation { msr_index, value }); + } Ok(VmExit::Cancelled()) => { // If cancellation was not requested for this specific guest function call, // the vcpu was interrupted by a stale cancellation. This can occur when: diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs index 82206f787..47bd36d1c 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -41,16 +41,21 @@ use crate::hypervisor::gdb::{ #[cfg(gdb)] use crate::hypervisor::gdb::{DebugError, DebugMemoryAccessError}; use crate::hypervisor::regs::{ - CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, + CommonDebugRegs, CommonFpu, CommonMsrs, CommonRegisters, CommonSpecialRegisters, MsrResetState, + core_reset_indices, is_mtrr_reset_index, }; +#[cfg(kvm)] +use crate::hypervisor::regs::{MSR_KERNEL_GS_BASE, MSR_TSC}; #[cfg(not(gdb))] use crate::hypervisor::virtual_machine::VirtualMachine; #[cfg(kvm)] use crate::hypervisor::virtual_machine::kvm::KvmVm; #[cfg(mshv3)] use crate::hypervisor::virtual_machine::mshv::MshvVm; +#[cfg(all(mshv3, target_arch = "x86_64"))] +use crate::hypervisor::virtual_machine::mshv::host_supports_msr; #[cfg(target_os = "windows")] -use crate::hypervisor::virtual_machine::whp::WhpVm; +use crate::hypervisor::virtual_machine::whp::{WhpVm, host_supports_msr}; use crate::hypervisor::virtual_machine::{ HypervisorType, RegisterError, VmError, get_available_hypervisor, }; @@ -90,13 +95,35 @@ impl HyperlightVm { #[cfg(not(gdb))] type VmType = Box; - let vm: VmType = match get_available_hypervisor() { + let (vm, required_mtrrs): (VmType, Vec) = match get_available_hypervisor() { #[cfg(kvm)] - Some(HypervisorType::Kvm) => Box::new(KvmVm::new().map_err(VmError::CreateVm)?), + Some(HypervisorType::Kvm) => { + let kvm_vm = KvmVm::new().map_err(VmError::CreateVm)?; + kvm_vm + .configure_msr_access(config.get_allowed_msrs()) + .map_err(VmError::CreateVm)?; + (Box::new(kvm_vm), Vec::new()) + } #[cfg(mshv3)] - Some(HypervisorType::Mshv) => Box::new(MshvVm::new().map_err(VmError::CreateVm)?), + Some(HypervisorType::Mshv) => { + let mshv_vm = MshvVm::new().map_err(VmError::CreateVm)?; + let required_mtrrs = mshv_vm.mtrr_reset_indices().map_err(VmError::CreateVm)?; + // The allow list adds reset state because MSHV has no MSR filter. + mshv_vm + .validate_allowed_msrs(config.get_allowed_msrs()) + .map_err(VmError::CreateVm)?; + (Box::new(mshv_vm), required_mtrrs) + } #[cfg(target_os = "windows")] - Some(HypervisorType::Whp) => Box::new(WhpVm::new().map_err(VmError::CreateVm)?), + Some(HypervisorType::Whp) => { + let whp_vm = WhpVm::new().map_err(VmError::CreateVm)?; + let required_mtrrs = whp_vm.mtrr_reset_indices().map_err(VmError::CreateVm)?; + // The allow list adds reset state because WHP has no MSR filter. + whp_vm + .validate_allowed_msrs(config.get_allowed_msrs()) + .map_err(VmError::CreateVm)?; + (Box::new(whp_vm), required_mtrrs) + } None => return Err(CreateHyperlightVmError::NoHypervisorFound), }; @@ -168,11 +195,50 @@ impl HyperlightVm { trace_info, #[cfg(crashdump)] rt_cfg, + msr_reset: None, }; ret.update_snapshot_mapping(snapshot_mem)?; ret.update_scratch_mapping(scratch_mem)?; + // Resolve the reset policy after vCPU initialization exposes host state. + { + // KVM resets SWAPGS state and TSC in addition to allowed MSRs. + // Filterless backends probe every core stateful MSR. + let core: Vec = match get_available_hypervisor() { + #[cfg(kvm)] + Some(HypervisorType::Kvm) => vec![MSR_KERNEL_GS_BASE, MSR_TSC], + #[cfg(mshv3)] + Some(HypervisorType::Mshv) => core_reset_indices() + .filter(|i| !is_mtrr_reset_index(*i)) + .filter(|i| host_supports_msr(*i)) + .filter(|i| ret.vm.msrs(&[*i]).is_ok()) + .collect(), + // WHP can reset named registers that read successfully. + #[cfg(target_os = "windows")] + Some(HypervisorType::Whp) => core_reset_indices() + .filter(|i| !is_mtrr_reset_index(*i)) + .filter(|i| host_supports_msr(*i)) + .filter(|i| ret.vm.msrs(&[*i]).is_ok()) + .collect(), + _ => Vec::new(), + }; + if !core.is_empty() + || !required_mtrrs.is_empty() + || !config.get_allowed_msrs().is_empty() + { + let mut indices: Vec = core + .into_iter() + .chain(required_mtrrs) + .chain(config.get_allowed_msrs().iter().copied()) + .collect(); + indices.sort_unstable(); + indices.dedup(); + let baseline = ret.vm.msrs(&indices).map_err(VmError::Register)?; + ret.msr_reset = Some(MsrResetState { indices, baseline }); + } + } + // Send the interrupt handle to the GDB thread if debugging is enabled // This is used to allow the GDB thread to stop the vCPU #[cfg(gdb)] @@ -270,6 +336,62 @@ impl HyperlightVm { Ok(self.vm.sregs()?) } + /// Returns the current state selected by the MSR reset policy. + /// Returns `None` when the reset policy is inactive. + pub(crate) fn get_msr_reset_state(&self) -> Result, AccessPageTableError> { + match &self.msr_reset { + Some(state) => Ok(Some(self.vm.msrs(&state.indices)?)), + None => Ok(None), + } + } + + /// Restores snapshot MSRs or the initialization baseline. + pub(crate) fn restore_msrs( + &mut self, + snap_msrs: Option<&CommonMsrs>, + ) -> std::result::Result<(), ResetVcpuError> { + match (&self.msr_reset, snap_msrs) { + (None, None) => {} + (None, Some(msrs)) if msrs.is_empty() => {} + (None, Some(msrs)) => { + return Err(RegisterError::InvalidSnapshotMsrCount { + expected: 0, + actual: msrs.len(), + } + .into()); + } + (Some(state), None) => self.vm.set_msrs(&state.baseline)?, + (Some(state), Some(msrs)) => { + let validated = state.validate_snapshot(msrs)?; + self.vm.set_msrs(&validated)?; + } + } + Ok(()) + } + + /// Reads arbitrary backend MSRs for tests. + #[cfg(all(test, mshv3, target_arch = "x86_64"))] + pub(crate) fn capture_msrs_for_test( + &self, + indices: &[u32], + ) -> std::result::Result { + self.vm.msrs(indices) + } + + /// Attempts one backend MSR write for tests. + #[cfg(all(test, target_arch = "x86_64"))] + #[allow(dead_code)] + pub(crate) fn try_set_msr_for_test(&self, index: u32, value: u64) -> bool { + use crate::hypervisor::regs::{MsrEntry, MsrKind}; + self.vm + .set_msrs(&vec![MsrEntry { + index, + value, + kind: MsrKind::Stateful, + }]) + .is_ok() + } + /// Dispatch a call from the host to the guest using the given pointer /// to the dispatch function _in the guest's address space_. /// diff --git a/src/hyperlight_host/src/hypervisor/regs/x86_64/mod.rs b/src/hyperlight_host/src/hypervisor/regs/x86_64/mod.rs index 88724d95a..12d65f056 100644 --- a/src/hyperlight_host/src/hypervisor/regs/x86_64/mod.rs +++ b/src/hyperlight_host/src/hypervisor/regs/x86_64/mod.rs @@ -16,11 +16,13 @@ limitations under the License. mod debug_regs; mod fpu; +mod msrs; mod special_regs; mod standard_regs; pub(crate) use debug_regs::*; pub(crate) use fpu::*; +pub(crate) use msrs::*; pub(crate) use special_regs::*; pub(crate) use standard_regs::*; diff --git a/src/hyperlight_host/src/hypervisor/regs/x86_64/msrs.rs b/src/hyperlight_host/src/hypervisor/regs/x86_64/msrs.rs new file mode 100644 index 000000000..c8ca01dd9 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/regs/x86_64/msrs.rs @@ -0,0 +1,460 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//! Model-specific register (MSR) state restored with a snapshot. +//! +//! The reset set contains every MSR whose guest-written state can persist. +//! Access policy is independent. Allowed MSRs remain in the reset set. + +/// A single MSR captured for reset. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct MsrEntry { + /// The index passed to `RDMSR` and `WRMSR`. + pub index: u32, + /// The captured value. + pub value: u64, + /// How restore handles the MSR. + pub kind: MsrKind, +} + +/// How an MSR is treated by capture and restore. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum MsrKind { + /// State captured at initialization and written on restore. + Stateful, + /// A write-only command with no retained state. + WriteOnlyCommand, +} + +/// A set of MSRs captured for reset. +pub(crate) type CommonMsrs = Vec; + +/// The MSR policy and initialization baseline for one VM. +#[derive(Debug, Clone)] +pub(crate) struct MsrResetState { + /// Core host indices plus the validated allow list. + pub indices: Vec, + /// Values used when a snapshot has no MSR state. + pub baseline: CommonMsrs, +} + +impl MsrResetState { + /// Validates snapshot indices and applies local MSR classifications. + pub fn validate_snapshot( + &self, + snapshot: &[MsrEntry], + ) -> Result { + use crate::hypervisor::virtual_machine::RegisterError; + + if snapshot.len() != self.indices.len() { + return Err(RegisterError::InvalidSnapshotMsrCount { + expected: self.indices.len(), + actual: snapshot.len(), + }); + } + + self.indices + .iter() + .zip(snapshot) + .enumerate() + .map(|(position, (&expected, supplied))| { + if supplied.index != expected { + return Err(RegisterError::InvalidSnapshotMsrIndex { + position, + expected, + actual: supplied.index, + }); + } + + let kind = classify_msr(expected).ok_or(RegisterError::MsrsUnsupported)?; + Ok(MsrEntry { + index: expected, + value: supplied.value, + kind, + }) + }) + .collect() + } +} + +pub(crate) const MSR_SYSENTER_CS: u32 = 0x174; +pub(crate) const MSR_SYSENTER_ESP: u32 = 0x175; +pub(crate) const MSR_SYSENTER_EIP: u32 = 0x176; +pub(crate) const MSR_DEBUGCTL: u32 = 0x1D9; +pub(crate) const MSR_PAT: u32 = 0x277; +pub(crate) const MSR_STAR: u32 = 0xC000_0081; +pub(crate) const MSR_LSTAR: u32 = 0xC000_0082; +pub(crate) const MSR_CSTAR: u32 = 0xC000_0083; +pub(crate) const MSR_SFMASK: u32 = 0xC000_0084; +pub(crate) const MSR_KERNEL_GS_BASE: u32 = 0xC000_0102; +pub(crate) const MSR_SPEC_CTRL: u32 = 0x48; + +pub(crate) const MSR_U_CET: u32 = 0x6A0; +pub(crate) const MSR_S_CET: u32 = 0x6A2; +pub(crate) const MSR_PL0_SSP: u32 = 0x6A4; +pub(crate) const MSR_PL1_SSP: u32 = 0x6A5; +pub(crate) const MSR_PL2_SSP: u32 = 0x6A6; +pub(crate) const MSR_PL3_SSP: u32 = 0x6A7; +pub(crate) const MSR_INTERRUPT_SSP_TABLE_ADDR: u32 = 0x6A8; +pub(crate) const MSR_XSS: u32 = 0xDA0; + +pub(crate) const MSR_TSC: u32 = 0x10; +pub(crate) const MSR_TSC_ADJUST: u32 = 0x3B; +pub(crate) const MSR_TSC_AUX: u32 = 0xC000_0103; +pub(crate) const MSR_MPERF: u32 = 0xE7; +pub(crate) const MSR_APERF: u32 = 0xE8; +pub(crate) const MSR_TSX_CTRL: u32 = 0x122; +pub(crate) const MSR_XFD: u32 = 0x1C4; +pub(crate) const MSR_XFD_ERR: u32 = 0x1C5; +pub(crate) const MSR_MTRR_DEF_TYPE: u32 = 0x2FF; +pub(crate) const MSR_MTRR_CAP: u32 = 0xFE; +const HYPERV_VARIABLE_MTRR_COUNT: u8 = 16; +pub(crate) const MSR_MTRR_FIX64K_00000: u32 = 0x250; + +pub(crate) const MSR_PRED_CMD: u32 = 0x49; +pub(crate) const MSR_FLUSH_CMD: u32 = 0x10B; + +// Every guest-writable retained value must be host-readable and host-writable. +// EFER, APIC_BASE, FS_BASE, and GS_BASE are part of the special-register state. +const MSR_TABLE: &[(u32, MsrKind)] = &[ + // Guest and host access use the matching SYSENTER state. + (MSR_SYSENTER_CS, MsrKind::Stateful), + (MSR_SYSENTER_ESP, MsrKind::Stateful), + (MSR_SYSENTER_EIP, MsrKind::Stateful), + // WHP exposes no writable DEBUGCTL bits under Germanium compatibility. + (MSR_DEBUGCTL, MsrKind::Stateful), + // Guest and host access use the same PAT state. + (MSR_PAT, MsrKind::Stateful), + // Guest and host access use the matching syscall state. + (MSR_STAR, MsrKind::Stateful), + (MSR_LSTAR, MsrKind::Stateful), + (MSR_CSTAR, MsrKind::Stateful), + (MSR_SFMASK, MsrKind::Stateful), + // SWAPGS and host access use the same KERNEL_GS_BASE state. + (MSR_KERNEL_GS_BASE, MsrKind::Stateful), + // Guest and host access use the same SPEC_CTRL state. + (MSR_SPEC_CTRL, MsrKind::Stateful), + // Host probing omits CET state when CET is unavailable. + (MSR_U_CET, MsrKind::Stateful), + (MSR_S_CET, MsrKind::Stateful), + // Host probing omits shadow-stack state when it is unavailable. + (MSR_PL0_SSP, MsrKind::Stateful), + (MSR_PL1_SSP, MsrKind::Stateful), + (MSR_PL2_SSP, MsrKind::Stateful), + (MSR_PL3_SSP, MsrKind::Stateful), + (MSR_INTERRUPT_SSP_TABLE_ADDR, MsrKind::Stateful), + // MSHV maps XSS through its U_XSS alias. + (MSR_XSS, MsrKind::Stateful), + // Guest and host access use the same virtual counter. + (MSR_TSC, MsrKind::Stateful), + // Hyper-V stores TSC_ADJUST independently from TSC. + (MSR_TSC_ADJUST, MsrKind::Stateful), + // Host probing omits TSC_AUX when RDTSCP is unavailable. + (MSR_TSC_AUX, MsrKind::Stateful), + // Hyper-V exposes MPERF and APERF as per-VP counters. + (MSR_MPERF, MsrKind::Stateful), + (MSR_APERF, MsrKind::Stateful), + // Host probing omits TSX_CTRL when TSX control is unavailable. + (MSR_TSX_CTRL, MsrKind::Stateful), + // Host probing omits XFD state when XFD is unavailable. + (MSR_XFD, MsrKind::Stateful), + (MSR_XFD_ERR, MsrKind::Stateful), + // Hyper-V accepts fixed-MTRR writes even when MTRRCAP.FIX is clear. + (MSR_MTRR_DEF_TYPE, MsrKind::Stateful), + (0x200, MsrKind::Stateful), // PHYSBASE0 + (0x201, MsrKind::Stateful), // PHYSMASK0 + (0x202, MsrKind::Stateful), // PHYSBASE1 + (0x203, MsrKind::Stateful), // PHYSMASK1 + (0x204, MsrKind::Stateful), // PHYSBASE2 + (0x205, MsrKind::Stateful), // PHYSMASK2 + (0x206, MsrKind::Stateful), // PHYSBASE3 + (0x207, MsrKind::Stateful), // PHYSMASK3 + (0x208, MsrKind::Stateful), // PHYSBASE4 + (0x209, MsrKind::Stateful), // PHYSMASK4 + (0x20A, MsrKind::Stateful), // PHYSBASE5 + (0x20B, MsrKind::Stateful), // PHYSMASK5 + (0x20C, MsrKind::Stateful), // PHYSBASE6 + (0x20D, MsrKind::Stateful), // PHYSMASK6 + (0x20E, MsrKind::Stateful), // PHYSBASE7 + (0x20F, MsrKind::Stateful), // PHYSMASK7 + (0x210, MsrKind::Stateful), // PHYSBASE8 + (0x211, MsrKind::Stateful), // PHYSMASK8 + (0x212, MsrKind::Stateful), // PHYSBASE9 + (0x213, MsrKind::Stateful), // PHYSMASK9 + (0x214, MsrKind::Stateful), // PHYSBASEA + (0x215, MsrKind::Stateful), // PHYSMASKA + (0x216, MsrKind::Stateful), // PHYSBASEB + (0x217, MsrKind::Stateful), // PHYSMASKB + (0x218, MsrKind::Stateful), // PHYSBASEC + (0x219, MsrKind::Stateful), // PHYSMASKC + (0x21A, MsrKind::Stateful), // PHYSBASED + (0x21B, MsrKind::Stateful), // PHYSMASKD + (0x21C, MsrKind::Stateful), // PHYSBASEE + (0x21D, MsrKind::Stateful), // PHYSMASKE + (0x21E, MsrKind::Stateful), // PHYSBASEF + (0x21F, MsrKind::Stateful), // PHYSMASKF + (MSR_MTRR_FIX64K_00000, MsrKind::Stateful), // FIX64K_00000 + (0x258, MsrKind::Stateful), // FIX16K_80000 + (0x259, MsrKind::Stateful), // FIX16K_A0000 + (0x268, MsrKind::Stateful), // FIX4K_C0000 + (0x269, MsrKind::Stateful), // FIX4K_C8000 + (0x26A, MsrKind::Stateful), // FIX4K_D0000 + (0x26B, MsrKind::Stateful), // FIX4K_D8000 + (0x26C, MsrKind::Stateful), // FIX4K_E0000 + (0x26D, MsrKind::Stateful), // FIX4K_E8000 + (0x26E, MsrKind::Stateful), // FIX4K_F0000 + (0x26F, MsrKind::Stateful), // FIX4K_F8000 + // Classification gives allow requests for stateless commands a clear error. + (MSR_PRED_CMD, MsrKind::WriteOnlyCommand), + (MSR_FLUSH_CMD, MsrKind::WriteOnlyCommand), +]; + +/// Returns the known classification for an MSR index. +pub(crate) fn classify_msr(index: u32) -> Option { + MSR_TABLE + .iter() + .find(|(i, _)| *i == index) + .map(|(_, kind)| *kind) +} + +/// Returns core stateful indices for host filtering. +pub(crate) fn core_reset_indices() -> impl Iterator { + MSR_TABLE + .iter() + .filter(|(_, kind)| matches!(kind, MsrKind::Stateful)) + .map(|(index, _)| *index) +} + +pub(crate) fn hyperv_mtrr_reset_indices( + mtrr_cap: u64, +) -> Result, crate::hypervisor::virtual_machine::CreateVmError> { + use crate::hypervisor::virtual_machine::CreateVmError; + + let advertised = (mtrr_cap & 0xff) as u8; + if advertised > HYPERV_VARIABLE_MTRR_COUNT { + return Err(CreateVmError::UnexpectedVariableMtrrCount { + advertised, + maximum: HYPERV_VARIABLE_MTRR_COUNT, + }); + } + + let mut indices = Vec::with_capacity(1 + usize::from(advertised) * 2 + 11); + indices.push(MSR_MTRR_DEF_TYPE); + indices.extend((0..u32::from(advertised) * 2).map(|offset| 0x200 + offset)); + indices.extend([ + MSR_MTRR_FIX64K_00000, + 0x258, + 0x259, + 0x268, + 0x269, + 0x26A, + 0x26B, + 0x26C, + 0x26D, + 0x26E, + 0x26F, + ]); + Ok(indices) +} + +pub(crate) fn is_mtrr_reset_index(index: u32) -> bool { + index == MSR_MTRR_DEF_TYPE + || (0x200..=0x21F).contains(&index) + || matches!( + index, + MSR_MTRR_FIX64K_00000 + | 0x258 + | 0x259 + | 0x268 + | 0x269 + | 0x26A + | 0x26B + | 0x26C + | 0x26D + | 0x26E + | 0x26F + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hypervisor::virtual_machine::{CreateVmError, RegisterError}; + + fn entry(index: u32, value: u64) -> MsrEntry { + MsrEntry { + index, + value, + kind: MsrKind::Stateful, + } + } + + fn state() -> MsrResetState { + MsrResetState { + indices: vec![MSR_SYSENTER_CS, MSR_SYSENTER_ESP, MSR_KERNEL_GS_BASE], + baseline: Vec::new(), + } + } + + #[test] + fn hyperv_mtrr_indices_follow_guest_capability() { + assert_eq!( + hyperv_mtrr_reset_indices(2).unwrap(), + [ + MSR_MTRR_DEF_TYPE, + 0x200, + 0x201, + 0x202, + 0x203, + MSR_MTRR_FIX64K_00000, + 0x258, + 0x259, + 0x268, + 0x269, + 0x26A, + 0x26B, + 0x26C, + 0x26D, + 0x26E, + 0x26F, + ] + ); + let fixed_bit_does_not_change_reset_set = hyperv_mtrr_reset_indices(2 | (1 << 8)).unwrap(); + assert_eq!( + fixed_bit_does_not_change_reset_set, + hyperv_mtrr_reset_indices(2).unwrap() + ); + } + + #[test] + fn hyperv_mtrr_count_rejects_more_than_sixteen_pairs() { + let indices = hyperv_mtrr_reset_indices(16).unwrap(); + assert!(indices.contains(&0x21F)); + assert_eq!(indices.last(), Some(&0x26F)); + assert!( + indices + .iter() + .all(|&index| classify_msr(index) == Some(MsrKind::Stateful)) + ); + assert!(matches!( + hyperv_mtrr_reset_indices(17), + Err(CreateVmError::UnexpectedVariableMtrrCount { + advertised: 17, + maximum: 16 + }) + )); + } + + #[test] + fn snapshot_msr_validation_accepts_exact_canonical_set() { + let supplied = vec![ + entry(MSR_SYSENTER_CS, 1), + entry(MSR_SYSENTER_ESP, 2), + entry(MSR_KERNEL_GS_BASE, 3), + ]; + + assert_eq!(state().validate_snapshot(&supplied).unwrap(), supplied); + } + + #[test] + fn snapshot_msr_validation_rebuilds_untrusted_metadata() { + let mut supplied = vec![ + entry(MSR_SYSENTER_CS, 1), + entry(MSR_SYSENTER_ESP, 2), + entry(MSR_KERNEL_GS_BASE, 3), + ]; + supplied[1].kind = MsrKind::WriteOnlyCommand; + + let validated = state().validate_snapshot(&supplied).unwrap(); + assert_eq!(validated[1].kind, MsrKind::Stateful); + assert_eq!(validated[1].value, 2); + } + + #[test] + fn snapshot_msr_validation_rejects_wrong_counts() { + let canonical = [ + entry(MSR_SYSENTER_CS, 1), + entry(MSR_SYSENTER_ESP, 2), + entry(MSR_KERNEL_GS_BASE, 3), + ]; + for supplied in [ + vec![], + canonical[1..].to_vec(), + vec![canonical[0], canonical[2]], + canonical[..2].to_vec(), + vec![ + entry(MSR_PAT, 4), + entry(MSR_SYSENTER_CS, 1), + entry(MSR_SYSENTER_ESP, 2), + entry(MSR_KERNEL_GS_BASE, 3), + ], + vec![ + entry(MSR_SYSENTER_CS, 1), + entry(MSR_PAT, 4), + entry(MSR_SYSENTER_ESP, 2), + entry(MSR_KERNEL_GS_BASE, 3), + ], + vec![ + entry(MSR_SYSENTER_CS, 1), + entry(MSR_SYSENTER_ESP, 2), + entry(MSR_KERNEL_GS_BASE, 3), + entry(MSR_PAT, 4), + ], + ] { + assert!(matches!( + state().validate_snapshot(&supplied), + Err(RegisterError::InvalidSnapshotMsrCount { .. }) + )); + } + } + + #[test] + fn snapshot_msr_validation_rejects_noncanonical_indices() { + let cases = [ + vec![ + entry(MSR_SYSENTER_ESP, 2), + entry(MSR_SYSENTER_CS, 1), + entry(MSR_KERNEL_GS_BASE, 3), + ], + vec![ + entry(MSR_SYSENTER_CS, 1), + entry(MSR_SYSENTER_CS, 2), + entry(MSR_KERNEL_GS_BASE, 3), + ], + vec![ + entry(MSR_SYSENTER_CS, 1), + entry(MSR_SYSENTER_ESP, 2), + entry(MSR_PAT, 3), + ], + ]; + + for supplied in cases { + assert!(matches!( + state().validate_snapshot(&supplied), + Err(RegisterError::InvalidSnapshotMsrIndex { .. }) + )); + } + } + + #[test] + fn snapshot_msr_validation_accepts_empty_canonical_set() { + let state = MsrResetState { + indices: Vec::new(), + baseline: Vec::new(), + }; + assert!(state.validate_snapshot(&[]).unwrap().is_empty()); + } +} diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs index 3dc8ec87a..9a5e8cd8d 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs @@ -14,16 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ +use std::collections::HashSet; use std::sync::LazyLock; use hyperlight_common::outb::VmAction; #[cfg(gdb)] use kvm_bindings::kvm_guest_debug; use kvm_bindings::{ - kvm_debugregs, kvm_fpu, kvm_regs, kvm_sregs, kvm_userspace_memory_region, kvm_xsave, + Msrs, kvm_debugregs, kvm_enable_cap, kvm_fpu, kvm_msr_entry, kvm_regs, kvm_sregs, + kvm_userspace_memory_region, kvm_xsave, }; use kvm_ioctls::Cap::UserMemory; -use kvm_ioctls::{Kvm, VcpuExit, VcpuFd, VmFd}; +use kvm_ioctls::{ + Cap, Kvm, MsrExitReason, MsrFilterDefaultAction, MsrFilterRange, MsrFilterRangeFlags, VcpuExit, + VcpuFd, VmFd, +}; use tracing::{Span, instrument}; #[cfg(feature = "trace_guest")] use tracing_opentelemetry::OpenTelemetrySpanExt; @@ -33,8 +38,8 @@ use vmm_sys_util::eventfd::EventFd; #[cfg(gdb)] use crate::hypervisor::gdb::{DebugError, DebuggableVm}; use crate::hypervisor::regs::{ - CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, FP_CONTROL_WORD_DEFAULT, - MXCSR_DEFAULT, + CommonDebugRegs, CommonFpu, CommonMsrs, CommonRegisters, CommonSpecialRegisters, + FP_CONTROL_WORD_DEFAULT, MXCSR_DEFAULT, MsrEntry, MsrKind, classify_msr, }; #[cfg(test)] use crate::hypervisor::virtual_machine::XSAVE_BUFFER_SIZE; @@ -110,6 +115,42 @@ pub(crate) struct KvmVm { static KVM: LazyLock> = LazyLock::new(|| Kvm::new().map_err(|e| CreateVmError::HypervisorNotAvailable(e.into()))); +/// Cached host indices reported by `KVM_GET_MSR_INDEX_LIST`. +/// An empty set makes support checks fail closed. +static HOST_MSR_INDICES: LazyLock> = LazyLock::new(|| match KVM.as_ref() { + Ok(kvm) => match kvm.get_msr_index_list() { + Ok(list) => list.as_slice().iter().copied().collect(), + Err(e) => { + tracing::warn!("KVM_GET_MSR_INDEX_LIST failed: {e}"); + HashSet::new() + } + }, + Err(_) => HashSet::new(), +}); + +/// Returns the set of MSR indices the host KVM supports for get/set. +pub(crate) fn host_msr_indices() -> &'static HashSet { + &HOST_MSR_INDICES +} + +/// KVM allows at most this many MSR filter ranges. +const KVM_MSR_FILTER_MAX_RANGES: usize = 16; + +/// Returns the smallest contiguous ranges covering the supplied indices. +fn coalesce_msr_ranges(indices: &[u32]) -> Vec<(u32, usize)> { + let mut sorted: Vec = indices.to_vec(); + sorted.sort_unstable(); + sorted.dedup(); + let mut groups: Vec<(u32, usize)> = Vec::new(); + for idx in sorted { + match groups.last_mut() { + Some((base, count)) if *base + *count as u32 == idx => *count += 1, + _ => groups.push((idx, 1)), + } + } + groups +} + #[cfg(feature = "hw-interrupts")] impl KvmVm { /// Create the in-kernel IRQ chip and register an irqfd for GSI 0. @@ -228,6 +269,24 @@ impl KvmVm { } Ok(VcpuExit::MmioRead(addr, _)) => return Ok(VmExit::MmioRead(addr)), Ok(VcpuExit::MmioWrite(addr, _)) => return Ok(VmExit::MmioWrite(addr)), + // Complete filtered access through the default run path. + Ok(VcpuExit::X86Rdmsr(msr_exit)) => { + let msr_index = msr_exit.index; + *msr_exit.error = 1; + self.vcpu_fd.set_kvm_immediate_exit(1); + let _ = self.vcpu_fd.run(); + self.vcpu_fd.set_kvm_immediate_exit(0); + return Ok(VmExit::MsrRead(msr_index)); + } + Ok(VcpuExit::X86Wrmsr(msr_exit)) => { + let msr_index = msr_exit.index; + let value = msr_exit.data; + *msr_exit.error = 1; + self.vcpu_fd.set_kvm_immediate_exit(1); + let _ = self.vcpu_fd.run(); + self.vcpu_fd.set_kvm_immediate_exit(0); + return Ok(VmExit::MsrWrite { msr_index, value }); + } #[cfg(gdb)] Ok(VcpuExit::Debug(debug_exit)) => { return Ok(VmExit::Debug { @@ -275,6 +334,25 @@ impl KvmVm { Ok(VcpuExit::IoOut(port, data)) => Ok(VmExit::IoOut(port, data.to_vec())), Ok(VcpuExit::MmioRead(addr, _)) => Ok(VmExit::MmioRead(addr)), Ok(VcpuExit::MmioWrite(addr, _)) => Ok(VmExit::MmioWrite(addr)), + // Reentering KVM_RUN completes the failed MSR exit and injects #GP. + // immediate_exit prevents further guest execution. + Ok(VcpuExit::X86Rdmsr(msr_exit)) => { + let msr_index = msr_exit.index; + *msr_exit.error = 1; + self.vcpu_fd.set_kvm_immediate_exit(1); + let _ = self.vcpu_fd.run(); + self.vcpu_fd.set_kvm_immediate_exit(0); + Ok(VmExit::MsrRead(msr_index)) + } + Ok(VcpuExit::X86Wrmsr(msr_exit)) => { + let msr_index = msr_exit.index; + let value = msr_exit.data; + *msr_exit.error = 1; + self.vcpu_fd.set_kvm_immediate_exit(1); + let _ = self.vcpu_fd.run(); + self.vcpu_fd.set_kvm_immediate_exit(0); + Ok(VmExit::MsrWrite { msr_index, value }) + } #[cfg(gdb)] Ok(VcpuExit::Debug(debug_exit)) => Ok(VmExit::Debug { dr6: debug_exit.dr6, @@ -292,6 +370,150 @@ impl KvmVm { ))), } } + + /// Installs a deny filter containing the validated allow list. + /// Requires `KVM_CAP_X86_USER_SPACE_MSR` and `KVM_CAP_X86_MSR_FILTER`. + pub(crate) fn configure_msr_access( + &self, + allowed: &[u32], + ) -> std::result::Result<(), CreateVmError> { + let hv = KVM.as_ref().map_err(|e| e.clone())?; + if !hv.check_extension(Cap::X86UserSpaceMsr) || !hv.check_extension(Cap::X86MsrFilter) { + tracing::error!( + "KVM does not support KVM_CAP_X86_USER_SPACE_MSR or KVM_CAP_X86_MSR_FILTER." + ); + return Err(CreateVmError::MsrFilterNotSupported); + } + + // Every permitted guest write must have restorable host state. + for &msr in allowed { + self.validate_allowed_msr(msr)?; + } + + // Tell KVM to exit to userspace on filtered MSR access. + let cap = kvm_enable_cap { + cap: Cap::X86UserSpaceMsr as u32, + args: [MsrExitReason::Filter.bits() as u64, 0, 0, 0], + ..Default::default() + }; + self.vm_fd + .enable_cap(&cap) + .map_err(|e| CreateVmError::InitializeVm(e.into()))?; + + // Each contiguous group consumes one KVM filter range. + let groups = coalesce_msr_ranges(allowed); + if groups.len() > KVM_MSR_FILTER_MAX_RANGES { + return Err(CreateVmError::TooManyMsrRanges(groups.len())); + } + + // The bitmaps must live through set_msr_filter. + let bitmaps: Vec> = groups + .iter() + .map(|(_, count)| { + let mut bytes = vec![0u8; count.div_ceil(8)]; + for bit in 0..*count { + bytes[bit / 8] |= 1 << (bit % 8); + } + bytes + }) + .collect(); + + // Default deny requires at least one range. + static DENY_BITMAP: [u8; 1] = [0u8]; + let ranges: Vec = if groups.is_empty() { + vec![MsrFilterRange { + flags: MsrFilterRangeFlags::READ | MsrFilterRangeFlags::WRITE, + base: 0, + msr_count: 1, + bitmap: &DENY_BITMAP, + }] + } else { + groups + .iter() + .zip(bitmaps.iter()) + .map(|((base, count), bitmap)| MsrFilterRange { + flags: MsrFilterRangeFlags::READ | MsrFilterRangeFlags::WRITE, + base: *base, + msr_count: *count as u32, + bitmap: bitmap.as_slice(), + }) + .collect() + }; + + self.vm_fd + .set_msr_filter(MsrFilterDefaultAction::DENY, &ranges) + .map_err(|e| CreateVmError::InitializeVm(e.into()))?; + Ok(()) + } + + /// Validates that an allowed MSR has restorable host state. + fn validate_allowed_msr(&self, msr: u32) -> std::result::Result<(), CreateVmError> { + if !matches!(classify_msr(msr), Some(MsrKind::Stateful)) { + return Err(CreateVmError::MsrNotAllowable { + msr, + reason: "MSR is not an explicitly classified stateful MSR".to_string(), + }); + } + if !host_msr_indices().contains(&msr) { + return Err(CreateVmError::MsrNotAllowable { + msr, + reason: "MSR is not supported by the host".to_string(), + }); + } + let value = self + .read_msr(msr) + .map_err(|e| CreateVmError::MsrNotAllowable { + msr, + reason: format!("MSR is not readable: {e}"), + })?; + self.write_msr(msr, value) + .map_err(|e| CreateVmError::MsrNotAllowable { + msr, + reason: format!("MSR is not resettable: {e}"), + })?; + Ok(()) + } + + /// Reads one vCPU MSR without the guest filter. + fn read_msr(&self, index: u32) -> std::result::Result { + let mut msrs = Msrs::from_entries(&[kvm_msr_entry { + index, + ..Default::default() + }]) + .map_err(|e| RegisterError::MsrBuild(format!("{e:?}")))?; + let n = self + .vcpu_fd + .get_msrs(&mut msrs) + .map_err(|e| RegisterError::GetMsrs(e.into()))?; + if n != 1 { + return Err(RegisterError::MsrShortCount { + expected: 1, + actual: n, + }); + } + Ok(msrs.as_slice()[0].data) + } + + /// Writes one vCPU MSR without the guest filter. + fn write_msr(&self, index: u32, data: u64) -> std::result::Result<(), RegisterError> { + let msrs = Msrs::from_entries(&[kvm_msr_entry { + index, + data, + ..Default::default() + }]) + .map_err(|e| RegisterError::MsrBuild(format!("{e:?}")))?; + let n = self + .vcpu_fd + .set_msrs(&msrs) + .map_err(|e| RegisterError::SetMsrs(e.into()))?; + if n != 1 { + return Err(RegisterError::MsrShortCount { + expected: 1, + actual: n, + }); + } + Ok(()) + } } impl VirtualMachine for KvmVm { @@ -403,6 +625,72 @@ impl VirtualMachine for KvmVm { Ok(()) } + fn msrs(&self, indices: &[u32]) -> std::result::Result { + if indices.is_empty() { + return Ok(Vec::new()); + } + let entries: Vec = indices + .iter() + .map(|&index| kvm_msr_entry { + index, + ..Default::default() + }) + .collect(); + let mut msrs = + Msrs::from_entries(&entries).map_err(|e| RegisterError::MsrBuild(format!("{e:?}")))?; + let n = self + .vcpu_fd + .get_msrs(&mut msrs) + .map_err(|e| RegisterError::GetMsrs(e.into()))?; + if n != indices.len() { + return Err(RegisterError::MsrShortCount { + expected: indices.len(), + actual: n, + }); + } + Ok(msrs + .as_slice() + .iter() + .map(|e| { + let kind = classify_msr(e.index).unwrap_or(MsrKind::Stateful); + MsrEntry { + index: e.index, + value: e.data, + kind, + } + }) + .collect()) + } + + fn set_msrs(&self, msrs: &CommonMsrs) -> std::result::Result<(), RegisterError> { + // Stateless commands must never be replayed. + let entries: Vec = msrs + .iter() + .filter(|e| e.kind == MsrKind::Stateful) + .map(|e| kvm_msr_entry { + index: e.index, + data: e.value, + ..Default::default() + }) + .collect(); + if entries.is_empty() { + return Ok(()); + } + let kvm_msrs = + Msrs::from_entries(&entries).map_err(|e| RegisterError::MsrBuild(format!("{e:?}")))?; + let n = self + .vcpu_fd + .set_msrs(&kvm_msrs) + .map_err(|e| RegisterError::SetMsrs(e.into()))?; + if n != entries.len() { + return Err(RegisterError::MsrShortCount { + expected: entries.len(), + actual: n, + }); + } + Ok(()) + } + #[allow(dead_code)] fn xsave(&self) -> std::result::Result, RegisterError> { let xsave = self @@ -570,10 +858,36 @@ impl DebuggableVm for KvmVm { } #[cfg(test)] -#[cfg(feature = "hw-interrupts")] -mod hw_interrupt_tests { +mod tests { use super::*; + #[test] + fn coalesces_unsorted_contiguous_indices() { + assert_eq!( + coalesce_msr_ranges(&[0x176, 0x174, 0x175]), + vec![(0x174, 3)] + ); + } + + #[test] + fn deduplicates_indices() { + assert_eq!( + coalesce_msr_ranges(&[0x174, 0x174, 0x176]), + vec![(0x174, 1), (0x176, 1)] + ); + } + + #[test] + fn preserves_sixteen_range_boundary() { + let indices: Vec = (0..KVM_MSR_FILTER_MAX_RANGES as u32) + .map(|index| index * 2) + .collect(); + let ranges = coalesce_msr_ranges(&indices); + assert_eq!(ranges.len(), KVM_MSR_FILTER_MAX_RANGES); + assert!(ranges.iter().all(|(_, count)| *count == 1)); + } + + #[cfg(feature = "hw-interrupts")] #[test] fn halt_port_is_not_standard_device() { // VmAction::Halt port must not overlap in-kernel PIC/PIT/speaker ports diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs index dac344711..07a2b191c 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs @@ -21,6 +21,8 @@ use tracing::{Span, instrument}; #[cfg(gdb)] use crate::hypervisor::gdb::DebugError; +#[cfg(target_arch = "x86_64")] +use crate::hypervisor::regs::CommonMsrs; use crate::hypervisor::regs::{ CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, }; @@ -139,6 +141,12 @@ pub(crate) enum VmExit { MmioRead(u64), /// The vCPU tried to write to the given (unmapped) addr MmioWrite(u64), + /// The vCPU tried to read from the given MSR + #[cfg(all(target_arch = "x86_64", any(kvm, target_os = "windows")))] + MsrRead(u32), + /// The vCPU tried to write to the given MSR with the given value + #[cfg(all(target_arch = "x86_64", any(kvm, target_os = "windows")))] + MsrWrite { msr_index: u32, value: u64 }, /// The vCPU execution has been cancelled Cancelled(), /// The vCPU has exited for a reason that is not handled by Hyperlight @@ -183,6 +191,30 @@ pub enum CreateVmError { HypervisorNotAvailable(HypervisorError), #[error("Initialize VM failed: {0}")] InitializeVm(HypervisorError), + #[cfg(all(kvm, target_arch = "x86_64"))] + #[error("KVM MSR filtering requires KVM_CAP_X86_USER_SPACE_MSR and KVM_CAP_X86_MSR_FILTER")] + MsrFilterNotSupported, + #[cfg(target_arch = "x86_64")] + #[error("MSR {msr:#x} cannot be allowed: {reason}")] + MsrNotAllowable { msr: u32, reason: String }, + #[cfg(target_arch = "x86_64")] + #[error("Failed to read IA32_MTRRCAP: {0}")] + GetMtrrCap(RegisterError), + #[cfg(target_arch = "x86_64")] + #[error("Guest-visible MTRRs cannot be reset: {0}")] + RequiredMtrrsNotResettable(RegisterError), + #[cfg(target_arch = "x86_64")] + #[error("Guest exposes {advertised} variable MTRR pairs, expected at most {maximum}")] + UnexpectedVariableMtrrCount { advertised: u8, maximum: u8 }, + #[cfg(all(kvm, target_arch = "x86_64"))] + #[error("Too many allowed MSR filter ranges: {0}. Maximum is 16")] + TooManyMsrRanges(usize), + #[cfg(target_os = "windows")] + #[error("Get Partition Property failed: {0}")] + GetPartitionProperty(HypervisorError), + #[cfg(target_os = "windows")] + #[error("WHP exposes {advertised} processor feature banks, expected {expected}")] + UnexpectedProcessorFeatureBankCount { advertised: u32, expected: u32 }, #[error("Set Partition Property failed: {0}")] SetPartitionProperty(HypervisorError), #[cfg(target_os = "windows")] @@ -241,6 +273,46 @@ pub enum RegisterError { }, #[error("Invalid xsave alignment")] InvalidXsaveAlignment, + #[cfg(target_arch = "x86_64")] + #[error("MSR operation not supported on this hypervisor")] + MsrsUnsupported, + #[cfg(target_arch = "x86_64")] + #[error("Failed to build MSR list: {0}")] + MsrBuild(String), + #[cfg(target_arch = "x86_64")] + #[error("Failed to get MSRs: {0}")] + GetMsrs(HypervisorError), + #[cfg(target_arch = "x86_64")] + #[error("Failed to set MSRs: {0}")] + SetMsrs(HypervisorError), + #[cfg(target_arch = "x86_64")] + #[error("Invalid snapshot MSR count: expected {expected}, got {actual}")] + InvalidSnapshotMsrCount { + /// Number of MSRs in the locally resolved reset set. + expected: usize, + /// Number of MSRs supplied by the snapshot. + actual: usize, + }, + #[cfg(target_arch = "x86_64")] + #[error( + "Invalid snapshot MSR at position {position}: expected index {expected:#x}, got {actual:#x}" + )] + InvalidSnapshotMsrIndex { + /// Position in the canonical, sorted reset set. + position: usize, + /// Locally authorized architectural MSR index. + expected: u32, + /// Architectural MSR index supplied by the snapshot. + actual: u32, + }, + #[cfg(all(kvm, target_arch = "x86_64"))] + #[error("MSR batch short count: expected {expected}, applied {actual}")] + MsrShortCount { + /// Number of MSRs requested + expected: usize, + /// Number of MSRs actually applied before KVM stopped + actual: usize, + }, #[cfg(target_os = "windows")] #[error("Failed to get xsave size: {0}")] GetXsaveSize(#[from] HypervisorError), @@ -359,6 +431,13 @@ pub(crate) trait VirtualMachine: Debug + Send { #[allow(dead_code)] fn set_debug_regs(&self, drs: &CommonDebugRegs) -> std::result::Result<(), RegisterError>; + /// Reads the requested MSRs. + #[cfg(target_arch = "x86_64")] + fn msrs(&self, indices: &[u32]) -> std::result::Result; + /// Writes the supplied MSRs. + #[cfg(target_arch = "x86_64")] + fn set_msrs(&self, msrs: &CommonMsrs) -> std::result::Result<(), RegisterError>; + /// Get xsave #[allow(dead_code)] #[cfg(not(target_arch = "aarch64"))] diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs index 1fd50d29a..0134177b5 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs @@ -31,8 +31,10 @@ use mshv_bindings::{ hv_message_type_HVMSG_X64_HALT, hv_message_type_HVMSG_X64_IO_PORT_INTERCEPT, hv_partition_property_code_HV_PARTITION_PROPERTY_SYNTHETIC_PROC_FEATURES, hv_partition_synthetic_processor_features, hv_register_assoc, - hv_register_name_HV_X64_REGISTER_RIP, hv_register_value, mshv_create_partition_v2, - mshv_user_mem_region, + hv_register_name_HV_X64_REGISTER_MSR_MTRR_PHYS_BASE0, + hv_register_name_HV_X64_REGISTER_MSR_MTRR_PHYS_MASK0, hv_register_name_HV_X64_REGISTER_RIP, + hv_register_name_HV_X64_REGISTER_U_XSS, hv_register_value, mshv_create_partition_v2, + mshv_user_mem_region, msr_to_hv_reg_name as mshv_msr_to_hv_reg_name, }; #[cfg(feature = "hw-interrupts")] use mshv_bindings::{ @@ -49,8 +51,10 @@ use tracing_opentelemetry::OpenTelemetrySpanExt; #[cfg(gdb)] use crate::hypervisor::gdb::{DebugError, DebuggableVm}; use crate::hypervisor::regs::{ - CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, FP_CONTROL_WORD_DEFAULT, - MXCSR_DEFAULT, + CommonDebugRegs, CommonFpu, CommonMsrs, CommonRegisters, CommonSpecialRegisters, + FP_CONTROL_WORD_DEFAULT, MSR_APERF, MSR_MPERF, MSR_MTRR_CAP, MSR_TSX_CTRL, MSR_XFD, + MSR_XFD_ERR, MSR_XSS, MXCSR_DEFAULT, MsrEntry, MsrKind, classify_msr, + hyperv_mtrr_reset_indices, }; #[cfg(test)] use crate::hypervisor::virtual_machine::XSAVE_BUFFER_SIZE; @@ -77,6 +81,68 @@ pub(crate) fn is_hypervisor_present() -> bool { } } +/// Maps an MSR index to the Hyper-V register used for host reset. +fn msr_to_hv_register_name(index: u32) -> Result { + const HV_X64_REGISTER_MCOUNT: u32 = 0x0008_007E; + const HV_X64_REGISTER_A_COUNT: u32 = 0x0008_007F; + const HV_X64_REGISTER_TSX_CTRL: u32 = 0x0008_0088; + const HV_X64_REGISTER_XFD: u32 = 0x0008_0099; + const HV_X64_REGISTER_XFD_ERR: u32 = 0x0008_009A; + + if (0x200..=0x21F).contains(&index) { + let pair = (index - 0x200) / 2; + return Ok(if index & 1 == 0 { + hv_register_name_HV_X64_REGISTER_MSR_MTRR_PHYS_BASE0 + pair + } else { + hv_register_name_HV_X64_REGISTER_MSR_MTRR_PHYS_MASK0 + pair + }); + } + if index == MSR_XSS { + return Ok(hv_register_name_HV_X64_REGISTER_U_XSS); + } + if index == MSR_MPERF { + return Ok(HV_X64_REGISTER_MCOUNT); + } + if index == MSR_APERF { + return Ok(HV_X64_REGISTER_A_COUNT); + } + if index == MSR_TSX_CTRL { + return Ok(HV_X64_REGISTER_TSX_CTRL); + } + if index == MSR_XFD { + return Ok(HV_X64_REGISTER_XFD); + } + if index == MSR_XFD_ERR { + return Ok(HV_X64_REGISTER_XFD_ERR); + } + mshv_msr_to_hv_reg_name(index) +} + +pub(crate) fn host_supports_msr(index: u32) -> bool { + msr_to_hv_register_name(index).is_ok() +} + +#[cfg(test)] +mod msr_mapping_tests { + use super::*; + use crate::hypervisor::regs::core_reset_indices; + + #[test] + fn maps_all_stateful_msrs() { + for index in core_reset_indices() { + assert!( + host_supports_msr(index), + "missing MSR mapping for {index:#x}" + ); + } + assert_eq!(msr_to_hv_register_name(MSR_MPERF), Ok(0x0008_007E)); + assert_eq!(msr_to_hv_register_name(MSR_APERF), Ok(0x0008_007F)); + assert_eq!(msr_to_hv_register_name(MSR_TSX_CTRL), Ok(0x0008_0088)); + assert_eq!(msr_to_hv_register_name(MSR_XFD), Ok(0x0008_0099)); + assert_eq!(msr_to_hv_register_name(MSR_XFD_ERR), Ok(0x0008_009A)); + } +} + /// A MSHV implementation of a single-vcpu VM #[derive(Debug)] pub(crate) struct MshvVm { @@ -102,13 +168,13 @@ impl MshvVm { let mshv = MSHV.as_ref().map_err(|e| e.clone())?; #[allow(unused_mut)] - let mut pr: mshv_create_partition_v2 = Default::default(); + let mut pr = mshv_create_partition_v2::default(); // Enable LAPIC for hw-interrupts — required for interrupt delivery // via request_virtual_interrupt. #[cfg(feature = "hw-interrupts")] { use mshv_bindings::MSHV_PT_BIT_LAPIC; - pr.pt_flags = 1u64 << MSHV_PT_BIT_LAPIC; + pr.pt_flags |= 1u64 << MSHV_PT_BIT_LAPIC; } // It's important to use create_vm_with_args() (not create_vm()), // because create_vm() sets up a SynIC partition by default. @@ -151,6 +217,40 @@ impl MshvVm { timer: None, }) } + + /// Validates that each allowed MSR has restorable Hyper-V state. + pub(crate) fn validate_allowed_msrs( + &self, + allowed: &[u32], + ) -> std::result::Result<(), CreateVmError> { + for &msr in allowed { + if !matches!(classify_msr(msr), Some(MsrKind::Stateful)) { + return Err(CreateVmError::MsrNotAllowable { + msr, + reason: "MSR is not an explicitly classified stateful MSR".to_string(), + }); + } + if !host_supports_msr(msr) { + return Err(CreateVmError::MsrNotAllowable { + msr, + reason: "MSR has no hv_register_name mapping on this host".to_string(), + }); + } + } + Ok(()) + } + + /// Returns every guest-visible MTRR required for restore. + pub(crate) fn mtrr_reset_indices(&self) -> std::result::Result, CreateVmError> { + let mtrr_cap = self + .msrs(&[MSR_MTRR_CAP]) + .map_err(CreateVmError::GetMtrrCap)?[0] + .value; + let indices = hyperv_mtrr_reset_indices(mtrr_cap)?; + self.msrs(&indices) + .map_err(CreateVmError::RequiredMtrrsNotResettable)?; + Ok(indices) + } } impl VirtualMachine for MshvVm { @@ -432,6 +532,57 @@ impl VirtualMachine for MshvVm { Ok(()) } + fn msrs(&self, indices: &[u32]) -> std::result::Result { + if indices.is_empty() { + return Ok(Vec::new()); + } + let mut registers: Vec = indices + .iter() + .map(|&index| { + Ok(hv_register_assoc { + name: msr_to_hv_register_name(index) + .map_err(|_| RegisterError::MsrsUnsupported)?, + ..Default::default() + }) + }) + .collect::>()?; + self.vcpu_fd + .get_reg(&mut registers) + .map_err(|e| RegisterError::GetMsrs(e.into()))?; + Ok(registers + .iter() + .zip(indices) + .map(|(register, &index)| MsrEntry { + index, + // SAFETY: get_reg initialized each association as a 64-bit register. + value: unsafe { register.value.reg64 }, + kind: classify_msr(index).unwrap_or(MsrKind::Stateful), + }) + .collect()) + } + + fn set_msrs(&self, msrs: &CommonMsrs) -> std::result::Result<(), RegisterError> { + let registers: Vec = msrs + .iter() + .filter(|entry| entry.kind == MsrKind::Stateful) + .map(|entry| { + Ok(hv_register_assoc { + name: msr_to_hv_register_name(entry.index) + .map_err(|_| RegisterError::MsrsUnsupported)?, + value: hv_register_value { reg64: entry.value }, + ..Default::default() + }) + }) + .collect::>()?; + if registers.is_empty() { + return Ok(()); + } + self.vcpu_fd + .set_reg(®isters) + .map_err(|e| RegisterError::SetMsrs(e.into()))?; + Ok(()) + } + #[allow(dead_code)] fn xsave(&self) -> std::result::Result, RegisterError> { let xsave = self diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs index 6de2b29f1..293141d3f 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs @@ -32,10 +32,14 @@ use windows_result::HRESULT; #[cfg(gdb)] use crate::hypervisor::gdb::{DebugError, DebuggableVm}; use crate::hypervisor::regs::{ - Align16, CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, - FP_CONTROL_WORD_DEFAULT, MXCSR_DEFAULT, WHP_DEBUG_REGS_NAMES, WHP_DEBUG_REGS_NAMES_LEN, + Align16, CommonDebugRegs, CommonFpu, CommonMsrs, CommonRegisters, CommonSpecialRegisters, + FP_CONTROL_WORD_DEFAULT, MSR_APERF, MSR_CSTAR, MSR_INTERRUPT_SSP_TABLE_ADDR, + MSR_KERNEL_GS_BASE, MSR_LSTAR, MSR_MPERF, MSR_MTRR_CAP, MSR_PAT, MSR_PL0_SSP, MSR_PL1_SSP, + MSR_PL2_SSP, MSR_PL3_SSP, MSR_S_CET, MSR_SFMASK, MSR_SPEC_CTRL, MSR_STAR, MSR_SYSENTER_CS, + MSR_SYSENTER_EIP, MSR_SYSENTER_ESP, MSR_TSX_CTRL, MSR_U_CET, MSR_XFD, MSR_XFD_ERR, + MXCSR_DEFAULT, MsrEntry, MsrKind, WHP_DEBUG_REGS_NAMES, WHP_DEBUG_REGS_NAMES_LEN, WHP_FPU_NAMES, WHP_FPU_NAMES_LEN, WHP_REGS_NAMES, WHP_REGS_NAMES_LEN, WHP_SREGS_NAMES, - WHP_SREGS_NAMES_LEN, + WHP_SREGS_NAMES_LEN, classify_msr, hyperv_mtrr_reset_indices, }; use crate::hypervisor::surrogate_process::SurrogateProcess; use crate::hypervisor::surrogate_process_manager::{ @@ -73,6 +77,129 @@ pub(crate) fn is_hypervisor_present() -> bool { } } +/// Maps an MSR index to the WHP register used for host reset. +fn msr_to_whv_register_name(index: u32) -> Option { + Some(match index { + MSR_MTRR_CAP => WHvX64RegisterMsrMtrrCap, + MSR_SYSENTER_CS => WHvX64RegisterSysenterCs, + MSR_SYSENTER_ESP => WHvX64RegisterSysenterEsp, + MSR_SYSENTER_EIP => WHvX64RegisterSysenterEip, + MSR_PAT => WHvX64RegisterPat, + MSR_STAR => WHvX64RegisterStar, + MSR_LSTAR => WHvX64RegisterLstar, + MSR_CSTAR => WHvX64RegisterCstar, + MSR_SFMASK => WHvX64RegisterSfmask, + MSR_KERNEL_GS_BASE => WHvX64RegisterKernelGsBase, + MSR_SPEC_CTRL => WHvX64RegisterSpecCtrl, + MSR_U_CET => WHvX64RegisterUCet, + MSR_S_CET => WHvX64RegisterSCet, + MSR_PL0_SSP => WHvX64RegisterPl0Ssp, + MSR_PL1_SSP => WHvX64RegisterPl1Ssp, + MSR_PL2_SSP => WHvX64RegisterPl2Ssp, + MSR_PL3_SSP => WHvX64RegisterPl3Ssp, + MSR_INTERRUPT_SSP_TABLE_ADDR => WHvX64RegisterInterruptSspTableAddr, + // TSC and TSC offset/aux. + 0x10 => WHvX64RegisterTsc, + 0x3B => WHvX64RegisterTscAdjust, + 0xC000_0103 => WHvX64RegisterTscAux, + MSR_MPERF => WHvX64RegisterMCount, + MSR_APERF => WHvX64RegisterACount, + MSR_TSX_CTRL => WHvX64RegisterTsxCtrl, + MSR_XFD => WHvX64RegisterXfd, + MSR_XFD_ERR => WHvX64RegisterXfdErr, + // XSAVE supervisor state mask (IA32_XSS). + 0xDA0 => WHvX64RegisterXss, + // MTRRs: def type, variable base/mask pairs 0..=9, fixed ranges. + 0x2FF => WHvX64RegisterMsrMtrrDefType, + 0x200 => WHvX64RegisterMsrMtrrPhysBase0, + 0x201 => WHvX64RegisterMsrMtrrPhysMask0, + 0x202 => WHvX64RegisterMsrMtrrPhysBase1, + 0x203 => WHvX64RegisterMsrMtrrPhysMask1, + 0x204 => WHvX64RegisterMsrMtrrPhysBase2, + 0x205 => WHvX64RegisterMsrMtrrPhysMask2, + 0x206 => WHvX64RegisterMsrMtrrPhysBase3, + 0x207 => WHvX64RegisterMsrMtrrPhysMask3, + 0x208 => WHvX64RegisterMsrMtrrPhysBase4, + 0x209 => WHvX64RegisterMsrMtrrPhysMask4, + 0x20A => WHvX64RegisterMsrMtrrPhysBase5, + 0x20B => WHvX64RegisterMsrMtrrPhysMask5, + 0x20C => WHvX64RegisterMsrMtrrPhysBase6, + 0x20D => WHvX64RegisterMsrMtrrPhysMask6, + 0x20E => WHvX64RegisterMsrMtrrPhysBase7, + 0x20F => WHvX64RegisterMsrMtrrPhysMask7, + 0x210 => WHvX64RegisterMsrMtrrPhysBase8, + 0x211 => WHvX64RegisterMsrMtrrPhysMask8, + 0x212 => WHvX64RegisterMsrMtrrPhysBase9, + 0x213 => WHvX64RegisterMsrMtrrPhysMask9, + 0x214 => WHvX64RegisterMsrMtrrPhysBaseA, + 0x215 => WHvX64RegisterMsrMtrrPhysMaskA, + 0x216 => WHvX64RegisterMsrMtrrPhysBaseB, + 0x217 => WHvX64RegisterMsrMtrrPhysMaskB, + 0x218 => WHvX64RegisterMsrMtrrPhysBaseC, + 0x219 => WHvX64RegisterMsrMtrrPhysMaskC, + 0x21A => WHvX64RegisterMsrMtrrPhysBaseD, + 0x21B => WHvX64RegisterMsrMtrrPhysMaskD, + 0x21C => WHvX64RegisterMsrMtrrPhysBaseE, + 0x21D => WHvX64RegisterMsrMtrrPhysMaskE, + 0x21E => WHvX64RegisterMsrMtrrPhysBaseF, + 0x21F => WHvX64RegisterMsrMtrrPhysMaskF, + 0x250 => WHvX64RegisterMsrMtrrFix64k00000, + 0x258 => WHvX64RegisterMsrMtrrFix16k80000, + 0x259 => WHvX64RegisterMsrMtrrFix16kA0000, + 0x268 => WHvX64RegisterMsrMtrrFix4kC0000, + 0x269 => WHvX64RegisterMsrMtrrFix4kC8000, + 0x26A => WHvX64RegisterMsrMtrrFix4kD0000, + 0x26B => WHvX64RegisterMsrMtrrFix4kD8000, + 0x26C => WHvX64RegisterMsrMtrrFix4kE0000, + 0x26D => WHvX64RegisterMsrMtrrFix4kE8000, + 0x26E => WHvX64RegisterMsrMtrrFix4kF0000, + 0x26F => WHvX64RegisterMsrMtrrFix4kF8000, + _ => return None, + }) +} + +/// Returns whether WHP exposes a host register for the MSR. +pub(crate) fn host_supports_msr(index: u32) -> bool { + msr_to_whv_register_name(index).is_some() +} + +#[cfg(test)] +mod msr_mapping_tests { + use super::*; + use crate::hypervisor::regs::{MSR_DEBUGCTL, core_reset_indices}; + + #[test] + fn maps_all_stateful_msrs_except_debugctl() { + for index in core_reset_indices() { + if index != MSR_DEBUGCTL { + assert!( + host_supports_msr(index), + "missing MSR mapping for {index:#x}" + ); + } + } + + assert!(!host_supports_msr(MSR_DEBUGCTL)); + assert_eq!( + msr_to_whv_register_name(MSR_MPERF), + Some(WHvX64RegisterMCount) + ); + assert_eq!( + msr_to_whv_register_name(MSR_APERF), + Some(WHvX64RegisterACount) + ); + assert_eq!( + msr_to_whv_register_name(MSR_TSX_CTRL), + Some(WHvX64RegisterTsxCtrl) + ); + assert_eq!(msr_to_whv_register_name(MSR_XFD), Some(WHvX64RegisterXfd)); + assert_eq!( + msr_to_whv_register_name(MSR_XFD_ERR), + Some(WHvX64RegisterXfdErr) + ); + } +} + /// Helper: release a host-side file mapping view and its handle. /// Called from both `unmap_memory` and `WhpVm::drop`. fn release_file_mapping(view_base: *mut c_void, mapping_handle: HandleWrapper) { @@ -177,6 +304,9 @@ impl WhpVm { #[cfg(feature = "hw-interrupts")] Self::enable_lapic_emulation(p)?; + // Hyper-V permits MSR intercepts only for unimplemented indices. + // Implemented MSR isolation therefore depends on reset. + WHvSetupPartition(p).map_err(|e| CreateVmError::InitializeVm(e.into()))?; WHvCreateVirtualProcessor(p, 0, 0) .map_err(|e| CreateVmError::CreateVcpuFd(e.into()))?; @@ -226,6 +356,40 @@ impl WhpVm { ) } } + + /// Validates that each allowed MSR has restorable WHP state. + pub(crate) fn validate_allowed_msrs( + &self, + allowed: &[u32], + ) -> std::result::Result<(), CreateVmError> { + for &msr in allowed { + if !matches!(classify_msr(msr), Some(MsrKind::Stateful)) { + return Err(CreateVmError::MsrNotAllowable { + msr, + reason: "MSR is not an explicitly classified stateful MSR".to_string(), + }); + } + if !host_supports_msr(msr) { + return Err(CreateVmError::MsrNotAllowable { + msr, + reason: "MSR has no WHP register mapping on this host".to_string(), + }); + } + } + Ok(()) + } + + /// Returns every guest-visible MTRR required for restore. + pub(crate) fn mtrr_reset_indices(&self) -> std::result::Result, CreateVmError> { + let mtrr_cap = self + .msrs(&[MSR_MTRR_CAP]) + .map_err(CreateVmError::GetMtrrCap)?[0] + .value; + let indices = hyperv_mtrr_reset_indices(mtrr_cap)?; + self.msrs(&indices) + .map_err(CreateVmError::RequiredMtrrsNotResettable)?; + Ok(indices) + } } impl VirtualMachine for WhpVm { @@ -456,6 +620,18 @@ impl VirtualMachine for WhpVm { } return Ok(VmExit::Halt()); } + WHvRunVpExitReasonX64MsrAccess => { + // Preserve violation handling for future WHP intercept support. + let ctx = unsafe { exit_context.Anonymous.MsrAccess }; + let msr_index = ctx.MsrNumber; + let is_write = unsafe { ctx.AccessInfo.Anonymous._bitfield } & 1 != 0; + if is_write { + // WRMSR value is EDX:EAX. + let value = ((ctx.Rdx as u32 as u64) << 32) | (ctx.Rax as u32 as u64); + return Ok(VmExit::MsrWrite { msr_index, value }); + } + return Ok(VmExit::MsrRead(msr_index)); + } WHvRunVpExitReasonMemoryAccess => { let gpa = unsafe { exit_context.Anonymous.MemoryAccess.Gpa }; let access_info = unsafe { @@ -667,6 +843,60 @@ impl VirtualMachine for WhpVm { } } + fn msrs(&self, indices: &[u32]) -> std::result::Result { + if indices.is_empty() { + return Ok(Vec::new()); + } + // Callers validate every index before reaching this mapping. + let names: Vec = indices + .iter() + .map(|&i| msr_to_whv_register_name(i).ok_or(RegisterError::MsrsUnsupported)) + .collect::>()?; + let mut values: Vec> = + vec![unsafe { std::mem::zeroed() }; names.len()]; + unsafe { + WHvGetVirtualProcessorRegisters( + self.partition, + 0, + names.as_ptr(), + names.len() as u32, + values.as_mut_ptr() as *mut WHV_REGISTER_VALUE, + ) + .map_err(|e| RegisterError::GetMsrs(e.into()))?; + } + Ok(indices + .iter() + .zip(values) + .map(|(&index, v)| { + let kind = classify_msr(index).unwrap_or(MsrKind::Stateful); + MsrEntry { + index, + value: unsafe { v.0.Reg64 }, + kind, + } + }) + .collect()) + } + + fn set_msrs(&self, msrs: &CommonMsrs) -> std::result::Result<(), RegisterError> { + // Stateless commands must never be replayed. + let regs: Vec<(WHV_REGISTER_NAME, Align16)> = msrs + .iter() + .filter(|e| e.kind == MsrKind::Stateful) + .map(|e| { + msr_to_whv_register_name(e.index) + .map(|name| (name, Align16(WHV_REGISTER_VALUE { Reg64: e.value }))) + .ok_or(RegisterError::MsrsUnsupported) + }) + .collect::>()?; + if regs.is_empty() { + return Ok(()); + } + self.set_registers(®s) + .map_err(|e| RegisterError::SetMsrs(e.into()))?; + Ok(()) + } + fn debug_regs(&self) -> std::result::Result { let mut whp_debug_regs_values: [Align16; WHP_DEBUG_REGS_NAMES_LEN] = Default::default(); diff --git a/src/hyperlight_host/src/sandbox/config.rs b/src/hyperlight_host/src/sandbox/config.rs index f12387a0b..0fd059202 100644 --- a/src/hyperlight_host/src/sandbox/config.rs +++ b/src/hyperlight_host/src/sandbox/config.rs @@ -29,6 +29,18 @@ pub struct DebugInfo { pub port: u16, } +/// Errors returned when configuring guest MSR access. +#[cfg(target_arch = "x86_64")] +#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)] +pub enum AllowMsrError { + /// The requested allow list exceeds its fixed capacity. + #[error("MSR allow list exceeds its maximum of {maximum} distinct entries")] + CapacityExceeded { + /// Maximum number of distinct allowed MSRs. + maximum: usize, + }, +} + /// The complete set of configuration needed to create a Sandbox #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(C)] @@ -74,6 +86,12 @@ pub struct SandboxConfiguration { interrupt_vcpu_sigrtmin_offset: u8, /// How much writable memory to offer the guest scratch_size: usize, + /// Requested MSR policy stored inline to keep this type `Copy`. + #[cfg(target_arch = "x86_64")] + allowed_msrs: [u32; Self::MAX_ALLOWED_MSRS], + /// Number of valid entries in `allowed_msrs`. + #[cfg(target_arch = "x86_64")] + allowed_msrs_count: usize, } impl SandboxConfiguration { @@ -93,6 +111,9 @@ impl SandboxConfiguration { pub const DEFAULT_HEAP_SIZE: u64 = 131072; /// The default size of the scratch region pub const DEFAULT_SCRATCH_SIZE: usize = 0x48000; + /// Maximum number of distinct requested MSRs. + #[cfg(target_arch = "x86_64")] + pub const MAX_ALLOWED_MSRS: usize = 64; #[allow(clippy::too_many_arguments)] /// Create a new configuration for a sandbox with the given sizes. @@ -118,6 +139,10 @@ impl SandboxConfiguration { guest_debug_info, #[cfg(crashdump)] guest_core_dump, + #[cfg(target_arch = "x86_64")] + allowed_msrs: [0; Self::MAX_ALLOWED_MSRS], + #[cfg(target_arch = "x86_64")] + allowed_msrs_count: 0, } } @@ -159,6 +184,66 @@ impl SandboxConfiguration { self.interrupt_vcpu_sigrtmin_offset } + /// Requests an MSR for the sandbox policy. + /// VM creation verifies that the backend can restore it. + /// + /// # Errors + /// + /// Returns [`AllowMsrError::CapacityExceeded`] if adding the index would + /// exceed [`Self::MAX_ALLOWED_MSRS`] distinct entries. + #[cfg(target_arch = "x86_64")] + #[instrument(skip_all, parent = Span::current(), level= "Trace")] + pub fn allow_msr(&mut self, index: u32) -> Result<&mut Self, AllowMsrError> { + if self.allowed_msrs[..self.allowed_msrs_count].contains(&index) { + return Ok(self); + } + if self.allowed_msrs_count == Self::MAX_ALLOWED_MSRS { + return Err(AllowMsrError::CapacityExceeded { + maximum: Self::MAX_ALLOWED_MSRS, + }); + } + self.allowed_msrs[self.allowed_msrs_count] = index; + self.allowed_msrs_count += 1; + Ok(self) + } + + /// Requests MSRs for the sandbox policy. + /// VM creation verifies that the backend can restore them. + /// + /// # Errors + /// + /// Returns [`AllowMsrError::CapacityExceeded`] if the distinct entries + /// would exceed [`Self::MAX_ALLOWED_MSRS`]. The policy is unchanged on + /// error. + #[cfg(target_arch = "x86_64")] + #[instrument(skip_all, parent = Span::current(), level= "Trace")] + pub fn allow_msrs(&mut self, indices: &[u32]) -> Result<&mut Self, AllowMsrError> { + let additional = indices + .iter() + .enumerate() + .filter(|(position, index)| { + !self.allowed_msrs[..self.allowed_msrs_count].contains(index) + && !indices[..*position].contains(index) + }) + .count(); + if additional > Self::MAX_ALLOWED_MSRS - self.allowed_msrs_count { + return Err(AllowMsrError::CapacityExceeded { + maximum: Self::MAX_ALLOWED_MSRS, + }); + } + for &index in indices { + self.allow_msr(index)?; + } + Ok(self) + } + + /// Returns the requested MSR policy. + #[cfg(target_arch = "x86_64")] + #[instrument(skip_all, parent = Span::current(), level= "Trace")] + pub(crate) fn get_allowed_msrs(&self) -> &[u32] { + &self.allowed_msrs[..self.allowed_msrs_count] + } + /// Sets the offset from `SIGRTMIN` to determine the real-time signal used for /// interrupting the VCPU thread. /// @@ -261,7 +346,38 @@ impl Default for SandboxConfiguration { #[cfg(test)] mod tests { - use super::SandboxConfiguration; + use super::{AllowMsrError, SandboxConfiguration}; + + #[test] + #[cfg(target_arch = "x86_64")] + fn msr_allow_list_reports_overflow() { + let mut cfg = SandboxConfiguration::default(); + for index in 0..SandboxConfiguration::MAX_ALLOWED_MSRS as u32 { + cfg.allow_msr(index).unwrap(); + } + + cfg.allow_msr(0).unwrap(); + assert_eq!( + cfg.allow_msr(SandboxConfiguration::MAX_ALLOWED_MSRS as u32), + Err(AllowMsrError::CapacityExceeded { + maximum: SandboxConfiguration::MAX_ALLOWED_MSRS, + }) + ); + } + + #[test] + #[cfg(target_arch = "x86_64")] + fn bulk_msr_allow_list_overflow_is_atomic() { + let mut cfg = SandboxConfiguration::default(); + cfg.allow_msrs(&[1, 2]).unwrap(); + let oversized: Vec = (3..=SandboxConfiguration::MAX_ALLOWED_MSRS as u32 + 1).collect(); + + assert!(matches!( + cfg.allow_msrs(&oversized), + Err(AllowMsrError::CapacityExceeded { .. }) + )); + assert_eq!(cfg.get_allowed_msrs(), &[1, 2]); + } #[test] fn overrides() { diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index 6f9e87e17..c303afa64 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -304,6 +304,14 @@ impl MultiUseSandbox { crate::hypervisor::hyperlight_vm::HyperlightVmError::Restore(e.into()), ) })?; + + // Restore captured MSR state. + #[cfg(target_arch = "x86_64")] + vm.restore_msrs(snapshot.msrs()).map_err(|e| { + crate::HyperlightError::HyperlightVmError( + crate::hypervisor::hyperlight_vm::HyperlightVmError::Restore(e), + ) + })?; } #[cfg(gdb)] @@ -385,6 +393,11 @@ impl MultiUseSandbox { .vm .get_snapshot_sregs() .map_err(|e| HyperlightError::HyperlightVmError(e.into()))?; + #[cfg(target_arch = "x86_64")] + let msrs = self + .vm + .get_msr_reset_state() + .map_err(|e| HyperlightError::HyperlightVmError(e.into()))?; let entrypoint = self.vm.get_entrypoint(); let host_functions = (&*self.host_funcs.try_lock().map_err(|e| { crate::new_error!("Error locking host_funcs at {}:{}: {}", file!(), line!(), e) @@ -399,6 +412,10 @@ impl MultiUseSandbox { entrypoint, host_functions, )?; + #[cfg_attr(not(target_arch = "x86_64"), allow(unused_mut))] + let mut memory_snapshot = memory_snapshot; + #[cfg(target_arch = "x86_64")] + memory_snapshot.set_msrs(msrs); let snapshot = Arc::new(memory_snapshot); self.snapshot = Some(snapshot.clone()); Ok(snapshot) @@ -543,6 +560,13 @@ impl MultiUseSandbox { HyperlightVmError::Restore(e) })?; + // Restore captured MSR state. + #[cfg(target_arch = "x86_64")] + self.vm.restore_msrs(snapshot.msrs()).map_err(|e| { + self.poisoned = true; + HyperlightVmError::Restore(e) + })?; + self.vm.set_stack_top(snapshot.stack_top_gva()); self.vm.set_entrypoint(snapshot.entrypoint()); @@ -2720,6 +2744,1275 @@ mod tests { ); } + #[cfg(target_arch = "x86_64")] + mod msr_tests { + use super::*; + use crate::HostFunctions; + use crate::hypervisor::hyperlight_vm::{CreateHyperlightVmError, HyperlightVmError}; + use crate::hypervisor::virtual_machine::{ + CreateVmError, RegisterError, ResetVcpuError, VmError, + }; + use crate::sandbox::snapshot::Snapshot; + + const KERNEL_GS_BASE: u32 = 0xC000_0102; + const SYSENTER_CS: u32 = 0x174; + + fn assert_msr_not_allowable(error: &HyperlightError, expected: u32) { + assert!( + matches!( + error, + HyperlightError::HyperlightVmError(HyperlightVmError::Create( + CreateHyperlightVmError::Vm(VmError::CreateVm( + CreateVmError::MsrNotAllowable { msr, .. } + )) + )) if *msr == expected + ), + "expected MsrNotAllowable for {expected:#x}, got: {error:?}" + ); + } + + fn assert_invalid_snapshot_msr(error: &HyperlightError) { + assert!( + matches!( + error, + HyperlightError::HyperlightVmError(HyperlightVmError::Restore( + ResetVcpuError::Register(RegisterError::InvalidSnapshotMsrIndex { .. }) + )) + ), + "expected InvalidSnapshotMsrIndex, got: {error:?}" + ); + } + + #[test] + fn kernel_gs_base_does_not_leak_through_swapgs() { + let mut sandbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap() + .evolve() + .unwrap(); + + let cpuid: Vec = sandbox.call("ReadCPUID", (7u32, 0u32)).unwrap(); + let ebx = u32::from_le_bytes(cpuid[4..8].try_into().unwrap()); + if ebx & 1 == 0 { + return; + } + + let original: u64 = sandbox.call("ReadKernelGsBaseViaSwapgs", ()).unwrap(); + let sentinel = if original == 0x0000_7AAA_5555_AAAA { + 0x0000_6BBB_4444_BBBB + } else { + 0x0000_7AAA_5555_AAAA + }; + let snapshot = sandbox.snapshot().unwrap(); + + sandbox + .call::<()>("WriteKernelGsBaseViaSwapgs", sentinel) + .unwrap(); + assert_eq!( + sandbox + .call::("ReadKernelGsBaseViaSwapgs", ()) + .unwrap(), + sentinel + ); + + sandbox.restore(snapshot).unwrap(); + assert_eq!( + sandbox + .call::("ReadKernelGsBaseViaSwapgs", ()) + .unwrap(), + original, + "KERNEL_GS_BASE leaked across restore" + ); + } + + #[test] + fn snapshot_msr_values_survive_full_in_memory_lifecycle() { + let mut config = SandboxConfiguration::default(); + config.allow_msr(KERNEL_GS_BASE).unwrap(); + let mut source = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + Some(config), + ) + .unwrap() + .evolve() + .unwrap(); + let first = 0x1111; + let second = 0x2222; + let third = 0x3333; + + source + .call::<()>("WriteMSR", (KERNEL_GS_BASE, first)) + .unwrap(); + assert_eq!( + source.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + first + ); + let first_snapshot = source.snapshot().unwrap(); + + source + .call::<()>("WriteMSR", (KERNEL_GS_BASE, second)) + .unwrap(); + assert_eq!( + source.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + second + ); + source.restore(first_snapshot.clone()).unwrap(); + assert_eq!( + source.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + first + ); + + let mut clone = MultiUseSandbox::from_snapshot( + first_snapshot.clone(), + HostFunctions::default(), + Some(config), + ) + .unwrap(); + assert_eq!(clone.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), first); + + clone + .call::<()>("WriteMSR", (KERNEL_GS_BASE, third)) + .unwrap(); + assert_eq!(clone.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), third); + let third_snapshot = clone.snapshot().unwrap(); + source.restore(third_snapshot.clone()).unwrap(); + assert_eq!( + source.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + third + ); + + let mut second_clone = MultiUseSandbox::from_snapshot( + third_snapshot, + HostFunctions::default(), + Some(config), + ) + .unwrap(); + assert_eq!( + second_clone.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + third + ); + second_clone.restore(first_snapshot).unwrap(); + assert_eq!( + second_clone.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + first + ); + } + + #[test] + fn equivalent_msr_configs_are_order_independent_across_sandboxes() { + let source_order = [KERNEL_GS_BASE, SYSENTER_CS]; + let target_order = [SYSENTER_CS, KERNEL_GS_BASE]; + let mut source_config = SandboxConfiguration::default(); + source_config.allow_msrs(&source_order).unwrap(); + let mut source = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + Some(source_config), + ) + .unwrap() + .evolve() + .unwrap(); + source + .call::<()>("WriteMSR", (KERNEL_GS_BASE, 0x4444u64)) + .unwrap(); + assert_eq!( + source.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + 0x4444 + ); + source + .call::<()>("WriteMSR", (SYSENTER_CS, 0x5555u64)) + .unwrap(); + assert_eq!(source.call::("ReadMSR", SYSENTER_CS).unwrap(), 0x5555); + let snapshot = source.snapshot().unwrap(); + + let mut target_config = SandboxConfiguration::default(); + target_config.allow_msrs(&target_order).unwrap(); + let mut target = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + Some(target_config), + ) + .unwrap() + .evolve() + .unwrap(); + target + .call::<()>("WriteMSR", (KERNEL_GS_BASE, 0xAAAAu64)) + .unwrap(); + assert_eq!( + target.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + 0xAAAA + ); + target + .call::<()>("WriteMSR", (SYSENTER_CS, 0xBBBBu64)) + .unwrap(); + assert_eq!(target.call::("ReadMSR", SYSENTER_CS).unwrap(), 0xBBBB); + target.restore(snapshot.clone()).unwrap(); + assert_eq!( + target.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + 0x4444 + ); + assert_eq!(target.call::("ReadMSR", SYSENTER_CS).unwrap(), 0x5555); + + let mut clone = MultiUseSandbox::from_snapshot( + snapshot, + HostFunctions::default(), + Some(target_config), + ) + .unwrap(); + assert_eq!( + clone.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + 0x4444 + ); + assert_eq!(clone.call::("ReadMSR", SYSENTER_CS).unwrap(), 0x5555); + } + + #[test] + fn from_pre_init_snapshot_uses_local_msr_policy() { + let mut config = SandboxConfiguration::default(); + config.allow_msr(KERNEL_GS_BASE).unwrap(); + let snapshot = Arc::new( + Snapshot::from_env( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + config, + ) + .unwrap(), + ); + assert!(snapshot.msrs().is_none()); + + let mut sandbox = MultiUseSandbox::from_snapshot( + snapshot.clone(), + HostFunctions::default(), + Some(config), + ) + .unwrap(); + let baseline: u64 = sandbox.call("ReadMSR", KERNEL_GS_BASE).unwrap(); + sandbox + .call::<()>("WriteMSR", (KERNEL_GS_BASE, baseline ^ 0x55)) + .unwrap(); + assert_eq!( + sandbox.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + baseline ^ 0x55 + ); + } + + #[test] + #[cfg(kvm)] + fn running_snapshot_msr_set_must_match_destination_policy() { + use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor}; + + if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) { + return; + } + + let mut source_config = SandboxConfiguration::default(); + source_config.allow_msr(KERNEL_GS_BASE).unwrap(); + let mut source = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + Some(source_config), + ) + .unwrap() + .evolve() + .unwrap(); + source + .call::<()>("WriteMSR", (KERNEL_GS_BASE, 0x1234u64)) + .unwrap(); + assert_eq!( + source.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + 0x1234 + ); + let snapshot = source.snapshot().unwrap(); + + for destination_indices in [&[SYSENTER_CS][..], &[KERNEL_GS_BASE, SYSENTER_CS][..]] { + let mut config = SandboxConfiguration::default(); + config.allow_msrs(destination_indices).unwrap(); + let err = MultiUseSandbox::from_snapshot( + snapshot.clone(), + HostFunctions::default(), + Some(config), + ) + .expect_err("from_snapshot must reject a different local MSR policy"); + assert!( + format!("{err:?}").contains("InvalidSnapshotMsrCount"), + "expected an MSR count mismatch, got: {err:?}" + ); + + let mut target = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + Some(config), + ) + .unwrap() + .evolve() + .unwrap(); + let err = target + .restore(snapshot.clone()) + .expect_err("restore must reject a different local MSR policy"); + assert!( + format!("{err:?}").contains("InvalidSnapshotMsrCount"), + "expected an MSR count mismatch, got: {err:?}" + ); + assert!(target.poisoned()); + assert!(matches!( + target.call::("Echo", "hi".to_string()), + Err(HyperlightError::PoisonedSandbox) + )); + } + } + + #[test] + #[cfg(kvm)] + fn snapshot_without_msrs_uses_destination_policy() { + use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor}; + + if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) { + return; + } + + let mut source = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap() + .evolve() + .unwrap(); + let snapshot = source.snapshot().unwrap(); + source.snapshot = None; + let Ok(mut snap) = Arc::try_unwrap(snapshot) else { + panic!("snapshot should be uniquely owned"); + }; + // A snapshot without MSRs uses the destination baseline. + snap.set_msrs(None); + let snapshot = Arc::new(snap); + + let mut config = SandboxConfiguration::default(); + config.allow_msr(KERNEL_GS_BASE).unwrap(); + let mut target = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + Some(config), + ) + .unwrap() + .evolve() + .unwrap(); + let baseline: u64 = target.call("ReadMSR", KERNEL_GS_BASE).unwrap(); + target + .call::<()>("WriteMSR", (KERNEL_GS_BASE, baseline ^ 0x55)) + .unwrap(); + assert_eq!( + target.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + baseline ^ 0x55 + ); + target.restore(snapshot.clone()).unwrap(); + assert_eq!( + target.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + baseline + ); + target + .call::<()>("WriteMSR", (KERNEL_GS_BASE, baseline ^ 0xAA)) + .unwrap(); + assert_eq!( + target.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + baseline ^ 0xAA + ); + + let mut clone = + MultiUseSandbox::from_snapshot(snapshot, HostFunctions::default(), Some(config)) + .unwrap(); + let clone_baseline: u64 = clone.call("ReadMSR", KERNEL_GS_BASE).unwrap(); + clone + .call::<()>("WriteMSR", (KERNEL_GS_BASE, clone_baseline ^ 0xCC)) + .unwrap(); + assert_eq!( + clone.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + clone_baseline ^ 0xCC + ); + } + + #[test] + fn malformed_snapshot_msrs_poison_and_trusted_restore_recovers() { + let indices = [SYSENTER_CS, KERNEL_GS_BASE]; + let mut config = SandboxConfiguration::default(); + config.allow_msrs(&indices).unwrap(); + let mut source = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + Some(config), + ) + .unwrap() + .evolve() + .unwrap(); + let snapshot = source.snapshot().unwrap(); + source.snapshot = None; + let Ok(mut snapshot) = Arc::try_unwrap(snapshot) else { + panic!("snapshot should be uniquely owned"); + }; + let mut msrs = snapshot.msrs().unwrap().clone(); + msrs[0].index = 0xDEAD; + snapshot.set_msrs(Some(msrs)); + let snapshot = Arc::new(snapshot); + + let error = MultiUseSandbox::from_snapshot( + snapshot.clone(), + HostFunctions::default(), + Some(config), + ) + .expect_err("from_snapshot must reject malformed snapshot MSRs"); + assert_invalid_snapshot_msr(&error); + + let mut target = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + Some(config), + ) + .unwrap() + .evolve() + .unwrap(); + let trusted_value: u64 = target.call("ReadMSR", KERNEL_GS_BASE).unwrap(); + let recovery_snapshot = target.snapshot().unwrap(); + target + .call::<()>("WriteMSR", (KERNEL_GS_BASE, trusted_value ^ 0x55)) + .unwrap(); + let error = target + .restore(snapshot) + .expect_err("restore must reject malformed snapshot MSRs"); + assert_invalid_snapshot_msr(&error); + assert!(target.poisoned()); + assert!(matches!( + target.call::("Echo", "hi".to_string()), + Err(HyperlightError::PoisonedSandbox) + )); + + target.restore(recovery_snapshot).unwrap(); + assert!(!target.poisoned()); + assert_eq!( + target.call::("ReadMSR", KERNEL_GS_BASE).unwrap(), + trusted_value + ); + } + + #[test] + #[cfg(kvm)] + fn denied_msr_access_poisons_sandbox() { + use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor}; + + match get_available_hypervisor() { + Some(HypervisorType::Kvm) => {} + _ => { + return; + } + } + + let mut sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap() + .evolve() + .unwrap(); + + let snapshot = sbox.snapshot().unwrap(); + let msr_index: u32 = 0xC000_0102; // IA32_KERNEL_GS_BASE + + let result = sbox.call::("ReadMSR", msr_index); + assert!( + matches!( + &result, + Err(HyperlightError::MsrReadViolation(idx)) if *idx == msr_index + ), + "RDMSR 0x{:X}: expected MsrReadViolation, got: {:?}", + msr_index, + result + ); + assert!(sbox.poisoned()); + + sbox.restore(snapshot.clone()).unwrap(); + + let result = sbox.call::<()>("WriteMSR", (msr_index, 0x5u64)); + assert!( + matches!( + &result, + Err(HyperlightError::MsrWriteViolation(idx, _)) if *idx == msr_index + ), + "WRMSR 0x{:X}: expected MsrWriteViolation, got: {:?}", + msr_index, + result + ); + assert!(sbox.poisoned()); + } + + /// A write-only command cannot enter the reset policy. + #[test] + #[cfg(target_arch = "x86_64")] + fn test_allow_non_resettable_msr_fails_creation() { + let mut cfg = SandboxConfiguration::default(); + cfg.allow_msr(0x49).unwrap(); // IA32_PRED_CMD, a write-only command MSR + + let err = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + Some(cfg), + ) + .unwrap() + .evolve() + .unwrap_err(); + + assert_msr_not_allowable(&err, 0x49); + } + + /// Host support cannot authorize an unclassified MSR. + #[test] + #[cfg(kvm)] + fn unclassified_allowed_msr_rejected_at_creation() { + use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor}; + + if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) { + return; + } + + let mut cfg = SandboxConfiguration::default(); + cfg.allow_msr(0x1A0).unwrap(); // IA32_MISC_ENABLE: host-probeable, not in MSR_TABLE + + let err = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + Some(cfg), + ) + .unwrap() + .evolve() + .expect_err("an unclassified allowed MSR must be rejected at creation"); + + assert_msr_not_allowable(&err, 0x1A0); + } + + #[test] + #[cfg(all(kvm, target_arch = "x86_64"))] + fn test_multiple_allowed_msrs_reset_across_restore() { + use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor}; + + match get_available_hypervisor() { + Some(HypervisorType::Kvm) => {} + _ => { + return; + } + } + + // These indices create two filter ranges and accept arbitrary values. + let msrs: [u32; 4] = [0x174, 0x175, 0x176, 0xC000_0102]; + let mut cfg = SandboxConfiguration::default(); + cfg.allow_msrs(&msrs).unwrap(); + + let mut sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + Some(cfg), + ) + .unwrap() + .evolve() + .unwrap(); + + let baseline_snapshot = sbox.snapshot().unwrap(); + + let value: u64 = 0x1000; + for &msr in &msrs { + sbox.call::<()>("WriteMSR", (msr, value)).unwrap(); + let read_value: u64 = sbox.call("ReadMSR", msr).unwrap(); + assert_eq!(read_value, value, "MSR 0x{msr:X} should be writable"); + } + + sbox.restore(baseline_snapshot).unwrap(); + for &msr in &msrs { + let read_value: u64 = sbox.call("ReadMSR", msr).unwrap(); + assert_ne!( + read_value, value, + "MSR 0x{msr:X} should be reset to baseline across restore" + ); + } + } + + /// An allowed guest write must not survive restore. + #[test] + #[cfg(target_arch = "x86_64")] + fn test_allowed_msr_does_not_leak_across_restore() { + let msr_index: u32 = 0xC000_0102; // IA32_KERNEL_GS_BASE + let sentinel: u64 = 0xCAFE_F00D; + + let mut cfg = SandboxConfiguration::default(); + cfg.allow_msr(msr_index).unwrap(); + let mut sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + Some(cfg), + ) + .unwrap() + .evolve() + .unwrap(); + + let baseline = sbox.snapshot().unwrap(); + let original: u64 = sbox.call("ReadMSR", msr_index).unwrap(); + assert_ne!( + original, sentinel, + "test sentinel must differ from the baseline value" + ); + + sbox.call::<()>("WriteMSR", (msr_index, sentinel)).unwrap(); + assert_eq!( + sbox.call::("ReadMSR", msr_index).unwrap(), + sentinel, + "sentinel should be observable before restore" + ); + sbox.restore(baseline).unwrap(); + + let after: u64 = sbox.call("ReadMSR", msr_index).unwrap(); + assert_ne!(after, sentinel, "sentinel leaked across restore"); + assert_eq!(after, original, "MSR not reset to its baseline value"); + } + + /// KVM denies DEBUGCTL through its filter and x2APIC through xAPIC mode. + #[test] + #[cfg(all(kvm, target_arch = "x86_64"))] + fn test_debugctl_and_x2apic_msr_denied_by_default() { + use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor}; + + if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) { + return; + } + + let cases: [(u32, bool); 2] = [(0x1D9, true), (0x800, false)]; + for (msr_index, expect_filter_violation) in cases { + let mut sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap() + .evolve() + .unwrap(); + + let result = sbox.call::<()>("WriteMSR", (msr_index, 0x1u64)); + if expect_filter_violation { + assert!( + matches!( + &result, + Err(HyperlightError::MsrWriteViolation(idx, _)) if *idx == msr_index + ), + "WRMSR 0x{msr_index:X}: expected MsrWriteViolation, got: {result:?}" + ); + } else { + assert!( + matches!(&result, Err(HyperlightError::GuestAborted(_, _))), + "WRMSR 0x{msr_index:X}: expected direct #GP, got: {result:?}" + ); + } + assert!( + sbox.poisoned(), + "sandbox should be poisoned after a denied WRMSR to 0x{msr_index:X}" + ); + } + } + + /// A partial KVM MSR restore poisons the sandbox. + #[test] + #[cfg(all(kvm, target_arch = "x86_64"))] + fn test_partial_set_msrs_poisons_sandbox() { + use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor}; + + if !matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) { + return; + } + + let mut cfg = SandboxConfiguration::default(); + cfg.allow_msr(0xC000_0102).unwrap(); + let mut sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + Some(cfg), + ) + .unwrap() + .evolve() + .unwrap(); + + let baseline = sbox.snapshot().unwrap(); + sbox.snapshot = None; + let Ok(mut snap) = Arc::try_unwrap(baseline) else { + panic!("snapshot should be uniquely owned after clearing the cache"); + }; + // KVM rejects this noncanonical KERNEL_GS_BASE value. + let mut msrs = snap.msrs().unwrap().clone(); + let kernel_gs_base = msrs + .iter_mut() + .find(|entry| entry.index == 0xC000_0102) + .expect("allowed MSR should be in the captured reset set"); + kernel_gs_base.value = 0xDEAD_0000_0000_0000; + snap.set_msrs(Some(msrs)); + let snap = Arc::new(snap); + + let err = sbox + .restore(snap) + .expect_err("restore should fail on short SET_MSRS"); + assert!( + format!("{err:?}").to_lowercase().contains("msr") + || format!("{err:?}").to_lowercase().contains("restore"), + "expected an MSR restore error, got: {err:?}" + ); + assert!( + sbox.poisoned(), + "sandbox should be poisoned after failed restore" + ); + + let call = sbox.call::("Echo", "hi".to_string()); + assert!( + matches!(call, Err(HyperlightError::PoisonedSandbox)), + "poisoned sandbox should reject guest calls, got: {call:?}" + ); + } + + /// Unresettable PMU, FRED, and LBR state must remain unavailable. + #[test] + #[cfg(target_arch = "x86_64")] + fn unresettable_msr_classes_do_not_leak() { + let cases: &[(u32, &str)] = &[ + (0xC1, "PMU IA32_PMC0"), + (0x186, "PMU IA32_PERFEVTSEL0"), + (0x38F, "PMU IA32_PERF_GLOBAL_CTRL"), + (0x1C8, "LBR_SELECT"), + (0x14CE, "arch-LBR IA32_LBR_CTL"), + (0x1D4, "FRED IA32_FRED_CONFIG"), + ]; + + let mut sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap() + .evolve() + .unwrap(); + + #[cfg(kvm)] + let filterless_backend = !matches!( + crate::hypervisor::virtual_machine::get_available_hypervisor(), + Some(crate::hypervisor::virtual_machine::HypervisorType::Kvm) + ); + #[cfg(not(kvm))] + let filterless_backend = true; + if filterless_backend { + let leaf7 = guest_cpuid(&mut sbox, 7, 0); + let leaf7_1 = guest_cpuid(&mut sbox, 7, 1); + let leaf_a = guest_cpuid(&mut sbox, 0xA, 0); + assert_eq!(leaf_a[0] & 0xFF, 0, "guest PMU must be disabled"); + assert_eq!( + leaf7[3] & (1 << 19), + 0, + "guest architectural LBR must be disabled" + ); + assert_eq!(leaf7_1[0] & (1 << 17), 0, "guest FRED must be disabled"); + } + + for &(msr, _name) in cases { + assert_msr_write_does_not_survive_restore(&mut sbox, msr, 0x1); + } + } + + /// A rejected MSHV MSR restore poisons the sandbox. + #[test] + #[cfg(all(mshv3, target_arch = "x86_64"))] + fn test_mshv_rejected_msr_restore_poisons_sandbox() { + use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor}; + + if !matches!(get_available_hypervisor(), Some(HypervisorType::Mshv)) { + return; + } + + let mut sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap() + .evolve() + .unwrap(); + + let baseline = sbox.snapshot().unwrap(); + sbox.snapshot = None; + let Ok(mut snap) = Arc::try_unwrap(baseline) else { + panic!("snapshot should be uniquely owned after clearing the cache"); + }; + // MSHV rejects this noncanonical KERNEL_GS_BASE value. + let mut msrs = snap.msrs().unwrap().clone(); + let kernel_gs_base = msrs + .iter_mut() + .find(|entry| entry.index == 0xC000_0102) + .expect("KERNEL_GS_BASE should be in the MSHV reset set"); + kernel_gs_base.value = 0xDEAD_0000_0000_0000; + snap.set_msrs(Some(msrs)); + let snap = Arc::new(snap); + + let err = sbox + .restore(snap) + .expect_err("restore should fail on a rejected set_msrs"); + assert!( + format!("{err:?}").to_lowercase().contains("msr") + || format!("{err:?}").to_lowercase().contains("restore"), + "expected an MSR restore error, got: {err:?}" + ); + assert!( + sbox.poisoned(), + "sandbox should be poisoned after failed restore" + ); + + let call = sbox.call::("Echo", "hi".to_string()); + assert!( + matches!(call, Err(HyperlightError::PoisonedSandbox)), + "poisoned sandbox should reject guest calls, got: {call:?}" + ); + } + + /// Every stateful table entry needs runtime reset coverage. + #[test] + #[cfg(target_arch = "x86_64")] + fn runtime_msr_table_entries_are_justified() { + use crate::hypervisor::regs::core_reset_indices; + + #[cfg(kvm)] + let kernel_gs_uses_instruction_side_effect = matches!( + crate::hypervisor::virtual_machine::get_available_hypervisor(), + Some(crate::hypervisor::virtual_machine::HypervisorType::Kvm) + ); + #[cfg(not(kvm))] + let kernel_gs_uses_instruction_side_effect = false; + + let mut sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap() + .evolve() + .unwrap(); + + let reset_indices: Vec = sbox + .snapshot() + .unwrap() + .msrs() + .expect("filterless backend should have an MSR reset set") + .iter() + .map(|entry| entry.index) + .collect(); + + for index in core_reset_indices() { + if !reset_indices.contains(&index) { + assert_omitted_msr_does_not_retain(&mut sbox, index); + } else if kernel_gs_uses_instruction_side_effect && index == KERNEL_GS_BASE { + // Direct WRMSR is denied. The dedicated SWAPGS test proves + // the instruction-side mutation is restored. + } else if (index == 0x10 && !kernel_gs_uses_instruction_side_effect) + || matches!(index, 0xE7 | 0xE8) + { + assert_guest_counter_is_writable_and_restored(&mut sbox, index); + } else if let Some(sentinel) = positive_write_sentinel(index) { + assert_guest_msr_is_writable_and_restored(&mut sbox, index, sentinel); + } else { + assert!( + reset_exception_reason(index).is_some(), + "MSR 0x{index:X} is in the reset set without positive guest-write coverage or an explicit reason" + ); + } + } + } + + fn assert_omitted_msr_does_not_retain(sbox: &mut MultiUseSandbox, index: u32) { + let baseline = sbox.snapshot().unwrap(); + let original: u64 = match sbox.call("ReadMSR", index) { + Ok(value) => value, + Err(_) => { + assert!(sbox.poisoned(), "0x{index:X}: fault did not poison sandbox"); + sbox.restore(baseline).unwrap(); + return; + } + }; + let preferred = positive_write_sentinel(index).unwrap_or(original ^ 1); + let candidates = [preferred, original ^ 1, original ^ 2, 0, 1, 0x1000]; + + for candidate in candidates { + if candidate == original { + continue; + } + if sbox.call::<()>("WriteMSR", (index, candidate)).is_err() { + assert!(sbox.poisoned(), "0x{index:X}: fault did not poison sandbox"); + sbox.restore(baseline.clone()).unwrap(); + continue; + } + let written: u64 = sbox.call("ReadMSR", index).unwrap_or_else(|error| { + panic!("0x{index:X}: read after successful write failed: {error:?}") + }); + if written != original { + sbox.restore(baseline).unwrap(); + let after: u64 = sbox.call("ReadMSR", index).unwrap(); + assert_eq!( + after, original, + "0x{index:X}: guest retained a write but the MSR is absent from the reset set" + ); + return; + } + sbox.restore(baseline.clone()).unwrap(); + } + } + + fn positive_write_sentinel(index: u32) -> Option { + match index { + 0x174 => Some(0x10), // SYSENTER_CS + 0x175 | 0x176 => Some(0x1000), // SYSENTER_ESP/EIP + 0x277 => Some(0x0007_0406_0007_0406), // PAT + 0xC000_0081 => Some(0x001B_0008_0000_0000), // STAR + 0xC000_0082 | 0xC000_0083 => Some(0x1000), // LSTAR/CSTAR + 0xC000_0084 => Some(0x200), // SFMASK + 0xC000_0102 => Some(0x1000), // KERNEL_GS_BASE + 0x3B => Some(0x1000), // TSC_ADJUST + 0xC000_0103 => Some(0x5), // TSC_AUX + 0x2FF => Some(0xC00), // MTRR_DEF_TYPE + 0x200..=0x21F if index & 1 == 0 => Some(0x6), // MTRR_PHYSBASEn + 0x200..=0x21F => Some(0x800), // MTRR_PHYSMASKn + 0x250 | 0x258 | 0x259 | 0x268..=0x26F => Some(0x0606_0606_0606_0606), + _ => None, + } + } + + fn reset_exception_reason(index: u32) -> Option<&'static str> { + match index { + 0x10 => Some("KVM denies direct guest TSC MSR access"), + 0x1D9 => Some("DEBUGCTL support depends on exposed debug features"), + 0x48 => Some("SPEC_CTRL writable bits depend on mitigation features"), + 0x6A0 | 0x6A2 | 0x6A4..=0x6A8 => { + Some("CET writable state depends on exposed CET features") + } + 0x122 => Some("TSX_CTRL writable bits depend on exposed TSX features"), + 0x1C4 | 0x1C5 => Some("XFD writable bits depend on exposed XSAVE features"), + 0xDA0 => Some("XSS writable bits depend on exposed XSAVE features"), + _ => None, + } + } + + fn assert_guest_msr_is_writable_and_restored( + sbox: &mut MultiUseSandbox, + index: u32, + sentinel: u64, + ) { + let baseline = sbox.snapshot().unwrap(); + let original: u64 = sbox + .call("ReadMSR", index) + .unwrap_or_else(|error| panic!("0x{index:X}: guest RDMSR failed: {error:?}")); + let value = if original == sentinel { 0 } else { sentinel }; + + sbox.call::<()>("WriteMSR", (index, value)) + .unwrap_or_else(|error| panic!("0x{index:X}: guest WRMSR failed: {error:?}")); + let written: u64 = sbox + .call("ReadMSR", index) + .unwrap_or_else(|error| panic!("0x{index:X}: guest read-back failed: {error:?}")); + assert_eq!(written, value, "0x{index:X}: guest write did not stick"); + + sbox.restore(baseline).unwrap(); + let restored: u64 = sbox.call("ReadMSR", index).unwrap(); + assert_eq!( + restored, original, + "0x{index:X}: restore did not recover the baseline" + ); + } + + fn assert_guest_counter_is_writable_and_restored(sbox: &mut MultiUseSandbox, index: u32) { + let baseline = sbox.snapshot().unwrap(); + let original: u64 = sbox.call("ReadMSR", index).unwrap(); + let jump = original.wrapping_add(1 << 60); + + sbox.call::<()>("WriteMSR", (index, jump)).unwrap(); + let written: u64 = sbox.call("ReadMSR", index).unwrap(); + assert!( + written >= jump / 2, + "0x{index:X}: guest write did not stick" + ); + + sbox.restore(baseline).unwrap(); + let restored: u64 = sbox.call("ReadMSR", index).unwrap(); + assert!( + restored < jump / 2, + "0x{index:X}: restore did not pull the counter below the guest-written jump" + ); + } + + /// Verifies that a guest MSR write faults or resets to its baseline. + #[cfg(target_arch = "x86_64")] + fn assert_msr_write_does_not_survive_restore( + sbox: &mut MultiUseSandbox, + msr: u32, + sentinel: u64, + ) { + let baseline = sbox.snapshot().unwrap(); + let original: u64 = match sbox.call("ReadMSR", msr) { + Ok(v) => v, + Err(_) => { + assert!( + sbox.poisoned(), + "0x{msr:X}: a faulting RDMSR should poison the sandbox" + ); + sbox.restore(baseline).unwrap(); + return; + } + }; + assert_ne!( + original, sentinel, + "0x{msr:X}: sentinel must differ from baseline" + ); + + if sbox.call::<()>("WriteMSR", (msr, sentinel)).is_err() { + assert!( + sbox.poisoned(), + "0x{msr:X}: a faulting WRMSR should poison the sandbox" + ); + sbox.restore(baseline).unwrap(); + return; + } + + sbox.restore(baseline).unwrap(); + let after: u64 = sbox.call("ReadMSR", msr).unwrap(); + assert_eq!( + after, original, + "0x{msr:X}: MSR leaked across restore (expected 0x{original:X}, got 0x{after:X})" + ); + } + + fn guest_cpuid(sbox: &mut MultiUseSandbox, leaf: u32, subleaf: u32) -> [u32; 4] { + let bytes: Vec = sbox.call("ReadCPUID", (leaf, subleaf)).unwrap(); + let mut registers = [0; 4]; + for (register, bytes) in registers.iter_mut().zip(bytes.chunks_exact(4)) { + *register = u32::from_le_bytes(bytes.try_into().unwrap()); + } + registers + } + + /// Omitted MSR features must not be advertised to the guest. + #[test] + #[cfg(target_arch = "x86_64")] + fn omitted_feature_msrs_are_not_advertised() { + let mut sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap() + .evolve() + .unwrap(); + + #[cfg(kvm)] + if matches!( + crate::hypervisor::virtual_machine::get_available_hypervisor(), + Some(crate::hypervisor::virtual_machine::HypervisorType::Kvm) + ) { + return; + } + let leaf7 = guest_cpuid(&mut sbox, 7, 0); + let hyperv_features = guest_cpuid(&mut sbox, 0x4000_0003, 0); + assert_eq!(leaf7[1] & (1 << 14), 0, "guest MPX must be disabled"); + assert_eq!(leaf7[2] & (1 << 5), 0, "guest WAITPKG must be disabled"); + assert_eq!( + hyperv_features[0], 0, + "guest synthetic Hyper-V MSR features must be disabled" + ); + } + + /// Audits Hyper-V MSR bitmap ranges for guest state retained by restore. + #[test] + #[ignore = "slow host-dependent hardware MSR audit"] + #[cfg(target_arch = "x86_64")] + fn test_no_msr_leaks_across_restore_full_window_sweep() { + // Free-running counters use a magnitude check after restore. + const FREE_RUNNING: &[u32] = &[ + 0x10, // IA32_TIME_STAMP_COUNTER + 0xE7, // IA32_MPERF + 0xE8, // IA32_APERF + ]; + + #[cfg(kvm)] + if matches!( + crate::hypervisor::virtual_machine::get_available_hypervisor(), + Some(crate::hypervisor::virtual_machine::HypervisorType::Kvm) + ) { + return; + } + + let mut sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap() + .evolve() + .unwrap(); + + let baseline = sbox.snapshot().unwrap(); + + // At least one retained write must exercise restore. + let mut readable = 0usize; + let mut exercised: Vec = Vec::new(); + let mut read_only: Vec = Vec::new(); + let mut masked_only: Vec = Vec::new(); + // Collect all free-running leaks for one diagnostic. + let mut free_running_leaked: Vec = Vec::new(); + + // Architectural and low model-specific indices. + let low = 0x0000_0000u32..=0x0000_1FFF; + // Hyper-V synthetic indices. + let hyperv_synthetic = 0x4000_0000u32..=0x4000_1FFF; + // Extended and AMD model-specific indices. + let extended = 0xC000_0000u32..=0xC001_FFFF; + let windows = low.chain(hyperv_synthetic).chain(extended); + for msr in windows { + let original: u64 = match sbox.call("ReadMSR", msr) { + Ok(v) => v, + Err(_) => { + sbox.restore(baseline.clone()).unwrap(); + continue; + } + }; + readable += 1; + + // A large jump distinguishes reset from normal counter progress. + if FREE_RUNNING.contains(&msr) { + let jump = original.wrapping_add(1 << 60); + if sbox.call::<()>("WriteMSR", (msr, jump)).is_err() { + sbox.restore(baseline.clone()).unwrap(); + read_only.push(msr); + continue; + } + let planted = match sbox.call::("ReadMSR", msr) { + Ok(v) => v, + Err(_) => { + sbox.restore(baseline.clone()).unwrap(); + masked_only.push(msr); + continue; + } + }; + if planted < jump / 2 { + sbox.restore(baseline.clone()).unwrap(); + masked_only.push(msr); + continue; + } + sbox.restore(baseline.clone()).unwrap(); + let after: u64 = sbox.call("ReadMSR", msr).unwrap(); + if after < jump / 2 { + exercised.push(msr); + } else { + free_running_leaked.push(msr); + } + continue; + } + + // Multiple candidates cover MSRs with restricted writable bits. + let candidates = [ + original ^ 0x55, + original ^ 0x1, + original ^ (1 << 12), + original ^ (1 << 20), + original ^ (1 << 32), + original.wrapping_add(1), + 0, + ]; + let mut planted = false; + let mut saw_write = false; + for cand in candidates { + if cand == original { + continue; + } + if sbox.call::<()>("WriteMSR", (msr, cand)).is_err() { + sbox.restore(baseline.clone()).unwrap(); + continue; + } + saw_write = true; + match sbox.call::("ReadMSR", msr) { + Ok(v) if v != original => { + planted = true; + break; + } + _ => { + sbox.restore(baseline.clone()).unwrap(); + } + } + } + + if planted { + sbox.restore(baseline.clone()).unwrap(); + match sbox.call::("ReadMSR", msr) { + Ok(after) => assert_eq!( + after, original, + "0x{msr:X}: a guest MSR write leaked across restore \ + (expected 0x{original:X}, got 0x{after:X})" + ), + Err(e) => panic!("0x{msr:X}: read-back after restore failed: {e:?}"), + } + exercised.push(msr); + } else if saw_write { + masked_only.push(msr); + } else { + read_only.push(msr); + } + } + + let fmt = |v: &[u32]| { + v.iter() + .map(|m| format!("0x{m:X}")) + .collect::>() + .join(", ") + }; + eprintln!( + "full-window MSR sweep: readable={readable} exercised={} masked_only={} read_only={}", + exercised.len(), + masked_only.len(), + read_only.len() + ); + eprintln!(" exercised: [{}]", fmt(&exercised)); + eprintln!(" masked_only: [{}]", fmt(&masked_only)); + eprintln!(" read_only: [{}]", fmt(&read_only)); + eprintln!(" free_running_leaked: [{}]", fmt(&free_running_leaked)); + assert!( + free_running_leaked.is_empty(), + "free-running MSRs not reset across restore on this backend: [{}]", + fmt(&free_running_leaked) + ); + assert!( + !exercised.is_empty(), + "sweep was vacuous: no guest MSR write ever retained a value that restore \ + then rolled back, so the rollback path was never exercised" + ); + } + + /// A host TSC write must reset the guest-visible MSHV counter. + #[test] + #[cfg(all(mshv3, target_arch = "x86_64"))] + fn mshv_host_tsc_writeback_resets_guest_tsc() { + use crate::hypervisor::virtual_machine::{HypervisorType, get_available_hypervisor}; + + if !matches!(get_available_hypervisor(), Some(HypervisorType::Mshv)) { + return; + } + + let mut sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap() + .evolve() + .unwrap(); + + let base = sbox.vm.capture_msrs_for_test(&[0x10]).unwrap()[0].value; + + let jump = base.wrapping_add(1 << 60); + sbox.call::<()>("WriteMSR", (0x10u32, jump)).unwrap(); + let planted: u64 = sbox.call("ReadMSR", 0x10u32).unwrap(); + assert!( + planted >= jump, + "guest TSC write did not take (planted=0x{planted:X} jump=0x{jump:X})" + ); + + assert!( + sbox.vm.try_set_msr_for_test(0x10, base), + "host set of HV_X64_REGISTER_TSC failed" + ); + + let after: u64 = sbox.call("ReadMSR", 0x10u32).unwrap(); + eprintln!( + "mshv TSC writeback probe: base=0x{base:X} jump=0x{jump:X} planted=0x{planted:X} after=0x{after:X}" + ); + assert!( + after < jump / 2, + "host TSC write-back did NOT reset the guest TSC (after=0x{after:X} still near \ + jump=0x{jump:X}); the reset approach is not viable on this host" + ); + } + } + /// Tests for [`MultiUseSandbox::from_snapshot`] in-memory. mod from_snapshot { use std::sync::Arc; diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs index 59fcd6372..0963c0d80 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs @@ -871,6 +871,8 @@ impl Snapshot { load_info: crate::mem::exe::LoadInfo::dummy(), stack_top_gva: cfg.stack_top_gva, sregs: Some(cfg.sregs), + #[cfg(target_arch = "x86_64")] + msrs: None, entrypoint, snapshot_generation, host_functions, diff --git a/src/hyperlight_host/src/sandbox/snapshot/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/mod.rs index 9a638a98e..5758c4064 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -31,6 +31,8 @@ use hyperlight_common::vmem::{ use tracing::{Span, instrument}; use crate::Result; +#[cfg(target_arch = "x86_64")] +use crate::hypervisor::regs::CommonMsrs; use crate::hypervisor::regs::CommonSpecialRegisters; use crate::mem::exe::{ExeInfo, LoadInfo}; use crate::mem::layout::SandboxMemoryLayout; @@ -92,6 +94,10 @@ pub struct Snapshot { /// tables are relocated during snapshot. sregs: Option, + /// MSR reset state captured from a running vCPU. + #[cfg(target_arch = "x86_64")] + msrs: Option, + /// The next action that should be performed on this snapshot entrypoint: NextAction, @@ -381,6 +387,8 @@ impl Snapshot { load_info, stack_top_gva: exn_stack_top_gva, sregs: None, + #[cfg(target_arch = "x86_64")] + msrs: None, entrypoint: NextAction::Initialise(load_addr + entrypoint_va - base_va), snapshot_generation: 0, host_functions: HostFunctionDetails { @@ -560,6 +568,8 @@ impl Snapshot { load_info, stack_top_gva, sregs: Some(sregs), + #[cfg(target_arch = "x86_64")] + msrs: None, entrypoint, snapshot_generation, host_functions, @@ -603,6 +613,18 @@ impl Snapshot { self.sregs.as_ref() } + /// Returns the captured MSR reset state. + #[cfg(target_arch = "x86_64")] + pub(crate) fn msrs(&self) -> Option<&CommonMsrs> { + self.msrs.as_ref() + } + + /// Stores captured MSR reset state. + #[cfg(target_arch = "x86_64")] + pub(crate) fn set_msrs(&mut self, msrs: Option) { + self.msrs = msrs; + } + pub(crate) fn entrypoint(&self) -> NextAction { self.entrypoint } diff --git a/src/tests/rust_guests/simpleguest/src/main.rs b/src/tests/rust_guests/simpleguest/src/main.rs index 2da2eb7ca..f7b83b8c5 100644 --- a/src/tests/rust_guests/simpleguest/src/main.rs +++ b/src/tests/rust_guests/simpleguest/src/main.rs @@ -1095,6 +1095,95 @@ fn call_host_expect_error(hostfuncname: String) -> Result<()> { Ok(()) } +#[guest_function("ReadMSR")] +#[cfg(target_arch = "x86_64")] +fn read_msr(msr: u32) -> u64 { + let (read_eax, read_edx): (u32, u32); + unsafe { + core::arch::asm!( + "rdmsr", + in("ecx") msr, + out("eax") read_eax, + out("edx") read_edx, + options(nostack, nomem) + ); + } + ((read_edx as u64) << 32) | (read_eax as u64) +} + +#[guest_function("ReadCPUID")] +#[cfg(target_arch = "x86_64")] +fn read_cpuid(leaf: u32, subleaf: u32) -> Vec { + let result = core::arch::x86_64::__cpuid_count(leaf, subleaf); + [result.eax, result.ebx, result.ecx, result.edx] + .into_iter() + .flat_map(u32::to_le_bytes) + .collect() +} + +#[guest_function("WriteKernelGsBaseViaSwapgs")] +#[cfg(target_arch = "x86_64")] +fn write_kernel_gs_base_via_swapgs(value: u64) { + // SAFETY: The caller checks CPUID.FSGSBASE. CR4 is restored before return. + unsafe { + core::arch::asm!( + "mov {original_cr4}, cr4", + "mov {enabled_cr4}, {original_cr4}", + "or {enabled_cr4}, {fsgsbase}", + "mov cr4, {enabled_cr4}", + "wrgsbase {value}", + "swapgs", + "mov cr4, {original_cr4}", + original_cr4 = out(reg) _, + enabled_cr4 = out(reg) _, + fsgsbase = const 1 << 16, + value = in(reg) value, + options(nostack) + ); + } +} + +#[guest_function("ReadKernelGsBaseViaSwapgs")] +#[cfg(target_arch = "x86_64")] +fn read_kernel_gs_base_via_swapgs() -> u64 { + let value: u64; + // SAFETY: The caller checks CPUID.FSGSBASE. GS base and CR4 are restored. + unsafe { + core::arch::asm!( + "mov {original_cr4}, cr4", + "mov {enabled_cr4}, {original_cr4}", + "or {enabled_cr4}, {fsgsbase}", + "mov cr4, {enabled_cr4}", + "swapgs", + "rdgsbase {value}", + "swapgs", + "mov cr4, {original_cr4}", + original_cr4 = out(reg) _, + enabled_cr4 = out(reg) _, + value = lateout(reg) value, + fsgsbase = const 1 << 16, + options(nostack) + ); + } + value +} + +#[guest_function("WriteMSR")] +#[cfg(target_arch = "x86_64")] +fn write_msr(msr: u32, value: u64) { + let eax = (value & 0xFFFFFFFF) as u32; + let edx = ((value >> 32) & 0xFFFFFFFF) as u32; + unsafe { + core::arch::asm!( + "wrmsr", + in("ecx") msr, + in("eax") eax, + in("edx") edx, + options(nostack, nomem) + ); + } +} + #[hyperlight_guest_bin::main] #[instrument(skip_all, parent = Span::current(), level= "Trace")] fn main() {