Skip to content

The x86-64 wall clock is the loader's reading of firmware, and the CMOS driver is gone - #1213

Merged
Japabu merged 3 commits into
mainfrom
cmos-wall-clock-from-loader
Nov 11, 2026
Merged

Japabu merged 3 commits into
mainfrom
cmos-wall-clock-from-loader

Conversation

@Japabu

@Japabu Japabu commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The loader asks firmware's GetTime on every architecture and hands the instant in KernelArgs::wall_clock_*. AArch64 anchored its wall clock on that. x86-64 ignored it and read the CMOS itself, with the FADT's century register, so two readers kept one clock and the field was written and never read (issues/x86-64-reads-the-cmos-for-a-wall-clock-the-loader-already-hands-it.md).

The CMOS driver had a second reader: the clock source took the RTC's update-ended edges on IRQ8 as a third reference.

What changes

  • The wall clock. clock::init_wall reads the loader's handoff on both architectures. arch/x86_64/rtc.rs, arch/aarch64/rtc.rs, acpi::rtc_century_register and toyos-acpi's century decode are deleted.
  • The driver goes whole. IRQ8, vector 0x26, the census's rtc source, the edge store, and kernel::clock::reference's Edges and third_says are deleted with it.
  • What fell out with it. time::Bound, whose one user was the CMOS read's wait, and the per-architecture census hook, whose one source was IRQ8.
  • Ports 0x70/0x71 stay refused to the acpi claim's holder: a write there sets the clock firmware answers the loader from.
  • The manual (architecture/boot.md) says the kernel drives no real-time clock.

git ls-files 'kernel/src/*.rs': 214 files and 55,303 lines on main ad5db5a65, 211 files and 54,823 after (−480). kernel/pure loses 214 more. The whole diff is +521 −990, the additions mostly the two rows, their host cases and the record.

The third reference: what it protected, and why it goes

What it did. Where the TSC and the HPET disagree, the ACPI PM timer arbitrates. If the PM timer sides with the HPET, either the TSC stepped or both chipset counters stopped together. The RTC's edges, on their own 32.768 kHz crystal, chose between those two:

RTC's reading across the span Verdict Action
near the step TSC stepped, "confirmed" slew the first forward step, demote on a second
neither, or no edge yet TSC stepped, "unconfirmed" the same
near zero both chipset counters stalled leave the span out, do nothing

So the RTC changed the clock's action in exactly one case, the third row.

What the documents say of that case.

  • The TSC may not step: "The invariant TSC will run at a constant rate in all ACPI P-, C-, and T-states" (Intel SDM Vol. 3B §20.17.1, fetched). AMD's APM states the same of TscInvariant (Vol. 3, CPUID Fn8000_0007_EDX[8]; recalled, amd.com refuses the fetch).
  • The chipset's counters may not stop in S0. The HPET's main counter "is permitted, but not required, to run during S1 or S2 states" (IA-PC HPET 1.0a §2.3.3, fetched). The PM timer counts "until the system's 14.31818 MHz clock is stopped upon entering its Sx state" (ACPI 6.5 §4.8.3.3; uefi.org answers automated clients 403, and the reviewer verified the sentence against an archived copy).

A TSC step and a joint stall of both counters are each a violation, and no document prefers one, so only machines decide. One shape takes less: ACPI makes the PM timer optional (the same §4.8.3.3), so on a machine without a counting one, the HPET alone standing still in S0 is read as the TSC's step. Before, the RTC decided alone there.

What the machines say. In every boot published to the evidence repository (309 branches searched):

  • The third row, "the chipset's counters moved … with the TSC, so the span was left out", occurs zero times.
  • The AMD bench's real 1.7 ms step is "confirmed" in 18 published lines, 16 of them with the RTC within 10 µs of the other two, and "unconfirmed (no RTC edge either side)" in 8, where the step came before enough edges. Some boots are published on more than one branch.
  • Staged 10 ms steps under QEMU read from 4.2 to 11.7 ms on the RTC: 16 lines "confirmed", 3 "unconfirmed".

The RTC has never changed an action on either bench. Its one result was settling, on the AMD bench, that the TSC moves and the FCH's clock does not stop. That result stays recorded in issues/the-amd-benchs-tsc-steps-1-7-ms-forward-once-a-boot.md.

Whether another reference gives the same discrimination. None present does.

  • The HPET and the PM timer are the two counters in question. On the AMD bench they share one clock by their periods (HPET 69,841,278 fs is 14.318 MHz; the PM timer is that over four). On the T14 the HPET reads 52,083,333 fs, 19.2 MHz, so it is not on the 14.318 MHz clock there; whether the two share a source upstream is not known.
  • The LAPIC timer is one-shot and rearmed at every scheduler deadline, so it has no free-running count to compare. Making it one would be new kernel machinery, and its clock is the core's own.

