Skip to content

AArch64 stage 4: the boot-timing handoff names the CPU's counter, and the loader hands the count the kernel's clock reads - #657

Merged
Japabu merged 5 commits into
mainfrom
wt/toyos-arm6
Oct 2, 2026
Merged

Japabu merged 5 commits into
mainfrom
wt/toyos-arm6

Conversation

@Japabu

@Japabu Japabu commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Stage 4 of issues/kernel/toyos-runs-on-arm64.md: the boot-timing handoff names the CPU's counter, and the AArch64 loader fills it with the count the kernel's clock reads. This closes issues/kernel/the-boot-timing-handoff-is-named-for-the-tsc.md.

Head 5bf3347, on main at dd87383 (#647).

What changed, per decision

The ABI (toyos-abi/src/boot.rs):

  • loader_entry_tsc and loader_handoff_tsc become loader_entry_counter and loader_handoff_counter, readings of cpu::counter. root_read_tsc becomes root_read_ticks, a span in that counter's ticks.
  • The offsets are unchanged and the offset_of! asserts still pin them. LAYOUT is the struct's size and does not move.

The loader:

  • The tsc() wrapper is deleted. Its doc said the counter "counts from reset", which is not true of the generic timer. Every reading calls arch::counter.
  • Its lines say Loader counter: and counter ticks. src/kernelconsole.rs's fixture of the ROOT-read line follows.

The kernel: report_power_on says "the counter went backwards".

x86-64 is a rename. arch::counter there is RDTSC, which tsc() called, so the loader hands the same bytes at the same offsets. Three lines' text is all that changes on that architecture.

AArch64 at EL2: the loader reads CNTPCT_EL0 (bootloader/src/arch/aarch64.rs, 97bb624).

  • The kernel converts the handoff's counts against its own CNTVCT_EL0.
  • Entered at EL2, the kernel's entry writes CNTVOFF_EL2 zero before its clock starts, so from then on its virtual count equals the physical count.
  • The loader read CNTVCT_EL0 before that write, under whatever offset firmware had left. The Arm ARM resets CNTVOFF_EL2 to an UNKNOWN value.
  • At EL1 the offset is the hypervisor's and the kernel never writes it, so CNTVCT_EL0 is still read there.
  • counter and cpu_as_entered share current_el.

No test is added, and tests/toyos.rs has no diff against main.

  • The first round's guest row, virt_boot_from_power_on, is deleted with its four constants, its judge and its arm (5bf3347).
  • It could not fail on the claim it was added for: with the EL2 arm reverted and nothing else it stays green (f0 below).
  • What it did fail on is the architecture-neutral handoff in kernel/src/main.rs and bootloader/src/main.rs. That behaviour's tier is already named on main: stage A of issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md owes it the metal row boot_from_power_on, the judge 520c0d1 cut from QEMU.
  • The judge survives only in the mutation patches of the check below.

The merge (7192287). main's cut of the guest suite (520c0d1) deleted the x86-64 guest test boot_from_power_on, which this branch had changed, and those changes went with it. In bootloader/src/main.rs the branch's side is the rename on main's start_kernel.

Issues:

  • The closed file is deleted with its citations, two in the track and one in issues/kernel/portable-kernel-code-names-the-tsc.md.
  • issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md names root_read_ticks.
  • The track's stage 4 is otherwise main's. 91b4b08 had filed the loader's EL2 read as owed its red beside the entry's three EL2 writes; 5bf3347 takes that sentence out. The exit main wrote there has two arms, and the second, "a loader that writes the opposite values before the handoff", is f1 and f2 below: the red is shown here, so nothing is owed.

Lines against main (git diff --shortstat origin/main...5bf33470c): 11 files, +67 −83. Production +64 −55, a host-test fixture +1 −1, tests 0, issues +2 −27.

Gates, all at 5bf3347

Logs are in /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/657-r2/; summary-gates.txt, summary-guest.txt and summary-mutations.txt carry each exit with the host's load, which ran between 4 and 12 on 14 cores.

Command Exit Log
cargo run -- --build-only 0 build-x86.log
cargo run -- --arch aarch64 --build-only 0 build-a64.log
cargo run -- --clippy 0 (17 invocations clean) clippy.log
cargo run -- --ci host 0 (66 steps, all green) ci-host.log
cargo test --test toyos-build -- virt_ --nocapture 0 (19 passed) guest-virt.log
cargo test --test toyos-build -- --nocapture (the whole guest suite) 0 (25 passed) guest-whole.log

Check: the loader's count at EL2, against an offset firmware may leave

The handoff is the loader-to-kernel boundary of the ABI. The claim 97bb624 makes is that the count the loader hands is in the kernel's frame whatever CNTVOFF_EL2 holds at the loader's entry.

Each mutation is one checked patch on the clean head: the deleted row (judge.patch: registration, constants, judge, arm, as 91b4b08 had them) and a change to bootloader/src/arch/aarch64.rs. Each is run as cargo test --test toyos-build -- virt_boot_from_power_on --nocapture and taken back, the tree clean after each. The patches and the script are in the newest mutation comment below. Logs are mut-<name>.log; every one rebuilt the loader, and the red is the judge's own line.

Mutation Exit What the row read
f1: an hour in CNTVOFF_EL2 around each loader reading at EL2, the loader as it stands 0 boot: power-on to loader 633 ms, loader 167 ms (ROOT read 8 ms), kernel to Boot: complete 1563 ms
f2: f1 with the EL2 arm reading CNTVCT_EL0 1 boot: the counter went backwards: 3600631144000 at the loader's entry, 3600799116000 at its handoff, 2362285000 at Boot: complete
f0: 97bb624's counter reverted and nothing staged 0 boot: power-on to loader 638 ms, loader 168 ms (ROOT read 8 ms), kernel to Boot: complete 1563 ms
r0: cpu_as_entered also prints mrs cntvoff_el2, nothing written 0 CPU: entered at EL2, HCR_EL2.E2H 0, CNTVOFF_EL2 0x0, ID_AA64MMFR4_EL1.E2H0 0x0: the kernel's entry writes E2H clear
  • f1 and f2 are the negative control. f1 is the base the green arm is measured on, carrying an offset the Arm ARM lets firmware leave. f2 is the change reverted onto it. Both mask interrupts for the reading and put the offset back after it, so firmware's timer never sees it. f2 keeps current_el, which the staging itself needs; every behaviour 97bb624 changed is reverted in it.
  • f0 is green because this machine's firmware leaves no offset. r0 reads CNTVOFF_EL2 as 0x0 in cpu_as_entered, which start_kernel calls beside the loader's handoff reading and not at its entry, under the VirtEl2 profile; the loader writes the register nowhere, so there the two counts are one and a plain revert shows nothing. That is why the control stages the offset.

Oracle.

  • The Arm ARM defines CNTVCT_EL0 as the physical count minus CNTVOFF_EL2 and resets CNTVOFF_EL2 to an UNKNOWN value. QEMU's TCG is a third party's implementation of that definition, and f2's red is its model applying the offset.
  • The loader's own line. The loader prints its raw counts before the handoff, and the kernel converts the fields it received at a rate it states itself. The rate is CNTFRQ_EL0, which the Arm ARM makes firmware's to program.

The T14: nothing is requested

No METAL row reaches this diff.

  • boot_from_power_on is not a METAL row on main. 520c0d1 cut the guest test, and stage A of issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md lists it as owed a metal row. git grep -n boot_from_power_on -- tests src at the head answers nothing.
  • No judge reads a line or a field the diff changes. git grep -n -E 'Loader (TSC|counter)|TSC cycles|counter ticks|power-on to loader|went backwards|loader_(entry|handoff)_|root_read_' -- tests src answers src/kernelconsole.rs's host fixture, and the wall clock's and two censuses' own "went backwards" lines.
  • On x86-64 the handed bytes are unchanged, as above.

What I am unsure of

  • The handoff has no standing test on any tier, on either architecture. That is main's state since 520c0d1 and the guest-suite track owns it. The metal row it owes runs the architecture-neutral source both architectures share. The AArch64 loader's EL2 arm is held by nothing standing: it was shown red once, by f2, with a judge that is not in the tree.
  • One x86-64 reading by hand stands in for that row here. An x86-64 boot in guest-whole.log ([serial 1]) printed Loader counter: 2193977000 at entry, 2671751000 at the handoff, TSC: 999MHz (period=1000400fs, a ROOT read in 33459000 counter ticks, and boot: power-on to loader 2194 ms, loader 477 ms (ROOT read 33 ms). bc makes those counts 2194, 477 and 33 ms at that period.
  • Two commit messages on the branch say what the record no longer does. 97bb624's says f1 and f2 were measured; they were not until 7192287. 91b4b08's says the track owes the EL2 read its red; 5bf3347's and this body say why it does not.

🤖 Generated with Claude Code

https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm

Japabu and others added 4 commits October 1, 2026 04:03
… virt judges it

Closes issues/kernel/the-boot-timing-handoff-is-named-for-the-tsc.md, the
stage-4 item that waited on an ABI brief; the owner's ruling that the ABI is
completely unstable lifts that wait.

KernelArgs carried loader_entry_tsc, loader_handoff_tsc and root_read_tsc, and
the kernel's report said "the TSC went backwards". On AArch64 both loaders'
readings come from arch::counter, CNTVCT_EL0 there, so the ABI and the report
named a counter the machine does not have.

- toyos-abi: the fields are loader_entry_counter, loader_handoff_counter
  (readings of the CPU's counter) and root_read_ticks (a span in its ticks).
  Same offsets; the offset_of! asserts still hold them.
- The loader: its tsc() wrapper, whose doc said the counter "counts from
  reset", goes; every reading calls arch::counter. Its lines say "Loader
  counter:" and "counter ticks". src/kernelconsole.rs's fixture of the
  ROOT read line follows.
- The kernel: report_power_on says "the counter went backwards".
- tests: boot_from_power_on takes the guest's Arch and reads the rate off
  that architecture's clock record: x86-64's TSC period, AArch64's
  CNTFRQ_EL0 rate divided into a second as the kernel divides it. A
  "counter went backwards" line is its own refusal. virt_boot_from_power_on
  (VirtEl2) runs the same judge on AArch64, where the loader reads the
  generic timer at EL2 and both binaries speak on the PL011.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
… kernel's clock reads

KernelArgs' counter readings are converted by the kernel against its own
cpu::counter, CNTVCT_EL0. Entered at EL2, the kernel's entry writes
CNTVOFF_EL2 zero before its clock starts, so from then on its virtual count is
the physical count. The loader read CNTVCT_EL0 before that write, under
whatever offset firmware left; the Arm ARM resets CNTVOFF_EL2 to an UNKNOWN
value. The loader's readings and the kernel's were in one frame only where
firmware left the offset zero.

At EL2 the loader now reads CNTPCT_EL0, which EL2 may always read and which no
offset moves. At EL1 the offset is the hypervisor's, the kernel never writes
it, and CNTVCT_EL0 stays the right count. cpu_as_entered and counter share
current_el.

Shown by virt_boot_from_power_on under a mutation that puts an hour's offset
in CNTVOFF_EL2 around each loader reading and takes it back after, interrupts
masked so firmware's timer never sees it: green with this change, and red
("the counter went backwards") with the EL2 arm reading CNTVCT_EL0 again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Two conflicts.

bootloader/src/main.rs: main's start_kernel takes no layout and a RootImage
that is always there; the branch's side is the rename alone, entry_counter and
root_read_ticks, on main's signature, handoff and call.

tests/toyos.rs: main's cut of the guest suite (520c0d1) deleted the x86-64
guest test boot_from_power_on, its MACHINE_TESTS entry and comment, its
run_machine_test arm and its judge; the guest-suite track owes it a metal row.
The branch had changed all four. Hunk by hunk of the branch's side:
- the SCREEN_TESTS row virt_boot_from_power_on: kept, in main's two-field shape;
- the MACHINE_TESTS comment and the run_machine_test arm passing Arch::X86_64:
  gone with the test main deleted;
- the run_screen_test arm virt_boot_from_power_on: kept;
- LOADER_COUNTER, POWER_ON, COUNTER_BACKWARDS, GENERIC_TIMER_HZ and the judge:
  kept for the virt row, AArch64's alone. TSC_PERIOD, the Arch parameter, the
  x86-64 period arm and the IA32_TSC_ADJUST tail had the deleted test as their
  only caller and go with it. A refusal carries the serial it read, as the
  other virt rows' do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…he entry's three writes

Measured at 7192287 with bootloader/src/arch/aarch64.rs's counter put back
to 38d2886's, one read of CNTVCT_EL0 at either level: `cargo test --test
toyos-build -- virt_boot_from_power_on --nocapture` exits 0. QEMU's firmware
hands the loader EL2 with CNTVOFF_EL2 zero, so the two counts read the same
there and the row cannot tell which the loader took.

97bb624's message says the arm is "shown by virt_boot_from_power_on under a
mutation"; that pair was not run until 7192287, where it reads: an hour in
CNTVOFF_EL2 around each loader reading exits 0 as the loader stands, and exits
1 on "boot: the counter went backwards" with the EL2 arm reading CNTVCT_EL0.
A mutation is no standing test, so stage 4's owed list takes the arm, with the
exit the entry's writes already have there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Mutation patches for the head 91b4b08, each applied with git apply --check and git apply on the clean head, run as cargo test --test toyos-build -- virt_boot_from_power_on --nocapture, and taken back with git apply -R, the tree clean after each (mutate.sh, below). The exits are in the body.

f0-el2-reads-cntvct.patch (sha256 738d97d2885fa0e27ef931eccd0fe7dbd7b3f3765e9e29b1e53ea20bf3bc50c5)
diff --git a/bootloader/src/arch/aarch64.rs b/bootloader/src/arch/aarch64.rs
index 6a30fa656..a85bd2560 100644
--- a/bootloader/src/arch/aarch64.rs
+++ b/bootloader/src/arch/aarch64.rs
@@ -15,29 +15,12 @@ pub fn typing(write_back: &[(u64, u64)]) -> Typing<'_> {
     Typing::ByMap(write_back)
 }
 
-/// The exception level the loader runs at, from `CurrentEL`.
-fn current_el() -> u64 {
-    let current: u64;
-    // SAFETY: reads `CurrentEL`, which EL1 and above may read.
-    unsafe { core::arch::asm!("mrs {}, currentel", out(reg) current, options(nomem, nostack, preserves_flags)) };
-    (current >> 2) & 0b11
-}
-
-/// The count the kernel's clock reads, its virtual count. At EL2 that is the
-/// physical count, `CNTPCT_EL0`, because the kernel's entry writes
-/// `CNTVOFF_EL2` zero, which resets UNKNOWN; at EL1 the offset is the
-/// hypervisor's for good and `CNTVCT_EL0` is read.
+/// The generic timer's virtual count, `CNTVCT_EL0`.
 pub fn counter() -> u64 {
     let count: u64;
-    if current_el() == 2 {
-        // SAFETY: reads a counter EL2 may always read; the `ISB` keeps the
-        // read in program order.
-        unsafe { core::arch::asm!("isb", "mrs {}, cntpct_el0", out(reg) count, options(nomem, nostack, preserves_flags)) };
-    } else {
-        // SAFETY: reads a counter EL1 may always read; the `ISB` keeps the
-        // read in program order.
-        unsafe { core::arch::asm!("isb", "mrs {}, cntvct_el0", out(reg) count, options(nomem, nostack, preserves_flags)) };
-    }
+    // SAFETY: reads a counter EL1 and EL2 may always read; the `ISB` keeps the
+    // read in program order.
+    unsafe { core::arch::asm!("isb", "mrs {}, cntvct_el0", out(reg) count, options(nomem, nostack, preserves_flags)) };
     count
 }
 
@@ -48,7 +31,10 @@ pub fn counter() -> u64 {
 /// the console still prints; the entry's read-back of `HCR_EL2` stays as the
 /// last line of defence.
 pub fn cpu_as_entered() -> Result<Option<alloc::string::String>, alloc::string::String> {
-    let el = current_el();
+    let current: u64;
+    // SAFETY: reads `CurrentEL`, which EL1 and above may read.
+    unsafe { core::arch::asm!("mrs {}, currentel", out(reg) current, options(nomem, nostack, preserves_flags)) };
+    let el = (current >> 2) & 0b11;
     if el != 2 {
         return Ok(Some(alloc::format!("CPU: entered at EL{el}")));
     }
f1-firmware-offset.patch (sha256 347be4c5cdd1d1087bf845f0b8696a76b07b6d63703f46b881eadc0543b52195)
diff --git a/bootloader/src/arch/aarch64.rs b/bootloader/src/arch/aarch64.rs
index 6a30fa656..2951d96fb 100644
--- a/bootloader/src/arch/aarch64.rs
+++ b/bootloader/src/arch/aarch64.rs
@@ -30,9 +30,43 @@ fn current_el() -> u64 {
 pub fn counter() -> u64 {
     let count: u64;
     if current_el() == 2 {
+        // MUTATION: firmware's offset, an hour ahead, in CNTVOFF_EL2 for this
+        // reading alone, with every interrupt masked so firmware's timer never
+        // sees it; the offset and the mask are put back after the reading.
+        let (daif, offset): (u64, u64);
+        // SAFETY: a mutation; EL2 owns both registers it writes and restores.
+        unsafe {
+            core::arch::asm!(
+                "mrs {daif}, daif",
+                "msr daifset, #0xf",
+                "mrs {offset}, cntvoff_el2",
+                "mrs {hour}, cntfrq_el0",
+                "mov {secs}, #3600",
+                "mul {hour}, {hour}, {secs}",
+                "neg {hour}, {hour}",
+                "msr cntvoff_el2, {hour}",
+                "isb",
+                daif = out(reg) daif,
+                offset = out(reg) offset,
+                hour = out(reg) _,
+                secs = out(reg) _,
+                options(nomem, nostack, preserves_flags),
+            )
+        };
         // SAFETY: reads a counter EL2 may always read; the `ISB` keeps the
         // read in program order.
         unsafe { core::arch::asm!("isb", "mrs {}, cntpct_el0", out(reg) count, options(nomem, nostack, preserves_flags)) };
+        // SAFETY: puts back what the block above read.
+        unsafe {
+            core::arch::asm!(
+                "msr cntvoff_el2, {offset}",
+                "isb",
+                "msr daif, {daif}",
+                offset = in(reg) offset,
+                daif = in(reg) daif,
+                options(nomem, nostack, preserves_flags),
+            )
+        };
     } else {
         // SAFETY: reads a counter EL1 may always read; the `ISB` keeps the
         // read in program order.
f2-firmware-offset-unfixed.patch (sha256 0f2e50dc168e7e0c9149b1c6b5b0023644e85f421a524933a16768d3a0180d25)
diff --git a/bootloader/src/arch/aarch64.rs b/bootloader/src/arch/aarch64.rs
index 6a30fa656..cba7fa7d9 100644
--- a/bootloader/src/arch/aarch64.rs
+++ b/bootloader/src/arch/aarch64.rs
@@ -30,9 +30,43 @@ fn current_el() -> u64 {
 pub fn counter() -> u64 {
     let count: u64;
     if current_el() == 2 {
+        // MUTATION: firmware's offset, an hour ahead, in CNTVOFF_EL2 for this
+        // reading alone, with every interrupt masked so firmware's timer never
+        // sees it; the offset and the mask are put back after the reading.
+        let (daif, offset): (u64, u64);
+        // SAFETY: a mutation; EL2 owns both registers it writes and restores.
+        unsafe {
+            core::arch::asm!(
+                "mrs {daif}, daif",
+                "msr daifset, #0xf",
+                "mrs {offset}, cntvoff_el2",
+                "mrs {hour}, cntfrq_el0",
+                "mov {secs}, #3600",
+                "mul {hour}, {hour}, {secs}",
+                "neg {hour}, {hour}",
+                "msr cntvoff_el2, {hour}",
+                "isb",
+                daif = out(reg) daif,
+                offset = out(reg) offset,
+                hour = out(reg) _,
+                secs = out(reg) _,
+                options(nomem, nostack, preserves_flags),
+            )
+        };
         // SAFETY: reads a counter EL2 may always read; the `ISB` keeps the
         // read in program order.
-        unsafe { core::arch::asm!("isb", "mrs {}, cntpct_el0", out(reg) count, options(nomem, nostack, preserves_flags)) };
+        unsafe { core::arch::asm!("isb", "mrs {}, cntvct_el0", out(reg) count, options(nomem, nostack, preserves_flags)) };
+        // SAFETY: puts back what the block above read.
+        unsafe {
+            core::arch::asm!(
+                "msr cntvoff_el2, {offset}",
+                "isb",
+                "msr daif, {daif}",
+                offset = in(reg) offset,
+                daif = in(reg) daif,
+                options(nomem, nostack, preserves_flags),
+            )
+        };
     } else {
         // SAFETY: reads a counter EL1 may always read; the `ISB` keeps the
         // read in program order.
m1-kernel-reads-entry-as-handoff.patch (sha256 a487235c2136c45ecdafa337616afb2fb972178be7fb8572b827dd05d231f705)
diff --git a/kernel/src/main.rs b/kernel/src/main.rs
index 0534567c8..77051b4fd 100644
--- a/kernel/src/main.rs
+++ b/kernel/src/main.rs
@@ -183,7 +183,7 @@ fn register_gpu(driver: Box<dyn gpu::Gpu>, info: gpu::GpuInfo) {
 /// the counter started.
 fn report_power_on(args: &KernelArgs, complete: u64) {
     arch::boot::report_counter_origin();
-    let (entry, handoff) = (args.loader_entry_counter, args.loader_handoff_counter);
+    let (entry, handoff) = (args.loader_entry_counter, args.loader_entry_counter);
     if handoff < entry || complete < handoff {
         log!(
             "boot: the counter went backwards: {entry} at the loader's entry, {handoff} at its handoff, \
m2-loader-hands-no-handoff.patch (sha256 3e63279bd71ea4893cce115b11fa9ca58f943b7d06f07266da4ce3392be5b1ec)
diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs
index a49c0663f..ebe03ca8e 100644
--- a/bootloader/src/main.rs
+++ b/bootloader/src/main.rs
@@ -654,10 +654,9 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec<u8>, cmdline: v
         mem::size_of::<KernelArgs>() as u64,
     );
 
-    kernel_args.loader_handoff_counter = arch::counter();
     println!(
         "Loader counter: {entry_counter} at entry, {} at the handoff",
-        kernel_args.loader_handoff_counter,
+        arch::counter(),
     );
 
     // Last, and after every line above: a console write, a FAT write and a
n0-whole-revert.patch (sha256 dcef4d2199aa70d29ac629b826dd71c032044d6ebc649d3c8001fd87cd371530)
diff --git a/bootloader/src/arch/aarch64.rs b/bootloader/src/arch/aarch64.rs
index 67b935439..0c98d7591 100644
--- a/bootloader/src/arch/aarch64.rs
+++ b/bootloader/src/arch/aarch64.rs
@@ -15,29 +15,12 @@ pub fn typing(write_back: &[(u64, u64)]) -> Typing<'_> {
     Typing::ByMap(write_back)
 }
 
-/// The exception level the loader runs at, from `CurrentEL`.
-fn current_el() -> u64 {
-    let current: u64;
-    // SAFETY: reads `CurrentEL`, which EL1 and above may read.
-    unsafe { core::arch::asm!("mrs {}, currentel", out(reg) current, options(nomem, nostack, preserves_flags)) };
-    (current >> 2) & 0b11
-}
-
-/// The count the kernel's clock reads, its virtual count. At EL2 that is the
-/// physical count, `CNTPCT_EL0`, because the kernel's entry writes
-/// `CNTVOFF_EL2` zero, which resets UNKNOWN; at EL1 the offset is the
-/// hypervisor's for good and `CNTVCT_EL0` is read.
+/// The generic timer's virtual count, `CNTVCT_EL0`.
 pub fn counter() -> u64 {
     let count: u64;
-    if current_el() == 2 {
-        // SAFETY: reads a counter EL2 may always read; the `ISB` keeps the
-        // read in program order.
-        unsafe { core::arch::asm!("isb", "mrs {}, cntpct_el0", out(reg) count, options(nomem, nostack, preserves_flags)) };
-    } else {
-        // SAFETY: reads a counter EL1 may always read; the `ISB` keeps the
-        // read in program order.
-        unsafe { core::arch::asm!("isb", "mrs {}, cntvct_el0", out(reg) count, options(nomem, nostack, preserves_flags)) };
-    }
+    // SAFETY: reads a counter EL1 and EL2 may always read; the `ISB` keeps the
+    // read in program order.
+    unsafe { core::arch::asm!("isb", "mrs {}, cntvct_el0", out(reg) count, options(nomem, nostack, preserves_flags)) };
     count
 }
 
@@ -48,7 +31,10 @@ pub fn counter() -> u64 {
 /// the console still prints; the entry's read-back of `HCR_EL2` stays as the
 /// last line of defence.
 pub fn cpu_as_entered() -> Result<Option<alloc::string::String>, alloc::string::String> {
-    let el = current_el();
+    let current: u64;
+    // SAFETY: reads `CurrentEL`, which EL1 and above may read.
+    unsafe { core::arch::asm!("mrs {}, currentel", out(reg) current, options(nomem, nostack, preserves_flags)) };
+    let el = (current >> 2) & 0b11;
     if el != 2 {
         return Ok(Some(alloc::format!("CPU: entered at EL{el}")));
     }
diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs
index a93546df5..d118e05c2 100644
--- a/bootloader/src/main.rs
+++ b/bootloader/src/main.rs
@@ -499,8 +499,13 @@ fn report_reach(what: &str, at: u64, len: u64) {
     );
 }
 
+/// The CPU's free-running counter, which counts from reset.
+fn tsc() -> u64 {
+    arch::counter()
+}
+
 #[allow(clippy::too_many_arguments)]
-fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec<u8>, cmdline: vec::Vec<u8>, rsdp_addr: u64, gop: Option<GopInfo>, boot_part: Option<BootPartition>, log_partition_guid: [u8; 16], root_image: rootimage::RootImage, entry_counter: u64, system_table: SystemTable<Boot>) -> ! {
+fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec<u8>, cmdline: vec::Vec<u8>, rsdp_addr: u64, gop: Option<GopInfo>, boot_part: Option<BootPartition>, log_partition_guid: [u8; 16], root_image: rootimage::RootImage, entry_tsc: u64, system_table: SystemTable<Boot>) -> ! {
     // Said before it is refused, for `report_reach`'s reason.
     match arch::cpu_as_entered() {
         Ok(None) => {}
@@ -592,7 +597,7 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec<u8>, cmdline: v
             None => ([0u8; 16], 0, 0, 0),
         };
 
-    let (root_image_addr, root_image_len, root_partition_guid, root_read_ticks) = root_image.handoff();
+    let (root_image_addr, root_image_len, root_partition_guid, root_read_tsc) = root_image.handoff();
 
     // Built before the exit so the address the kernel is handed is one this
     // loader can still print and refuse on.
@@ -628,9 +633,9 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec<u8>, cmdline: v
         root_image_addr,
         root_image_len,
         root_partition_guid,
-        loader_entry_counter: entry_counter,
-        loader_handoff_counter: 0,
-        root_read_ticks,
+        loader_entry_tsc: entry_tsc,
+        loader_handoff_tsc: 0,
+        root_read_tsc,
     };
     report_reach(
         "Kernel arguments",
@@ -638,10 +643,10 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec<u8>, cmdline: v
         mem::size_of::<KernelArgs>() as u64,
     );
 
-    kernel_args.loader_handoff_counter = arch::counter();
+    kernel_args.loader_handoff_tsc = tsc();
     println!(
-        "Loader counter: {entry_counter} at entry, {} at the handoff",
-        kernel_args.loader_handoff_counter,
+        "Loader TSC: {entry_tsc} at entry, {} at the handoff",
+        kernel_args.loader_handoff_tsc,
     );
 
     // Last, and after every line above: a console write, a FAT write and a
@@ -745,8 +750,8 @@ fn end_this_pass(system_table: &SystemTable<Boot>, exit_event: Option<Event>) ->
 
 #[entry]
 fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
-    // First, so this reading is firmware's time and none of the loader's.
-    let entry_counter = arch::counter();
+    // First: the TSC counts from reset, so this is what firmware took.
+    let entry_tsc = tsc();
     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
@@ -956,5 +961,5 @@ fn main(handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
     watchdog::arm(&system_table, rsdp_addr, params);
 
     println!("Starting kernel...");
-    start_kernel(loaded_kernel, kernel_bytes, cmdline, rsdp_addr, gop, boot_part, log_guid, chosen.root, entry_counter, system_table);
+    start_kernel(loaded_kernel, kernel_bytes, cmdline, rsdp_addr, gop, boot_part, log_guid, chosen.root, entry_tsc, system_table);
 }
diff --git a/bootloader/src/rootimage.rs b/bootloader/src/rootimage.rs
index c0f93626e..b511b67f1 100644
--- a/bootloader/src/rootimage.rs
+++ b/bootloader/src/rootimage.rs
@@ -58,15 +58,15 @@ pub struct RootImage {
     len: u64,
     /// The partition it was read from, raw as in its GPT entry.
     partition: [u8; 16],
-    /// The counter ticks the read took.
-    ticks: u64,
+    /// The TSC cycles the read took.
+    cycles: u64,
 }
 
 impl RootImage {
     /// Where the kernel is told the image is, which partition it came from, and
-    /// the counter ticks reading it took.
+    /// the cycles reading it took.
     pub fn handoff(&self) -> (u64, u64, [u8; 16], u64) {
-        (self.at, self.len, self.partition, self.ticks)
+        (self.at, self.len, self.partition, self.cycles)
     }
 
     /// The image's bytes, for the hash its slot's header names.
@@ -222,10 +222,10 @@ impl<'a> Disk<'a> {
         // Chunks are whole `BLOCK`s from a page-aligned buffer, so each one
         // keeps the `IoAlign` `open` checked against `BLOCK`.
         let chunk = chunk::chunk_bytes(CHUNK_BOUND, BLOCK, self.lba_bytes, granularity.unwrap_or(0));
-        let began = crate::arch::counter();
+        let began = crate::tsc();
         let mut device = Firmware { io: &self.io, media_id: self.media_id };
         let read = chunk::read(&mut device, part.first_lba(), self.lba_bytes, chunk, into);
-        let image = RootImage { at, len, partition: part.unique_guid().0, ticks: crate::arch::counter().wrapping_sub(began) };
+        let image = RootImage { at, len, partition: part.unique_guid().0, cycles: crate::tsc().wrapping_sub(began) };
         if let Err(failed) = read {
             let why = alloc::format!(
                 "the read of {} blocks at LBA {} failed: {:?}, after {} of {len} bytes read",
@@ -238,14 +238,14 @@ impl<'a> Disk<'a> {
             return Err(why);
         }
         println!(
-            "{READ_AT} {at:#x}+{len:#x} from LBA {}+{}, {chunk} bytes a request (optimal granularity: {}), in {} counter ticks",
+            "{READ_AT} {at:#x}+{len:#x} from LBA {}+{}, {chunk} bytes a request (optimal granularity: {}), in {} TSC cycles",
             part.first_lba(),
             len / u64::from(self.lba_bytes),
             match granularity {
                 Some(lbas) => alloc::format!("{lbas} block(s)"),
                 None => String::from("not reported"),
             },
-            image.ticks
+            image.cycles
         );
         Ok(image)
     }
diff --git a/bootloader/src/slot.rs b/bootloader/src/slot.rs
index e5130ef5f..fd072b7c6 100644
--- a/bootloader/src/slot.rs
+++ b/bootloader/src/slot.rs
@@ -145,9 +145,9 @@ fn verify(
         println!("{HEAD} {letter}: ROOT: {why}");
         Refusal::Unreadable("root")
     })?;
-    let began = crate::arch::counter();
+    let began = crate::tsc();
     let root_hash = toyos_update::sha256(root.bytes());
-    println!("{HEAD} {letter}: ROOT hashed in {} counter ticks", crate::arch::counter().wrapping_sub(began));
+    println!("{HEAD} {letter}: ROOT hashed in {} TSC cycles", crate::tsc().wrapping_sub(began));
     if root_hash != header.root().sha256 {
         root.free(bs);
         return Err(Refusal::Hash("root"));
diff --git a/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md b/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md
index 1c3aec6e7..cb9922628 100644
--- a/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md
+++ b/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md
@@ -188,7 +188,7 @@ Each stage lands on its own, in this order.
    `kernel_args_last_layout_refused` boots with
    `loader-writes-the-last-layout` and finds the kernel's refusal naming both
    words before any `black box:` record. Moving `layout` after
-   `root_read_ticks` fails to build, and so does padding `KernelArgs` back to
+   `root_read_tsc` fails to build, and so does padding `KernelArgs` back to
    1272 bytes: `size_of::<KernelArgs>()` is then stage 1's size again, the
    derived `LAYOUT` collapses onto the literal `LAST_LAYOUT`
    (`0x5459_0000 | 1272`), and `assert!(LAYOUT != LAST_LAYOUT)` fails the
diff --git a/issues/kernel/portable-kernel-code-names-the-tsc.md b/issues/kernel/portable-kernel-code-names-the-tsc.md
index 251f599c8..d2c1ea18d 100644
--- a/issues/kernel/portable-kernel-code-names-the-tsc.md
+++ b/issues/kernel/portable-kernel-code-names-the-tsc.md
@@ -12,6 +12,8 @@ portable kernel still calls it the TSC: `kernel/src/clock.rs`'s
 `AT_TSC`, and the xHCI driver's, `hardlockup`'s and `panic_reboot`'s waits
 and comments read it by that name. `clock::counter_ticks`, which the AArch64
 timer reads, is renamed; the rest reads as x86-64's on both machines.
+`issues/kernel/the-boot-timing-handoff-is-named-for-the-tsc.md` is the ABI's
+half of the same name.
 
 **Exit condition**: no item or comment outside `kernel/src/arch/x86_64/`
 names the TSC for the counter `crate::arch::cpu::counter` reads.
diff --git a/issues/kernel/the-boot-timing-handoff-is-named-for-the-tsc.md b/issues/kernel/the-boot-timing-handoff-is-named-for-the-tsc.md
new file mode 100644
index 000000000..8da9acbaa
--- /dev/null
+++ b/issues/kernel/the-boot-timing-handoff-is-named-for-the-tsc.md
@@ -0,0 +1,21 @@
+---
+status: open
+kind: defect
+opened: 2026-09-26
+---
+
+# The boot-timing handoff is named for the TSC
+
+`toyos-abi/src/boot.rs`'s `KernelArgs` carries `loader_entry_tsc` and
+`loader_handoff_tsc`, and `kernel/src/main.rs`'s `report_power_on` reports
+"the TSC went backwards". On AArch64 the loader fills both from `CNTVCT_EL0`,
+the generic timer's count, so the ABI and the message name a counter the
+machine does not have.
+
+Owned by stage 4 of `issues/kernel/toyos-runs-on-arm64.md`, which makes the
+generic timer the clock. `KernelArgs` is the ABI, so the rename lands with
+that stage's other ABI work.
+
+**Exit condition**: the two fields and the report name the CPU's counter
+(`cpu::counter`) rather than the TSC, and both architectures' loaders fill
+them.
diff --git a/issues/kernel/toyos-runs-on-arm64.md b/issues/kernel/toyos-runs-on-arm64.md
index dad275173..b850705f6 100644
--- a/issues/kernel/toyos-runs-on-arm64.md
+++ b/issues/kernel/toyos-runs-on-arm64.md
@@ -201,6 +201,7 @@ before any aarch64 file exists, with x86 as its only user:
 Each is its own issue, owned by the stage that removes it:
 
 - `issues/kernel/msi-and-pin-routing-take-an-x86-vector-and-apic-id.md` (stage 6)
+- `issues/kernel/the-boot-timing-handoff-is-named-for-the-tsc.md` (stage 4)
 - `issues/kernel/the-crash-evidence-records-x86-fault-registers.md` (stage 5)
 - `issues/kernel/the-aarch64-kernel-builds-with-dead-code-allowed.md` (stage 7)
 
@@ -261,16 +262,14 @@ Each stage names its exit; "measured" means a number from a run.
    exposes no RNDR and the kernel's hash seed refuses there until stage 6's
    virtio-rng. Each judges an event, never a rate: no QEMU test measures time.
    Owed before the exit holds: the interrupts-off window against x86's, a
-   measurement only metal can make, with no instrument on either arch yet; the
+   measurement only metal can make, with no instrument on either arch yet;
+   `issues/kernel/the-boot-timing-handoff-is-named-for-the-tsc.md`; the
    instruction-cache maintenance before a mapping is executable
    (`cache::make_executable`), the break-before-make ordering of a live
    entry's replacement, and the TLB flush before a reclaimed ASID is issued
    again, which QEMU's TCG, the only oracle this stage has, cannot fail on:
    the first HVF run, once stage 6 gives HVF its RNDR, is their exit; and the
-   three deletions shown red, and with them the loader's read of `CNTPCT_EL0`
-   at EL2 (`bootloader/src/arch/aarch64.rs`'s `counter`): with that arm
-   reading `CNTVCT_EL0`, `virt_boot_from_power_on` stays green, because
-   QEMU's firmware leaves `CNTVOFF_EL2` zero. They are shown red on a machine whose
+   three deletions shown red. They are shown red on a machine whose
    firmware leaves the registers otherwise, or by a loader that writes the
    opposite values before the handoff. The ITS moves to stage 6: a claimed
    function is its only consumer the small-kernel track leaves, and it needs that
diff --git a/kernel/src/main.rs b/kernel/src/main.rs
index 7d2a6a32b..50d976831 100644
--- a/kernel/src/main.rs
+++ b/kernel/src/main.rs
@@ -167,15 +167,15 @@ fn register_gpu(driver: Box<dyn gpu::Gpu>, info: gpu::GpuInfo) {
     gpu::register(driver, info);
 }
 
-/// The boot from power-on, off the loader's [`cpu::counter`] readings and
-/// `complete`'s, at the clock's rate. The first span is firmware's time since
-/// the counter started.
+/// 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.
 fn report_power_on(args: &KernelArgs, complete: u64) {
     arch::boot::report_counter_origin();
-    let (entry, handoff) = (args.loader_entry_counter, args.loader_handoff_counter);
+    let (entry, handoff) = (args.loader_entry_tsc, args.loader_handoff_tsc);
     if handoff < entry || complete < handoff {
         log!(
-            "boot: the counter went backwards: {entry} at the loader's entry, {handoff} at its handoff, \
+            "boot: the TSC went backwards: {entry} at the loader's entry, {handoff} at its handoff, \
              {complete} at Boot: complete"
         );
         return;
@@ -185,7 +185,7 @@ fn report_power_on(args: &KernelArgs, complete: u64) {
         "boot: power-on to loader {} ms, loader {} ms (ROOT read {} ms), kernel to Boot: complete {} ms",
         ms(entry),
         ms(handoff - entry),
-        ms(args.root_read_ticks),
+        ms(args.root_read_tsc),
         ms(complete - handoff),
     );
 }
@@ -536,9 +536,9 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! {
     }
 
     report_log_destination();
-    let complete = cpu::counter();
+    let complete_tsc = cpu::counter();
     boot_phase!("complete", 0);
-    report_power_on(kernel_args, complete);
+    report_power_on(kernel_args, complete_tsc);
 
     #[cfg(feature = "boot-actuators")]
     if actuator::test_late_panic() {
diff --git a/src/kernelconsole.rs b/src/kernelconsole.rs
index e23b86349..f3c15fbc1 100644
--- a/src/kernelconsole.rs
+++ b/src/kernelconsole.rs
@@ -57,7 +57,7 @@ mod tests {
          \"UEFI QEMU QEMU USB HARDDRIVE TOYOS0BOOTSTICK1\" from PciRoot(0x0)/Pci(0x1,0x0)/USB(0x0,0x0)\n\
          ToyOS Bootloader 1.0\n\
          ROOT: read into memory at 0x7c894000+0x800000 from LBA 212992+16384, 1048576 bytes a request \
-         (optimal granularity: not reported), in 22519000 counter ticks\n\
+         (optimal granularity: not reported), in 22519000 TSC cycles\n\
          Loader log: the kernel handoff begins, so ";
 
     const KERNEL: &str = "[kernel 0.000 cpu0 boot] black box: 0x8000000 is this boot's, 16344 bytes \
diff --git a/toyos-abi/src/boot.rs b/toyos-abi/src/boot.rs
index 33c34e9f1..07703906a 100644
--- a/toyos-abi/src/boot.rs
+++ b/toyos-abi/src/boot.rs
@@ -101,13 +101,13 @@ pub struct KernelArgs {
     /// GPT entry like [`Self::boot_partition_guid`]; zero with no image. The
     /// kernel holds that partition so no claim writes the slot it is running.
     pub root_partition_guid: [u8; 16],
-    /// The CPU's counter, the one the kernel's clock reads, at the loader's
-    /// entry and at its handoff, and the ticks its read of ROOT took (zero with
-    /// no image). The first is firmware's time since the counter started; the
-    /// kernel converts all three at its clock's rate.
-    pub loader_entry_counter: u64,
-    pub loader_handoff_counter: u64,
-    pub root_read_ticks: u64,
+    /// The time-stamp counter at the loader's entry and at its handoff, and the
+    /// cycles its read of ROOT took (zero with no image). The TSC counts from
+    /// reset, so the first is firmware's time since power-on unless firmware
+    /// wrote the counter; the kernel converts all three at its calibrated rate.
+    pub loader_entry_tsc: u64,
+    pub loader_handoff_tsc: u64,
+    pub root_read_tsc: u64,
 }
 
 /// [`KernelArgs::layout`] for the struct this file declares: the struct's own
@@ -222,9 +222,9 @@ const _: () = {
     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_counter) == 1248);
-    assert!(offset_of!(KernelArgs, loader_handoff_counter) == 1256);
-    assert!(offset_of!(KernelArgs, root_read_ticks) == 1264);
+    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::<KernelArgs>() == 1272);
     assert!(LAYOUT as i32 > 1440 || (LAYOUT as i32) < -1440);
     assert!(align_of::<KernelArgs>() == 8);
@@ -317,9 +317,9 @@ mod tests {
         root_image_addr: 0,
         root_image_len: 0,
         root_partition_guid: [0; 16],
-        loader_entry_counter: 0,
-        loader_handoff_counter: 0,
-        root_read_ticks: 0,
+        loader_entry_tsc: 0,
+        loader_handoff_tsc: 0,
+        root_read_tsc: 0,
     };
 
     #[test]

n0-whole-revert.patch is git diff 91b4b0823 dd8738302 -- . ':!tests/toyos.rs' ':!rust'; f0-el2-reads-cntvct.patch is git diff 97bb62406 38d28863e -- bootloader/src/arch/aarch64.rs.

mutate.sh
#!/bin/bash
# mutate.sh <patch-name>...: each mutation as a checked patch on the clean head, the guest
# row run on it, its exit code recorded, the patch taken back, the tree clean again.
set -u
S=/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/657-round
ROW=virt_boot_from_power_on
cd /Users/jan/Dev/jan/toyos-arm6 || exit 2
for name in "$@"; do
  patch="$S/mutations/$name.patch"
  if [ -n "$(git status --porcelain --ignore-submodules=none)" ]; then echo "$name ABORT: tree not clean before" >> "$S/summary-mutations.txt"; exit 2; fi
  head=$(git rev-parse --short=9 HEAD)
  if ! git apply --check "$patch"; then echo "$name @$head DOES-NOT-APPLY" >> "$S/summary-mutations.txt"; continue; fi
  git apply "$patch"
  t0=$(date +%s)
  cargo test --test toyos-build -- "$ROW" --nocapture < /dev/null > "$S/mut-$name.log" 2>&1
  rc=$?
  git apply -R "$patch"; back=$?
  dirty=$(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ')
  echo "mut-$name @$head EXIT=$rc ($(( $(date +%s)-t0 ))s) restored=$back dirty-after=$dirty load $(sysctl -n vm.loadavg): cargo test --test toyos-build -- $ROW --nocapture" >> "$S/summary-mutations.txt"
  [ "$dirty" = 0 ] || exit 2
done
echo "mutate DONE: $*" >> "$S/summary-mutations.txt"

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #657 at 91b4b0823 against origin/main (dd8738302), round 1.

Net lines, git diff --shortstat origin/main...91b4b0823: 12 files, +151 −84. Production +64 −55, of which the only growth is bootloader/src/arch/aarch64.rs (+22 −8); a host-test fixture +1 −1; the guest test +80 −0; issues +6 −28. Without the guest row the branch is +71 −84.

Evidence at the head: none of the three is missing. summary-gates.txt and summary-guest.txt carry exit 0 for both builds, clippy, --ci host ([ci] Host: 66 step(s), all green), the row, virt_ (20 passed) and the whole suite (26 passed). Each of the six mutation logs rebuilt the crate its patch touches and reads the exit the body's table gives. No hardware reading is owed (below).

Ruled

  • The T14: no boot is owed. On x86-64 the branch changes no behaviour a reading could carry. tsc() was arch::counter() on main (bootloader/src/main.rs:502-505 there), arch::counter is still _rdtsc, the three fields keep offsets 1248, 1256 and 1264 under the same offset_of! asserts, and git grep at the head finds no METAL judge and no src/ reader of a changed line but src/kernelconsole.rs's fixture. That the x86-64 handoff has no test on any tier is main's state since 520c0d1, owned by issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md:42. This branch did not make that debt and does not owe it.
  • f0's exit 0 does not fault 97bb624. f1 (exit 0) and f2 (exit 1, boot: the counter went backwards: 3600713612000 …) are the negative control root CLAUDE.md asks: the change reverted onto the base its green arm was measured on, that base carrying the offset the Arm ARM lets firmware leave, with the Arm ARM as the oracle. f0 reverts it onto a base with no offset, where the two counts are one. What f0 does fault is the row and the track sentence, below.
  • 97bb624's message: accepted as handled. History is not rewritten; 91b4b08's message and the body carry the correction, and the exits they give are the ones in at-71922876e/summary-mutations.txt and summary-mutations.txt.

BLOCKER

  • tests/toyos.rs:149, :1380-1443, :2018-2031, and the body's "Why it is a guest test" — virt_boot_from_power_on is a new guest test whose three stated reasons are all one behaviour, the frame of CNTPCT_EL0 and CNTVCT_EL0 across the entry's msr cntvoff_el2, and that is the behaviour the standing row cannot fail on: f0 exits 0 (mut-f0-el2-reads-cntvct.log, the row green at 676 ms and 175 ms). What it does fail on is n0 (the loader's line renamed), m1 (kernel/src/main.rs's report_power_on) and m2 (bootloader/src/main.rs's start_kernel): architecture-neutral source that a reader of the diff catches, and that issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md:42 already gives its tier in this judge's own words, "metal boot_from_power_on: the power-on spans are the loader's counts at the kernel's rate". 520c0d1 cut this judge from QEMU for that, and the branch brings it back under a virt_ name. So the body says of nothing the row holds why a metal row cannot reach it, and on the ABI's handoff the row is a test that cannot fail on the claim it was added for. Deletion named: the row, its four constants, the judge and the arm; the branch is then +71 −84. Closed by one of: (a) they are gone, and f1 and f2 are measured again at the new head with the judge carried in their patches, exit 0 and exit 1; or (b) the body names a behaviour only an AArch64 guest reaches that the standing row fails on, with the mutation, its exit 1 and its log, and why no host test reaches it.

NOTE

  • issues/kernel/toyos-runs-on-arm64.md:270-275 — the sentence files the loader's EL2 read as owed under the exit main wrote for the entry's three writes, and that exit's second arm, "a loader that writes the opposite values before the handoff", is what f1 and f2 are; f2 exits 1. As written, the pull request that files the debt has paid it, while the body says no standing test holds the arm. One of the two is false: the record says which red is still owed, or the sentence goes. It also names virt_boot_from_power_on, which the BLOCKER may remove.
  • the same lines, and the body's f0 bullet — "because QEMU's firmware leaves CNTVOFF_EL2 zero" is a register nobody read. f0's green shows only that the loader's count is not ahead of the kernel's. The nearest thing in the logs is the gap between Boot: complete (N ms) and the power-on line's kernel span: 27, 25, 27 and 26 ms in the head's, f0's, f1's and n0's. One mrs cntvoff_el2 printed beside HCR_EL2.E2H in a mutation of cpu_as_entered reads it; the clause stands on that reading or goes.
  • tests/toyos.rs:2022-2029 — only if a judge survives the BLOCKER: it boots the guest virt_user_mode boots at :1991-2000, same config, profile and BootOptions, and guest-whole.log carries both lines on that boot's serial ([serial 5]). The judge reads that serial; its own sentence names what failed.

REMOVE

  • tests/toyos.rs:2019-2021 — restates bootloader/src/arch/aarch64.rs:26-29 and narrates the console.

SEND BACK

…, and the handoff's tier is metal's

Review of 91b4b08 (#657, issuecomment-5960716527).

The row was added to judge 97bb624, the loader reading `CNTPCT_EL0` at
EL2. With that arm reverted and nothing else it stays green (f0, exit 0):
on QEMU's firmware the two counts are one. What it did fail on is the
architecture-neutral handoff in `kernel/src/main.rs` and
`bootloader/src/main.rs`, whose tier
`issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md`
already names: the metal row `boot_from_power_on`, the judge 520c0d1 cut
from QEMU. So the row, its four constants, the judge and the arm go, and
`tests/toyos.rs` is `main`'s again.

The track's sentence that filed the loader's EL2 read as owed its red goes
with it. The exit `main` wrote for the entry's three writes has two arms,
and the second, "a loader that writes the opposite values before the
handoff", is the negative control this pull request carries: with an hour
staged in `CNTVOFF_EL2` around each loader reading, the loader as it
stands is green and the loader reading `CNTVCT_EL0` is red, the judge
carried in the patch. That red is measured at this commit and recorded in
the pull request's body; nothing is owed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Mutation patches for the head 5bf3347, which has no virt_boot_from_power_on. Each patch is judge.patch, the row as 91b4b08 had it (its registration, four constants, judge and arm), followed by its own change to bootloader/src/arch/aarch64.rs, in one file. Each was applied with git apply --check and git apply on the clean head, run as cargo test --test toyos-build -- virt_boot_from_power_on --nocapture, and taken back with git apply -R, the tree clean after each (mutate.sh, below). The exits are in the body.

judge.patch, the first 105 lines of each patch (sha256 f8a54be41597c36036d5ae7522bf15cb6e54f429b459390e8137a26fb53ea483)
diff --git a/tests/toyos.rs b/tests/toyos.rs
index 7fb33c083..fc1193b09 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -146,6 +146,7 @@ const SCREEN_TESTS: &[(&str, qemu::Profile)] = &[
     ("virt_early_fault", qemu::Profile::Virt),
     ("virt_el2_drop", qemu::Profile::VirtEl2NoVhe),
     ("virt_user_mode", qemu::Profile::VirtEl2),
+    ("virt_boot_from_power_on", qemu::Profile::VirtEl2),
     ("virt_timer_preempts", qemu::Profile::VirtEl2),
     ("virt_irq_storm", qemu::Profile::VirtEl2),
     ("virt_timer_floor", qemu::Profile::VirtEl2),
@@ -1376,6 +1377,71 @@ fn judge_virt_job(qemu: &mut QemuInstance, job: &str, said: &str) -> Result<Stri
     Ok(serial)
 }
 
+/// The loader's line, followed by its counter at its entry and at its handoff.
+const LOADER_COUNTER: &str = "Loader counter: ";
+/// The kernel's line, followed by its four spans in milliseconds.
+const POWER_ON: &str = "boot: power-on to loader ";
+/// The kernel's line in place of [`POWER_ON`] when the loader's counts and its
+/// own are out of order.
+const COUNTER_BACKWARDS: &str = "boot: the counter went backwards";
+/// The kernel's `clock:` record, followed by the rate `CNTFRQ_EL0` states.
+const GENERIC_TIMER_HZ: &str = "clock: the generic timer counts at ";
+
+/// **The boot from power-on is the loader's raw counts at the kernel's rate.**
+/// The kernel's first two spans are the loader's counts converted at the rate
+/// its clock record gives, divided into a second as the kernel divides it; the
+/// ROOT read sits inside the loader's span, and `Boot: complete`'s own count
+/// inside the kernel's.
+fn boot_from_power_on(log: &str) -> Result<(), String> {
+    if let Some(line) = log.lines().find(|l| l.contains(COUNTER_BACKWARDS)) {
+        return Err(format!("{line}\nserial:\n{log}"));
+    }
+    let after = |head: &str| -> Result<Vec<u128>, String> {
+        let at = log.find(head).ok_or_else(|| format!("no {head:?} line on the PL011\nserial:\n{log}"))?;
+        let line = log[at + head.len()..].lines().next().unwrap_or("");
+        Ok(line
+            .split(|c: char| !c.is_ascii_digit())
+            .filter(|word| !word.is_empty())
+            .map(|word| word.parse().expect("a run of digits"))
+            .collect())
+    };
+    let (loader, spans, rate) = (after(LOADER_COUNTER)?, after(POWER_ON)?, after(GENERIC_TIMER_HZ)?);
+    let (&[entry, handoff, ..], &[to_loader, in_loader, root_read, to_complete], Some(period_fs)) = (
+        &loader[..],
+        &spans[..],
+        rate.first().and_then(|&hz| 1_000_000_000_000_000u128.checked_div(hz)),
+    ) else {
+        return Err(format!(
+            "the lines do not carry their numbers: {loader:?} after {LOADER_COUNTER:?}, {spans:?} \
+             after {POWER_ON:?}, {rate:?} after {GENERIC_TIMER_HZ:?}"
+        ));
+    };
+    let ms = |ticks: u128| ticks * period_fs / 1_000_000_000_000;
+    if (to_loader, in_loader) != (ms(entry), ms(handoff - entry)) {
+        return Err(format!(
+            "the kernel says {to_loader} ms to the loader and {in_loader} ms in it; the loader's \
+             counts {entry} and {handoff} at {period_fs} fs a tick are {} and {}",
+            ms(entry),
+            ms(handoff - entry)
+        ));
+    }
+    if root_read > in_loader {
+        return Err(format!("the ROOT read took {root_read} ms of a loader that took {in_loader}"));
+    }
+    let complete = after("Boot: complete (")?;
+    if complete.first().is_none_or(|&own| own > to_complete) {
+        return Err(format!(
+            "`Boot: complete` counts {complete:?} ms from its own start, inside a kernel span the \
+             power-on line puts at {to_complete} ms"
+        ));
+    }
+    eprintln!(
+        "  [boot] power-on to loader {to_loader} ms, loader {in_loader} ms (ROOT read {root_read} \
+         ms), kernel {to_complete} ms"
+    );
+    Ok(())
+}
+
 /// What `unmap_touch` says once every read of a page just unmapped,
 /// on the unmapping thread and on another, ended its process.
 const UNMAP_TOUCH_SAID: &str =
@@ -1949,6 +2015,20 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re
             }
             Ok(())
         }
+        "virt_boot_from_power_on" => {
+            // Entered at EL2: the loader reads the physical count, the kernel
+            // the virtual one once its entry writes the offset zero. Both speak
+            // on the PL011.
+            let mut qemu = QemuInstance::boot_with_options(
+                test_config,
+                &[],
+                &[],
+                BootOptions { profile, ready_marker: "control registers: SCTLR_EL1=", ..Default::default() },
+            );
+            let rest =
+                qemu.drain_until(Duration::from_secs(180), |l| l.contains(POWER_ON) || l.contains(COUNTER_BACKWARDS));
+            boot_from_power_on(&format!("{}\n{rest}", qemu.boot_log()))
+        }
         "virt_timer_preempts" => virt_job(profile, "preempt", "preempt: the counting thread was preempted twice"),
         "virt_fp_isolation" => virt_job(profile, "fp_isolation", "fp_isolation: v0-v31, FPCR and FPSR survived"),
         "virt_first_entry" => virt_job(profile, "first_entry", "first_entry: x1-x30 were zero"),
r0-cntvoff-read.patch after judge.patch (sha256 of the whole file 7f4b38084f137bdef01c1dde2e9169b81dc6b4e7203e6e53bfb105267c1fb468)
diff --git a/bootloader/src/arch/aarch64.rs b/bootloader/src/arch/aarch64.rs
--- a/bootloader/src/arch/aarch64.rs
+++ b/bootloader/src/arch/aarch64.rs
@@ -52,15 +52,17 @@ pub fn cpu_as_entered() -> Result<Option<alloc::string::String>, alloc::string::String> {
     if el != 2 {
         return Ok(Some(alloc::format!("CPU: entered at EL{el}")));
     }
-    let (hcr, mmfr4): (u64, u64);
+    let (hcr, mmfr4, cntvoff): (u64, u64, u64);
     // SAFETY: at EL2 both are readable. `ID_AA64MMFR4_EL1` by its encoding,
     // `S3_0_C0_C7_4`, which sits in the ID space an older CPU reads as zero.
     unsafe {
         core::arch::asm!("mrs {}, hcr_el2", out(reg) hcr, options(nomem, nostack, preserves_flags));
         core::arch::asm!("mrs {}, S3_0_C0_C7_4", out(reg) mmfr4, options(nomem, nostack, preserves_flags));
+        // MUTATION: the offset firmware left, read and printed, nothing written.
+        core::arch::asm!("mrs {}, cntvoff_el2", out(reg) cntvoff, options(nomem, nostack, preserves_flags));
     }
     let e2h = (hcr >> 34) & 1;
     let e2h0 = (mmfr4 >> 24) & 0xF;
-    let state = alloc::format!("CPU: entered at EL2, HCR_EL2.E2H {e2h}, ID_AA64MMFR4_EL1.E2H0 {e2h0:#x}");
+    let state = alloc::format!("CPU: entered at EL2, HCR_EL2.E2H {e2h}, CNTVOFF_EL2 {cntvoff:#x}, ID_AA64MMFR4_EL1.E2H0 {e2h0:#x}");
     if e2h0 != 0 {
         return Err(alloc::format!(
             "{state}: REFUSED, HCR_EL2.E2H is RES1 on a CPU without FEAT_E2H0, and the kernel's \
f0-el2-reads-cntvct.patch after judge.patch (sha256 of the whole file 6b60adaca37834abba2a5e0e93102deecab0b6e4a8d22684f2d7e0b461c0d3b0)
diff --git a/bootloader/src/arch/aarch64.rs b/bootloader/src/arch/aarch64.rs
index 6a30fa656..a85bd2560 100644
--- a/bootloader/src/arch/aarch64.rs
+++ b/bootloader/src/arch/aarch64.rs
@@ -15,29 +15,12 @@ pub fn typing(write_back: &[(u64, u64)]) -> Typing<'_> {
     Typing::ByMap(write_back)
 }
 
-/// The exception level the loader runs at, from `CurrentEL`.
-fn current_el() -> u64 {
-    let current: u64;
-    // SAFETY: reads `CurrentEL`, which EL1 and above may read.
-    unsafe { core::arch::asm!("mrs {}, currentel", out(reg) current, options(nomem, nostack, preserves_flags)) };
-    (current >> 2) & 0b11
-}
-
-/// The count the kernel's clock reads, its virtual count. At EL2 that is the
-/// physical count, `CNTPCT_EL0`, because the kernel's entry writes
-/// `CNTVOFF_EL2` zero, which resets UNKNOWN; at EL1 the offset is the
-/// hypervisor's for good and `CNTVCT_EL0` is read.
+/// The generic timer's virtual count, `CNTVCT_EL0`.
 pub fn counter() -> u64 {
     let count: u64;
-    if current_el() == 2 {
-        // SAFETY: reads a counter EL2 may always read; the `ISB` keeps the
-        // read in program order.
-        unsafe { core::arch::asm!("isb", "mrs {}, cntpct_el0", out(reg) count, options(nomem, nostack, preserves_flags)) };
-    } else {
-        // SAFETY: reads a counter EL1 may always read; the `ISB` keeps the
-        // read in program order.
-        unsafe { core::arch::asm!("isb", "mrs {}, cntvct_el0", out(reg) count, options(nomem, nostack, preserves_flags)) };
-    }
+    // SAFETY: reads a counter EL1 and EL2 may always read; the `ISB` keeps the
+    // read in program order.
+    unsafe { core::arch::asm!("isb", "mrs {}, cntvct_el0", out(reg) count, options(nomem, nostack, preserves_flags)) };
     count
 }
 
@@ -48,7 +31,10 @@ pub fn counter() -> u64 {
 /// the console still prints; the entry's read-back of `HCR_EL2` stays as the
 /// last line of defence.
 pub fn cpu_as_entered() -> Result<Option<alloc::string::String>, alloc::string::String> {
-    let el = current_el();
+    let current: u64;
+    // SAFETY: reads `CurrentEL`, which EL1 and above may read.
+    unsafe { core::arch::asm!("mrs {}, currentel", out(reg) current, options(nomem, nostack, preserves_flags)) };
+    let el = (current >> 2) & 0b11;
     if el != 2 {
         return Ok(Some(alloc::format!("CPU: entered at EL{el}")));
     }
f1-firmware-offset.patch after judge.patch (sha256 of the whole file 3ddcc8f42962545c7647a91866d5c18e9def3d412d5e4e60c02a97513db70b20)
diff --git a/bootloader/src/arch/aarch64.rs b/bootloader/src/arch/aarch64.rs
index 6a30fa656..2951d96fb 100644
--- a/bootloader/src/arch/aarch64.rs
+++ b/bootloader/src/arch/aarch64.rs
@@ -30,9 +30,43 @@ fn current_el() -> u64 {
 pub fn counter() -> u64 {
     let count: u64;
     if current_el() == 2 {
+        // MUTATION: firmware's offset, an hour ahead, in CNTVOFF_EL2 for this
+        // reading alone, with every interrupt masked so firmware's timer never
+        // sees it; the offset and the mask are put back after the reading.
+        let (daif, offset): (u64, u64);
+        // SAFETY: a mutation; EL2 owns both registers it writes and restores.
+        unsafe {
+            core::arch::asm!(
+                "mrs {daif}, daif",
+                "msr daifset, #0xf",
+                "mrs {offset}, cntvoff_el2",
+                "mrs {hour}, cntfrq_el0",
+                "mov {secs}, #3600",
+                "mul {hour}, {hour}, {secs}",
+                "neg {hour}, {hour}",
+                "msr cntvoff_el2, {hour}",
+                "isb",
+                daif = out(reg) daif,
+                offset = out(reg) offset,
+                hour = out(reg) _,
+                secs = out(reg) _,
+                options(nomem, nostack, preserves_flags),
+            )
+        };
         // SAFETY: reads a counter EL2 may always read; the `ISB` keeps the
         // read in program order.
         unsafe { core::arch::asm!("isb", "mrs {}, cntpct_el0", out(reg) count, options(nomem, nostack, preserves_flags)) };
+        // SAFETY: puts back what the block above read.
+        unsafe {
+            core::arch::asm!(
+                "msr cntvoff_el2, {offset}",
+                "isb",
+                "msr daif, {daif}",
+                offset = in(reg) offset,
+                daif = in(reg) daif,
+                options(nomem, nostack, preserves_flags),
+            )
+        };
     } else {
         // SAFETY: reads a counter EL1 may always read; the `ISB` keeps the
         // read in program order.
f2-firmware-offset-unfixed.patch after judge.patch (sha256 of the whole file 5cbfc0f0c26329bd8149287d765a15518018d4a97cb9394173b6379d4936a15b)
diff --git a/bootloader/src/arch/aarch64.rs b/bootloader/src/arch/aarch64.rs
index 6a30fa656..cba7fa7d9 100644
--- a/bootloader/src/arch/aarch64.rs
+++ b/bootloader/src/arch/aarch64.rs
@@ -30,9 +30,43 @@ fn current_el() -> u64 {
 pub fn counter() -> u64 {
     let count: u64;
     if current_el() == 2 {
+        // MUTATION: firmware's offset, an hour ahead, in CNTVOFF_EL2 for this
+        // reading alone, with every interrupt masked so firmware's timer never
+        // sees it; the offset and the mask are put back after the reading.
+        let (daif, offset): (u64, u64);
+        // SAFETY: a mutation; EL2 owns both registers it writes and restores.
+        unsafe {
+            core::arch::asm!(
+                "mrs {daif}, daif",
+                "msr daifset, #0xf",
+                "mrs {offset}, cntvoff_el2",
+                "mrs {hour}, cntfrq_el0",
+                "mov {secs}, #3600",
+                "mul {hour}, {hour}, {secs}",
+                "neg {hour}, {hour}",
+                "msr cntvoff_el2, {hour}",
+                "isb",
+                daif = out(reg) daif,
+                offset = out(reg) offset,
+                hour = out(reg) _,
+                secs = out(reg) _,
+                options(nomem, nostack, preserves_flags),
+            )
+        };
         // SAFETY: reads a counter EL2 may always read; the `ISB` keeps the
         // read in program order.
-        unsafe { core::arch::asm!("isb", "mrs {}, cntpct_el0", out(reg) count, options(nomem, nostack, preserves_flags)) };
+        unsafe { core::arch::asm!("isb", "mrs {}, cntvct_el0", out(reg) count, options(nomem, nostack, preserves_flags)) };
+        // SAFETY: puts back what the block above read.
+        unsafe {
+            core::arch::asm!(
+                "msr cntvoff_el2, {offset}",
+                "isb",
+                "msr daif, {daif}",
+                offset = in(reg) offset,
+                daif = in(reg) daif,
+                options(nomem, nostack, preserves_flags),
+            )
+        };
     } else {
         // SAFETY: reads a counter EL1 may always read; the `ISB` keeps the
         // read in program order.
mutate.sh
#!/bin/bash
# mutate.sh <patch-name>...: each mutation as a checked patch on the clean head. Every patch
# carries the row the head no longer has (mutations/judge.patch: the registration, the judge and
# the arm) beside its own change. The row is run on it, its exit code recorded, the patch taken
# back, the tree clean again.
set -u
S=/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/657-r2
ROW=virt_boot_from_power_on
cd /Users/jan/Dev/jan/toyos-arm6 || exit 2
for name in "$@"; do
  patch="$S/mutations/$name.patch"
  if [ -n "$(git status --porcelain --ignore-submodules=none)" ]; then echo "$name ABORT: tree not clean before" >> "$S/summary-mutations.txt"; exit 2; fi
  head=$(git rev-parse --short=9 HEAD)
  if ! git apply --check "$patch"; then echo "$name @$head DOES-NOT-APPLY" >> "$S/summary-mutations.txt"; continue; fi
  git apply "$patch"
  t0=$(date +%s)
  cargo test --test toyos-build -- "$ROW" --nocapture < /dev/null > "$S/mut-$name.log" 2>&1
  rc=$?
  git apply -R "$patch"; back=$?
  dirty=$(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ')
  echo "mut-$name @$head EXIT=$rc ($(( $(date +%s)-t0 ))s) restored=$back dirty-after=$dirty load $(sysctl -n vm.loadavg): cargo test --test toyos-build -- $ROW --nocapture" >> "$S/summary-mutations.txt"
  [ "$dirty" = 0 ] || exit 2
done
echo "mutate DONE: $*" >> "$S/summary-mutations.txt"
summary-mutations.txt
mut-r0-cntvoff-read @5bf33470c EXIT=0 (6s) restored=0 dirty-after=0 load { 6.64 6.73 16.07 }: cargo test --test toyos-build -- virt_boot_from_power_on --nocapture
mut-f0-el2-reads-cntvct @5bf33470c EXIT=0 (5s) restored=0 dirty-after=0 load { 7.63 6.93 16.09 }: cargo test --test toyos-build -- virt_boot_from_power_on --nocapture
mut-f1-firmware-offset @5bf33470c EXIT=0 (5s) restored=0 dirty-after=0 load { 7.10 6.83 16.00 }: cargo test --test toyos-build -- virt_boot_from_power_on --nocapture
mut-f2-firmware-offset-unfixed @5bf33470c EXIT=1 (5s) restored=0 dirty-after=0 load { 6.69 6.75 15.92 }: cargo test --test toyos-build -- virt_boot_from_power_on --nocapture
mutate DONE: r0-cntvoff-read f0-el2-reads-cntvct f1-firmware-offset f2-firmware-offset-unfixed

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #657 at 5bf33470c against origin/main (dd8738302), round 2.

Round 1's BLOCKER

  • CLOSED — virt_boot_from_power_on, a guest row that could not fail on the claim it was added for. Closed by (a). The row, its four constants, the judge and the arm are gone: git diff origin/main 5bf33470c -- tests/ is empty, and tests/toyos.rs is blob 7fb33c083 on both sides. f1 and f2 are measured again at this head with the judge carried in each patch (the first 105 lines of each are judge.patch, sha256 f8a54be4…): summary-mutations.txt reads mut-f1-firmware-offset @5bf33470c EXIT=0 and mut-f2-firmware-offset-unfixed @5bf33470c EXIT=1, each restored=0 dirty-after=0. Both logs rebuilt the loader (Compiling bootloader, line 8 of each). mut-f1-firmware-offset.log:210 is boot: power-on to loader 633 ms, loader 167 ms (ROOT read 8 ms), kernel to Boot: complete 1563 ms; mut-f2-firmware-offset-unfixed.log:210 is boot: the counter went backwards: 3600631144000 at the loader's entry, 3600799116000 at its handoff, 2362285000 at Boot: complete, the judge fails on that line, and the log ends exit status: 1.

Round 1's NOTEs, REMOVE and rulings

  • The track sentence: closed. issues/kernel/toyos-runs-on-arm64.md differs from main's by the two citations of the closed issue and by nothing else (+1 −3); stage 4's exit is main's text again.
  • The CNTVOFF_EL2 clause: closed but for the NOTE below. mut-r0-cntvoff-read.log:75 is CPU: entered at EL2, HCR_EL2.E2H 0, CNTVOFF_EL2 0x0, ID_AA64MMFR4_EL1.E2H0 0x0: the kernel's entry writes E2H clear, exit 0, loader rebuilt.
  • The judge's own boot, and the REMOVE at tests/toyos.rs:2019-2021: moot, the lines are gone.
  • The three rulings stand. git diff 91b4b0823..5bf33470c is tests/toyos.rs and the track sentence: nothing since touches x86-64 or a line they rest on.

Net lines, git diff --shortstat origin/main...5bf33470c: 11 files, +67 −83. Production +64 −55, the only growth bootloader/src/arch/aarch64.rs (+22 −8), accepted in round 1; a host-test fixture +1 −1; guest tests 0; issues +2 −27.

Evidence at the head: none of the three is missing. summary-gates.txt and summary-guest.txt, every line @5bf33470c: both builds EXIT=0, clippy EXIT=0 (clippy: 17 invocations clean), --ci host EXIT=0 (ci-host.log:7146, [ci] Host: 66 step(s), all green), virt_ EXIT=0 (19 passed), the whole suite EXIT=0 (25 passed). guest-virt.log:14 rebuilt the loader from the clean tree after the last mutation was taken back, and the harness with it (19 tests, not 20). No hardware reading is owed, as ruled.

BLOCKER

None.

NOTE

  • PR body, "f0 is green because this machine's firmware leaves no offset" — "r0 reads CNTVOFF_EL2 as 0x0 at the loader's entry" names a place r0 did not read — r0's mrs is in cpu_as_entered, which start_kernel calls (bootloader/src/main.rs:505), after Starting kernel...: mut-r0-cntvoff-read.log:75, with the handoff reading at :87 (Loader counter: 643378000 at entry, 810128000 at the handoff). The register was read beside the handoff reading, not the entry one. The body says where it was read, or drops the clause.

REMOVE

None.

LAND AFTER NAMED CHANGES

@Japabu
Japabu marked this pull request as ready for review October 2, 2026 20:29
@Japabu
Japabu enabled auto-merge October 2, 2026 20:29
@Japabu
Japabu added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 8f6abbc Oct 2, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-arm6 branch October 2, 2026 21:16
Japabu added a commit that referenced this pull request Oct 2, 2026
Main moved during the round: #657 (8f6abbc) and #649 (c59e09e) landed
after b102097 merged 5daab30. No conflict, and no file is changed on
both sides since 5daab30; the `rust` gitlink is the same on both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant