Skip to content

openvmm: keep the sub-100 ns part of the PIT's last tick time - #1557

Merged
Pedro Henrique Penna (ppenna) merged 2 commits into
devfrom
agents/issue-1065-pit-sub-100ns-remainder
Nov 12, 2026
Merged

Pedro Henrique Penna (ppenna) merged 2 commits into
devfrom
agents/issue-1065-pit-sub-100ns-remainder

Conversation

@ppenna

@ppenna Pedro Henrique Penna (ppenna) commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1065
Fixes #1162

What and why

OpenVMM's emulated 8254 PIT counted part of its elapsed time twice. It ran fast, and the faster the more often the guest accessed it: evaluated every 900 ns for 90 ms, it counted 4.75% more ticks than evaluated once (#1162), which misleads a guest that calibrates a clock against it. Two latched reads of a counter at one VM time could also see different counts, and a restored PIT counted the dropped time again, so it read another count than the PIT that was saved, at the same VM time (#1065). #1162 duplicates #1065, and this change fixes both (umbrella #1184).

Root cause, in OpenVMM: PitDevice::evaluate counts the whole 838 ns ticks that have elapsed since last, the VM time of the last counted tick, and advanced last by them with VmTime::wrapping_add, which keeps only whole 100 ns units of VM time. Every evaluation dropped the 0-99 ns by which the last tick follows a 100 ns step, and the next one counted them again; every access to the PIT's ports or to port 0x61 evaluates the PIT.

The pin, microsoft/openvmm#5065 at 8eede133c84771b59088774c84f59451b3939aaa, adds one commit to feature/microvm/dev, on 5d656d854, #1063's pin:

  • 8eede133c chipset: keep the sub-100 ns part of the PIT's last tick time. The PIT keeps the nanoseconds, below 100, by which its last counted tick follows last, counts ticks from that exact time, measures its next wakeup from it, and clears it on reset. Its saved state keeps them in a new default-zero uint32 field, last_frac_ns (#[mesh(3)]), which restore requires to be below 100, and restore refuses a last tick after the current time, as it refused a last after it. It adds six unit tests, four of which reproduce the issues and fail without the change.

The NVX commits:

  • 1d6d2c59c76 openvmm: pin the PIT's sub-100 ns tick time. It changes only the gitlink, from 5d656d854 to 8eede133c.
  • e96dc8840da doc: map the PIT's sub-100 ns tick time in the OpenVMM roadmap. Position 264, unassigned.

Decisions:

  • A carried remainder, not a fixed origin. PIT evaluate drops sub-100ns time, so the PIT runs fast and same-instant reads differ #1065 also suggests counting ticks from a fixed origin. Both count the same ticks: last plus the remainder stays exactly a whole number of 838 ns ticks after the last reset or restore, so the ticks counted by any VM time no longer depend on how often the PIT was evaluated. The remainder adds one field below 100 to the saved state and keeps last's meaning, on which earlier saved states and earlier OpenVMMs rely; a fixed origin would need an origin and a tick count instead.
  • No version changes. The new field defaults to zero: a saved state from an earlier OpenVMM restores with 0, so its PIT counts from last as it did, at most one tick early, and an earlier OpenVMM ignores the field and counts as it did. So no snapshot format, device state schema, time ABI, restore packet, or microVM ABI version changes, and nvx::contract does not either.
  • A u32 field, unlike the issues' u8. mesh decodes a u8 field's varint as u8, so a saved value of 256 would restore as a valid 0 rather than fail restore's check.
  • The wakeup keeps rounding down. arm_wakeup measures the wakeup from the exact last tick, as the issues propose, and still rounds the deadline down to a 100 ns step. A VM time timeout fires only once VM time has passed it, so that wakes the PIT at or after the tick; measuring it from last alone could wake the PIT before the tick of the output change, which the 20-tick minimum would then defer by about 16 us. A test covers it.
  • Restore checks the exact last tick. Restore already refused a last after the current time; it now also refuses a last equal to it with a nonzero fraction, which puts the last tick after it and which no saved PIT holds.
  • Time ABI. NVX guests disable the PIT at boot, and capture still refuses a periodically counting channel 0 (E_PIT_ACTIVE). The catch-up after downtime, which evaluates the PIT when it starts after a restore, now counts from the exact saved tick, so a restored PIT counts what the saved one would have counted by the same VM time.
  • Tests. The new tests drive the PIT through its ports with the GuestPit helper that chipset: PIT LSB/MSB access mode keeps the buffered LSB after the MSB write #1063's commit added, which this commit extends with read, program_counter0, set_time, and poll.

