Skip to content

[DO NOT REVIEW] microVM: publish the host's clock correction with each time sample - #4809

Open
Pedro Henrique Penna (ppenna) wants to merge 6 commits into
feature/microvm/devfrom
copilot/microvm-time-abi-v2-host-clock
Open

Pedro Henrique Penna (ppenna) wants to merge 6 commits into
feature/microvm/devfrom
copilot/microvm-time-abi-v2-host-clock

Conversation

@ppenna

@ppenna Pedro Henrique Penna (ppenna) commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Moves every microVM to NVX time ABI 2, for microsoft/nvx#1009: OpenVMM pairs each time sample's host UTC with the guest TSC on the host and publishes with it the host's clock correction, which the guest applies instead of estimating over minutes how fast host UTC runs.

Based on feature/microvm/dev. This branch is six commits directly on bcfc47cff, the head of feature/microvm/dev since #4818 merged. It was rebased there from c32c57d12: git range-diff shows commits 1-3 and 5-6 unchanged; commit 4 also makes the CPU-check test backend that the newer base added implement the new guest_tsc_offset obligation. The second commit retains the earlier fix that reads Windows' precise time adjustment in the right direction (see Validation). microsoft/nvx#1018 pins this head.

Why

Under time ABI 1 the guest pairs host UTC with its own clock: it reads that clock around the port write that latches a time sample and accepts the pairing only within 1 ms at boot and restore, or 50 µs for its discipline. A port write that a loaded host serves late costs a sample, and three cost a boot or a restore. The guest then estimates how fast host UTC runs against its TSC by steering the kernel's phase-locked loop over minutes, although the host's time service already knows the correction that it applies to host UTC. Time ABI 2 has the VMM pair host UTC with the guest TSC, which does not depend on how fast the host serves the port write, and publish the host's correction.

Shared host TSC bracket (fc442fda6)

HostTscBracket, which reads the host TSC around an operation with LFENCE on both sides, cycles_to_ns_ceil, and pair_host_sample move from the KVM backend to virt::time_abi::tsc, since every backend now needs them. The KVM backend uses them unchanged.

Host UTC's discipline (60b80ecc2)

host_clock_discipline reads the host's correction from the host OS, whatever time service runs there, rather than from a hypervisor feature:

  • Linux: adjtimex with no modes: the frequency correction plus the tick's deviation from its nominal 10 ms, the status (STA_CLOCKERR is unreliable, STA_UNSYNC free-running, anything else synchronized), and the maximum error. The current clocksource names the clock's basis: the TSC at the kernel's kHz calibration, or at the hypervisor's rate through the Hyper-V TSC page.
  • Windows: GetSystemTimeAdjustmentPrecise. Its increment is the performance counter's frequency, and Windows counts a second of UTC for every adjustment's worth of the counter's ticks. UTC therefore runs at the increment over the adjustment of the counter's rate, and a larger adjustment slows it. An adjustment that Windows does not apply is a free-running clock, and a zero adjustment an unknown one.

host_suspended_ns reveals a host suspension: CLOCK_BOOTTIME minus CLOCK_MONOTONIC, and the interrupt time minus the unbiased interrupt time.

Publishing (732f827f0)

virt::time_abi::publish computes the host's fields of a time sample:

  • Host UTC paired with the guest TSC: the tightest of up to 16 pairings of a host time sample with the host TSC, plus the guest TSC's offset from the host TSC, and the pairing's bound, which adds the offset's uncertainty.
  • The correction against the guest TSC counted at the declared rate, in ppm scaled by 2^16: (1 + host correction) * F / F_d - 1. On a restore it includes the native rate's deviation from the declared one.
  • A bound on the correction's error: the rounding when the host's clock and the backend's native rate come from one calibration (the kernel's kHz rate for KVM, the hypervisor's rate for MSHV and WHP), one kHz of the native rate when they do not, and unknown when the host's clocksource may not follow the TSC.
  • The status and maximum error of host UTC, as its time service reports them.
  • A disruption counter, which changes on a host suspension, on a step of host UTC of 1 ms or more beyond what the correction explains, and on a change of the guest TSC's offset.

It adds the host-rate-offset-ppm test hook, which publishes host UTC as running that many ppm faster than it does, with the correction to match, so that tests can check that a guest follows a host clock whose rate differs from the TSC's. The publisher also applies utc-offset-ms, which the portb device applied until now.

The guest TSC's offset on every backend (dcd96dee4)

TimeAbiBackend::guest_tsc_offset runs at every start of the VM, with every VP bound and stopped, after the TSC writes of a cold boot, a restore, or a reset, and before any VP runs:

  • KVM returns VP 0's KVM_VCPU_TSC_OFFSET, which preflight verifies is the unscaled offset.
  • MSHV and WHP read VP 0's TSC between two host TSC reads, up to eight times, and keep the tightest reading. Both freeze partition time until a partition starts, so the measurement resumes it first, as the partition's start would right after.

Preflight also reports what each backend's native rate measures, which bounds the published correction's error.

Time ABI 2 (61374b068)

The time sample that writing 0xa7 to the portb status port latches becomes version 2, 48 bytes, little-endian:

Offset Size Field
0 1 Version, 2
1 1 Flags; bit 3: a test hook is active
2 1 Status of host UTC: 0 unknown, 1 synchronized, 2 free-running, 3 unreliable
3 1 Reserved, 0
4 4 Generation
8 8 Host UTC, ns
16 8 Guest TSC at the same instant
24 4 Pairing bound, ns
28 4 Disruption counter
32 8 Correction, signed, ppm × 2^16
40 4 Bound of the correction's error, ppm × 2^16; 0xffffffff unknown
44 4 Maximum error of host UTC, µs; 0xffffffff unknown

The portb device takes the host's fields from a platform resource, TimeSampleSourceKind, which the VM worker resolves to the time ABI's clock; the device keeps the generation, the test-hook flag, and the sample-delay-us hook. The partition unit now depends on the time-abi unit, so the offset's measurement follows the TSC writes of every start and precedes the VPs' release, including a resume after a failed capture. Until the first measurement, or when the host cannot pair a sample, the window stays empty and the guest rejects the sample.

The restore packet becomes version 5, which drops host UTC: the guest takes a time sample for its repair, so the packet no longer latches UTC at its first selection, and its header shrinks to 24 bytes.

CPUID leaf 0x40000002 reports time ABI 2.0 in EBX (0x00020000), and snapshots record time ABI version 2. A snapshot of time ABI 1 fails restore with E_MANIFEST_TIME and must be recaptured. The CPU time bits do not change, so every pinned CPU profile serves time ABI 2 as it is.

Petri's microVM configuration no longer sets the portb device's utc_offset_ms, which the publisher applies now, and the guest of the ttrpc microVM VMM test reads restore packet 5: version 5, and 17 header bytes after the range count instead of 25.

Guide (090fbab88)

The snapshots page describes restore packet 5 and the version-2 sample field by field, how OpenVMM pairs the guest TSC and where it reads the correction, that the samples discipline only the guest's wall clock and are no paravirtual clock, and that a snapshot of time ABI 1 must be recaptured. The CLI and gRPC references name restore packet version 5.

Tests

  • virt::time_abi::tsc: the cycle conversion, and the bracket's midpoint and its refusal of a TSC that went backwards.
  • virt::time_abi::host: the Windows adjustment's arithmetic, its direction with two Windows hosts' readings, and its rounding, that each host reports a status and a plausible correction, and the clock kinds.
  • virt::time_abi::publish: the correction's arithmetic against exact values, its bounds, the pairing of a sample, the disruptions of Linux and Windows clocks, the hooks, and a real host sample's pairing.
  • chipset_resources::microvm_time: the version-2 sample and the version-5 packet, encoded and decoded byte for byte.
  • chipset::microvm: the portb device latches the source's fields into its window, and leaves it empty without a sample.
  • openvmm_core: the time-abi unit's start measures the offset that samples use, and a changed offset is a disruption. A restore rejects a version-1 time contract.
  • Hardware tests: the MSHV test checks that the measurement resumes partition time and agrees with a direct read of VP 0's TSC, and a new WHP test checks that the offset holds while time runs (on a Windows 11 host, two measurements 500 ms apart differed by 87 cycles, within the 3,700 cycles of their uncertainty).

Validation

  • Windows, WHP: cargo clippy --all-targets -- -D warnings, cargo doc --no-deps, and cargo nextest run for the changed packages, the WHP time-ABI hardware tests, and cargo xtask fmt: clean.
  • Linux, KVM (an Azure VM): cargo nextest run for the changed packages (796 passed) and the KVM time-ABI hardware tests (5 passed).
  • Linux, MSHV (an Azure VM): the MSHV time-ABI hardware tests (5 passed) and cargo doc --no-deps. cargo clippy --all-targets -- -D warnings for every changed package found two useless_conversion errors in host.rs, which the second commit now fixes; clippy and virt's time_abi tests (66) then passed.
  • This pull request's first CI run failed CI's quick check on petri, which still set the portb device's removed utc_offset_ms. The fifth commit now drops it, and the quick check's clippy, cargo clippy --locked --keep-going --all-targets --workspace --all-features --target x86_64-unknown-linux-gnu --exclude guest_test_uefi --exclude opentmk_executor with -Dwarnings, passes on the Azure MSHV VM. Looking for other users of the changed formats, the ttrpc microVM VMM test's guest still parsed restore packet 4, so the fifth commit updates it too. cargo xflowey vmm-tests-run passed both microVM VMM tests, ttrpc::test_ttrpc_microvm_linux_direct_lifecycle_and_snapshot and x86_64::microvm::openvmm_linux_x64_phase_1_lifecycle, on the Azure KVM VM, and the ttrpc one failed without the update (RESTORE-PACKET-INVALID).
  • After the rebase onto bcfc47cff, at 090fbab88: git range-diff keeps commits 1-3 and 5-6 patch-equivalent and adds only the newer base's CpuBackend::guest_tsc_offset mock to commit 4. On Windows, package-scoped check, clippy with -D warnings, rustdoc, and 697 unit tests pass for every generic and WHP crate; 18 ignored WHP hardware tests and cargo xtask fmt --fix pass. On Linux/KVM, the generic and KVM crates pass check, clippy, rustdoc, 731 unit tests, 12 ignored hardware tests, and both affected VMM tests. On Linux/MSHV, virt_mshv passes check, clippy, rustdoc, 79 unit tests, and 13 ignored hardware tests.
  • The host readers on real hosts: adjtimex reports synchronized clocks on the Azure Linux test VMs and all ten Linux CI runners, at corrections from -76 to +7 ppm, and GetSystemTimeAdjustmentPrecise reports applied adjustments on an Azure Windows VM, on all ten Windows CI runners, from -72.5 to +6.9 ppm, and at +17.2 ppm on a bare-metal Windows host (read in the right direction; see below).
  • NVX: time ABI: publish the host's clock correction with each time sample nvx#1018 pins this head, moves NVX's guest to time ABI 2, and adds a microVM scenario, time-host-correction, which checks with the host-rate-offset-ppm hook that the guest follows a host clock that runs at another rate than the TSC. With this branch's OpenVMM, NVX's default microVM suite and the scenario passed on Linux/KVM, Linux/MSHV, and Windows/WHP Azure test VMs.
  • The Windows correction's direction. NVX's CI ran that scenario on a Windows runner whose time service ran host UTC 48.6 ppm slow against the guest TSC, while this OpenVMM published +48.7 ppm.
    • Cause: the second commit read GetSystemTimeAdjustmentPrecise as the legacy GetSystemTimeAdjustment, which adds its adjustment once per increment.
    • Evidence: a 20 s comparison of UTC with QueryPerformanceCounter showed that a larger adjustment slows UTC. On that runner, whose adjustment moved from 10,000,501 to 10,000,588 for an increment of 10,000,000, UTC ran 53.1 ppm slow. On the Windows test VM, at an adjustment of 10,000,005, UTC ran 0.447 ppm slow.
    • Fix: the amended second commit takes the increment over the adjustment, minus one, and its tests check that direction with these readings.
    • Validation at the head: virt's formatting, clippy with -Dwarnings, and its time ABI tests (66) pass on Windows and Linux.
    • End to end on the Windows/WHP VM: NVX's scenario published corrections within 0.04 to 0.12 ppm of host UTC's measured rate against the guest TSC; the old reading left gaps of 0.24 to 0.93 ppm. NVX's H4 read -0.600 ppm, where UTC measured -0.612 ppm against the counter.
  • CI at the previous head 75765b660 passed. Exact-head CI for 090fbab88 is https://github.com/microsoft/openvmm/actions/runs/41015566000. The earlier binary-size check needed its base commit's CI run as a baseline and passed on rerun; the size comparison found nothing. The Petri tests that the bot reports are OpenHCL UEFI Windows tests marked unstable, which timed out.

Notes

  • This is a breaking change of the time ABI: a guest of time ABI 1 fails its conformance check against this OpenVMM, and snapshots of time ABI 1 must be recaptured. NVX moves its guest to time ABI 2 in the commit that pins this head.
  • The samples discipline only the guest's wall clock. They are no paravirtual clock: the guest's clocksource stays its TSC.

@github-actions github-actions Bot added Guide unsafe Related to unsafe code labels Oct 29, 2026
@github-actions

Copy link
Copy Markdown

⚠️ Unsafe Code Detected

This PR modifies files containing unsafe Rust code. Extra scrutiny is required during review.

For more on why we check whole files, instead of just diffs, check out the Rustonomicon

Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 29, 2026
Record the six commits that the first commit of this series pins as
positions 360-365 of the microVM chain, and move the NVX pin to
b27be7e6a.

Positions 360-365 move NVX's time ABI to version 2: the shared host TSC
bracket, the host OS's discipline of host UTC, the publication of host
UTC paired with the guest TSC with the host's clock correction, the
guest TSC's offset on every backend, the version-2 time sample and
restore packet 5, and the Guide's description of them. They change the
time-ABI backends, the portb device's time sample, the restore packet,
and the microVM worker's time-abi unit, which positions 159-269 added,
so they follow those positions upstream; say so in the table's
remarks. The pin is now 208 commits past the mapped tip.

Positions 355-359 are no longer only a proposal:
microsoft/openvmm#4794 merged them, so position 359, bd2b27a2e, is the
head of feature/microvm/dev. Say so in the scope, and name
microsoft/openvmm#4809 as the proposal of positions 360-365.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 29, 2026
Record the six commits that the first commit of this series pins as
positions 360-365 of the microVM chain, and move the NVX pin to
b27be7e6a.

Positions 360-365 move NVX's time ABI to version 2: the shared host TSC
bracket, the host OS's discipline of host UTC, the publication of host
UTC paired with the guest TSC with the host's clock correction, the
guest TSC's offset on every backend, the version-2 time sample and
restore packet 5, and the Guide's description of them. They change the
time-ABI backends, the portb device's time sample, the restore packet,
and the microVM worker's time-abi unit, which positions 159-269 added,
so they follow those positions upstream; say so in the table's
remarks. The pin is now 208 commits past the mapped tip.

Positions 355-359 are no longer only a proposal:
microsoft/openvmm#4794 merged them, so position 359, bd2b27a2e, is the
head of feature/microvm/dev. Say so in the scope, and name
microsoft/openvmm#4809 as the proposal of positions 360-365.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the copilot/microvm-time-abi-v2-host-clock branch from b27be7e to 93b2c4d Compare October 29, 2026 19:51
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 29, 2026
Pin microsoft/openvmm#4809 at 93b2c4d913a04f33ec2031cbee9362aceb1a8199,
six commits on bd2b27a2e, the previous pin, which is the head of
feature/microvm/dev.

Under time ABI 1 the guest paired host UTC with its own clock: nvx-time
read CLOCK_REALTIME around the port write that latches a time sample and
accepted the pairing only within 1 ms at boot and restore, or 50 us for
its discipline, so a port write that a loaded host served late cost a
sample, and three cost a boot or a restore. It then estimated how fast
host UTC runs against its TSC by steering the kernel's PLL over minutes,
although the host's time service already knows that rate.

Time ABI 2 has OpenVMM pair each sample's host UTC with the guest TSC on
the host and publish with it the host's clock correction, which the host
OS reports (adjtimex on Linux, GetSystemTimeAdjustmentPrecise on
Windows), the bound of the correction's error, the status and maximum
error of host UTC, and a disruption counter, which changes on a host
suspension, a step of host UTC, or a change of the guest TSC's offset.
The version-2 sample is 48 bytes. CPUID leaf 0x40000002 reports time ABI
2.0 (EBX 0x00020000). Restore packet 5 drops host UTC, so its header
shrinks to 24 bytes, and the guest's repair takes a time sample instead.

The guest moves with the pin, since a guest of one time ABI fails the
other's conformance check:

- nvx::contract: TIME_ABI_VERSION 2 and RESTORE_PACKET_VERSION 5.
- Pairing: the guest reads its TSC before and after the selector write,
  and CLOCK_REALTIME between those reads, and accepts a sample only if
  its TSC lies within that bracket, widened by the published pairing
  bound and 1 us of skew. It compares host UTC with CLOCK_REALTIME at
  the sample's TSC, moving its CLOCK_REALTIME read there at the rate at
  which Linux counts it; the offset's uncertainty is the pairing bound,
  half the bracket, and 2,000 ppm of that move.
- Rate: from the host's correction and the TSC rate, which the guest
  reads once from the Hyper-V TSC frequency MSR (0x40000022) and keeps
  in its time state, the guest computes the adjtimex frequency that
  makes CLOCK_REALTIME, which Linux counts at its kHz rounding of the
  TSC rate, run at host UTC's rate. It clamps that frequency to Linux's
  500 ppm limit and reports whether it did.
- Discipline: boot steps the clock to host UTC and holds that frequency
  with STA_FREQHOLD, so that the kernel's PLL corrects only the phase.
  Every poll holds the latest correction and slews the measured offset,
  or steps it from 128 ms on. A changed disruption counter returns the
  daemon to its fast cadence. Boot, restore, and polls take up to three
  samples within 100 us; a boot that gets none fails its check, and a
  restore fails its repair. The maximum and estimated errors that the
  guest sets include host UTC's maximum error only when the host
  reports a synchronized clock; the guest never marks its clock
  unsynchronized on the host's behalf.
- Restore: the repair reads packet 5 and takes its time sample in the
  same port transaction, then steps the clock and holds the frequency.
- State: the time state records the host's status, correction, and
  correction bound, host UTC's maximum error, the disruption counter,
  and whether the frequency was limited. nvx-time sample prints a
  sample's fields and the guest's pairing, and the test hooks packet,
  sample, pair, frequency, and correction take the new fields.

Templates record time ABI version 2, so the manifest reader refuses a
template of time ABI 1, which must be recaptured. The fixtures'
manifests, which OpenVMM's generator regenerated at the pin, differ from
the previous ones only in that version. The template-key vectors keep
the contract versions that their test fixes. Comments, the nvx-rs
README, and nvx-snapshot name the new versions.

OpenVMM's protocol vectors and CPU profiles are unchanged at the pin.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 29, 2026
Record the six commits that the first commit of this series pins as
positions 360-365 of the microVM chain, and move the NVX pin to
93b2c4d91.

Positions 360-365 move NVX's time ABI to version 2: the shared host TSC
bracket, the host OS's discipline of host UTC, the publication of host
UTC paired with the guest TSC with the host's clock correction, the
guest TSC's offset on every backend, the version-2 time sample and
restore packet 5, and the Guide's description of them. They change the
time-ABI backends, the portb device's time sample, the restore packet,
and the microVM worker's time-abi unit, which positions 159-269 added,
so they follow those positions upstream; say so in the table's
remarks. The pin is now 208 commits past the mapped tip.

Positions 355-359 are no longer only a proposal:
microsoft/openvmm#4794 merged them, so position 359, bd2b27a2e, is the
head of feature/microvm/dev. Say so in the scope, and name
microsoft/openvmm#4809 as the proposal of positions 360-365.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 29, 2026
Pin microsoft/openvmm#4809 at 93b2c4d913a04f33ec2031cbee9362aceb1a8199,
six commits on bd2b27a2e, the previous pin, which is the head of
feature/microvm/dev.

Under time ABI 1 the guest paired host UTC with its own clock: nvx-time
read CLOCK_REALTIME around the port write that latches a time sample and
accepted the pairing only within 1 ms at boot and restore, or 50 us for
its discipline, so a port write that a loaded host served late cost a
sample, and three cost a boot or a restore. It then estimated how fast
host UTC runs against its TSC by steering the kernel's PLL over minutes,
although the host's time service already knows that rate.

Time ABI 2 has OpenVMM pair each sample's host UTC with the guest TSC on
the host and publish with it the host's clock correction, which the host
OS reports (adjtimex on Linux, GetSystemTimeAdjustmentPrecise on
Windows), the bound of the correction's error, the status and maximum
error of host UTC, and a disruption counter, which changes on a host
suspension, a step of host UTC, or a change of the guest TSC's offset.
The version-2 sample is 48 bytes. CPUID leaf 0x40000002 reports time ABI
2.0 (EBX 0x00020000). Restore packet 5 drops host UTC, so its header
shrinks to 24 bytes, and the guest's repair takes a time sample instead.

The guest moves with the pin, since a guest of one time ABI fails the
other's conformance check:

- nvx::contract: TIME_ABI_VERSION 2 and RESTORE_PACKET_VERSION 5.
- Pairing: the guest reads its TSC before and after the selector write,
  and CLOCK_REALTIME between those reads, and accepts a sample only if
  its TSC lies within that bracket, widened by the published pairing
  bound and 1 us of skew. It compares host UTC with CLOCK_REALTIME at
  the sample's TSC, moving its CLOCK_REALTIME read there at the rate at
  which Linux counts it; the offset's uncertainty is the pairing bound,
  half the bracket, and 2,000 ppm of that move.
- Rate: from the host's correction and the TSC rate, which the guest
  reads once from the Hyper-V TSC frequency MSR (0x40000022) and keeps
  in its time state, the guest computes the adjtimex frequency that
  makes CLOCK_REALTIME, which Linux counts at its kHz rounding of the
  TSC rate, run at host UTC's rate. It clamps that frequency to Linux's
  500 ppm limit and reports whether it did.
