Repository navigation
openvmm: keep the sub-100 ns part of the PIT's last tick time - #1557
Merged
Pedro Henrique Penna (ppenna) merged 2 commits intoNov 12, 2026
Merged
Pedro Henrique Penna (ppenna) merged 2 commits into
Pedro Henrique Penna (ppenna) merged 2 commits into
Conversation
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>
Contributor
There was a problem hiding this comment.
🔵 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
Pedro Henrique Penna (ppenna)
deleted the
agents/issue-1065-pit-sub-100ns-remainder
branch
November 12, 2026 22:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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::evaluatecounts the whole 838 ns ticks that have elapsed sincelast, the VM time of the last counted tick, and advancedlastby them withVmTime::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 tofeature/microvm/dev, on5d656d854, #1063's pin:8eede133cchipset: 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 followslast, 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-zerouint32field,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 alastafter it. It adds six unit tests, four of which reproduce the issues and fail without the change.The NVX commits:
1d6d2c59c76openvmm: pin the PIT's sub-100 ns tick time. It changes only the gitlink, from5d656d854to8eede133c.e96dc8840dadoc: map the PIT's sub-100 ns tick time in the OpenVMM roadmap. Position 264, unassigned.Decisions:
evaluatedrops 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:lastplus 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 keepslast's meaning, on which earlier saved states and earlier OpenVMMs rely; a fixed origin would need an origin and a tick count instead.lastas 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, andnvx::contractdoes not either.u32field, unlike the issues'u8. mesh decodes au8field's varintas u8, so a saved value of 256 would restore as a valid 0 rather than fail restore's check.arm_wakeupmeasures 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 fromlastalone 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.lastafter the current time; it now also refuses alastequal to it with a nonzero fraction, which puts the last tick after it and which no saved PIT holds.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.GuestPithelper that chipset: PIT LSB/MSB access mode keeps the buffered LSB after the MSB write #1063's commit added, which this commit extends withread,program_counter0,set_time, andpoll.Validation
Reproduction and fix, in OpenVMM's
chipsetcrate, on Windows with Rust 1.97.1. The tests are deterministic: VM time stands still except where a test sets it.5d656d854, before8eede133creads_at_one_vm_time_see_one_count: two latched reads of counter 0 at 2.5 us, mode 2 with 0x50restored_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 uspolling_does_not_speed_up_the_pit: counter 0, mode 2 with 0xffff, evaluated every 900 ns for 90 ms, against one read at 90 mswakeup_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 nsThe other two tests,
reset_pit_counts_ticks_from_the_resetandrestore_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; anarm_wakeupthat ignores the remainder raises IRQ 0 at 34.3 us. Before #1063's change, atc2c21ddb2, 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, andcargo nextest run --profile agentforchipset: 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 ischeck_test_changes.py(0 changes to existing tests),check_markdown_links.py,check_test_layout.py, and the ignoredopenvmm_vectors(4) andopenvmm_cpu_profiles(1) tests ofnvx-rs.nvx-cligate: not run locally; this change does not touchscripts/. It is left to CI'sNVX CLI / LinuxandNVX CLI / Windowsjobs.Remote runs, in a temporary worktree of this head on each test VM, with OpenVMM built from
8eede133c(clean) bynvx.py build-openvmm --backend BACKENDand the guest artifacts ofv0.1.0-dev.cc12bae1b84c, three first-parent commits earlier, whichnvx-rs downloadinstalled. The scenarios areguest-boot,interrupt-map,snapshot-core,restore-processors,smp-snapshot,restore-downtime,time-abi-conformance, andtime-host-correction, with--processors 1 2:cargo nextest run --profile agent -p chipsetinopenvmm/, on Linux080c1664d46905f4(musl)137f44cfNVX'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, includingRequired status checkandOpenVMM CI of the pinned revision. OpenVMM CI of the pin: the six jobs thatcheck-openvmm-cirequires passed in run 43306820936. microsoft/openvmm#5065 has landed:feature/microvm/devis now exactly8eede133c84771b59088774c84f59451b3939aaa.No existing NVX test changes, so no
tests-modifiedlabel.8eede133c84771b59088774c84f59451b3939aaa.5d656d854, the head offeature/microvm/devafter [DO NOT REVIEW] chipset: start each PIT low/high count with its low byte openvmm#5061 (chipset: PIT LSB/MSB access mode keeps the buffered LSB after the MSB write #1063, openvmm: consecutive PIT low/high writes reuse the preceding count's low byte #1146). There are no other prerequisites.57859ec2b. The local checks above ran at this exact revision.feature/microvm/devfrom5d656d854to exactly8eede133c84771b59088774c84f59451b3939aaa. OpenVMM's merge button would squash the commit and leave the pin off the branch. This pull request then merges through the queue.