Validation

Reproduction and fix, in OpenVMM's chipset crate, on Windows with Rust 1.97.1. The tests are deterministic: VM time stands still except where a test sets it.

Test Issue At 5d656d854, before At 8eede133c
reads_at_one_vm_time_see_one_count: two latched reads of counter 0 at 2.5 us, mode 2 with 0x50 #1065 0x4f, then 0x4e 0x4f, 0x4f
restored_pit_counts_from_the_saved_tick: a PIT restored at 2.5 us from its encoded saved state, then both PITs at every VM time up to 100 us #1065 restored PIT reads 0x4e 0x4f, and the ticks elapsed at every VM time
polling_does_not_speed_up_the_pit: counter 0, mode 2 with 0xffff, evaluated every 900 ns for 90 ms, against one read at 90 ms #1162 18572 against 23673 23673, 23673
wakeup_raises_irq0_once_the_output_changes: IRQ 0 of a mode 0 count of 19 written at 2.5 us, ending at the 22nd tick, 18,436 ns - rises at 19.2 us rises at 18.5 us, the first VM time after the tick

The other two tests, reset_pit_counts_ticks_from_the_reset and restore_rejects_an_invalid_last_tick_time, check the change's own edges. Reverting each part of the change, one at a time, fails the test that covers it; an arm_wakeup that ignores the remainder raises IRQ 0 at 34.3 us. Before #1063's change, at c2c21ddb2, the same four scenarios failed with the same values.

OpenVMM package checks, at 8eede133c, on Windows: cargo check, cargo clippy --all-targets, cargo doc --no-deps, and cargo nextest run --profile agent for chipset: 110 passed, including #1063's tests; cargo xtask fmt --fix: clean.

Local NVX checks at this head (Windows):

  • python scripts/nvx.py verify: passed.
  • python scripts/openvmm_pin.py source 8eede133c84771b59088774c84f59451b3939aaa --pr 5065: good signature, and the revision is the pull request's head.
  • python scripts/openvmm_pin.py check-branch --topic: passed (2 signed commits, no coordinated files).
  • python scripts/openvmm_pin.py check-roadmap --worktree: passed (264 positions, 273 references).
  • python scripts/local_gates.py --base origin/dev --only test-changes markdown-links test-layout openvmm-vectors openvmm-cpu-profiles --run: passed. That is check_test_changes.py (0 changes to existing tests), check_markdown_links.py, check_test_layout.py, and the ignored openvmm_vectors (4) and openvmm_cpu_profiles (1) tests of nvx-rs.
  • The nvx-cli gate: not run locally; this change does not touch scripts/. It is left to CI's NVX CLI / Linux and NVX CLI / Windows jobs.

Remote runs, in a temporary worktree of this head on each test VM, with OpenVMM built from 8eede133c (clean) by nvx.py build-openvmm --backend BACKEND and the guest artifacts of v0.1.0-dev.cc12bae1b84c, three first-parent commits earlier, which nvx-rs download installed. The scenarios are guest-boot, interrupt-map, snapshot-core, restore-processors, smp-snapshot, restore-downtime, time-abi-conformance, and time-host-correction, with --processors 1 2:

Profile Backend Host type Check Before After
nvx-kvm-test kvm virtual-machine cargo nextest run --profile agent -p chipset in openvmm/, on Linux not run 110 passed
nvx-kvm-test kvm virtual-machine the scenarios, OpenVMM binary 080c1664 not run passed, in 224 s
nvx-mshv-test mshv virtual-machine the scenarios, OpenVMM binary d46905f4 (musl) not run passed, in 219 s
nvx-whp-test whp virtual-machine the scenarios, OpenVMM binary 137f44cf not run passed, in 243 s

NVX's guests disable the PIT at boot and never program it again, so these runs check that guests still boot, capture, and restore, with downtimes up to the simulated year, with the changed PIT and its saved state on each backend; the crate-level tests above are the reproduction. Bare-metal hosts and the remaining scenarios are left to CI.