- Discipline: boot steps the clock to host UTC and holds that frequency
  with STA_FREQHOLD, so that the kernel's PLL corrects only the phase.
  Every poll holds the latest correction and slews the measured offset,
  or steps it from 128 ms on. A changed disruption counter returns the
  daemon to its fast cadence. Boot, restore, and polls take up to three
  samples within 100 us; a boot that gets none fails its check, and a
  restore fails its repair. The maximum and estimated errors that the
  guest sets include host UTC's maximum error only when the host
  reports a synchronized clock; the guest never marks its clock
  unsynchronized on the host's behalf.
- Restore: the repair reads packet 5 and takes its time sample in the
  same port transaction, then steps the clock and holds the frequency.
- State: the time state records the host's status, correction, and
  correction bound, host UTC's maximum error, the disruption counter,
  and whether the frequency was limited. nvx-time sample prints a
  sample's fields and the guest's pairing, and the test hooks packet,
  sample, pair, frequency, and correction take the new fields.

Templates record time ABI version 2, so the manifest reader refuses a
template of time ABI 1, which must be recaptured. The fixtures'
manifests, which OpenVMM's generator regenerated at the pin, differ from
the previous ones only in that version. The template-key vectors keep
the contract versions that their test fixes. Comments, the nvx-rs
README, and nvx-snapshot name the new versions.

OpenVMM's protocol vectors and CPU profiles are unchanged at the pin.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 29, 2026
Record the six commits that the first commit of this series pins as
positions 360-365 of the microVM chain, and move the NVX pin to
93b2c4d91.

Positions 360-365 move NVX's time ABI to version 2: the shared host TSC
bracket, the host OS's discipline of host UTC, the publication of host
UTC paired with the guest TSC with the host's clock correction, the
guest TSC's offset on every backend, the version-2 time sample and
restore packet 5, and the Guide's description of them. They change the
time-ABI backends, the portb device's time sample, the restore packet,
and the microVM worker's time-abi unit, which positions 159-269 added,
so they follow those positions upstream; say so in the table's
remarks. The pin is now 208 commits past the mapped tip.

Positions 355-359 are no longer only a proposal:
microsoft/openvmm#4794 merged them, so position 359, bd2b27a2e, is the
head of feature/microvm/dev. Say so in the scope, and name
microsoft/openvmm#4809 as the proposal of positions 360-365.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the copilot/microvm-time-abi-v2-host-clock branch from 93b2c4d to e71c2e1 Compare October 29, 2026 20:12
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 29, 2026
Pin microsoft/openvmm#4809 at e71c2e1303712e60f527ae93fa8e031b6b3b19bd,
six commits on bd2b27a2e, the previous pin, which is the head of
feature/microvm/dev.

Under time ABI 1 the guest paired host UTC with its own clock: nvx-time
read CLOCK_REALTIME around the port write that latches a time sample and
accepted the pairing only within 1 ms at boot and restore, or 50 us for
its discipline, so a port write that a loaded host served late cost a
sample, and three cost a boot or a restore. It then estimated how fast
host UTC runs against its TSC by steering the kernel's PLL over minutes,
although the host's time service already knows that rate.

Time ABI 2 has OpenVMM pair each sample's host UTC with the guest TSC on
the host and publish with it the host's clock correction, which the host
OS reports (adjtimex on Linux, GetSystemTimeAdjustmentPrecise on
Windows), the bound of the correction's error, the status and maximum
error of host UTC, and a disruption counter, which changes on a host
suspension, a step of host UTC, or a change of the guest TSC's offset.
The version-2 sample is 48 bytes. CPUID leaf 0x40000002 reports time ABI
2.0 (EBX 0x00020000). Restore packet 5 drops host UTC, so its header
shrinks to 24 bytes, and the guest's repair takes a time sample instead.

The guest moves with the pin, since a guest of one time ABI fails the
other's conformance check:

- nvx::contract: TIME_ABI_VERSION 2 and RESTORE_PACKET_VERSION 5.
- Pairing: the guest reads its TSC before and after the selector write,
  and CLOCK_REALTIME between those reads, and accepts a sample only if
  its TSC lies within that bracket, widened by the published pairing
  bound and 1 us of skew. It compares host UTC with CLOCK_REALTIME at
  the sample's TSC, moving its CLOCK_REALTIME read there at the rate at
  which Linux counts it; the offset's uncertainty is the pairing bound,
  half the bracket, and 2,000 ppm of that move.
- Rate: from the host's correction and the TSC rate, which the guest
  reads once from the Hyper-V TSC frequency MSR (0x40000022) and keeps
  in its time state, the guest computes the adjtimex frequency that
  makes CLOCK_REALTIME, which Linux counts at its kHz rounding of the
  TSC rate, run at host UTC's rate. It clamps that frequency to Linux's
  500 ppm limit and reports whether it did.
- Discipline: boot steps the clock to host UTC and holds that frequency
  with STA_FREQHOLD, so that the kernel's PLL corrects only the phase.
  Every poll holds the latest correction and slews the measured offset,
  or steps it from 128 ms on. A changed disruption counter returns the
  daemon to its fast cadence. Boot, restore, and polls take up to three
  samples within 100 us; a boot that gets none fails its check, and a
  restore fails its repair. The maximum and estimated errors that the
  guest sets include host UTC's maximum error only when the host
  reports a synchronized clock; the guest never marks its clock
  unsynchronized on the host's behalf.
- Restore: the repair reads packet 5 and takes its time sample in the
  same port transaction, then steps the clock and holds the frequency.
- State: the time state records the host's status, correction, and
  correction bound, host UTC's maximum error, the disruption counter,
  and whether the frequency was limited. nvx-time sample prints a
  sample's fields and the guest's pairing, and the test hooks packet,
  sample, pair, frequency, and correction take the new fields.

Templates record time ABI version 2, so the manifest reader refuses a
template of time ABI 1, which must be recaptured. The fixtures'
manifests, which OpenVMM's generator regenerated at the pin, differ from
the previous ones only in that version. The template-key vectors keep
the contract versions that their test fixes. Comments, the nvx-rs
README, and nvx-snapshot name the new versions.

OpenVMM's protocol vectors and CPU profiles are unchanged at the pin.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 29, 2026
Record the six commits that the first commit of this series pins as
positions 360-365 of the microVM chain, and move the NVX pin to
e71c2e130.

Positions 360-365 move NVX's time ABI to version 2: the shared host TSC
bracket, the host OS's discipline of host UTC, the publication of host
UTC paired with the guest TSC with the host's clock correction, the
guest TSC's offset on every backend, the version-2 time sample and
restore packet 5, and the Guide's description of them. They change the
time-ABI backends, the portb device's time sample, the restore packet,
and the microVM worker's time-abi unit, which positions 159-269 added,
so they follow those positions upstream; say so in the table's
remarks. The pin is now 208 commits past the mapped tip.

Positions 355-359 are no longer only a proposal:
microsoft/openvmm#4794 merged them, so position 359, bd2b27a2e, is the
head of feature/microvm/dev. Say so in the scope, and name
microsoft/openvmm#4809 as the proposal of positions 360-365.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the copilot/microvm-time-abi-v2-host-clock branch from e71c2e1 to a417f94 Compare October 29, 2026 20:17
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 29, 2026
Pin microsoft/openvmm#4809 at a417f946af14f8a7434cc6a55fc4caed3cb51636,
six commits on c32c57d12, the previous pin, which is the head of
feature/microvm/dev.

Under time ABI 1 the guest paired host UTC with its own clock: nvx-time
read CLOCK_REALTIME around the port write that latches a time sample and
accepted the pairing only within 1 ms at boot and restore, or 50 us for
its discipline, so a port write that a loaded host served late cost a
sample, and three cost a boot or a restore. It then estimated how fast
host UTC runs against its TSC by steering the kernel's PLL over minutes,
although the host's time service already knows that rate.

