#942

devices/rtc_pl031: keep the guest clock right across a host sleep

Openjansavwants to merge intofrom

Problem

After the Mac sleeps, a guest's real-time clock is behind by the length of the sleep, and it stays behind until the guest reboots. Anything in the guest that sets the wall clock from the RTC (hwclock -s, or an agent that steps the clock to the RTC after a resume) sets it wrong. Certificates, make, TOTP codes and log timestamps then see a clock that is minutes or hours old.

Cause

The PL031 RTC returns "the time it was set to, plus the time elapsed since", and measures the elapsed time with std::time::Instant. On macOS, Instant reads CLOCK_UPTIME_RAW, which stops while the Mac sleeps. On Linux, Instant reads CLOCK_MONOTONIC, which stops during a host suspend in the same way.

Fix

Measure the elapsed time on a clock that keeps running across a host sleep: CLOCK_MONOTONIC on macOS (which, unlike Linux's, counts sleep) and CLOCK_BOOTTIME on Linux. Nothing else changes: the RTC still starts at the host's wall clock, and a guest write to RTCLR still sets it.

Evidence

Setup: M4 Pro, macOS 26.7, aarch64 Linux guest with 2 vCPUs and 2 GiB. A guest agent steps the guest's wall clock to /sys/class/rtc/rtc0/since_epoch every 5 s. The Mac was put to sleep from the Apple menu, then woken, and the guest clock was compared with the host clock.

BuildHost sleepGuest clock after the wake
upstream main109 s107 s behind
upstream main283 s283 s behind
this branch460 sright, within 1 s

With the fix, the guest's clock matched the host within a second of the wake, the same result Apple's Virtualization.framework gives on the same Mac. The guest's uptime (CLOCK_MONOTONIC in the guest) does not include the sleep, before or after the fix, which is also what Virtualization.framework does.

A host sleep cannot be triggered from a test, so there is no new test. The existing rtc_pl031 unit test passes.

Tests

make test, M4 Pro, macOS 26.7, dynamic linking:

RefResult
upstream main15/16 pass; virtiofs-misc fails in uid_gid_mapping_preexisting
this branch15/16 pass; the same failure, nothing new

The 17 skipped tests need FreeBSD assets, pjdfstest, iperf, TAP or passt, none of which this Mac has.

Related work

#762 (VM snapshot/restore) saves and restores the RTC through the Instant field that this PR replaces. Whichever merges second needs a small change: save host_clock_ns() - previous_now instead of the Instant difference.