Repository navigation
openvmm: zero-extend wide reads of the RTC's ports - #1563
Merged
Pedro Henrique Penna (ppenna) merged 2 commits intoNov 13, 2026
Merged
Conversation
OpenVMM's emulated CMOS RTC stored only the first byte of a read of its ports, 0x70 and 0x71, although its comment says that a wider read returns the register's byte zero-extended to the size of the access. The other bytes kept what the backend's buffer held. On MSHV and WHP, OpenVMM passes the guest's EAX as that buffer, so a 16- or 32-bit IN from either port kept the guest's upper bytes of AX or EAX, as an 8-bit IN does (#1151). KVM zeroes its PIO data before it exits to the VMM for an IN, which hid the defect there. Pin 60e160369b1f1f10543e0699d4ae1e95810a1e62, the head of microsoft/openvmm#5072, one commit on 6f57a6d61, the head of feature/microvm/dev: - 60e160369 chipset: zero-extend wide reads of the RTC's ports Rtc::io_read now reads the register, then zeroes the whole buffer and stores the register's byte in its first byte. A one-byte read returns the same byte as before, and a read of another port still fails without touching the buffer. Evidence: in NVX's Alpine microVM, a guest program selected status D, 0x80, and read each port with 16- and 32-bit INs that start with EAX = 0xdeadbeef, three times per boot. With OpenVMM 6f57a6d61, from the release v0.1.0-dev.8e9e9eb74cf2, every read returned 0xdeadbe0d from port 0x70 and 0xdeadbe80 from port 0x71 on nvx-mshv-test and nvx-whp-test. On nvx-kvm-test, the reads returned the zero-extended values, 0xdead000d and 0x0000000d, and 0xdead0080 and 0x00000080, since a 16-bit IN keeps EAX[31:16]. With OpenVMM built from the pin, all three hosts returned the zero-extended values. The revision's new chipset test fails at 6f57a6d61, where a 2-byte read of port 0x70 returns [0x40, 0xa5], and passes at the pin with the crate's other 112 tests, on Windows and on Linux. Tests: with OpenVMM built from the pin, the microVM scenarios guest-boot, lifecycle, which reads the RTC's status B one byte at a time, interrupt-map, snapshot-core, restore-processors, smp-snapshot, restore-downtime, time-abi-conformance, and time-host-correction pass with 1 and 2 processors on nvx-kvm-test, nvx-mshv-test, and nvx-whp-test. 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 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>
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues remain for the OpenVMM pin and RTC fix documentation update.
No open issues 🎉
What changed in this PR
This PR updates NVX’s OpenVMM pin to fix zero-extension of wide RTC port reads, addressing #1151, and records the fix in the upstream roadmap.
Changes:
- Updates the documented pin to
60e160369at position 267. - Updates chain counts and adds the RTC fix to the commit mapping.
- Documents that the fix can be upstreamed independently.
| File | Description |
|---|---|
| doc/openvmm-upstream-roadmap.md | Records the new pin, position 267, and the RTC fix’s upstream applicability. |
Review effort: Balanced
Pedro Henrique Penna (ppenna)
deleted the
agents/issue-1151-rtc-wide-reads
branch
November 13, 2026 04:16
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.
Fixes #1151
What and why
A guest's 16- or 32-bit
infrom the CMOS RTC's index port0x70or data port0x71returned the register's byte with the guest's own upper bytes of AX or EAX left in place, as an 8-bitindoes, on MSHV and WHP. The RTC'sio_readsays that a read wider than a byte returns the register's byte zero-extended to the size of the access.Root cause, in OpenVMM:
Rtc::io_readstored the register's byte indata[0]and left the rest of the buffer as the caller passed it, and what that buffer holds depends on the backend:virt_support_x86emu::emulate::emulate_iopasses the guest's EAX as the buffer, so the guest's upper bytes survived the read.kvm_runPIO data area, as the issue says, but KVM zeroes it before it exits to the VMM for anin(emulator_pio_in_outinarch/x86/kvm/x86.c, in Linux 5.15, 6.1, 6.8, and master), so the reads were zero-extended there by accident. The issue's explanation of stale bytes from an earlier exit does not hold on these kernels.inspath throughx86emupasses a zeroed buffer on every backend.The stale bytes are the guest's own register bytes, or zeros, so no host data reaches the guest.
The pin, microsoft/openvmm#5072 at
60e160369b1f1f10543e0699d4ae1e95810a1e62, adds one commit tofeature/microvm/dev,6f57a6d61, #1066's pin:60e160369chipset: zero-extend wide reads of the RTC's ports.io_readreads the register first, so that a read of another port still fails withInvalidRegisterand leaves the buffer as it was, then zeroes the whole buffer and stores the register's byte in its first byte, as the DMA controller'sio_readdoes. A one-byte read returns the same byte as before, and a read of port0x71still reads the selected register once. Its test,wide_reads_are_zero_extended, reads both ports with 2- and 4-byte buffers that hold0xa5bytes.The NVX commits:
403184f58c4openvmm: pin the RTC's zero-extended wide reads. It changes only the gitlink, from6f57a6d61to60e160369.0e159a5808edoc: map the RTC's zero-extended wide reads in the OpenVMM roadmap. Position 267, unassigned.Decisions:
vmotherboard's dispatch zeroing it for every device: the chipset devices already own the whole buffer, as the PIC, the PIT, and the i8042 reject a wide access withInvalidAccessSize, and the DMA controller and the microVM devices fill the buffer.test_default,test_readonly, andtest_writeablealready cover throughget_cmos_data.Not in this change, found while auditing the read path:
Piix4CmosRtc::io_readinchipset_legacy, which the microVM does not use, also writes onlydata[0]for ports0x72,0x73,0x76, and0x77. Its other ports go toRtc::io_read, which this change fixes.inon MSHV and WHP.emulate_ioreturns RAX with bits 63:32 cleared after an 8- or 16-bitin, where the processor keeps bits 63:8 or 63:16, as KVM'skvm_fast_pio_indoes. This is read from the code, not reproduced.Validation
Local checks at this head,
0e159a5808e(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 (267 positions, 276 references).python scripts/openvmm_pin.py source 60e160369b1f1f10543e0699d4ae1e95810a1e62 --pr 5072: 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 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,python -m unittest discoveroverscripts/: not run locally. This change does not touchscripts/, and the gate takes over two hours on this shared machine. It is left to CI'sNVX CLI / LinuxandNVX CLI / Windowsjobs.openvmm-cigate,python scripts/nvx.py check-openvmm-ci --revision 60e160369b1f1f10543e0699d4ae1e95810a1e62:unit tests [x64-linux]passed, and the other required jobs are running in run 43352068114, which this pull request'sopenvmm-cijob waits for.OpenVMM package checks for
chipset, at60e160369: on Windows,cargo check,cargo clippy --all-targets(clean apart from the environment's LNK4199 linker notices),cargo doc --no-deps, andcargo nextest run --profile agent(113 passed), thencargo xtask fmt --fix(no changes); on Linux, on nvx-kvm-test,cargo nextest run --profile agentandcargo test --lib(113 passed each). At6f57a6d61, the new test fails: "port 0x70, 2 bytes",[64, 165]where[64, 0]is expected. On upstream main with the commit replayed, which applies without conflicts,chipsettype-checks and its 64 tests pass.Reproduction, in NVX's Alpine microVM: a static guest program that selects status D,
0x80, and reads each port with an 8-, a 16-, and a 32-bitin, each starting with EAX =0xdeadbeef, three times per boot, with identical results each time. A 16-bitinkeeps EAX[31:16], so0xdead00NNis its zero-extended result:inw 0x70inl 0x70inw 0x71inl 0x71dead000d0000000ddead0080000000806f57a6d61dead000d0000000ddead0080000000806f57a6d61deadbe0ddeadbe0ddeadbe80deadbe806f57a6d61deadbe0ddeadbe0ddeadbe80deadbe8060e160369dead000d0000000ddead00800000008060e160369dead000d0000000ddead00800000008060e160369dead000d0000000ddead008000000080The 8-bit reads returned
deadbe0danddeadbe80in every run. The "before" runs used a temporary worktree of8e9e9eb74cfon each host, with the artifacts and OpenVMM of the releasev0.1.0-dev.8e9e9eb74cf2, whichnvx-rs downloadinstalled and whose OpenVMM provenance is6f57a6d61(clean). The "after" runs used a temporary worktree of6df1012c3f9, whose tree,0a0e4918e, is this head's, with the same release's guest artifacts and OpenVMM built from60e160369(clean) bynvx.py build-openvmm --backend BACKEND, which replaced the release's binary:3d3e0af6on KVM,7564afc5on MSHV, andd753a8e6on WHP. The probe is a 1296-byte static executable, SHA-256b14268f0eea0b5ba16a277968a929bc0571c458b8119d6248def90ef0b25aec9, built with GCC 13.2.0 on nvx-kvm-test and typed into the guest throughrun_guest_script:The probe's source, built with
gcc -static -nostdlib -ffreestanding -fno-pie -no-pie -Os -mgeneral-regs-only -fno-stack-protector -fno-asynchronous-unwind-tables -fcf-protection=none -Wall -Wextra -Werror -Wl,--build-id=none -Wl,-z,noseparate-code -Wl,-z,noexecstack -sRemote runs at the head's tree, in the temporary worktree of
6df1012c3f9, with OpenVMM built from60e160369. The scenarios areguest-boot,lifecycle, which reads the RTC's status B one byte at a time through/dev/port,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 chipsetandcargo test -p chipset --libinopenvmm/3d3e0af67564afc5(musl)d753a8e6NVX's guests read the RTC one byte at a time, so the scenarios check that guests still boot, read the RTC, capture, and restore through the changed device on each backend; the probe is the reproduction. Bare-metal hosts and the remaining scenarios are left to CI.
CI at this head,
0e159a5808e: passed, run 43354885451, withOpenVMM CI of the pinned revision, which found that OpenVMM's CI of60e160369, run 43352068114, passed every job that NVX requires. That run'srun vmm-tests [x64-windows-amd-snp], which NVX does not require, failed in one of its 114 tests,multiarch::vmgs::hyperv_openhcl_uefi_x64_windows_datacenter_core_2025_x64_prepped_snp_vmgstool_copy_igvmfile, whose Hyper-V VM never started: the runner's Hyper-V failed to load the CVM firmwarevmfirmwarecvm.dll(0x80070002). The same test failed the same way at6f57a6d61, in microsoft/openvmm#5067's run 43331611028, so it predates this change.No existing test changes, so no
tests-modifiedlabel.60e160369b1f1f10543e0699d4ae1e95810a1e62.6f57a6d61, the head offeature/microvm/devafter [DO NOT REVIEW] chipset: reject a saved PIT control word with RW = 0 openvmm#5067 (chipset: PIT restore accepts a control word with rw = 0, then hitsunreachable!()on the next access #1066). There are no other prerequisites.5e3e2a1ab.feature/microvm/devfrom6f57a6d61to exactly60e160369b1f1f10543e0699d4ae1e95810a1e62. OpenVMM's merge button would squash the commit and leave the pin off the branch. This pull request then merges through the queue.