Time ABI 2 has OpenVMM pair each sample's host UTC with the guest TSC on
the host and publish with it the host's clock correction, which the host
OS reports (adjtimex on Linux, GetSystemTimeAdjustmentPrecise on
Windows), the bound of the correction's error, the status and maximum
error of host UTC, and a disruption counter, which changes on a host
suspension, a step of host UTC, or a change of the guest TSC's offset.
The version-2 sample is 48 bytes. CPUID leaf 0x40000002 reports time ABI
2.0 (EBX 0x00020000). Restore packet 5 drops host UTC, so its header
shrinks to 24 bytes, and the guest's repair takes a time sample instead.

The guest moves with the pin, since a guest of one time ABI fails the
other's conformance check:

- nvx::contract: TIME_ABI_VERSION 2 and RESTORE_PACKET_VERSION 5.
- Pairing: the guest reads its TSC before and after the selector write,
  and CLOCK_REALTIME between those reads, and accepts a sample only if
  its TSC lies within that bracket, widened by the published pairing
  bound and 1 us of skew. It compares host UTC with CLOCK_REALTIME at
  the sample's TSC, moving its CLOCK_REALTIME read there at the rate at
  which Linux counts it; the offset's uncertainty is the pairing bound,
  half the bracket, and 2,000 ppm of that move.
- Rate: from the host's correction and the TSC rate, which the guest
  reads once from the Hyper-V TSC frequency MSR (0x40000022) and keeps
  in its time state, the guest computes the adjtimex frequency that
  makes CLOCK_REALTIME, which Linux counts at its kHz rounding of the
  TSC rate, run at host UTC's rate. It clamps that frequency to Linux's
  500 ppm limit and reports whether it did.
- Discipline: boot steps the clock to host UTC and holds that frequency
  with STA_FREQHOLD, so that the kernel's PLL corrects only the phase.
  Every poll holds the latest correction and slews the measured offset,
  or steps it from 128 ms on. A changed disruption counter returns the
  daemon to its fast cadence. Boot, restore, and polls take up to three
  samples within 100 us; a boot that gets none fails its check, and a
  restore fails its repair. The maximum and estimated errors that the
  guest sets include host UTC's maximum error only when the host
  reports a synchronized clock; the guest never marks its clock
  unsynchronized on the host's behalf.
- Restore: the repair reads packet 5 and takes its time sample in the
  same port transaction, then steps the clock and holds the frequency.
- State: the time state records the host's status, correction, and
  correction bound, host UTC's maximum error, the disruption counter,
  and whether the frequency was limited. nvx-time sample prints a
  sample's fields and the guest's pairing, and the test hooks packet,
  sample, pair, frequency, and correction take the new fields.

Templates record time ABI version 2, so the manifest reader refuses a
template of time ABI 1, which must be recaptured. The fixtures'
manifests, which OpenVMM's generator regenerated at the pin, differ from
the previous ones only in that version. The template-key vectors keep
the contract versions that their test fixes. Comments, the nvx-rs
README, and nvx-snapshot name the new versions.

OpenVMM's protocol vectors and CPU profiles are unchanged at the pin.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 29, 2026
Record the six commits that the first commit of this series pins as
positions 362-367 of the microVM chain, and move the NVX pin to
a417f946a.

Positions 362-367 move NVX's time ABI to version 2: the shared host TSC
bracket, the host OS's discipline of host UTC, the publication of host
UTC paired with the guest TSC with the host's clock correction, the
guest TSC's offset on every backend, the version-2 time sample and
restore packet 5, and the Guide's description of them. They change the
time-ABI backends, the portb device's time sample, the restore packet,
and the microVM worker's time-abi unit, which positions 159-269 added,
so they follow those positions upstream. Position 366 also adapts
petri's microVM configuration, which PR15's position 088 adds, and the
ttrpc microVM test, which PR25's position 146 adds, to the portb
device's configuration and restore packet 5. Say so in the table's
remarks. The pin is now 210 commits past the mapped tip.

Positions 360 and 361 are no longer only a proposal:
microsoft/openvmm#4807 merged them, so position 361, c32c57d12, is the
head of feature/microvm/dev. Say so in the scope, and name
microsoft/openvmm#4809 as the proposal of positions 362-367.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 29, 2026
Record the six commits that the first commit of this series pins as
positions 362-367 of the microVM chain, and move the NVX pin to
a417f946a.

Positions 362-367 move NVX's time ABI to version 2: the shared host TSC
bracket, the host OS's discipline of host UTC, the publication of host
UTC paired with the guest TSC with the host's clock correction, the
guest TSC's offset on every backend, the version-2 time sample and
restore packet 5, and the Guide's description of them. They change the
time-ABI backends, the portb device's time sample, the restore packet,
and the microVM worker's time-abi unit, which positions 159-269 added,
so they follow those positions upstream. Position 366 also adapts
petri's microVM configuration, which PR15's position 088 adds, and the
ttrpc microVM test, which PR25's position 146 adds, to the portb
device's configuration and restore packet 5. Say so in the table's
remarks. The pin is now 210 commits past the mapped tip.

Positions 360 and 361 are no longer only a proposal:
microsoft/openvmm#4807 merged them, so position 361, c32c57d12, is the
head of feature/microvm/dev. Say so in the scope, and name
microsoft/openvmm#4809 as the proposal of positions 362-367.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 29, 2026
Pin microsoft/openvmm#4809 at a417f946af14f8a7434cc6a55fc4caed3cb51636,
six commits on c32c57d12, the previous pin, which is the head of
feature/microvm/dev.

Under time ABI 1 the guest paired host UTC with its own clock: nvx-time
read CLOCK_REALTIME around the port write that latches a time sample and
accepted the pairing only within 1 ms at boot and restore, or 50 us for
its discipline, so a port write that a loaded host served late cost a
sample, and three cost a boot or a restore. It then estimated how fast
host UTC runs against its TSC by steering the kernel's PLL over minutes,
although the host's time service already knows that rate.

Time ABI 2 has OpenVMM pair each sample's host UTC with the guest TSC on
the host and publish with it the host's clock correction, which the host
OS reports (adjtimex on Linux, GetSystemTimeAdjustmentPrecise on
Windows), the bound of the correction's error, the status and maximum
error of host UTC, and a disruption counter, which changes on a host
suspension, a step of host UTC, or a change of the guest TSC's offset.
The version-2 sample is 48 bytes. CPUID leaf 0x40000002 reports time ABI
2.0 (EBX 0x00020000). Restore packet 5 drops host UTC, so its header
shrinks to 24 bytes, and the guest's repair takes a time sample instead.

The guest moves with the pin, since a guest of one time ABI fails the
other's conformance check:

- nvx::contract: TIME_ABI_VERSION 2 and RESTORE_PACKET_VERSION 5.
- Pairing: the guest reads its TSC before and after the selector write,
  and CLOCK_REALTIME between those reads, and accepts a sample only if
  its TSC lies within that bracket, widened by the published pairing
  bound and 1 us of skew. It compares host UTC with CLOCK_REALTIME at
  the sample's TSC, moving its CLOCK_REALTIME read there at the rate at
  which Linux counts it; the offset's uncertainty is the pairing bound,
  half the bracket, and 2,000 ppm of that move.
- Rate: from the host's correction and the TSC rate, which the guest
  reads once from the Hyper-V TSC frequency MSR (0x40000022) and keeps
  in its time state, the guest computes the adjtimex frequency that
  makes CLOCK_REALTIME, which Linux counts at its kHz rounding of the
  TSC rate, run at host UTC's rate. It clamps that frequency to Linux's
  500 ppm limit and reports whether it did.
