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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 25 additions & 22 deletions bootloader/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ mod rootimage;
mod seed;
mod slot;
mod stamp;
mod wallclock;
mod watchdog;

/// The largest file the bootloader will read off the ESP.
Expand Down Expand Up @@ -497,7 +498,7 @@ fn report_reach(what: &str, at: u64, len: u64) {
}

#[allow(clippy::too_many_arguments)]
fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec<u8>, cmdline: vec::Vec<u8>, rsdp_addr: u64, gop: Option<GopInfo>, boot_part: Option<BootPartition>, log_partition_guid: [u8; 16], root_image: rootimage::RootImage, entry_counter: u64, system_table: SystemTable<Boot>) -> ! {
fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec<u8>, cmdline: vec::Vec<u8>, rsdp_addr: u64, gop: Option<GopInfo>, boot_part: Option<BootPartition>, log_partition_guid: [u8; 16], root_image: rootimage::RootImage, entry_counter: u64, wall_clock: Option<(toyos_wallclock::Civil, u64)>, system_table: SystemTable<Boot>) -> ! {
// Said before it is refused, for `report_reach`'s reason.
match arch::cpu_as_entered() {
Ok(None) => {}
Expand Down Expand Up @@ -631,8 +632,16 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec<u8>, cmdline: v
// Read into this struct below, and nowhere else: the kernel zeroes it here.
loader_seed: [0; toyos_abi::boot::SEED_LEN],
loader_seed_len: 0,
wall_clock_secs: 0,
wall_clock_counter: 0,
wall_clock_known: 0,
};
kernel_args.loader_seed_len = seed::read(&system_table, &mut kernel_args.loader_seed);
if let Some((civil, counter)) = wall_clock {
kernel_args.wall_clock_secs = civil.to_unix_secs();
kernel_args.wall_clock_counter = counter;
kernel_args.wall_clock_known = 1;
}
report_reach(
"Kernel arguments",
&kernel_args as *const KernelArgs as u64,
Expand Down Expand Up @@ -705,25 +714,6 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec<u8>, cmdline: v
unsafe { arch::enter_kernel(image, kernel.entry_offset as u64, &kernel_args) }
}

/// When this pass armed the page, in Unix seconds, or 0 where firmware would
/// not say.
///
/// The zone is not applied and does not need to be: what a reader of the stick
/// asks of this number is whether the record beside it is *this* boot's
/// predecessor's or one left over, and an hour either way answers that.
fn armed_at(system_table: &SystemTable<Boot>) -> u64 {
let Ok(t) = system_table.runtime_services().get_time() else { return 0 };
toyos_wallclock::Civil {
year: u64::from(t.year()),
month: u64::from(t.month()),
day: u64::from(t.day()),
hour: u64::from(t.hour()),
min: u64::from(t.minute()),
sec: u64::from(t.second()),
}
.to_unix_secs()
}

/// End a pass that read the black box and boots no kernel, by resetting the
/// machine rather than returning to the boot manager.
///
Expand Down Expand Up @@ -959,9 +949,22 @@ fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
// Query UEFI GOP before exiting boot services
let gop = query_gop(&system_table);

// Read once, outside the watchdog's bound: the page is stamped with it, and
// the kernel carries it forward to its boot on the counter read beside it.
let wall_clock = match wallclock::now(&system_table) {
Ok((civil, counter)) => {
println!("Wall clock: firmware's GetTime reads {civil} UTC at counter {counter}");
Some((civil, counter))
}
Err(why) => {
println!("Wall clock: {why}, so the kernel is handed none");
None
}
};

// The page says a kernel is running, and `BootNext` says this loader gets the
// machine again however that kernel ends.
blackbox::arm(page, armed_at(&system_table), log_guid);
blackbox::arm(page, wall_clock.map_or(0, |(civil, _)| civil.to_unix_secs()), log_guid);
bootnext::point_at_us(handle, &system_table);

// The last act before the jump, so the smallest possible span of this loader
Expand All @@ -970,5 +973,5 @@ fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
watchdog::arm(&system_table, rsdp_addr, params);

println!("Starting kernel...");
start_kernel(loaded_kernel, kernel_bytes, cmdline, rsdp_addr, gop, boot_part, log_guid, chosen.root, entry_counter, system_table);
start_kernel(loaded_kernel, kernel_bytes, cmdline, rsdp_addr, gop, boot_part, log_guid, chosen.root, entry_counter, wall_clock, system_table);
}
43 changes: 43 additions & 0 deletions bootloader/src/wallclock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//! The wall clock as firmware keeps it: `GetTime` (UEFI 2.11 §8.3.1), a
//! runtime service the kernel never calls, asked here before
//! `ExitBootServices`. The hardware clock keeps UTC, so `EFI_TIME::TimeZone`
//! is not read; a date that does not exist is refused here, at the firmware's
//! boundary, and never reaches the kernel.

