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

179 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
```bash
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:m: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".