- Discipline: boot steps the clock to host UTC and holds that frequency
  with STA_FREQHOLD, so that the kernel's PLL corrects only the phase.
  Every poll holds the latest correction and slews the measured offset,
  or steps it from 128 ms on. A changed disruption counter returns the
  daemon to its fast cadence. Boot, restore, and polls take up to three
  samples within 100 us; a boot that gets none fails its check, and a
  restore fails its repair. The maximum and estimated errors that the
  guest sets include host UTC's maximum error only when the host
  reports a synchronized clock; the guest never marks its clock
  unsynchronized on the host's behalf.
- Restore: the repair reads packet 5 and takes its time sample in the
  same port transaction, then steps the clock and holds the frequency.
- State: the time state records the host's status, correction, and
  correction bound, host UTC's maximum error, the disruption counter,
  and whether the frequency was limited. nvx-time sample prints a
  sample's fields and the guest's pairing, and the test hooks packet,
  sample, pair, frequency, and correction take the new fields.

Templates record time ABI version 2, so the manifest reader refuses a
template of time ABI 1, which must be recaptured. The fixtures'
manifests, which OpenVMM's generator regenerated at the pin, differ from
the previous ones only in that version. The template-key vectors keep
the contract versions that their test fixes. Comments, the nvx-rs
README, and nvx-snapshot name the new versions.

OpenVMM's protocol vectors and CPU profiles are unchanged at the pin.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 29, 2026
Record the six commits that the first commit of this series pins as
positions 362-367 of the microVM chain, and move the NVX pin to
a417f946a.

Positions 362-367 move NVX's time ABI to version 2: the shared host TSC
bracket, the host OS's discipline of host UTC, the publication of host
UTC paired with the guest TSC with the host's clock correction, the
guest TSC's offset on every backend, the version-2 time sample and
restore packet 5, and the Guide's description of them. They change the
time-ABI backends, the portb device's time sample, the restore packet,
and the microVM worker's time-abi unit, which positions 159-269 added,
so they follow those positions upstream. Position 366 also adapts
petri's microVM configuration, which PR15's position 088 adds, and the
ttrpc microVM test, which PR25's position 146 adds, to the portb
device's configuration and restore packet 5. Say so in the table's
remarks. The pin is now 210 commits past the mapped tip.

Positions 360 and 361 are no longer only a proposal:
microsoft/openvmm#4807 merged them, so position 361, c32c57d12, is the
head of feature/microvm/dev. Say so in the scope, and name
microsoft/openvmm#4809 as the proposal of positions 362-367.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the copilot/microvm-time-abi-v2-host-clock branch from a417f94 to 75765b6 Compare October 29, 2026 21:48
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 29, 2026
Pin microsoft/openvmm#4809 at 75765b6608c3dd1f0be49fc4d5c1fc84a7b6ce96,
six commits on c32c57d12, the previous pin, which is the head of
feature/microvm/dev.

Under time ABI 1 the guest paired host UTC with its own clock: nvx-time
read CLOCK_REALTIME around the port write that latches a time sample and
accepted the pairing only within 1 ms at boot and restore, or 50 us for
its discipline, so a port write that a loaded host served late cost a
sample, and three cost a boot or a restore. It then estimated how fast
host UTC runs against its TSC by steering the kernel's PLL over minutes,
although the host's time service already knows that rate.

Time ABI 2 has OpenVMM pair each sample's host UTC with the guest TSC on
the host and publish with it the host's clock correction, which the host
OS reports (adjtimex on Linux, GetSystemTimeAdjustmentPrecise on
Windows), the bound of the correction's error, the status and maximum
error of host UTC, and a disruption counter, which changes on a host
suspension, a step of host UTC, or a change of the guest TSC's offset.
The version-2 sample is 48 bytes. CPUID leaf 0x40000002 reports time ABI
2.0 (EBX 0x00020000). Restore packet 5 drops host UTC, so its header
shrinks to 24 bytes, and the guest's repair takes a time sample instead.

The guest moves with the pin, since a guest of one time ABI fails the
other's conformance check:

- nvx::contract: TIME_ABI_VERSION 2 and RESTORE_PACKET_VERSION 5.
- Pairing: the guest reads its TSC before and after the selector write,
  and CLOCK_REALTIME between those reads, and accepts a sample only if
  its TSC lies within that bracket, widened by the published pairing
  bound and 1 us of skew. It compares host UTC with CLOCK_REALTIME at
  the sample's TSC, moving its CLOCK_REALTIME read there at the rate at
  which Linux counts it; the offset's uncertainty is the pairing bound,
  half the bracket, and 2,000 ppm of that move.
- Rate: from the host's correction and the TSC rate, which the guest
  reads once from the Hyper-V TSC frequency MSR (0x40000022) and keeps
  in its time state, the guest computes the adjtimex frequency that
  makes CLOCK_REALTIME, which Linux counts at its kHz rounding of the
  TSC rate, run at host UTC's rate. It clamps that frequency to Linux's
  500 ppm limit and reports whether it did.
- Discipline: boot steps the clock to host UTC and holds that frequency
  with STA_FREQHOLD, so that the kernel's PLL corrects only the phase.
  Every poll holds the latest correction and slews the measured offset,
  or steps it from 128 ms on. A changed disruption counter returns the
  daemon to its fast cadence. Boot, restore, and polls take up to three
  samples within 100 us; a boot that gets none fails its check, and a
  restore fails its repair. The maximum and estimated errors that the
  guest sets include host UTC's maximum error only when the host
  reports a synchronized clock; the guest never marks its clock
  unsynchronized on the host's behalf.
- Restore: the repair reads packet 5 and takes its time sample in the
  same port transaction, then steps the clock and holds the frequency.
- State: the time state records the host's status, correction, and
  correction bound, host UTC's maximum error, the disruption counter,
  and whether the frequency was limited. nvx-time sample prints a
  sample's fields and the guest's pairing, and the test hooks packet,
  sample, pair, frequency, and correction take the new fields.

Templates record time ABI version 2, so the manifest reader refuses a
template of time ABI 1, which must be recaptured. The fixtures'
manifests, which OpenVMM's generator regenerated at the pin, differ from
the previous ones only in that version. The template-key vectors keep
the contract versions that their test fixes. Comments, the nvx-rs
README, and nvx-snapshot name the new versions.

OpenVMM's protocol vectors and CPU profiles are unchanged at the pin.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 29, 2026
Record the six commits that the first commit of this series pins as
positions 362-367 of the microVM chain, and move the NVX pin to
75765b660.

Positions 362-367 move NVX's time ABI to version 2: the shared host TSC
bracket, the host OS's discipline of host UTC, the publication of host
UTC paired with the guest TSC with the host's clock correction, the
guest TSC's offset on every backend, the version-2 time sample and
restore packet 5, and the Guide's description of them. They change the
time-ABI backends, the portb device's time sample, the restore packet,
and the microVM worker's time-abi unit, which positions 159-269 added,
so they follow those positions upstream. Position 366 also adapts
petri's microVM configuration, which PR15's position 088 adds, and the
ttrpc microVM test, which PR25's position 146 adds, to the portb
device's configuration and restore packet 5. Say so in the table's
remarks. The pin is now 210 commits past the mapped tip.

Positions 360 and 361 are no longer only a proposal:
microsoft/openvmm#4807 merged them, so position 361, c32c57d12, is the
head of feature/microvm/dev. Say so in the scope, and name
microsoft/openvmm#4809 as the proposal of positions 362-367.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 29, 2026
Record the six commits that the first commit of this series pins as
positions 362-367 of the microVM chain, and move the NVX pin to
75765b660.

Positions 362-367 move NVX's time ABI to version 2: the shared host TSC
bracket, the host OS's discipline of host UTC, the publication of host
UTC paired with the guest TSC with the host's clock correction, the
guest TSC's offset on every backend, the version-2 time sample and
restore packet 5, and the Guide's description of them. They change the
time-ABI backends, the portb device's time sample, the restore packet,
and the microVM worker's time-abi unit, which positions 159-269 added,
so they follow those positions upstream. Position 366 also adapts
petri's microVM configuration, which PR15's position 088 adds, and the
ttrpc microVM test, which PR25's position 146 adds, to the portb
device's configuration and restore packet 5. Say so in the table's
remarks. The pin is now 210 commits past the mapped tip.

Positions 360 and 361 are no longer only a proposal:
microsoft/openvmm#4807 merged them, so position 361, c32c57d12, is the
head of feature/microvm/dev. Say so in the scope, and name
microsoft/openvmm#4809 as the proposal of positions 362-367.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

The KVM backend pairs a host time sample with the host TSC by reading
the TSC on both sides of the sample, and pairs a vCPU's TSC with the
host TSC the same way. Time ABI 2 pairs every time sample's host UTC
with the guest TSC on every backend, and MSHV and WHP need the same
bracket to measure the guest TSC's offset from the host TSC, so the
bracket belongs with the time ABI rather than with one backend.

Move HostTscBracket, which reads the host TSC around an operation with
LFENCE on both sides, and the conversion of its half width to
nanoseconds, cycles_to_ns_ceil and pair_host_sample, to
virt::time_abi::tsc. Their arithmetic is available on every host, and
only the TSC read itself on an x86-64 host. The KVM backend uses them
unchanged.

The cycle conversion test moves with them, and a new test checks the
bracket's midpoint and its refusal of a TSC that went backwards.

Part of microsoft/nvx#1009.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Time ABI 2 publishes, with every time sample, how fast host UTC runs,
so that the guest applies the host's clock correction instead of
estimating it over minutes. The host already knows that correction:
its time service steers host UTC by it.

Add host_clock_discipline, which reads it from the host OS, whatever
time service runs there, rather than from a hypervisor feature:

- On Linux, adjtimex with no modes: the frequency correction plus the
  tick's deviation from its nominal 10 ms, the status (STA_CLOCKERR is
  unreliable, STA_UNSYNC free-running, anything else synchronized), and
  the maximum error that the time service sets. It also names the
  clock's basis from the current clocksource: the TSC at the kernel's
  kHz calibration, or the TSC at the hypervisor's rate through the
  Hyper-V TSC page.
- On Windows, GetSystemTimeAdjustmentPrecise: its increment is the
  performance counter's frequency, and Windows counts a second of UTC
  for every adjustment of the counter's ticks, so UTC runs at the
  increment over the adjustment of the counter's rate, and a larger
  adjustment slows it. Over 20 s, UTC ran 0.45 ppm slow against the
  counter on a Windows host that reported an adjustment of 10,000,005
  for an increment of 10,000,000, and 53 ppm slow on one whose time
  service moved its adjustment from 10,000,501 to 10,000,588. An
  adjustment that Windows does not apply is a free-running clock with
  no correction, and a zero one an unknown clock. Under Hyper-V the
  performance counter runs at the hypervisor's TSC rate.

Add host_suspended_ns, how far the monotonic clock that counts
suspended time has run ahead of one that does not (CLOCK_BOOTTIME
minus CLOCK_MONOTONIC, and the interrupt time minus the unbiased
interrupt time), from which a change reveals a suspension, and
HostClockKind::follows_utc_rate, which says whether the anchor's
monotonic clock runs at host UTC's corrected rate, as Linux's does and
Windows' does not, so that a step of host UTC shows against it.

The tests check the Windows adjustment's arithmetic, its direction
with the readings of the host above, and its rounding, that each host
reports a status and a plausible correction, and the clock kinds.

Part of microsoft/nvx#1009.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
In time ABI 1 the guest pairs host UTC with its own clock: it reads its
clock around the port write that latches a time sample, and accepts the
pairing only within 1 ms at boot and restore, or 50 us for its
discipline, so a port write that a loaded host serves late costs a
sample, and three cost a boot or a restore. It also estimates over
minutes how fast host UTC runs, although the host's time service knows.

Add virt::time_abi::publish, which computes the host's fields of a time
sample under time ABI 2:

- Host UTC paired with the guest TSC on the host: the tightest of up to
  16 pairings of a host time sample with the host TSC, plus the guest
  TSC's offset from the host TSC, and the pairing's bound, which adds
  the offset's uncertainty. GuestTscOffset holds that offset; it
  measures it from guest TSC reads between two host TSC reads and keeps
  the tightest, and says whether two measurements can be the same
  offset.
- The correction: how fast host UTC runs against the guest TSC counted
  at the declared rate, minus one, in ppm scaled by 2^16, that is
  (1 + host correction) * F / F_d - 1. On a restore it includes the
  native rate's deviation from the declared one.
- A bound on that correction's error: the rounding when the host's
  clock and the backend's native rate come from one calibration, the
  kernel's kHz rate for KVM and the hypervisor's rate for MSHV and WHP,
  one kHz of the native rate when they do not, and none when the host's
  clocksource may not follow the TSC.
- The status and maximum error of host UTC, as its time service reports
  them.
- A disruption counter, which changes when the host's suspended time
  changes, when host UTC moves against the host monotonic clock by 1 ms
  or more beyond what the correction explains (Linux corrects both
  alike; on Windows the correction may change between samples by up to
  500 ppm), and when the guest TSC's offset changes.

Add the host-rate-offset-ppm test hook, which publishes host UTC as
running that many ppm faster than it does from the process's first
sample on, with the correction to match, so that tests can check that a
guest follows a host clock whose rate differs from the TSC's. The
publisher applies utc-offset-ms too, which the portb device applied
until now.

The tests check the correction's arithmetic against exact values, the
bounds, the pairing of a sample, the disruptions of Linux and Windows
clocks, the hooks, and a real host sample's pairing.

Part of microsoft/nvx#1009.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pairing a time sample's host UTC with the guest TSC on the host needs
the guest TSC at an instant of the host TSC while the VPs run. The
guest TSC is never scaled, so it is the host TSC plus an offset that
holds until the VMM writes a guest TSC or stops partition time; what
differs between backends is how to learn it.

Add TimeAbiBackend::guest_tsc_offset, which core calls at every start
of the VM, with every VP bound and stopped, after the TSC writes of a
cold boot, a restore, or a reset, and before any VP runs:

- KVM returns VP 0's KVM_VCPU_TSC_OFFSET, which preflight verifies is
  the unscaled offset, so it is exact.
- MSHV and WHP read VP 0's TSC between two host TSC reads, up to eight
  times, and keep the tightest reading; it ends early within 2,000
  cycles. Both freeze partition time until a partition starts (MSHV
  after preflight and a reset, WHP from creation and after a reset, and
  both when the partition unit stops), so the measurement resumes it
  first, as the partition's start would right after, and the offset
  holds once the VPs run. Resuming time that runs is harmless on both.

Preflight also reports what each backend's native rate measures, which
bounds the error of the published correction: KVM_GET_TSC_KHZ is the
kernel's calibration of the TSC to the kHz, and MSHV's and WHP's
processor clock frequency is the hypervisor's rate to the Hz.

The MSHV hardware test checks that the measurement resumes partition
time and agrees with a direct read of VP 0's TSC, and a new WHP
hardware test checks that the offset holds while time runs: on a
Windows 11 host, two measurements 500 ms apart differed by 87 cycles,
within the 3,700 cycles of their uncertainty.

