Skip to content

openvmm: reject a saved PIT control word with RW = 0 - #1558

Open
Pedro Henrique Penna (ppenna) wants to merge 2 commits into
devfrom
agents/issue-1066-pit-restore-rw0
Open

Pedro Henrique Penna (ppenna) wants to merge 2 commits into
devfrom
agents/issue-1066-pit-restore-rw0

Conversation

@ppenna

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

Copy link
Copy Markdown
Contributor

Fixes #1066
Fixes #1147

What and why

A modified snapshot whose PIT state held a counter control word with RW = 0 restored, and the guest's next read or write of that counter's port, 0x40, 0x41, or 0x42, panicked OpenVMM's VM worker at unreachable!() in Timer::read or Timer::write. #1147 reports the same defect as #1066, so this fixes both.

Root cause, in OpenVMM: RW = 0, bits 5:4 of an 8254 control word, encodes the counter latch command, which latches a counter's count without programming the counter, so no counter holds it. Timer::set_control latches the count and never stores such a control word, and Timer::read and Timer::write handle only RW = 1, 2, and 3. PitDevice::restore installed each saved control word with ControlWord::from, which takes any byte, without that check.

The pin, microsoft/openvmm#5067 at 6f57a6d61160486a662530d528ca184430a76c7b, adds two commits to feature/microvm/dev, 8eede133c, #1065's pin:

  • 607407fa9 chipset: reject a saved PIT control word with RW = 0. restore checks every timer's saved control word, after the fraction of the last tick and before it changes any state, and fails with RestoreError::InvalidSavedState, "timer N control word 0xNN has no read/write mode", when one has RW = 0, so a rejected restore leaves the PIT as it was. Two unit tests come with it.
  • 6f57a6d61 chipset: keep the PIT restore tests within Linux's open file limit. It changes only the first commit's tests, which created a GuestPit for each case: a dropped GuestPit leaves some of its files open on Linux, so they exhausted the default limit of 1024 open files per process. They now reset and reuse five.

The NVX commits:

  • 4e50d08e885 openvmm: pin the PIT restore's control word check. It changes only the gitlink, from 8eede133c to 6f57a6d61.
  • cef6ac02417 doc: map the PIT restore's control word check in the OpenVMM roadmap. Positions 265 and 266, unassigned; 266 folds into 265.

Decisions:

Not in this change, found while auditing what restored state the PIT accepts, neither of which panics:

  1. A latched count that is never released. A restored read_high set while the counter's control word has RW = 1 or 2, which no guest programming produces, keeps Timer::read from releasing a latched count, so the counter's latched reads repeat one count until the guest writes a control word for the counter.
  2. State installed by a failed restore. The existing InvalidLastTick and E_PIT_ACTIVE checks run after restore installs the saved timers, last, and last_frac_ns, so a restore that fails them leaves that state installed. A failed restore ends the VM before any processor runs.

Validation

Local checks at this head, cef6ac02417 (Windows):

  • python scripts/openvmm_pin.py check-branch --topic: passed (2 signed commits, no coordinated files).
  • python scripts/openvmm_pin.py check-roadmap --worktree: passed (266 positions, 275 references).
  • python scripts/openvmm_pin.py source 6f57a6d61160486a662530d528ca184430a76c7b --pr 5067: good signature, and the revision is the pull request's head.
  • python scripts/nvx.py verify: passed.
  • 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, python -m unittest discover over scripts/: not run locally. This change does not touch scripts/, and the gate takes over two hours on this shared machine. It is left to CI's NVX CLI / Linux and NVX CLI / Windows jobs.

OpenVMM package checks, at 6f57a6d61, for chipset: on Windows, cargo check, cargo clippy --all-targets (clean apart from the environment's LNK4199 linker notices), cargo doc --no-deps, and cargo nextest run --profile agent (112 passed), then cargo xtask fmt --fix (no changes); on Linux, on nvx-kvm-test, at the default limit of 1024 open files, cargo nextest run --profile agent and cargo test --lib (112 passed each). At 607407fa9, the first commit's tests passed on Windows, but restore_rejects_rw0_control_word_without_changing_state failed on that Linux host with "Too many open files (os error 24)" and passed there with a limit of 16384, which the second commit fixes.

Reproduction, with cargo test -p chipset on Windows x64:

Revision Restore of #1066's state (counter 0's control word 0x10 changed to 0x00 in the encoded saved state), then write and read port 0x40 Restore of #1147's states (bits 5:4 of counter 0's, 1's, or 2's control word cleared), then read the counter's port restore_rejects_rw0_control_word_without_changing_state
Before: c2c21ddb2, and 8eede133c with the check disabled restore succeeds; the write panics, "internal error: entered unreachable code", in Timer::write each restore succeeds; each read panics in Timer::read fails: counter 0's control word 0x00 restores
After: 6f57a6d61 restore fails, "timer 0 control word 0x00 has no read/write mode"; the port reads and writes each restore fails, "timer N control word 0x00 has no read/write mode"; the reads return passes

With the check in the loop that installs the timers, as in #1147's patch, the test fails for counter 1, whose rejection leaves counter 0's saved state installed. A scratch test that restored 300,000 random saved states, with random timers, last up to the current time, and last_frac_ns, and then accessed every port, in a debug build, panicked at once without the check and passed with it.

Remote runs, in a temporary worktree of this head on each test VM, with OpenVMM built from 6f57a6d61 (clean) by nvx.py build-openvmm --backend BACKEND and the guest artifacts of v0.1.0-dev.c5fdc8b850c6, 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 Result
nvx-kvm-test kvm virtual-machine cargo nextest run --profile agent -p chipset in openvmm/, on Linux, at the default limit of 1024 open files 112 passed
nvx-kvm-test kvm virtual-machine the scenarios, OpenVMM binary f73e4587 passed, in 244 s
nvx-mshv-test mshv virtual-machine the scenarios, OpenVMM binary ebee1b04 (musl) passed, in 219 s
nvx-whp-test whp virtual-machine the scenarios, OpenVMM binary 80116a90 passed, in 242 s

The same scenarios also passed on the KVM and MSHV test VMs with OpenVMM built from 607407fa9, whose code outside the tests is the pin'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, through the changed restore on each backend; the crate-level tests above are the reproduction, since a restore that the check rejects needs a modified saved state. Bare-metal hosts and the remaining scenarios are left to CI.

CI at this head, cef6ac02417: in progress, run 43331990118. Its openvmm-ci job waits for OpenVMM's CI of 6f57a6d61, run 43331611028.

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

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 OpenVMM pin remains on an unmerged PR, so a maintainer must confirm exact-pin CI and runtime results and fast-forward landing on feature/microvm/dev before integration.

No open issues 🎉

What changed in this PR

This PR updates NVX’s OpenVMM pin to reject saved PIT control words with RW = 0 before changing device state, preventing malformed snapshots from causing a panic on subsequent counter access. It addresses #1066 and #1147.

Changes:

  • Records OpenVMM revision 607407fa9 as roadmap position 265.
  • Updates chain counts and documents the fix’s tests, dependencies, and upstream replay considerations.
File Description
doc/​openvmm-upstream-roadmap.md Updates the pinned revision and maps the PIT restore validation fix into the upstream roadmap.

Review effort: Balanced

OpenVMM's emulated 8254 PIT restored a saved control word with RW = 0,
bits 5:4, which encodes the counter latch command: it latches a
counter's count without programming the counter, so no counter holds
it. The PIT's control register handles it as a latch and never stores
it, and its counter reads and writes handle only RW = 1, 2, and 3. A
modified snapshot whose PIT state held RW = 0 thus restored, and the
guest's next read or write of that counter's port reached
unreachable!() and panicked OpenVMM's VM worker (#1066, #1147).

Pin 6f57a6d61160486a662530d528ca184430a76c7b, the head of
microsoft/openvmm#5067, two commits on 8eede133c, the head of
feature/microvm/dev:

- 607407fa9 chipset: reject a saved PIT control word with RW = 0
- 6f57a6d61 chipset: keep the PIT restore tests within Linux's open
  file limit

PitDevice::restore now checks every timer's control word before it
changes any state, and fails with RestoreError::InvalidSavedState when
one has RW = 0, so a rejected restore leaves the PIT as it was. Every
state that the PIT saved still restores, and no saved-state field,
snapshot format, device state schema, or time ABI version changes. The
second commit changes only the first's tests, which created PITs for
each case and so exhausted Linux's default limit of 1024 open files
per process.

Evidence: on Windows, the revision's chipset tests reproduce both
issues and fail without the fix: restoring a fresh PIT's saved state,
with a counter's control word changed to one with RW = 0, succeeds,
after which the next write of port 0x40 panics in Timer::write, and
the next read of port 0x40, 0x41, or 0x42 in Timer::read. At the pin,
the restore fails with "timer N control word 0x00 has no read/write
mode" and leaves the PIT unchanged, and the tests pass with the other
110 tests of chipset, on Windows and on Linux.

NVX needs no coordinated change: the control-session vectors, the
source excerpts, the contract versions, the activation bound, 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 6f57a6d61, position 266, 135 commits past the
mapped tip. Positions 265 and 266, which microsoft/openvmm#5067
proposes for feature/microvm/dev on its head, 8eede133c, are
unassigned, and position 266 folds into position 265. The Scope also
says that microsoft/openvmm#5065 merged position 264.

Position 265 makes PitDevice::restore reject a saved control word with
RW = 0, the counter latch command, before it changes any state, in
code that upstream main has, and adds two tests on position 263's test
helper, in a file that PR10's position 022 and positions 139, 263, and
264 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 where it adds its error, its tests' import, and
its tests next to code of positions 139, 263, and 264, which upstream
main lacks; its check applies there. It follows position 264 upstream.
Position 266 changes only position 265's tests, which created PITs for
each case and so exhausted Linux's default limit of open files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/issue-1066-pit-restore-rw0 branch from 10b3d52 to cef6ac0 Compare November 12, 2026 23:48
Copilot AI balanced review requested due to automatic review settings November 12, 2026 23:48

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.

🟡 Changes recommended

The PR description, landing target, and validation references still identify an earlier revision rather than the current two-commit head, leaving the landing instructions inconsistent with the actual pin.

1 open issue

Review effort: Balanced

Comment thread doc/openvmm-upstream-roadmap.md Resolved

This branch has not been deployed

No deployments
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