Repository navigation
[DO NOT REVIEW] vmcore: map VM times beyond the host clock to its latest time - #4820
Open
Pedro Henrique Penna (ppenna) wants to merge 2 commits into
Open
Pedro Henrique Penna (ppenna) wants to merge 2 commits into
Pedro Henrique Penna (ppenna) wants to merge 2 commits into
Conversation
VmTime is a wrapping 64-bit count of 100 ns units, which is_before and checked_sub order circularly: a time less than half the ring, 2^63 units or about 29,000 years, after another follows it. The host clock, pal_async's Instant, counts nanoseconds in 64 bits, about 584 years. TimerState::timestamp maps a VM time to the host time at which VM time reaches it: it adds the time's circular distance from the last start to the host time of that start, with Instant's Add, which panics when the sum overflows the host clock, so for any time more than about 584 years, less the host's uptime at the start, after the start. Every timeout passes through timestamp: set_next arms the keeper's host timer with it whenever a timeout becomes the earliest that is pending, and VmTimeAccess::host_time returns it. A timeout that far ahead therefore panics the task that arms it. microsoft/nvx#1038 reaches this with public calls alone: a timeout at 1.8e16 is armed while VM time is stopped, and VM time is restored to 1.58e19, which the timeout follows by 2.66e18 units, so that start() panics the time source's task (SecondaryKeeper::run) in TimerState::start. A device that sets a timeout 1,000 years ahead panics its own task. A guest can choose such a timeout where OpenVMM emulates the Hyper-V synthetic timers, as WHP does when it does not offload the enlightenments: a one-shot timer expires at the absolute reference time that the guest writes, which virt_whp turns into a VM-time timeout. Saturate the sum at the host clock's latest time, with Instant::saturating_add, since the host clock never reaches a time that it cannot represent. Every timer backend already accepts that deadline: a timeout just short of overflowing reached it before, and the epoll, kqueue, IOCP, thread pool, io_uring, and local backends saturate their own conversions of it. A timeout that far ahead thus keeps its waiter waiting, and an earlier timeout still replaces it. The documentation of timestamp and host_time states the new bound. Saturating the distance itself at a day instead, as the issue suggests after TimerState::poll's default deadline, would stop the panic but make the keeper spin: once VM time has run for a day since the start, the deadline of a timeout further ahead is in the past, so the host timer fires, wake re-arms it with the same past deadline, and poll loops with the timer state's lock held. The new tests arm a timeout 1,000 years ahead of a running keeper, and the issue's sequence of calls. They check that host_time returns the host clock's latest time, that the timeout keeps waiting, and that an earlier one still fires. Both panic with "duration too large" without the fix. Part of microsoft/nvx#1038. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TimerState::timestamp maps a VM time to the host time at which VM time reaches it, from the time's circular distance after the last start, and maps a time that VM time's circular order puts before the start to the host time of that start. The keeper's waiters, though, compare their timeouts with the current time: poll_timeout waits for a timeout that is not before now, and wake re-arms the host timer for one after now. Once VM time has run since the start, the two disagree about a time nearly half the ring, about 29,000 years, after now: it follows now, but it is more than half the ring after the start, which the circular order puts before it. Such a timeout spins the keeper. set_next arms the host timer with the host time of the start, which has passed, so the timer fires at once. TimerState::poll then wakes, finds the timeout still after now, re-arms the same deadline, and loops without returning, with the timer state's write lock held, so every VmTimeAccess of the time source blocks. A test that sets such a timeout spends 9.99 s of user CPU time in 10 s, on Linux, until it is killed. VmTimeAccess::host_time returns the same past host time, which OpenHCL's VP loop uses as the deadline of its own timer. As with a timeout beyond the host clock, a guest can choose such a timeout where OpenVMM emulates the Hyper-V synthetic timers, since a one-shot timer may expire up to half the ring after the current reference time. Map a time that the circular order puts before the start, but after the current time, to the host clock's latest time instead: it is in the future, further ahead than the host clock can represent. timestamp reads the current time only for a time before the start, so the common case costs nothing more. A time before the start and not after now is in the past, and still maps to the host time of the start. The documentation of timestamp and host_time says which times each bound covers. The new test sets a timeout at the latest time that follows now, after VM time has run for 10 ms since the start. It checks that host_time returns the host clock's latest time, that a time in the past before the start still maps to the start, that the timeout keeps waiting, and that an earlier one still fires. Without the fix, host_time returns the host time of the start, and without that check, the wait never ends. Part of microsoft/nvx#1038. 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
Oct 30, 2026
Record the two commits that the previous commit pins as positions 368 and 369 of the microVM chain, and move the NVX pin to 5e0ab772e. Positions 368 and 369 make OpenVMM's VM time keeper map a timeout beyond what the host clock can represent to the host clock's latest time: one more than about 584 years after the last start no longer panics the task that arms it, and one nearly half the ring after now, which VM time's circular order puts before the start, no longer arms the host timer in the past. They change only vmcore's vmtime.rs, which upstream main and the fork base have alike, and apply to upstream main as they are; say so in the table's remarks. The pin is now 212 commits past the mapped tip. Position 367, bcfc47cff, is the head of feature/microvm/dev since microsoft/openvmm#4818 merged positions 366 and 367. Say so in the scope, and name microsoft/openvmm#4820 as the proposal of positions 368 and 369. Refs #1038 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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 the VM time keeper's panic of microsoft/nvx#1038, and a spin of the keeper next to it. A timeout that VM time reaches beyond what the host clock can represent, more than about 584 years ahead, now maps to the host clock's latest time instead of panicking the task that arms it. So does a timeout nearly half the ring, about 29,000 years, ahead of now, which mapped to a host time in the past and made the keeper spin.
Based on
feature/microvm/dev. This branch is two commits directly onbcfc47cff, the head offeature/microvm/devsince #4818 merged, which microsoft/nvx'sdevpins. microsoft/nvx#1042 pins this branch's head.Why
VmTimecounts 100 ns units in 64 bits and orders them circularly: a time less than half the ring after another follows it.TimerState::timestampmaps a VM time to the host time at which VM time reaches it, by adding the time's circular distance from the last start to the host time of that start.set_nextarms the keeper's host timer with it, andVmTimeAccess::host_timereturns it. Two kinds of time broke it:Instantcounts nanoseconds in 64 bits, about 584 years, and itsAddpanics on overflow, so a timeout more than about 584 years after the start panicked the task that armed it. In vmtime: armed timeout + restore to a large time panics the keeper task on start() nvx#1038's sequence, a timeout is armed while VM time is stopped, and VM time is restored to a time that the timeout follows by 2.66e18 units, sostart()panics the time source's task with "duration too large".timestampmapped it to the host time of the start, in the past. The waiters compare with now, though, and keep waiting: the host timer fired at once,wakere-armed the same deadline, andTimerState::polllooped forever with the timer state's write lock held.A guest can choose both kinds where OpenVMM emulates the Hyper-V synthetic timers, as WHP does when it does not offload the enlightenments: a one-shot timer expires at the absolute reference time that the guest writes, up to half the ring after the current one.
The issue suggests clamping the distance at a day, after
TimerState::poll's default deadline. That stops the panic but makes the keeper spin, as above, once VM time has run for a day since the start, since the clamped deadline is then in the past: aTimerStatestarted two days ago with a timeout 1,000 years ahead never returns frompollwith it.Saturation (
342887c99)timestampadds the distance withInstant::saturating_add, so a time beyond the host clock maps to its latest time, which the host clock never reaches. Every timer backend already accepts that deadline: a timeout just short of overflowing reached it before, and the epoll, kqueue, IOCP, thread pool, io_uring, and local backends saturate their own conversions of it.test_timeout_beyond_host_clockarms a timeout 1,000 years ahead of a running keeper and checks thathost_timereturns the host clock's latest time.test_timeout_beyond_host_clock_after_restorefollows the issue's sequence. Both check that the timeout keeps waiting and that an earlier timeout still fires, and both panic with "duration too large" without the fix.Times nearly half the ring ahead (
5e0ab772e)timestampmaps a time that the circular order puts before the start, but after the current time, to the host clock's latest time too. It reads the current time only for a time before the start, so the common case costs nothing more. A time before the start and not after now is in the past, and still maps to the host time of the start.test_timeout_nearly_half_the_ring_aheadsets a timeout at the latest time that follows now, after VM time has run for 10 ms, and checks that a time in the past before the start still maps to the start. Without the fix,host_timereturns the host time of the start, and without that check, the wait spins until nextest kills the test.Validation
cargo nextest run --profile agent -p vmcore(32 and 33 tests passed) andcargo doc --no-deps -p vmcore. On Windows, alsocargo clippy --all-targets -p vmcore -- -D warningsandcargo xtask fmt --fix. All clean.bcfc47cff, on both, the new tests fail as described: the two overflow tests panic inInstant::add, the issue's with its backtrace (timestamp←set_next←wake←TimerState::start←SecondaryKeeper::run), and the wait for a timeout nearly half the ring ahead spends 9.99 s of user CPU time in 10 s until it is killed.x64-windows-amd-snp, two SNP tests panicked inopenhcl_boot, which does not depend onvmcore, with "Imported regions hash mismatch", as that job did in at least seven other pull requests' runs of the past day. Inx64-windows-intel,hibernate_tokenfailed withos error 10022in petri's harness before OpenVMM started.