use core::fmt;

use toyos_wallclock::Civil;
use uefi::prelude::*;

/// Why firmware gave no time.
pub enum Refusal {
Failed(uefi::Status),
NotADate(Civil),
}

impl fmt::Display for Refusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Failed(status) => write!(f, "firmware's GetTime failed ({status:?})"),
Self::NotADate(civil) => write!(f, "firmware's GetTime answered {civil:?}, which is not a date"),
}
}
}

/// Firmware's time, and [`crate::arch::counter`] as it answered.
pub fn now(system_table: &SystemTable<Boot>) -> Result<(Civil, u64), Refusal> {
let time = system_table.runtime_services().get_time().map_err(|e| Refusal::Failed(e.status()))?;
let counter = crate::arch::counter();
let civil = Civil {
year: u64::from(time.year()),
month: u64::from(time.month()),
day: u64::from(time.day()),
hour: u64::from(time.hour()),
min: u64::from(time.minute()),
sec: u64::from(time.second()),
};
if !civil.is_valid() {
return Err(Refusal::NotADate(civil));
}
Ok((civil, counter))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
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_*` (`bootloader/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.

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.
56 changes: 56 additions & 0 deletions kernel/pure/clock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! What the clock computes from counter ticks: their span in nanoseconds
//! ([`ticks_to_nanos`]), and the Unix second at boot that a wall-clock reading
//! taken at some counter value fixes ([`anchor`]).

#![forbid(unsafe_code)]

const NANOS_PER_SEC: u64 = 1_000_000_000;

/// `ticks` of a counter whose tick is `period_fs` femtoseconds, in nanoseconds.
pub const fn ticks_to_nanos(ticks: u64, period_fs: u64) -> u64 {
((ticks as u128 * period_fs as u128) / 1_000_000) as u64
}

/// Unix seconds at the counter's `boot`, from a reading of `secs` true when the
/// counter read `at`, which is after boot where the kernel read the clock and
/// before it where the loader did. Only the whole seconds between the two move
/// the answer.
pub const fn anchor(secs: u64, at: u64, boot: u64, period_fs: u64) -> u64 {
if at >= boot {
secs.saturating_sub(ticks_to_nanos(at - boot, period_fs) / NANOS_PER_SEC)
} else {
secs.saturating_add(ticks_to_nanos(boot - at, period_fs) / NANOS_PER_SEC)
}
}

#[cfg(test)]
mod tests {
use super::*;

/// A one-nanosecond tick, so a tick count reads as nanoseconds.
const NS: u64 = 1_000_000;
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, NS), SECS - 3);
}

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

#[test]
fn a_reading_at_boot_is_boot() {
assert_eq!(anchor(SECS, BOOT, BOOT, NS), SECS);
}

#[test]
fn a_tick_is_counted_at_its_period() {
// A 24 MHz counter: 41_666_666 fs a tick, so 24e6 ticks fall just short of a second.
assert_eq!(ticks_to_nanos(24_000_000, 41_666_666), 999_999_984);
assert_eq!(anchor(SECS, BOOT - 48_000_001, BOOT, 41_666_666), SECS + 2);
}
}
7 changes: 4 additions & 3 deletions kernel/pure/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//! What the kernel decides without touching the machine: the scheduler core
//! ([`sched`]), the process and thread lifecycle ([`proclife`]), which PCID an
//! address space is handed ([`pcid`]), what type the range registers give a
//! range ([`mtrr`]), what a pipe's ends are told of each other ([`pipe`]) and
//! what a CPU waiting on the boot processor's write to `SMI_CMD` decides
//! ([`bootwrite`]).
//! range ([`mtrr`]), what a pipe's ends are told of each other ([`pipe`]), what
//! a CPU waiting on the boot processor's write to `SMI_CMD` decides
//! ([`bootwrite`]) and what the clock computes from counter ticks ([`clock`]).
//! The kernel binary links it; the host runs
//! its tests, because none of it reads a register, a clock or a kernel lock.

Expand All @@ -15,6 +15,7 @@ extern crate alloc;
extern crate std;

pub mod bootwrite;
pub mod clock;
pub mod mtrr;
pub mod pcid;
pub mod pipe;
Expand Down
7 changes: 4 additions & 3 deletions kernel/src/arch/aarch64/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,11 +346,12 @@ 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. No wall clock: `super::rtc` says why.
pub fn clock(_args: &KernelArgs) {
/// what the counter counts at; and the wall clock the loader handed over.
pub fn clock(args: &KernelArgs) {
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(), 1_000_000_000_000_000 / hz);
log!("clock: the generic timer counts at {hz} Hz; no wall clock is read on this architecture");
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
Expand Down
29 changes: 21 additions & 8 deletions kernel/src/arch/aarch64/rtc.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
//! The wall clock at boot: on an ACPI Arm machine, a PL031 the tables name,
//! the port's stage 6.
//! 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. None yet: the read is owed.
/// Why this machine did not say what time it is.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RtcFault {}
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 {}
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotHanded => write!(f, "the loader handed no reading of firmware's GetTime"),
}
}
}

pub fn read(_century_reg: Option<u8>) -> Result<Civil, RtcFault> {
owed!("the wall clock", "no stage yet")
/// 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"),
}
}
2 changes: 1 addition & 1 deletion kernel/src/arch/x86_64/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ pub fn clock(args: &KernelArgs) {
None
}
};
crate::clock::init_wall(century_reg);
crate::clock::init_wall(super::rtc::read(century_reg).map(|civil| (civil, super::cpu::counter())));
}

/// Where the TSC counts from: `IA32_TSC_ADJUST`, where CPUID says the CPU has
Expand Down
26 changes: 13 additions & 13 deletions kernel/src/clock.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
//! The machine's clocks: monotonic since boot, read off the CPU's free-running
//! counter at the period the architecture's boot gives [`set_counter`] (on
//! x86-64, measured against the HPET); wall-clock, read from the
//! architecture's RTC exactly once — a CMOS read can block for up to a
//! second — in [`init_wall`], and answered after as that reading plus
//! x86-64, measured against the HPET); 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 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.
Expand All @@ -12,6 +13,8 @@ use core::sync::atomic::{AtomicBool, AtomicU64, Ordering::{Acquire, Relaxed, Rel
use crate::arch::cpu;
use crate::log::LogStamp;
use crate::time::Instant;
use kernel::clock::{anchor, ticks_to_nanos};
use toyos_wallclock::Civil;

static TSC_BOOT: AtomicU64 = AtomicU64::new(0);
static TSC_PERIOD_FS: AtomicU64 = AtomicU64::new(0);
Expand Down Expand Up @@ -43,10 +46,6 @@ pub fn set_counter(boot: u64, period_fs: u64) {
publish_page(boot, period_fs, at_boot);
}

fn ticks_to_nanos(ticks: u64, period_fs: u64) -> u64 {
((ticks as u128 * period_fs as u128) / 1_000_000) as u64
}

/// Now as a log line's time: nanoseconds since the counter's zero — power-on,
/// or the reset since — at the clock's rate, and before [`set_counter`] at the
/// rate the CPU states. `None` where it states none and the clock has not
Expand Down Expand Up @@ -176,18 +175,19 @@ 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);

/// Reads the RTC, which keeps UTC, once, after [`init`], and anchors the wall
/// clock to it.
pub fn init_wall(century_reg: Option<u8>) {
let civil = match crate::arch::rtc::read(century_reg) {
Ok(civil) => civil,
/// 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}");
return;
}
};

BOOT_SECS.store(civil.to_unix_secs().saturating_sub(nanos_since_boot() / NANOS_PER_SEC), Relaxed);
BOOT_SECS.store(anchor(civil.to_unix_secs(), at, TSC_BOOT.load(Relaxed), TSC_PERIOD_FS.load(Relaxed)), Relaxed);
WALL_KNOWN.store(true, Release);
log!("clock: the RTC reads {civil} UTC");
}
Expand Down
7 changes: 7 additions & 0 deletions tests/common/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,9 @@ pub struct BootOptions {
/// update's ROOT of this many bytes: a machine that updates itself.
/// `None` for every guest whose subject is not the update.
pub second_slot: Option<u64>,
/// The instant the machine's RTC starts at, as QEMU's `-rtc base=` takes
/// it, and the host's own UTC with `None`.
pub rtc_base: Option<&'static str>,
}

impl BootOptions {
Expand Down Expand Up @@ -1087,6 +1090,7 @@ impl Default for BootOptions {
extra_root_files: Vec::new(),
psci_trace: None,
second_slot: None,
rtc_base: None,
}
}
}
Expand Down Expand Up @@ -2216,6 +2220,9 @@ fn qemu_command(
(Arch::Aarch64, other) => panic!("`virt` has no `-vga {other}`"),
}
qemu.arg("-display").arg("none").arg("-no-reboot");
if let Some(base) = options.rtc_base {
qemu.arg("-rtc").arg(format!("base={base}"));
}
if let Some((w, h)) = shape.panel {
assert_eq!(arch, Arch::X86_64, "a panel is declared through VGA's EDID, and `virt` has no VGA");
// A panel on a machine with no VGA adapter is a declaration nothing
Expand Down
Loading
Loading