diff --git a/CHANGELOG.md b/CHANGELOG.md index c095f6de1..ea55d5a1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). * Guest libc accepts null buffers for zero-length writes. * Fix calls to guest functions with more than 11 parameters. Registration accepts up to 32, but every call with more than 11 failed. +* Fix restore on KVM not rewinding the guest TSC when restores are less than a second apart. ## [v0.18.0] - 2026-10-13 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 9bd348647..b691742a1 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 @@ -562,14 +562,27 @@ impl VirtualMachine for KvmVm { } fn set_msrs(&self, msrs: &[MsrEntry]) -> std::result::Result<(), RegisterError> { - let entries: Vec = msrs - .iter() - .map(|e| kvm_msr_entry { + let mut entries = Vec::with_capacity(msrs.len() + 1); + for e in msrs { + let mut value = e.value; + if e.index == MSR_TSC { + // KVM syncs a zero write instead of applying it. + value = value.max(1); + // KVM ignores a host TSC write within one second of the last write + // plus elapsed time. A write half the counter range away first makes + // the next one exact. + entries.push(kvm_msr_entry { + index: MSR_TSC, + data: value.wrapping_add(1 << 63), + ..Default::default() + }); + } + entries.push(kvm_msr_entry { index: e.index, - data: e.value, + data: value, ..Default::default() - }) - .collect(); + }); + } if entries.is_empty() { return Ok(()); } diff --git a/src/hyperlight_host/src/sandbox/initialized.rs b/src/hyperlight_host/src/sandbox/initialized.rs index dcc3fa0b5..097ae9834 100644 --- a/src/hyperlight_host/src/sandbox/initialized.rs +++ b/src/hyperlight_host/src/sandbox/initialized.rs @@ -4780,6 +4780,47 @@ mod tests { ); } + /// KVM ignores a host TSC write within one second of the value it + /// expects, so restores closer together than that exercise the KVM path. + /// The 500 ms gaps tolerate host stalls shorter than that. WHP is + /// excluded because it keeps elapsed time on hosts with `TSC_ADJUST`. + #[test] + #[cfg(any(kvm, mshv3))] + fn restore_rewinds_tsc() { + let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf()) + .build() + .unwrap(); + let snapshot = sbox.snapshot().unwrap(); + + for restore in 0..3 { + std::thread::sleep(std::time::Duration::from_millis(500)); + let before: u64 = sbox.call("ReadTsc", ()).unwrap(); + sbox.restore(snapshot.clone()).unwrap(); + let after: u64 = sbox.call("ReadTsc", ()).unwrap(); + assert!( + after < before, + "restore {restore} did not rewind TSC (before={before}, after={after})" + ); + } + } + + /// KVM syncs a host TSC write of zero to the current offset. + #[test] + #[cfg(kvm)] + fn restore_handles_zero_tsc() { + let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf()) + .build() + .unwrap(); + sbox.vm + .restore_msrs(&[crate::hypervisor::regs::MsrEntry { + index: MSR_TSC, + value: 0, + }]) + .unwrap(); + let tsc: u64 = sbox.call("ReadTsc", ()).unwrap(); + assert!(tsc < 1 << 62, "TSC jumped to {tsc:#x} after restoring zero"); + } + #[test] fn snapshot_msr_values_survive_full_in_memory_lifecycle() { let mut source = SandboxBuilder::from_file(simple_guest_as_pathbuf()) diff --git a/src/tests/rust_guests/simpleguest/src/main.rs b/src/tests/rust_guests/simpleguest/src/main.rs index fbf5df928..42b678eb3 100644 --- a/src/tests/rust_guests/simpleguest/src/main.rs +++ b/src/tests/rust_guests/simpleguest/src/main.rs @@ -1344,6 +1344,13 @@ fn call_host_expect_error(hostfuncname: String) -> Result<()> { Ok(()) } +#[guest_function("ReadTsc")] +#[cfg(target_arch = "x86_64")] +fn read_tsc() -> u64 { + // SAFETY: RDTSC reads the time-stamp counter and has no memory effects. + unsafe { core::arch::x86_64::_rdtsc() } +} + #[guest_function("ReadMSR")] #[cfg(target_arch = "x86_64")] fn read_msr(msr: u32) -> u64 {