Part of microsoft/nvx#1009.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move every microVM to NVX time ABI 2 (microsoft/nvx#1009). In time ABI
1, the guest estimates how fast its TSC runs against host UTC: it pairs
host UTC samples with its own clock, bracketing each port write with
reads of that clock, and steers the kernel's phase-locked loop over
minutes. The host already knows that rate, because its time service
steers host UTC by it, and the guest's brackets depend on how fast the
host serves the port write. Time ABI 2 has the VMM pair host UTC with
the guest TSC and publish the host's correction, which the guest
applies instead of estimating it.

The time sample, which writing 0xa7 to the portb status port latches,
becomes version 2, 48 bytes: the generation counter, host UTC and the
guest TSC at the same instant, the bound of their pairing, the status
of host UTC, the disruption counter, the correction against the TSC at
the declared rate, and the bounds of the correction's error and of host
UTC's error. The portb device takes these host fields from a platform
resource, TimeSampleSourceKind, which the VM worker resolves to the time
ABI's clock; the device keeps the generation, the test-hook flag, and
the sample-delay-us hook, which delays the latch.

The worker's clock pairs host UTC with the guest TSC through the offset
that the time-abi state unit measures at every start, before any VP
runs: the partition unit now depends on the time-abi unit, so the
measurement follows the TSC writes of a cold boot, a restore's
synchronized set, or a reset, and precedes the VPs' release on every
start, including a resume after a failed capture. An offset that
changes across a start is a disruption. Until the first measurement, or
when the host cannot pair a sample, the window stays empty, and the
guest rejects the sample.

The restore packet becomes version 5, which drops host UTC: the guest
takes a time sample for its repair, which pairs host UTC with its TSC
on the host, so the packet no longer latches UTC at its first
selection, and its header shrinks to 24 bytes.

CPUID leaf 0x40000002 reports time ABI 2.0 in EBX (0x00020000), and
snapshots record time ABI version 2. A snapshot of time ABI 1 fails
restore with E_MANIFEST_TIME and must be recaptured: its guest decodes
only version-1 samples, and a version-4 packet with UTC. The CPU time
bits do not change, so every pinned CPU profile serves time ABI 2 as it
is.

The portb device no longer takes utc-offset-ms, which the publisher
applies, so petri's microVM configuration drops it too, and the guest of
the ttrpc microVM test reads restore packet 5: version 5, and 17 header
bytes after the range count instead of 25.

The tests encode and decode the version-2 sample and the version-5
packet byte for byte, check that the portb device latches the source's
fields into its window and leaves it empty without a sample, check
that the unit's start measures the offset that samples use and that a
changed offset is a disruption, and check that restore rejects a
version-1 time contract.

Part of microsoft/nvx#1009.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The snapshots page described restore packet version 4, with host UTC
latched at its first selection, and the 16-byte time sample of time ABI
1. Describe restore packet version 5, which carries no UTC, and the
48-byte sample of time ABI 2, field by field: host UTC paired with the
guest TSC on the host, the pairing's bound, the status of host UTC, the
disruption counter, the host's clock correction against the TSC at the
declared rate, and the bounds of the correction's and of host UTC's
errors.

Say how OpenVMM pairs the guest TSC (KVM's offset, and MSHV's and WHP's
offset measured from VP 0's TSC at every start) and where it reads the
correction (adjtimex on Linux, GetSystemTimeAdjustmentPrecise on
Windows), that the correction includes a restore's rate deviation, that
the samples discipline only the guest's wall clock and are no
paravirtual clock, and that a snapshot of time ABI 1 must be recaptured.
The CLI and gRPC references name restore packet version 5.

Part of microsoft/nvx#1009.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the copilot/microvm-time-abi-v2-host-clock branch from 75765b6 to 090fbab Compare October 30, 2026 06:41
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Oct 30, 2026
Pin microsoft/openvmm#4809 at 090fbab88f2cc2e069147c8dca122e343c9363ed,
six commits on bcfc47cff6606afadffef97551e538962895b598, the previous pin
and the head of feature/microvm/dev.

Under time ABI 1 the guest paired host UTC with its own clock: nvx-time
read CLOCK_REALTIME around the port write that latches a time sample and
accepted the pairing only within 1 ms at boot and restore, or 50 us for
its discipline, so a port write that a loaded host served late cost a
sample, and three cost a boot or a restore. It then estimated how fast
host UTC runs against its TSC by steering the kernel's PLL over minutes,
although the host's time service already knows that rate.

Time ABI 2 has OpenVMM pair each sample's host UTC with the guest TSC on
the host and publish with it the host's clock correction, which the host
OS reports (adjtimex on Linux, GetSystemTimeAdjustmentPrecise on
Windows), the bound of the correction's error, the status and maximum
error of host UTC, and a disruption counter, which changes on a host
suspension, a step of host UTC, or a change of the guest TSC's offset.
The version-2 sample is 48 bytes. CPUID leaf 0x40000002 reports time ABI
2.0 (EBX 0x00020000). Restore packet 5 drops host UTC, so its header
shrinks to 24 bytes, and the guest's repair takes a time sample instead.

The guest moves with the pin, since a guest of one time ABI fails the
other's conformance check:

- nvx::contract: TIME_ABI_VERSION 2 and RESTORE_PACKET_VERSION 5.
- Pairing: the guest reads its TSC before and after the selector write,
  and CLOCK_REALTIME between those reads, and accepts a sample only if
  its TSC lies within that bracket, widened by the published pairing
  bound and 1 us of skew. It compares host UTC with CLOCK_REALTIME at
  the sample's TSC, moving its CLOCK_REALTIME read there at the rate at
  which Linux counts it; the offset's uncertainty is the pairing bound,
  half the bracket, and 2,000 ppm of that move.
- Rate: from the host's correction and the TSC rate, which the guest
  reads once from the Hyper-V TSC frequency MSR (0x40000022) and keeps
  in its time state, the guest computes the adjtimex frequency that
  makes CLOCK_REALTIME, which Linux counts at its kHz rounding of the
  TSC rate, run at host UTC's rate. It clamps that frequency to Linux's
  500 ppm limit and reports whether it did.
- Discipline: boot steps the clock to host UTC and holds that frequency
  with STA_FREQHOLD, so that the kernel's PLL corrects only the phase.
  Every poll holds the latest correction and slews the measured offset,
  or steps it from 128 ms on. A changed disruption counter returns the
  daemon to its fast cadence. Boot, restore, and polls take up to three
  samples within 100 us; a boot that gets none fails its check, and a
  restore fails its repair. The maximum and estimated errors that the
  guest sets include host UTC's maximum error only when the host
  reports a synchronized clock; the guest never marks its clock
  unsynchronized on the host's behalf.
- Restore: the repair reads packet 5 and takes its time sample in the
  same port transaction, then steps the clock and holds the frequency.
- State: the time state records the host's status, correction, and
  correction bound, host UTC's maximum error, the disruption counter,
  and whether the frequency was limited. nvx-time sample prints a
  sample's fields and the guest's pairing, and the test hooks packet,
  sample, pair, frequency, and correction take the new fields.

Templates record time ABI version 2, so the manifest reader refuses a
template of time ABI 1, which must be recaptured. The fixtures'
manifests, which OpenVMM's generator regenerated at the pin, differ from
the previous ones only in that version. The template-key vectors keep
the contract versions that their test fixes. Comments, the nvx-rs
README, and nvx-snapshot name the new versions.

OpenVMM's protocol vectors and CPU profiles are unchanged at the pin.

Part of #1009

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Guide unsafe Related to unsafe code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant