Repository navigation
[DO NOT REVIEW] chipset: reject a saved PIT control word with RW = 0 - #5067
Merged
Pedro Henrique Penna (ppenna) merged 2 commits intoNov 13, 2026
Conversation
PitDevice::restore installed each timer's saved control word with ControlWord::from, which takes any byte. 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: Timer::set_control latches the count and never stores it, and Timer::read and Timer::write handle only RW = 1, 2, and 3. A saved state in which a counter's control word had RW = 0, as a modified snapshot can hold, thus restored, and the guest's next read or write of that counter's port reached unreachable!() in Timer::read or Timer::write and panicked the VM worker (microsoft/nvx#1066, microsoft/nvx#1147). restore now checks every timer's control word before it changes any state, and fails with RestoreError::InvalidSavedState when one has RW = 0, which leaves the device as it was. The other bits of a control word need no check: every mode decodes, 6 and 7 as the 8254's aliases of modes 2 and 3, and the counter-select bits of a stored control word affect nothing. Timer::set_control has never stored RW = 0, and a reset counter holds RW = 1, so every saved state that the PIT produced still restores, and the saved-state format does not change. Tests: restoring a saved state in which any counter's control word has RW = 0, whatever its other bits, fails as invalid saved state, both into a fresh PIT and into one whose counters run, and leaves the PIT unchanged, so that the counter's port then reads and writes as in a PIT that never restored; at 8eede13 the restore succeeds. Every control word that a guest can program, for each counter, still survives a save and restore. Evidence: at 8eede13, with cargo test -p chipset on Windows x64, the restores of both issues' reproductions succeed, and then the next write of port 0x40 panics in Timer::write, and the next read of port 0x40, 0x41, or 0x42 in Timer::read. With this commit the restores fail with "timer N control word 0x00 has no read/write mode", and the ports read and write. A scratch test that restores 300,000 random saved states and then accesses the ports, in a debug build, panics without the check and passes with it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
requested a review
from a team
as a code owner
November 12, 2026 22:57
restore_rejects_rw0_control_word_without_changing_state created three GuestPits for each of its 384 cases, and restore_accepts_every_programmable_control_word two for each of its 144. A GuestPit leaves some of its files open on Linux after it is dropped, so the first test failed there with "failed to create event: Too many open files (os error 24)" at the default limit of 1024 open files per process, and passed with a limit of 16384. Windows, where the tests passed, has no such limit. The tests now create five GuestPits in all, and each case resets the PITs that it uses, so that every case starts from the same state. The first test restores the same fresh PIT's saved state into one of two PITs at 0.1 s and compares it with the other, and the second programs each control word into one PIT and restores it into another. The first test's saved state also holds a fraction of its last tick, which the reset PITs lack, as the PITs that it created for each case had one that the saved state lacked, so that a failed restore that installed only the fraction still shows. Tests: on Linux, on nvx-kvm-test, the chipset tests pass at the default limit of 1024 open files, with cargo nextest and cargo test, as on Windows. Without the restore check, the first test fails at counter 0's control word 0x00, which restores; with the check in the loop that installs the timers, it fails at counter 1, whose rejection leaves counter 0's saved state installed; with a failed restore that installs only the fraction, it fails at counter 0's control word 0x00; and with a restore that keeps the PIT's output instead of the saved one, the second test fails at control word 0x12. 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
Nov 12, 2026
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>
Pedro Henrique Penna (ppenna)
added a commit
to microsoft/nvx
that referenced
this pull request
Nov 12, 2026
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>
Pedro Henrique Penna (ppenna)
merged commit Nov 13, 2026
6f57a6d
into
feature/microvm/dev
73 of 74 checks passed
Pedro Henrique Penna (ppenna)
added a commit
to microsoft/nvx
that referenced
this pull request
Nov 13, 2026
The NVX pin moves to 60e160369, position 267, 136 commits past the mapped tip. Position 267, which microsoft/openvmm#5072 proposes for feature/microvm/dev on its head, 6f57a6d61, is unassigned. The Scope also says that microsoft/openvmm#5067 merged positions 265 and 266. Position 267 makes the RTC's io_read zero-extend a read of port 0x70 or 0x71 that is wider than a byte, in code that upstream main has, and adds a test, in a file that PR10's position 020, PR15's position 057, and position 200 also change, none of them in the code that it changes. It changes nothing that a mapped PR adds. Replaying it on upstream main applies without conflicts, and chipset type-checks there and its 64 tests pass, so it can go upstream on its own. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This was referenced Nov 13, 2026
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.
Makes the emulated 8254 PIT reject a saved state in which a counter's control word has RW = 0, for microsoft/nvx#1066 and its duplicate microsoft/nvx#1147. 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_controllatches the count and never stores it, andTimer::readandTimer::writehandle only RW = 1, 2, and 3.PitDevice::restoreinstalled each saved control word withControlWord::from, which takes any byte, so a modified snapshot whose PIT state held RW = 0 restored, and the guest's next read or write of that counter's port reachedunreachable!()and panicked the VM worker.Two commits on
8eede133c, the head offeature/microvm/dev, after #5061 and #5065 changed the same file. The second changes only the first's tests, and can fold into it.Reject a saved PIT control word with RW = 0 (
607407fa9)PitDevice::restorebuilt each timer'sTimerStatewithcontrol: ControlWord::from(control), without the check thatTimer::set_controlmakes of every control word that a guest writes. Counter 0's control word0x10changed to0x00in a fresh PIT's saved state, then a write to port0x40, panicked inTimer::write(chipset: PIT restore accepts a control word with rw = 0, then hitsunreachable!()on the next access nvx#1066), and any counter's control word with bits 5:4 cleared, then a read of its port, panicked inTimer::read(openvmm: PIT restore accepts RW=0 and subsequent counter I/O can panic nvx#1147).restorechecks every timer's saved control word, after the fraction of the last tick and before it changes any state, and fails withRestoreError::InvalidSavedState, "timer N control word 0xNN has no read/write mode", when one has RW = 0, so a rejected restore leaves the device as it was. The other bits of a control word need no check: every mode decodes, 6 and 7 as the 8254's aliases of modes 2 and 3, and the counter-select bits of a stored control word affect nothing.Timer::set_controlhas never stored RW = 0, and a reset counter holds RW = 1, so every state that the PIT saved still restores; no field, format, or version changes.unreachable!()on the next access nvx#1066 expects. MakingTimer::readandTimer::writetolerate RW = 0 would keep the invalid state rather than reject it where it enters.restore_rejects_rw0_control_word_without_changing_state: for each counter and each of the 64 control words with RW = 0, restoring a fresh PIT's saved state with that control word, as both issues do, fails withInvalidSavedState, into a PIT whose counters are reset and into one whose three counters run, and leaves the PIT's saved state equal to that of an identical PIT that never restored; the counter's port then reads, takes a write, and reads as in that PIT.restore_accepts_every_programmable_control_word: for each counter, every control word that a guest can program, with RW = 1, 2, or 3, modes 0 to 7, and binary or BCD counts, survives a save and restore, and the restored PIT reads and writes the counter as the saved one does.Keep the PIT restore tests within Linux's open file limit (
6f57a6d61)GuestPitfor each case, three for each of 384 cases and two for each of 144. AGuestPitleaves some of its files open on Linux after it is dropped, sorestore_rejects_rw0_control_word_without_changing_statefailed there with "failed to create event: Too many open files (os error 24)" at the default limit of 1024 open files per process, and passed with a limit of 16384. Windows, where the tests passed, has no such limit.GuestPits in all, and each case resets the PITs that it uses, so that every case starts from the same state. The negative test's saved state also holds a fraction of its last tick that the reset PITs lack, as the PITs that it created for each case had one that the saved state lacked, so that a failed restore that installed only the fraction still shows.0x00, which restores. With the check in the loop that installs the timers, as in openvmm: PIT restore accepts RW=0 and subsequent counter I/O can panic nvx#1147's patch, it fails at counter 1, whose rejection leaves counter 0's saved state installed. With a failed restore that installs only the fraction, it fails at counter 0's control word0x00. With a restore that keeps the PIT's output instead of the saved one, the positive test fails at control word0x12.Not in this change, found while auditing what restored state the PIT accepts, neither of which panics:
read_highset while the counter's control word has RW = 1 or 2, which no guest programming produces, keepsTimer::readfrom releasing a latched count, so the counter's latched reads repeat one count until the guest writes a control word for the counter.InvalidLastTickandE_PIT_ACTIVEchecks run afterrestoreinstalls the saved timers,last, andlast_frac_ns, so a restore that fails them leaves that state installed. A failed restore ends the VM before any processor runs.Validation
With Rust 1.97.1, on Windows, for
chipset, at6f57a6d61:cargo check,cargo clippy --all-targets(clean apart from the environment's LNK4199 linker notices),cargo doc --no-deps, andcargo nextest run --profile agent(112 passed), thencargo xtask fmt --fix(rustfmt, lints, verify-fuzzers, and verify-flowey complete, no changes). On Linux, on the Azure Linux 3 KVM test VM, at the default limit of 1024 open files:cargo nextest run --profile agent -p chipsetandcargo test -p chipset --lib, 112 passed each.At
8eede133c, as atc2c21ddb2, the reproductions of both issues restore, and then the next write of port0x40panics with "internal error: entered unreachable code" inTimer::write, and the next read of port0x40,0x41, or0x42inTimer::read. With these commits, each restore fails with "timer N control word 0x00 has no read/write mode", and the ports read and write. A scratch test that restored 300,000 random saved states, with random timers,lastup to the current time, andlast_frac_ns, and then accessed every port, in a debug build, panicked at once without the check and passed with it; with an audit of the device's panics and arithmetic, it finds no other restored state that panics the PIT.The PIT is OpenVMM's own on every backend, so this change is backend-independent; NVX's microVM tests on KVM, MSHV, and WHP run with it through microsoft/nvx#1558, which pins this head.