utils/vmbox/DOCS-rtc-wake.md
Krzysztof kuhy Rudnicki 694c013b6b
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file length / file-length (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
palette drift / palette (push) Has been cancelled
shared gate tests / gate unit tests (push) Has been cancelled
vmbox: UEFI sandboxes and 'vm wake' RTC-wake emulation for poweroff/hibernate; worktree copies skip gitignored files
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-01 12:29:54 +02:00

9.6 KiB
Raw Blame History

RTC wake emulation (vm wake) and UEFI mode

Tests the "PC wakes itself" flow (wake-alarm's rtcwake -m no -t <epoch> + hibernate or poweroff) in a sandbox:

vm new rw --uefi                       # or without --uefi (SeaBIOS, default)
VMBOX_MEM=2048 vm hibernate-setup rw   # swap + resume= (overlay only)
vm wake rw --accel 10 -- 'sudo rtcwake -m no -s 120 && sudo systemctl hibernate'
bash tests/rtcwake_e2e.sh [--uefi]     # the end-to-end proof, see below

Run anything that boots a guest under ~/.claude/scripts/capped.sh (CAP_MEM=4G CAP_CPU_PCT=20) with VMBOX_MEM=2048: a 4 GiB guest does not fit a 4 GiB cap.

Why the firmware half is emulated

Measured on this host (qemu 11.1.1, edk2-ovmf 202608, q35, 2026-10-01). The guest armed an alarm, went down, and we waited well past it:

Guest goes to Firmware CMOS after the alarm time QEMU
S3 (rtcwake -m mem) SeaBIOS — SUSPEND, then WAKEUP 20 s later — native
S5 (systemctl poweroff) SeaBIOS REG_C = 0xB0 (alarm flag set) stays shutdown
S4 (rtcwake -m disk) SeaBIOS REG_C = 0xB0 stays shutdown
S5 OVMF REG_C = 0xB0 stays shutdown
S4 (systemctl hibernate) OVMF REG_C = 0xB0 stays shutdown

In every S4/S5 row the guest had also set RTC_EN in ACPI PM1_EN (0x0520 / 0x0720) and AIE in REG_B — everything a real chipset needs. QEMU only acts on a wakeup request while the machine is suspended (S3); a powered-off machine has no wake path at all. So vm wake plays the board: it reads the alarm, waits, and presses the power button.

VM artefacts around a resume

All four were found by the e2e test, each broke a resume the guest itself had done correctly, and none exists on real hardware. A hibernate resume in QEMU is always a relaunch of the process, so any device state that lived only in the old process is gone when the guest's drivers pick up again:

  • kvmclock. A KVM guest's persistent clock is kvmclock, i.e. the HOST's wall clock. A cold boot still follows the RTC (rtc_cmos hctosys), but a resume does not: powered on with its RTC at the alarm instant, a guest resumed 102 s before its own alarm. Real hardware reads the CMOS on resume. vm hibernate-setup therefore adds no-kvmclock, and vm wake fails with exit 14 (clock-not-from-rtc) when the guest clock is behind the alarm it was powered on at, instead of passing silently.
  • TCO watchdog. Every resumed guest was reset ~20 s in (WATCHDOG action=reset; the native S3 wake hit the same ~34 s in). Measured on the ICH9 TCO block at 0x660: TCO_TMR (0x672) reads 0x0032 after a cold boot — the driver's 30 s heartbeat at 0.6 s/tick — and 0x0004, QEMU's 2.4 s reset default, right after a resume, while the resumed driver still reports timeout=30. A fresh QEMU process lost the value, the restored driver does not reprogram it, and its 15 s pings cannot beat a 2.4 s timer. vm wake powers on with -action watchdog=none (VMBOX_WATCHDOG_ACTION): expiries are still logged in events.jsonl (one per ping, every 15 s), but ignored. The guest's own watchdog setup is untouched.
  • 9p host share. Observed: after a resume every access to /mnt/hostrepo blocked, and the next poweroff sat 4.5 min in "A stop job is running for /mnt/hostrepo"; unmounting before and remounting after fails with "no channels available for device hostrepo", and unbinding the device after the resume hangs. Inferred, not verified: 9pnet_virtio does not re-create its virtqueue on restore. What works (measured): hibernate-setup's system-sleep hook unmounts and unbinds the device before hibernating, then rebinds and remounts it after.
  • Display (NOT fixed). virtio-gpu fences never complete after a resume: Xorg — the resumed one and a freshly started one alike — sits in dma_fence_default_wait. Anything that needs X after a resume cannot be tested here. guest/vmbox-x11.sh now bounds its one X call (i3 --get-socketpath), which had hung every later vm run; hibernate-setup installs that copy, since older base images bake in the unbounded one.

What vm wake does

  1. Boots the sandbox if needed, then set-action shutdown=pause over QMP — the runtime form of -no-shutdown. Launch defaults are untouched; only this run's machine pauses instead of exiting when the guest goes down.
  2. Runs the guest command over ssh and waits (bounded, VMBOX_WAKE_DOWN_TIMEOUT, 240 s) for QMP SHUTDOWN. SUSPEND_DISK right before it means hibernate (ACPI S4); anything else (reboot, panic) is exit 13.
  3. Reads the stopped guest's CMOS through HMP port I/O (lib/rtc_alarm.py): alarm sec/min/hour (0x01/0x03/0x05), REG_B (AIE, BCD/binary, 12/24 h), PM1_EN at 0x602, and the RTC itself via qom-get /machine rtc-time. Register C is never read — reading clears it. QEMU's FADT has no day alarm ("alarms up to one day"), so the wake instant is the next time the RTC matches hⓂ️s — the hardware's own comparison.
  4. AIE off → NO WAKE ARMED, the machine is left off, exit 9.
  5. Waits ceil(lead / accel) real seconds (--accel, default 1; refuses with exit 12 beyond --max-wait, default 900 s).
  6. Quits QEMU and relaunches it with -rtc base=<alarm instant> (one-shot VMBOX_RTC_ONCE; the sandbox's --rtc pin is never rewritten), on the same RAM, SMP and firmware the guest went down on — read from the running QEMU's /proc/<pid>/cmdline, because a resume on different RAM discards the image. For S5 that is a cold boot; for S4 the kernel resumes from swap. Exit 11 if ssh does not answer within 180 s.
  7. Prints RTCWAKE key=value lines (below) and WAKE: … after its alarm; exit 14 if the guest's clock came back behind the alarm (see below).

Relaunching instead of resuming the paused process is faithful, not a shortcut: in S4/S5 only the disk and the CMOS survive on real hardware, and both are carried over (the overlay is the disk; the RTC base is the clock).

Key Meaning
down poweroff or hibernate
rtc_at_down guest RTC when the harness read the CMOS
cmos_regs, aie, rtc_en raw registers and the two enable bits
armed_epoch, armed_utc decoded alarm (RTC read as UTC, like rtcwake)
lead_guest_s alarm − RTC at read time
accel, waited_real_s acceleration and the real wait
poweron_rtc RTC the machine was powered on with
back_after_real_s relaunch → ssh answering, wall clock
up_after_alarm_guest_s guest date +%s at first ssh − alarm
result woke (0), not-armed (9), never-down (10), not-back (11), too-far (12), wrong-stop:* (13), clock-not-from-rtc (14)

UEFI mode

vm new <name> --uefi stores firmware=uefi in meta and copies OVMF_VARS.4m.fd to the sandbox's efivars.fd; vm run <name> --uefi … overrides one launch (a BIOS sandbox gets its varstore on first UEFI boot). Paths come from edk2-ovmf's descriptor /usr/share/qemu/firmware/60-edk2-ovmf-x86_64-4m.json (VMBOX_OVMF_CODE/VMBOX_OVMF_VARS override). A missing OVMF is an error, never a silent BIOS boot.

  • The cloud image boots both ways unmodified: its ESP (/efi) carries EFI/BOOT/BOOTX64.EFI, the only loader a fresh varstore tries.
  • The varstore persists across launches (systemd keeps HibernateLocation there) and is re-copied by vm reset.
  • vm list shows a FW column. The base image is unchanged.

vm hibernate-setup

The base image has a 512 MiB btrfs swapfile and a systemd initramfs, but no resume=: a hibernated BIOS guest cold-boots and logs PM: Image not found. guest/hibernate-setup.sh grows /swap/swapfile to RAM + 512 MiB (btrfs filesystem mkswapfile), adds resume=UUID=… resume_offset=… no-kvmclock to GRUB_CMDLINE_LINUX_DEFAULT, regenerates grub.cfg (shared by the BIOS and EFI GRUB), reboots, and proves it: those args in /proc/cmdline and logind CanHibernate = yes. Overlay only — never vm build --force.

vm run <name> 'sudo systemctl hibernate' now reports VERDICT: hibernated (exit 8) instead of a DIRTY shutdown, and skips the exit-code recovery boot, which would have consumed the image.

What this proves, and what only the host can

Proven in the VM (by tests/rtcwake_e2e.sh):

  • the wake instant is armed correctly: the alarm decoded from the CMOS equals the guest's own /sys/class/rtc/rtc0/wakealarm;
  • it survives the guest going down, for poweroff and for hibernate — the value is read after the stop, with AIE and RTC_EN still set;
  • after a power-on at that instant the guest comes back: a boot-time unit runs after the alarm (poweroff), or the same boot resumes with a pre-hibernate process alive and the post-resume hook run (hibernate);
  • an alarm disabled before going down leaves the machine off (exit 9).

Not proven — host only:

  • that real firmware wakes at all. That is the whole emulated part. The host's S4 wake was verified separately (wake-alarm DOCS-wake-enforcement.md); S5 stays unverified until tested on the host.
  • firmware quirks: a BIOS setting that disables RTC wake, an RTC kept in local time, a board that ignores RTC_EN from S5, a day-of-month alarm (QEMU has none, so alarms > 24 h ahead cannot be modelled);
  • boot/resume duration on real hardware, and the real disk/ESP layout;
  • anything graphical after a resume (the guest display is dead, above), and how real GPU/audio/USB drivers come back from S4;
  • the guest's wall clock after resume follows the RTC only because of no-kvmclock; on the host it is the firmware's CMOS, as it should be.

Report a green run as "passed in vmbox, not verified on the host".