CI at this head (e96dc8840da): run 43307906059 passed, including Required status check and OpenVMM CI of the pinned revision. OpenVMM CI of the pin: the six jobs that check-openvmm-ci requires passed in run 43306820936. microsoft/openvmm#5065 has landed: feature/microvm/dev is now exactly 8eede133c84771b59088774c84f59451b3939aaa.

No existing NVX test changes, so no tests-modified label.

OpenVMM's emulated 8254 PIT counted part of its elapsed time twice.
PitDevice::evaluate counts the whole 838 ns ticks since the VM time of
its last counted tick, and advanced that time with
VmTime::wrapping_add, which keeps only whole 100 ns units, so every
evaluation dropped the 0-99 ns by which the last tick follows a 100 ns
step, and the next one counted them again. Every access to the PIT's
ports or to port 0x61 evaluates the PIT, so it ran fast, and the faster
the more often the guest accessed it: evaluated every 900 ns for 90 ms,
it counted 4.75% more ticks than evaluated once (#1162). Two latched
reads at one VM time could also see different counts, and a restored
PIT counted the dropped time again (#1065).

Pin 8eede133c84771b59088774c84f59451b3939aaa, the head of
microsoft/openvmm#5065, one commit on 5d656d854, the head of
feature/microvm/dev:

- 8eede133c chipset: keep the sub-100 ns part of the PIT's last tick
  time

The PIT now keeps the nanoseconds, below 100, by which its last counted
tick follows that VM time, counts ticks from that exact time, measures
its next wakeup from it, and clears it on reset. Its saved state keeps
them in a new default-zero uint32 field, which restore requires to be
below 100, and restore refuses a last tick after the current time. A
saved state of an earlier OpenVMM restores with 0, and an earlier
OpenVMM ignores the field, so no snapshot format, device state schema,
or time ABI version changes.

Evidence: on Windows, the revision's chipset tests reproduce both
issues through the PIT's ports and fail without the fix: two latched
reads at one VM time read 0x4f, then 0x4e; a PIT restored from its
saved state reads 0x4e where the saved one read 0x4f; a PIT evaluated
every 900 ns for 90 ms reads 18572 where one read once at 90 ms reads
23673; and IRQ 0 of a mode 0 count rises at 19.2 us rather than at
18.5 us, the first VM time after its tick. They pass at the pin, with
the other 104 tests of chipset.

NVX needs no coordinated change: the control-session vectors, the
source excerpts, the contract versions, and the CPU profiles that
nvx-rs copies from OpenVMM match the revision's.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The NVX pin moves to 8eede133c, position 264, 133 commits past the
mapped tip. Position 264, which microsoft/openvmm#5065 proposes for
feature/microvm/dev on its head, 5d656d854, is unassigned. The Scope
also says that microsoft/openvmm#5061 merged position 263.

It keeps the part of the PIT's last tick time below VM time's 100 ns
resolution, which PitDevice::evaluate dropped and counted again, in
the PIT's evaluation, wakeup, reset, and saved state, code that
upstream main has, and adds six tests that extend position 263's test
helper, in a file that PR10's position 022 and positions 139 and 263
also change. It changes none of the code of positions 022 and 139, and
nothing that a mapped PR adds. Replaying it on upstream main conflicts
in that file, which lacks position 263's helper; after position 263,
it conflicts only where its tests follow position 139's test, so it
follows position 263 upstream.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The PIT scheduling and saved-state restore changes still lack exact-pin Linux/backend runtime evidence, and prerequisite microsoft/openvmm#5065 has not yet landed on feature/microvm/dev.

No open issues 🎉

What changed in this PR

Documents the OpenVMM pin that fixes PIT timing drift and inconsistent same-time reads, addressing #1065 and #1162 within the chipset correctness work tracked by #1184.

Changes:

  • Updates the roadmap’s pinned revision to 8eede133c, position 264, and adjusts chain counts.
  • Records the PIT fractional-time fix and its upstream test-helper dependency on position 263.
File Description
doc/​openvmm-upstream-roadmap.md Updates the pin, commit mapping, and upstream integration notes for the PIT timing fix.

Review effort: Balanced

@ppenna
Pedro Henrique Penna (ppenna) added this pull request to the merge queue Nov 12, 2026
Merged via the queue into dev with commit 24fa723 Nov 12, 2026
49 checks passed
@ppenna
Pedro Henrique Penna (ppenna) deleted the agents/issue-1065-pit-sub-100ns-remainder branch November 12, 2026 22:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants