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
4 changes: 3 additions & 1 deletion docs/src/architecture/boot.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
(the TCG memory-overwrite request), so on firmware that defines and honours
the request a reset leaves nothing of this boot in RAM for whoever boots
next.
- The hardware clock keeps UTC.
- It reads the firmware's time once, and the kernel's wall clock counts on
from that reading: the kernel drives no real-time clock. The hardware clock
keeps UTC.

## Signed images

Expand Down
4 changes: 2 additions & 2 deletions issues/a-safe-port-read-reaches-any-declared-port.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ its own: a read of COM1's data register takes the received byte, and a read of
its line status clears the error bits. `outb` and `outw` are `unsafe`, under a
contract that the caller owns what the write commands; the reads carry none.

**Evidence:** the code. `inb`'s callers are `arch::console_uart`, `arch::rtc`
and `arch::acpi_mode`; `inw`'s are `arch::acpi_mode`, `arch::power` and
**Evidence:** the code. `inb`'s callers are `arch::console_uart` and
`arch::acpi_mode`; `inw`'s are `arch::acpi_mode`, `arch::power` and
`arch::watchdog`.

**Exit:** no safe call reads a port it was not handed.
59 changes: 59 additions & 0 deletions issues/chipset-counters-that-stall-together-read-as-a-tsc-step.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
status: open
kind: defect
opened: 2026-11-11
---

# Chipset counters that stall together read as a TSC step

Where the TSC and the HPET disagree, x86-64's clock asks the ACPI PM timer
which moved (`kernel/src/arch/x86_64/clocksource.rs`). A PM timer that sides
with the HPET, or a machine with no counting PM timer, is taken as the TSC
having stepped. Nothing tells that from the chipset's counters standing
still together while a sound TSC counts on. The first forward step of a boot
is slewed out of the clock; a second, or one backward, demotes the TSC, and
every process's reading of now then asks the kernel, which counts by the
HPET.

**Owner:** the clock.

**What a sound TSC loses.** The disagreement is found within a second and
logged with every counter's span, and a demotion says `clock: DEMOTED`. What
is wrong is whose it is said to be:

- (a) The HPET and the PM timer stall together twice in one boot. The first
stall is slewed away, so the clock stands the stall's length behind; the
second demotes the TSC to the HPET that stalls.
- (b) They stall together once, after the boot's one slew is spent. The AMD
bench spends it in every boot
(`issues/the-amd-benchs-tsc-steps-1-7-ms-forward-once-a-boot.md`), so there
one joint stall demotes.
- (c) The HPET alone stalls twice on a machine with no counting PM timer.

**What the documents allow.** An invariant TSC runs at a constant rate in
every P-, C- and T-state (Intel SDM Vol. 3B, "Invariant TSC"). The HPET's
main counter may stop in S1 or S2, not in S0 (IA-PC HPET Specification 1.0a,
§2.3.3). The PM timer counts through S0 (ACPI 6.5 §4.8.3.3), and the same
section makes it optional. So (a) and (b) take the HPET and the PM timer both
out of specification, and (c) takes the HPET alone.

**Evidence.** Until #1213 the RTC's update-ended edges, off a crystal of
their own, chose between a step and a joint stall, and a span they read as a
stall was left out. Across the 309 branches of the evidence repository on
2026-11-11 that verdict was written in no boot of either bench or of QEMU:
the edges confirmed the step or could not tell, and both took the action the
clock takes now
(https://github.com/ToyOSOrg/evidence/tree/3ae2cecf2b74a953ef15aab6238181433d7165e3/r1,
`rtc-verdicts-published.txt`). Both benches and QEMU have a PM timer that
counts. On the AMD bench the HPET and the PM timer run off one 14.318 MHz
clock by their periods, so their agreement against the TSC there is one
witness.

**Not measured.** A machine whose chipset counters stall in S0. None is on a
bench.

**Exit:** a boot of a supported machine that writes `clock: TSC STEP` or
`clock: DEMOTED` for a span its TSC did not step in reopens the choice, and
the clock then takes a reference outside the chipset's clock for that
machine; or the owner rules that the loss stands and this record becomes
`rejected`.
17 changes: 9 additions & 8 deletions issues/the-amd-benchs-tsc-steps-1-7-ms-forward-once-a-boot.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ opened: 2026-10-31

