Skip to content

[DO NOT REVIEW] chipset: start each PIT low/high count with its low byte - #5061

Merged
Pedro Henrique Penna (ppenna) merged 1 commit into
feature/microvm/devfrom
copilot/nvx-issue-1063-pit-low-high-reload
Nov 12, 2026
Merged

Pedro Henrique Penna (ppenna) merged 1 commit into
feature/microvm/devfrom
copilot/nvx-issue-1063-pit-low-high-reload

Conversation

@ppenna

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

Copy link
Copy Markdown
Contributor

Makes the emulated 8254 PIT take each count of its low-then-high access mode low byte first, for microsoft/nvx#1063 and its duplicate microsoft/nvx#1146. In that mode, a counter's write flip-flop alternates between the low and the high byte of a count, and a control word for the counter resets it to the low byte, so a guest can write one count after another without a new control word, as Linux's PIT one-shot clock event device does for each event after its control word 0x38. Timer::write kept the low byte that it buffered once the high byte completed the count, so after the first count it took every byte as a high byte with that stale low byte.

One commit on c2c21ddb2, the head of feature/microvm/dev. microsoft/nvx#1551 pins this head.

Start each PIT low/high count with its low byte (5d656d854)

  • Cause. In the RwMode::LOW_HIGH arm of Timer::write, if let Some(low) = self.state.cr_low read the buffered low byte without clearing it, and only set_control cleared it. Control word 0x30, then the counts 0 and 16, left counter 0 counting 65536 rather than 16 (chipset: PIT LSB/MSB access mode keeps the buffered LSB after the MSB write nvx#1063), and control word 0x34, then the counts 0x1234 and 0x5678, left the count register at 0x5634 (openvmm: consecutive PIT low/high writes reuse the preceding count's low byte nvx#1146): a wrong period or one-shot length, and IRQ0 at the wrong rate, for a guest that reloads a count without a control word.

  • Change. self.state.cr_low.take(): the high byte takes the buffered low byte, which returns the flip-flop to the low byte of the next count. The low-byte-only and high-byte-only access modes, the reset by a control word, the counter latch and read-back commands, and the read flip-flop (read_high) are unchanged. The saved state already holds cr_low, so no field, format, or version changes. A state saved without this change after such a count holds the stale low byte, which a restore keeps, since it cannot tell it from a low byte that the guest wrote; the guest's next counts then go wrong until it writes a control word for the counter, as they did before.

  • Tests.

    • low_high_access_takes_each_count_low_byte_first: in each of the six modes, after a control word and the count 0x1234, the counts 0x5678 and 0x9abc, written without a control word, each reach the count register.
    • low_high_access_reloads_a_count_without_a_control_word, through the I/O ports with VM time set tick by tick (chipset: PIT LSB/MSB access mode keeps the buffered LSB after the MSB write nvx#1063): control word 0x30, bytes 0x00 0x00 0x10 0x00; IRQ0 is still low after 16 ticks and high after 17, the tick that loads the count and its 16 counts.
    • low_high_access_reprograms_a_running_rate_generator, through the I/O ports (openvmm: consecutive PIT low/high writes reuse the preceding count's low byte nvx#1146): control word 0x34, bytes 0x34 0x12; one tick later the latched count is 0x1234; bytes 0x78 0x56 while it counts; at the end of the current period, 0x1234 ticks later, the latched count is 0x5678.

    Without the fix, the three fail: the count register holds 0x5634; IRQ0 is still low after 17 ticks, with the latched count 0xfff0 of a count of 65536; and the latched count is 0x5634.

Not in this change: in mode 0, the 8254 also stops counting and drives OUT low when the first byte of a two-byte count arrives, and loads the count on the clock after the second byte. Timer::write acts only on a complete count, so the previous count keeps running until the high byte arrives. That predates this change and is a separate behavior; this change does not alter what the first byte does.

Validation

With Rust 1.97.1, on Windows, for chipset: 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 (104 passed), then cargo xtask fmt --fix (rustfmt, lints, verify-fuzzers, and verify-flowey complete, no changes). At c2c21ddb2, the three new tests fail as the Tests item says, and the test attached to microsoft/nvx#1063 fails with "mode 0 with count 16 did not reach terminal count after 100 ticks (latched count 0xff9d)"; with this commit, both pass.

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 the NVX pull request that pins this head.

See microsoft/nvx#1551.

In the low-then-high access mode (RW = 3), an 8254 counter takes a
count as two bytes, the low byte first, through a write flip-flop that
a control word for the counter resets to the low byte and that each
byte then toggles, so that a guest can write one count after another
without a new control word. Timer::write kept the low byte that it had
buffered in cr_low once the high byte completed the count, and only
set_control cleared it, so after the first count every byte was taken
as a high byte with that stale low byte. Control word 0x30, then the
counts 0 and 16, left counter 0 counting 65536 rather than 16
(microsoft/nvx#1063), and control word 0x34, then the counts 0x1234
and 0x5678, left the count register at 0x5634 (microsoft/nvx#1146). A
guest that reloads a count without a control word, as Linux's PIT
one-shot clock event device does for each event after its control
word 0x38, so programmed a wrong period or one-shot length, and IRQ0
at the wrong rate.

The high byte now takes the buffered low byte, which returns the
flip-flop to the low byte of the next count. The low-byte-only and
high-byte-only access modes, the reset by a control word, and the saved
state, which already holds cr_low, are unchanged. A state that was
saved without this change after such a count holds the stale low byte,
which a restore keeps, as it cannot tell it from a low byte that the
guest wrote: the guest's next counts then go wrong until it writes a
control word for the counter, as they did before.

Tests: in every mode, two counts written after a first one without a
control word each take their low byte first. Through the I/O ports, as
the issues reproduce it: counter 0 in mode 0, given the count 0 and
then 16, raises IRQ0 on the 17th tick and not on the 16th, and counter
0 in mode 2, given 0x5678 while it counts 0x1234, holds 0x5678 at the
end of the current period. Without the fix, the three tests fail with
a count register of 0x5634, with IRQ0 still low after 17 ticks of a
count of 65536, and with a latched count of 0x5634. The test attached
to microsoft/nvx#1063 fails at the parent commit with the latched
count 0xff9d of a count of 65536, and passes with the fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added the release-nvx Targets a branch under feature/microvm/. label Nov 12, 2026
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Nov 12, 2026
In its low-then-high access mode (RW = 3), OpenVMM's emulated 8254 PIT
kept the low byte that it had buffered once the high byte completed a
count, so after the first count it took every byte as a high byte with
that stale low byte. A guest that wrote a count without a new control
word so programmed a wrong one: control word 0x30, then the counts 0
and 16, left counter 0 counting 65536 (#1063), and control word 0x34,
then the counts 0x1234 and 0x5678, left the count register at 0x5634
(#1146).

Pin 5d656d854a8daca0f23833666b0b71229fe2ab73, the head of
microsoft/openvmm#5061, one commit on c2c21ddb2, the head of
feature/microvm/dev:

- 5d656d854 chipset: start each PIT low/high count with its low byte

Timer::write now takes the buffered low byte with the high byte, which
returns the write flip-flop to the low byte of the next count. The
saved state, which already holds that byte, keeps its format.

Evidence: on Windows, the revision's three new chipset tests, two of
which program counter 0 through the I/O ports as the issues do, fail
without the fix, with a count register of 0x5634, with IRQ0 still low
after 17 ticks of a count of 65536, and with a latched count of 0x5634,
and pass at the pin, with the 101 other tests of chipset. The test
attached to #1063 fails at c2c21ddb2 with the latched count 0xff9d of a
count of 65536, and passes at the pin. NVX's guests disable the PIT at
boot and never program it again, so they never wrote the second count
that the defect corrupted.

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>
Pedro Henrique Penna (ppenna) added a commit to microsoft/nvx that referenced this pull request Nov 12, 2026
The NVX pin moves to 5d656d854, position 263, 132 commits past the
mapped tip. Position 263, which microsoft/openvmm#5061 proposes for
feature/microvm/dev on its head, c2c21ddb2, is unassigned. The Scope
also says that microsoft/openvmm#5056 merged position 262.

It makes the PIT's count writes in the low-then-high access mode take
each count's low byte first, in Timer::write, code that upstream main
has, and adds three tests, in a file that PR10's position 022 and
position 139 also change, neither 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 with its
tests, which pass, so it can go upstream on its own.

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

Copy link
Copy Markdown

@ppenna
Pedro Henrique Penna (ppenna) merged commit 5d656d8 into feature/microvm/dev Nov 12, 2026
73 of 74 checks passed
syeedali pushed a commit to syeedali/nvx that referenced this pull request Nov 13, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-nvx Targets a branch under feature/microvm/.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant