Skip to content

watchdog_core: poll the timeout action instead of blocking on it - #4634

Open
Robert Nowotny (bitranox) wants to merge 4 commits into
microsoft:mainfrom
bitranox:fix-watchdog-timeout-defer
Open

Robert Nowotny (bitranox) wants to merge 4 commits into
microsoft:mainfrom
bitranox:fix-watchdog-timeout-defer

Conversation

@bitranox

@bitranox Robert Nowotny (bitranox) commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

WatchdogServices::poll runs the platform's timeout action with pal_async::local::block_on, on the thread that polls chipset devices. For the guest watchdog, BaseWatchdogPlatform::on_timeout first writes the boot-failure flag to the guest watchdog's VMGS store, and the VMGS broker is spawned on that same thread. So the first timeout blocks the thread on work that only it can do. The configured action (reset, halt or exit) never runs, nothing is logged after "Encountered a watchdog timeout", and the guest stays frozen with all VPs idle, because its timers are served by that thread too.

The UEFI watchdog doesn't hit this, since its store completes without waiting.

This PR moves the platform into a future when the timer fires, and poll drives that future with its own context. The future hands the platform back when it finishes. Both watchdog devices wait for a running action in stop (WatchdogServices::finish_timeout_action), the same way #4633 finishes queued NVRAM writes, so state is never saved halfway through an action and the saved state doesn't change. A device is always stopped before it is reset, so WatchdogServices::reset asserts that no action is in flight. pal_async is now only a dev-dependency.

How we found it

We run a lot of end-to-end testing with OpenVMM on KVM. While working on #4633 we noticed the same block_on pattern here and wrote a test for it: an Ubuntu guest started with --guest-watchdog --guest-watchdog-action reset, wdat_wdt loaded (the firmware publishes a WDAT table), and the watchdog armed with a single write and never fed. Without this change the log stops at "Encountered a watchdog timeout" and the guest stays frozen; an inspect of the VM comes back with only unit metadata at its deadline, because the device thread never answers. With the change, the guest halts with reason Watchdog about 50 ms after the timeout, resets and boots again. 25 reboot and 25 reset cycles on a Windows guest and on a Linux guest showed no fd, thread or device regressions. After the follow-up commit we ran the watchdog test again, plus suspend/resume, a live snapshot, and 10 more reboot/reset cycles on each guest, all clean.

Tests

Five tests in watchdog_core drive poll with a platform whose on_timeout waits on a channel for permission, which stands in for a store served by the polling thread:

  • a timeout must leave poll with the action pending instead of blocking, and the action runs once the store answers,
  • finish_timeout_action runs an action that is still in flight,
  • a finished action hands the platform back, and a second timeout runs the action again,
  • a reset with an action still in flight trips the assert,
  • a reset cancels an armed timer: arm, reset, move the clock past the deadline, and no timeout fires.

A test in guest_watchdog checks that stopping the device finishes a running action.

The timer fires by restoring the stopped VmTimeKeeper past the deadline, so nothing waits on real time. With the block_on put back, the watchdog_core tests that fire a timeout hang. Without the call in stop, the guest_watchdog test fails, without the cancel in reset, reset_cancels_the_armed_timer fails, and without the assert, reset_with_an_in_flight_action_panics fails.

Stale timer after a reset

WatchdogServices::reset put the registers back to the unconfigured state but left the timer armed, so after a reset the guest didn't see coming, the old deadline fired in the next boot although nothing there had configured the watchdog. reset now cancels the timer. An orderly reboot doesn't hit this, because Linux's wdat_wdt stops the watchdog on the way down, but sysrq b (or a panic with reboot) does. VM time restarts at zero on reset, so the stale timeout comes back late: on an Ubuntu guest that armed a 120 s watchdog at 10 s of uptime and then ran sysrq b, it fired 141.6 s into the next boot without this change, and stayed quiet for the 188 s we watched with it.

WatchdogServices::poll ran the platform's on_timeout with
pal_async::local::block_on inside the device's poll. For the guest
watchdog, on_timeout first writes the boot-failure flag to its VMGS
store, and the VMGS broker runs on the same thread that polls chipset
devices. The first timeout therefore parked that thread for good: the
VPs were never halted, the configured action never ran, and the guest
froze with every VP idle. The UEFI watchdog was not affected because its
store completes without waiting.

The timeout now moves the platform into a future that poll() drives with
its own context, and the future hands the platform back when it is done.
A new saved-state field, timeout_pending (false for older states),
records that the action is still owed, so a state saved while it runs
still carries it out after a restore. reset() leaves a running action
alone. pal_async is now only a dev-dependency.
@bitranox
Robert Nowotny (bitranox) requested a review from a team as a code owner October 9, 2026 15:28
Copilot AI balanced review requested due to automatic review settings October 9, 2026 15:28

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

Reset followed by save can lose an in-flight timeout action during restore.

1 open finding
What changed in this PR

Reworks watchdog timeout handling to avoid blocking the device polling thread.

Changes:

  • Polls timeout actions asynchronously.
  • Persists pending timeout actions across save/restore.
  • Adds regression tests and scopes pal_async to tests.
File Description
watchdog_core/​src/​lib.rs Adds asynchronous timeout state management and tests.
watchdog_core/​Cargo.toml Moves pal_async to dev-dependencies.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread vm/devices/watchdog/watchdog_core/src/lib.rs Outdated
Drop the timeout_pending saved-state field. Both watchdog devices now
await WatchdogServices::finish_timeout_action() in stop() and reset(),
so state is never saved while a timeout action is still running, and
the saved state stays as it was.

WatchdogServices::reset() on its own still leaves a running action
alone, because the action owns the platform. The device reset finishes
it first.
Copilot AI balanced review requested due to automatic review settings October 11, 2026 21:22

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.

🟢 Approval recommended

The nonblocking state machine preserves platform ownership, lifecycle handling, and existing saved-state semantics with focused regression coverage.

0 open findings

1 resolved since last review

🧠 Review effort: Balanced

WatchdogServices::reset() put the registers back to the unconfigured
state but left the vmtime deadline armed. After a VM reset the guest
didn't see coming (sysrq b, or a panic with reboot), the old deadline
fired in the next boot, although nothing there had configured the
watchdog. reset() now cancels it.

reset_cancels_the_armed_timer arms the watchdog, resets it, moves the
clock past the deadline and requires no timeout.
Copilot AI balanced review requested due to automatic review settings October 11, 2026 22:17

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.

🟢 Approval recommended

The asynchronous lifecycle handling is consistent, preserves save-state behavior, and is covered by focused regression tests.

0 open findings

🧠 Review effort: Balanced

@github-actions

Copy link
Copy Markdown

Comment on lines +154 to +155
// An in-flight timeout action keeps running: it owns the platform, so
// dropping it would lose the platform, and the timeout did happen.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we actually get here with an in-flight action? Shouldn't stop have run by now?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If so, I'd rather we assert that as an invariant here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it can't. Devices are always stopped before they're reset, and stop already runs finish_timeout_action(), so there's never an action left by the time reset() runs. Added the assert in 7e6d205 and dropped the extra finish_timeout_action() calls from both devices' reset(). The test that reset with an action still in flight is now a should_panic test for the assert.

Devices are always stopped before they are reset, and stop already
finishes the timeout action, so reset never sees one in flight. Assert
that instead of handling it, and drop the extra finish_timeout_action()
calls from the guest watchdog's and UEFI device's reset.
Copilot AI balanced review requested due to automatic review settings October 14, 2026 10:05

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 documented standalone reset behavior directly contradicts the implementation and its tests.

1 open finding

🧠 Review effort: Balanced

Comment thread vm/devices/watchdog/watchdog_core/src/lib.rs
@github-actions

Copy link
Copy Markdown

@jstarks John Starks (jstarks) self-assigned this Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants