From 8d53ed247d99a8a63c97060e11ed3eb009758160 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 03:58:24 +0200 Subject: [PATCH 1/3] The x86-64 wall clock is the loader's reading of firmware, and the CMOS driver is gone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/src/architecture/boot.md | 4 +- ...afe-port-read-reaches-any-declared-port.md | 4 +- ...hs-tsc-steps-1-7-ms-forward-once-a-boot.md | 17 +- ...-runs-only-what-no-cheaper-tier-reaches.md | 4 +- ...-small-interrupts-post-and-threads-wait.md | 8 +- ...oes-only-what-must-precede-the-handover.md | 2 +- ...-wall-clock-the-loader-already-hands-it.md | 30 -- kernel/pure/clock/mod.rs | 15 +- kernel/pure/clock/reference.rs | 205 +------------ kernel/src/arch/aarch64/boot.rs | 5 +- kernel/src/arch/aarch64/mod.rs | 10 - kernel/src/arch/aarch64/rtc.rs | 33 --- kernel/src/arch/x86_64/boot.rs | 21 +- kernel/src/arch/x86_64/clocksource.rs | 131 +++------ kernel/src/arch/x86_64/idt/mod.rs | 5 - kernel/src/arch/x86_64/idt/rtc.rs | 12 - kernel/src/arch/x86_64/mod.rs | 15 +- kernel/src/arch/x86_64/pio.rs | 7 +- kernel/src/arch/x86_64/rtc.rs | 273 ------------------ kernel/src/clock.rs | 38 +-- kernel/src/drivers/acpi.rs | 26 +- kernel/src/irq_census.rs | 75 +++-- kernel/src/main.rs | 3 +- kernel/src/syscall/dispatch.rs | 3 +- kernel/src/time.rs | 38 +-- lib/acpi/src/fadt.rs | 34 +-- lib/acpi/src/lib.rs | 5 +- lib/acpi/tests/corpus.rs | 44 +-- lib/acpi/tests/fixtures.rs | 14 +- system/logkeeper/src/main.rs | 2 +- tests/checks.rs | 21 +- tests/checks/metal.rs | 6 +- tests/common/faults.rs | 34 ++- tests/common/irqcensus.rs | 2 +- tests/common/metal.rs | 13 + tests/metal/src/lib.rs | 30 +- tests/toyos.rs | 85 +++++- 37 files changed, 310 insertions(+), 964 deletions(-) delete mode 100644 issues/x86-64-reads-the-cmos-for-a-wall-clock-the-loader-already-hands-it.md delete mode 100644 kernel/src/arch/aarch64/rtc.rs delete mode 100644 kernel/src/arch/x86_64/idt/rtc.rs delete mode 100644 kernel/src/arch/x86_64/rtc.rs diff --git a/docs/src/architecture/boot.md b/docs/src/architecture/boot.md index 6c0682df78e..6d42a32a9b0 100644 --- a/docs/src/architecture/boot.md +++ b/docs/src/architecture/boot.md @@ -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 diff --git a/issues/a-safe-port-read-reaches-any-declared-port.md b/issues/a-safe-port-read-reaches-any-declared-port.md index 974d8a84ccf..8254638441f 100644 --- a/issues/a-safe-port-read-reaches-any-declared-port.md +++ b/issues/a-safe-port-read-reaches-any-declared-port.md @@ -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. diff --git a/issues/the-amd-benchs-tsc-steps-1-7-ms-forward-once-a-boot.md b/issues/the-amd-benchs-tsc-steps-1-7-ms-forward-once-a-boot.md index 6c297e7722e..b9a5b62b0ef 100644 --- a/issues/the-amd-benchs-tsc-steps-1-7-ms-forward-once-a-boot.md +++ b/issues/the-amd-benchs-tsc-steps-1-7-ms-forward-once-a-boot.md @@ -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. @@ -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* diff --git a/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md b/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md index 91e730f895c..a5c7e291670 100644 --- a/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md +++ b/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md @@ -95,11 +95,9 @@ 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 `wall_clock_utc`: the wall clock reads firmware's time 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. diff --git a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md index 012de414ccd..e5593f8bbc7 100644 --- a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md +++ b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md @@ -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 | @@ -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: @@ -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 diff --git a/issues/the-loader-does-only-what-must-precede-the-handover.md b/issues/the-loader-does-only-what-must-precede-the-handover.md index daa9b711299..35aacb3fce3 100644 --- a/issues/the-loader-does-only-what-must-precede-the-handover.md +++ b/issues/the-loader-does-only-what-must-precede-the-handover.md @@ -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 diff --git a/issues/x86-64-reads-the-cmos-for-a-wall-clock-the-loader-already-hands-it.md b/issues/x86-64-reads-the-cmos-for-a-wall-clock-the-loader-already-hands-it.md deleted file mode 100644 index 1f4cc83e719..00000000000 --- a/issues/x86-64-reads-the-cmos-for-a-wall-clock-the-loader-already-hands-it.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-10 ---- - -# x86-64 reads the CMOS for a wall clock the loader already hands it - -The loader asks firmware's `GetTime` before `ExitBootServices` on every -architecture and hands the instant, with the counter it was true at, in -`KernelArgs::wall_clock_*` (`loader/src/wallclock.rs`). AArch64 anchors -its wall clock on that (`kernel/src/arch/aarch64/rtc.rs`); x86-64 ignores it -and reads the CMOS itself (`kernel/src/arch/x86_64/rtc.rs`), with the FADT's -century register (`kernel/src/drivers/acpi.rs`), so the two architectures keep -two readers of one clock, and on x86-64 the field is written and never read. -Whether the T14's firmware answers `GetTime` from the same CMOS is not -measured. - -The wall clock is not the CMOS driver's only reader: x86-64's clock takes -the RTC's update-ended edges, on IRQ8, as the third reference that tells a -TSC step from a stall of the chipset's counters -(`kernel/src/arch/x86_64/clocksource.rs`, `rtc::last_edge`). So the exit -below deletes the wall-clock read and the century register, and -`arch/x86_64/rtc.rs` whole only if that reference goes or moves with it. - -Exit condition: x86-64 anchors on the loader's reading, and -`arch/x86_64/rtc.rs`, `acpi::rtc_century_register` and their callers are -deleted with it, against a metal row reading the T14's wall clock; or a -measurement on the T14 that its firmware's `GetTime` answers wrongly where the -CMOS answers right, recorded at `arch/x86_64/rtc.rs`, and this file deleted. diff --git a/kernel/pure/clock/mod.rs b/kernel/pure/clock/mod.rs index 2dac5aaf41e..6125b5bfb5c 100644 --- a/kernel/pure/clock/mod.rs +++ b/kernel/pure/clock/mod.rs @@ -13,16 +13,15 @@ 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 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 "; +/// 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 ns ...`), +/// written as the sampler finds it. +pub const TSC_STEP: &str = "clock: TSC STEP: "; /// 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. +/// `at`, which is before boot where the loader took it; 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) diff --git a/kernel/pure/clock/reference.rs b/kernel/pure/clock/reference.rs index 30dadc2e6d3..43e6ac80a2a 100644 --- a/kernel/pure/clock/reference.rs +++ b/kernel/pure/clock/reference.rs @@ -7,9 +7,7 @@ //! the counter readings either side of a reference reading bracket the moment //! it latched, and the reference's tick bounds where in that tick it was, so a //! bound that fails is a counter, a reference or a read that did not do what -//! it says. The one estimate is a third reference read by its interrupts -//! ([`Edges`]): their least latency, and its crystal's drift carried across a -//! span, which only choose between a step and none ([`third_says`]). +//! it says. use toyos_abi::clock::MULT_SHIFT; @@ -423,114 +421,6 @@ pub const fn arbitrate(first: Agreement, second: Agreement) -> Arbitration { } } -/// The RTC's edges the sampler has read, the latest [`EDGES_KEPT`]: each -/// edge's count and the counter as its interrupt was taken, which is the edge -/// plus the interrupt's latency, never earlier. -#[derive(Clone, Copy, Debug)] -pub struct Edges { - kept: [(u64, u64); EDGES_KEPT], - len: usize, -} - -pub const EDGES_KEPT: usize = 16; -/// The edges either side of a span its least-latency edge is taken from. -pub const EDGES_EACH_SIDE: usize = 3; - -impl Edges { - pub const fn new() -> Self { - Self { kept: [(0, 0); EDGES_KEPT], len: 0 } - } - - /// Keep the edge `seen` as the counter read `at`, where it is later than - /// the latest kept. - pub fn push(&mut self, seen: u64, at: u64) { - if self.len > 0 && self.kept[self.len - 1].0 >= seen { - return; - } - if self.len == EDGES_KEPT { - self.kept.copy_within(1.., 0); - self.len -= 1; - } - self.kept[self.len] = (seen, at); - self.len += 1; - } - - /// Drop the edges taken before the counter read `at`, where the counter - /// stepped: an edge from before a step and one from after it measure the - /// step, not the RTC crystal's drift, so neither a drift base nor a side - /// of a later span may reach across it. - pub fn forget_before(&mut self, at: u64) { - let kept = self.kept[..self.len].iter().position(|&(_, taken)| taken >= at).unwrap_or(self.len); - self.kept.copy_within(kept..self.len, 0); - self.len -= kept; - } - - /// Whether [`EDGES_EACH_SIDE`] edges have come after the counter read `to`. - pub fn complete_after(&self, to: u64) -> bool { - self.kept[..self.len].iter().filter(|&&(_, at)| at >= to).count() >= EDGES_EACH_SIDE - } - - /// The counter's span, at `mult`, less the RTC's seconds, from before - /// `from` to after `to`: from the least-latency of the [`EDGES_EACH_SIDE`] - /// edges before `from` to that of those after `to`. An edge reads late by - /// its interrupt's latency and never early, so the least on each side is - /// the nearest its edge. Less the RTC crystal's own drift from the - /// counter, where edges at least [`DRIFT_BASE_NS`] before give it: the - /// least-latency of the oldest kept to that before `from`, carried on - /// across the span. `None` without an edge either side. - pub fn apart_across(&self, from: u64, to: u64, mult: u64) -> Option { - let kept = &self.kept[..self.len]; - let &(seen_ref, at_ref) = kept.iter().rev().find(|&&(_, at)| at <= from)?; - let offset = |&(seen, at): &(u64, u64)| { - let ns = ((at as i128 - at_ref as i128) * mult as i128) >> MULT_SHIFT; - (ns - (seen as i128 - seen_ref as i128) * 1_000_000_000, at) - }; - let least = |edges: &mut dyn Iterator| edges.map(offset).min_by_key(|&(ns, _)| ns); - let before = least(&mut kept.iter().rev().filter(|&&(_, at)| at <= from).take(EDGES_EACH_SIDE))?; - let after = least(&mut kept.iter().filter(|&&(_, at)| at >= to).take(EDGES_EACH_SIDE))?; - let base_ticks = ((DRIFT_BASE_NS as u128) << MULT_SHIFT) / mult as u128; - let older = least(&mut kept.iter().filter(|&&(_, at)| (at as u128) + base_ticks <= before.1 as u128).take(EDGES_EACH_SIDE)); - let drift = older.map_or(0, |older| (before.0 - older.0) * (after.1 as i128 - before.1 as i128) / (before.1 as i128 - older.1 as i128)); - Some((after.0 - before.0 - drift) as i64) - } -} - -/// How far back edges must reach for [`Edges::apart_across`] to measure the -/// RTC crystal's drift from the counter: over this, a few microseconds of -/// latency are a part per million. -pub const DRIFT_BASE_NS: u64 = 4_000_000_000; - -impl Default for Edges { - fn default() -> Self { - Self::new() - } -} - -/// Which of the counter's step and no step at all a third reference, on -/// another oscillator, reads nearest: within a quarter of the step of -/// either, or of neither. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Third { - /// It reads the step: the counter moved. - Counter, - /// It reads no step: the two references moved together. - References, - /// It reads neither, or has no edge either side of the span yet. - Unsure, -} - -pub fn third_says(step_ns: i64, apart_ns: Option) -> Third { - let Some(apart) = apart_ns else { return Third::Unsure }; - let zone = step_ns.unsigned_abs() / 4; - if apart.abs_diff(step_ns) <= zone { - Third::Counter - } else if apart.unsigned_abs() <= zone { - Third::References - } else { - Third::Unsure - } -} - /// What the clock does about a step of its counter: the boot's first, forward, /// is slewed away; a second, or one back, leaves the counter for the /// reference. @@ -874,49 +764,6 @@ mod tests { assert_eq!(extend(0x0400_0010, 0x00ff_fff0, 24), 0x03ff_fff0); } - /// A 1 GHz counter, so ticks are nanoseconds. - const NS: u64 = 1 << MULT_SHIFT; - - /// Edges a second apart on the RTC, each read as late as its latency; - /// the counter steps by `step` at `step_at`. - fn edges(latency_ns: &[u64], step_at: u64, step: u64) -> Edges { - let mut edges = Edges::new(); - for (n, latency) in latency_ns.iter().enumerate() { - let edge = 10_000_000_000 + n as u64 * 1_000_000_000; - edges.push(n as u64 + 1, edge + latency + if edge >= step_at { step } else { 0 }); - } - edges - } - - /// The least-latency edge either side reads the step, however late the - /// edge nearest it was taken; and reads none where there was none. - #[test] - fn the_rtc_reads_a_step_through_its_interrupts_latency() { - let latency = [40_000, 700_000, 3_000, 650_000, 520_000, 9_000, 700_000, 2_000]; - let step = 1_700_000; - let stepped = edges(&latency, 14_500_000_000, step); - let (from, to) = (13_900_000_000, 14_900_000_000); - assert!(stepped.complete_after(to + step)); - let apart = stepped.apart_across(from, to + step, NS).expect("edges either side"); - assert!(apart.abs_diff(step as i64) <= 10_000, "read {apart} ns for a {step} ns step"); - assert_eq!(third_says(step as i64, Some(apart)), Third::Counter); - - let still = edges(&latency, u64::MAX, 0); - let apart = still.apart_across(from, to, NS).expect("edges either side"); - assert!(apart.unsigned_abs() <= 10_000, "read {apart} ns where nothing stepped"); - assert_eq!(third_says(step as i64, Some(apart)), Third::References); - } - - #[test] - fn the_rtc_is_unsure_without_an_edge_either_side_or_between_the_two() { - let few = edges(&[1_000, 1_000], u64::MAX, 0); - assert_eq!(few.apart_across(9_000_000_000, 10_500_000_000, NS), None); - assert!(!few.complete_after(10_500_000_000)); - assert_eq!(third_says(1_700_000, None), Third::Unsure); - assert_eq!(third_says(1_700_000, Some(850_000)), Third::Unsure); - assert_eq!(third_says(-1_700_000, Some(-1_600_000)), Third::Counter); - } - #[test] fn only_a_first_forward_step_is_slewed() { assert_eq!(respond(1_700_000, 0), Response::Slew); @@ -965,54 +812,4 @@ mod tests { assert!(later.debt_ns() < 0); assert!(later.mult(kept, period) > kept, "an overpaid slew runs the clock faster"); } - - /// An RTC crystal 60 ppm off the counter drifts 420 µs over the edges - /// either side of a span, about a quarter of a 1.7 ms step: corrected by - /// its own earlier edges, it reads the step and reads a stall as none. - #[test] - fn the_rtcs_crystal_drift_is_taken_out() { - let drifting = |step: u64| { - let mut edges = Edges::new(); - for n in 0..EDGES_KEPT as u64 { - let edge = 10_000_000_000 + n * 1_000_060_000; - let latency = [3_000, 650_000, 20_000][n as usize % 3]; - edges.push(n + 1, edge + latency + if n >= 11 { step } else { 0 }); - } - edges - }; - let (from, to) = (20_900_000_000, 21_900_000_000); - let apart = drifting(1_700_000).apart_across(from, to + 1_700_000, NS).expect("edges either side"); - assert_eq!(third_says(1_700_000, Some(apart)), Third::Counter, "read {apart} ns"); - let apart = drifting(0).apart_across(from, to, NS).expect("edges either side"); - assert!(apart.unsigned_abs() <= 30_000, "a stall read {apart} ns through the drift"); - assert_eq!(third_says(1_700_000, Some(apart)), Third::References); - } - - /// A 1.7 ms step after a 4 ms one reads as itself once the edges from - /// before the first are forgotten; with them, the drift base reaches - /// across the first step and takes a quarter of it for the crystal's drift. - #[test] - fn a_drift_base_never_reaches_across_a_step() { - let (first, second) = (4_000_000u64, 1_700_000u64); - let stepped = || { - let mut edges = Edges::new(); - for n in 0..EDGES_KEPT as u64 { - let edge = 10_000_000_000 + n * 1_000_000_000; - let latency = [3_000, 650_000, 20_000][n as usize % 3]; - let steps = if n >= 4 { first } else { 0 } + if n >= 13 { second } else { 0 }; - edges.push(n + 1, edge + latency + steps); - } - edges - }; - let first_taken_at = 14_000_000_000 + first; - let (from, to) = (22_500_000_000 + first, 22_900_000_000 + first + second); - - let mut forgotten = stepped(); - forgotten.forget_before(first_taken_at); - let apart = forgotten.apart_across(from, to, NS).expect("edges either side"); - assert_eq!(third_says(second as i64, Some(apart)), Third::Counter, "read {apart} ns"); - - let apart = stepped().apart_across(from, to, NS).expect("edges either side"); - assert_ne!(third_says(second as i64, Some(apart)), Third::Counter, "read {apart} ns across the first step"); - } } diff --git a/kernel/src/arch/aarch64/boot.rs b/kernel/src/arch/aarch64/boot.rs index 8b7d087ce53..114e7b71d7a 100644 --- a/kernel/src/arch/aarch64/boot.rs +++ b/kernel/src/arch/aarch64/boot.rs @@ -351,12 +351,11 @@ pub fn interrupts(rsdp_addr: u64) -> Platform { /// The clock: the generic timer's count, at the rate firmware states in /// `CNTFRQ_EL0`, which the Arm ARM makes firmware's to program and which is -/// what the counter counts at; and the wall clock the loader handed over. -pub fn clock(args: &KernelArgs) { +/// what the counter counts at. +pub fn clock(_rsdp_addr: u64) { let hz = super::cpu::stated_counter_hz().expect("clock: CNTFRQ_EL0 states no rate for the generic timer"); crate::clock::set_counter(super::cpu::counter(), toyos_abi::clock::Line::mult_of_hz(hz)); log!("clock: the generic timer counts at {hz} Hz"); - crate::clock::init_wall(super::rtc::read(args)); } /// Nothing: where the generic timer counts from is firmware's, and no diff --git a/kernel/src/arch/aarch64/mod.rs b/kernel/src/arch/aarch64/mod.rs index 9b46c11bbb0..c157df39df2 100644 --- a/kernel/src/arch/aarch64/mod.rs +++ b/kernel/src/arch/aarch64/mod.rs @@ -11,15 +11,6 @@ macro_rules! owed { }; } -/// The interrupt sources only this architecture counts: none past the -/// census's own (`irq_census`). -macro_rules! census_sources { - ($list:ident) => { - $list! {} - }; -} -pub(crate) use census_sources; - pub mod acpi_mode; pub mod barrier; pub mod boot; @@ -42,7 +33,6 @@ pub mod pio; pub mod pmu; pub mod power; pub mod psci; -pub mod rtc; pub mod smmu; pub mod smp; pub mod switch; diff --git a/kernel/src/arch/aarch64/rtc.rs b/kernel/src/arch/aarch64/rtc.rs deleted file mode 100644 index edfd5ddc2b3..00000000000 --- a/kernel/src/arch/aarch64/rtc.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! The wall clock at boot: firmware's `GetTime`, which the loader asked before -//! `ExitBootServices` and hands over in [`KernelArgs`], since the kernel calls -//! no UEFI service; on `virt` firmware reads it from the PL031, which only the -//! DSDT names. - -use core::fmt; - -use toyos_abi::boot::KernelArgs; -use toyos_wallclock::Civil; - -/// Why this machine did not say what time it is. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RtcFault { - /// The loader handed no reading; its own line says why. - NotHanded, -} - -impl fmt::Display for RtcFault { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NotHanded => write!(f, "the loader handed no reading of firmware's GetTime"), - } - } -} - -/// The instant firmware named, and the counter when it was true. -pub fn read(args: &KernelArgs) -> Result<(Civil, u64), RtcFault> { - match args.wall_clock_known { - 0 => Err(RtcFault::NotHanded), - 1 => Ok((Civil::from_unix_secs(args.wall_clock_secs), args.wall_clock_counter)), - other => panic!("the loader wrote {other} as wall_clock_known, which is 0 or 1"), - } -} diff --git a/kernel/src/arch/x86_64/boot.rs b/kernel/src/arch/x86_64/boot.rs index 62c853c6a5b..e6b24883f28 100644 --- a/kernel/src/arch/x86_64/boot.rs +++ b/kernel/src/arch/x86_64/boot.rs @@ -38,7 +38,7 @@ pub unsafe extern "C" fn _start(_kernel_args: &KernelArgs) -> ! { /// logs nothing: the read-back is `pat::check`, in [`after_console`], where a /// refusal has a channel to reach. /// The ACPI tables this architecture decodes: the MADT for its CPUs and I/O -/// APICs, the FADT for reset, the PM1a control block and the century register, the HPET for +/// APICs, the FADT for reset and the PM1a control block, the HPET for /// the clock, the MCFG for ECAM and the DMAR or IVRS for the IOMMU. pub const ACPI_TABLES: &[&[u8; 4]] = &[b"APIC", b"FACP", b"HPET", b"MCFG", b"DMAR", b"IVRS"]; @@ -114,18 +114,10 @@ pub fn interrupts(rsdp_addr: u64) -> Platform { Platform { madt, facs } } -/// The clock: the TSC, held to the HPET, and the CMOS wall clock. -pub fn clock(args: &KernelArgs) { - let hpet_base = acpi::find_hpet_base(args.rsdp_addr).expect("ACPI: HPET not found"); - super::clocksource::start(hpet_base, acpi::pm_timer(args.rsdp_addr)); - let century_reg = match acpi::rtc_century_register(args.rsdp_addr) { - Ok(reg) => reg, - Err(e) => { - log!("ACPI: the FADT is unreadable ({e:?}), so where the RTC keeps its century is unknown too"); - None - } - }; - crate::clock::init_wall(super::rtc::read(century_reg).map(|civil| (civil, super::cpu::counter()))); +/// The clock: the TSC, held to the HPET. +pub fn clock(rsdp_addr: u64) { + let hpet_base = acpi::find_hpet_base(rsdp_addr).expect("ACPI: HPET not found"); + super::clocksource::start(hpet_base, acpi::pm_timer(rsdp_addr)); } /// Where the TSC counts from: `IA32_TSC_ADJUST`, where CPUID says the CPU has @@ -149,9 +141,6 @@ pub fn platform_devices(platform: &Platform, rsdp_addr: u64) { super::pio::fill_i8042_row(); super::pio::fill_uart_row(); super::acpi_mode::init(rsdp_addr, platform.facs); - if let Err(why) = super::rtc::arm_update_edges() { - log!("clock: no RTC edges time the TSC: {why}"); - } } /// Every other CPU, running. diff --git a/kernel/src/arch/x86_64/clocksource.rs b/kernel/src/arch/x86_64/clocksource.rs index 382302e464c..814bce15aa0 100644 --- a/kernel/src/arch/x86_64/clocksource.rs +++ b/kernel/src/arch/x86_64/clocksource.rs @@ -5,16 +5,17 @@ //! The clock counts the TSC only where every CPU states it invariant, or under //! a hypervisor, whose TSC is its own to synthesise and whose CPUID need not //! say so. Every AP's TSC is bounded against the BSP's as it starts -//! ([`measure_offset`]). While it runs, one CPU a second samples the HPET, the -//! ACPI PM timer and the RTC's latest edge, and every CPU a second checks its -//! `IA32_TSC_ADJUST` and that its TSC is not behind another's ([`check`]). +//! ([`measure_offset`]). While it runs, one CPU a second samples the HPET and +//! the ACPI PM timer, and every CPU a second checks its `IA32_TSC_ADJUST` and +//! that its TSC is not behind another's ([`check`]). //! -//! **Where the TSC and the HPET disagree, the other two say which moved.** The -//! PM timer agreeing with the TSC is the HPET moving, and the span is left out. -//! The PM timer agreeing with the HPET leaves the chipset's 14.318 MHz clock -//! or the TSC; the RTC's edges, off its own 32.768 kHz crystal, choose: with -//! the TSC, the span is left out; with the chipset's counters, or saying -//! neither, the TSC stepped. The boot's first forward step is slewed away; +//! **Where the TSC and the HPET disagree, the PM timer says which moved.** It +//! agreeing with the TSC is the HPET moving, and the span is left out. It +//! agreeing with the HPET, or a machine with none, is taken as the TSC's +//! step. The chipset's two counters stopping together would read the same +//! and the clock would follow them: nothing here tells that from a step, and +//! neither counter may stop in S0 (ACPI 6.5 §4.8.3.3; IA-PC HPET +//! Specification 1.0a, §2.3.3). The boot's first forward step is slewed away; //! a second, one back, a check that finds a CPU's TSC written or behind, or //! an AP's start that proves its offset demotes the TSC ([`demote`]), and //! every process's reading of now asks the kernel, which counts by the HPET. @@ -22,7 +23,7 @@ use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use kernel::clock::reference::{self, Agreement, Arbitration, Calibration, Edges, Failure, Kept, Offset, Reference, Response, Sample, Slew, Third, Watch}; +use kernel::clock::reference::{self, Agreement, Arbitration, Calibration, Failure, Kept, Offset, Reference, Response, Sample, Slew, Watch}; use toyos_abi::clock::Line; use super::cpu; @@ -126,8 +127,8 @@ impl Hpet { /// The ACPI PM timer, the second reference: which of the TSC and the HPET /// moved, where the two disagree. **It and the HPET are both the chipset's -/// (the FCH's on AMD, the PCH's on Intel)**, and may share its clock, so the -/// RTC's edges judge a stall common to both. +/// (the FCH's on AMD, the PCH's on Intel)**, and may share its clock: a stall +/// common to both reads as the TSC's step. struct PmTimer { port: Declared, bits: u32, @@ -184,28 +185,13 @@ struct Held { hpet: Hpet, watch: Watch, pm: Option, - /// The RTC's edges, the third reference, on its own crystal. - edges: Edges, - /// A step the HPET and the PM timer read the TSC take, waiting for the - /// RTC's edges after it. - pending: Option, /// The steps this boot slewed away, and the slew under way. slewed: u32, slew: Option, - /// Spans left out: one reference moved, or the RTC said both did. + /// Spans left out: a reference moved, or the TSC stepped. left_out: u64, } -/// A step the chipset's counters agree the TSC took over `from..to`. -#[derive(Clone, Copy)] -struct Pending { - step_ns: i64, - from: Sample, - to: Sample, - /// When the sampler gives up waiting for the RTC's edges after it. - until_ns: u64, -} - /// A slew, and the line it runs on since the counter read `since`: at `ran` /// where the reference's time runs at `kept`. #[derive(Clone, Copy)] @@ -216,11 +202,6 @@ struct Slewing { kept: u64, } -/// How long a step waits for the RTC's edges after it before it is taken -/// unconfirmed: [`reference::EDGES_EACH_SIDE`] edges a second apart, and two -/// more for their interrupts. -const RTC_WAIT_NS: u64 = (reference::EDGES_EACH_SIDE as u64 + 2) * 1_000_000_000; - static HELD: Lock> = Lock::new(None); /// The last sample every check held, and the clock then: where a demoted @@ -321,7 +302,7 @@ pub fn start(hpet_base: u64, pm_timer: Result log!("clock: the ACPI PM timer at {:#x}, {} bits, tells the TSC from the HPET where they disagree", pm.port.port(0).number(), pm.bits), - Err(why) => log!("clock: no ACPI PM timer to tell the TSC from the HPET ({why}); the RTC alone tells them apart"), + Err(why) => log!("clock: no ACPI PM timer to tell the TSC from the HPET ({why}); where they disagree the TSC is taken to have stepped"), } LAST_HELD.publish([anchor.before, anchor.reference, crate::clock::stamp_at(anchor.before)]); *HPET_COPY.lock() = Some(hpet); @@ -330,8 +311,6 @@ pub fn start(hpet_base: u64, pm_timer: Result u64 { answered } -/// One sample of the HPET, of the PM timer beside it and of the RTC's -/// latest edge: each span the TSC and the HPET agree over is held; where -/// they do not, the PM timer and then the RTC say which moved. -fn sample(held: &mut Held, now_ns: u64) { +/// One sample of the HPET and of the PM timer beside it: each span the TSC +/// and the HPET agree over is held; where they do not, the PM timer says +/// which moved. +fn sample(held: &mut Held) { let kept = held.watch.kept(); let last = held.watch.last(); let hpet = held.hpet.sample(held.hpet.near(last, kept.mult), 1); let pm = held.pm.as_ref().map(|pm| (pm.last, pm.next(kept.mult))); - let edge = super::rtc::last_edge(); #[cfg(feature = "boot-actuators")] - let (hpet, pm, edge) = staged(hpet, pm, edge); - if let Some((seen, at)) = edge { - held.edges.push(seen, at); - } + let (hpet, pm) = staged(hpet, pm); let pm_agreement = held.pm.as_ref().zip(pm).map(|(timer, (from, to))| { let span = reference::Span::between(timer.reference, from, to); let tolerance = kept.bound_ppb + timer.reference.tolerance_over(timer.reference.span_ns(from, to)); @@ -721,18 +696,7 @@ fn sample(held: &mut Held, now_ns: u64) { Some(Arbitration::AllDisagree) => { log!("clock: the TSC, the HPET and the ACPI PM timer all disagree, so this span is left out: {spans}") } - Some(Arbitration::CounterMoved) | None => { - if let Some(earlier) = held.pending.take() { - settle(held, earlier); - } - log!( - "{}{} ns from the chipset's counters over the last span; the RTC's edges after it will \ - say which moved: {spans}", - kernel::clock::TSC_STEP_READ, - agreement.apart_ns, - ); - held.pending = Some(Pending { step_ns: agreement.apart_ns, from, to, until_ns: now_ns + RTC_WAIT_NS }); - } + Some(Arbitration::CounterMoved) | None => stepped(held, agreement.apart_ns, &spans), } } Err(Failure::Drift { since, to, agreement }) => panic!( @@ -744,53 +708,32 @@ fn sample(held: &mut Held, now_ns: u64) { agreement.allowed_ns, ), } - if let Some(pending) = held.pending.filter(|p| held.edges.complete_after(p.to.after) || now_ns >= p.until_ns) { - held.pending = None; - settle(held, pending); - } KEPT_MULT.store(held.watch.kept().mult, Ordering::Relaxed); slew(held); } -/// What the RTC says of a pending step, and what the clock does about it. -fn settle(held: &mut Held, pending: Pending) { +/// What the clock does about a step of `step` ns the TSC took against the +/// chipset's counters, whose readings of it are `spans`. +fn stepped(held: &mut Held, step: i64, spans: &str) { let kept = held.watch.kept(); - let apart = held.edges.apart_across(pending.from.before, pending.to.after, kept.mult); - let rtc = match apart { - Some(apart) => alloc::format!("the RTC's edges read {apart} ns"), - None => alloc::string::String::from("no RTC edge either side"), - }; - let step = pending.step_ns; - let confirmed = match reference::third_says(step, apart) { - Third::References => { - return log!( - "clock: the chipset's counters moved {step} ns from the TSC and {rtc}, with the TSC, so the span \ - was left out" - ); - } - Third::Counter => "confirmed by the RTC", - Third::Unsure => "unconfirmed by the RTC", - }; - held.edges.forget_before(pending.to.after); + let against = if held.pm.is_some() { "the HPET and the PM timer" } else { "the HPET" }; if IS_DEMOTED.load(Ordering::Acquire) { - return log!("clock: TSC STEP: the demoted TSC stepped {step} ns against the HPET and the PM timer, {confirmed} ({rtc}); not slewed"); + return log!("{}the demoted TSC stepped {step} ns against {against}; not slewed: {spans}", kernel::clock::TSC_STEP); } match reference::respond(step, held.slewed) { Response::Slew => { held.slewed += 1; held.slew = Some(Slewing { slew: Slew::new(step), since: cpu::counter(), ran: crate::clock::mult(), kept: kept.mult }); log!( - "clock: TSC STEP: the TSC stepped {step} ns forward against the HPET and the PM timer, {confirmed} \ - ({rtc}); the clock slews it away at most {} ppm, most of it in about {} ms", + "{}the TSC stepped {step} ns forward against {against}; the clock slews it away at most {} ppm, most \ + of it in about {} ms: {spans}", + kernel::clock::TSC_STEP, reference::SLEW_MOST_PPB / 1_000, u128::from(step.unsigned_abs()) * u128::from(reference::PPB) / reference::SLEW_MOST_PPB as u128 / 1_000_000, ); } Response::Demote => { - demote(format_args!( - "the TSC stepped {step} ns against the HPET and the PM timer, {confirmed} ({rtc}), after {} step(s) slewed", - held.slewed, - )); + demote(format_args!("the TSC stepped {step} ns against {against}, after {} step(s) slewed: {spans}", held.slewed)); } } } @@ -874,15 +817,13 @@ fn spans( ) } -/// The PM timer's span, and the RTC's latest edge, as one sample reads them. +/// The PM timer's span, as one sample reads it. #[cfg(feature = "boot-actuators")] type PmSpan = Option<(Sample, Sample)>; -#[cfg(feature = "boot-actuators")] -type Edge = Option<(u64, u64)>; /// `tsc-step` and `hpet-stall`: from the first report on, every reading reads -/// either the TSC 10 ms ahead of where it is, the HPET's, the PM timer's and -/// the RTC's edges alike, or the HPET 10 ms behind, as a TSC that stepped and +/// either the TSC 10 ms ahead of where it is, the HPET's and the PM timer's +/// alike, or the HPET 10 ms behind, as a TSC that stepped and /// an HPET that stood still; and the TSC 10 ms further once its first step is /// slewed away. A hypervisor's TSC may ignore a write (QEMU's TCG does), so /// each is staged in the readings the sampler takes, from the counter it @@ -904,12 +845,12 @@ mod stage { } #[cfg(feature = "boot-actuators")] -fn staged(hpet: Sample, pm: PmSpan, edge: Edge) -> (Sample, PmSpan, Edge) { +fn staged(hpet: Sample, pm: PmSpan) -> (Sample, PmSpan) { use stage::*; let forward = |s: Sample| Sample { before: ahead(s.before), after: ahead(s.after), ..s }; let stalled = if hpet.before >= STALLED_AT.load(Ordering::Relaxed) { STALL_TICKS.load(Ordering::Relaxed) } else { 0 }; let hpet = Sample { reference: hpet.reference - stalled, ..forward(hpet) }; - (hpet, pm.map(|(from, to)| (from, forward(to))), edge.map(|(seen, at)| (seen, ahead(at)))) + (hpet, pm.map(|(from, to)| (from, forward(to)))) } /// `tsc-step`'s and `hpet-stall`'s first, at the clock's first report. diff --git a/kernel/src/arch/x86_64/idt/mod.rs b/kernel/src/arch/x86_64/idt/mod.rs index cf6fdfa523a..99bb8b154f9 100644 --- a/kernel/src/arch/x86_64/idt/mod.rs +++ b/kernel/src/arch/x86_64/idt/mod.rs @@ -1,7 +1,6 @@ pub(crate) mod exceptions; mod device_irq; mod dma_fault; -mod rtc; mod isa; mod kick; mod nmi; @@ -37,9 +36,6 @@ pub const ISA_VECTORS: [u8; crate::isa::MAX_ROWS] = [Vector::Isa0 as u8, Vector: /// The vector an IOMMU writes into its own `FEDATA`. pub const DMA_FAULT_VECTOR: u8 = Vector::DmaFault as u8; -/// The vector the RTC's IRQ8 is routed to. -pub const RTC_VECTOR: u8 = Vector::Rtc as u8; - const PF_PRESENT: u64 = 1 << 0; const PF_WRITE: u64 = 1 << 1; const PF_INSTRUCTION_FETCH: u64 = 1 << 4; @@ -246,7 +242,6 @@ idt_vectors! { Nmi = 0x02, nmi::nmi_entry, ist 2; Timer = 0x20, timer::timer_entry; DmaFault = 0x25, dma_fault::dma_fault_entry; - Rtc = 0x26, rtc::rtc_entry; Isa0 = 0x2C, isa::isa0_entry; Isa1 = 0x2D, isa::isa1_entry; Isa2 = 0x2E, isa::isa2_entry; diff --git a/kernel/src/arch/x86_64/idt/rtc.rs b/kernel/src/arch/x86_64/idt/rtc.rs deleted file mode 100644 index a74244af2df..00000000000 --- a/kernel/src/arch/x86_64/idt/rtc.rs +++ /dev/null @@ -1,12 +0,0 @@ -use super::device_irq::device_irq_entry; - -extern "sysv64" fn rtc_handler() { - crate::arch::percpu::irq_took!(Rtc); - crate::arch::rtc::update_ended(); - crate::arch::apic::eoi(); -} - -device_irq_entry! { - /// The RTC's IRQ8 entry (see `device_irq_entry` for the asm contract). - pub(super) fn rtc_entry => rtc_handler -} diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs index a195f3bff0a..fda1932a0a7 100644 --- a/kernel/src/arch/x86_64/mod.rs +++ b/kernel/src/arch/x86_64/mod.rs @@ -5,22 +5,10 @@ //! [`percpu`] owns every `gs:` access; nothing outside this directory writes one. //! //! The PC platform's own devices live here too, because no other architecture -//! has them: the I/O APIC, the CMOS RTC, the chipset's watchdog, VT-d and +//! has them: the I/O APIC, the chipset's watchdog, VT-d and //! AMD-Vi. Generic code reaches each through the concept it serves //! (`watchdog`, `iommu_unit`, …), never by its name. -/// The interrupt sources only this architecture counts, handed to the -/// census's list (`irq_census`). -macro_rules! census_sources { - ($list:ident) => { - $list! { - /// Vector 0x26, the RTC's update-ended interrupt, once a second. - Rtc => "rtc", - } - }; -} -pub(crate) use census_sources; - pub mod acpi_mode; pub mod amdvi; pub mod apic; @@ -50,7 +38,6 @@ pub mod percpu; pub mod pio; pub mod pmu; pub mod power; -pub mod rtc; pub mod smi_cmd; pub mod smp; pub mod switch; diff --git a/kernel/src/arch/x86_64/pio.rs b/kernel/src/arch/x86_64/pio.rs index be5fc3065da..360c9d4d4f8 100644 --- a/kernel/src/arch/x86_64/pio.rs +++ b/kernel/src/arch/x86_64/pio.rs @@ -68,7 +68,6 @@ pub const PIC_PRIMARY: Declared = Declared::fixed(0x20, 2); pub const PIC_SECONDARY: Declared = Declared::fixed(0xA0, 2); /// POST codes, which nothing decodes: one bus cycle of delay. pub const POST: Declared = Declared::fixed(0x80, 1); -pub const CMOS: Declared = Declared::fixed(0x70, 2); /// The runs every machine has, and their holders. The PCI configuration /// mechanism is here though this kernel reaches configuration space through @@ -80,9 +79,9 @@ const FIXED: &[(&str, Declared, Mediated)] = &[ ("the 8259 pair", PIC_SECONDARY, Mediated::Kept), // A write nothing decodes costs the kernel nothing whoever makes it. ("the POST port", POST, Mediated::Open), - // Index and data: a holder's access between the kernel's two would move - // the index under it. - ("the CMOS RTC", CMOS, Mediated::Kept), + // Index and data, which nothing in ToyOS reads: a holder's write would + // set the clock firmware's `GetTime` answers the loader from. + ("the CMOS RTC", Declared::fixed(0x70, 2), Mediated::Kept), ("the PCI configuration mechanism", Declared::fixed(0xCF8, 1), Mediated::Kept), ("the PCI configuration mechanism", Declared::fixed(0xCFC, 4), Mediated::Kept), ]; diff --git a/kernel/src/arch/x86_64/rtc.rs b/kernel/src/arch/x86_64/rtc.rs deleted file mode 100644 index 5feb9256606..00000000000 --- a/kernel/src/arch/x86_64/rtc.rs +++ /dev/null @@ -1,273 +0,0 @@ -//! CMOS real-time clock: decoded once per boot into a [`Result`], never a -//! panic, because a read can be absent, wedged, or torn mid-update. -//! [`read`] takes the six-register set twice and accepts only two matching -//! reads, since a mid-update read is the only way two reads can disagree. - -use core::fmt; - -use crate::arch::cpu; -use crate::clock; -use crate::log; -use crate::time::{Bound, Duration}; -use toyos_wallclock::Civil; - -const CMOS_ADDR: super::pio::Port = super::pio::CMOS.port(0); -const CMOS_DATA: super::pio::Port = super::pio::CMOS.port(1); - -const SECONDS: u8 = 0x00; -const MINUTES: u8 = 0x02; -const HOURS: u8 = 0x04; -const DAY: u8 = 0x07; -const MONTH: u8 = 0x08; -const YEAR: u8 = 0x09; -const STATUS_A: u8 = 0x0A; -const STATUS_B: u8 = 0x0B; - -/// Register A bit 7: the clock registers are mid-update and must not be read. -const UPDATE_IN_PROGRESS: u8 = 1 << 7; -/// Register B bit 1: hours run 0..=23 rather than 1..=12 with a PM flag. -const HOUR_24: u8 = 1 << 1; -/// Register B bit 2: the registers hold binary rather than BCD. -const BINARY: u8 = 1 << 2; -/// Bit 7 of the hours register, in 12-hour mode only. -const PM: u8 = 1 << 7; - -/// Expiry is [`RtcFault::Updating`], not a retry or a panic. -/// 50x the hardware's own worst case: erring long only delays a boot whose -/// clock is already broken. -const MAX_UIP: Bound = Bound::from_spec( - Duration::from_millis(100), - "MC146818: the flag is raised 244us ahead of an update and cleared at most 1984us later", -); - -/// Four: three disagreements in a row means the registers never held one -/// instant. -const MAX_READ_ATTEMPTS: u32 = 4; - -/// Why this machine did not say what time it is. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RtcFault { - /// [`UPDATE_IN_PROGRESS`] never cleared inside [`MAX_UIP`]. - Updating, - /// No two of [`MAX_READ_ATTEMPTS`] reads agreed. - Unstable, - /// The registers agreed on a value that is not a valid date. - NotADate, -} - -impl fmt::Display for RtcFault { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Updating => write!( - f, - "its update flag never cleared in {} ms, which is what an absent one looks like", - MAX_UIP.duration().millis() - ), - Self::Unstable => write!( - f, - "no two of {MAX_READ_ATTEMPTS} reads agreed, so its registers never described one instant" - ), - Self::NotADate => write!(f, "its registers hold something that is not a date"), - } - } -} - -/// Current time; `century_reg` is `None` when firmware names no century -/// register. -pub fn read(century_reg: Option) -> Result { - // Requires the monotonic clock already running: this bound is a duration. - // An uncalibrated clock here is the kernel's own init order, not a - // hardware fault, so this fails fast rather than returning one. - assert!(clock::calibrated(), "rtc::read before the monotonic clock was calibrated"); - - let mut previous = read_registers(century_reg)?; - for _ in 1..MAX_READ_ATTEMPTS { - let current = read_registers(century_reg)?; - if current == previous { - return decode(current); - } - previous = current; - } - Err(RtcFault::Unstable) -} - -/// Equality between two reads is the evidence they describe one instant. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct Registers { - sec: u8, - min: u8, - hour: u8, - day: u8, - month: u8, - year: u8, - /// `None` means no century register, distinct from one holding zero. - century: Option, - status_b: u8, -} - -fn read_registers(century_reg: Option) -> Result { - wait_for_update()?; - Ok(Registers { - sec: cmos_read(SECONDS), - min: cmos_read(MINUTES), - hour: cmos_read(HOURS), - day: cmos_read(DAY), - month: cmos_read(MONTH), - year: cmos_read(YEAR), - century: century_reg.map(century_read), - status_b: cmos_read(STATUS_B), - }) -} - -/// Answers the *next* century, so a test can see the register's value reach -/// the target year. -fn century_read(reg: u8) -> u8 { - cmos_read(reg) -} - -fn wait_for_update() -> Result<(), RtcFault> { - let deadline = clock::nanos_since_boot() + MAX_UIP.nanos(); - while cmos_read(STATUS_A) & UPDATE_IN_PROGRESS != 0 { - if clock::nanos_since_boot() >= deadline { - return Err(RtcFault::Updating); - } - core::hint::spin_loop(); - } - Ok(()) -} - -/// Register B's format bits are read here; nothing past this point assumes -/// BCD or 12-hour. -fn decode(r: Registers) -> Result { - let binary = r.status_b & BINARY != 0; - let field = |raw: u8| { - if binary { Some(raw) } else { bcd_to_bin(raw) }.ok_or(RtcFault::NotADate) - }; - - let sec = field(r.sec)?; - let min = field(r.min)?; - let day = field(r.day)?; - let month = field(r.month)?; - let year_lo = field(r.year)?; - - let hour = if r.status_b & HOUR_24 != 0 { - field(r.hour)? - } else { - let afternoon = r.hour & PM != 0; - let twelve = field(r.hour & !PM)?; - if !(1..=12).contains(&twelve) { - return Err(RtcFault::NotADate); - } - twelve % 12 + if afternoon { 12 } else { 0 } - }; - - let year = match r.century { - Some(raw) => { - let century = field(raw)?; - // Below 1900 the register is unmaintained; refuse rather than - // fall back to the two-digit year. - if !(19..=99).contains(¢ury) { - return Err(RtcFault::NotADate); - } - century as u64 * 100 + year_lo as u64 - } - // 2000, not an older pivot: this kernel only boots UEFI machines. - None => 2000 + year_lo as u64, - }; - - let civil = Civil { - year, - month: month as u64, - day: day as u64, - hour: hour as u64, - min: min as u64, - sec: sec as u64, - }; - if !civil.is_valid() { - return Err(RtcFault::NotADate); - } - Ok(civil) -} - -/// `None` for a nibble above nine, which is not a digit and so not a time. -fn bcd_to_bin(bcd: u8) -> Option { - let (tens, ones) = (bcd >> 4, bcd & 0x0F); - (tens <= 9 && ones <= 9).then_some(tens * 10 + ones) -} - -fn port_read(reg: u8) -> u8 { - // SAFETY: `CMOS_ADDR`/`CMOS_DATA` are the fixed CMOS ports; `reg` is - // always this module's constants or the century index - // `acpi::rtc_century_register` already bounds below 0x80, so it never - // sets the index port's NMI-mask bit — no value makes the RTC do - // anything but answer a different byte. - unsafe { cpu::outb(CMOS_ADDR, reg) }; - cpu::inb(CMOS_DATA) -} - -/// The one substitution point for actuator-injected RTC faults; everything -/// downstream reads whatever this returns. -fn cmos_read(reg: u8) -> u8 { - port_read(reg) -} - -const STATUS_C: u8 = 0x0C; -/// Register B bit 4: raise IRQ8 as each update cycle ends. -const UPDATE_ENDED_INTERRUPT: u8 = 1 << 4; -/// Register C bit 4: an update cycle has ended since register C was last read. -const UPDATE_ENDED: u8 = 1 << 4; -/// ISA IRQ 8, which the RTC raises until register C is read (MC146818). -const RTC_IRQ: u8 = 8; - -/// The latest update-ended edge: how many the handler has seen, and the TSC -/// as it took the last. Published by IRQ8's handler on the one CPU it is routed to. -static EDGE: crate::seqlock::Published<2> = crate::seqlock::Published::new(); - -/// Raise IRQ8 at the end of each update cycle, once a second off the RTC's own -/// 32.768 kHz crystal, and take the TSC as each arrives: a reference outside -/// the chipset's 14.318 MHz domain, which the HPET and the ACPI PM timer share. -/// Called after the boot's [`read`], the only other CMOS access, so the index -/// port is the handler's alone from here. -pub fn arm_update_edges() -> Result<(), alloc::string::String> { - let line = super::ioapic::gsi_for_isa_irq(RTC_IRQ).ok_or("no I/O APIC carries IRQ8")?; - super::ioapic::route(line.gsi, super::idt::RTC_VECTOR, super::apic::MSG_DEST, line.trigger, line.polarity) - .map_err(|why| alloc::format!("{why:?}"))?; - let b = cmos_read(STATUS_B); - port_write(STATUS_B, b | UPDATE_ENDED_INTERRUPT); - let _ = cmos_read(STATUS_C); - super::ioapic::set_masked(line.gsi, false).map_err(|why| alloc::format!("{why:?}"))?; - // An update that ended while the pin was masked holds IRQ8 high with no - // edge the pin saw; reading C again lowers it, so the next update's edge - // arrives. Should the handler run first, it reads the same register. - let _ = cmos_read(STATUS_C); - log!( - "clock: the RTC's update-ended interrupt on gsi {} {} times the TSC against its 32.768 kHz crystal", - line.gsi.0, - super::ioapic::describe(line.trigger, line.polarity), - ); - Ok(()) -} - -/// IRQ8's handler: the TSC first, then register C, whose read lets the RTC -/// raise the next. -pub fn update_ended() { - let at = cpu::rdtsc_ordered(); - if cmos_read(STATUS_C) & UPDATE_ENDED != 0 { - let seen = EDGE.snapshot().map_or(0, |[seen, _]| seen); - EDGE.publish([seen + 1, at]); - } -} - -/// The edges seen and the TSC at the last, or `None` before the first. -pub fn last_edge() -> Option<(u64, u64)> { - EDGE.snapshot().filter(|&[seen, _]| seen > 0).map(|[seen, at]| (seen, at)) -} - -fn port_write(reg: u8, value: u8) { - // SAFETY: as `port_read`'s: `reg` is this module's constant, below 0x80, - // and the byte only enables the update-ended interrupt. - unsafe { - cpu::outb(CMOS_ADDR, reg); - cpu::outb(CMOS_DATA, value); - } -} diff --git a/kernel/src/clock.rs b/kernel/src/clock.rs index aa6903bfd49..b5890b601ac 100644 --- a/kernel/src/clock.rs +++ b/kernel/src/clock.rs @@ -1,10 +1,9 @@ //! The machine's clocks: monotonic since boot, read off the CPU's free-running //! counter along the line the architecture's boot starts in [`set_counter`] //! and may retune once it measures the counter's rate better ([`retune`]); -//! wall-clock, the architecture's one reading of the RTC — on x86-64 a CMOS -//! read, which can block for up to a second, and on AArch64 firmware's, which -//! the loader hands over — anchored in [`init_wall`] and answered after as -//! that reading plus [`nanos_since_boot`]; and the log's stamp ([`stamp`]), the +//! wall-clock, firmware's reading the loader hands over, anchored in +//! [`init_wall`] and answered after as that reading plus +//! [`nanos_since_boot`]; and the log's stamp ([`stamp`]), the //! same clock counted from the counter's zero, which is where every line of //! the log — the loader's, the kernel's and every program's — counts from. //! @@ -15,6 +14,7 @@ use core::sync::atomic::{AtomicBool, AtomicPtr, AtomicU64, Ordering::{Acquire, Relaxed, Release}}; use alloc::sync::Arc; +use toyos_abi::boot::KernelArgs; use toyos_abi::clock::{Clock, ClockPage, Line}; use crate::arch::cpu; @@ -240,35 +240,37 @@ static BOOT_SECS: AtomicU64 = AtomicU64::new(0); /// Whether the above means anything; zero is a valid instant, not a sentinel. static WALL_KNOWN: AtomicBool = AtomicBool::new(false); -/// Anchors the wall clock, once, after [`set_counter`], to the architecture's -/// reading of the RTC, which keeps UTC: the instant, and the counter when it -/// was true, which on a machine whose loader read it is before boot. -pub fn init_wall(reading: Result<(Civil, u64), impl core::fmt::Display>) { - let (civil, at) = match reading { - Ok(reading) => reading, - Err(fault) => { - log!("clock: this machine will not say what time it is — {fault}"); +/// Anchors the wall clock, once, after [`set_counter`], to firmware's +/// `GetTime`, which the loader asked before `ExitBootServices` since the +/// kernel calls no UEFI service: the instant, UTC, and the counter when it was +/// true, which is before boot. +pub fn init_wall(args: &KernelArgs) { + let (secs, at) = match args.wall_clock_known { + 0 => { + log!("clock: this machine will not say what time it is: the loader handed no reading of firmware's GetTime"); return; } + 1 => (args.wall_clock_secs, args.wall_clock_counter), + other => panic!("the loader wrote {other} as wall_clock_known, which is 0 or 1"), }; let clock = clock().expect("clock: the wall clock anchored before the counter started"); - BOOT_SECS.store(anchor(civil.to_unix_secs(), clock.stamp_of(at), clock.stamp_at_boot), Relaxed); + BOOT_SECS.store(anchor(secs, clock.stamp_of(at), clock.stamp_at_boot), Relaxed); WALL_KNOWN.store(true, Release); - log!("clock: the RTC reads {civil} UTC"); + log!("clock: firmware's clock read {} UTC", Civil::from_unix_secs(secs)); } /// Unix seconds, now — what `SYS_CLOCK_EPOCH` serves: the whole seconds of -/// [`utc_nanos`]. `None` if the RTC never answered. +/// [`utc_nanos`]. `None` if firmware never answered. pub fn utc_secs() -> Option { utc_nanos().map(|nanos| nanos / NANOS_PER_SEC) } const NANOS_PER_SEC: u64 = 1_000_000_000; -/// Nanoseconds since the Unix epoch, UTC: the RTC's whole-second reading carried -/// on by the counter, so its resolution is the counter's and its accuracy the -/// RTC's second. +/// Nanoseconds since the Unix epoch, UTC: firmware's whole-second reading +/// carried on by the counter, so its resolution is the counter's and its +/// accuracy firmware's second. pub fn utc_nanos() -> Option { WALL_KNOWN .load(Acquire) diff --git a/kernel/src/drivers/acpi.rs b/kernel/src/drivers/acpi.rs index e4c89b5761d..92e7285c92f 100644 --- a/kernel/src/drivers/acpi.rs +++ b/kernel/src/drivers/acpi.rs @@ -13,7 +13,7 @@ use core::mem::size_of; use core::ptr::{read_unaligned, read_volatile}; use crate::log; use crate::DirectMap; -use toyos_acpi::{Century, MadtEntry, Mapped, Memory, CMOS_RAM, MADT_ENTRIES, SDT_HEADER_LEN, SDT_REVISION}; +use toyos_acpi::{MadtEntry, Mapped, Memory, MADT_ENTRIES, SDT_HEADER_LEN, SDT_REVISION}; pub use toyos_acpi::{IoApicEntry, SourceOverride, TableError}; @@ -167,30 +167,6 @@ pub fn ecam_windows(rsdp_addr: u64) -> Vec { windows } -/// Which CMOS register holds the RTC's century, as the FADT names it. -// `Ok(None)` is "no century register", distinct from `Err`, which the caller must not treat as one. -pub fn rtc_century_register(rsdp_addr: u64) -> Result, TableError> { - let named = toyos_acpi::rtc_century(direct_phys(), rsdp_addr)?; - match named { - Century::Absent => { - log!("ACPI: the FADT names no RTC century register"); - Ok(None) - } - Century::OutOfRange(index) => { - log!( - "ACPI: the FADT puts the RTC century register at CMOS {index:#04x}, outside {:#04x}..={:#04x} — ignoring it", - CMOS_RAM.start(), - CMOS_RAM.end() - ); - Ok(None) - } - Century::At(index) => { - log!("ACPI: the FADT puts the RTC century register at CMOS {index:#04x}"); - Ok(Some(index)) - } - } -} - /// Given the RSDP address, parse XSDT -> HPET table -> return HPET MMIO base address. pub fn find_hpet_base(rsdp_addr: u64) -> Option { let base = match toyos_acpi::hpet_base(direct_phys(), rsdp_addr) { diff --git a/kernel/src/irq_census.rs b/kernel/src/irq_census.rs index 42d80bb1027..b61ed7a8c3f 100644 --- a/kernel/src/irq_census.rs +++ b/kernel/src/irq_census.rs @@ -8,50 +8,41 @@ use core::sync::atomic::{AtomicU64, Ordering}; use crate::arch::percpu; -/// The census's sources: these, and between the device vectors and the IPIs, -/// those the architecture names (`arch::census_sources!`). -macro_rules! sources { - ($($(#[$meta:meta])* $arch:ident => $name:literal,)*) => { - /// One variant per interrupt source this kernel counts; order matches `arch::idt`'s vector table. - /// Adding a device vector means adding a variant here, or the architecture's, and one `irq_took!` at its handler. - /// A variant nothing counts does not compile: `irq_took!` is the only reference, so `-D dead-code` refuses an unconstructed one. - #[derive(Clone, Copy, PartialEq, Eq, Debug)] - #[repr(usize)] - pub enum Source { - /// Vector 0x20, this CPU's LAPIC one-shot: an expiry and nothing else. - Timer, - /// Vector 0xFC, `irqchip::kick_cpu`'s IPI. - Kick, - /// Vectors 0x28-0x2C: the MSI-X of a PCI function a process drives, and - /// the lines of an ISA function one does. One count for all five: which - /// claim an interrupt belonged to is the claim's own record, and the census - /// is about this machine's interrupt routing. - UserDev, - /// Vector 0x25, the remapping unit's fault event. - DmaFault, - $($(#[$meta])* $arch,)* - /// Vector 0xFE, the TLB shootdown IPI. - Tlb, - /// Vector 0x02, which `crate::hardlockup` samples a CPU with. - Nmi, - /// Vector 0xFF, the local APIC's spurious vector. - /// A non-zero count on a machine that staged nothing is an interrupt-routing defect. - Spurious, - /// Every vector no `idt_vectors!` row claims — `arch::trap::unclaimed`. - /// A non-zero count a boot staged nothing for is a routing defect the gate kept off `#DF`. - Unclaimed, - } - - impl Source { - /// Order `tests/common/irqcensus.rs` parses back; must match variant order. - pub const NAMES: &'static [&'static str] = - &["timer", "kick", "userdev", "dmafault", $($name,)* "tlb", "nmi", "spurious", "unclaimed"]; - pub const COUNT: usize = Self::NAMES.len(); - } - }; +/// One variant per interrupt source this kernel counts; order matches `arch::idt`'s vector table. +/// Adding a device vector means adding a variant here and one `irq_took!` at its handler. +/// A variant nothing counts does not compile: `irq_took!` is the only reference, so `-D dead-code` refuses an unconstructed one. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[repr(usize)] +pub enum Source { + /// Vector 0x20, this CPU's LAPIC one-shot: an expiry and nothing else. + Timer, + /// Vector 0xFC, `irqchip::kick_cpu`'s IPI. + Kick, + /// Vectors 0x28-0x2C: the MSI-X of a PCI function a process drives, and + /// the lines of an ISA function one does. One count for all five: which + /// claim an interrupt belonged to is the claim's own record, and the census + /// is about this machine's interrupt routing. + UserDev, + /// Vector 0x25, the remapping unit's fault event. + DmaFault, + /// Vector 0xFE, the TLB shootdown IPI. + Tlb, + /// Vector 0x02, which `crate::hardlockup` samples a CPU with. + Nmi, + /// Vector 0xFF, the local APIC's spurious vector. + /// A non-zero count on a machine that staged nothing is an interrupt-routing defect. + Spurious, + /// Every vector no `idt_vectors!` row claims — `arch::trap::unclaimed`. + /// A non-zero count a boot staged nothing for is a routing defect the gate kept off `#DF`. + Unclaimed, } -crate::arch::census_sources!(sources); +impl Source { + /// Order `tests/common/irqcensus.rs` parses back; must match variant order. + pub const NAMES: &'static [&'static str] = + &["timer", "kick", "userdev", "dmafault", "tlb", "nmi", "spurious", "unclaimed"]; + pub const COUNT: usize = Self::NAMES.len(); +} /// Every source's count summed, bar the NMI's. fn sum_bar_nmi(counts: &[u64; Source::COUNT]) -> u64 { diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 20dd724630c..58b548b66a8 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -374,7 +374,8 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { symbols::set_kernel_base(kernel_args.kernel_memory_addr); symbols::load_kernel(kernel_image.symbols, mm::PHYS_OFFSET + kernel_args.kernel_memory_addr); - arch::boot::clock(kernel_args); + arch::boot::clock(kernel_args.rsdp_addr); + clock::init_wall(kernel_args); trace::enable(); arch::boot::timer(); // After both halves of what it needs: a counter period to convert its bound diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index bd7b8d3d927..0d27da4113c 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -193,8 +193,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> SYS_SET_THREAD_POINTER => sys_set_thread_pointer(a1), SYS_THREAD_JOIN => sys_thread_join(a1), SYS_YIELD => sys_yield(), - // Answers from the boot-time anchor, never the CMOS: NotSupported beats a - // 1970-epoch number a caller cannot tell from real time. + // NotSupported beats a 1970-epoch number a caller cannot tell from real time. SYS_CLOCK_EPOCH => { crate::clock::utc_secs().unwrap_or(SyscallError::NotSupported.to_u64()) } diff --git a/kernel/src/time.rs b/kernel/src/time.rs index e82238b6824..8115d92b6f7 100644 --- a/kernel/src/time.rs +++ b/kernel/src/time.rs @@ -1,4 +1,4 @@ -//! A duration is typed by what its expiry means: [`Bound`], [`Cadence`], +//! A duration is typed by what its expiry means: [`Cadence`], //! [`Tripwire`], [`Deadline`], [`Floor`], [`Budget`], [`Delay`]. No kind is //! constructible from a bare magnitude; every constructor takes a //! `&'static str` justification beside the number. @@ -116,36 +116,6 @@ impl Deadline { } } -/// The contract that says the thing will happen: sourced from a device -/// register or a cited spec section, and its expiry means the device broke — -/// a named error, never a retry. -#[derive(Clone, Copy)] -pub struct Bound { - limit: Duration, - cite: &'static str, -} - -impl Bound { - /// A bound a specification states. `cite` is the section. - pub const fn from_spec(limit: Duration, cite: &'static str) -> Self { - Self { limit, cite } - } - - pub const fn duration(self) -> Duration { - self.limit - } - - pub const fn nanos(self) -> u64 { - self.limit.nanos() - } -} - -impl fmt::Display for Bound { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} ({})", self.limit, self.cite) - } -} - /// How often a thing may be re-done, and what makes that rate affordable. /// Nothing expires; a `Cadence` is a rate, not a register-poll limit. #[derive(Clone, Copy)] @@ -171,9 +141,9 @@ impl fmt::Display for Cadence { } /// A duration whose expiry is a **panic**: the machine is broken, fail fast. -/// The argument is how long is absurd, not where the number came from — a -/// tripwire that could be cited would be a [`Bound`]. A tripwire smaller than -/// the largest [`Bound`] on the path it covers panics a healthy machine. +/// The argument is how long is absurd, not where the number came from. A +/// tripwire smaller than the longest wait a specification allows on the path +/// it covers panics a healthy machine. #[derive(Clone, Copy)] pub struct Tripwire { limit: Duration, diff --git a/lib/acpi/src/fadt.rs b/lib/acpi/src/fadt.rs index b2c0b112236..1c9f1bb3d8e 100644 --- a/lib/acpi/src/fadt.rs +++ b/lib/acpi/src/fadt.rs @@ -2,13 +2,12 @@ use core::num::NonZeroU8; -use crate::{find_table, Phys, Table, TableError, SDT_REVISION}; +use crate::{Phys, Table, SDT_REVISION}; /// Table 5.9 offsets, from the start of the table. pub(crate) const FADT_FIRMWARE_CTRL: usize = 36; pub const FADT_DSDT: usize = 40; pub const FADT_PM1A_CNT_BLK: usize = 64; -const FADT_CENTURY: usize = 108; const FADT_FLAGS: usize = 112; /// A Generic Address Structure (§5.2.3.2): space at +0, bit width at +1, bit offset at +2, address at +4. const FADT_RESET_REG: usize = 116; @@ -28,37 +27,6 @@ const SPACE_SYSTEM_MEMORY: u8 = 0; const SPACE_SYSTEM_IO: u8 = 1; const SPACE_PCI_CONFIG: u8 = 2; -/// What the FADT says about the RTC's century register. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Century { - /// The century field is zero: this firmware names no register. - Absent, - /// Outside CMOS RAM, so it is not a century register whatever it is. - OutOfRange(u8), - At(u8), -} - -/// 0x80+ selects with NMI-mask bit 7 set; below 0x0E is the RTC's own clock/status regs, not a century register. -pub const CMOS_RAM: core::ops::RangeInclusive = 0x0E..=0x7F; - -/// Which CMOS register holds the RTC's century, as the FADT names it. -pub fn rtc_century(phys: P, rsdp_addr: u64) -> Result { - const NEEDED: usize = FADT_CENTURY + 1; - let fadt = find_table(phys, rsdp_addr, b"FACP", NEEDED)?; - let declared = fadt - .byte(FADT_CENTURY) - .ok_or(TableError::Length { declared: fadt.len() as u32, needed: NEEDED })?; - Ok(century_of(declared)) -} - -pub fn century_of(index: u8) -> Century { - match index { - 0 => Century::Absent, - i if CMOS_RAM.contains(&i) => Century::At(i), - i => Century::OutOfRange(i), - } -} - /// The 8-bit System I/O port the FADT names and the byte to write there, or the /// field that refused one — the alternative being a guessed port, and a guess /// writes a byte to whatever else lives there. diff --git a/lib/acpi/src/lib.rs b/lib/acpi/src/lib.rs index 6b320540b10..0debb9e6251 100644 --- a/lib/acpi/src/lib.rs +++ b/lib/acpi/src/lib.rs @@ -32,9 +32,8 @@ use toyos_bootmap::DirectMapEnd; pub use ecdt::{ecdt, Ec, EcRefused, EcRegister, ECDT_NEEDED}; pub use facs::{acquire, facs, release, Facs, FacsRefused, FACS_GLOBAL_LOCK, OWNED, PENDING}; pub use fadt::{ - century_of, dsdt_address, fixed_hardware, pm1a_control, pm_timer, psci, reset_register, - rtc_century, - Century, Field, FixedHardware, FixedRefused, LegacyMode, PmTimer, PowerButton, Psci, Reset, SmiCmd, CMOS_RAM, + dsdt_address, fixed_hardware, pm1a_control, pm_timer, psci, reset_register, + Field, FixedHardware, FixedRefused, LegacyMode, PmTimer, PowerButton, Psci, Reset, SmiCmd, FADT_FOR_FIXED_HARDWARE, FADT_PM1A_CNT_BLK, }; use fadt::FADT_DSDT; diff --git a/lib/acpi/tests/corpus.rs b/lib/acpi/tests/corpus.rs index 41825050963..12f4c07ca9c 100644 --- a/lib/acpi/tests/corpus.rs +++ b/lib/acpi/tests/corpus.rs @@ -12,7 +12,7 @@ use common::{declare_len, entry, madt, rsdp, sdt, xsdt, Machine}; use toyos_abi::acpi::Block; use toyos_acpi::{ definition_blocks, dsdt_address, ecam_allocations, find_by_id, find_table, fixed_hardware, hpet_base, isa_line, - madt_entries, pm1a_control, psci, reset_register, rtc_century, sci_line, Century, + madt_entries, pm1a_control, psci, reset_register, sci_line, Field, FixedRefused, LegacyMode, Line, MadtEntry, MadtHalt, Phys, Polarity, PowerButton, Psci, SmiCmd, Reset, SourceOverride, Table, TableError, Trigger, FADT_FOR_FIXED_HARDWARE, MADT_ENTRIES, MAX_TABLE_LEN, @@ -236,13 +236,12 @@ fn an_rsdp_address_the_reader_cannot_reach_is_refused() { assert_eq!(hpet_base(Machine { regions }, RSDP_AT).err(), Some(TableError::BadRsdp)); } -/// An ACPI 1.0-length FADT: 116 bytes, no `X_DSDT`, and the century byte still -/// inside it. The 32-bit `DSDT` is what such a table names. +/// An ACPI 1.0-length FADT: 116 bytes, no `X_DSDT`. The 32-bit `DSDT` is +/// what such a table names. #[test] fn a_fadt_of_the_acpi_1_0_length_is_read_without_its_x_fields() { let mut body = vec![0u8; 80]; body[40 - 36..44 - 36].copy_from_slice(&0x7ffb_9000u32.to_le_bytes()); - body[108 - 36] = 0x32; let t = sdt(b"FACP", 1, &body); assert_eq!(t.len(), 116); let head = rsdp(XSDT_AT, 2, 36); @@ -252,20 +251,6 @@ fn a_fadt_of_the_acpi_1_0_length_is_read_without_its_x_fields() { let fadt = find_table(m, RSDP_AT, b"FACP", 109).expect("FADT"); assert_eq!(dsdt_address(&fadt), 0x7ffb_9000); - assert_eq!(rtc_century(m, RSDP_AT), Ok(Century::At(0x32))); -} - -/// A FADT that ends with the century byte reads it. -#[test] -fn a_fadt_that_ends_at_the_century_byte_reads_it() { - let t = sdt(b"FACP", 1, &[0u8; 73]); - assert_eq!(t.len(), 109); - let head = rsdp(XSDT_AT, 2, 36); - let root = xsdt(&[TABLE_AT]); - let regions: &[(u64, &[u8])] = &[(RSDP_AT, &head), (XSDT_AT, &root), (TABLE_AT, &t)]; - let m = Machine { regions }; - // The century byte at 108 is the last one it holds. - assert_eq!(rtc_century(m, RSDP_AT), Ok(Century::Absent)); } #[test] @@ -308,26 +293,6 @@ fn a_revision_1_fadt_long_enough_to_hold_x_dsdt_is_still_read_at_its_32_bit_fiel assert_eq!(dsdt_address(&fadt), 0xdead_beef_0000); } -#[test] -fn a_century_register_outside_cmos_ram_is_told_apart_from_none() { - for (byte, want) in [ - (0u8, Century::Absent), - (0x0d, Century::OutOfRange(0x0d)), - (0x80, Century::OutOfRange(0x80)), - (0xff, Century::OutOfRange(0xff)), - (0x0e, Century::At(0x0e)), - (0x7f, Century::At(0x7f)), - ] { - let mut body = vec![0u8; 80]; - body[108 - 36] = byte; - let t = sdt(b"FACP", 1, &body); - let head = rsdp(XSDT_AT, 2, 36); - let root = xsdt(&[TABLE_AT]); - let regions: &[(u64, &[u8])] = &[(RSDP_AT, &head), (XSDT_AT, &root), (TABLE_AT, &t)]; - assert_eq!(rtc_century(Machine { regions }, RSDP_AT), Ok(want), "century byte {byte:#04x}"); - } -} - const FIXTURES: &[(&str, &[u8], u64)] = &[ ("rsdp", include_bytes!("../fixtures/qemu-11.1.1/rsdp.bin"), 0x7fb7_e014), ("xsdt", include_bytes!("../fixtures/qemu-11.1.1/xsdt.bin"), 0x7fb7_d0e8), @@ -341,7 +306,7 @@ const FIXTURES: &[(&str, &[u8], u64)] = &[ /// **No panic and no unbounded walk, over every byte of every table this crate /// decodes.** Each byte of each fixture takes each of its 255 other values in -/// turn, and the MCFG walk, `hpet_base`, `rtc_century` and the +/// turn, and the MCFG walk, `hpet_base` and the /// MADT walk to exhaustion all run over it. A panic anywhere — including the /// reader's own, which fires on a read no bound accepted — reds this. /// @@ -387,7 +352,6 @@ fn no_single_byte_mutation_of_a_real_table_panics_or_runs_away() { assert!(allocations.count() <= MAX_TABLE_LEN / 16 + 1, "{which}[{offset}]={value:#04x}: the MCFG walk is not ending"); } let _ = hpet_base(m, rsdp_at); - let _ = rtc_century(m, rsdp_at); if let Ok(blocks) = definition_blocks(m, rsdp_at) { // The DSDT, and at most an item an 8-byte entry of the largest XSDT. assert!(blocks.count() <= 1 + MAX_TABLE_LEN / 8, "{which}[{offset}]={value:#04x}: the block walk is not ending"); diff --git a/lib/acpi/tests/fixtures.rs b/lib/acpi/tests/fixtures.rs index 573bd9ca380..904a112b1ce 100644 --- a/lib/acpi/tests/fixtures.rs +++ b/lib/acpi/tests/fixtures.rs @@ -6,9 +6,9 @@ mod common; use common::Machine; use toyos_abi::acpi::Block; use toyos_acpi::{ - century_of, definition_blocks, dsdt_address, ecam_allocations, find_table, fixed_hardware, hpet_base, - isa_line, madt_entries, pm1a_control, pm_timer, psci, reset_register, rtc_century, sci_line, PmTimer, - Century, FixedHardware, IoApicEntry, Line, MadtEntry, Polarity, PowerButton, Psci, Reset, SmiCmd, + definition_blocks, dsdt_address, ecam_allocations, find_table, fixed_hardware, hpet_base, + isa_line, madt_entries, pm1a_control, pm_timer, psci, reset_register, sci_line, PmTimer, + FixedHardware, IoApicEntry, Line, MadtEntry, Polarity, PowerButton, Psci, Reset, SmiCmd, SourceOverride, TableError, Trigger, FADT_FOR_FIXED_HARDWARE, FADT_PM1A_CNT_BLK, MADT_ENTRIES, }; @@ -97,14 +97,6 @@ fn the_hpet_table_names_the_window_the_timer_driver_mapped() { assert_eq!(hpet_base(machine(), RSDP), Ok(0xfed0_0000)); } -/// `ACPI: the FADT puts the RTC century register at CMOS 0x32`. -#[test] -fn the_fadt_names_the_century_register_the_wall_clock_read() { - assert_eq!(rtc_century(machine(), RSDP), Ok(Century::At(0x32))); - // The actuator's override is applied to the raw byte, not to the verdict. - assert_eq!(century_of(0), Century::Absent); -} - /// `ACPI: PM1a=0x604`, and the DSDT this boot found `\_S5_` in. #[test] fn the_fadt_names_the_power_block_and_the_dsdt() { diff --git a/system/logkeeper/src/main.rs b/system/logkeeper/src/main.rs index c4237b1953b..59f03b29ee3 100644 --- a/system/logkeeper/src/main.rs +++ b/system/logkeeper/src/main.rs @@ -152,7 +152,7 @@ fn main() { // The supervisor connected before it started anything, so this is already queued. let from_supervisor: Connection = acceptor.accept().expect("logkeeper: the supervisor's origins connection"); - // The wall clock, read once. The kernel reads the RTC once too, so a second + // The wall clock, read once. The kernel anchors it once too, so a second // reading later in the boot would answer out of the same anchor and tell // this program nothing new. let (stem, boot_secs, dated) = boot_stamp(); diff --git a/tests/checks.rs b/tests/checks.rs index fd4d2f856f8..44b0a0f1681 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -368,7 +368,7 @@ mod checks { const PROGRAM: &str = "[2026-11-03 22:34:24 13.800 test-runner pid=7] counters_metal settle: the log holds this first line"; const BEFORE: &str = "[2026-11-03 22:34:23 12.094 cpu2 kernel] isa: a line before the second"; const REFINED: &str = "[2026-11-04 06:25:59 13.700 cpu5 kernel] clock: over 1 s the TSC counted 2419185510351 mHz against the HPET, within 11437 ppb; the clock keeps CPUID 15H's 2419200000000 mHz"; - const STEP: &str = "[2026-11-04 06:26:03 13.750 cpu12 kernel] clock: the TSC reads 1718866 ns from the chipset's counters over the last span; the RTC's edges after it will say which moved"; + const STEP: &str = "[2026-11-04 06:26:03 13.750 cpu12 kernel] clock: TSC STEP: the TSC stepped 1718866 ns forward against the HPET and the PM timer; the clock slews it away at most 500 ppm, most of it in about 3437 ms"; const SLEWED: &str = "[2026-11-04 06:26:03 13.760 cpu12 kernel] clock: the TSC's step is slewed away, 601 ns left"; let log = |lines: &[&str]| lines.join("\n"); @@ -646,6 +646,25 @@ mod checks { Ok(()) } + /// The wall clock's row passes a boot dated in the span its host saw it + /// run in, and fails one the kernel anchored off the loader's reading, or + /// dated two hours east or a quarter of an hour west of that span. + #[test] + fn a_wall_clock_off_the_hosts_fails_its_row() { + const LOADER: &str = "Wall clock: firmware's GetTime reads 2026-09-29 18:22:37 UTC at counter 5000\n"; + let boot = |anchored: &str, dated: &str| { + let log = format!( + "[{dated} 0.400 cpu0 kernel] clock: firmware's clock read {anchored} UTC\n\ + [{dated} 1.165 cpu0 kernel] Boot: complete (1165ms)\n" + ); + metal::Readback::new("boot", LOADER.into(), log, &metal_checks::boot_file()).expect("a readback") + }; + assert_eq!(wall_clock_is_firmwares(&boot("2026-09-29 18:22:37", "2026-09-29 18:22:39")), Ok(())); + assert!(wall_clock_is_firmwares(&boot("2026-09-29 18:22:38", "2026-09-29 18:22:39")).is_err()); + assert!(wall_clock_is_firmwares(&boot("2026-09-29 18:22:37", "2026-09-29 20:22:39")).is_err()); + assert!(wall_clock_is_firmwares(&boot("2026-09-29 18:22:37", "2026-09-29 18:07:39")).is_err()); + } + /// A slewed step passes the clock's rows and a demotion fails them. #[test] fn a_demoted_clock_fails_its_rows() { diff --git a/tests/checks/metal.rs b/tests/checks/metal.rs index c6fdd638af0..49cd6ec842e 100644 --- a/tests/checks/metal.rs +++ b/tests/checks/metal.rs @@ -72,6 +72,10 @@ const STARTED: &str = "[2026-09-29 18:22:38 0.050 cpu0 kernel] spawn: /system/b [2026-09-29 18:22:38 0.050 supervisor] supervisor: started logkeeper\n"; /// A T14 boot's `boot.txt` as the loop writes it. +/// When [`boot_file`]'s boot came back, by its host's clock: 2026-09-29 +/// 18:23:19 UTC, 40 s after [`BOOTED`]'s date. +const BACK_AT: u64 = 1_790_706_199; + pub(super) fn boot_file() -> String { let phases = Phases { preamble: 3.1, @@ -82,7 +86,7 @@ pub(super) fn boot_file() -> String { read_log: 1.4, raw_log: Some(9.9), }; - BootFile { back: 40, machine: t14(), phases }.text() + BootFile { back: 40, back_at: BACK_AT, machine: t14(), phases }.text() } /// One boot's readback as the loop writes it. diff --git a/tests/common/faults.rs b/tests/common/faults.rs index d1d87f4b217..7e55ee7ae09 100644 --- a/tests/common/faults.rs +++ b/tests/common/faults.rs @@ -150,9 +150,9 @@ const TSC_DEMOTED: &str = "clock: DEMOTED: "; const DEMOTED_READS: &str = "clock: a process's reading of now asked the kernel since the TSC was demoted"; /// A TSC `tsc-step` reads 10 ms ahead, and 10 ms further once the clock has -/// slewed the first step away: the clock reports the first -/// ([`kernel::clock::TSC_STEP_READ`]) and slews it, the second demotes the -/// TSC, and processes' readings of now reach the kernel after it. +/// slewed the first step away: the clock reports the first and slews it, the +/// second demotes the TSC, and processes' readings of now reach the kernel +/// after it. pub fn a_stepped_tsc_is_slewed_then_demoted(test_config: &Path) -> Result<(), String> { let mut qemu = QemuInstance::boot_with_options( test_config, @@ -161,13 +161,12 @@ pub fn a_stepped_tsc_is_slewed_then_demoted(test_config: &Path) -> Result<(), St BootOptions { profile: qemu::Profile::Metal, kernel_params: &["tsc-step"], ready_marker: TSC_STEP_SLEWED, ..Default::default() }, ); let mut serial = qemu.boot_log().to_string(); - // A 10 ms step at 500 ppm is 20 s of slewing; the rest is the RTC's edges either side of each step. + // A 10 ms step at 500 ppm is 20 s of slewing. serial.push_str(&qemu.drain_until(std::time::Duration::from_secs(120), |l| l.contains(DEMOTED_READS) || l.contains("PANIC"))); let lines: Vec<&str> = serial.lines().collect(); let at = |marker: &str| lines.iter().position(|l| l.contains(marker)).ok_or_else(|| format!("no {marker:?} line\n{serial}")); let order = [ at(TSC_STEP_STAGED)?, - at(kernel::clock::TSC_STEP_READ)?, at(TSC_STEP_SLEWED)?, at(TSC_SLEWED_AWAY)?, at(TSC_STEP_AGAIN)?, @@ -177,11 +176,11 @@ pub fn a_stepped_tsc_is_slewed_then_demoted(test_config: &Path) -> Result<(), St if !order.is_sorted() || serial.contains("PANIC") { return Err(format!("the steps, the slew and the demotion are out of order: {order:?}\n{serial}")); } - let stepped: Option = lines[order[2]].split(TSC_STEP_SLEWED).nth(1).and_then(|r| r.split(" ns").next()).and_then(|n| n.parse().ok()); + let stepped: Option = lines[order[1]].split(TSC_STEP_SLEWED).nth(1).and_then(|r| r.split(" ns").next()).and_then(|n| n.parse().ok()); if !stepped.is_some_and(|ns| (9_000_000..=11_000_000).contains(&ns)) { - return Err(format!("the slewed step is not the staged 10 ms: {}\n{serial}", lines[order[2]].trim())); + return Err(format!("the slewed step is not the staged 10 ms: {}\n{serial}", lines[order[1]].trim())); } - for i in [1, 2, 5, 6] { + for i in [1, 4, 5] { eprintln!(" [tsc_step] {}", lines[order[i]].trim()); } Ok(()) @@ -219,3 +218,22 @@ pub fn a_stalled_hpet_is_left_out(test_config: &Path) -> Result<(), String> { eprintln!(" [hpet_stall] {}", staged_then_moved.trim()); Ok(()) } + +/// The same of a boot's whole kernel log, on a machine: the stall staged, the +/// span left out as the HPET's, a report after it, and the TSC neither +/// stepped nor demoted, which is what a PM timer that did not tell the two +/// apart would leave. +pub fn stalled_hpet_left_out(log: &str) -> Result<(), String> { + let staged = log + .find(HPET_STALL_STAGED) + .ok_or_else(|| format!("no {HPET_STALL_STAGED:?}: the boot was not armed, or ended before the clock's first report staged it"))?; + let moved = log[staged..].find(HPET_MOVED).map(|at| staged + at).ok_or_else(|| format!("no {HPET_MOVED:?} after the stall"))?; + if !log[moved..].contains(CLOCK_REPORT) { + return Err("the clock did not report again after it left the span out".to_string()); + } + if let Some(line) = log.lines().find(|line| line.contains(TSC_STEP_SLEWED) || line.contains(TSC_DEMOTED)) { + return Err(format!("the stall was read as the TSC's: {}", line.trim())); + } + eprintln!(" [hpet_stall] {}", log[moved..].lines().next().unwrap_or_default().trim()); + Ok(()) +} diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs index 20b038e198f..86e59f7d41c 100644 --- a/tests/common/irqcensus.rs +++ b/tests/common/irqcensus.rs @@ -11,7 +11,7 @@ use std::collections::BTreeMap; /// and [`census_sources`] refuses a line whose fields are not exactly these, so /// a source added on one side and not the other is a red rather than a silently /// dropped column. -pub const SOURCES: [&str; 9] = ["timer", "kick", "userdev", "dmafault", "rtc", "tlb", "nmi", "spurious", "unclaimed"]; +pub const SOURCES: [&str; 8] = ["timer", "kick", "userdev", "dmafault", "tlb", "nmi", "spurious", "unclaimed"]; /// Whether one line is a census line whose sources are exactly [`SOURCES`], /// or `None` for a line that is no census. diff --git a/tests/common/metal.rs b/tests/common/metal.rs index e792f31fdda..860c4b8aaeb 100644 --- a/tests/common/metal.rs +++ b/tests/common/metal.rs @@ -222,6 +222,8 @@ pub struct Readback { pub boot_ms: Option, /// What the machine spent getting back to Ubuntu's ssh server. pub back_secs: u64, + /// The host's clock as it got there, in Unix seconds. + back_at: u64, /// The machine the loop read before the flash. pub machine: Result, /// Where the loop's wall time went. @@ -238,6 +240,8 @@ impl Readback { let kernel = bootlog::kernel_records(&log); let back_secs = toyos_metal::back_secs(boot) .ok_or_else(|| format!("{label}'s boot file names no `back_secs`: {boot:?}"))?; + let back_at = toyos_metal::back_unix_secs(boot) + .ok_or_else(|| format!("{label}'s boot file names no `back_unix_secs`: {boot:?}"))?; let phases = toyos_metal::phases(boot) .map_err(|why| format!("{label}'s boot file {why}: {boot:?}"))?; Ok(Readback { @@ -247,6 +251,7 @@ impl Readback { kernel, log, back_secs, + back_at, machine: toyos_metal::machine(boot) .map_err(|why| format!("{label}'s boot file {why}")), phases, @@ -272,6 +277,14 @@ impl Readback { Serial::named(&format!("{}'s loader.log", self.label), self.loader.as_str()) } + /// The Unix seconds, by the host's clock, this boot ran within: from the + /// reboot that started it to the machine answering again, a second's + /// rounding either side. + pub fn ran_within(&self) -> std::ops::RangeInclusive { + let since = self.back_secs + self.phases.down.ceil() as u64 + 1; + self.back_at.saturating_sub(since)..=self.back_at + 1 + } + /// When the last record this boot left was written, on the log's clock. /// `None` on a log with no record at all. pub fn last_record_ms(&self) -> Option { diff --git a/tests/metal/src/lib.rs b/tests/metal/src/lib.rs index a80f7a138fa..b8879ed7bbc 100644 --- a/tests/metal/src/lib.rs +++ b/tests/metal/src/lib.rs @@ -704,6 +704,10 @@ pub const FLASHABLE: &[&str] = &[ // kernel path reads or writes a port for that entry, so it reaches no // device; what it changes is that a claim on the i8042's row is refused. "isa-port-declared", + // From the clock's first report on, the sampler's readings of the HPET are + // 10 ms behind what the counter read. It writes no register; the clock + // leaves one span out and the boot goes on. + "hpet-stall", // Three nested `scheduler::Operation`s with known deadlines, in both homes, // each printing what it asked for and what it observed. It establishes and // drops them and reaches nothing else. @@ -1690,6 +1694,10 @@ pub fn run(args: &Args) -> Result, Refusal> { } let (down, back) = driver.ride_the_reboot(wait_secs)?; + let back_at = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("this host's clock reads after 1970") + .as_secs(); println!("the machine answered ssh again after {back} s"); let began = std::time::Instant::now(); let (loader, log_text, stopped) = driver.read_log()?; @@ -1716,7 +1724,7 @@ pub fn run(args: &Args) -> Result, Refusal> { let raw_log = args.fat32_check.then(|| began.elapsed().as_secs_f64()); let phases = Phases { preamble, flash, dd_secs: installed.dd, entry, down, read_log, raw_log }; println!("phases: {phases}"); - let boot = BootFile { back, machine, phases }.text(); + let boot = BootFile { back, back_at, machine, phases }.text(); let verdict = judge_and_write_readback(&armed, &loader, &log_text, stopped, args.readback.as_deref(), &boot); // After the readback, so a boot that moved the order is still judged; the // order's refusal is the louder answer, since the next failure loops. @@ -1950,6 +1958,9 @@ pub const READBACK_VOLUME: &str = "log-partition.img"; /// The keys [`READBACK_BOOT`] carries, one ` ` per line. pub const BACK_SECS: &str = "back_secs"; +/// This host's clock as the machine answered `ssh` again, in Unix seconds: +/// the boot ran in the [`BACK_SECS`] and the going down before it. +pub const BACK_UNIX_SECS: &str = "back_unix_secs"; /// The machine the boot ran on, as [`Machine::QUERY`] answered before the flash. pub const VENDOR_KEY: &str = "machine_vendor"; @@ -2014,16 +2025,18 @@ impl fmt::Display for Phases { /// machine it ran on. pub struct BootFile { pub back: u64, + pub back_at: u64, pub machine: Machine, pub phases: Phases, } impl BootFile { pub fn text(&self) -> String { - let Self { back, machine, phases } = self; + let Self { back, back_at, machine, phases } = self; let secs = |s: f64| format!("{s:.3}"); let rows = [ (BACK_SECS, back.to_string()), + (BACK_UNIX_SECS, back_at.to_string()), (VENDOR_KEY, machine.vendor.clone()), (PRODUCT_KEY, machine.product.clone()), (BIOS_KEY, machine.bios.clone()), @@ -2119,6 +2132,10 @@ pub fn back_secs(text: &str) -> Option { key(text, BACK_SECS) } +pub fn back_unix_secs(text: &str) -> Option { + key(text, BACK_UNIX_SECS) +} + /// The machine a readback names, or why it names none. pub fn machine(text: &str) -> Result { match (word(text, VENDOR_KEY), word(text, PRODUCT_KEY), word(text, BIOS_KEY)) { @@ -2363,9 +2380,10 @@ mod tests { #[test] fn the_machine_crosses_in_the_boot_file() { - let boot = BootFile { back: 47, machine: t14(), phases: phased(None) }.text(); + let boot = BootFile { back: 47, back_at: 1_993_799_665, machine: t14(), phases: phased(None) }.text(); assert_eq!(machine(&boot), Ok(t14())); assert_eq!(back_secs(&boot), Some(47)); + assert_eq!(back_unix_secs(&boot), Some(1_993_799_665)); assert!(machine("back_secs 47\nmachine_vendor LENOVO\n").is_err()); } @@ -2374,11 +2392,11 @@ mod tests { #[test] fn the_phases_cross_in_the_boot_file() { for raw_log in [Some(9.875), None] { - let boot = BootFile { back: 141, machine: t14(), phases: phased(raw_log) }.text(); + let boot = BootFile { back: 141, back_at: 0, machine: t14(), phases: phased(raw_log) }.text(); assert_eq!(phases(&boot), Ok(phased(raw_log)), "{boot}"); assert_eq!(back_secs(&boot), Some(141)); for line in boot.lines().filter(|line| line.contains("_secs ")) { - if line.starts_with(BACK_SECS) { + if line.starts_with(BACK_SECS) || line.starts_with(BACK_UNIX_SECS) { continue; } let short = boot.replace(&format!("{line}\n"), ""); @@ -2386,7 +2404,7 @@ mod tests { } } assert!(phases("back_secs 47\n").is_err()); - let garbled = BootFile { back: 1, machine: t14(), phases: phased(None) } + let garbled = BootFile { back: 1, back_at: 0, machine: t14(), phases: phased(None) } .text() .replace("flash_secs 121.500", "flash_secs later"); assert!(phases(&garbled).is_err()); diff --git a/tests/toyos.rs b/tests/toyos.rs index 23851775012..a4f744435e6 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -1159,7 +1159,7 @@ const METAL: &[(&str, metal::Metal)] = &[ ), // ---- the boot facts, riding the same tests/images/testcases.toml image ---- // - // **Seven names and no extra minute of the machine.** Every needle below is a + // **Eight names and no extra minute of the machine.** Every needle below is a // record the *shipping* kernel writes on any boot it takes, so each rides // whatever image is already going on the bench; the two rows after them are // the two things this suite measures rather than reads, and those cost a @@ -1202,6 +1202,13 @@ const METAL: &[(&str, metal::Metal)] = &[ }, }, ), + ( + // The wall clock the kernel anchors on the loader's reading of + // firmware's `GetTime`, against this host's clock: which answer each + // bench's firmware gives is the machine's, and QEMU's is OVMF's. + "wall_clock_is_firmwares", + metal::Metal { arms: TESTCASES, judge: |b| wall_clock_is_firmwares(b[0]) }, + ), ( "pci_inventory", metal::Metal { arms: TESTCASES, judge: |b| pci_inventory(b[0].kernel().text()) }, @@ -1435,6 +1442,13 @@ const METAL: &[(&str, metal::Metal)] = &[ "lapic_spurious_vector", metal::Metal { arms: SELFTESTS, judge: |b| lapic_vectors(b[0].kernel().text()) }, ), + ( + // The HPET an actuator stalls, told from the TSC by each bench's own + // ACPI PM timer, which is all that tells them apart: its readings and + // their brackets are the machine's. + "a_stalled_hpet_is_told_by_the_pm_timer", + metal::Metal { arms: SELFTESTS, judge: |b| faults::stalled_hpet_left_out(b[0].kernel().text()) }, + ), ( // No drain on this side: the whole boot's records are on the log, so // the probe's line is either in them or it never ran. @@ -2139,12 +2153,15 @@ const PROCTREECASE: &[metal::Arm] = &[metal::once( /// what the machine *is*: each stages inputs the hardware cannot produce — a /// crafted capability list, a malformed descriptor, a vector nothing claims — /// runs a check over them and prints a count. So they arm the boot -/// [`ACTUATOR_TESTS`] ride, which is the same kernel. +/// [`ACTUATOR_TESTS`] ride, which is the same kernel. `hpet-stall` rides it +/// too: it changes one reading the clock's sampler takes, which leaves one +/// span out. const SELFTEST_PARAMS: &[&str] = &[ "pci-cap-selftest", "lapic-spurious-selftest", "unclaimed-vector-selftest", "sysret-ss-probe", + "hpet-stall", ]; const SELFTESTS: &[metal::Arm] = &[metal::Arm { @@ -7074,7 +7091,7 @@ const ACPI_FIRST_INTERRUPT: &str = "isa: the ACPI fixed hardware took its first /// once; /// - the clock's TSC refinement ([`kernel::clock::TSC_REFINED`]), written on a /// scheduler pass and arming no timer; -/// - its step report ([`kernel::clock::TSC_STEP_READ`]), which the AMD bench's +/// - its step report ([`kernel::clock::TSC_STEP`]), which the AMD bench's /// deep idle provokes once a boot /// (`issues/the-amd-benchs-tsc-steps-1-7-ms-forward-once-a-boot.md`). fn idle_second_lines(log: &str, from_ms: u64, to_ms: u64) -> Result, Vec<&str>> { @@ -7089,7 +7106,7 @@ fn idle_second_lines(log: &str, from_ms: u64, to_ms: u64) -> Result, V .is_some_and(|p| p.source == toyos_logstream::Source::Kernel && p.text == ACPI_FIRST_INTERRUPT); let clock = toyos_logstream::parse(line).is_some_and(|p| { p.source == toyos_logstream::Source::Kernel - && (p.text.starts_with(kernel::clock::TSC_REFINED) || p.text.starts_with(kernel::clock::TSC_STEP_READ)) + && (p.text.starts_with(kernel::clock::TSC_REFINED) || p.text.starts_with(kernel::clock::TSC_STEP)) }); named && once(line) || clock }; @@ -7918,6 +7935,58 @@ fn timer_calibration(log: &str) -> Result<(), String> { Ok(()) } +/// How far a bench's hardware clock may stand from this host's: a third of +/// the smallest step between two zones, 15 minutes, so a clock kept in any +/// zone but UTC is outside it, and far past what two disciplined clocks and a +/// whole-second reading leave. +const WALL_CLOCK_APART_SECS: u64 = 300; + +/// `YYYY-MM-DD HH:MM:SS` where `text` starts, as Unix seconds. +fn unix_secs_at(text: &str) -> Option { + let mut fields = text.get(..19)?.split(['-', ' ', ':']).map(|field| field.parse::().ok()); + let mut next = || fields.next().flatten(); + let civil = toyos_wallclock::Civil { year: next()?, month: next()?, day: next()?, hour: next()?, min: next()?, sec: next()? }; + civil.is_valid().then(|| civil.to_unix_secs()) +} + +/// **The wall clock is firmware's, and firmware's is the time.** The kernel +/// says the instant it anchored on, which is the one the loader said firmware's +/// `GetTime` answered; and the boot's last dated line, which `logkeeper` dates +/// by `SYS_CLOCK_EPOCH`, lies in the span this host's clock saw the boot run +/// in ([`metal::Readback::ran_within`]), [`WALL_CLOCK_APART_SECS`] either side. +fn wall_clock_is_firmwares(boot: &metal::Readback) -> Result<(), String> { + const LOADER_READ: &str = "Wall clock: firmware's GetTime reads "; + const KERNEL_READ: &str = "clock: firmware's clock read "; + let read = |line: &str, said: &str| { + line.split_once(said).and_then(|(_, rest)| unix_secs_at(rest)).ok_or_else(|| format!("no instant after {said:?} in {line:?}")) + }; + let (loader, kernel, log) = (boot.loader(), boot.kernel(), boot.log()); + let handed = read(loader.must_say(LOADER_READ)?, LOADER_READ)?; + let anchored = read(kernel.must_say(KERNEL_READ)?, KERNEL_READ)?; + if handed != anchored { + return Err(format!("the loader handed {handed} and the kernel anchored on {anchored}, in Unix seconds")); + } + let (line, dated) = log + .text() + .lines() + .rev() + .find_map(|line| Some((line, unix_secs_at(line.strip_prefix('[')?)?))) + .ok_or("no line of the log carries a date")?; + let ran = boot.ran_within(); + if dated + WALL_CLOCK_APART_SECS < *ran.start() || dated > ran.end() + WALL_CLOCK_APART_SECS { + return Err(format!( + "the log's last dated line reads {dated} in Unix seconds, and this host's clock saw the boot run in {ran:?}, \ + {WALL_CLOCK_APART_SECS} s allowed either side: {}", + line.trim() + )); + } + eprintln!( + " [wall] firmware read {handed}; the last dated line reads {dated}, {} s before the machine answered this host again", + ran.end().saturating_sub(dated) + ); + Ok(()) +} + /// The rate the boot started the clock at: CPUID 15H's, or one the HPET /// measured with its bound, which on the bench is a rate the clock can keep at /// all. Its bound is held against minutes by [`clock_holds_over_minutes`]. @@ -7973,11 +8042,11 @@ fn clock_report(line: &str) -> Option { }) } -/// Whether the boot's clock kept the TSC: every step it slewed, every span it -/// left out and every step it waited on the RTC for is printed, and a -/// demotion fails the row, since a demoted boot's processes read the HPET. +/// Whether the boot's clock kept the TSC: every step it slewed and every span +/// it left out is printed, and a demotion fails the row, since a demoted +/// boot's processes read the HPET. fn clock_kept_its_counter(log: &str) -> Result<(), String> { - const SAID: [&str; 4] = ["clock: TSC STEP: ", "so this span is left out", "so the span was left out", "the RTC's edges after it will say which moved"]; + const SAID: [&str; 2] = [kernel::clock::TSC_STEP, "so this span is left out"]; for line in log.lines().filter(|line| SAID.iter().any(|said| line.contains(said))) { eprintln!(" [clock] {}", line.trim()); } From 2bddec19e0673a040d572e730baff3f2ecd8f5bb Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 05:11:02 +0200 Subject: [PATCH 2/3] Review r1 of #1213: the stall's row holds its own boot, the wall clock'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 32e11074e 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 --- ...-that-stall-together-read-as-a-tsc-step.md | 59 +++++++++ ...-runs-only-what-no-cheaper-tier-reaches.md | 1 - kernel/pure/clock/mod.rs | 18 +-- kernel/src/arch/x86_64/clocksource.rs | 7 +- lib/wallclock/src/lib.rs | 2 +- sdk/abi/src/syscall.rs | 10 +- tests/checks.rs | 78 +++++++++--- tests/checks/metal.rs | 2 +- tests/common/faults.rs | 38 ++++-- .../rust-tests/src/bin/hpet_stall_read.rs | 38 ++++++ tests/toyos.rs | 117 ++++++++++++------ 11 files changed, 277 insertions(+), 93 deletions(-) create mode 100644 issues/chipset-counters-that-stall-together-read-as-a-tsc-step.md create mode 100644 tests/guest/rust-tests/src/bin/hpet_stall_read.rs diff --git a/issues/chipset-counters-that-stall-together-read-as-a-tsc-step.md b/issues/chipset-counters-that-stall-together-read-as-a-tsc-step.md new file mode 100644 index 00000000000..75cbdd2b0e2 --- /dev/null +++ b/issues/chipset-counters-that-stall-together-read-as-a-tsc-step.md @@ -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`. diff --git a/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md b/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md index a5c7e291670..87f50a577f5 100644 --- a/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md +++ b/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md @@ -95,7 +95,6 @@ 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 firmware's time as UTC. - metal `spawn_cwd`: a spawn's working directory. - host `log_conservation_smp2`: every record is read or counted lost. - unrepresentable `lock_across_switch_halts`: a lock guard cannot be held across a switch. diff --git a/kernel/pure/clock/mod.rs b/kernel/pure/clock/mod.rs index 6125b5bfb5c..ff72bcf8553 100644 --- a/kernel/pure/clock/mod.rs +++ b/kernel/pure/clock/mod.rs @@ -18,16 +18,11 @@ pub const TSC_REFINED: &str = "clock: over "; /// written as the sampler finds it. pub const TSC_STEP: &str = "clock: TSC STEP: "; -/// Unix seconds at boot, from a reading of `secs` true when the clock read -/// `at`, which is before boot where the loader took it; both are stamps, -/// nanoseconds since the counter's zero. Only the whole seconds between the -/// two move the answer. +/// 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)] @@ -37,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); diff --git a/kernel/src/arch/x86_64/clocksource.rs b/kernel/src/arch/x86_64/clocksource.rs index 814bce15aa0..13b14118435 100644 --- a/kernel/src/arch/x86_64/clocksource.rs +++ b/kernel/src/arch/x86_64/clocksource.rs @@ -12,10 +12,9 @@ //! **Where the TSC and the HPET disagree, the PM timer says which moved.** It //! agreeing with the TSC is the HPET moving, and the span is left out. It //! agreeing with the HPET, or a machine with none, is taken as the TSC's -//! step. The chipset's two counters stopping together would read the same -//! and the clock would follow them: nothing here tells that from a step, and -//! neither counter may stop in S0 (ACPI 6.5 §4.8.3.3; IA-PC HPET -//! Specification 1.0a, §2.3.3). The boot's first forward step is slewed away; +//! step, which the chipset's counters standing still together would read as +//! too (`issues/chipset-counters-that-stall-together-read-as-a-tsc-step.md`). +//! The boot's first forward step is slewed away; //! a second, one back, a check that finds a CPU's TSC written or behind, or //! an AP's start that proves its offset demotes the TSC ([`demote`]), and //! every process's reading of now asks the kernel, which counts by the HPET. diff --git a/lib/wallclock/src/lib.rs b/lib/wallclock/src/lib.rs index 33fe876339a..1818911b9c7 100644 --- a/lib/wallclock/src/lib.rs +++ b/lib/wallclock/src/lib.rs @@ -12,7 +12,7 @@ const DAY: u64 = 86_400; /// A wall-clock instant in the fields a human reads. /// -/// The one calendar in the tree. The RTC decodes its registers into this, the +/// The one calendar in the tree. The loader reads firmware's time into this, the /// log's file names come out of it, the compositor's clock reads it and FAT's /// directory stamps are built from it — so there is one conversion between /// seconds and dates rather than one per caller. diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index a27d64aafb3..f33ce7547c5 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -765,7 +765,7 @@ pub struct Stat { /// When the file was last written: nanoseconds since the Unix epoch, UTC, /// off the wall clock at the write, to the resolution its mount keeps — /// the nanosecond on `/tmp` and DATA, two seconds at the flush on FAT. 0 is - /// undated: written on a machine whose RTC never answered, or shipped in + /// undated: written on a machine whose firmware gave no time, or shipped in /// ROOT's reproducible image. pub mtime: u64, /// The volume the file is on; 0 for an object that is no file of any @@ -1305,10 +1305,10 @@ pub fn random(buf: &mut [u8]) -> Result<(), SyscallError> { /// Seconds since the Unix epoch (1970-01-01 00:00:00 UTC). /// -/// `None` is a machine that never said what time it is — an RTC that is absent, -/// wedged, or answering with something that is not a date. It is `None` for the -/// whole of such a boot rather than intermittently, because the kernel reads -/// the clock once. +/// `None` is a machine that never said what time it is: firmware's `GetTime` +/// failed, or answered something that is not a date. It is `None` for the +/// whole of such a boot rather than intermittently, because the loader asks +/// once. /// Cheap: the kernel serves it from an anchor it took at boot plus the /// monotonic clock, so this is a syscall and not a device access. pub fn clock_epoch() -> Option { diff --git a/tests/checks.rs b/tests/checks.rs index 44b0a0f1681..a9967250663 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -646,23 +646,69 @@ mod checks { Ok(()) } - /// The wall clock's row passes a boot dated in the span its host saw it - /// run in, and fails one the kernel anchored off the loader's reading, or - /// dated two hours east or a quarter of an hour west of that span. + /// A line dated by the loader's reading carried on the log's clock + /// passes, to the second it lands in; an anchor ten seconds off is red, + /// whichever way; and a bench whose clock stands two hours east carries + /// its reading as well as any, and is red as the bench's. #[test] - fn a_wall_clock_off_the_hosts_fails_its_row() { - const LOADER: &str = "Wall clock: firmware's GetTime reads 2026-09-29 18:22:37 UTC at counter 5000\n"; - let boot = |anchored: &str, dated: &str| { - let log = format!( - "[{dated} 0.400 cpu0 kernel] clock: firmware's clock read {anchored} UTC\n\ - [{dated} 1.165 cpu0 kernel] Boot: complete (1165ms)\n" - ); - metal::Readback::new("boot", LOADER.into(), log, &metal_checks::boot_file()).expect("a readback") - }; - assert_eq!(wall_clock_is_firmwares(&boot("2026-09-29 18:22:37", "2026-09-29 18:22:39")), Ok(())); - assert!(wall_clock_is_firmwares(&boot("2026-09-29 18:22:38", "2026-09-29 18:22:39")).is_err()); - assert!(wall_clock_is_firmwares(&boot("2026-09-29 18:22:37", "2026-09-29 20:22:39")).is_err()); - assert!(wall_clock_is_firmwares(&boot("2026-09-29 18:22:37", "2026-09-29 18:07:39")).is_err()); + fn a_date_off_the_loaders_reading_is_red_and_a_bench_off_utc_is_the_benchs() { + const RATE: &str = "[10.300 cpu0 kernel] clock: the TSC counts at 1000000000 Hz, as CPUID 15H states, held to the HPET from a reading 90 cycles wide\n"; + // Read 10.000 s on the log's clock, so a line at 12.165 is dated two seconds on. + let loader = |read: &str| format!("[10.000 cpu0 loader] Wall clock: firmware's GetTime reads {read} UTC at counter 10000000000\n"); + let log = |dated: &str| format!("[{dated} 12.165 cpu0 kernel] Boot: complete (1165ms)\n"); + let carried = |read: &str, dated: &str| dates_carry_the_loaders_reading(&loader(read), RATE, &log(dated)); + let read = carried("2026-09-29 18:22:37", "2026-09-29 18:22:39").expect("a line dated by the reading"); + for dated in ["2026-09-29 18:22:37", "2026-09-29 18:22:40"] { + assert_eq!(carried("2026-09-29 18:22:37", dated), Ok(read), "{dated} is inside what the divisions drop"); + } + for dated in ["2026-09-29 18:22:49", "2026-09-29 18:22:29", "2026-09-29 18:22:36", "2026-09-29 18:22:41"] { + let refused = carried("2026-09-29 18:22:37", dated).expect_err(dated); + assert!(refused.contains("from the loader's reading"), "{refused}"); + } + assert!(dates_carry_the_loaders_reading(&loader("2026-09-29 18:22:37"), RATE, "[12.165 cpu0 kernel] undated\n").is_err()); + + let boot = metal::Readback::new("boot", String::new(), String::new(), &metal_checks::boot_file()).expect("a readback"); + assert_eq!(bench_clock_keeps_utc(read, boot.ran_within()), Ok(())); + let east = carried("2026-09-29 20:22:37", "2026-09-29 20:22:39").expect("a bench off UTC carries its reading too"); + for off in [east, read - 900] { + let refused = bench_clock_keeps_utc(off, boot.ran_within()).expect_err("a bench off UTC"); + assert!(refused.contains("this bench's hardware clock stands"), "{refused}"); + } + } + + /// The staged stall's row reads the staged span alone: left out with the + /// PM timer 10 ms from the HPET and a report after it, on a bench whose + /// TSC steps for real in another span or in that one; and red where the + /// span was read as the TSC's, where the PM timer stood with the HPET, or + /// never, or no report followed. + #[test] + fn a_staged_stall_is_judged_by_its_own_span() { + const FIRST: &str = "[11.370 cpu0 kernel] clock: over 1 s the TSC counted 2419185510351 mHz against the HPET, within 11437 ppb; the clock keeps CPUID 15H's 2419200000000 mHz; 0 spans left out; 0 TSC step(s) slewed\n"; + const STAGED: &str = "[11.370 cpu0 kernel] hpet-stall: the HPET reads 192000 ticks behind from here\n"; + const MOVED: &str = "[12.375 cpu3 kernel] clock: the HPET moved and the TSC did not, so this span is left out: the TSC counted 2431296000 ticks, the HPET 995001000 ns and reads 9999052 ns apart, 523256 allowed (a to b); the PM timer counted 1005000100 ns and reads 1487 ns apart, 516738 allowed (c to d)\n"; + const REPORT: &str = "[13.380 cpu1 kernel] clock: over 2 s the TSC counted 2419185510351 mHz against the HPET, within 5718 ppb; the clock keeps CPUID 15H's 2419200000000 mHz; 1 spans left out; 0 TSC step(s) slewed\n"; + const REAL_STEP: &str = "[ 9.120 cpu5 kernel] clock: TSC STEP: the TSC stepped 1718866 ns forward against the HPET and the PM timer; the clock slews it away at most 500 ppm, most of it in about 3437 ms: the TSC counted 1799000000 ticks, the HPET 998281134 ns and reads 1718866 ns apart, 520000 allowed (a to b); the PM timer counted 998281000 ns and reads 1718700 ns apart, 518000 allowed (c to d)\n"; + const AS_THE_TSCS: &str = "[12.375 cpu3 kernel] clock: TSC STEP: the TSC stepped 9999052 ns forward against the HPET; the clock slews it away at most 500 ppm, most of it in about 19998 ms: the TSC counted 2431296000 ticks, the HPET 995001000 ns and reads 9999052 ns apart, 523256 allowed (a to b); no PM timer\n"; + const ALL: &str = "[12.375 cpu3 kernel] clock: the TSC, the HPET and the ACPI PM timer all disagree, so this span is left out: the TSC counted 2431296000 ticks, the HPET 995001000 ns and reads 11717918 ns apart, 523256 allowed (a to b); the PM timer counted 1003281134 ns and reads 1718866 ns apart, 516738 allowed (c to d)\n"; + let judged = |lines: &[&str]| faults::stalled_hpet_left_out(&lines.concat()); + + assert_eq!(judged(&[FIRST, STAGED, MOVED, REPORT]), Ok(())); + // The AMD bench: its own step before the staging, or after the staged span was read. + assert_eq!(judged(&[REAL_STEP, FIRST, STAGED, MOVED, REPORT]), Ok(())); + assert_eq!(judged(&[FIRST, STAGED, MOVED, REAL_STEP, REPORT]), Ok(())); + // Its own step inside the staged span: every counter disagrees, and the PM timer still tells the stall. + assert_eq!(judged(&[FIRST, STAGED, ALL, REPORT]), Ok(())); + let untold = ALL.replace("reads 1718866 ns apart", "reads 9100000 ns apart"); + for (why, lines) in [ + ("read as the TSC's", &[FIRST, STAGED, AS_THE_TSCS, REPORT][..]), + ("a PM timer that stands with neither", &[FIRST, STAGED, untold.as_str(), REPORT]), + ("the bench's step read where the stall should be", &[FIRST, STAGED, REAL_STEP, MOVED, REPORT]), + ("no report after", &[FIRST, STAGED, MOVED]), + ("never read", &[FIRST, STAGED]), + ("never staged", &[FIRST, MOVED, REPORT]), + ] { + assert!(judged(lines).is_err(), "{why} judged green"); + } } /// A slewed step passes the clock's rows and a demotion fails them. diff --git a/tests/checks/metal.rs b/tests/checks/metal.rs index 49cd6ec842e..e3c38ee2631 100644 --- a/tests/checks/metal.rs +++ b/tests/checks/metal.rs @@ -71,11 +71,11 @@ fn loader() -> String { const STARTED: &str = "[2026-09-29 18:22:38 0.050 cpu0 kernel] spawn: /system/bin/logkeeper pid=6\n\ [2026-09-29 18:22:38 0.050 supervisor] supervisor: started logkeeper\n"; -/// A T14 boot's `boot.txt` as the loop writes it. /// When [`boot_file`]'s boot came back, by its host's clock: 2026-09-29 /// 18:23:19 UTC, 40 s after [`BOOTED`]'s date. const BACK_AT: u64 = 1_790_706_199; +/// A T14 boot's `boot.txt` as the loop writes it. pub(super) fn boot_file() -> String { let phases = Phases { preamble: 3.1, diff --git a/tests/common/faults.rs b/tests/common/faults.rs index 7e55ee7ae09..86483605ec9 100644 --- a/tests/common/faults.rs +++ b/tests/common/faults.rs @@ -219,21 +219,33 @@ pub fn a_stalled_hpet_is_left_out(test_config: &Path) -> Result<(), String> { Ok(()) } -/// The same of a boot's whole kernel log, on a machine: the stall staged, the -/// span left out as the HPET's, a report after it, and the TSC neither -/// stepped nor demoted, which is what a PM timer that did not tell the two -/// apart would leave. +/// The records `log` holds of the span `hpet-stall` staged, on a machine: the +/// first disagreement the clock wrote after the staging is that span's, since +/// the sample after the staging reads it. The span is left out, and the PM +/// timer reads the TSC 10 ms nearer than the HPET does, which is the staged +/// stall put on the HPET and not on the TSC; and the clock reported again +/// after it. A step the machine's own TSC took is no part of it, in another +/// span or in this one, where every counter then disagrees and the PM timer +/// still stands the 10 ms from the HPET. pub fn stalled_hpet_left_out(log: &str) -> Result<(), String> { - let staged = log - .find(HPET_STALL_STAGED) - .ok_or_else(|| format!("no {HPET_STALL_STAGED:?}: the boot was not armed, or ended before the clock's first report staged it"))?; - let moved = log[staged..].find(HPET_MOVED).map(|at| staged + at).ok_or_else(|| format!("no {HPET_MOVED:?} after the stall"))?; - if !log[moved..].contains(CLOCK_REPORT) { - return Err("the clock did not report again after it left the span out".to_string()); + const ALL_DISAGREE: &str = "all disagree, so this span is left out: "; + const DISAGREED: [&str; 4] = [HPET_MOVED, ALL_DISAGREE, kernel::clock::TSC_STEP, TSC_DEMOTED]; + let mut after = log.lines().skip_while(|line| !line.contains(HPET_STALL_STAGED)).skip(1); + let read = after + .by_ref() + .find(|line| DISAGREED.iter().any(|said| line.contains(said))) + .ok_or_else(|| format!("no record of the span {HPET_STALL_STAGED:?} staged: the boot was not armed, or ended before the next sample"))?; + let apart = |spans: &str| -> Option { spans.split_once(" and reads ")?.1.split_once(" ns apart")?.0.parse().ok() }; + let told = [HPET_MOVED, ALL_DISAGREE].iter().find_map(|said| read.split_once(said)).and_then(|(_, spans)| { + let (hpet, pm) = spans.split_once("; the PM timer counted ")?; + Some(apart(hpet)? - apart(pm)?) + }); + if !told.is_some_and(|ns| (9_000_000..=11_000_000).contains(&ns)) { + return Err(format!("the PM timer did not put the staged 10 ms on the HPET and leave the span out: {}", read.trim())); } - if let Some(line) = log.lines().find(|line| line.contains(TSC_STEP_SLEWED) || line.contains(TSC_DEMOTED)) { - return Err(format!("the stall was read as the TSC's: {}", line.trim())); + if !after.any(|line| line.contains(CLOCK_REPORT)) { + return Err("the clock did not report again after it left the staged span out".to_string()); } - eprintln!(" [hpet_stall] {}", log[moved..].lines().next().unwrap_or_default().trim()); + eprintln!(" [hpet_stall] {}", read.trim()); Ok(()) } diff --git a/tests/guest/rust-tests/src/bin/hpet_stall_read.rs b/tests/guest/rust-tests/src/bin/hpet_stall_read.rs new file mode 100644 index 00000000000..51577bcfb07 --- /dev/null +++ b/tests/guest/rust-tests/src/bin/hpet_stall_read.rs @@ -0,0 +1,38 @@ +//! Wait until the kernel's clock has read the span `hpet-stall` staged and +//! reported once more, and exit non-zero where the runner's own bound comes +//! near first. +//! +//! The `a_stalled_hpet_is_told_by_the_pm_timer` metal row runs it on the boot +//! armed with `hpet-stall`: the kernel stages the stall at its clock's first +//! report, the next sample reads the staged span, and the report after that +//! says the clock went on. The row's judge reads those records, so this holds +//! the boot until they exist and judges nothing. + +use std::time::Duration; + +#[path = "../served_log.rs"] +mod served_log; + +/// The kernel's records, as `kernel/src/arch/x86_64/clocksource.rs` writes +/// them: the stall staged, and a report of the clock's. +const STAGED: &str = "hpet-stall: the HPET reads "; +const REPORT: &str = "clock: over "; + +/// How long after boot the wait gives up: the runner's bound less a tenth. +const UNTIL_MS: u64 = toyos_tco::JOB_BOUND_MS - toyos_tco::JOB_BOUND_MS / 10; + +fn main() { + let since_boot = toyos_abi::clock::nanos_since_boot() / 1_000_000; + let bound = Duration::from_millis(UNTIL_MS.saturating_sub(since_boot)); + let mut staged = false; + served_log::Log::open().until("the clock's report after the span hpet-stall staged", bound, |line| { + let kernel = toyos_logstream::parse(line).filter(|said| said.source == toyos_logstream::Source::Kernel); + let Some(said) = kernel else { return false }; + if said.text.starts_with(STAGED) { + staged = true; + return false; + } + staged && said.text.starts_with(REPORT) + }); + println!("hpet_stall_read: the clock read the staged span and reported after it"); +} diff --git a/tests/toyos.rs b/tests/toyos.rs index a4f744435e6..7ee57ffcfaa 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -80,6 +80,9 @@ const RUST_SKIP: &[&str] = &[ // The log-write load `mask_windows` reads windows under, on the boot // whose kernel reports them. "log_write", + // It waits for records only a boot armed with `hpet-stall` writes: the + // `a_stalled_hpet_is_told_by_the_pm_timer` metal row runs it there. + "hpet_stall_read", // It opens a window, which needs a compositor the shared boot does not // run: `a_window_is_drawn_at_the_scale_and_relaid_at_the_next` runs it on // tests/images/scalecase.toml, and the `gop_edid_scale` metal row on @@ -1203,9 +1206,10 @@ const METAL: &[(&str, metal::Metal)] = &[ }, ), ( - // The wall clock the kernel anchors on the loader's reading of - // firmware's `GetTime`, against this host's clock: which answer each - // bench's firmware gives is the machine's, and QEMU's is OVMF's. + // Every dated line is the loader's reading of firmware's `GetTime` + // carried on by the log's clock, and that reading is this host's + // time: which answer each bench's firmware gives is the machine's, + // and QEMU's is OVMF's. "wall_clock_is_firmwares", metal::Metal { arms: TESTCASES, judge: |b| wall_clock_is_firmwares(b[0]) }, ), @@ -1447,7 +1451,16 @@ const METAL: &[(&str, metal::Metal)] = &[ // ACPI PM timer, which is all that tells them apart: its readings and // their brackets are the machine's. "a_stalled_hpet_is_told_by_the_pm_timer", - metal::Metal { arms: SELFTESTS, judge: |b| faults::stalled_hpet_left_out(b[0].kernel().text()) }, + metal::Metal { + arms: &[metal::Arm { + features: ACTUATOR_KERNEL, + ..metal::once("shared-debug", "tests/images/testcases.toml", SELFTEST_PARAMS, &["test_rs_hpet_stall_read"]) + }], + judge: |b| { + b[0].job_passed("test_rs_hpet_stall_read")?; + faults::stalled_hpet_left_out(b[0].kernel().text()) + }, + }, ), ( // No drain on this side: the whole boot's records are on the log, so @@ -7937,9 +7950,9 @@ fn timer_calibration(log: &str) -> Result<(), String> { /// How far a bench's hardware clock may stand from this host's: a third of /// the smallest step between two zones, 15 minutes, so a clock kept in any -/// zone but UTC is outside it, and far past what two disciplined clocks and a -/// whole-second reading leave. -const WALL_CLOCK_APART_SECS: u64 = 300; +/// zone but UTC is outside it, and far past what the bench's other system +/// leaves a clock it disciplines. A bound on the bench, not on ToyOS. +const BENCH_CLOCK_APART_SECS: u64 = 300; /// `YYYY-MM-DD HH:MM:SS` where `text` starts, as Unix seconds. fn unix_secs_at(text: &str) -> Option { @@ -7949,44 +7962,72 @@ fn unix_secs_at(text: &str) -> Option { civil.is_valid().then(|| civil.to_unix_secs()) } -/// **The wall clock is firmware's, and firmware's is the time.** The kernel -/// says the instant it anchored on, which is the one the loader said firmware's -/// `GetTime` answered; and the boot's last dated line, which `logkeeper` dates -/// by `SYS_CLOCK_EPOCH`, lies in the span this host's clock saw the boot run -/// in ([`metal::Readback::ran_within`]), [`WALL_CLOCK_APART_SECS`] either side. -fn wall_clock_is_firmwares(boot: &metal::Readback) -> Result<(), String> { - const LOADER_READ: &str = "Wall clock: firmware's GetTime reads "; - const KERNEL_READ: &str = "clock: firmware's clock read "; - let read = |line: &str, said: &str| { - line.split_once(said).and_then(|(_, rest)| unix_secs_at(rest)).ok_or_else(|| format!("no instant after {said:?} in {line:?}")) - }; - let (loader, kernel, log) = (boot.loader(), boot.kernel(), boot.log()); - let handed = read(loader.must_say(LOADER_READ)?, LOADER_READ)?; - let anchored = read(kernel.must_say(KERNEL_READ)?, KERNEL_READ)?; - if handed != anchored { - return Err(format!("the loader handed {handed} and the kernel anchored on {anchored}, in Unix seconds")); +/// **Every dated line is the loader's reading carried on by the log's own +/// clock.** The loader says the instant firmware's `GetTime` answered and the +/// counter then; the counter over the rate the clock started at is that +/// reading's time on the log's clock, which counts from the counter's zero. +/// A line stamped `ms` is dated the reading plus the seconds between, less up +/// to three and plus up to one: the kernel's anchor, `SYS_CLOCK_EPOCH`, and +/// `logkeeper`'s two divisions each drop a part of a second, three one way and +/// one the other. A tenth of a second more either side is the rate the clock +/// retunes to and a step it slews, far under what they move a boot's stamps. +/// No host's clock is read. +fn dates_carry_the_loaders_reading(loader: &str, kernel: &str, log: &str) -> Result { + const READ: &str = "Wall clock: firmware's GetTime reads "; + const AT: &str = " UTC at counter "; + let said = loader.lines().find(|line| line.contains(READ)).ok_or_else(|| format!("the loader never said {READ:?}"))?; + let (read, counter) = said + .split_once(READ) + .and_then(|(_, rest)| Some((unix_secs_at(rest)?, rest.split_once(AT)?.1.trim().parse::().ok()?))) + .ok_or_else(|| format!("no instant and counter in {said:?}"))?; + let read_ms = u128::from(counter) * 1_000_000 / u128::from(boot_rate(kernel)?.millihertz); + let mut dated = 0; + for line in log.lines() { + let Some(date) = line.strip_prefix('[').and_then(unix_secs_at) else { continue }; + let Some(ms) = toyos_logstream::parse(line).and_then(|said| said.ms) else { continue }; + let carried = i128::from(read) * 1_000 + i128::from(ms) - read_ms as i128; + let ahead = i128::from(date) * 1_000 - carried; + if !(-3_100..=1_100).contains(&ahead) { + return Err(format!( + "a line is dated {ahead} ms from the loader's reading of {read} carried {} ms on by the log's clock: {}", + i128::from(ms) - read_ms as i128, + line.trim() + )); + } + dated += 1; } - let (line, dated) = log - .text() - .lines() - .rev() - .find_map(|line| Some((line, unix_secs_at(line.strip_prefix('[')?)?))) - .ok_or("no line of the log carries a date")?; - let ran = boot.ran_within(); - if dated + WALL_CLOCK_APART_SECS < *ran.start() || dated > ran.end() + WALL_CLOCK_APART_SECS { + if dated == 0 { + return Err("no line of the log carries a date and a time".to_string()); + } + eprintln!(" [wall] {dated} dated lines carry the loader's reading of {read}, taken {read_ms} ms on the log's clock"); + Ok(read) +} + +/// **The bench's part: its hardware clock keeps UTC.** Firmware's reading +/// lies in the span this host's clock saw the boot run in +/// ([`metal::Readback::ran_within`]), [`BENCH_CLOCK_APART_SECS`] either side. +/// With the bench's clock set, it is the independent reading of the loader's +/// decode of `EFI_TIME`. +fn bench_clock_keeps_utc(read: u64, ran: std::ops::RangeInclusive) -> Result<(), String> { + let apart = if read < *ran.start() { ran.start() - read } else { read.saturating_sub(*ran.end()) }; + if apart > BENCH_CLOCK_APART_SECS { return Err(format!( - "the log's last dated line reads {dated} in Unix seconds, and this host's clock saw the boot run in {ran:?}, \ - {WALL_CLOCK_APART_SECS} s allowed either side: {}", - line.trim() + "this bench's hardware clock stands {apart} s from this host's: firmware read {read} in Unix seconds and the \ + host saw the boot run in {ran:?}. The design requires the hardware clock to keep UTC; this is the bench's \ + setting and not ToyOS's reading of it" )); } - eprintln!( - " [wall] firmware read {handed}; the last dated line reads {dated}, {} s before the machine answered this host again", - ran.end().saturating_sub(dated) - ); + eprintln!(" [wall] firmware read {read}, {apart} s outside the span the host saw the boot run in"); Ok(()) } +/// The wall clock on a bench: ToyOS's carrying of firmware's reading, then +/// the bench's own clock. +fn wall_clock_is_firmwares(boot: &metal::Readback) -> Result<(), String> { + let read = dates_carry_the_loaders_reading(boot.loader().text(), boot.kernel().text(), boot.log().text())?; + bench_clock_keeps_utc(read, boot.ran_within()) +} + /// The rate the boot started the clock at: CPUID 15H's, or one the HPET /// measured with its bound, which on the bench is a rate the clock can keep at /// all. Its bound is held against minutes by [`clock_holds_over_minutes`]. From d7d8daf9890f3dc653ea8db64fcd7ec3476ea6c0 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 09:29:17 +0200 Subject: [PATCH 3/3] Review r2 of #1213: six comments say firmware where they said the RTC, 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 --- lib/fat32/src/time.rs | 2 +- lib/wallclock/src/lib.rs | 8 ++++---- system/fileserver/src/volume.rs | 2 +- tests/common/faults.rs | 3 ++- tests/guest/rust-tests/src/bin/file_mtime.rs | 4 ++-- tests/toyos.rs | 4 ++-- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/fat32/src/time.rs b/lib/fat32/src/time.rs index 63d96087058..32cfce80ce0 100644 --- a/lib/fat32/src/time.rs +++ b/lib/fat32/src/time.rs @@ -4,7 +4,7 @@ use toyos_wallclock::Civil; /// /// This crate never reads a clock. Every call that stamps an entry takes one /// of these, so the only time that reaches the disk is time the caller chose — -/// the kernel's RTC in production, a fixed value in a test that wants a +/// the kernel's wall clock in production, a fixed value in a test that wants a /// reproducible image. /// /// FAT's encoding is lossy in three ways that no wrapper can hide, so they are diff --git a/lib/wallclock/src/lib.rs b/lib/wallclock/src/lib.rs index 1818911b9c7..82d9ce2d208 100644 --- a/lib/wallclock/src/lib.rs +++ b/lib/wallclock/src/lib.rs @@ -37,8 +37,8 @@ impl Civil { /// /// The Unix epoch is the floor because everything downstream counts /// unsigned seconds from it. A leap second lands on 60 and is rejected: the - /// RTC does not report one, and a clock that does is not one this kernel - /// understands. + /// firmware's clock does not report one, and a clock that does is not one + /// this kernel understands. pub fn is_valid(&self) -> bool { (1970..=9999).contains(&self.year) && (1..=12).contains(&self.month) @@ -59,8 +59,8 @@ impl Civil { /// needs: a timestamp is not load-bearing enough to fail a volume read /// over, and its `every_bit_pattern_decodes` asserts it over all 65,536 /// date encodings. [`Self::is_valid`] is the *other* caller's answer — the - /// RTC's — and refusing there is what keeps an impossible instant out of - /// the wall clock. + /// loader's, of firmware's reading — and refusing there is what keeps an + /// impossible instant out of the wall clock. pub fn to_unix_secs(&self) -> u64 { days_from_civil(self.year, self.month, self.day) * DAY + self.hour * 3_600 diff --git a/system/fileserver/src/volume.rs b/system/fileserver/src/volume.rs index 2b29716cbd3..4a18f7e4dd7 100644 --- a/system/fileserver/src/volume.rs +++ b/system/fileserver/src/volume.rs @@ -397,7 +397,7 @@ pub fn take_anchor() { /// What a file written now is stamped with (`toyos_abi::syscall::Stat::mtime`): /// nanoseconds since the Unix epoch, UTC, and 0 — undated — on a machine whose -/// RTC never answered. +/// firmware gave no time. pub fn now_nanos() -> u64 { let anchor = ANCHOR.get().expect("fileserver takes the anchor at its start"); anchor.map_or(0, |secs| secs.saturating_mul(NANOS_PER_SEC).saturating_add(nanos_since_boot())) diff --git a/tests/common/faults.rs b/tests/common/faults.rs index 86483605ec9..b41698f7d43 100644 --- a/tests/common/faults.rs +++ b/tests/common/faults.rs @@ -226,7 +226,8 @@ pub fn a_stalled_hpet_is_left_out(test_config: &Path) -> Result<(), String> { /// stall put on the HPET and not on the TSC; and the clock reported again /// after it. A step the machine's own TSC took is no part of it, in another /// span or in this one, where every counter then disagrees and the PM timer -/// still stands the 10 ms from the HPET. +/// still stands the 10 ms from the HPET, to within a millisecond: the two +/// counters' allowances over a second, about half a millisecond each. pub fn stalled_hpet_left_out(log: &str) -> Result<(), String> { const ALL_DISAGREE: &str = "all disagree, so this span is left out: "; const DISAGREED: [&str; 4] = [HPET_MOVED, ALL_DISAGREE, kernel::clock::TSC_STEP, TSC_DEMOTED]; diff --git a/tests/guest/rust-tests/src/bin/file_mtime.rs b/tests/guest/rust-tests/src/bin/file_mtime.rs index bb7d6983c4f..903cd048bd6 100644 --- a/tests/guest/rust-tests/src/bin/file_mtime.rs +++ b/tests/guest/rust-tests/src/bin/file_mtime.rs @@ -6,8 +6,8 @@ //! a create with truncation, a create of a missing file, a truncation — and a //! later write's stamp is later, which a clock of whole seconds cannot say. //! `write ` makes the first judgement on `path` and `read ` only -//! prints what it finds there. `undated` runs on a machine whose RTC never -//! answered and never asks the time: a file written there, on `/tmp` or on +//! prints what it finds there. `undated` runs on a machine whose firmware +//! gave no time and never asks the time: a file written there, on `/tmp` or on //! `/home`, is undated, which std reports as an error and not as 1970. use std::fs::{self, OpenOptions}; diff --git a/tests/toyos.rs b/tests/toyos.rs index 7ee57ffcfaa..c4292f79f7c 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -7969,8 +7969,8 @@ fn unix_secs_at(text: &str) -> Option { /// A line stamped `ms` is dated the reading plus the seconds between, less up /// to three and plus up to one: the kernel's anchor, `SYS_CLOCK_EPOCH`, and /// `logkeeper`'s two divisions each drop a part of a second, three one way and -/// one the other. A tenth of a second more either side is the rate the clock -/// retunes to and a step it slews, far under what they move a boot's stamps. +/// one the other. A tenth of a second more either side is for the rate the +/// clock retunes to and a step it slews, which move a boot's stamps far less. /// No host's clock is read. fn dates_carry_the_loaders_reading(loader: &str, kernel: &str, log: &str) -> Result { const READ: &str = "Wall clock: firmware's GetTime reads ";