Exactly what is lost. Attribution, not detection. Every disagreement is still found within a second and logged with every counter's span, and a wrong TSC is never kept where main demoted it. A sound TSC is slewed or demoted to a stalling HPET, logged DEMOTED, in three shapes main survived while its edges worked:

  • (a) the HPET and the PM timer stall together twice in one boot: the first is slewed out of the clock, the second demotes;
  • (b) they stall together once after the boot's one slew is spent, which the AMD bench spends on its real step in every boot;
  • (c) the HPET alone stalls twice on a machine with no counting PM timer.

The record is issues/chipset-counters-that-stall-together-read-as-a-tsc-step.md, with the evidence, the owner and the exit.

What is gained besides the lines. A step is slewed in the sample that finds it, where it used to wait up to five seconds for edges while readers saw the stepped clock. cpu0 takes one interrupt a second fewer. A step is one log line, not two.

This is my decision on the evidence above, as the brief asked; it is not an owner ruling.

Tests

  • wall_clock_is_firmwares (metal, new, rides testcases), two claims with their own failures:
    1. ToyOS's: every dated line of the log is the loader's reading of GetTime, carried on by the log's own stamps from the reading's counter at the boot's rate. The allowance is what the four divisions between the reading and a line's date drop, less three seconds and plus one, and a tenth of a second for retunes. No host clock is read.
    2. The bench's: firmware's reading lies in the span the host's clock saw the boot run in, 300 s either side. A red says the bench's hardware clock stands off the host's and that the design requires UTC. The metal loop records the host's clock at readback as back_unix_secs for this.
  • a_stalled_hpet_is_told_by_the_pm_timer (metal, new, rides shared-debug): hpet-stall staged against each bench's own PM timer, now the only arbiter.
    • Its job, hpet_stall_read, reads the served log until the clock has reported after the staged span, under the runner's bound, so the row holds its own boot. The AMD bench's whole shared-debug boot at 32e11074e showed why: the stall was staged at 5.387 s and the boot rebooted at 5.929 s.
    • The judge reads the staged span's record alone: left out, with the PM timer 10 ms nearer the TSC than the HPET is. The bench's own step passes it in another span, and in the staged one, where every counter disagrees and the PM timer still tells the stall.
  • Host:
    • a_date_off_the_loaders_reading_is_red_and_a_bench_off_utc_is_the_benchs: an anchor ten seconds off is red either way, a date one second outside the allowance is red, and a bench two hours east carries its reading and is red as the bench's.
    • a_staged_stall_is_judged_by_its_own_span: T14-shaped and AMD-shaped logs pass; a stall read as the TSC's, a PM timer standing with neither, no report after, never read and never staged are red.
  • Removed: the pure crate's five Edges/third_says cases with the code they tested, toyos-acpi's three century cases, and anchor's case for a reading after boot with its arm.

Not added: a metal boot for tsc-step. It demotes the TSC for the rest of its boot, so it cannot share one, and the AMD bench steps for real in every boot.

Checks

Head d7d8daf98, on main ad5db5a65. The bench images and their judging are at 8dbc5357c. Evidence: https://github.com/ToyOSOrg/evidence/tree/d91ed9f31cf8a0a6359dfef1673a88d9891c14f9/r3 (its README.md has every command, exit code and log; r1 and r2 are the earlier rounds).

Check Revision Result
cargo run -- --ci host d7d8daf98 exit 1: 1 of 110 steps red, the build system, whose two failing tests are issues/the-llvm-environment-test-breaks-git-on-macos.md on a macOS host; clippy clean
cargo run -- --build-only, x86-64 and --arch aarch64 d7d8daf98 exit 0 both
Guest rows virt_wall_clock_utc a_stalled_hpet_is_left_out a_stepped_tsc_is_slewed_then_demoted, run 42979867354 d7d8daf98 exit 0: 3 passed, 3 total
Metal, T14: testcases and shared-debug whole 8dbc5357c exit 0: 21 passed, 0 failed
Metal, AMD bench: the same two boots 8dbc5357c exit 0: 21 passed, 0 failed

What the benches showed:

  • T14, testcases: 7,204 dated lines carry the loader's reading; firmware's reading is inside the span the host saw. No TSC STEP, no DEMOTED.
  • T14, shared-debug: the staged span is the "HPET moved" record, the HPET 10,005,528 ns apart and the PM timer 2,475 ns apart with 516,371 allowed; a report follows; the job exits 0.
  • AMD bench, testcases: 7,226 dated lines carry the loader's reading; firmware's reading is inside the host's span. The bench's own 1,694,650 ns step is written once, in the sample that finds it, and is slewed away twelve seconds later. No DEMOTED.
  • AMD bench, shared-debug: the bench's own step landed in the staged span, so the record is "all disagree": the HPET 11,698,294 ns apart and the PM timer 1,697,502 ns, 10,000,792 ns from each other. The span is left out and that step is not slewed in that boot. No DEMOTED; the job exits 0.

8dbc5357c to d7d8daf98 is a rebase over main's thirteen commits, #1210's KernelArgs layout among them, with one conflict in an issue file, and one commit of comments in six files. The bench boots ran main's code as of 9f6269832; the guest rows ran the wall clock's handoff through #1210's layout.

Risks and open points

  • The ACPI §4.8.3.3 sentence was recalled when written; the reviewer has since verified it against an archived copy of the page.
  • On the AMD bench the staged stall shared its span with the bench's own step. The row passes that shape by design, and that step goes unslewed in the actuator's boot.
  • kernel/src/syscall/dispatch.rs loses one stale comment line and sdk/abi/src/syscall.rs has two doc comments reworded; both are A move names the rights it delivers: a ring and a manifest's SysCap stay where they were given #1204's files, in other hunks.
  • The loader and KernelArgs are untouched, so nothing here meets the loader-named-facts change.
  • A metal readback written before this change names no back_unix_secs and is refused by name; its boot has to be run again to be judged by this harness.

🤖 Generated with Claude Code

@Japabu
Japabu force-pushed the cmos-wall-clock-from-loader branch from e03485e to 32e1107 Compare November 11, 2026 02:28
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1213 at 32e11074e, round 1

Architecture

One reader of firmware's clock instead of two, in shared code on both architectures: the loader makes the firmware call and refuses a date that does not exist, the kernel carries the reading on its counter. The CMOS driver, IRQ8 with its vector and census source, the FADT century decode, Edges/third_says and time::Bound go with no user left behind. That is the right ownership and a real simplification. Recounted: kernel/src 215 files / 55,378 lines to 212 / 54,899; production +119 −835 (kernel/src, kernel/pure, lib/acpi/src), tests +173 −76. The manual has no page on the clock's arbitration, so boot.md is the only page owed and it is updated.

Deleting the third reference is defensible and I do not ask for it back. What blocks is the two new metal rows (one cannot be green as registered, the other does not hold the claim made for it), the missing record of what was given up, and leftovers of the deleted reader.

Answers to what was asked

The account of the old logic is accurate. On main, settle returns before respond only for Third::References; Counter and Unsure (no edge included) act alike. One qualification: the arm was Some(CounterMoved) | None, so on a machine with no PM timer the RTC decided alone (B3).

No other consumer is left dangling. Calibration and the runtime check use the HPET only. Hard-lockup NMIs are LAPIC-delivered, so port 0x70's NMI-mask bit never gated them; the old driver wrote only indices below 0x80 and nothing writes the port now. The watchdog is the TCO. No resume path re-reads the clock. ioapic::route, gsi_for_isa_irq, describe and MSG_DEST keep other users. The kernel's census names and the harness's copy match by inspection.

What is lost, against main.

  • A wrong TSC is never kept where main demoted it: the only exit removed is "leave the span out". A misread by the RTC can no longer keep a stepped TSC.
  • A sound TSC is demoted to a stalling HPET in three shapes main survived while its edges worked: (a) two joint stalls of the HPET and PM timer in one boot; (b) one joint stall after the boot's single slew is spent, which the AMD bench spends every boot on its real step; (c) two stalls of the HPET alone on a machine with no counting PM timer.
  • On the AMD bench the HPET and PM timer share one clock, so their agreement against the TSC is one witness. The independent one is gone; issues/the-amd-benchs-tsc-steps-1-7-ms-forward-once-a-boot.md keeps its result.
  • SMIs hold every CPU while all three counters count on, so they widen a bracket and cause no disagreement. Unchanged.
  • From my recall, not verified: Intel client platforms whose HPET halts in the deepest package idle, with firmware that leaves the PM timer off, are publicly reported. That is shape (c). Neither bench is one today.
  • Acceptable because detection stays and is loud, the removed exit was never taken on metal, and the price was a driver, an interrupt a second and five seconds of stepped clock. It needs its record (B3).

Wall clock anchor. Validated at firmware's boundary in the loader (Civil::is_valid, 1970..=9999; a failed GetTime hands known = 0). With none the kernel logs it, SYS_CLOCK_EPOCH answers NotSupported and logkeeper writes undated lines. Any other known panics, which is right for the loader's own handoff. No zone is applied, as boot.md and the loader track say. Published boots show GetTime and the CMOS reading the same second on the T14 (pr-1189).

ACPI citation: verified. uefi.org refuses automated clients; the Wayback copy of its page (snapshot 2026-10-15) carries the sentence in §4.8.3.3, Table 4.14, TMR_VAL: "counts while in the S0 working system state … continues counting until the system's 14.31818 MHz clock is stopped upon entering its Sx state." The same section opens "This optional read-only register … if it is implemented on the platform" (B3). I could not fetch the HPET specification; its quote stands on the author's fetch.

Evidence. Host log read: 1 of 110 red, the build system, both failures in llvm::tests on the macOS git path the recorded issue describes; build/ is untouched, so independent. checks::a_wall_clock_off_the_hosts_fails_its_row passes in it, and kernel clippy ran with both actuator feature sets on both targets. Guest run 42945488277 is success at e03485e1b, 3 of 3; e03485e1b..32e11074e is one rewrapped paragraph of an issue file, so the run holds for the head. Beyond the bench labels the repository itself uses, the evidence carries no model string, host name, user or path.

A staged tsc-step boot on metal should not be required. What this change alters in the step path is logic the QEMU row and host cases hold, and the AMD bench's real step takes the changed path on metal every boot. A staged one demotes its boot, so it costs a boot a bench to show the same readings again.

Old readbacks refused by name: fine. Note for the orchestrator: a readback taken before this lands cannot be judged by a harness after it, for any row, so judge in-flight ones before rebasing or run them again.

Overlap. #1204 and #1210 touch the same files textually only; the branch merges cleanly with origin/main. init_wall reads wall_clock_* by name, so #1210's layout change does not reach it.

BLOCKERS

B1. tests/toyos.rs:1449, tests/common/faults.rs:226-237 — a_stalled_hpet_is_told_by_the_pm_timer cannot pass as registered, and on the AMD bench cannot pass at all.

  • Timing. The stall is staged at the clock's first report (a second of held spans), found by the next sample and followed by a report only at the sample after: about 3.2 s after the clock starts. A plain T14 boot in pr-1189 shows the cadence: HPET at 10.231, reports at 11.370 and 12.375. The row's arm, SELFTESTS, names no job, so asked by name its boot is reboot alone. Published shared-debug boots with one job end 0.8 s after the clock starts and write no report (pr-1189 round 3: clock at 10.650, TEST_START reboot at 11.464; round 2 the same; pr-1200, yesterday: supervisor at 10.860, reboot at 11.562). No whole shared-debug boot is published, so its length is unknown. The row's verdict is therefore decided by how long unrelated co-riders run, and its failure message blames the arming.
  • The AMD bench. Line 234 fails on any clock: TSC STEP: the TSC stepped in the boot. That bench writes one in every boot, so the row reds with the real step named as the stall's. The staged span also starts about a second after the clock does, near where testcases takes the real step after the hand-over; should the two share a span, the arbitration reads AllDisagree, neither needle is written and the real step is never slewed.
  • No host case holds this judge; one with an AMD-shaped log would have shown the second defect.
  • Needed: the row holds its own boot until its records exist, by an awaited event with a timeout; it judges the staged span, not the whole boot; host cases cover both.

B2. tests/toyos.rs:7957-7988, kernel/src/clock.rs:260 — wall_clock_is_firmwares does not hold "the kernel's anchor is the instant the loader said".

  • The kernel's line prints Civil::from_unix_secs(secs) of the handed field, not BOOT_SECS. handed != anchored compares two prints of one KernelArgs word.
  • The anchor is held only by the last dated line against the host, 300 s either side of a span of about 70 s. An anchor tens of seconds off passes, and that is the size of error a mis-carried loader counter would make (power-on to loader is about 10 s on the T14).
  • A red cannot tell the bench's setting from ToyOS, and the author expects one on the AMD bench for the bench's setting.
  • What the row should claim, in two parts with their own failures:
    1. ToyOS: the date on a line is the loader's reading plus what the log's own stamps say elapsed since that reading, to a second or two. No host clock, no dependence on the bench's zone.
    2. The bench's precondition: firmware's reading lies in the span the host saw, 300 s either side, failing as "this bench's hardware clock stands N s from the host's; the design requires UTC". With the bench set to UTC this is also the independent oracle for the loader's EFI_TIME decode.
  • 300 s is justified for part 2 only: it rejects every zone, and its other side rests on the bench's other system disciplining the hardware clock. It is not a bound on ToyOS.
  • "Red on the base by its first needle" is a renamed log line, not a control. With part 1 the control is a host case: an anchor ten seconds off is red.
  • The AMD bench's clock is set to UTC on the bench and recorded. The bound is not widened and the bench not exempted.

