Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions issues/ap-tsc-trail-is-assumed-and-never-checked.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,15 @@ QEMU synthesises every guest TSC from one host clock and cannot answer, so this
belongs on the metal session's table beside the control-register delta
(`issues/a-metal-session-runs-a-pre-flash-gate-first.md`). Owed by
whoever prepares that session: build the log line first, then read it.

**2026-10-28, part measured.** `smp` now logs each AP's `rdtsc` inside a
bracket of the BSP's, about 19.8 M cycles (11 ms) wide on the AMD bench, so
it answers "trails by more than the boot took" and no smaller offset. Under
the AMD bench's Linux, CPU 0 and each of the other 15 took turns reading
their TSCs through idle gaps up to 1 s (`tsc-probe warp`, PR #1079's
evidence). Every pair's worst step was positive, 54 to 342 cycles, which
bounds each CPU's offset from CPU 0 by 342 cycles, some 190 ns. That is that
firmware's synchronisation under Linux, not a ToyOS measurement: the log
line this file owes is still a bracket, not an offset.
`issues/the-system-clock-takes-the-tsc-on-one-reading-and-never-checks-it-again.md`
holds the clock's other unchecked assumptions.
52 changes: 52 additions & 0 deletions issues/the-kernels-50-ms-tsc-calibration-misses-the-tscs-rate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
status: open
kind: defect
opened: 2026-10-28
---

# The kernel's 50 ms TSC calibration misses the TSC's rate

The kernel measures the TSC against the HPET once, over 50 ms
(`kernel/src/arch/x86_64/hpet.rs`), and keeps time at that rate for the
whole boot. On the AMD bench (Ryzen 7 4800U) that rate is consistently low.
The TSC counted 1796.6248 MHz against the same HPET over 10 minutes and
against the ACPI PM timer over 5, the same in every idle and busy phase
(`tsc-drift` under the bench's Linux, 2026-10-28, PR #1079's evidence). All
but one of the kernel's calibrations in the session's AMD boot logs read
1796.506 to 1796.551 MHz, 41 to 66 ppm low, and 1796.519 to 1796.532 MHz in
most; the one other reads 1796.597 MHz, 15 ppm low. A rate read low is a
period kept long (`hpet.rs` stores HPET femtoseconds per TSC cycle), so every
interval reads long: at 58 ppm a ToyOS clock on that machine gains about 5 s
a day.

The error is a bias, not noise: every reading falls on one side. It is not
the period's resolution either: one step of the femtoseconds the kernel
stores is 3,227 Hz there, 1.8 ppm, and truncation biases the other way. An
inference, not measured: the HPET is read before the first TSC read and after
the last, so the HPET interval brackets the TSC interval by up to two
uncached MMIO reads. About 1.5 µs over 50 ms is about 30 ppm low, the sign
and size seen.

The T14 states its TSC's rate (CPUID 15H, 2,419,200,000 Hz), and the kernel
logs its own measurement beside it. Over 5 minutes against the HPET under
its Linux, the TSC counted 2419.2002 MHz, the stated rate within 0.1 ppm,
worst residual 0.2 µs (`tsc-drift`, 2026-10-28). The session's T14 boot logs
hold two clusters, 285 ppm under and 59 ppm over, in 345 and 250 log files
(some of them copies of one boot), and a few readings between and beyond
them (404 to 440 and 759 to 766 ppm under). Those are the kernel's error: the
285 ppm cluster gains about 25 s a day and the 59 ppm one loses about 5. The
bracketing above explains neither, which needs 14 µs and the other sign.
That the two clusters are two kernel revisions is an inference no one has
checked. On that machine a stated rate is there to be used, and right, and
the kernel keeps its measurement instead.

**Exit**: on the T14 and the AMD bench, the rate the kernel keeps is within a
bound the fix derives and logs. That bound is set by its window, its reads'
cost and the reference's own accuracy, against the rate measured over minutes
against the same reference: `tsc-drift` on the AMD bench, and CPUID 15H or
`tsc-drift` on the T14.

Whether the TSC may keep time at all, and whether it still does, is
`issues/the-system-clock-takes-the-tsc-on-one-reading-and-never-checks-it-again.md`.

**Owner**: the kernel's x86-64 clock (`kernel/src/arch/x86_64/hpet.rs`).
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
status: open
kind: defect
opened: 2026-10-28
---

# The system clock takes the TSC on one reading and never checks it again

x86-64's system clock is the TSC. The boot measures it against the HPET
once, over 50 ms (`kernel/src/arch/x86_64/hpet.rs`), and `clock::set_counter`
publishes that one period for the machine's life. Three things the clock
then relies on go unchecked:

- **That the TSC may keep time at all.** CPUID 80000007H:EDX[8], the
invariant TSC (Intel's SDM "Invariant TSC"; AMD's PPRs
`CPUID_Fn80000007_EDX[TscInvariant]`), is read only to admit deep idle
states (`kernel/src/arch/x86_64/idle.rs::facts`). The clock starts on a
TSC without it the same way.
- **That every CPU's TSC agrees.** Each AP's reading at start is checked to
fall inside a bracket of the BSP's, about 19.8 M cycles wide on the AMD
bench, 11 ms (the `SMP:` lines), and nothing compares the CPUs again
(`issues/ap-tsc-trail-is-assumed-and-never-checked.md`).
- **That the rate stays the one measured.** No later reading is set against
the HPET or another reference, so a step or a change of rate after boot
goes into every timestamp, timeout and deadline unannounced.

On the AMD bench (Ryzen 7 4800U) the one calibration is also 41 to 66 ppm
off (`issues/the-kernels-50-ms-tsc-calibration-misses-the-tscs-rate.md`).
That CPU states the invariant bit. Under its Linux, it counts one rate in
every phase over 10 minutes, and over 15 minutes across 16 CPUs and idle
gaps up to 1 s no CPU's TSC read behind another's and no step exceeded a
read's 1.2 µs. But in 5 of 30 boots' kernel logs that Linux's watchdog saw
the TSC some 1.7 ms ahead of the HPET over half a second, 2.6 s into boot in
the one boot whose dmesg was kept. Every time it was the same size, and it
came with a calibration that was right. The cause is unknown (`tsc-probe`
and the boots' figures, PR #1079's evidence). ToyOS's one calibration also
runs early in boot. A step inside its 50 ms would be a 3% error in every
timestamp after it, and a clock that never checks would not see it.

**Exit**: the kernel keeps time on the TSC only where every CPU states it
invariant. It bounds the CPUs' offsets at start to a figure it logs and
derives, and checks the rate and offsets against a reference while it runs.
When they leave their bound it fails clearly, or falls back to a counter
that holds and says so in the log. The runtime bound is derived from the
reference's and the reads' own error, so the check does not mistake its
own calibration for the TSC's: 12 of the 17 times the AMD's Linux watchdog
fired, it was Linux's own calibration that was wrong. Where a vendor
documents a cheaper detector, the check prefers it, such as `IA32_TSC_ADJUST`
for a write to the TSC (Intel's SDM, Vol. 3). Each part is shown on the T14
and the AMD bench.

**Owner**: the kernel's x86-64 clock (`kernel/src/arch/x86_64/hpet.rs`,
`kernel/src/clock.rs`).