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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -562,14 +562,27 @@ impl VirtualMachine for KvmVm {
}

fn set_msrs(&self, msrs: &[MsrEntry]) -> std::result::Result<(), RegisterError> {
let entries: Vec<kvm_msr_entry> = 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(());
}
Expand Down
41 changes: 41 additions & 0 deletions src/hyperlight_host/src/sandbox/initialized.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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})"
Comment thread
ludfjig marked this conversation as resolved.
Resolved
);
}
}

/// 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())
Expand Down
7 changes: 7 additions & 0 deletions src/tests/rust_guests/simpleguest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading