diff --git a/bootloader/src/arch/aarch64.rs b/bootloader/src/arch/aarch64.rs index dbb0b327022..a85bd2560b2 100644 --- a/bootloader/src/arch/aarch64.rs +++ b/bootloader/src/arch/aarch64.rs @@ -24,15 +24,6 @@ pub fn counter() -> u64 { count } -/// What the loader's report says beside the counter: its rate. Where it counts -/// from is firmware's to say and no register here does. -pub fn counter_origin() -> alloc::string::String { - let hz: u64; - // SAFETY: reads a register EL1 and EL2 may always read. - unsafe { core::arch::asm!("mrs {}, cntfrq_el0", out(reg) hz, options(nomem, nostack, preserves_flags)) }; - alloc::format!("CNTFRQ_EL0 {hz} Hz; the counter's origin is firmware's") -} - /// What the loader says about the CPU as firmware handed it over, or why the /// kernel cannot run on it: entered at EL2 on a CPU without FEAT_E2H0, /// `HCR_EL2.E2H` is RES1, so the kernel's entry cannot clear it and every diff --git a/bootloader/src/arch/x86_64.rs b/bootloader/src/arch/x86_64.rs index 6a34eea1420..ccb1ada9b72 100644 --- a/bootloader/src/arch/x86_64.rs +++ b/bootloader/src/arch/x86_64.rs @@ -20,24 +20,6 @@ pub fn counter() -> u64 { unsafe { core::arch::x86_64::_rdtsc() } } -/// What the loader's report says beside the counter: `IA32_TSC_ADJUST`, -/// where CPUID says the CPU has it — every write to the TSC since reset is -/// added to it (Intel SDM Vol. 3B, "Time-Stamp Counter Adjustment"), so zero -/// is a counter firmware never wrote and the TSC is time since power-on. -pub fn counter_origin() -> alloc::string::String { - let max = core::arch::x86_64::__cpuid(0).eax; - // Leaf 7 exists when the maximum leaf reaches it. - if max < 7 || core::arch::x86_64::__cpuid_count(7, 0).ebx & (1 << 1) == 0 { - return alloc::string::String::from("IA32_TSC_ADJUST not on this CPU"); - } - let (lo, hi): (u32, u32); - // SAFETY: the loader runs at CPL 0, and CPUID.07H:EBX[1] says the MSR exists. - unsafe { - core::arch::asm!("rdmsr", in("ecx") 0x3bu32, out("eax") lo, out("edx") hi, options(nomem, nostack)) - }; - alloc::format!("IA32_TSC_ADJUST {}", ((u64::from(hi) << 32) | u64::from(lo)) as i64) -} - /// `CLFLUSH`'s line on every x86-64 part. const LINE: u64 = 64; diff --git a/bootloader/src/attempt.rs b/bootloader/src/attempt.rs index d70560a2b42..5104349b588 100644 --- a/bootloader/src/attempt.rs +++ b/bootloader/src/attempt.rs @@ -2,31 +2,8 @@ //! back, and which slot's image it handed it to, kept on the stick it boots //! from. //! -//! **The bound on a hang, and the machine has no other way out of one.** -//! `bootnext::point_at_us` aims `BootNext` at this loader before every kernel -//! handoff, so a kernel that hangs and an owner who cuts power get: firmware, -//! this loader, the same kernel, the same hang — for ever. The black box cannot -//! break it, because a power cut is exactly what empties the black box: the next -//! pass finds nothing to report, arms a fresh record and boots the same kernel -//! again. The only ways out of that are the firmware's boot menu and pulling the -//! stick, and neither of those is the loop. -//! -//! What survives a power cut is the stick. So the count is a file on the log -//! partition, beside `loader.log`: **one flash writes a fresh log partition, so -//! the count is per image and per flash without anything having to say so.** It -//! carries the partition's own signature anyway, because a file that says what -//! it counts for is one a reader can be handed on its own. -//! //! One hand per hang, never two: the second attempt of an image whose first -//! never reported boots no kernel at all. **The same file is the slots' -//! record** (`toyos_update::record`): the image a pass handed the machine to, -//! and every image that died on a boot of its own, so the pass after a hang or -//! a death boots the other slot rather than the one that died. -//! -//! Written twice a pass: before the log opens, with the count and whatever the -//! last boot's end taught, so a pass that dies before its handoff has still -//! counted; and after the slot is chosen, through the log's own open volume, -//! with the image it chose. +//! never reported boots no kernel at all. use alloc::string::String; use toyos_update::record::{self, Record}; diff --git a/bootloader/src/blackbox.rs b/bootloader/src/blackbox.rs index 85a25013d35..0f749810338 100644 --- a/bootloader/src/blackbox.rs +++ b/bootloader/src/blackbox.rs @@ -143,17 +143,7 @@ pub fn harvest( return (None, None); }; // **A record belongs to the stick that wrote it, and this is not always that - // stick.** The page is DRAM at a fixed address and nothing between two - // operating systems clears it: on the T14 a `DONE` record outlived two hours - // of Ubuntu, and the pass after it — booting a *different* image — read that - // record, took itself for its reporting pass, and handed the machine back - // without booting a kernel at all. No stamp could have caught it: the record - // was written before this boot, which is exactly what a real predecessor's - // is. - // - // Cleared rather than reported, and then this pass goes on to boot its - // kernel. A record this stick did not write is one nothing here can report - // truthfully, and leaving it would hand the same trap to the boot after. + // stick.** if was != identity { toyos_blackbox::clear(page); flush(at); diff --git a/bootloader/src/floor.rs b/bootloader/src/floor.rs index b0210e5615a..e29e4f06a9c 100644 --- a/bootloader/src/floor.rs +++ b/bootloader/src/floor.rs @@ -8,9 +8,6 @@ //! `EFI_VARIABLE_RUNTIME_ACCESS` it is neither readable nor writable once //! `ExitBootServices` has run), so no kernel this loader hands the machine to //! — and nothing it lets write the disk — can lower it. -//! -//! What it cannot defend is `toyos_update::policy`'s to say: anything booted -//! before this loader, and the firmware's own reset of its variables. use alloc::string::String; use alloc::vec::Vec; diff --git a/bootloader/src/loaderlog.rs b/bootloader/src/loaderlog.rs index feec501f273..b8185644743 100644 --- a/bootloader/src/loaderlog.rs +++ b/bootloader/src/loaderlog.rs @@ -32,12 +32,6 @@ pub const GOP_AT: &str = "GOP: mode"; const ENDS_AT: &str = "Loader log: the kernel handoff begins, so this file ends here"; /// The last line of a pass that reads the black box and boots no kernel. -/// -/// It says the reset and not a return, because the reset is what it does: -/// returning is what leaves this image's exit-boot-services callback registered -/// for the next operating system to call into, and `end_this_pass` exists to -/// not do it. A line describing the shape this code was written to avoid is a -/// line that will be read as evidence one day. pub const ENDS_AT_CHAIN: &str = "Loader log: the last boot is accounted for, so this pass resets the machine"; @@ -78,11 +72,6 @@ static VOLUME: Volume = Volume(UnsafeCell::new(None)); /// reported on and the pass reporting on it are never read as one. pub const SEPARATOR: &str = "--- the pass after the reset, reading what the boot above left"; -/// Open `loader.log` on the partition `guid` names. -/// -/// `truncate` replaces what the last boot left; a pass that appends has a -/// *report about* that boot, and the boot's own account has to stay readable -/// under it. One file for now: per-pass names are their own change. /// The handle of the filesystem on the partition `guid` names, or why this /// machine has none. /// diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index cfdc293def4..f1abaa423c6 100644 --- a/bootloader/src/main.rs +++ b/bootloader/src/main.rs @@ -51,16 +51,6 @@ mod slot; mod watchdog; /// The largest file the bootloader will read off the ESP. -/// -/// Nothing here has a caller to return an error to and nothing has run that -/// could recover, so every check in this file ends in a named panic rather -/// than an error path. This one exists so that a corrupt or hostile directory -/// entry is a refusal that says what it refused, instead of a firmware pool -/// request sized by whatever the ESP claimed. -/// -/// Policy, and generous: `kernel.elf` is the largest file ToyOS puts on the -/// ESP, and this bound is orders of magnitude above it while still far below -/// what a UEFI implementation would serve in one allocation. const MAX_ESP_FILE: u64 = 1024 * 1024 * 1024; /// Descriptors of room held above what the map measured, for the descriptors @@ -182,8 +172,7 @@ struct BootPartition { /// `None` is a machine, not a failure: PXE, an unpartitioned device, and a /// signature type firmware chose not to fill in all land here, and the kernel /// is expected to boot on all of them knowing it has no partition of its own. -/// Every early-return below is one of those, so none of them panics — which -/// makes this the one function in this file that does not. +/// Every early-return below is one of those, so none of them panics. fn boot_partition(handle: Handle, system_table: &SystemTable) -> Option { let bs = system_table.boot_services(); let image = bs.open_protocol_exclusive::(handle).ok()?; @@ -239,53 +228,6 @@ fn log_partition_guid(handle: Handle, system_table: &SystemTable) -> [u8; }) } -/// What firmware says the machine's time zone is, in minutes to add to the -/// CMOS RTC's own reading to get UTC. -/// -/// Asked here because `GetTime` is a runtime service and the kernel never maps -/// the runtime, and asked at all because the RTC's registers carry no zone: the -/// same registers read 14:00 on a machine that keeps UTC and on one two hours -/// east of it that keeps local time, and only firmware can tell those apart. -/// `EFI_TIME::TimeZone` is the field, and its spec relation is -/// `Localtime = UTC - TimeZone`. -/// -/// `None` is a machine and not a failure — the same as [`boot_partition`] — so -/// this does not panic where the rest of this file does. Firmware that declines -/// to say (`EFI_UNSPECIFIED_TIMEZONE`, which is what OVMF ships) and firmware -/// that cannot be asked are one answer to the kernel: it treats the RTC as UTC -/// and logs that it is doing so. -/// -/// The range check is on untrusted input in the strict sense — the field is -/// whatever a vendor's NVRAM holds — and out of range is refused rather than -/// clamped, because an offset that is not a zone is not evidence about which -/// zone the machine is in. -fn rtc_utc_offset(system_table: &SystemTable) -> Option { - /// The field's own bounds, from the UEFI spec: a day either side of UTC. - const MAX_OFFSET_MINUTES: i32 = 1440; - - let time = match system_table.runtime_services().get_time() { - Ok(time) => time, - Err(e) => { - println!("RTC zone: firmware's GetTime failed ({e:?}), so the kernel assumes UTC"); - return None; - } - }; - let Some(zone) = time.time_zone() else { - println!("RTC zone: firmware names none ({time:?}), so the kernel assumes UTC"); - return None; - }; - let zone = zone as i32; - if !(-MAX_OFFSET_MINUTES..=MAX_OFFSET_MINUTES).contains(&zone) { - println!( - "RTC zone: firmware names {zone} minutes, outside +/-{MAX_OFFSET_MINUTES}, so it is \ - ignored and the kernel assumes UTC" - ); - return None; - } - println!("RTC zone: {zone} minutes to add to the RTC for UTC ({time:?})"); - Some(zone) -} - /// [`toyos_tco::FIRMWARE_BOUND_MS`] in the seconds `set_watchdog_timer` takes. const FIRMWARE_WATCHDOG_SECS: usize = (toyos_tco::FIRMWARE_BOUND_MS / 1_000) as usize; @@ -323,19 +265,6 @@ fn file_range(bytes: &[u8], offset: u64, len: u64) -> Option<&[u8]> { } fn load_kernel_elf(kernel_elf_bytes: &[u8]) -> LoadedKernel { - // `toyos-elf` is the tree's one ELF decoder: the crate the kernel reads - // every program image with reads the kernel's own image here. Refused by - // name before anything is allocated — ELF32, big-endian, a version that is - // not `EV_CURRENT`, an `e_type` that is not `ET_DYN`, a machine that is not - // this loader's own, no program headers or a table outside the file, more than - // `toyos_elf::MAX_LOAD_SEGMENTS` `PT_LOAD`s or none at all, a `PT_LOAD` - // with `p_filesz > p_memsz` or a `p_vaddr + p_memsz` or `p_offset + - // p_filesz` that overflows, and an `e_entry` no segment covers. - // - // `p_filesz <= p_memsz` matters for the same reason it does in the kernel's - // loader: the pair is a (copy length, destination size) pair here too, as - // the image is sized from every `p_memsz` and each segment is then copied - // in at `p_filesz`. let layout = toyos_elf::Layout::parse(kernel_elf_bytes, arch::ELF_MACHINE) .unwrap_or_else(|e| panic!("kernel.elf: {e}")); @@ -590,7 +519,7 @@ fn tsc() -> u64 { } #[allow(clippy::too_many_arguments)] -fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: vec::Vec, rsdp_addr: u64, gop: Option, boot_part: Option, log_partition_guid: [u8; 16], rtc_utc_offset: Option, root_image: Option, entry_tsc: u64, system_table: SystemTable) -> ! { +fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: vec::Vec, rsdp_addr: u64, gop: Option, boot_part: Option, log_partition_guid: [u8; 16], layout: u32, root_image: Option, entry_tsc: u64, system_table: SystemTable) -> ! { // Said before it is refused, for `report_reach`'s reason. match arch::cpu_as_entered() { Ok(None) => {} @@ -620,10 +549,6 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v let pt_mem = unsafe { alloc::alloc::alloc_zeroed(pt_layout) }; assert!(!pt_mem.is_null(), "page table allocation failed"); - // Before the exit: `_print` unwraps a system table uefi-services nulls in its exit callback, so `println!` past it panics. - // - // Said before it is applied: a machine this refuses leaves the refusal in - // `loader.log`, which is the artifact a machine with no console has. // Where firmware loaded this image, which is where the x86-64 switch to // the boot map runs from: the map holds it wherever that is. let loader = { @@ -716,8 +641,7 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v boot_partition_guid, boot_partition_present, log_partition_guid, - rtc_utc_offset_minutes: rtc_utc_offset.unwrap_or(0), - rtc_utc_offset_known: rtc_utc_offset.is_some() as u32, + layout, cmdline_addr: cmdline.as_ptr() as u64, cmdline_len: cmdline.len() as u64, root_bridge_window_count, @@ -737,9 +661,8 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v kernel_args.loader_handoff_tsc = tsc(); println!( - "Loader TSC: {entry_tsc} at entry, {} at the handoff; {}", + "Loader TSC: {entry_tsc} at entry, {} at the handoff", kernel_args.loader_handoff_tsc, - arch::counter_origin(), ); // Last, and after every line above: a console write, a FAT write and a @@ -825,14 +748,6 @@ fn armed_at(system_table: &SystemTable) -> u64 { /// `SIGNAL_EXIT_BOOT_SERVICES` callback that lives here; the next operating /// system signals that group from inside its own `ExitBootServices`, and /// firmware calls into memory that is no longer ours. -/// -/// So the event is closed *and* the pass resets. Closing it is the invariant — -/// a pass that does not hand off leaves nothing registered in the firmware — and -/// the reset is what makes that invariant not have to be complete: the next -/// operating system comes up on firmware this image has never run on, for one -/// reboot. `BootNext` was consumed by this pass and this pass sets none, so the -/// firmware's own order takes the machine, and the page was cleared as it was -/// read, so a boot that does come back here boots normally. fn end_this_pass(system_table: &SystemTable, exit_event: Option) -> ! { println!("{}", loaderlog::ENDS_AT_CHAIN); loaderlog::close_without_a_kernel(); @@ -849,10 +764,6 @@ fn end_this_pass(system_table: &SystemTable, exit_event: Option) -> fn main(handle: Handle, mut system_table: SystemTable) -> Status { // First: the TSC counts from reset, so this is what firmware took. let entry_tsc = tsc(); - // The event is kept, not discarded: it is a callback *inside this image* - // that firmware holds until it is closed, and a pass that returns to the - // boot manager is a pass whose image the boot manager then unloads. See - // `end_this_pass`. let exit_event = uefi_services::init(&mut system_table).unwrap(); // First, because it covers everything below it: firmware starts a // five-minute countdown when it loads an image and resets the machine if @@ -1045,6 +956,13 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { .unwrap_or_else(|e| panic!("slot {}'s cmdline is not UTF-8: {e}", chosen.which.letter())); println!("Boot parameter: {params:?}"); + let layout = if toyos_abi::boot::actuators(params).any(|token| token == toyos_abi::boot::WRITE_NO_LAYOUT_PARAM) { + println!("Kernel arguments: layout 0 on {}", toyos_abi::boot::WRITE_NO_LAYOUT_PARAM); + 0 + } else { + toyos_abi::boot::LAYOUT + }; + let root_image = if toyos_abi::boot::actuators(params).any(|token| token == toyos_abi::boot::WITHHOLD_ROOT_PARAM) { println!("ROOT: withheld on {}; the kernel is handed no image", toyos_abi::boot::WITHHOLD_ROOT_PARAM); chosen.root.free(system_table.boot_services()); @@ -1059,10 +977,6 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { // Query UEFI GOP before exiting boot services let gop = query_gop(&system_table); - // Last of the firmware questions and for the same reason as the GOP: both - // answers die with Boot Services. - let rtc_offset = rtc_utc_offset(&system_table); - // 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); @@ -1074,5 +988,5 @@ fn main(handle: Handle, mut system_table: SystemTable) -> 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, rtc_offset, root_image, entry_tsc, system_table); + start_kernel(loaded_kernel, kernel_bytes, cmdline, rsdp_addr, gop, boot_part, log_guid, layout, root_image, entry_tsc, system_table); } diff --git a/bootloader/src/rootbridge.rs b/bootloader/src/rootbridge.rs index a3b7da563db..1efe118b190 100644 --- a/bootloader/src/rootbridge.rs +++ b/bootloader/src/rootbridge.rs @@ -15,9 +15,6 @@ use core::ffi::c_void; use core::ptr::read_volatile; -use alloc::string::String; -use core::fmt::Write; - use toyos_abi::boot::RootBridgeWindow; use toyos_acpi::{memory_windows, Phys, MAX_LIST_BYTES}; use uefi::prelude::*; @@ -132,23 +129,7 @@ pub fn windows(system_table: &SystemTable, out: &mut [RootBridgeWindow]) - } let list = List { at: resources as u64 }; - let walk = memory_windows(list, list.at, &mut out[found..]); - - // `readable` again here rather than resting on the walk's: `Phys`'s - // contract is that a byte is asked for only where a `readable` in the - // same reach accepted it, and a reader that argues its bound across two - // functions is one an edit to either can break silently. - let mut hex = String::with_capacity(walk.bytes * 2); - for i in 0..walk.bytes { - let at = list.at + i as u64; - if !list.readable(at, 1) { - break; - } - let _ = write!(hex, "{:02x}", list.byte(at)); - } - println!("{HEAD} {index} (segment {}) {} bytes: {hex}", bridge.segment_number, walk.bytes); - - match walk.windows { + match memory_windows(list, list.at, &mut out[found..]) { Ok(count) => found += count, Err(why) => { println!("{HEAD} {index} (segment {}) {why}, so the kernel is handed no window", bridge.segment_number); diff --git a/bootloader/src/slot.rs b/bootloader/src/slot.rs index 9d04623bf9f..424bcfe8764 100644 --- a/bootloader/src/slot.rs +++ b/bootloader/src/slot.rs @@ -191,9 +191,7 @@ fn signed_header(bs: &BootServices, which: Which, slot: &Slot) -> Result<(Header /// The version the image the record says the last boot proved carries, read /// out of its slot's signed header, verified in this pass; or why no version -/// is: **the record is on a partition the running system writes**, so its -/// word is only which slot to read and the digest that slot's header must -/// hash to. +/// is. pub fn proven(handle: Handle, system_table: &SystemTable, booted: &Booted) -> Result { let bs = system_table.boot_services(); let letter = booted.slot.letter(); diff --git a/issues/filesystem/where-everything-lives.md b/issues/filesystem/where-everything-lives.md index 8ec1c6c1988..2695ecbc044 100644 --- a/issues/filesystem/where-everything-lives.md +++ b/issues/filesystem/where-everything-lives.md @@ -83,10 +83,8 @@ until that lands nothing verifies it. login row, and `toy` stops being a constant in `toyos-manifest`. **Exit**: init names no user. 4. **The time zone.** One file in `/config` names the machine's zone, one - program writes it, and local time is read through it rather than recovered - by subtracting `SYS_CLOCK_REALTIME` from `SYS_CLOCK_EPOCH`. **Exit**: a - guest test sets the zone and reads its local time back, and nothing - recovers the zone by subtraction. + program writes it, and local time is read through it. **Exit**: a guest + test sets the zone and reads its local time back. 5. **The language.** One file in `/config` names the machine's default language, one program writes it, and init sets it on every program it starts. **Exit**: a guest test sets the language and a program init starts diff --git a/issues/kernel/sys-clock-realtime-is-now-a-format-of-sys-clock-epoch.md b/issues/kernel/sys-clock-realtime-is-now-a-format-of-sys-clock-epoch.md new file mode 100644 index 00000000000..29c87694a98 --- /dev/null +++ b/issues/kernel/sys-clock-realtime-is-now-a-format-of-sys-clock-epoch.md @@ -0,0 +1,20 @@ +--- +status: open +kind: defect +opened: 2026-09-28 +--- + +# `SYS_CLOCK_REALTIME` is now a format of `SYS_CLOCK_EPOCH`, and retiring it is an ABI discussion + +Since the RTC-keeps-UTC ruling, `SYS_CLOCK_REALTIME`'s handler and +`SYS_CLOCK_EPOCH`'s both read `crate::clock::utc_secs()`; the first packs it +into `h:m:s` (`toyos_abi::syscall::clock_realtime`), which every caller today +could derive from `clock_epoch()` instead, with no information +`SYS_CLOCK_REALTIME` carries that `SYS_CLOCK_EPOCH` does not. + +Exit condition: `SYS_CLOCK_REALTIME`'s number (42) is retired and never +reused, its kernel handler and `toyos_abi::syscall::clock_realtime` are +deleted, `toyos::system::clock_realtime` and both its callers +(`userland/compositor/src/render.rs`, +`tests/toyos-rust-tests/src/bin/wall_clock_now.rs`) move to `clock_epoch`, all +in a PR of their own, and this file is deleted in that PR's merge. diff --git a/kernel/src/actuator.rs b/kernel/src/actuator.rs index cd2f2cd8bae..c2f9f63ed2b 100644 --- a/kernel/src/actuator.rs +++ b/kernel/src/actuator.rs @@ -43,6 +43,11 @@ actuators! { /// refuse by name; read by the loader as [`toyos_abi::boot::WITHHOLD_ROOT_PARAM`]. loader_withholds_root = "loader-withholds-root"; + /// The loader writes 0 as the `KernelArgs` layout word, which `kernel_main` + /// has to refuse by name; read by the loader as + /// [`toyos_abi::boot::WRITE_NO_LAYOUT_PARAM`]. + loader_writes_no_layout = "loader-writes-no-layout"; + /// Panic between arming the on-screen console and `mm::init`. test_early_panic = "test-early-panic"; @@ -497,9 +502,6 @@ actuators! { /// Make the century register read `0x21`. rtc_century_next = "rtc-century-next"; - /// Make firmware name its own timezone. - rtc_zone_east = "rtc-zone-east"; - /// Run the leak-rollback controls (device mint, FAT reopen) after mount. leak_rollback_selftest = "leak-rollback-selftest"; @@ -691,3 +693,5 @@ const _: () = { // spells it as a literal; the two are one name or the build fails. #[cfg(feature = "boot-actuators")] const _: () = assert!(str_eq("loader-withholds-root", toyos_abi::boot::WITHHOLD_ROOT_PARAM)); +#[cfg(feature = "boot-actuators")] +const _: () = assert!(str_eq("loader-writes-no-layout", toyos_abi::boot::WRITE_NO_LAYOUT_PARAM)); diff --git a/kernel/src/arch/aarch64/boot.rs b/kernel/src/arch/aarch64/boot.rs index c3fd3144c8d..4e974a930fc 100644 --- a/kernel/src/arch/aarch64/boot.rs +++ b/kernel/src/arch/aarch64/boot.rs @@ -253,6 +253,10 @@ pub fn clock(_args: &KernelArgs) { owed!("the clock", "stage 4") } +/// Nothing: where the generic timer counts from is firmware's, and no +/// register says it. +pub fn report_counter_origin() {} + /// The per-CPU timer. pub fn timer() { owed!("the timer", "stage 4") diff --git a/kernel/src/arch/x86_64/boot.rs b/kernel/src/arch/x86_64/boot.rs index b2b3cf98401..bbffc71d8a6 100644 --- a/kernel/src/arch/x86_64/boot.rs +++ b/kernel/src/arch/x86_64/boot.rs @@ -102,7 +102,6 @@ pub fn clock(args: &KernelArgs) { let hpet_base = acpi::find_hpet_base(args.rsdp_addr) .expect("ACPI: HPET not found"); super::hpet::calibrate_counter(hpet_base); - // Century register and time zone both come from ACPI/firmware, not the RTC's own registers. let century_reg = match acpi::rtc_century_register(args.rsdp_addr) { Ok(reg) => reg, Err(e) => { @@ -110,7 +109,21 @@ pub fn clock(args: &KernelArgs) { None } }; - crate::clock::init_wall(century_reg, args.rtc_utc_offset()); + crate::clock::init_wall(century_reg); +} + +/// Where the TSC counts from: `IA32_TSC_ADJUST`, where CPUID says the CPU has +/// it. Every write to the TSC since reset is added to it (Intel SDM Vol. 3B, +/// "Time-Stamp Counter Adjustment"), so zero is a counter firmware never wrote +/// and the TSC is time since power-on. +pub fn report_counter_origin() { + const IA32_TSC_ADJUST: u32 = 0x3b; + // Leaf 7 exists when the maximum leaf reaches it. + if super::cpu::cpuid(0, 0).0 < 7 || super::cpu::cpuid(7, 0).1 & (1 << 1) == 0 { + log!("boot: IA32_TSC_ADJUST not on this CPU"); + return; + } + log!("boot: IA32_TSC_ADJUST {}", super::cpu::rdmsr(IA32_TSC_ADJUST) as i64); } /// The per-CPU timer, once the clock converts its bound. diff --git a/kernel/src/clock.rs b/kernel/src/clock.rs index 439b8c83a4d..b02d7940274 100644 --- a/kernel/src/clock.rs +++ b/kernel/src/clock.rs @@ -5,7 +5,7 @@ //! second — in [`init_wall`], and answered after as that reading plus //! [`nanos_since_boot`]. -use core::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering::{Acquire, Relaxed, Release}}; +use core::sync::atomic::{AtomicBool, AtomicU64, Ordering::{Acquire, Relaxed, Release}}; use crate::arch::cpu; use crate::time::Instant; @@ -133,20 +133,14 @@ pub fn settles(nanos: u64, ready: impl Fn() -> bool) -> bool { true } -/// Unix seconds, in the machine's own zone, at `nanos_since_boot() == 0`. -static BOOT_LOCAL_SECS: AtomicU64 = AtomicU64::new(0); -/// Seconds to add to the machine's zone to get UTC (`Localtime = UTC - TimeZone`). -static UTC_OFFSET_SECS: AtomicI64 = AtomicI64::new(0); -/// Whether the two above mean anything; zero is a valid instant and offset, not a sentinel. +/// Unix seconds at `nanos_since_boot() == 0`. +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 once, after [`init`], and anchors the wall clock to it. -pub fn init_wall(century_reg: Option, utc_offset_minutes: Option) { - // OVMF never names a zone, so `rtc_zone_east` is a test actuator forcing - // UTC+2 (`Localtime = UTC - TimeZone`, so east is negative: -120). - let utc_offset_minutes = - if crate::actuator::rtc_zone_east() { Some(-120) } else { utc_offset_minutes }; - +/// Reads the RTC, which keeps UTC, once, after [`init`], and anchors the wall +/// clock to it. +pub fn init_wall(century_reg: Option) { let civil = match crate::arch::rtc::read(century_reg) { Ok(civil) => civil, Err(fault) => { @@ -155,29 +149,15 @@ pub fn init_wall(century_reg: Option, utc_offset_minutes: Option) { } }; - let local = civil.to_unix_secs(); - let offset_secs = utc_offset_minutes.unwrap_or(0) as i64 * 60; - BOOT_LOCAL_SECS.store(local.saturating_sub(nanos_since_boot() / 1_000_000_000), Relaxed); - UTC_OFFSET_SECS.store(offset_secs, Relaxed); + BOOT_SECS.store(civil.to_unix_secs().saturating_sub(nanos_since_boot() / 1_000_000_000), Relaxed); WALL_KNOWN.store(true, Release); - - match utc_offset_minutes { - Some(minutes) => log!("clock: the RTC reads {civil}, {minutes} minutes from UTC by firmware"), - None => log!("clock: the RTC reads {civil}; firmware named no zone, so it is taken as UTC"), - } + log!("clock: the RTC reads {civil} UTC"); } -/// Local wall-clock time — what FAT stamps use, since FAT stores local time -/// by specification. `None` if the RTC never answered. -pub fn local_secs() -> Option { +/// Unix seconds, now. `None` if the RTC never answered. +pub fn utc_secs() -> Option { WALL_KNOWN .load(Acquire) - .then(|| BOOT_LOCAL_SECS.load(Relaxed) + nanos_since_boot() / 1_000_000_000) -} - -/// The same instant in Unix seconds (UTC) — what `SYS_CLOCK_EPOCH` serves. -pub fn utc_secs() -> Option { - let local = local_secs()?; - Some(local.saturating_add_signed(UTC_OFFSET_SECS.load(Relaxed))) + .then(|| BOOT_SECS.load(Relaxed) + nanos_since_boot() / 1_000_000_000) } diff --git a/kernel/src/fat32_adapter.rs b/kernel/src/fat32_adapter.rs index 6ad2b748d51..4309b398e18 100644 --- a/kernel/src/fat32_adapter.rs +++ b/kernel/src/fat32_adapter.rs @@ -653,10 +653,11 @@ pub struct FatFs { repair_named: RepairNotice, } -/// What to stamp on an entry: reads `clock` directly, in local time as FAT -/// requires — the VFS's `mtime` is nanoseconds since boot, not a time of day. +/// What to stamp on an entry: reads `clock` directly — the VFS's `mtime` is +/// nanoseconds since boot, not a time of day. FAT specifies local time; this +/// stamps UTC because the owner ruled the hardware clock is UTC. fn now() -> FatTime { - crate::clock::local_secs().map_or(FatTime::EPOCH, FatTime::from_unix_secs) + crate::clock::utc_secs().map_or(FatTime::EPOCH, FatTime::from_unix_secs) } /// What one of `toyos-fat32`'s errors means to the [`FileSystem`] caller; diff --git a/kernel/src/main.rs b/kernel/src/main.rs index ef64ca60a95..c8363ae12d5 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -190,9 +190,9 @@ const DATA_PATHS: [&str; 4] = ["apps", "config", "home", "state"]; /// The boot from power-on, off the loader's TSC readings and `complete`'s, at /// the calibrated rate. The TSC counts from reset, so the first span is -/// firmware's unless firmware wrote the counter, which the loader's -/// `IA32_TSC_ADJUST` says where the CPU has one. +/// firmware's unless firmware wrote the counter. fn report_power_on(args: &KernelArgs, complete: u64) { + arch::boot::report_counter_origin(); let (entry, handoff) = (args.loader_entry_tsc, args.loader_handoff_tsc); if handoff < entry || complete < handoff { log!( @@ -248,6 +248,16 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { // Before serial::init: the screen may be the only surviving channel if serial::init itself faults. drivers::panic_console::arm(&kernel_args, maps); + // Before the first field a layout change can move; `rsdp_addr` sits below + // the word, so the refusal reaches the UART. + if kernel_args.layout != toyos_abi::boot::LAYOUT { + serial::init(kernel_args.rsdp_addr); + panic!( + "boot: the loader wrote KernelArgs layout {:#x} and this kernel reads layout {:#x}", + kernel_args.layout, + toyos_abi::boot::LAYOUT + ); + } // Beside it, and out of the raw buffer: a panic between here and // `params::init` — inside `serial::init`, or on the parameter line's own // UTF-8 check — is one the page has to carry past the reset, and neither @@ -279,6 +289,13 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { params::init(cmdline); deadline::claim(cmdline); actuator::init(cmdline); + // The actuator's other half: a loader that ignored it would boot on unrefused. + if actuator::loader_writes_no_layout() { + panic!( + "boot: {} is armed and the loader wrote this kernel's layout anyway", + toyos_abi::boot::WRITE_NO_LAYOUT_PARAM + ); + } let root_image = rootfs::init(cmdline, &kernel_args, maps); // Armed here so the next record — the architecture's first — reaches the console and the panel keeps the one before it. @@ -327,8 +344,7 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { ); log!("boot: log partition guid {:02x?}", kernel_args.log_partition_guid); log!( - "boot: rtc utc offset {} minutes (known={}), cmdline {:#x}+{}", - kernel_args.rtc_utc_offset_minutes, kernel_args.rtc_utc_offset_known, + "boot: cmdline {:#x}+{}", kernel_args.cmdline_addr, kernel_args.cmdline_len ); // Before `mm::init`, which may hand the parameter's memory out. This record diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index e0f8a33e842..01f126e8296 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -268,8 +268,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> SYS_THREAD_JOIN => sys_thread_join(a1), // Both answer from the boot-time anchor, never the CMOS: NotSupported beats a // 1970-epoch number a caller cannot tell from real time. - // Local time here, UTC in SYS_CLOCK_EPOCH: seconds-since-epoch are UTC by definition. - SYS_CLOCK_REALTIME => crate::clock::local_secs().map_or( + SYS_CLOCK_REALTIME => crate::clock::utc_secs().map_or( SyscallError::NotSupported.to_u64(), |secs| { let now = toyos_wallclock::Civil::from_unix_secs(secs); diff --git a/tests/common/fwvars.rs b/tests/common/fwvars.rs new file mode 100644 index 00000000000..0dc3578b578 --- /dev/null +++ b/tests/common/fwvars.rs @@ -0,0 +1,101 @@ +//! The firmware's variable store, as OVMF keeps it in its `VARS` file: a +//! firmware volume holding an authenticated variable store, read and written +//! by the layout EDK2 declares for it (`MdeModulePkg/Include/Guid/ +//! VariableFormat.h`) and not by anything the loader shares. + +use std::path::Path; + +/// `EFI_FIRMWARE_VOLUME_HEADER`: its signature and its header's length. +const FV_SIGNATURE: (usize, &[u8]) = (0x28, b"_FVH"); +const FV_HEADER_LEN_AT: usize = 0x30; +/// `VARIABLE_STORE_HEADER`: signature GUID, size, format, state, reserved. +const STORE_HEADER: usize = 16 + 4 + 1 + 1 + 2 + 4; +/// `AUTHENTICATED_VARIABLE_HEADER`: start id, state, reserved, attributes, +/// monotonic count, time stamp, public key index, name size, data size, +/// vendor GUID. +const HEADER: usize = 2 + 1 + 1 + 4 + 8 + 16 + 4 + 4 + 4 + 16; +const START_ID: u16 = 0x55AA; +const VAR_ADDED: u8 = 0x3F; +/// `VAR_ADDED & VAR_IN_DELETED_TRANSITION`: still the variable until the +/// copy replacing it is added. +const IN_TRANSITION: u8 = 0x3E; + +pub struct Var { + pub name: String, + pub data: Vec, +} + +/// Where the variables begin and where the store ends. +fn store(bytes: &[u8]) -> Result<(usize, usize), String> { + let (at, sig) = FV_SIGNATURE; + if bytes.get(at..at + sig.len()) != Some(sig) { + return Err("the variable file is no firmware volume".into()); + } + let header = u16::from_le_bytes([bytes[FV_HEADER_LEN_AT], bytes[FV_HEADER_LEN_AT + 1]]) as usize; + let size = u32::from_le_bytes(bytes[header + 16..header + 20].try_into().expect("four bytes")) as usize; + Ok((header + STORE_HEADER, header + size)) +} + +/// One variable header in the store. +struct Found { + state: u8, + vendor: [u8; 16], + var: Var, +} + +/// Every variable header in the store, and where the erased space after +/// them begins. +fn walk(bytes: &[u8]) -> Result<(Vec, usize), String> { + let (mut at, end) = store(bytes)?; + let mut out = Vec::new(); + while at + HEADER <= end && u16::from_le_bytes([bytes[at], bytes[at + 1]]) == START_ID { + let word = |off: usize| u32::from_le_bytes(bytes[at + off..at + off + 4].try_into().expect("four bytes")) as usize; + let (name_len, data_len) = (word(36), word(40)); + let vendor: [u8; 16] = bytes[at + 44..at + 60].try_into().expect("sixteen bytes"); + let name_at = at + HEADER; + let units: Vec = bytes[name_at..name_at + name_len] + .chunks(2) + .map(|c| u16::from_le_bytes([c[0], c[1]])) + .take_while(|&u| u != 0) + .collect(); + let data = bytes[name_at + name_len..name_at + name_len + data_len].to_vec(); + out.push(Found { state: bytes[at + 2], vendor, var: Var { name: String::from_utf16_lossy(&units), data } }); + at = (name_at + name_len + data_len).next_multiple_of(4); + } + Ok((out, at)) +} + +/// The live variables under `vendor`, a GUID in the byte order `EFI_GUID` +/// stores. +pub fn live(path: &Path, vendor: &[u8; 16]) -> Result, String> { + let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?; + Ok(walk(&bytes)? + .0 + .into_iter() + .filter(|found| found.vendor == *vendor && (found.state == VAR_ADDED || found.state == IN_TRANSITION)) + .map(|found| found.var) + .collect()) +} + +/// Add `name` under `vendor` with `attributes` and `data`, as the firmware +/// would have added it. +pub fn plant(path: &Path, vendor: &[u8; 16], name: &str, attributes: u32, data: &[u8]) -> Result<(), String> { + let mut bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?; + let (_, end) = store(&bytes)?; + let (_, at) = walk(&bytes)?; + let mut units: Vec = name.encode_utf16().chain([0]).flat_map(u16::to_le_bytes).collect(); + let mut var = vec![0u8; HEADER]; + var[..2].copy_from_slice(&START_ID.to_le_bytes()); + var[2] = VAR_ADDED; + var[4..8].copy_from_slice(&attributes.to_le_bytes()); + var[36..40].copy_from_slice(&(units.len() as u32).to_le_bytes()); + var[40..44].copy_from_slice(&(data.len() as u32).to_le_bytes()); + var[44..60].copy_from_slice(vendor); + var.append(&mut units); + var.extend_from_slice(data); + if at + var.len() > end || bytes[at..at + var.len()].iter().any(|&b| b != 0xFF) { + return Err(format!("no erased room for {name} at byte {at} of the variable store")); + } + bytes[at..at + var.len()].copy_from_slice(&var); + std::fs::write(path, bytes).map_err(|e| format!("{}: {e}", path.display())) +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 2958914faeb..c40f05778be 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -21,6 +21,7 @@ pub mod console; pub mod devices; #[allow(dead_code)] pub mod faults; +pub mod fwvars; #[allow(dead_code)] pub mod gpt; #[allow(dead_code)] diff --git a/tests/common/update.rs b/tests/common/update.rs index bb0c95e01f2..b4ff7f4a6b4 100644 --- a/tests/common/update.rs +++ b/tests/common/update.rs @@ -16,7 +16,7 @@ //! What the running system can write and the loader must not trust — the //! slots' record, the slot table — the host writes into the image between or //! beneath boots, and the variable store it reads and plants in by EDK2's own -//! layout ([`vars`]). +//! layout ([`fwvars`]). use std::path::{Path, PathBuf}; use std::time::Instant; @@ -29,6 +29,7 @@ use toyos_update::floor::{self as floors, Scope}; use toyos_update::record::{Booted, Record}; use toyos_update::slots::Which; +use super::fwvars; use super::qemu::{self, BootOptions, QemuInstance, Staged, DEFAULT_READY}; use super::ssh::{self, Identity, HOST}; @@ -607,8 +608,8 @@ pub fn update_floor_is_the_images_own(_: &Path, _: &[(String, Vec)], _: &[(S let fresh = || toyos_build::firmware::of(toyos_build::arch::Arch::X86_64)?.fresh_vars(&rig.vars); fresh()?; - vars::plant(&rig.vars, &owner, floors::ATTRIBUTES, &u64::MAX.to_le_bytes())?; - vars::plant(&rig.vars, &other, floors::ATTRIBUTES, &u64::MAX.to_le_bytes())?; + fwvars::plant(&rig.vars, &FLOOR_VENDOR, &owner, floors::ATTRIBUTES, &u64::MAX.to_le_bytes())?; + fwvars::plant(&rig.vars, &FLOOR_VENDOR, &other, floors::ATTRIBUTES, &u64::MAX.to_le_bytes())?; let (mut guest, mut console) = rig.boot()?; owed(&console, 0, &format!("{SLOT_RECORD} A, the one the slot table marks"))?; loader_said(&guest, 0, &format!("Anti-rollback floor: {other} is no floor this image loader keeps; deleted"))?; @@ -617,7 +618,7 @@ pub fn update_floor_is_the_images_own(_: &Path, _: &[(String, Vec)], _: &[(S loader_said(&guest, uart, &format!("Anti-rollback floor: {BASE}, raised from 0 by the boot that proved it"))?; drop(guest); - let stored = vars::live(&rig.vars)?; + let stored = fwvars::live(&rig.vars, &FLOOR_VENDOR)?; let value = |name: &str| stored.iter().filter(|v| v.name == name).map(|v| v.data.clone()).collect::>(); let want = [(&owner, vec![u64::MAX.to_le_bytes().to_vec()]), (&own, vec![BASE.to_le_bytes().to_vec()]), (&other, vec![])]; for (name, holds) in want { @@ -628,7 +629,7 @@ pub fn update_floor_is_the_images_own(_: &Path, _: &[(String, Vec)], _: &[(S eprintln!(" [update] the owner's floor and another image's held this one to nothing; the other's went, the owner's stayed"); fresh()?; - vars::plant(&rig.vars, &own, floors::ATTRIBUTES, &[1; 9])?; + fwvars::plant(&rig.vars, &FLOOR_VENDOR, &own, floors::ATTRIBUTES, &[1; 9])?; let refused = rig.launch(FLOOR_REFUSED); said(refused.boot_log(), &[&format!("Anti-rollback floor: {own}: it holds 9 bytes where this loader writes 8")])?; drop(refused); @@ -636,108 +637,6 @@ pub fn update_floor_is_the_images_own(_: &Path, _: &[(String, Vec)], _: &[(S Ok(()) } -/// The firmware's variable store, as OVMF keeps it in its `VARS` file: a -/// firmware volume holding an authenticated variable store, read and written -/// by the layout EDK2 declares for it (`MdeModulePkg/Include/Guid/ -/// VariableFormat.h`) and not by anything the loader shares — only the -/// floor's vendor GUID, which the store is asked for. -mod vars { - use std::path::Path; - - /// The floor's vendor, `33BE3D4A-30E6-49F5-8050-F169D93A20FB`, in the - /// byte order `EFI_GUID` stores. - const VENDOR: [u8; 16] = [0x4a, 0x3d, 0xbe, 0x33, 0xe6, 0x30, 0xf5, 0x49, 0x80, 0x50, 0xf1, 0x69, 0xd9, 0x3a, 0x20, 0xfb]; - /// `EFI_FIRMWARE_VOLUME_HEADER`: its signature and its header's length. - const FV_SIGNATURE: (usize, &[u8]) = (0x28, b"_FVH"); - const FV_HEADER_LEN_AT: usize = 0x30; - /// `VARIABLE_STORE_HEADER`: signature GUID, size, format, state, reserved. - const STORE_HEADER: usize = 16 + 4 + 1 + 1 + 2 + 4; - /// `AUTHENTICATED_VARIABLE_HEADER`: start id, state, reserved, attributes, - /// monotonic count, time stamp, public key index, name size, data size, - /// vendor GUID. - const HEADER: usize = 2 + 1 + 1 + 4 + 8 + 16 + 4 + 4 + 4 + 16; - const START_ID: u16 = 0x55AA; - const VAR_ADDED: u8 = 0x3F; - /// `VAR_ADDED & VAR_IN_DELETED_TRANSITION`: still the variable until the - /// copy replacing it is added. - const IN_TRANSITION: u8 = 0x3E; - - pub struct Var { - pub name: String, - pub data: Vec, - } - - /// Where the variables begin and where the store ends. - fn store(bytes: &[u8]) -> Result<(usize, usize), String> { - let (at, sig) = FV_SIGNATURE; - if bytes.get(at..at + sig.len()) != Some(sig) { - return Err("the variable file is no firmware volume".into()); - } - let header = u16::from_le_bytes([bytes[FV_HEADER_LEN_AT], bytes[FV_HEADER_LEN_AT + 1]]) as usize; - let size = u32::from_le_bytes(bytes[header + 16..header + 20].try_into().expect("four bytes")) as usize; - Ok((header + STORE_HEADER, header + size)) - } - - /// One variable header in the store. - struct Found { - state: u8, - vendor: [u8; 16], - var: Var, - } - - /// Every variable header in the store, and where the erased space after - /// them begins. - fn walk(bytes: &[u8]) -> Result<(Vec, usize), String> { - let (mut at, end) = store(bytes)?; - let mut out = Vec::new(); - while at + HEADER <= end && u16::from_le_bytes([bytes[at], bytes[at + 1]]) == START_ID { - let word = |off: usize| u32::from_le_bytes(bytes[at + off..at + off + 4].try_into().expect("four bytes")) as usize; - let (name_len, data_len) = (word(36), word(40)); - let vendor: [u8; 16] = bytes[at + 44..at + 60].try_into().expect("sixteen bytes"); - let name_at = at + HEADER; - let units: Vec = bytes[name_at..name_at + name_len] - .chunks(2) - .map(|c| u16::from_le_bytes([c[0], c[1]])) - .take_while(|&u| u != 0) - .collect(); - let data = bytes[name_at + name_len..name_at + name_len + data_len].to_vec(); - out.push(Found { state: bytes[at + 2], vendor, var: Var { name: String::from_utf16_lossy(&units), data } }); - at = (name_at + name_len + data_len).next_multiple_of(4); - } - Ok((out, at)) - } - - /// The live variables under the floor's vendor. - pub fn live(path: &Path) -> Result, String> { - let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?; - Ok(walk(&bytes)? - .0 - .into_iter() - .filter(|found| found.vendor == VENDOR && (found.state == VAR_ADDED || found.state == IN_TRANSITION)) - .map(|found| found.var) - .collect()) - } - - /// Add `name` under the floor's vendor with `attributes` and `data`, as - /// the firmware would have added it. - pub fn plant(path: &Path, name: &str, attributes: u32, data: &[u8]) -> Result<(), String> { - let mut bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?; - let (_, end) = store(&bytes)?; - let (_, at) = walk(&bytes)?; - let mut units: Vec = name.encode_utf16().chain([0]).flat_map(u16::to_le_bytes).collect(); - let mut var = vec![0u8; HEADER]; - var[..2].copy_from_slice(&START_ID.to_le_bytes()); - var[2] = VAR_ADDED; - var[4..8].copy_from_slice(&attributes.to_le_bytes()); - var[36..40].copy_from_slice(&(units.len() as u32).to_le_bytes()); - var[40..44].copy_from_slice(&(data.len() as u32).to_le_bytes()); - var[44..60].copy_from_slice(&VENDOR); - var.append(&mut units); - var.extend_from_slice(data); - if at + var.len() > end || bytes[at..at + var.len()].iter().any(|&b| b != 0xFF) { - return Err(format!("no erased room for {name} at byte {at} of the variable store")); - } - bytes[at..at + var.len()].copy_from_slice(&var); - std::fs::write(path, bytes).map_err(|e| format!("{}: {e}", path.display())) - } -} +/// The floor's vendor, `33BE3D4A-30E6-49F5-8050-F169D93A20FB`, in the byte +/// order `EFI_GUID` stores. +const FLOOR_VENDOR: [u8; 16] = [0x4a, 0x3d, 0xbe, 0x33, 0xe6, 0x30, 0xf5, 0x49, 0x80, 0x50, 0xf1, 0x69, 0xd9, 0x3a, 0x20, 0xfb]; diff --git a/tests/common/wallclock.rs b/tests/common/wallclock.rs index d04bb3416ea..7046dfc9223 100644 --- a/tests/common/wallclock.rs +++ b/tests/common/wallclock.rs @@ -25,9 +25,10 @@ //! and everything downstream of it shipped code. use std::io::Write; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::time::Duration; +use super::fwvars; use super::qemu::{self, BootOptions, QemuInstance}; use super::serial; use super::volumes::{self, Entry}; @@ -79,7 +80,6 @@ fn names(entries: &[&Entry]) -> String { entries.iter().map(|e| e.name.as_str()).collect::>().join(", ") } -/// The clock lines from a boot log, for a failure message that says why. /// What `wall_clock_now` printed for `SYS_CLOCK_EPOCH`. fn probed_epoch(log: &str) -> Option { let line = log.lines().find(|l| l.contains("wall-clock: epoch="))?; @@ -115,6 +115,7 @@ fn boot_and_read( image_name: &str, params: &'static [&'static str], stage: &[(String, Vec)], + firmware_vars: Option, ) -> Result<(Vec, String, Duration), String> { let image_path = super::lane::dir().join(image_name); let mut image = qemu::build_boot_image(test_config, c_bins, rust_bins, params); @@ -137,6 +138,7 @@ fn boot_and_read( boot_image: Some(qemu::Staged::Written(image_path.clone())), kernel_params: params, rtc_base: Some(RTC_BASE), + firmware_vars, ..Default::default() }, ); @@ -156,8 +158,6 @@ fn boot_and_read( writeln!(qemu.stdin_mut(), "run echo {WINDOW_MARKER}") .map_err(|e| format!("stage the between-tests window: {e}"))?; qemu.flush_stdin(); - // What the two clock syscalls answer, which nothing on the volume can show: - // the file name is local time and `SYS_CLOCK_EPOCH` serves UTC. let probe = qemu.run_test("test_rs_wall_clock_now", Duration::from_secs(30)); log.push_str(&probe.before); log.push_str(&probe.stdout); @@ -189,36 +189,59 @@ fn boot_and_read( Ok((entries, log, launched.elapsed())) } -/// A firmware-named zone separates local time from UTC, in the direction UEFI -/// defines. -/// -/// The clock the RTC reads is local by firmware's account, so the file name and -/// every FAT stamp stay on the staged instant; only `SYS_CLOCK_EPOCH` moves. -/// UEFI's relation is `Localtime = UTC - TimeZone`, so the two hours east this -/// stages report -120 and UTC comes out *behind* the RTC — the sign that a -/// reader of the field gets backwards, and the one that would put a dual-booted -/// laptop four hours out rather than two. -pub fn zone_from_firmware( +/// `PcatRealTimeClockRuntimeDxe`'s `FILE_GUID`, `378D7B65-8DA9-4773-B6E4-A47826A833E1`, +/// in the byte order `EFI_GUID` stores: the vendor of the `RTC` variable its +/// `PcRtcInit` reads `EFI_TIME::TimeZone` out of (edk2 +/// `PcAtChipsetPkg/PcatRealTimeClockRuntimeDxe/PcRtc.c`). +const PC_RTC_VENDOR: [u8; 16] = + [0x65, 0x7b, 0x8d, 0x37, 0xa9, 0x8d, 0x73, 0x47, 0xb6, 0xe4, 0xa4, 0x78, 0x26, 0xa8, 0x33, 0xe1]; +/// `EFI_VARIABLE_NON_VOLATILE | BOOTSERVICE_ACCESS | RUNTIME_ACCESS`, what +/// `PcRtcSetTime` stores the zone with. +const PC_RTC_ATTRIBUTES: u32 = 0x7; +/// UTC+2 in `EFI_TIME::TimeZone`, whose relation is `Localtime = UTC - TimeZone`. +const FIRMWARE_ZONE_MINUTES: i16 = -120; + +/// How far `h:m:s` is past the staged instant's own time of day; the base is +/// far enough from midnight that no boot crosses one. +fn past_the_base(h: &str, m: &str, s: &str) -> Option { + let [h, m, s] = [h, m, s].map(|field| field.parse::().ok()); + Some(h? * 3_600 + m? * 60 + s? - RTC_BASE_SECS.rem_euclid(86_400)) +} + +/// What `wall_clock_now` printed for `SYS_CLOCK_REALTIME`, past the base. +fn probed_realtime(log: &str) -> Option { + let line = log.lines().find(|l| l.contains("wall-clock: epoch="))?; + let hms = line.split("realtime=").nth(1)?.split_whitespace().next()?; + let [h, m, s] = hms.split(':').collect::>()[..] else { return None }; + past_the_base(h, m, s) +} + +pub fn rtc_is_utc( test_config: &Path, c_bins: &[(String, Vec)], rust_bins: &[(String, Vec)], ) -> Result<(), String> { - const PARAMS: &[&str] = &["rtc-zone-east"]; - /// What `clock::init_wall` stages, in seconds: two hours east of UTC. - const OFFSET_SECS: i64 = -120 * 60; - - let (entries, log, lived) = - boot_and_read(test_config, c_bins, rust_bins, "wall-clock-zone.img", PARAMS, &[])?; + let vars = super::lane::dir().join("wall-clock-utc-vars.fd"); + toyos_build::firmware::of(qemu::Profile::Metal.arch())?.fresh_vars(&vars)?; + let zone = u32::from(FIRMWARE_ZONE_MINUTES as u16).to_le_bytes(); + fwvars::plant(&vars, &PC_RTC_VENDOR, "RTC", PC_RTC_ATTRIBUTES, &zone)?; + let booted = + boot_and_read(test_config, c_bins, rust_bins, "wall-clock-utc.img", &[], &[], Some(vars.clone())); + let _ = std::fs::remove_file(&vars); + let (entries, log, lived) = booted?; let logs = logs(&entries); let [only] = logs.as_slice() else { return Err(format!("the volume holds {} logs, wanted one: {}", logs.len(), names(&logs))); }; - // Unmoved: FAT stores local time by specification, and so does the name. - if !only.name.starts_with(RTC_BASE_DATE) { + let named = only.name.strip_prefix(RTC_BASE_DATE).and_then(|hms| hms.strip_suffix(".log")).and_then(|hms| { + let (h, ms) = hms.split_at_checked(2)?; + let (m, s) = ms.split_at_checked(2)?; + past_the_base(h, m, s) + }); + if !named.is_some_and(|drift| after_the_base(drift, lived)) { return Err(format!( - "a zone moved this boot's *local* time: the log is {} and the host staged \ - {RTC_BASE}\n{}", + "the log is {} and the host staged {RTC_BASE}\n{}", only.name, clock_lines(&log) )); @@ -226,7 +249,8 @@ pub fn zone_from_firmware( let stamp_drift = only.modified - RTC_BASE_SECS; if !after_the_base(stamp_drift, lived) { return Err(format!( - "a zone moved this boot's FAT timestamp by {stamp_drift}s, and FAT stores local time" + "this boot's FAT timestamp is {stamp_drift}s from the staged instant\n{}", + clock_lines(&log) )); } @@ -236,20 +260,30 @@ pub fn zone_from_firmware( clock_lines(&log) )); }; - let drift = epoch - (RTC_BASE_SECS + OFFSET_SECS); + let drift = epoch - RTC_BASE_SECS; if !after_the_base(drift, lived) { - let unshifted = epoch - RTC_BASE_SECS; return Err(format!( - "with firmware naming -120 minutes, `SYS_CLOCK_EPOCH` answered {epoch}: {drift}s from \ - the UTC that implies, and {unshifted}s from the RTC's own reading. Zero for the \ - second means the offset was dropped; {}s means its sign is inverted\n{}", - -OFFSET_SECS * 2, + "`SYS_CLOCK_EPOCH` answered {epoch}, {drift}s from the staged instant\n{}", + clock_lines(&log) + )); + } + let Some(realtime_drift) = probed_realtime(&log) else { + return Err(format!( + "the guest never printed what `SYS_CLOCK_REALTIME` answered\n{}", + clock_lines(&log) + )); + }; + if !after_the_base(realtime_drift, lived) { + return Err(format!( + "`SYS_CLOCK_REALTIME` answered a time of day {realtime_drift}s from the staged \ + instant's\n{}", clock_lines(&log) )); } eprintln!( - " [clock] firmware naming -120 minutes: {} keeps local time, epoch is {}s behind it", - only.name, -OFFSET_SECS + " [clock] with firmware naming {FIRMWARE_ZONE_MINUTES} minutes, {}, its FAT stamp, epoch \ + {epoch} and the time of day sit on the staged instant", + only.name ); Ok(()) } @@ -264,7 +298,8 @@ pub fn undated( params: &'static [&'static str], because: &str, ) -> Result<(), String> { - let (entries, log, _) = boot_and_read(test_config, c_bins, rust_bins, image_name, params, &[])?; + let (entries, log, _) = + boot_and_read(test_config, c_bins, rust_bins, image_name, params, &[], None)?; let logs = logs(&entries); // The refusal, by name and with its reason. A kernel that silently took @@ -331,7 +366,7 @@ pub fn no_century( // ignoring it gives 2133. const PARAMS: &[&str] = &["rtc-no-century", "rtc-century-next"]; let (entries, log, _) = - boot_and_read(test_config, c_bins, rust_bins, "wall-clock-no-century.img", PARAMS, &[])?; + boot_and_read(test_config, c_bins, rust_bins, "wall-clock-no-century.img", PARAMS, &[], None)?; let logs = logs(&entries); if !log.contains("ACPI: the FADT names no RTC century register") { @@ -372,7 +407,7 @@ pub fn century_from_the_register( ) -> Result<(), String> { const PARAMS: &[&str] = &["rtc-century-next"]; let (entries, log, _) = - boot_and_read(test_config, c_bins, rust_bins, "wall-clock-century.img", PARAMS, &[])?; + boot_and_read(test_config, c_bins, rust_bins, "wall-clock-century.img", PARAMS, &[], None)?; let logs = logs(&entries); let [only] = logs.as_slice() else { diff --git a/tests/test-durations b/tests/test-durations index 895ee3c3f96..891c8b6af74 100644 --- a/tests/test-durations +++ b/tests/test-durations @@ -380,7 +380,6 @@ wall_clock_no_century 6987 wall_clock_now 13 wall_clock_rtc_dead 8070 wall_clock_rtc_unstable 7805 -wall_clock_zone 9347 watchdog_resets 9393 window_refusal 16 writeback_durability 8888 diff --git a/tests/toyos-rust-tests/src/bin/wall_clock_now.rs b/tests/toyos-rust-tests/src/bin/wall_clock_now.rs index 08c6d3417e0..6343a540b33 100644 --- a/tests/toyos-rust-tests/src/bin/wall_clock_now.rs +++ b/tests/toyos-rust-tests/src/bin/wall_clock_now.rs @@ -1,11 +1,5 @@ //! What the two clock syscalls answer, from inside the machine. //! -//! The host stages the RTC with `-rtc base=` and reads the *name* of the file -//! the kernel writes, which is local time. Neither of those can see -//! `SYS_CLOCK_EPOCH`, which serves UTC — and the difference between the two is -//! the whole of the timezone question. This prints both so the host can put -//! them against the instant it set, in `tests/common/wallclock.rs`. -//! //! What it asserts itself is only what holds on *every* machine, because it //! runs on four of them: the shared boot, and the three whose clocks are staged //! broken. A machine with no wall clock is not a failure here — printing that @@ -32,7 +26,7 @@ fn main() { return; }; println!( - "wall-clock: epoch={epoch} local={:02}:{:02}:{:02}", + "wall-clock: epoch={epoch} realtime={:02}:{:02}:{:02}", time.hours, time.minutes, time.seconds ); diff --git a/tests/toyos.rs b/tests/toyos.rs index fe340049e57..ba9c9e66b13 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -600,6 +600,9 @@ const MACHINE_TESTS: &[(&str, Sched, Tier)] = &[ // clock in the verdict. ("root_from_memory", Sched::Parallel, Tier::Nightly), ("root_withheld_refused", Sched::Parallel, Tier::Nightly), + // A loader built to another `KernelArgs` layout is refused by name before + // the kernel reads a field the layout could have moved. + ("kernel_args_layout_refused", Sched::Parallel, Tier::Nightly), // The boot from power-on, as the kernel converts the loader's TSC readings: // judged against the loader's raw counts and the kernel's own rate. ("boot_from_power_on", Sched::Parallel, Tier::Nightly), @@ -1225,7 +1228,7 @@ const MACHINE_TESTS: &[(&str, Sched, Tier)] = &[ ("wall_clock_rtc_unstable", Sched::Parallel, Tier::Weekly), ("wall_clock_no_century", Sched::Parallel, Tier::Weekly), ("wall_clock_century_register", Sched::Parallel, Tier::Weekly), - ("wall_clock_zone", Sched::Parallel, Tier::Weekly), + ("wall_clock_utc", Sched::Parallel, Tier::Weekly), // `xhci_slow_connect`'s shape against the disk's port, but its actuator // masks the port until `BOOT_SCAN_DONE` — a kernel event, not a duration — // so what it stages is an ordering with no wall-clock margin on either @@ -1477,7 +1480,7 @@ const CARRIES: &[(&str, &[&str])] = &[ ("wall_clock_rtc_unstable", &["test_rs_wall_clock_now"]), ("wall_clock_no_century", &["test_rs_wall_clock_now"]), ("wall_clock_century_register", &["test_rs_wall_clock_now"]), - ("wall_clock_zone", &["test_rs_wall_clock_now"]), + ("wall_clock_utc", &["test_rs_wall_clock_now"]), ("screen_console_clear", &["test_rs_test_screen_graffiti"]), ("screen_console_scroll", &["test_rs_test_screen_churn"]), ("screen_console_panic", &["test_rs_test_panic_child"]), @@ -10090,9 +10093,7 @@ fn run_machine_test( "wall_clock_century_register" => { common::wallclock::century_from_the_register(test_config, c_bins, rust_bins) } - "wall_clock_zone" => { - common::wallclock::zone_from_firmware(test_config, c_bins, rust_bins) - } + "wall_clock_utc" => common::wallclock::rtc_is_utc(test_config, c_bins, rust_bins), "late_storage_connect" => common::volumes::late_storage_connect(test_config, c_bins, rust_bins), "root_candidate_malformed" => { common::volumes::root_candidate_malformed(test_config, c_bins, rust_bins) @@ -12154,6 +12155,20 @@ fn run_machine_test( // Both channels: this kernel dies before virtio-console init. root_withheld_refused(&format!("{}{}", qemu.boot_log(), qemu.uart_log())) } + "kernel_args_layout_refused" => { + let qemu = QemuInstance::boot_with_options( + test_config, + c_bins, + rust_bins, + BootOptions { + kernel_params: &[LAYOUT_ZERO_PARAM], + ready_marker: LAYOUT_REFUSAL, + ..Default::default() + }, + ); + // Both channels: this kernel dies before virtio-console init. + kernel_args_layout_refused(&format!("{}{}", qemu.boot_log(), qemu.uart_log())) + } "acpi_table_inventory" => { let qemu = QemuInstance::boot(test_config, c_bins, rust_bins); acpi_table_inventory(qemu.boot_log()) @@ -17560,7 +17575,6 @@ fn root_from_memory(log: &str) -> Result<(), String> { Ok(()) } -/// The loader's line: its TSC at entry, at the handoff, and `IA32_TSC_ADJUST`. const LOADER_TSC: &str = "Loader TSC: "; /// The kernel's line, followed by its four spans in milliseconds. const POWER_ON: &str = "boot: power-on to loader "; @@ -17617,6 +17631,31 @@ fn boot_from_power_on(log: &str) -> Result<(), String> { Ok(()) } +/// `toyos_abi::boot::WRITE_NO_LAYOUT_PARAM`. +const LAYOUT_ZERO_PARAM: &str = toyos_abi::boot::WRITE_NO_LAYOUT_PARAM; +/// The kernel's refusal of a `KernelArgs` layout word of 0, up to the layout +/// it reads. +const LAYOUT_REFUSAL: &str = "boot: the loader wrote KernelArgs layout 0x0 and this kernel reads layout 0x"; +/// `blackbox::arm`'s record, the kernel's first read of a field after the word. +const BLACK_BOX_ARMED: &str = "black box: "; + +/// A loader that wrote another layout is a boot refused by name, before the +/// kernel read the boot parameter. Checked against the kernel's own word, +/// `toyos_abi::boot::LAYOUT` in hex, and not merely the message's prefix: a +/// kernel that printed its own `kernel_args.layout` instead would still be 0x0 +/// and still match the prefix. +fn kernel_args_layout_refused(log: &str) -> Result<(), String> { + let refusal = format!("{LAYOUT_REFUSAL}{:x}", toyos_abi::boot::LAYOUT); + if !log.contains(&refusal) { + return Err(format!("no {refusal:?} in the boot log")); + } + if log.contains(BLACK_BOX_ARMED) { + return Err(format!("a boot refused its layout still said {BLACK_BOX_ARMED:?}")); + } + eprintln!(" [boot] a loader that wrote layout 0 was refused by name before the boot parameter"); + Ok(()) +} + /// A loader that hands no ROOT image is a boot refused by name: the kernel's /// refusal is on the console, and nothing after it mounted ROOT from anywhere. fn root_withheld_refused(log: &str) -> Result<(), String> { diff --git a/toyos-abi/src/boot.rs b/toyos-abi/src/boot.rs index 1c1c40a5050..07703906afb 100644 --- a/toyos-abi/src/boot.rs +++ b/toyos-abi/src/boot.rs @@ -55,34 +55,11 @@ pub struct KernelArgs { /// Naming the partition is all this does. Whether one with that GUID is on /// the disk is the kernel's question, and its answer there may well be no. pub log_partition_guid: [u8; 16], - /// Minutes to add to the CMOS RTC's own reading to get UTC, as firmware - /// reported it in `EFI_TIME::TimeZone`. - /// - /// The RTC's registers carry a wall clock and no zone, and no two operating - /// systems agree on which zone that is: a machine that has ever run Windows - /// keeps local time there, one that has only run Linux keeps UTC. Firmware - /// is the one party that both knows and can be asked, and `GetTime` is the - /// call — a *runtime* service, so it is asked here rather than in the - /// kernel, which never maps the runtime. - /// - /// UEFI's relation is `Localtime = UTC - TimeZone`, so a machine keeping - /// local time in UTC+2 reports -120 and the kernel adds -120 minutes to what - /// it reads off the CMOS. - pub rtc_utc_offset_minutes: i32, - /// Whether firmware answered the question above at all. - /// - /// Zero when `GetTime` failed, or reported `EFI_UNSPECIFIED_TIMEZONE`, or - /// named an offset outside the range its own spec gives the field. The - /// middle one is the ordinary state of a machine nothing has ever told its - /// zone to, and it is what OVMF ships. The kernel then treats the RTC as UTC - /// and says so, because with the one party that knows declining to answer - /// there is nothing else left to assume. - /// - /// A flag rather than a sentinel in the field above, for the same reason - /// [`Self::boot_partition_present`] is one: `0x7FF` is a value the *wire* - /// format defines, and carrying it inward would make every reader of this - /// struct know that. - pub rtc_utc_offset_known: u32, + /// The layout the loader wrote this struct in: [`LAYOUT`] from a loader built + /// with this file. The kernel refuses any other value before it reads a + /// field after this one, so every field before it keeps its offset in every + /// layout. + pub layout: u32, /// The boot parameter, as ASCII with no terminator: comma-separated tokens /// read out of `\toyos\cmdline` on the volume the bootloader loaded itself /// from. [`root_uuid`] and [`actuators`] are the two readings of it. @@ -133,6 +110,16 @@ pub struct KernelArgs { pub root_read_tsc: u64, } +/// [`KernelArgs::layout`] for the struct this file declares: the struct's own +/// size folded in. Never within -1440..=1440 as an `i32`: a loader older than +/// the word wrote a firmware zone in minutes at its offset. +pub const LAYOUT: u32 = 0x5459_0000 | core::mem::size_of::() as u32; + +/// The boot parameter on which the loader writes 0 as [`KernelArgs::layout`], +/// what an older loader writes there on firmware that names no zone: the +/// negative control on the kernel's refusal, and read by both of them. +pub const WRITE_NO_LAYOUT_PARAM: &str = "loader-writes-no-layout"; + /// The boot parameter on which the loader hands the kernel no ROOT image: the /// negative control on the kernel's refusal, and read by both of them. pub const WITHHOLD_ROOT_PARAM: &str = "loader-withholds-root"; @@ -201,15 +188,6 @@ pub fn actuators(cmdline: &str) -> impl Iterator { } impl KernelArgs { - /// Firmware's answer about the zone the RTC keeps, as one value. - /// - /// The two fields exist because this struct is a C layout shared by two - /// binaries; this is where they become the option they describe, and no - /// caller inward of here handles the pair. - pub fn rtc_utc_offset(&self) -> Option { - (self.rtc_utc_offset_known != 0).then_some(self.rtc_utc_offset_minutes) - } - /// The windows firmware named, and none of the array behind them. The /// loader is the only writer of the count, so one past the array panics /// here rather than clamping. @@ -225,9 +203,7 @@ impl KernelArgs { /// /// The size and alignment are here for the other half of the contract: the /// bootloader writes this struct and the kernel reads it, and the two are -/// separate binaries built for separate targets. They share this file, so they -/// cannot disagree about the layout — but only as long as nothing else does -/// the arithmetic by hand. +/// separate binaries built for separate targets. const _: () = { use core::mem::{align_of, offset_of, size_of}; assert!(offset_of!(KernelArgs, kernel_memory_addr) == 16); @@ -238,19 +214,19 @@ const _: () = { assert!(offset_of!(KernelArgs, boot_partition_guid) == 128); assert!(offset_of!(KernelArgs, boot_partition_present) == 144); assert!(offset_of!(KernelArgs, log_partition_guid) == 148); - assert!(offset_of!(KernelArgs, rtc_utc_offset_minutes) == 164); - assert!(offset_of!(KernelArgs, rtc_utc_offset_known) == 168); - assert!(offset_of!(KernelArgs, cmdline_addr) == 176); - assert!(offset_of!(KernelArgs, cmdline_len) == 184); - assert!(offset_of!(KernelArgs, root_bridge_window_count) == 192); - assert!(offset_of!(KernelArgs, root_bridge_windows) == 200); - assert!(offset_of!(KernelArgs, root_image_addr) == 1224); - assert!(offset_of!(KernelArgs, root_image_len) == 1232); - assert!(offset_of!(KernelArgs, root_partition_guid) == 1240); - assert!(offset_of!(KernelArgs, loader_entry_tsc) == 1256); - assert!(offset_of!(KernelArgs, loader_handoff_tsc) == 1264); - assert!(offset_of!(KernelArgs, root_read_tsc) == 1272); - assert!(size_of::() == 1280); + assert!(offset_of!(KernelArgs, layout) == 164); + assert!(offset_of!(KernelArgs, cmdline_addr) == 168); + assert!(offset_of!(KernelArgs, cmdline_len) == 176); + assert!(offset_of!(KernelArgs, root_bridge_window_count) == 184); + assert!(offset_of!(KernelArgs, root_bridge_windows) == 192); + assert!(offset_of!(KernelArgs, root_image_addr) == 1216); + assert!(offset_of!(KernelArgs, root_image_len) == 1224); + assert!(offset_of!(KernelArgs, root_partition_guid) == 1232); + assert!(offset_of!(KernelArgs, loader_entry_tsc) == 1248); + assert!(offset_of!(KernelArgs, loader_handoff_tsc) == 1256); + assert!(offset_of!(KernelArgs, root_read_tsc) == 1264); + assert!(size_of::() == 1272); + assert!(LAYOUT as i32 > 1440 || (LAYOUT as i32) < -1440); assert!(align_of::() == 8); assert!(size_of::() == 16); assert!(align_of::() == 8); @@ -332,8 +308,7 @@ mod tests { boot_partition_guid: [0; 16], boot_partition_present: 0, log_partition_guid: [0; 16], - rtc_utc_offset_minutes: 0, - rtc_utc_offset_known: 0, + layout: LAYOUT, cmdline_addr: 0, cmdline_len: 0, root_bridge_window_count: 0, diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index f471ce741fe..e1b8cb5709d 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -1097,8 +1097,6 @@ pub fn random(buf: &mut [u8]) -> Result<(), SyscallError> { check_unit(syscall(SYS_RANDOM, buf.as_mut_ptr() as u64, buf.len() as u64, 0, 0)) } -/// The time of day in the zone the machine keeps its clock in. -/// /// `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 diff --git a/toyos-acpi/src/lib.rs b/toyos-acpi/src/lib.rs index 48edc0db15e..13df39bd4d7 100644 --- a/toyos-acpi/src/lib.rs +++ b/toyos-acpi/src/lib.rs @@ -30,7 +30,7 @@ pub use madt::{ madt_entries, Gicc, IoApicEntry, MadtEntries, MadtEntry, MadtHalt, SourceOverride, MADT_ENTRIES, }; pub use gtdt::{gtdt, Gtdt, TimerInterrupt, GTDT_NEEDED}; -pub use resource::{memory_windows, ResourceError, Walk, MAX_LIST_BYTES}; +pub use resource::{memory_windows, ResourceError, MAX_LIST_BYTES}; pub use spcr::{spcr, Gas, SerialInterface, Spcr, GAS_SYSTEM_MEMORY, SPCR_NEEDED}; /// Physical memory, as this decoder reads it. diff --git a/toyos-acpi/src/resource.rs b/toyos-acpi/src/resource.rs index 6579e06e895..80304260c28 100644 --- a/toyos-acpi/src/resource.rs +++ b/toyos-acpi/src/resource.rs @@ -104,18 +104,6 @@ impl core::fmt::Display for ResourceError { } } -/// What one walk of a descriptor list found. -pub struct Walk { - /// How far the list ran: to and including its End Tag, or through the - /// descriptor the walk refused, or as far as the reader would go. **This is - /// the evidence** — what the bootloader logs the raw bytes of, on a list - /// this decoder reads and on one it refuses alike. - pub bytes: usize, - /// How many memory windows were written into the caller's slice, or why the - /// list cannot be used. - pub windows: Result, -} - /// One descriptor's tag and its whole length, header included. struct Item { tag: u8, @@ -161,40 +149,33 @@ fn u64le(phys: P, at: u64, offset: usize) -> u64 { v } -/// Every memory window the list at `at` names, written into `out`. +/// Every memory window the list at `at` names, written into `out`, and how many. /// /// **A refusal carries no windows at all**: a caller handed the ones decoded /// before the refusal would be holding an aperture with a hole in it, and would /// place a BAR in the hole. -pub fn memory_windows(phys: P, at: u64, out: &mut [RootBridgeWindow]) -> Walk { +pub fn memory_windows(phys: P, at: u64, out: &mut [RootBridgeWindow]) -> Result { let mut offset = 0usize; let mut found = 0usize; loop { if offset >= MAX_LIST_BYTES { - return Walk { bytes: offset.min(MAX_LIST_BYTES), windows: Err(ResourceError::Unterminated) }; + return Err(ResourceError::Unterminated); } - let item = match item(phys, at, offset) { - Ok(item) => item, - Err(why) => return Walk { bytes: offset, windows: Err(why) }, - }; + let item = item(phys, at, offset)?; let head = at + offset as u64; - // Stepped over the refusing descriptor as well, so the bytes logged - // beside a refusal are the ones the refusal is about. let through = offset + item.len; if item.tag == END_TAG { - return Walk { bytes: through.min(MAX_LIST_BYTES), windows: Ok(found) }; + return Ok(found); } if item.tag != QWORD_ADDRESS_SPACE { - return Walk { bytes: through, windows: Err(ResourceError::UnknownTag { tag: item.tag }) }; + return Err(ResourceError::UnknownTag { tag: item.tag }); } if item.len < QWORD_BYTES { - let why = ResourceError::Short { tag: item.tag, whole: item.len, needed: QWORD_BYTES }; - return Walk { bytes: through, windows: Err(why) }; + return Err(ResourceError::Short { tag: item.tag, whole: item.len, needed: QWORD_BYTES }); } let kind = phys.byte(head + RESOURCE_TYPE as u64); if !matches!(kind, TYPE_MEMORY | TYPE_IO | TYPE_BUS) { - let why = ResourceError::UnknownResourceType { kind }; - return Walk { bytes: through, windows: Err(why) }; + return Err(ResourceError::UnknownResourceType { kind }); } if kind == TYPE_MEMORY { let min = u64le(phys, head, QWORD_MINIMUM); @@ -215,14 +196,12 @@ pub fn memory_windows(phys: P, at: u64, out: &mut [RootBridgeWindow]) - None }; if let Some(why) = refusal { - return Walk { bytes: through, windows: Err(why) }; + return Err(why); } let room = out.len(); match out.get_mut(found) { Some(slot) => *slot = RootBridgeWindow { base: min, length }, - None => { - return Walk { bytes: through, windows: Err(ResourceError::TooMany { room }) } - } + None => return Err(ResourceError::TooMany { room }), } found += 1; } diff --git a/toyos-acpi/tests/corpus.rs b/toyos-acpi/tests/corpus.rs index e0201ec03d1..896789ac93a 100644 --- a/toyos-acpi/tests/corpus.rs +++ b/toyos-acpi/tests/corpus.rs @@ -553,16 +553,7 @@ fn no_single_byte_mutation_of_a_firmwares_descriptor_list_panics_or_runs_away() mutated[offset] = value; let regions: &[(u64, &[u8])] = &[(ROOT_BRIDGE_AT, &mutated)]; let mut out = [RootBridgeWindow::default(); 8]; - let walk = memory_windows(Machine { regions }, ROOT_BRIDGE_AT, &mut out); - // The list is the whole of what can be read, so a walk reporting - // more bytes than it holds is one that stopped advancing or - // stepped past a descriptor it had not read. - assert!( - walk.bytes <= original.len(), - "{which} byte {offset} as {value:#04x}: the walk reported {} bytes", - walk.bytes - ); - match walk.windows { + match memory_windows(Machine { regions }, ROOT_BRIDGE_AT, &mut out) { Ok(count) => assert!( out[..count].iter().all(|w| w.length != 0), "{which} byte {offset} as {value:#04x}: a window of no length was carried" diff --git a/toyos-acpi/tests/fixtures.rs b/toyos-acpi/tests/fixtures.rs index 4d5e862e33b..1e8edfecdf5 100644 --- a/toyos-acpi/tests/fixtures.rs +++ b/toyos-acpi/tests/fixtures.rs @@ -149,11 +149,7 @@ const T14_BRIDGE: &[u8] = include_bytes!("../fixtures/thinkpad-t14/root-bridge-0 fn bridge_windows(bytes: &'static [u8]) -> Vec { let regions: &[(u64, &[u8])] = &[(ROOT_BRIDGE, bytes)]; let mut out = [RootBridgeWindow::default(); 8]; - let walk = memory_windows(Machine { regions }, ROOT_BRIDGE, &mut out); - let count = walk.windows.expect("bytes a firmware answered with"); - // The four descriptors' own declared lengths tile the file exactly, which - // is what says the loader logged the whole list and no more. - assert_eq!(walk.bytes, bytes.len()); + let count = memory_windows(Machine { regions }, ROOT_BRIDGE, &mut out).expect("bytes a firmware answered with"); out[..count].to_vec() } diff --git a/toyos-acpi/tests/resource.rs b/toyos-acpi/tests/resource.rs index 41bab94529e..298a97ec326 100644 --- a/toyos-acpi/tests/resource.rs +++ b/toyos-acpi/tests/resource.rs @@ -5,7 +5,7 @@ mod common; use common::Machine; use toyos_abi::boot::RootBridgeWindow; -use toyos_acpi::{memory_windows, ResourceError, Walk, MAX_LIST_BYTES}; +use toyos_acpi::{memory_windows, ResourceError, MAX_LIST_BYTES}; /// Where a firmware pool allocation sits in the crafted machines below. const AT: u64 = 0x7f00_1234; @@ -44,19 +44,12 @@ fn list(descriptors: &[Vec]) -> Vec { /// One walk of `bytes`, in a machine holding `bytes` at [`AT`] and nothing /// else — so a decoder reading one byte past the list panics rather than /// answering. -fn walk(bytes: &[u8], room: usize) -> (Walk, Vec) { +fn windows(bytes: &[u8], room: usize) -> Result, ResourceError> { let regions: &[(u64, &[u8])] = &[(AT, bytes)]; let mut out = vec![RootBridgeWindow::default(); room]; - let walked = memory_windows(Machine { regions }, AT, &mut out); - if let Ok(count) = walked.windows { - out.truncate(count); - } - (walked, out) -} - -fn windows(bytes: &[u8], room: usize) -> Result, ResourceError> { - let (walked, out) = walk(bytes, room); - walked.windows.map(|_| out) + let count = memory_windows(Machine { regions }, AT, &mut out)?; + out.truncate(count); + Ok(out) } /// The shape the T14's firmware answers in, as Linux's own journal reports it: @@ -71,10 +64,8 @@ fn only_the_memory_ranges_of_a_root_bridge_become_windows() { qword(MEMORY, 0xa080_0000, 0x1f80_0000, 0), qword(MEMORY, 0x40_0000_0000, 0x40_0000_0000, 0), ]); - let (walked, decoded) = walk(&bytes, 8); - assert_eq!(walked.bytes, bytes.len()); assert_eq!( - decoded, + windows(&bytes, 8).expect("a list this decoder reads"), [ RootBridgeWindow { base: 0x000a_0000, length: 0x0002_0000 }, RootBridgeWindow { base: 0xa080_0000, length: 0x1f80_0000 }, @@ -217,32 +208,13 @@ fn a_list_with_no_end_tag_stops_at_the_bound() { while bytes.len() < MAX_LIST_BYTES + 46 { bytes.extend_from_slice(&qword(IO, 0x1000, 0x10, 0)); } - let (walked, _) = walk(&bytes, 8); - assert_eq!(walked.windows, Err(ResourceError::Unterminated)); - assert_eq!(walked.bytes, MAX_LIST_BYTES); + assert_eq!(windows(&bytes, 8), Err(ResourceError::Unterminated)); } -/// A list the reader runs out of is a refusal naming the bytes it wanted, and -/// the walk answers with what it did reach — which is what the bootloader logs -/// beside the refusal. +/// A list the reader runs out of is a refusal naming the bytes it wanted. #[test] fn a_list_that_runs_off_the_end_of_what_can_be_read_is_refused() { let mut bytes = qword(MEMORY, 0xa080_0000, 0x1000, 0); bytes.truncate(40); - let (walked, _) = walk(&bytes, 8); - assert_eq!(walked.windows, Err(ResourceError::Unreadable { at: AT, len: 46 })); - assert_eq!(walked.bytes, 0); -} - -/// A refused list is walked *through* the descriptor that refused it, so the -/// bytes the bootloader logs beside a refusal are the ones it is about. -#[test] -fn the_bytes_a_walk_reports_cover_the_descriptor_it_refused() { - let bytes = list(&[ - qword(MEMORY, 0xa080_0000, 0x1000, 0), - qword(MEMORY, 0x1000_0000, 0x1000, 0x4000_0000), - ]); - let (walked, _) = walk(&bytes, 8); - assert_eq!(walked.windows, Err(ResourceError::Translated { min: 0x1000_0000, offset: 0x4000_0000 })); - assert_eq!(walked.bytes, 92); + assert_eq!(windows(&bytes, 8), Err(ResourceError::Unreadable { at: AT, len: 46 })); } diff --git a/toyos-update/src/lib.rs b/toyos-update/src/lib.rs index c40d9b6ce2b..2410b1ac513 100644 --- a/toyos-update/src/lib.rs +++ b/toyos-update/src/lib.rs @@ -18,13 +18,6 @@ //! marked slot, and falls back to the other where the marked one is refused or //! died on its last boot ([`record`]); the updater writes only the slot the //! machine is not running, and moves the mark last. -//! -//! **What anti-rollback is here** ([`policy`]): the loader refuses an image -//! whose version is below the highest version a boot has proven, and keeps -//! that floor in a firmware variable no running kernel can write, one per -//! signing key ([`floor`]); the updater refuses an image older than what the -//! machine runs. Neither can defend a machine whose firmware variables anyone -//! with the machine in hand can reset. #![cfg_attr(not(test), no_std)] #![forbid(unsafe_code)] diff --git a/toyos-update/src/record.rs b/toyos-update/src/record.rs index 8276c941582..6e83411f707 100644 --- a/toyos-update/src/record.rs +++ b/toyos-update/src/record.rs @@ -18,12 +18,6 @@ //! partition guid [16] | count u8 | booted u8 ('A', 'B' or 0) | 0 [6] //! | version u64 | signed-header sha256 [32] | dead A [32] | dead B [32] //! ``` -//! -//! A boot that hands the machine back on purpose proves its image, and the -//! pass that reads so raises the anti-rollback floor ([`crate::policy`]) — -//! **never to a version read here**: the file is on a partition the running -//! system writes, so what it names is only which slot's signed header the -//! loader verifies again, and the digest that header must hash to. use crate::slots::Which; use crate::Digest; diff --git a/toyos-wallclock/src/lib.rs b/toyos-wallclock/src/lib.rs index 4baf011df5f..01eeb337fcd 100644 --- a/toyos-wallclock/src/lib.rs +++ b/toyos-wallclock/src/lib.rs @@ -1,29 +1,12 @@ -//! The calendar, and the zone offset a userland reader has to recover. +//! The calendar. //! //! Two things live here because two programs need them and the host is where //! either can be tested: the kernel decodes an RTC into a [`Civil`] and stamps //! FAT directory entries from it, and `/system/bin/logd` names one file per boot from -//! the same calendar in the same zone. Before this crate there was one -//! implementation in `kernel/src/clock.rs` that userland could not reach, and -//! the second copy would have been the one whose correctness argument mattered -//! most and whose tests could not run. +//! the same calendar. //! //! Nothing here allocates, nothing here is `unsafe`, and nothing here reads a //! device: it is arithmetic over numbers its callers hand it. -//! -//! # The zone recovery -//! -//! [`resolve`] is the whole of `/log`'s wall-clock question, and [`Recovery`] -//! is its honest answer type. The syscall surface gives userland two readings -//! of one instant — `SYS_CLOCK_EPOCH`, which is UTC seconds, and -//! `SYS_CLOCK_REALTIME`, which is local `h:m:s` and no date — so the offset -//! between the zones is a subtraction of seconds-of-day. That -//! subtraction pins the offset **modulo 24 hours**, and the real range of zone -//! offsets is 26 hours wide (UTC−12:00 to UTC+14:00), so a two-hour band is -//! genuinely ambiguous: the same pair of readings is UTC+13 on one day and -//! UTC−11 on the day before. [`Recovery::Ambiguous`] is that band, named rather -//! than guessed, because the two answers differ by a whole day in a file name. -//! `userland/logd/src/wall.rs` is the caller and carries the argument in full. #![no_std] @@ -32,12 +15,6 @@ use core::fmt; /// Seconds in a day. const DAY: u64 = 86_400; -/// The easternmost real zone offset, UTC+14:00 (Line Islands, Kiribati). -pub const MAX_EAST_SECS: u64 = 14 * 3_600; - -/// The westernmost, UTC−12:00 (Baker Island), as a positive magnitude. -pub const MAX_WEST_SECS: u64 = 12 * 3_600; - /// A wall-clock instant in the fields a human reads. /// /// The one calendar in the tree. The RTC decodes its registers into this, the @@ -146,8 +123,7 @@ pub fn is_stem(stem: &str) -> bool { }) } -/// The name a boot gets when the machine would not say what time it is — or -/// would not say it unambiguously (`wall`). +/// The name a boot gets when the machine would not say what time it is. /// /// A word and not a zero date: `0000-00-00-000000.log` sorts correctly and /// reads as a real timestamp that happens to be absurd, and the difference @@ -191,58 +167,6 @@ pub fn classify(name: &str) -> Option { is_stem(stem).then_some(Class::Dated) } -/// What two readings of one instant can be made to say about the zone. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Recovery { - /// The offset east of UTC, in seconds, and it is the only real one the two - /// readings admit. - Offset(i64), - /// Two real zones a day apart, both consistent with the readings. Seconds - /// east of UTC, so a caller reporting the refusal can name them. - Ambiguous { east: i64, west: i64 }, -} - -/// **There is no third answer, and this is the proof of it.** -/// -/// [`Recovery`] used to carry a `NoZone` for "past UTC+14 going east and past -/// UTC−12 going west at once", described as *the middle of the band*. The band -/// has no middle: it **is** the overlap of the two ranges. Their widths sum to -/// 26 hours against a day's 24, so every one of the 86,400 offsets is -/// east-real, or west-real, or both — and the variant stood for a state the -/// arithmetic cannot reach, which every caller then had to write an arm for. -/// `every_offset_of_the_day_is_placed` is the empirical half, over the whole -/// domain rather than over examples; this is the half that holds at compile -/// time and fails the build if either bound is ever narrowed. -const _: () = assert!(MAX_EAST_SECS + MAX_WEST_SECS >= DAY); - -/// Recover the local zone's offset from a UTC instant and a local time of day. -/// -/// `epoch` is seconds since the Unix epoch (UTC) and `local_secs_of_day` is -/// `h*3600 + m*60 + s` read from the same instant. Both must be readings of one -/// instant — a caller that cannot guarantee that has to bracket them, because -/// the subtraction below has no slop in it to absorb a tick. -pub fn resolve(epoch: u64, local_secs_of_day: u64) -> Recovery { - let usod = epoch % DAY; - // Reduced mod `DAY` on both sides already, so one addition keeps the - // subtraction inside the unsigned domain. - let off = (local_secs_of_day % DAY + DAY - usod) % DAY; - - let east = off as i64; - let west = off as i64 - DAY as i64; - let east_real = off <= MAX_EAST_SECS; - let west_real = DAY - off <= MAX_WEST_SECS; - - // `(false, false)` is not written because it cannot be produced — the - // `const` assertion above the enum is the argument — so `(false, _)` is - // exactly "west and only west" and says so without a fourth arm that no - // input reaches and no test can cover. - match (east_real, west_real) { - (true, true) => Recovery::Ambiguous { east, west }, - (true, false) => Recovery::Offset(east), - (false, _) => Recovery::Offset(west), - } -} - fn is_leap(year: u64) -> bool { year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400)) } @@ -353,115 +277,4 @@ mod tests { assert!(!Civil { year: 1969, month: 1, day: 1, hour: 0, min: 0, sec: 0 }.is_valid()); assert!(!Civil { year: 2026, month: 1, day: 1, hour: 0, min: 0, sec: 60 }.is_valid()); } - - /// The owner's own zone, UTC+2, and the zero-offset machine every image - /// whose firmware names no zone boots as. Both well inside the unique band. - #[test] - fn the_two_zones_this_tree_actually_boots_in_recover_exactly() { - let epoch = 1_786_795_200; // 2026-08-15 12:00:00 UTC - assert_eq!(resolve(epoch, 14 * 3_600), Recovery::Offset(2 * 3_600)); - assert_eq!(resolve(epoch, epoch % DAY), Recovery::Offset(0)); - } - - /// **The day boundary, which is the case the recovery has to be argued - /// over.** A western zone reads a local time of day on the far side of - /// midnight from UTC's, and the answer is still one zone and one date. - #[test] - fn a_zone_across_midnight_from_utc_is_still_unique() { - // 2026-08-15 02:00:00 UTC is 2026-08-14 21:00:00 at UTC−5. - let epoch = 1_786_759_200; - assert_eq!(resolve(epoch, 21 * 3_600), Recovery::Offset(-5 * 3_600)); - let Recovery::Offset(off) = resolve(epoch, 21 * 3_600) else { panic!("not unique") }; - let local = Civil::from_unix_secs(epoch.saturating_add_signed(off)); - assert_eq!(format!("{}", local.stem()), "2026-08-14-210000"); - } - - /// The band that cannot be recovered, by the pair that produces it: one - /// reading pair, two real zones, two different local **days**. - #[test] - fn the_pacific_band_is_two_answers_and_says_so() { - // 2026-08-15 00:30:00 UTC. Local 13:30 is UTC+13 on the 15th and - // UTC−11 on the 14th, and nothing in the two readings separates them. - let epoch = 1_786_753_800; - assert_eq!( - resolve(epoch, 13 * 3_600 + 1_800), - Recovery::Ambiguous { east: 13 * 3_600, west: -11 * 3_600 } - ); - // And the two candidates really are a day apart, which is what makes - // guessing between them a mis-named file rather than a rounding error. - let east = Civil::from_unix_secs(epoch.saturating_add_signed(13 * 3_600)); - let west = Civil::from_unix_secs(epoch - 11 * 3_600); - assert_eq!(format!("{}", east.stem()), "2026-08-15-133000"); - assert_eq!(format!("{}", west.stem()), "2026-08-14-133000"); - } - - /// Both edges of the band, so the refusal starts where the argument says it - /// does and not one second either side. - #[test] - fn the_band_is_exactly_the_twelve_to_fourteen_hours_the_argument_names() { - let epoch = 1_786_753_800; - let at = |off: i64| { - resolve(epoch, ((epoch % DAY) as i64 + off).rem_euclid(DAY as i64) as u64) - }; - assert_eq!(at(12 * 3_600 - 1), Recovery::Offset(12 * 3_600 - 1)); - assert!(matches!(at(12 * 3_600), Recovery::Ambiguous { .. })); - assert!(matches!(at(13 * 3_600), Recovery::Ambiguous { .. })); - assert!(matches!(at(14 * 3_600), Recovery::Ambiguous { .. })); - assert_eq!(at(14 * 3_600 + 1), Recovery::Offset(14 * 3_600 + 1 - DAY as i64)); - assert_eq!(at(-12 * 3_600), Recovery::Ambiguous { east: 12 * 3_600, west: -12 * 3_600 }); - } - - /// Every offset a quarter-hour apart across the whole real range is either - /// recovered exactly or named as one of the two candidates. **A silent - /// wrong answer is what this refuses to have**, so the assertion is over - /// the whole domain rather than over three examples. - #[test] - fn no_real_offset_is_ever_answered_with_a_different_one() { - let epoch = 1_786_795_200; - let mut ambiguous = 0; - let mut minutes = -12 * 60; - while minutes <= 14 * 60 { - let off = minutes as i64 * 60; - let lsod = ((epoch % DAY) as i64 + off).rem_euclid(DAY as i64) as u64; - match resolve(epoch, lsod) { - Recovery::Offset(got) => assert_eq!(got, off, "offset {off} came back as {got}"), - Recovery::Ambiguous { east, west } => { - assert!(off == east || off == west, "offset {off} is neither candidate"); - ambiguous += 1; - } - } - minutes += 15; - } - // The band is two hours wide at quarter-hour steps, counted from both - // ends of the range: nine offsets east and nine west name each other. - assert_eq!(ambiguous, 18, "the ambiguous band changed width"); - } - - /// Every second of the day is placed, and the counts are what say there is - /// no third answer. - /// - /// The test above walks the *real* offsets a quarter-hour apart; this one - /// walks the whole input domain, including the 79,199 seconds-of-day that - /// no zone sits on. Neither `Offset` nor `Ambiguous` may go missing and - /// nothing may be left over: 43,200 east-only, 35,999 west-only and 7,201 - /// in the overlap is 86,400 exactly, which is why the fourth case - /// [`Recovery`] used to have a variant for is a state the arithmetic cannot - /// produce. - #[test] - fn every_offset_of_the_day_is_placed() { - let epoch = 1_786_795_200; - let (mut offset, mut ambiguous) = (0u32, 0u32); - for lsod in 0..DAY { - match resolve(epoch, lsod) { - Recovery::Offset(_) => offset += 1, - Recovery::Ambiguous { east, west } => { - assert_eq!(east - west, DAY as i64, "the two candidates are a day apart"); - ambiguous += 1; - } - } - } - assert_eq!(offset, 79_199); - assert_eq!(ambiguous, 7_201); - assert_eq!(u64::from(offset + ambiguous), DAY); - } } diff --git a/toyos/src/system.rs b/toyos/src/system.rs index 17d07177d0c..50c51a29339 100644 --- a/toyos/src/system.rs +++ b/toyos/src/system.rs @@ -7,8 +7,7 @@ use toyos_abi::syscall; /// a reader that walks off by a field the day one of them moves. pub use toyos_abi::syscall::{SYSINFO_ENTRY_SIZE, SYSINFO_HEADER_SIZE}; -/// The time of day in the machine's own zone, or `None` on a machine whose -/// clock never answered. +/// The time of day, or `None` on a machine whose clock never answered. pub fn clock_realtime() -> Option { syscall::clock_realtime() } diff --git a/userland/CLAUDE.md b/userland/CLAUDE.md index ccd51c39ff4..9589142df4b 100644 --- a/userland/CLAUDE.md +++ b/userland/CLAUDE.md @@ -6,5 +6,4 @@ The module header at the site owns its subject — surfaces, translators and the ## Caveats that bite every agent -- **Local time is recovered, not asked for** — `SYS_CLOCK_EPOCH` is UTC and `SYS_CLOCK_REALTIME` is `h:m:s`, so the zone comes from subtracting them, and `toyos-wallclock` refuses the UTC+12..+14 band where two real zones fit one reading a day apart. - **Nothing composes against the scanout** — reads from it miss every cache, which is why `window::Screen` has no read path. WC is weakly ordered: a blit ends with an `sfence` or the last partial buffer stays off the panel. diff --git a/userland/logd/src/main.rs b/userland/logd/src/main.rs index 86f4167eee3..bdbf78f7670 100644 --- a/userland/logd/src/main.rs +++ b/userland/logd/src/main.rs @@ -87,7 +87,6 @@ mod origin; mod policy; mod serve; mod store; -mod wall; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -164,21 +163,21 @@ fn main() { // The wall clock, read once. The kernel reads the RTC 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_local, zone) = boot_stamp(); + let (stem, boot_secs, dated) = boot_stamp(); let volume = Volume::open(stem, rotate_at, |line| say!("{line}")); match &volume { // This program's half of the startup report, in one line: the kernel // says whether it has a console, this program whether it has a volume // and what the name it chose was decided by. - Some(v) => say!("logd: this boot's kernel log is {} ({zone})", v.path()), + Some(v) => say!("logd: this boot's kernel log is {} ({dated})", v.path()), None => say!( "logd: no {DIR} on this machine - this boot's kernel log is on the console only \ - ({zone})" + ({dated})" ), } - let hub = Arc::new(serve::Hub::start(REPLAY_BYTES, boot_local)); + let hub = Arc::new(serve::Hub::start(REPLAY_BYTES, boot_secs)); let published = Arc::new(inspect::Published::new(hub.network())); if let Some(acceptor) = endow::acceptor(SERVICE) { inspect::serve(acceptor, Arc::clone(&published), Arc::clone(&hub)); @@ -200,7 +199,7 @@ fn main() { owed: false, retrying_since: None, degraded: false, - boot_local, + boot_secs, hub, stall: Stall::from_args(), stopping: None, @@ -232,7 +231,7 @@ struct Log { retrying_since: Option, /// Whether the volume answers, slower than `LOG_WRITE_BUDGET` a round. degraded: bool, - boot_local: Option, + boot_secs: Option, hub: Arc, stall: Option, /// init's flush was answered and the machine stops: the file's text held @@ -542,13 +541,13 @@ impl Log { for line in &lines { match &line.kind { Kind::Kernel(record) => { - file.push_str(&format!("{}\n", record.tagged(&stamp(self.boot_local, record.at_ns)))); + file.push_str(&format!("{}\n", record.tagged(&stamp(self.boot_secs, record.at_ns)))); } Kind::Program { tag, owner, said } => { let severity = said.severity; let tag = Tag::new(tag).expect("an origin's name is a tag"); let pid = (said.pid != *owner).then_some(said.pid); - let at = stamp(self.boot_local, said.at_ns); + let at = stamp(self.boot_secs, said.at_ns); let mut line = ProgramLine { stamp: &at, at_ns: said.at_ns, @@ -823,47 +822,24 @@ impl Stall { } } -/// This boot's file stem, and the local epoch second the machine booted at. +/// This boot's file stem, and the epoch second the machine booted at. /// /// `None` for the stem is a boot that cannot be placed in time, which takes an -/// `unknown-NN` name — and the two ways to get there are named separately, -/// because "this machine has no clock" and "this machine has a clock whose zone -/// two readings cannot separate" are different facts about the machine. +/// `unknown-NN` name. fn boot_stamp() -> (Option, Option, String) { - match wall::local_now() { - wall::Wall::Local { secs, offset_secs } => { - let civil = Civil::from_unix_secs(secs); - let uptime_secs = toyos_abi::clock::nanos_since_boot() / 1_000_000_000; - ( - Some(format!("{}", civil.stem())), - Some(secs.saturating_sub(uptime_secs)), - format!("{civil} at UTC{:+} recovered from two readings", offset_secs / 3_600), - ) - } - wall::Wall::Unknown => { - (None, None, "undated: this machine will not say what time it is".into()) - } - // Named rather than guessed. The two candidates are the same time of day - // on different days, so a file named from either is a day wrong half the - // time; `wall`'s module header is the argument. - wall::Wall::Ambiguous { east, west } => ( - None, - None, - format!( - "undated: the clock is UTC{:+} or UTC{:+} on these two readings and nothing \ - separates them", - east / 3_600, - west / 3_600 - ), - ), - } + let Some(secs) = toyos::system::clock_epoch() else { + return (None, None, "undated: this machine will not say what time it is".into()); + }; + let civil = Civil::from_unix_secs(secs); + let uptime_secs = toyos_abi::clock::nanos_since_boot() / 1_000_000_000; + (Some(format!("{}", civil.stem())), Some(secs.saturating_sub(uptime_secs)), format!("{civil} UTC")) } -/// A line's wall-clock stamp: the local second the machine booted at, plus the +/// A line's wall-clock stamp: the second the machine booted at, plus the /// line's own monotonic offset — which the line carries too, so `/log` holds /// both clocks. -pub(crate) fn stamp(boot_local: Option, at_ns: u64) -> String { - match boot_local { +pub(crate) fn stamp(boot_secs: Option, at_ns: u64) -> String { + match boot_secs { Some(base) => format!("{}", Civil::from_unix_secs(base + at_ns / 1_000_000_000)), // An undated boot writes the space the stamp would have taken, so the // columns line up and nothing has to be re-parsed to notice that a diff --git a/userland/logd/src/serve.rs b/userland/logd/src/serve.rs index 3fdc05151b5..3c6086a0fc4 100644 --- a/userland/logd/src/serve.rs +++ b/userland/logd/src/serve.rs @@ -85,20 +85,20 @@ struct Shared { network: AtomicUsize, local: AtomicUsize, /// The wall clock the boot started at, for a line a reader is owed. - boot_local: Option, + boot_secs: Option, } impl Hub { /// Start serving the network, where the manifest gave this program netd. /// A reader on this machine is handed over by the `log` port's thread /// ([`Hub::read`]). - pub fn start(cap: usize, boot_local: Option) -> Self { + pub fn start(cap: usize, boot_secs: Option) -> Self { let shared = Arc::new(Shared { replay: Mutex::new(Replay::new(cap)), grew: Condvar::new(), network: AtomicUsize::new(0), local: AtomicUsize::new(0), - boot_local, + boot_secs, }); // A row with no `receives` gives this program no namespace, so no netd, // and no thread to learn so on: its exit would be a kernel record at a @@ -333,7 +333,7 @@ fn feed(shared: &Shared, mut sink: impl Write) -> (u64, Left) { } Next::Evicted { lost, at: resume } => { at = resume; - break evicted(shared.boot_local, lost); + break evicted(shared.boot_secs, lost); } Next::CaughtUp => { replay = shared.grew.wait(replay).expect("logd: the replay is poisoned"); @@ -350,11 +350,11 @@ fn feed(shared: &Shared, mut sink: impl Write) -> (u64, Left) { } /// The line a reader gets in place of what the replay no longer holds. -fn evicted(boot_local: Option, lost: u64) -> Vec { +fn evicted(boot_secs: Option, lost: u64) -> Vec { let at_ns = toyos_abi::clock::nanos_since_boot(); let text = format!("logd: the first {lost} bytes of this boot are no longer held here; /log has them"); let tag = Tag::new(LOGD).expect("logd's own name is a tag"); - let stamp = crate::stamp(boot_local, at_ns); + let stamp = crate::stamp(boot_secs, at_ns); let line = ProgramLine { stamp: &stamp, at_ns, diff --git a/userland/logd/src/wall.rs b/userland/logd/src/wall.rs deleted file mode 100644 index 43f0cde900e..00000000000 --- a/userland/logd/src/wall.rs +++ /dev/null @@ -1,132 +0,0 @@ -//! What time it is, in the zone the machine keeps its clock in — recovered -//! from two syscalls rather than asked for with a third. -//! -//! # The problem -//! -//! `/log`'s file names are local timestamps and have been since the kernel -//! wrote them — the naming is identical across the move, so a stick from before -//! this change and one from after sort together. The kernel had -//! `clock::local_secs()`. Userland has two calls and neither is it: -//! -//! - `clock_epoch` — seconds since the Unix epoch, which is **UTC** by -//! definition. A full instant, in the wrong zone. -//! - `clock_realtime` — the **local** time of day as `h:m:s`. The right zone, -//! and no date. -//! -//! Neither alone names a local *date*, and a file called `2026-08-15-…` is one. -//! -//! # The recovery, and exactly how far it is sound -//! -//! The offset is the difference of the two readings' seconds-of-day, which -//! `toyos_wallclock::resolve` computes and this module brackets: -//! -//! ```text -//! usod = epoch mod 86400 // UTC seconds of day -//! lsod = h*3600 + m*60 + s // local seconds of day -//! off = (lsod - usod) mod 86400 // in [0, 86400) -//! ``` -//! -//! **The two readings must be of one instant, so [`local_now`] brackets them**: -//! epoch, then local, then epoch again, retrying while the two epoch reads -//! differ. A second cannot have ticked inside a bracket that closed on the value -//! it opened with, so the subtraction is exact and carries no slop — the -//! quarter-hour granularity that real zone offsets have is corroboration here -//! rather than something the arithmetic leans on. -//! -//! **What `off` pins is the offset modulo 24 hours, and that is not the whole -//! answer.** `lsod` is a time of day, so the two candidates differ by exactly -//! 86,400 seconds, and whether both are real is the question: -//! -//! - `off ∈ [0h, 12h)` — the other candidate, `off − 24h`, is west of UTC−12:00 -//! (Baker Island), which is no zone. **Unique.** -//! - `off ∈ (14h, 24h)` — `off` itself is east of UTC+14:00 (Line Islands), -//! which is no zone, so `off − 24h ∈ (−10h, 0)` is the only one left. -//! **Unique.** -//! - `off ∈ [12h, 14h]` — **both are real.** `+12:00` (New Zealand) against -//! `−12:00`; `+13:00` (Tonga) against `−11:00` (Samoa); `+14:00` (Kiribati) -//! against `−10:00` (Hawaii). Nothing in the two readings separates them, and -//! the two answers are the same time of day on **different days**. -//! -//! The band exists because the real range of offsets is 26 hours wide and a -//! time of day is only 24. That is the day-boundary case stated exactly: a -//! machine at UTC+13 reading local 13:30 on 2026-08-15 and one at UTC−11 -//! reading local 13:30 on 2026-08-14 produce the *same* pair of syscall -//! answers, and a file named from a guess between them is a day wrong. -//! -//! # What this program does about it -//! -//! It refuses. The band answers [`Wall::Ambiguous`] with both candidates named, -//! and `main` falls back to the `unknown-NN.log` name the format already has -//! for a boot that cannot be placed in time — because a file that claims a date -//! it cannot establish is worse than one that says it has none, which is the -//! rule `UNDATED_STEM` was written under in the first place. -//! -//! **Not shipped as a permanent answer.** The clean fix is one field: -//! `SYS_CLOCK_REALTIME` answering a full civil date rather than `h:m:s`, or a -//! call handing back the offset the kernel already holds in `UTC_OFFSET_SECS`. -//! An ABI change is the owner's, so this recovers what is recoverable and -//! refuses the rest rather than guessing on its own authority. -//! -//! The arithmetic and its whole-domain gate are `toyos-wallclock/`, which is a -//! host-workspace member: the argument above is the one thing here that a test -//! inside a guest could not check cheaply, and it is checked on the host at -//! every real offset a quarter-hour apart across the entire range. - -use toyos::system::{clock_epoch, clock_realtime}; -use toyos_wallclock::{resolve, Recovery}; - -/// How many times [`local_now`] re-reads the clock to close its bracket. -/// -/// A bracket fails only when a second ticks between the first epoch read and -/// the second, which is a window two syscalls wide; three attempts is already -/// past what any machine that is running needs, and the bound is here so a -/// machine whose clock is doing something inexplicable cannot spin. -const ATTEMPTS: u32 = 3; - -/// What the machine can be persuaded to say about the wall clock. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Wall { - /// Local seconds since the Unix epoch, and the offset they were recovered - /// with. - Local { secs: u64, offset_secs: i64 }, - /// This machine never said what time it is — no RTC, one that is wedged, or - /// one answering with something that is not a date. It is this for the whole - /// of such a boot rather than intermittently, because the kernel reads the - /// clock once. - Unknown, - /// Two readings naming two real zones a day apart, in seconds east of UTC, - /// so the line that reports the refusal can print both. - Ambiguous { east: i64, west: i64 }, -} - -/// Local seconds since the Unix epoch, now. -pub fn local_now() -> Wall { - for _ in 0..ATTEMPTS { - let Some(before) = clock_epoch() else { return Wall::Unknown }; - let Some(local) = clock_realtime() else { return Wall::Unknown }; - let Some(after) = clock_epoch() else { return Wall::Unknown }; - if before != after { - // A second ticked inside the bracket, so the two readings are not of - // one instant. Take it again rather than subtract them anyway. - continue; - } - let lsod = - local.hours as u64 * 3_600 + local.minutes as u64 * 60 + local.seconds as u64; - // **Two answers and not three.** There used to be a `Recovery::NoZone` - // arm here for "past UTC+14 going east and past UTC−12 going west at - // once", called the middle of the band — and the band has no middle, - // because the band *is* where the two ranges overlap. Their widths sum - // to 26 hours against a day's 24, so every second of the day is placed - // by one of the two arms below; `toyos_wallclock`'s `const` assertion - // is the proof and its whole-domain test is the measurement. - return match resolve(after, lsod) { - Recovery::Offset(offset_secs) => { - Wall::Local { secs: after.saturating_add_signed(offset_secs), offset_secs } - } - Recovery::Ambiguous { east, west } => Wall::Ambiguous { east, west }, - }; - } - // Three brackets in a row that would not close is a clock this program - // cannot read, which has the same answer as one that will not speak. - Wall::Unknown -}