On the AMD bench (Lenovo 82A2, BIOS DMCN38WW; Ryzen 7 4800U, CPUID
signature 0x860f01, Family 17h Model 60h), the TSC jumps 1.69–1.73 ms ahead
once in every boot. The HPET, the ACPI PM timer and the RTC agree on it
within 37 µs. The RTC runs off its own 32.768 kHz crystal, and the other two
off the FCH's 14.318 MHz clock. Linux on the same machine sees the event in
5 of 30 boots.
once in every boot. The HPET and the ACPI PM timer agree on it, and so did
the RTC's update-ended edges, within 37 µs, while the kernel timed them
(#1104 to #1165's boots). The RTC runs off its own 32.768 kHz crystal, and the
other two off the FCH's 14.318 MHz clock, so it is the TSC that moves and not
the FCH's clock that stops. Linux on the same machine sees the event in 5 of
30 boots.

**Owner:** the clock (`kernel/src/arch/x86_64/clocksource.rs`) and
acpiserver, which hands over the idle state the step lands in.
Expand All @@ -32,10 +34,9 @@ not known.

**What the clock does meanwhile.** Readers see the forward step: the clock
reads up to 1.7 ms ahead from the step until the sampler finds it, within a
second, and through the RTC's few seconds after. A forward step the HPET and
the PM timer agree on is then slewed away at no more than 500 ppm. This
happens whether the RTC confirms the step or cannot tell, and the correction
never steps back. A second step, or a backward one, demotes the TSC
second. A forward step the HPET and the PM timer agree on is then slewed away
at no more than 500 ppm, and the correction never steps back. A second step,
or a backward one, demotes the TSC
(`issues/a-demoted-tsc-keeps-the-kernels-clock-apart-from-processes.md`).

**Not read.** AMD's *Processor Programming Reference for Family 17h Model 60h*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,8 @@ and a compile-fail case.
- metal `tls_rebase_window`: a thread's TLS block is never reachable before its rebase.
- metal `kernel_heartbeat`: the kernel's heartbeat record arrives on its period; brings back
`kernel/src/heartbeat.rs`.
- metal `wall_clock_utc`: the wall clock reads the RTC as UTC.
- metal `spawn_cwd`: a spawn's working directory.
- host `log_conservation_smp2`: every record is read or counted lost.
- host `wall_clock_rtc_dead`, `wall_clock_rtc_unstable`, `wall_clock_no_century`,
`wall_clock_century_register`: each RTC shape decodes or is refused.
- unrepresentable `lock_across_switch_halts`: a lock guard cannot be held across a switch.
- unrepresentable `hash_seed_precedes_every_map`: no kernel map is built before its seed.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ state. Nothing here is ruled, and the ranking is a proposal.
| Scheduling: the core's driver, context switch, idle states, jobs, preemption | 20 | 3,160 | yes |
| Firmware tables, ACPI mode, reset and power-off, the machine's stop, what the loader named | 12 | 3,099 | yes for the tables, the `acpi` row and power; the rest below |
| Processes, threads, spawn | 8 | 2,736 | yes, less the userland text a spawn carries (below) |
| Clocks: TSC and generic timer, the wall clock, typed durations | 7 | 1,847 | the clock yes; the CMOS driver is a recorded defect (below) |
| Clocks: TSC and generic timer, the wall clock, typed durations | 7 | 1,847 | yes; the x86-64 CMOS driver and its IRQ8, 285 of these lines, went after this count |
| Counters, the diary, IRQ census, mask windows, boot deadline, hard lockup, chipset watchdog | 14 | 1,844 | counters and diary yes; the three bounds are unnamed and inferred to be the kernel's, since they bound the kernel |
| Paged objects and their pager calls | 2 | 1,325 | yes |
| Syscall entry and argument decode | 3 | 806 | yes |
Expand Down Expand Up @@ -399,9 +399,6 @@ Changes of the design, each inferred and none of them work until resolved:
- **The quit's walk**:
`issues/the-kernel-decides-who-is-asked-to-quit-and-who-is-killed.md`.

Already a defect with its exit: the x86-64 CMOS driver and its IRQ8 (285
lines), `issues/x86-64-reads-the-cmos-for-a-wall-clock-the-loader-already-hands-it.md`.

### The next removals, ranked (a proposal)

By mechanisms eliminated:
Expand Down Expand Up @@ -429,9 +426,6 @@ out, and a larger number needs one of the design changes above.
- `SYS_STACK_INFO` is called by `tests/guest/rust-tests/src/bin/std_threads.rs`
alone, and `SYS_VM_DECOMMIT` by two guest tests alone (`mmap_stress`,
`starveling`).
- `KernelArgs::wall_clock_*` is read by no x86-64 code. (Its
`boot_partition_start_lba` and `boot_partition_blocks`, read by one log
line, went with #1210.)
- `KernelStack::heap` (`kernel/src/loader/start.rs`) charges every thread
two strings at 64 KiB and a page, "as a rename's paths". No rename is
left: a spawn's path and arguments are the two strings today. After #1200
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ Each stage lands on its own, in this order.
8. **The kernel arms the TCO before anything unbounded.** The loader's arm is
the only bound today from `ExitBootServices` to `deadline::start` on
`boot-deadline=` boots, and to `arch::watchdog::init` on the others:
through `mm::init`, ACPI, interrupts, HPET calibration, the RTC read, PCI
through `mm::init`, ACPI, interrupts, HPET calibration, PCI
enumeration, `pcidev::publish` and `iommu::init`.
- The kernel arms as its first act, before `mm::init`: `toyos_acpi::ecam_base`
and the bus-0 scan `loader/src/watchdog.rs` does, through a boot map
Expand Down

This file was deleted.

29 changes: 9 additions & 20 deletions kernel/pure/clock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,16 @@ const NANOS_PER_SEC: u64 = 1_000_000_000;
/// by the first scheduler pass after each doubling span.
pub const TSC_REFINED: &str = "clock: over ";

/// What opens the kernel's report of a span in which the TSC moved apart from
/// the chipset's counters (`clock: the TSC reads <n> ns from the chipset's
/// counters ...`): the clock's word on a step the machine made, written as
/// the sampler finds it.
pub const TSC_STEP_READ: &str = "clock: the TSC reads ";

/// Unix seconds at boot, from a reading of `secs` true when the clock read
/// `at`, which is after boot where the kernel read the wall clock and before
/// it where the loader did; both are stamps, nanoseconds since the counter's
/// zero. Only the whole seconds between the two move the answer.
/// What opens the kernel's report of a span in which the TSC stepped against
/// the chipset's counters (`clock: TSC STEP: the TSC stepped <n> ns ...`),
/// written as the sampler finds it.
pub const TSC_STEP: &str = "clock: TSC STEP: ";

/// Unix seconds at boot, from the loader's reading of `secs`, true when the
/// clock read `at`, before boot; both are stamps, nanoseconds since the
/// counter's zero. Only the whole seconds between the two move the answer.
pub const fn anchor(secs: u64, at: u64, boot: u64) -> u64 {
if at >= boot {
secs.saturating_sub((at - boot) / NANOS_PER_SEC)
} else {
secs.saturating_add((boot - at) / NANOS_PER_SEC)
}
secs.saturating_add(boot.saturating_sub(at) / NANOS_PER_SEC)
}

#[cfg(test)]
Expand All @@ -38,11 +32,6 @@ mod tests {
const SECS: u64 = 1_993_799_665;
const BOOT: u64 = 50_000_000_000;

#[test]
fn a_reading_after_boot_is_carried_back_to_it() {
assert_eq!(anchor(SECS, BOOT + 3_700_000_000, BOOT), SECS - 3);
}

#[test]
fn a_reading_before_boot_is_carried_forward_to_it() {
assert_eq!(anchor(SECS, BOOT - 2_500_000_000, BOOT), SECS + 2);
Expand Down
Loading
Loading