B3. Records — the compromise is in a module header only, and one account is incomplete.

  • kernel/src/arch/x86_64/clocksource.rs:12-18 is the only place the loss is written. Root's "No silent debt" wants it in issues/ with evidence (zero occurrences in 309 branches; the documents), owner (the clock) and exit, covering shapes (a) to (c).
  • ACPI makes the PM timer optional, so shape (c) takes one specification violation, not two. The header's "or a machine with none" sits beside "neither counter may stop in S0" as if that covered it. The PR body's "both events are violations" has the same gap; that correction is the orchestrator's.
  • issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md:98 — the track says a pull request deletes the lines it meets. This one adds the row that line names and rewords the line. Delete it, or say what the row does not hold.

B4. Leftovers of the deleted reader.

  • lib/wallclock/src/lib.rs:15 — "The RTC decodes its registers into this" is false now.
  • sdk/abi/src/syscall.rs:1311-1314 — None is described as "an RTC that is absent, wedged, …" and "the kernel reads the clock once". Neither is what None means now. Another open PR touching the file is no reason to leave a false contract: the hunks differ.
  • tests/checks/metal.rs:74 — the new BACK_AT was inserted under boot_file's doc line, so "A T14 boot's boot.txt as the loop writes it" now documents the constant and boot_file has none.
  • kernel/pure/clock/mod.rs:26-27 and a_reading_after_boot_is_carried_back_to_it — the arm for a reading after boot had the CMOS read as its only caller. The comment was changed to say so and the arm and its case stayed.

SUGGESTIONS

  • clocksource.rs header: once B3's record carries the reasoning, the paragraph can shrink to the invariant.
  • If a metalcase boot is going on a bench anyway, its ps2server row is the only run that compares the census's two lists; inspection is enough otherwise.

What must be green before landing

At the revision with B1 and B2 fixed, one testcases and one whole shared-debug boot a bench:

  • T14, testcases: wall_clock_is_firmwares (both parts), timer_calibration, smp_roster_and_tsc_trail; no TSC STEP and no DEMOTED in the log. This is the row the deleted issue's exit names.
  • T14, shared-debug: every member and selftest row, and a_stalled_hpet_is_told_by_the_pm_timer with the stall staged, left out as the HPET's with the PM timer inside its allowance, and a report after it.
  • AMD bench, testcases: the same three rows, with the bench's clock set to UTC first; the real step written once, in the sample that finds it, slewed away, and no DEMOTED. This is the metal evidence for the changed step path.
  • AMD bench, shared-debug: as the T14's, with the real step in the same log not redding the stall's row.

SEND BACK

Japabu added a commit that referenced this pull request Nov 11, 2026
…k's row carries the loader's reading, and the loss has its record

B1. `a_stalled_hpet_is_told_by_the_pm_timer` names a job, `hpet_stall_read`,
which reads the served log until the clock has reported after the span
`hpet-stall` staged, under the runner's bound: the AMD bench's whole
`shared-debug` boot at 32e1107 staged the stall at 5.387 s and rebooted at
5.929 s, before the next sample. The judge reads the staged span's record
alone: left out, with the PM timer 10 ms nearer the TSC than the HPET is. A
step the bench's own TSC takes passes it in another span and in the staged
one, where every counter disagrees and the PM timer still tells the stall.

B2. `wall_clock_is_firmwares` is two claims. ToyOS's: every dated line is
the loader's reading carried on by the log's own stamps from the reading's
counter at the boot's rate, within what four divisions drop; no host clock.
The bench's: firmware's reading lies in the span the host saw the boot run
in, 300 s either side, and a red names the bench's hardware clock.

B3. `issues/chipset-counters-that-stall-together-read-as-a-tsc-step.md`
records what was given up, in its three shapes, with the evidence and the
documents; ACPI makes the PM timer optional, so the third takes one counter
out of specification. The clock's header keeps the invariant and names the
record. The guest-suite track loses the line for the row this adds.

B4. `anchor` loses the arm for a reading after boot, which only the CMOS
read made, and its case; three comments that still named the RTC say what
is true now; `boot_file` has its doc line back.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the cmos-wall-clock-from-loader branch from 32e1107 to 8dbc535 Compare November 11, 2026 03:11
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1213 at 8dbc5357c, round 2

No blocker remains in the code or the records. Landing waits on the four bench boots, which have not run at this head.

Architecture

Unchanged from round 1 and still right: one reader of firmware's clock, and the kernel drives no real-time clock. Round 2 adds no production mechanism. Its commit is +13 −24 in production (kernel/, lib/, sdk/), +205 −68 in tests and +59 −1 in issues/. The test growth is the two judges, their host cases and a 38-line job.

The rebase

Nothing semantic changed.

B1: resolved

The wait (tests/guest/rust-tests/src/bin/hpet_stall_read.rs) is an awaited event with a failure timeout and no fixed delay.

  • It polls the served log's pipe and panics on expiry, so the job exits non-zero and job_passed reds the row by name.
  • The bound is 54 s since boot, under the list's 62.7 s, the same construction as acpi_counted.
  • The first report after the staging line cannot precede the staged span's record. The sample after the staging fails as a step and writes no report. Watch::take then reports at 2 s of held spans, so the wait ends about two seconds after the staging.
  • The served log starts at the boot's first line, so a job that starts after the staging still sees it. In round 1's AMD boot the job would have started near 5.63 s, with the staging at 5.387 s.

The judge (tests/common/faults.rs:231-250) holds the claim.

  • It reads the first disagreement after the staging line, which is the staged span's by construction.
  • A span read as the TSC's (TSC STEP or DEMOTED), a missing PM timer, a PM timer that stalled with the HPET, no report after, and no staging are each red. The host case covers all of them.
  • Sign checked: apart_ns is the counter's span less the reference's, so a stalled HPET reads +10 ms and the difference is +10 ms.

The real step landing in the staged span is sound.

  • The HPET then reads about 11.7 ms apart and the PM timer about 1.7 ms, so arbitrate answers AllDisagree and the span is left out.
  • The judge passes only if the HPET's and the PM timer's readings stand 10 ms apart. That is exactly "the PM timer did not stall with the HPET", whatever the TSC did.
  • The wrong arbitration here is CounterMoved, which writes TSC STEP or DEMOTED and is red.
  • Leaving that real step unslewed is acceptable. The arm is byte-for-byte main's, it needs the actuator to coincide with the step, and the clock stays 1.7 ms ahead with nothing comparing against it afterwards. In round 1's AMD boots the step was found in the first sample, before the first report, so the collision is not the usual case.

One limit, not a blocker. The judge cannot tell a real step from a PM timer up to 1 ms off the TSC, because both write "all disagree" with the 10 ms intact. On the T14, whose TSC takes no step, an "all disagree" record would pass and should still be read as a finding. See the T14 shared-debug condition below.

A guest row is not worth requiring.

  • Every part of the job except its composition is already held. The served log is used by eight sibling jobs on this image config, toyos_logstream::parse has host cases for dated, tid= and kernel lines, and the staging at 8dbc5357c built the binary into a 15-job list.
  • What is left is the event itself, which is the row's claim and only a bench shows.
  • A defect in the job fails loudly on the first bench boot, with its own panic text.
  • The boots are already being run, so the first shared-debug result answers this.

B2: resolved

The −3 s / +1 s allowance is derived correctly and stated at its site (tests/toyos.rs:7965-7974). Traced through the code, with at the reading's stamp, B the stamp at boot, t0 logkeeper's read and s a line's stamp:

  • anchor gives read + floor(B − at), which drops under a second.
  • SYS_CLOCK_EPOCH floors t0 − B, which drops under a second.
  • logkeeper subtracts floor(t0), which adds under a second.
  • logkeeper adds floor(s) per line, which drops under a second.
  • So a date less the carried reading lies in (−3 s, +1 s): three one way and one the other, as the comment says.

Checked against round 1's AMD excerpts by hand. testcases read 03:13:02 at counter 7258841100, which is 4.040 s at the boot's rate. Its lines land −109, −829, −56 and −343 ms from the carried reading. shared-debug lands −108, −323 and −867 ms. All are inside the allowance.

Tightness.

  • An error over 3.1 s either way is always red, because every dated line is judged and the per-line term sweeps its whole second.
  • The realistic mis-carry is a reading taken as true at the counter's zero. That errs by the power-on-to-loader time, 3.8 s on the AMD bench and about 10 s on the T14, and is red on every line of both.
  • Dropping at in favour of boot cannot be caught on these benches, and no test could: the loader-to-clock gap is about 0.1 s, so anchor returns the same second either way. Sub-second carrying is below what a whole-second clock shows.
  • Tighter needs the three fixed fractions, which the log does not carry. Not worth the machinery.

The bench's part is separate, reads the host's clock only there, and its red names the bench's hardware clock. The host case has an anchor ten seconds off red both ways, the two edges of the allowance red, and a bench two hours east red as the bench's.

B3: resolved

  • issues/chipset-counters-that-stall-together-read-as-a-tsc-step.md carries shapes (a) to (c), the documents, the evidence, the owner and an exit. It says ACPI makes the PM timer optional, so (c) takes one counter out of specification. Its frontmatter is valid under issues/README.md.
  • The module header is down to the invariant and names the record.
  • The guest-suite track line is deleted.

B4: resolved

  • All four leftovers are fixed, and Stat::mtime's doc with them.
  • anchor is one saturating expression and its dead case is gone. A reading after boot now reads as boot, which the loader's order makes unreachable.

Evidence

  • Host log, read: exit 1 with one red step of 110, the build system. Its two failures are llvm::tests::keyed_and_built and the_caller_s_environment_reaches_neither_the_build_nor_the_key, both on the macOS git/xcodebuild path the recorded issue describes. build/ is untouched, so it is independent. The other FAILED lines are the loom negative controls reaching their verdicts.
  • New host cases: checks::a_date_off_the_loaders_reading_is_red_and_a_bench_off_utc_is_the_benchs, checks::a_staged_stall_is_judged_by_its_own_span and the two remaining anchor cases pass in that log.
  • Guest run 42951062671: workflow_dispatch on guest-rows.yml, head 8dbc5357c, success, 3 passed of 3.
  • Identifiers: none found. Paths are masked as <wt>, <scratch>, <home> and <tmp>. The instrument lines read <cpu model>, 12 core(s). No host name, user, address or model string appears.
  • Metal: staged at 8dbc5357c with --metal-readback, exit 2, nothing judged. The AMD excerpts are at 32e11074e and are information only, as the README says.

SUGGESTIONS

  • tests/toyos.rs:7972-7973 — "a tenth of a second more … far under what they move a boot's stamps" has the relation inverted. Retunes and a slewed step move a boot's stamps about 3 ms, which is far under the tenth. The code is right. One word, on any next push.
  • tests/common/faults.rs:243 — the 9..=11 ms window is unexplained beside "10 ms". One clause would do: the two counters' allowances over a second, about 0.52 ms each.
  • The same kind of leftover as B4, which round 1 did not list: lib/wallclock/src/lib.rs:40,62, lib/fat32/src/time.rs:7, system/fileserver/src/volume.rs:400, and tests/guest/rust-tests/src/bin/file_mtime.rs:9,129 still say the RTC where the loader's reading of firmware is meant.
  • The QEMU row a_stalled_hpet_is_left_out could run the job and use stalled_hpet_left_out, leaving one judge where there are two. Optional.

What must be green before landing

One testcases and one whole shared-debug boot a bench, at 8dbc5357c or a descendant that changes comments only.

  • T14, testcases:
    • wall_clock_is_firmwares, both parts, with its two [wall] lines printed.
    • timer_calibration and smp_roster_and_tsc_trail.
    • No TSC STEP and no DEMOTED in the log.
  • T14, shared-debug:
    • Every member and selftest row.
    • test_rs_hpet_stall_read exits 0.
    • The printed [hpet_stall] line is the "HPET moved" record, with the PM timer inside its allowance. An "all disagree" record here is a finding even though the row is green.
    • A report after it.
  • AMD bench, testcases:
    • The same three rows, with part 2 inside 300 s. Round 1's excerpt shows the bench now reads UTC.
    • The real step written once, in the sample that finds it, slewed away, and no DEMOTED.
  • AMD bench, shared-debug:
    • Every member and selftest row, and the job exits 0.
    • The staged span is left out with the PM timer 10 ms from the HPET.
    • The real step is in the same log, in another span or in the staged one, and does not red the row.
    • No DEMOTED.

A red of hpet_stall_read for its own reason, such as its bound expiring or a line it does not match, is a send-back when it appears.

LAND

Japabu and others added 3 commits November 11, 2026 09:28
…OS driver is gone

The loader asks firmware's GetTime on every architecture and hands the
instant with the counter it was true at. AArch64 anchored on it; x86-64
ignored it and read the CMOS itself, with the FADT's century register. Both
now anchor in `clock::init_wall`, which reads `KernelArgs::wall_clock_*`,
and `arch/x86_64/rtc.rs`, `arch/aarch64/rtc.rs`, `acpi::rtc_century_register`
and `toyos-acpi`'s century decode are deleted.

The driver's other reader goes with it. The clock took the RTC's
update-ended edges (IRQ8, vector 0x26) as a third reference where the HPET
and the ACPI PM timer both disagreed with the TSC by the same amount: near
the step, or unsure, the TSC was taken to have stepped; near zero, the
chipset's two counters were taken to have stopped together and the span was
left out. Now that case is always the TSC's step.

What is lost is that one attribution, not a detection: every disagreement is
still found within a second and logged with all three spans. A machine whose
HPET and PM timer stop together while its TSC counts on would have its first
stall slewed out of the clock, which then stands the stall's length behind,
and a second would demote a sound TSC to the stalling HPET, loudly. Neither
counter may stop in S0 (ACPI 6.5 §4.8.3.3; IA-PC HPET 1.0a §2.3.3), and no
published boot of either bench has read that case: the RTC's verdict was
"confirmed" or "unconfirmed" in every one, and both take the same action.

A step is now slewed in the sample that finds it, not up to five seconds of
edges later, and written as one line. cpu0 takes one interrupt a second
fewer. `time::Bound`, whose one user was the CMOS read's wait, and the
per-architecture census hook, whose one source was IRQ8, go too.

Tests. `wall_clock_is_firmwares` (metal, riding `testcases`) holds the
kernel's anchor to the loader's reading and the boot's last dated line to
the span the host's clock saw the boot run in, which the loop now records
as `back_unix_secs`. `a_stalled_hpet_is_told_by_the_pm_timer` (metal, riding
`shared-debug`) stages `hpet-stall` against each bench's own PM timer, now
the only arbiter.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…k's row carries the loader's reading, and the loss has its record

B1. `a_stalled_hpet_is_told_by_the_pm_timer` names a job, `hpet_stall_read`,
which reads the served log until the clock has reported after the span
`hpet-stall` staged, under the runner's bound: the AMD bench's whole
`shared-debug` boot at 32e1107 staged the stall at 5.387 s and rebooted at
5.929 s, before the next sample. The judge reads the staged span's record
alone: left out, with the PM timer 10 ms nearer the TSC than the HPET is. A
step the bench's own TSC takes passes it in another span and in the staged
one, where every counter disagrees and the PM timer still tells the stall.

B2. `wall_clock_is_firmwares` is two claims. ToyOS's: every dated line is
the loader's reading carried on by the log's own stamps from the reading's
counter at the boot's rate, within what four divisions drop; no host clock.
The bench's: firmware's reading lies in the span the host saw the boot run
in, 300 s either side, and a red names the bench's hardware clock.

B3. `issues/chipset-counters-that-stall-together-read-as-a-tsc-step.md`
records what was given up, in its three shapes, with the evidence and the
documents; ACPI makes the PM timer optional, so the third takes one counter
out of specification. The clock's header keeps the invariant and names the
record. The guest-suite track loses the line for the row this adds.

B4. `anchor` loses the arm for a reading after boot, which only the CMOS
read made, and its case; three comments that still named the RTC say what
is true now; `boot_file` has its doc line back.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…, and two say their bound's reason

Comments only. The allowance's tenth of a second is for retunes and a
slewed step, which move a boot's stamps far less; the staged stall's
millisecond is the two counters' allowances over a second. `file_mtime`'s
panic text still says "RTC": it is a string its binary carries.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the cmos-wall-clock-from-loader branch from 8dbc535 to d7d8daf Compare November 11, 2026 07:29
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator verification at d7d8daf, after review r2's LAND at 8dbc535.

  • Metal precondition met. I ran the four boots of the 8dbc535 images; the implementer judged them with a tree at that revision: both benches exit 0, 21 passed, 0 failed. T14: both [wall] lines, timer_calibration, smp_roster_and_tsc_trail, no TSC STEP, no DEMOTED; [hpet_stall] is the "HPET moved" record with the PM timer 2,475 ns apart (516,371 allowed), a report after. AMD bench: the real step written once in the sample that finds it and slewed, no DEMOTED; in shared-debug the real step landed in the staged span ("all disagree", HPET and PM timer 10,000,792 ns apart), the span left out, the row green — the shape r2 judged sound. The bench's firmware clock reads UTC (0 s outside the host's span on both).
  • Since 8dbc535: a rebase onto main ad5db5a and one commit. git range-diff shows the review commit identical and the first commit differing in one hunk of issues/the-kernel-is-small-interrupts-post-and-threads-wait.md (a bullet both this branch and Where a boot came from is the supervisor's to read, not the kernel's #1210 made moot). The new commit is comments in six files (r2's wording suggestions and four leftover "RTC" comments); I read its diff and it has no non-comment line.
  • The boots ran on main as of 9f62698, before Where a boot came from is the supervisor's to read, not the kernel's #1210 moved KernelArgs' wall-clock fields; init_wall reads them by name, and guest run 42979867354 at this head (3 of 3) ran the handoff through the new layout. Host checks at this head: one red step, the recorded macOS llvm test; clippy clean.

Queueing.

@Japabu
Japabu marked this pull request as ready for review November 11, 2026 08:08
@Japabu
Japabu enabled auto-merge November 11, 2026 08:08
@Japabu
Japabu added this pull request to the merge queue Nov 11, 2026
Merged via the queue into main with commit cb3f9c0 Nov 11, 2026
11 checks passed
@Japabu
Japabu deleted the cmos-wall-clock-from-loader branch November 11, 2026 08:35
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.

1 participant