Skip to content

ARM64 port, stages 0 to 3: shared groundwork, aarch64-unknown-toyos with std and the userland, and the loader and kernel reach the PL011 on virt - #524

Merged
Japabu merged 57 commits into
mainfrom
wt/toyos-arm64
Sep 26, 2026
Merged

Japabu merged 57 commits into
mainfrom
wt/toyos-arm64

Conversation

@Japabu

@Japabu Japabu commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

The ARM64 port (issues/kernel/toyos-runs-on-arm64.md), stages 0 to 3, as one PR: the groundwork shared with x86, the aarch64-unknown-toyos target with std and the userland, then the UEFI loader and the kernel reaching the PL011 on QEMU virt under HVF. AArch64 links through rust-lld (owner's ruling, 2026-09-26); x86-64 links as it did.

What a boot does now, on this M4 Pro: qemu-system-aarch64 -M virt,gic-version=3 -accel hvf with AAVMF loads the loader off the USB stick. The loader reads ROOT through firmware (ROOT now carries the AArch64 userland), says at which EL firmware handed it the CPU (at EL2 with HCR_EL2.E2H and ID_AA64MMFR4_EL1.E2H0, refusing by name a CPU whose E2H is RES1), and hands off. The kernel drops from EL2 when entered there, writes its control-register declaration whole, turns on the loader's tables, installs its vectors, finds the PL011 through SPCR, and prints the boot log, the memory map, the MADT's GIC structures and the GTDT's timers. It then stops at the first item a later stage owns, with an EARLY PANIC on serial and on the ramfb panel:

  • under HVF: RNDR, which HVF does not expose (virtio-rng is stage 6);
  • under TCG -cpu max: the kernel's own page tables (stage 4).

Every stand-in for later work is an owed! that panics naming its stage; none returns a guess.

Stage 0: the shared groundwork, on x86

  • One Arch (src/arch.rs) names every triple, QEMU binary, firmware, CPU and accelerator, and now which linker an architecture's guest binaries use (links_through_toyos_ld) and QEMU's -boot for its firmware. The build, cargo run, CI and the harness read it.

  • toyos-elf takes a machine: EM_AARCH64 and R_AARCH64_RELATIVE, so the loader reads an AArch64 kernel. toyos-ld is byte for byte main's (see "The linker").

  • The kernel's arch/ is x86_64/ behind one selector (arch/mod.rs):

    • the syscall handlers moved out; only the gate is x86;
    • one IrqGuard replaces three hand-rolled ones, and fixed their nomem compiler-ordering bug;
    • one MSI doorbell;
    • the PC's own devices (i8042, I/O APIC, TCO, CMOS RTC, VT-d), the page tables and the scheduler's Hw boundary live under arch/x86_64/, and generic code names each by concept (keyboard_controller, irqchip, trap, iommu_unit, cpu::counter, …).
  • Mmio carries writel/readl ordering (arch::barrier: a compiler barrier on x86, DMB OSHST/OSHLD on AArch64). Every descriptor-then-doorbell site names its barrier (dma_wmb/dma_rmb). Three ordering bugs fixed on the way:

    • NVMe read a completion's body before its phase tag was known new;
    • xHCI's TrbRing::put wrote the cycle bit with the body;
    • xHCI's event read did the same in reverse.
  • The architecture rules (src/sourcegate.rs, ARCH_RULES):

    • assembly only in an architecture's module;
    • target_arch only there or in a selector;
    • no path into arch::x86_64::/arch::aarch64:: from generic code;
    • none of the three in a pure crate.

    core::arch/std::arch count in any spelling a line scan can resolve: the path, a use … as of it, an arch element of a core::{…} group. A glob or a rename of core/std itself is refused outright, outside the places: use core::*;, core::{*}, use core as k;, extern crate core as k;, core::{self as k}, use {…, std as k}. What those bring into scope is a question for name resolution, which a line scan does not have; following each alias to a later arch would be one more pattern per spelling and never sound, while refusing the import is, and no file outside an arch module has another use for either (the whole-tree rule is green). A rename counts only in an import item, so core as u32 on a variable and crate::core as c are not reds; the fixture holds both.

    Each rule has a fixture test that plants a violation and reds naming the rule and its places. libc, toyos-window and metalprobe now keep theirs in arch/ modules (stage 1). The guest probes are the declared rows left (issues/build/assembly-outside-an-arch-module-in-userland-and-guest-probes.md).

  • The Relaxed audit: all 761 lines naming Relaxed under kernel/src (84 files) read.

    • Changed: the panic console's published framebuffer descriptor was a seqlock over a plain cell, a data race. It is now atomic words (drivers/panic_console/published.rs) with a loom model.
    • Three publications that promised an order they did not have: panic.rs's crash evidence, hardlockup's spin record, and the USB fault staging.
    • The serial backend's lock (drivers/serial_lock.rs): a lost try_lock once built the guard it had lost and released the holder's lock when it dropped. The lock is a file kernel-loom compiles, and kernel-loom/tests/serial_lock.rs models it.
    • Mmio's contract says a write orders prior stores only, as writel does: a doorbell that hands back entries this CPU read is ordered by dependency alone.

Stage 1: aarch64-unknown-toyos, std and the userland

  • The target: the rust pin is c34ecdf0ab6, this branch's 851c6bfcaa8 merged with Where everything lives: the layout as ruled, and its first stage #531's 80ea645f83b, and merged into toyos-inbox as 2434fb9d1bc. aarch64-unknown-toyos is the ToyOS base options with +v8a, 128-bit atomics and rust-lld (GNU flavour). Outline atomics are off: their helpers ask the OS through an auxiliary vector ToyOS does not have.
  • std (fork):
    • _start reads argc/argv off the stack and ends the frame chain.
    • TLS is variant I: TCB at TPIDR_EL0, DTV in its first word.
    • No TLS-descriptor resolver: nothing on AArch64 loads a shared library yet, so fork commit 851c6bfcaa8 deleted the one this branch had written with no caller, and the kernel refuses R_AARCH64_TLSDESC by name. An executable is refused on its count: RelaCounts::for_executable returns ExeRefusal::TlsDescriptor (or TooLarge), and it is the only way to the ExeReservation whose capacities parse_rela_entries reserves, so the kernel cannot skip the refusal and still reserve. A library is refused in rela::validate (RelocError::TlsDescriptor). The resolver returns in the port's stage 5 with its loader half and a dlopen test.
  • libc (userland/libc/src/arch/): _start, memcpy/memmove/memset and the square roots, one module per architecture, with their AArch64 versions (pair loads/stores with a byte tail, fsqrt). toyos-libc-copies compiles that module for the host and holds every copy, fill and square root against Rust's own; each host architecture checks its own module.
  • The build:
    • GUEST_TARGETS gains the AArch64 userland target. Every architecture has a userland, so USERLAND_ARCHS and the plan's Userland switch are deleted: its Absent arm was unreachable and without_userland uncalled.
    • cargo run -- --arch aarch64 --build-only builds std, libtoyos_c and the userland for AArch64 and puts them on ROOT. This includes toyos-ld and toyos-cc as guest programs.
    • Left off an AArch64 ROOT by name at build time (NOT_YET_BUILT): calc, snake and doom. softbuffer's and winit's toyos forks resolve the published toyos-window 0.2.0, whose framebuffer is x86-only (issues/build/the-toolkit-forks-resolve-an-x86-only-toyos-window.md). Existing Rust GUI apps on ToyOS: iced on one winit backend, paced redraws, forks on one branch per base #528 moved those forks and is merged here. Whether they now build for AArch64 was not measured, so the entry stands.
    • toyos-window's and metalprobe's scanout drain went into arch modules (SFENCE; DSB ST), so toyos-window is 0.19.0: main's is 0.18.0 (Existing Rust GUI apps on ToyOS: iced on one winit backend, paced redraws, forks on one branch per base #528).
  • A worktree builds the compiler its fork names (src/compiler.rs). check_compiler refused a fork checkout whose compiler/ differed from the primary's, which contradicted the landed rule that every worktree builds the toolchain its own sources name.
    • Where the fork's compiler/ is the one the primary recorded, the worktree uses the primary's stage2.
    • Otherwise bootstrap builds a stage-2 compiler, host only, in the worktree's own fork checkout (build/toyos-compiler), with rust-lld. It is placed at rust/build/compilers/<key>/.
    • The key is the identity (src/identity.rs) of compiler/, src/bootstrap, src/tools, src/stage0 and Cargo.lock, the src/llvm-project commit, and the recipe. It is content only, so committing what was built as local changes is the same compiler.
    • A primary with no record of which compiler its stage2 is is refused by name; it used to read as empty and send every worktree to build its own.
    • The sysroot key already named the compiler's identity, so a sysroot is cloned from, and compiled by, the compiler its worktree names.
    • Nothing of the primary's is written: not its stage2, not its record, not the rustup toyos link (a sysroot is named by directory). The global lock is not taken.
    • A compiler key's lock (buildlock::Keyed, shared with sysroots) orders before the sysroot key's. --worktree remove and every placement sweep unnamed compilers; the one in use is held and stays, so an edit loop keeps one compiler, not one per edit.
    • hosted-rustc is refused in such a worktree: the hosted rustc is the primary's.

The linker

By the owner's ruling, AArch64 links through rust-lld and toyos-ld is frozen. This branch's toyos-ld work is reverted by a new commit, leaving toyos-ld identical to main: the machine parameter, the ARM64 PE, the AArch64 TLS descriptors, and the AArch64 GOT/PLT fixes. Net for the ruling's two commits: −750 lines (toyos-ld −784, the rust-lld wiring +34).

  • The toolchain builds with lld = true, which puts rust-lld in every stage's sysroot. Bootstrap names the AArch64 userland's linker rust-lld with rpath = false: bootstrap's -Wl,-rpath is a cc driver's argument.
  • The hosted rustc builds with lld = false, and the host stage2 is then reassembled with lld = true (build_hosted_rustc, write_config). Under lld = true, bootstrap's Assemble for the x86_64-unknown-toyos stage-2 compiler ensures LldWrapper, hence llvm::Lld and a cmake LLVM for a ToyOS host (fork compile.rs:2474). Under lld = false alone, Sysroot::run removes the host stage2 on every assemble (compile.rs:1938), and the rust-lld std_config names goes with it. So the hosted build runs with lld = false, and the host-only build then runs once more under lld = true. That run compiles nothing new and reassembles stage2 with rust-lld. It is Link everything that boots with rust-lld, and freeze toyos-ld #532's shape, which the two reconcile on.
    • Rejected: keeping a copy of rust-lld outside stage2 and naming it. Every AArch64 link names rust-lld by name and finds it in the sysroot it is compiled with, and each sysroot is cloned from stage2. A copy elsewhere would fix std_config and leave every userland link without its linker. It would also be a second copy of bootstrap's copy_lld_artifacts, and gcc-ld/ would be left out.
    • The host's default-linker-linux-override is pinned "off" in every config that builds a host compiler (HOST_LINKER_PIN, in write_config and the worktree compiler's config_text). Bootstrap ties that override to lld for x86_64-unknown-linux-gnu: lld = true sets CFG_DEFAULT_LINKER_SELF_CONTAINED_LLD_CC, which rustc_target reads through option_env!. The toggle would rebuild the host rustc on Linux in both the hosted build and the reassembly. "off" is what main's lld = false already gave, so no host rustc changes, and on macOS nothing changes at all. The worktree compiler's recipe moves with it: rebuilt here in 13 s.
    • assert_toolchain_is_honest refuses by name a toolchain with no rust-lld (toolchain::rust_lld). The reassembly asserts it too.
  • The kernel and loader drop their AArch64 toyos-ld override.
  • The loader takes the kernel as a static PIE at base 0, which rust-lld makes of the target's static-model code with -pie -z notext. PIC codegen is not usable there: its GOT slots hold virtual addresses the MMU-off entry cannot use.
  • rust-lld's layout exposed a loader defect. The kernel's stack began at the image's last byte, wherever the linker ended it: lld's …908. On the M4 the first stp x29, x30, [sp, #-16]! took an SP alignment fault (ESR_EL1 0x9A000000, read through QEMU's gdb stub on a hardware breakpoint at firmware's vector). TCG does not check SCTLR_EL1.SA there. The stack now starts on the next page.

Stage 2: the loader on AArch64

  • Builds for aarch64-unknown-uefi through rust-lld; reads ROOT through firmware block I/O.
  • toyos-bootmap types a leaf by concept: Cache::{Firmware, Memory, Device, Scanout} under a Typing. On x86 that is the MTRRs, unchanged; on AArch64, firmware's write-back ranges, with a part-memory 2 MiB page refused. It encodes both architectures' descriptors, host-tested against the Arm ARM's bit positions. The one MAIR_EL1 is declared beside them.
  • A scanout's partial 2 MiB pages are split into 4 KiB leaves where memory is typed by the map.
  • The loader jumps to the image's physical entry after cleaning it to the point of coherency.
  • AAVMF is QEMU 11.1.0's edk2-stable202408 DEBUG build, pinned by hash in NOTICE and src/licence.rs. It carries OpenSSL 3.0.9 (Apache-2.0), now recorded: QEMU builds it with NETWORK_TLS_ENABLE, and ArmVirtQemu links the bundled OpenSSL either way.
  • The five-second boot is firmware's boot-menu timeout, not its DEBUG build. Arch::boot passes -boot menu=on,splash-time=0 for AArch64 on both launch paths (see Measured).

TLS, in each machine's variant

The kernel built x86-64's variant II for every thread; the AArch64 std reads its DTV at TP+0, and rust-lld resolves an AArch64 executable's own local-exec accesses to align_up(16, p_align) above TP. toyos_elf::tls now lays out either variant from a Static whose alignments are powers of two by construction, with tpoff per variant. The kernel takes the variant from its ELF machine, places the executable first in variant I and last in variant II, and writes each TCB: [self, DTV] or [DTV, reserved]. x86-64's arithmetic is unchanged.

Stage 3: the kernel on virt

  • kernel/src/arch/aarch64/:
    • the entry: at EL2, HCR_EL2 first, an ISB, and a read-back that halts in refused_hcr_el2_readback on any difference from the declaration, before any EL1 register is written; then the drop. E2H held set is one such difference, since the declaration has it clear, and the loader refuses that case by name first, where the console still prints. The kernel's separate E2H branch is deleted. At EL1, MMU off-then-on. The declaration's read-back asserts EL1 on SP_EL1;
    • the control-register declaration, written from constants, read back and asserted;
    • the vectors: every exception reports its class, ELR, FAR, the registers and a backtrace, then panics;
    • the PL011 found through SPCR, barriers, cache maintenance, RNDR.
  • toyos-acpi decodes SPCR, GTDT and the MADT's GIC structures, as far as stage 3 reads them.
  • The halt every fatal path funnels into is generic (panic::halt_all_cpus).
  • The AArch64 kernel builds with -Adead_code -Aunfulfilled_lint_expectations until stage 7 (issues/kernel/the-aarch64-kernel-builds-with-dead-code-allowed.md).

The harness

Profile::Virt is QEMU virt: GICv3, AAVMF, ramfb, the stick on xHCI, the PL011. Profile::VirtEl2 is the same machine with virtualization=on under TCG -cpu max, where AAVMF hands the loader the CPU at EL2. Profile::arch names every variant, with no wildcard. Three tests are in Tier::Local, because no hosted runner boots AArch64 yet (stage 8), and each waits on its last asserted line with drain_until:

  • virt_early_panic: the loader's CPU: entered at EL1, the boot log on the PL011, and the panic on both channels in its fatal colours;
  • virt_early_fault: an undefined instruction, reported by the vectors;
  • virt_el2_drop: the loader's CPU: entered at EL2, HCR_EL2.E2H …, ID_AA64MMFR4_EL1.E2H0 0x0, HCR_EL2 read back as declared, dropped, and the early panic after it.

The entry's EL2 writes of CNTHCTL_EL2, CNTVOFF_EL2 and CPTR_EL2 can each be deleted and virt_el2_drop stays green, because stage 3 reads no counter and runs no FP. The track now records those three mutations under stage 4, whose tests are the first that can red on them.

Measured

  • Boot to EARLY PANIC, AArch64 under HVF with test-early-panic armed, from QEMU's start. Pinned AAVMF, three runs per arm, interleaved in one session:

    runs (s) loader's first line
    before (the earlier 5.44 s) 5.70, 5.69, 5.70 5.32–5.33 s
    with -boot menu=on,splash-time=0 0.69, 0.69, 0.70 0.33 s

    virt_early_panic and virt_early_fault now take 2 s each (7 s before).

  • The RELEASE AAVMF of the owner's ruling was tried and not adopted. The ruling's premise, that the delay is the DEBUG build's, is refuted, and the RELEASE builds tried do not boot reliably:

    • Debian 13's qemu-efi-aarch64_2025.02-8+deb13u1 (licence checked: BSD-2-Clause-Patent AND Apache-2.0, OpenSSL 3.4.0 and Mbed TLS): never reached the loader in six boots.
    • Debian 12's 2022.11-6+deb12u2: 2 of 5 boots, and those took 5.71 and 6.08 s.
  • AArch64 userland: the whole workspace but calc/snake/doom builds and links through rust-lld (cargo run -- --arch aarch64 --build-only EXIT=0). init is an ET_DYN AArch64 PIE whose PT_TLS is 64-aligned and whose only dynamic relocations are RELATIVE.

  • The primary's bootstrap config, run once. write_config's exact text for this host (lld = true, the host and six guest targets, toyos-ld for x86_64-unknown-toyos, rust-lld with rpath = false for aarch64-unknown-toyos), with one line added, build-dir pointing at the worktree fork checkout's build/toyos-compiler, run as ./x build --stage 2 --warnings warn on the fork at c3cb792df3f: EXIT=0 in 4 min 6 s. It built stage1 std for all seven targets and uplifted each to stage 2, and rust-lld is in the stage 2 sysroot. With that stage2, a std hello-world links for aarch64-unknown-toyos (rust-lld, a static PIE) and for x86_64-unknown-toyos (toyos-ld), EXIT=0 each. What it does not exercise: a fresh build directory (it reused the keyed compiler's, 14 GB, which grew to 15 GB), and the primary's own build/, which holds lld = false artifacts. A fresh one needs more than the 21 to 29 GB this disk had free during the session; the primary's own build/aarch64-apple-darwin is 47 GB.

  • The hosted-rustc sequence, from a fresh clone (nightly run 36266825579 at d8bee311; each job runs full_bootstrap, then the hosted rustc, then the reassembly, then the sysroot). Bootstrap's own Build completed times:

    job host toolchain hosted rustc (lld = false) reassembly (lld = true) sysroot std
    portability-linux (debian sid) 36:54 22:10 0:05 9:39
    portability-macos 38:46 21:47 0:28 7:38
    build (--ci toolchain, ubuntu) 26:59 16:13 0:04 6:34

    The reassembly compiled no crate on either host, which is what the Linux pin is for.

  • One compiler of a worktree's own costs 583 MB at rust/build/compilers/<key>/ and 15 GB of incremental build in the worktree's fork checkout (build/toyos-compiler). Its key takes 1.3 to 2.1 s to compute on this fork, 0.6 s of it src/tools.

  • Mmio on x86, the x86 kernel built at 3f3ae53e^ and at 3f3ae53e with one toolchain: .text is 0x1c4000 both times; the text symbols sum to 1,753,507 and 1,753,519 bytes (+12) and the disassembly has 471,322 and 471,377 instructions (+55, +0.012%). The commit also moved NVMe's and xHCI's reads, so +55 is an upper bound on what the compiler barriers cost.

  • virt's MPIDRs: with 17 CPUs (TCG, -cpu max, GICv3) the MADT names CPU 16 mpidr=0x100, which hardware_id() & 63 puts on slot 0.

Merging main (#530, #528)

origin/main moved to fd62f567 during this round and is merged (a1a143f8). Seven conflicts, each resolved to carry both sides:

The AArch64 loader reads the slotted layout: virt_ passes on the merge.

Gates, each with its exit code

At 235c5a5b, the merge plus the iced-counter lock. The head, 8c5be843, adds only issue files.

  • cargo run -- --ci host: 45 steps, all green, EXIT=0. The whole output was read; its FAILED lines are the loom controls' expected reds and the worktree tests' planted refusals.
  • cargo run -- --ci abi-split: EXIT=0 (toyos-window 0.18.0 -> 0.19.0). The first run, before the iced-counter lock followed, was EXIT=1 and named that lockfile.
  • cargo test -- virt_: 3 passed, EXIT=0 (at a1a143f8, whose tree differs only in that lockfile). Before the merge it also passed after the last mutation below was restored.
  • cargo test --manifest-path kernel-loom/Cargo.toml: 56 passed, EXIT=0
  • cargo run -- --build-only and cargo run -- --arch aarch64 --build-only: EXIT=0 each (at a1a143f8)
  • The x86 fast tier, cargo test: 397 passed, 4 failed, 6 quarantined and green, EXIT=1. --known-red answers NO for each. The host was loaded throughout by another worktree's spinner at 397% CPU, with a load average of 20 to 28.
    • metal_job_reboot: green alone, and red on main in this PR's earlier A/B (below).
    • lan_mdns_answer: main's SUN_LEN red.
    • quiesce_stops_the_machine: stayed up for 266 s and was green alone. This is issues/kernel/quiesce-stops-the-machine-stayed-up-beside-other-guests.md; the recurrence is appended there.
    • sched_check_build: 85 samples where its p90 needs 100, green alone. Its quarantine row covers another signature, and this one is appended to that row's issue.
  • Before the merge, at 8846c021, the fast tier gave 400 passed, 2 failed, EXIT=1: lan_mdns_answer, and quiesce_wakes_on_the_last_exit (green alone; appended to its issue).
  • The nightly, before the merge, at d8bee311 (https://github.com/ToyOSOrg/ToyOS/actions/runs/36266825579):
    • portability-linux success, portability-macos success, build success, host success.
    • portability-windows failed, as on main: it is the declared continue-on-error frontier.
    • Every guest, tcg and audio lane is red on nothing left in $TMPDIR: … toyos-toolchain.tar.zst. That is main's: the step comes from Every test's scratch goes with it, and a killed run's with the next #529 and release::install from before it, and main has had no nightly since Every test's scratch goes with it, and a killed run's with the next #529. Filed as issues/build/a-guest-lane-leaves-the-toolchain-tarball-in-its-tmpdir.md.
    • The lanes' suite reds: screen_diag_boot, home_budget_refusal_retried, metal_job_reboot, usb_transport_break and log_flush_retry are each red on main's last nightly too (run 36228604597). fpu_isolation is not: it passed there. Checked out at main's 17eb66a4 in this worktree, with the fork at main's pin, it is red the same way (thread_join answers NotFound), so it is main's. Filed as issues/kernel/fpu-isolation-s-thread-join-answers-not-found-on-main.md.
  • The nightly at the head 8c5be843 (https://github.com/ToyOSOrg/ToyOS/actions/runs/36273557690): portability-linux success, portability-macos success, build success, host success. The hosted-rustc reassembly took 0:05 on Linux, 0:25 on macOS and 0:07 in build. Seven guest, tcg and audio lanes had finished at hand-back, and every one was red on the $TMPDIR tarball above. Their suite reds were home_budget_refusal_retried and fpu_isolation, both main's as above, plus two tests Storage stage 3, steps 1–4: toyos-blockring, SYS_DEVICE_DMA_MAP, blockd and partition sessions #525 added after main's last nightly, which no main nightly has run: blockd_survives_its_death and partition_claim_departure. Those two were not A/B'd, and I do not know whose they are.

The two reds of the earlier fast tier (fef790a6), by a same-session A/B. Main at 17eb66a4 in a scratch worktree (/Users/jan/Dev/jan/toyos-arm64-ab) and the branch at da1df5f0, five rounds, each round running both tests on both arms at once. Counts are of each run's own exit code; a run whose harness panicked before any guest booted, on issues/build/two-suites-in-one-worktree-race-on-the-c-corpus-libc.md's race, carries no verdict.

test main branch
metal_job_reboot 3 green, 1 red (QEMU died before ===READY===), 1 no verdict 5 green
lan_mdns_answer 4 red (path must be shorter than SUN_LEN), 1 no verdict 5 red, the same line
  • metal_job_reboot's fast-tier red was the job drain carried no kernel output at all (24 bytes), then ALONE ... GREEN; the earlier A/B at d65446cc saw that signature on main twice in five.
  • lan_mdns_answer is issues/build/a-lane-s-tap-socket-path-is-past-sun-len-on-the-dev-host.md, which Where everything lives: the layout as ruled, and its first stage #531 filed; this branch's duplicate of it is deleted.

The two checks

Negative controls, each applied as a patch, shown to build, run, and restored:

control test EXIT
try_lock built with then_some (feature serial-try-lock-then-some, in CONTROLS) kernel-loom --test serial_lock, both models 101
msr hcr_el2 deleted virt_el2_drop (the boot halts silently) 1
SPSR_EL2_TO_EL1 = 0x3C4 virt_el2_drop (running at EL1 on SP_EL0) 1
HCR_EL2 declared with E2H, rerun with the kernel's E2H branch deleted virt_el2_drop (boot timed out before EARLY PANIC) 1
the loader's E2H0 test inverted (e2h0 == 0 refuses) virt_el2_drop: the loader prints …: REFUSED, HCR_EL2.E2H is RES1 … on the console and stops 1
the loader commit reverted whole, its test lines kept virt_el2_drop ("CPU: entered at EL2, HCR_EL2.E2H " not on the PL011) 1
the hosted-rustc fix reverted whole: this branch at 446447ca nightly run 36263298060: portability-macos, portability-linux and build each fail building LLVM for x86_64-unknown-toyos, reached from Assemble → LldWrapper → llvm::Lld, then the hosted rustc build failed. On macOS cmake's compiler check fails (ld: unknown file type); on Linux, Building LLVM for x86_64-unknown-toyos finds no ninja failure
assert_toolchain_is_honest's rust-lld refusal deleted toolchain::tests::a_toolchain_bin_without_cargo_is_one_rustup_narrates 101
the arch rules' glob and rename cases reverted whole (17f930eb's function hunks), the fixtures kept sourcegate::tests::a_pure_crate_that_names_an_architecture_is_red 101
for_executable's TLSDESC refusal deleted toyos-elf --test tables (an_executable_s_reservation_is_had_only_through_its_refusals) 101
the sweep after a placement deleted compiler::tests (placing a compiler left the one it replaced) 101
the loader's stack not page-rounded (the whole fix, with its PAGE) virt_early_panic under HVF: silent after the handoff, the image ending at …039 1
AArch64 mapped to TLS variant II toyos-elf --test tls 101
the library's R_AARCH64_TLSDESC refusal (rela::validate) deleted toyos-elf --test tables 101
the arch rules matching core::arch:: as a substring, as before sourcegate fixture tests, both 101
sweep without its keyed_idle guard compiler::tests 101
a missing primary record read as empty compiler::tests 101
copy_backward's chunk test reading 8, not 16 toyos-libc-copies 101

Earlier on this branch, and unchanged: every worktree given the primary's compiler, and a worktree's compiler placed beside the primary's stage2 (compiler::tests, EXIT=101 each); the seqlock writer's fence removed (panic_console_publish, EXIT=101); VBAR_EL1 0x800 off (virt_early_fault, EXIT=1).

Independent oracles:

  • rust-lld's own output for variant I: an aarch64-unknown-toyos executable with a 64-aligned PT_TLS addresses .tdata offsets 0 and 0x40 at TP+0x40 and TP+0x80 (add x0, x8, #0x40 / #0x80), which variant_i_agrees_with_what_lld_linked asserts.
  • QEMU TCG's EL2 (virtualization=on) and AAVMF, for the drop.
  • The M4's own CPU under HVF, which caught the SCTLR_EL1 RES1 bits and the stack alignment TCG let pass, and reds the unrounded-stack control.
  • loom, for the serial lock and the seqlock.
  • Rust's own copy_within, fill and sqrt, for libc's module on the host.
  • QEMU virt and edk2's ArmVirtQemu; the Arm ARM, for every barrier and descriptor bit.
  • For the hosted-rustc build, the GitHub-hosted nightly from a fresh clone on both hosts (portability-linux, portability-macos), and the fork's bootstrap source (compile.rs:1938, :2474; config.rs's default_linux_linker_overrides) for why each config line is there.

What I am unsure of

  • The loader's E2H0 refusal is shown only by its inverted control: no QEMU CPU model lacks FEAT_E2H0, so the refusing arm has not run on a CPU that needs it. ID_AA64MMFR4_EL1 is read by its encoding on the Arm ARM's word that the ID space reads as zero where a register is not implemented.
  • The reassembly relies on nothing linking for AArch64 during the hosted build, while the host stage2 has no rust-lld: the guest std it would link is already built by then. The nightly is the evidence, not a test of that premise.
  • The kernel's variant I layout is exercised by no AArch64 process until userland runs on AArch64; the arithmetic is host-tested against lld's output, and the kernel's use of it is by reading.
  • --arch aarch64 --build-only has run only with this worktree's keyed compiler. The nightly's fresh-clone jobs build the x86 image; their sysroot step builds the AArch64 std too, with the rust-lld std_config names in the reassembled stage2, but no AArch64 userland links there.
  • No lock-order test: the order is compiler key, then sysroot key, then worktree, and a test would have to run a compiler resolve and a sysroot build in one process against fakes of both; not written.
  • PAR_EL1's inner nibble: boot_map_write_combining accepts 0b0000 as well as 0b0100, because HVF reports 0x40.
  • src/CLAUDE.md still says a fork commit with another compiler/ is refused; the orchestrator owns that file.

What #532 adopts when it reconciles write_config and std_config

Both branches build the hosted rustc under lld = false and then rerun the host-only build under lld = true. What #532 does not have, and must take from here:

  1. HOST_LINKER_PIN in write_config and compiler::config_text: [target.<host>] with default-linker-linux-override = "off". Without it, Link everything that boots with rust-lld, and freeze toyos-ld #532's lld = !with_hosted_rustc flips CFG_DEFAULT_LINKER_SELF_CONTAINED_LLD_CC for an x86_64-unknown-linux-gnu host between the two builds. rustc_target reads it through option_env!, so the host rustc is rebuilt in both on Linux, and Linux host rustc's own default linker changes from main's.
  2. std_config(compiler, …): it takes the stage2 of the Compiler a worktree resolved (src/compiler.rs), not toolchain::stage2(rust_dir), and names toolchain::rust_lld(compiler) by path. A worktree whose fork has its own compiler/ builds its std with its own compiler's linker. Per architecture it loops over Arch::ALL, and links_through_toyos_ld() chooses the linker: toyos-ld for x86-64 here. Link everything that boots with rust-lld, and freeze toyos-ld #532's x86 switch makes that false for both, and the toyos_ld parameter goes.
  3. write_config's guest targets: Arch::ALL, with linker = "rust-lld" and rpath = false for each target that does not link through toyos-ld. Link everything that boots with rust-lld, and freeze toyos-ld #532's hosted build names ci-llvm/bin/lld by path for x86_64-unknown-toyos, because the hosted build's sysroots carry no rust-lld. That stays Link everything that boots with rust-lld, and freeze toyos-ld #532's for x86. AArch64 keeps rust-lld by name, since it links nothing during the hosted build.
  4. rust_lld() and the rust-lld refusal in assert_toolchain_is_honest are Link everything that boots with rust-lld, and freeze toyos-ld #532's own shape and names (here pub(crate)); the refusal test is the same.
  5. compiler::RECIPE moved to …, host linker pinned; 3. sysroot::RECIPE is untouched here. Link everything that boots with rust-lld, and freeze toyos-ld #532 moves it to …, linked by rust-lld; 3, which is right for the merge.

Filed

  • issues/kernel/the-saved-kernel-context-names-x86-registers.md (stage 4)
  • issues/kernel/msi-and-pin-routing-take-an-x86-vector-and-apic-id.md (stage 4)
  • 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)
  • issues/build/the-primary-rebuilds-its-compiler-on-compiler-alone.md
  • issues/build/the-toolkit-forks-resolve-an-x86-only-toyos-window.md
  • issues/build/ovmf-s-licence-record-names-no-openssl.md
  • issues/build/a-defect-only-contention-exposes-is-classified-as-a-wrong-sched.md
  • issues/build/issue-files-cite-paths-that-moved.md: citations that name paths this branch moved; whether a gate belongs is the owner's, since src/CLAUDE.md says documentation carries no gates.
  • issues/build/two-suites-in-one-worktree-race-on-the-c-corpus-libc.md
  • issues/kernel/fpu-isolation-s-thread-join-answers-not-found-on-main.md (main's, measured at 17eb66a4)
  • issues/build/a-guest-lane-leaves-the-toolchain-tarball-in-its-tmpdir.md (main's)
  • Appended: the quiesce recurrence to issues/kernel/quiesce-dump-holds-the-stopped-reds-wide-with-usb-transport-breaks.md, the part-typed 2 MiB page to the boot-map issue, the port's stages to issues/kernel/toyos-runs-on-arm64.md, with the three EL2-write mutations stage 4 is the first to red, and the loaded recurrences of quiesce_wakes_on_the_last_exit, quiesce_stops_the_machine and sched_check_build to their issues. issues/build/the-primary-rebuilds-its-compiler-on-compiler-alone.md is assigned to the ARM64 track, closed before its stage 4 lands.

🤖 Generated with Claude Code

https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK

Japabu and others added 18 commits September 26, 2026 13:18
toyos-elf decodes EM_X86_64 and EM_AARCH64 and refuses any other e_machine
as UnknownMachine; Layout::parse takes the machine its caller runs and
refuses an image for the other one as WrongMachine before anything is
mapped. RelocKind names what a dynamic relocation asks for, and
RelocKind::from_raw(machine, r_type) is the one place a number is read, so
AArch64's RELATIVE, GLOB_DAT, JUMP_SLOT and TLS_{DTPMOD,DTPREL,TPREL} decode
to the same kinds x86-64's do. The kernel and the loader name their machine
through their arch module (ELF_MACHINE).

toyos-ld:
- The link's machine is the one its objects name. The first object decides
  and an object for the other machine is refused by name; an object for any
  third machine is refused. Before, anything that was not x86-64 was treated
  as AArch64, and one x86-64 object turned a whole AArch64 link into x86-64.
- e_machine, the dynamic relocation types and the PE header's machine come
  from that machine. The three copies of the .rela.dyn writer are one
  function over one table.
- COFF relocation numbers are read per machine: ARM64 and AMD64 reuse the
  same small integers. ARM64 COFF instruction relocations carry their addend
  in the immediate they patch, decoded as LLD's applyArm64* do.
- An AArch64 GOT load (ADR_GOT_PAGE / LD64_GOT_LO12_NC) gets a GOT slot.

tests/machines.rs reads the outputs back with the object crate: an AArch64
PIE declares EM_AARCH64 and rebases with R_AARCH64_RELATIVE, its ADRP/ADD
pair reaches its target; an ARM64 PE declares 0xAA64 and its BL, ADRP/ADD
and ADDR64 land where the section layout says; a mixed link is refused.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
`src/arch.rs` is the one place the build system and the harness learn a
machine's target triples (userland, kernel, loader), its QEMU, its firmware
images, its guest CPU and how this host accelerates it. Every hardcoded
`x86_64-unknown-*` and `qemu-system-x86_64` that was an architecture choice
now reads it:

- `Plan` carries an `Arch`, and `cargo run -- --arch <name>` sets it; with
  none it is x86-64, the architecture whose userland boots to a desktop.
  Kernel, loader and ROOT memo keys and staged artifact names carry it, and
  the ESP gets the removable-media loader name UEFI gives that architecture.
  An aarch64 image is `target/bootable.aarch64.img`; x86-64's keeps its name.
- `GUEST_TARGETS` gains `aarch64-unknown-none-softfloat` and
  `aarch64-unknown-uefi`, so a sysroot carries the aarch64 kernel's and
  loader's libraries; the target list is part of the sysroot key.
  `USERLAND_ARCHS` names the architectures whose userland target the pinned
  compiler carries, and `HOSTED_ARCH` the one the hosted rustc runs on.
- The aarch64 kernel target is the softfloat one, and the softfloat
  certification asks each architecture its own question (no SSE; no NEON
  or FP).
- `kvm_usable`, `CPU_KVM` and `CPU_TCG` become `Arch::accel` and
  `Arch::cpu`: the host's own hypervisor (KVM on Linux, HVF on macOS) when
  the host is the guest's architecture, TCG otherwise.
- `cargo run`'s launch and the CI instrument spawn the architecture's QEMU;
  `virt` gets a GICv3, SMMUv3 as its IOMMU and `ramfb` as its GOP.
- clippy lints the kernel and the loader for both architectures, and the
  host job installs both architectures' bare targets.
- `Plan::without_userland` builds a ROOT with no program in it, for a boot
  that ends before the kernel starts one.

Behaviour on x86-64 is unchanged apart from one host class: an x86-64 guest
on an Intel Mac now asks for HVF instead of TCG, which is the same rule an
aarch64 guest on Apple silicon follows and which no one here has measured.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
… one MSI doorbell

- `kernel/src/arch/mod.rs` selects `x86_64/` (and, next, `aarch64/`) and
  re-exports it: generic code keeps naming `crate::arch::…`, and a
  `target_arch` cfg appears in that file and inside the implementation only.
  Every x86 file moves under `arch/x86_64/` unchanged.
- The syscall handlers are not architecture: `arch/syscall/*` moves to
  `kernel/src/syscall/`, and the entry gate, the one x86 file among them, is
  `arch/x86_64/syscall.rs` (`arch::syscall::init`). The build system's
  `SYSCALL_ENTRY_SYMBOL` follows the new path.
- One interrupt guard. `arch::IrqGuard` replaces `hw::IrqGuard`,
  `arch::LogCommitGuard` and `serial::BackendGuard`'s private `SavedFlags`,
  which were three spellings of pushfq/cli/popfq. Two of them declared their
  `asm!` `nomem`, which tells the compiler the instruction touches no memory
  and lets it move loads and stores across `cli` and `popfq`: the scheduler's
  guarded region and the console lock's CAS were not compiler-ordered against
  the interrupt mask. The one guard is a compiler barrier on both edges, as
  `LogCommitGuard` already was. `log-unbracketed-reserve` keeps its staging
  through `IrqGuard::unclosed`, compiled only with `boot-actuators`.
- One MSI doorbell: `arch::MSI_DOORBELL` and `arch::msi_message(dest,
  vector)`, used by PCI's compatibility message and by VT-d's remappable
  format and fault event, where `0xFEE0_0000` was written three times.

Behaviour on x86-64 is unchanged except for the compiler ordering above.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…named

`arch::barrier` is the ordering a device's view of memory needs: `dma_wmb`
(stores before stores, as a device's DMA reads see them), `dma_rmb` (loads
of device-written memory after loads), what an MMIO store is preceded by and
what an MMIO load is followed by, and `scanout_flush` for the
write-combining panel. On x86-64 each is a compiler barrier, except the
scanout's `sfence`; TSO gives the rest for nothing. A `fence(Release)` is
not one of them on AArch64, where it is `dmb ish` and orders nothing a
device, outside the inner-shareable domain, observes.

`Mmio`'s contract is now Linux's `writel`/`readl`: every `write_*` is
ordered after the stores this CPU made before it, and every `read_*` before
the loads it makes after. That ordering was the `Sync` impl's unstated
assumption ("volatile accesses order correctly regardless of which CPU
issues them"), which held for x86 TSO and uncacheable memory only, and it
also was not true of the compiler: `volatile` orders a store against other
volatile accesses, not against the plain stores of a descriptor.

Every DMA ordering site now names its barrier:
- virtio: descriptors before the avail idx (`dma_wmb`), the avail idx before
  the notify (the `Mmio` write), the used idx before the element it counts
  (`dma_rmb`, both consumers).
- NVMe: the submission entry before the doorbell (the `Mmio` write). Fixed:
  the completion entry and the data it completes were read with nothing
  after the phase-tag poll to order them; `dma_rmb` does.
- xHCI: fixed, a TRB was written as one 16-byte volatile struct store,
  whose order the compiler chooses, so a controller running the ring could
  see the Cycle bit before the body; `TrbRing::put` writes the body,
  `dma_wmb`, then the control word. Fixed, an event TRB was read whole
  and its Cycle bit checked after; `next_event` reads the control word,
  checks it, `dma_rmb`, then reads the TRB. Doorbells are `Mmio` writes.
- The black box's write-back is `arch::cache::write_back`, the panel's
  `sfence` is `barrier::scanout_flush`: no x86 instruction is left in
  either generic file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…de OVMF

`aavmf/AAVMF_CODE.fd` and `aavmf/AAVMF_VARS.fd` are QEMU 11.1.0's
`share/qemu/edk2-aarch64-code.fd` and `edk2-arm-vars.fd`, unmodified,
committed the way `ovmf/` is and hashed in `NOTICE` and the binary gate.

`Arch::pflash` is the one place a launcher gets its two `-drive` values.
AAVMF's variable store boots as a snapshot QEMU discards: the build is a
DEBUG one, and with a read-only store `BdsDxe` asserts on its first
`NorFlashWriteBuffer` (measured: `ASSERT_RETURN_ERROR (Status = Device
Error)` at `PlatformBm.c(1069)`). With the snapshot, `qemu-system-aarch64
-M virt,gic-version=3 -accel hvf -cpu host` reaches the UEFI shell on this
M4 Pro in 10 s, five of them the shell's own `startup.nsh` countdown.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…e under arch/x86_64

Code that is x86 whatever else changes moves under the architecture that
has it, unchanged, so the next architecture does not compile it:

- The PC platform's devices: the i8042, the I/O APIC, the CMOS RTC, the
  chipset's TCO watchdog and VT-d. Generic code reaches each by the concept
  it serves — `arch::keyboard_controller`, `arch::watchdog`, `arch::rtc`,
  `arch::iommu_unit` — and never by its name; `iommu`'s backend-neutral
  front (`crate::iommu`) calls `unit::` where it called `vtd::`, and its
  identifiers' constructors are `pub(crate)` rather than `pub(in
  crate::iommu)`, since the backend is no longer inside it.
- `mm::paging` is the x86 PTE format, PCID and CR3 through and through; it
  moves to `arch/x86_64/paging.rs` and `crate::mm::paging` re-exports the
  architecture's.
- `hw.rs` is the scheduler core's `Machine`/`Hw` implementation — x2APIC,
  the TSC, the `context_switch` frame and SYSRET's SS erratum — and moves to
  `arch/x86_64/hw.rs`; `crate::hw` names it.
- `nmi_gate`, the `boot-actuators` harness for the SYSCALL entry's
  user-stack window, is about an x86 window that an AArch64 exception entry
  does not have.

The gates that name files by path (sourcegate, kernelkeys, kernel-loom's
tally include) follow them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
… instruction

No `asm!`, no `extern "sysv64"` and no x86 register name is left outside
`kernel/src/arch/`. What each place asked of x86-64 it now asks of the
architecture by what it is:

- `arch::boot`: the loader's entry (`_start`, moved out of `main.rs`) and
  the steps `kernel_main` takes at the points where one architecture differs
  from another — the memory type before the panel, CR0 and the PAT after
  the console, the AP trampoline's reserved page, interrupt bring-up
  (MADT, reset register, local APIC, per-CPU block, I/O APIC, IDT, syscall
  gate), the HPET-calibrated clock and the CMOS wall clock, the timer, the
  i8042, starting the APs, and the interrupt-controller selftests.
  `kernel_main` is `extern "C"` now that assembly calls it.
- `arch::irqchip` is the APIC and `arch::trap` the IDT to every generic
  caller; `trap::report_panic`, `trap::kernel_backtrace` and
  `trap::frame_interrupts_enabled` replace reads of `rbp` and `RFLAGS.IF`.
- `arch::cpu::{counter, frame_pointer, undefined_instruction,
  thread_pointer, stated_counter_hz, run_on_stack}` replace `rdtsc`, `mov
  rbp`, `ud2`, the FS base, CPUID leaves 15H/16H and the idle loop's stack
  switch.
- `arch::entropy` is the CPU's random source (`RDRAND` here) for the
  hasher's seed and `sys_random`.
- `arch::console_uart` is the 16550 at COM1; `drivers::serial` keeps the
  lock, the backends and the line discipline, and `serial::init` takes the
  RSDP an architecture that places its UART by table needs.
- `arch::pio` is the I/O port space, and ACPI refuses a SystemIO reset or
  PM1a register on an architecture that has none.
- `arch::pmu` is the counter that samples a CPU by NMI; `hardlockup` keeps
  the decision.
- `arch::percpu` carries the preempt count, the reschedule flag, the fault
  state and the interrupt counters behind functions, not `gs:` offsets;
  `irq_took!` lives with the handlers that use it.
- `arch::entry::initial_frame` lays out a new stack's first frame and the
  three trampolines live beside it; `context_switch` is
  `arch/x86_64/switch.rs`; the HPET calibration is `arch/x86_64/hpet.rs`
  and `clock::set_counter` is what it hands the clock.
- The mapping vocabulary (`Prot`, `WindowProt`, `CachePolicy`,
  `MmioPolicy`) is `mm::policy`, each architecture's page tables encode it,
  and `CachePolicy::DeferToMtrr` is `Normal`. The GOP's memory-type line
  asks `paging::scanout_memory_type`.

Behaviour on x86-64 is unchanged; every line the boot writes is the same.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…he owner put them

The owner's rulings of 2026-09-26, written as tests: `src/sourcegate.rs`
gains `ARCH_RULES`, a table of rules each stated as the only places its
spellings may appear, beside the scans already there. It reads code only,
over every `.rs` file outside the rust fork.

1. Assembly lives in an architecture's own module: `asm!`,
   `global_asm!`, `naked_asm!`, naked functions and `core::arch`/`std::arch`
   intrinsics appear only in `kernel/src/arch/<arch>/`, the loader's
   `src/arch/` and `toyos-abi`'s per-architecture syscall entry.
2. An architecture is selected in one place: `target_arch` appears only in
   the kernel's selector and its arch modules, the loader's arch module,
   toyos-abi's syscall entry, the build system's one reading of its host
   (`src/arch.rs`) and `toyos-cc`'s default target.
3. Generic kernel code reaches the machine through the arch interface: no
   path into `arch::x86_64` or `arch::aarch64` outside `kernel/src/arch/`.
4. A pure crate names no architecture: none of the above in any of the
   eleven crates `CLAUDE.md`'s layout marks pure, `toyos-sched` among them.

Each rule has a fixture test that plants a violation and reds, naming the
rule and its places, and the whole-repository test refuses a declared file
exception that no longer holds a needle. No existing scan was folded in: the
`BANS` table's rows are per-file counts, which a place rule does not
express.

To make rule 1 true of the loader, its x86 instructions move into
`bootloader/src/arch/x86_64.rs`: the TSC read and the `IA32_TSC_ADJUST` line,
the black box's `CLFLUSH`/`SFENCE` write-back, the TCO block's port I/O
(refused by name on an architecture with no I/O port space) and the CR3
switch and entry jump. The `sysv64` the jump names stays there, because this
target's own `"C"` is the Microsoft convention. `src/release.rs` asks
`Arch::HOST` instead of spelling `target_arch`.

What is left outside an arch module is declared, row by row, and filed at
`issues/build/assembly-outside-an-arch-module-in-userland-and-guest-probes.md`:
libc's square roots, string moves and `_start`, two userland framebuffer
`sfence`s, and seventeen x86 guest probes. Every site in the kernel is gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…y concept

`Origin` (who asked for a TLB invalidation) is a generic census, so it
moves to kernel/src/invalidation.rs and the x86 tlb re-exports it. The
xHCI driver's assertion about the shootdown ACK timeout belonged to the
module that owns the timeout; it lives there now and the constant is
private. The syscall path's NMI-gate calls go through
`arch::syscall::note_entry` / `window_storm`, so dispatch and the
scheduler name no x86 mechanism.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…virt

The loader builds for aarch64-unknown-uefi through toyos-ld (PE ARM64),
reads ROOT through firmware and hands off; the kernel's AArch64 entry
drops from EL2 when entered there, writes the control-register
declaration whole, turns on the loader's tables, installs its vectors and
reaches kernel_main; the console is the PL011 SPCR names. Measured under
`-M virt,gic-version=3` with AAVMF: HVF enters at EL1, TCG with
`virtualization=on` at EL2, and both print the boot log, the memory map,
the MADT's GIC structures and the GTDT's timers, then stop on the first
owed item with an EARLY PANIC on serial and on the ramfb panel.

Shared, and changed on x86 too:
- toyos-bootmap types a leaf by concept (Cache::{Firmware, Memory,
  Device, Scanout}) under a Typing (x86: the MTRRs; AArch64: firmware's
  write-back ranges, a part-memory page refused), encodes both
  architectures' descriptors, and splits a scanout's partial 2 MiB pages
  into 4 KiB leaves where the map types memory: ramfb is carved out of
  RAM at 0xbc7a0000. The x86 plan is the plan it was.
- The halt every fatal path funnels into is generic (panic::halt_all_cpus);
  the architecture stops the other CPUs (irqchip::stop_other_cpus) and
  reports its fault stack (trap::report_fault_stack).
- The frame-pointer backtrace, the crash report's memory tail
  (mm::report_on_crash), vma::Occupancy and acpi::inventory's table list
  (arch::boot::ACPI_TABLES) moved to where both architectures reach them.
- panic_reboot names the counter, not the TSC, and the loader's jump takes
  the image and reads the root out of KernelArgs on both architectures.
- toyos-ld checks a declared /MACHINE: against the objects, and drops
  AArch64 mapping symbols ($x/$d), which repeat per section.
- toyos-acpi decodes SPCR, GTDT, and the MADT's GICC/GICD/GICR/ITS.
- The build system gives an architecture with no userland yet a ROOT with
  none (Userland::of).

The AArch64 kernel builds with -Adead_code (kernel/.cargo/config.toml,
exit: the port's stage 7): until the syscall gate, the interrupt
controller and the scheduler exist there, what only they reach is
unreachable. Every stand-in for later-stage work is an owed! that panics
naming its stage.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
`Profile::Virt` is QEMU `virt` (GICv3, AAVMF, ramfb, the boot stick on
xHCI, the PL011): a profile is a whole machine, so it is the profile that
says which architecture a boot is, and `qemu_command` takes the firmware,
binary, accelerator, CPU and machine from it. The image is built for that
architecture, and one with no userland yet is refused the suite's
programs.

Two tests, both `Tier::Local` — every unsharded `cargo test`, never a CI
shard, because no hosted runner boots AArch64 yet (the port's stage 8
measures that):

- virt_early_panic: the boot log stage 3 promises on the PL011 (the
  PL011 SPCR placed, the control registers as declared, the memory map,
  the MADT's GIC, the GTDT) and the early panic on serial and on the
  panel, in its fatal colours.
- virt_early_fault: an undefined instruction right after the console step
  reaches the vectors, which report the entry and the class and panic.

Negative control, run on this tree: `VBAR_EL1` pointed 0x800 past the
table (one added `add` in trap::install) — virt_early_fault goes red, the
misrouted exception reporting itself as "synchronous from EL0 in AArch32".

`Profile::Virt` boots with 2 GiB: AAVMF allocates from the top of RAM,
which past 4 GiB is outside the loader's boot map
(issues/boot-media/the-boot-map-reaches-4-gib-and-firmware-decides-what-lands-in-it.md).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Every one of the 761 lines naming `Relaxed` under kernel/src (84 files)
was read against who stores, who loads, on which CPU, and what the load
goes on to trust. What changed:

- The panic console's published descriptor was a seqlock over a plain
  cell: a painter's read of it raced the publisher's write, which the Rust
  memory model makes undefined whatever the sequence check afterwards
  says. It is now `drivers/panic_console/published.rs`, a seqlock whose
  payload is atomic words, compiled into kernel-loom and driven by
  `tests/panic_console_publish.rs` (a snapshot is one publication whole).
  Negative control, run: `--features seqlock-writer-fence-off` drops the
  publisher's Release fence and the model goes red with a torn snapshot.
- panic.rs's crash evidence stored its lengths after the bytes "so a read
  mid-fill reports a short string" — with Relaxed on both sides that held
  for nothing, not even an NMI on the same CPU. Release/Acquire.
- hardlockup's spin record promised "a non-zero lock means the site beside
  it was already stored" to a report another CPU writes. Release/Acquire.
- The USB transport-fault staging (msc::fault, boot-actuators) armed the
  fault and its opcode Relaxed before the count a disk on another CPU
  takes; the count is now the Release/Acquire edge, loaded first.

What stayed Relaxed, by kind: counters and statistics nothing else reads
through; boot-time publications made before the other CPUs exist, which
the roster's Release/Acquire release orders; per-CPU and same-thread
state; diagnostics whose two words may tear and say so (i8042's
unexplained bytes, a task's cpu_ns/running_since pair, nmi_gate's held
sample); and the primitives kernel-loom already models, whose Relaxed
arms are their negative controls.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
licence of AAVMF, and the loom control declared

`cargo run -- --ci host` reddened on four things this branch did:
- clippy's `manual_is_multiple_of` (toyos-bootmap, the AArch64 trap
  frame), a doc comment left dangling where irq_took! moved into the
  architecture, and a doc comment the architecture rules were inserted
  under (sourcegate's CORPUS);
- the harness's `build_boot_image_with`, which takes its eighth argument
  (the architecture) under an allow that says why;
- NOTICE's AAVMF section, which named its terms in the heading but carried
  no `SPDX-License-Identifier:` line for the licence gate to read;
- `seqlock-writer-fence-off`, a new kernel-loom control that src/ci.rs's
  CONTROLS and the kernel's declared-feature list did not account for.

After this, `cargo run -- --ci host`: 39 steps, all green (EXIT=0).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
`BackendGuard::try_lock` became `.is_ok().then_some(Self { _irq: irq })`
when the one IrqGuard replaced its hand-rolled flags (0840b25).
`then_some` builds its argument whether or not the exchange won, so a CPU
that lost the race built a guard, dropped it, and its drop stored
`BACKEND_LOCKED = false` under the CPU that held the backend. Two CPUs
then wrote the virtio-console at once and the second found the TX slot
taken: "vconsole: no tx slot", 254 of them in one fast-tier run, taking
the shared boot and 130 tests down beside other guests (each green alone).
Back to an `if`, as it was before the IrqGuard change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…ther guests

A fast-tier red on this branch, green alone, whose own log reads as a slow
guest rather than the lost wake its sentence names.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
@Japabu Japabu changed the title ARM64 port, stages 0-3: shared groundwork, aarch64 toolchain, loader and kernel to the PL011 ARM64 port, stages 0, 2 and 3: shared groundwork, and the loader and kernel reach the PL011 on virt Sep 26, 2026
@Japabu
Japabu marked this pull request as ready for review September 26, 2026 14:06
Japabu and others added 10 commits September 26, 2026 16:09
The AArch64 machine support (EM_AARCH64, R_AARCH64_RELATIVE, PE 0xAA64,
COFF ARM64 relocations) changed toyos-ld's identity, and abi-split reds
on a published crate that changed under the version it already had.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
check_compiler refused a linked worktree whose fork checkout's compiler/
was not the one the primary built stage2 from, so a target spec or any
compiler change could be neither built nor tested before it landed. That
contradicts the landed rule that every worktree builds the toolchain its
own sources name.

src/compiler.rs now resolves the compiler a worktree's fork checkout
names: the primary's stage2 where its compiler/ is the one recorded, and
otherwise a compiler of its own, built by bootstrap in the worktree's own
fork checkout (build/toyos-compiler, host only, stage 2 of compiler/rustc
and library, the primary's profile) and placed at
rust/build/compilers/<key>/, keyed on the identity of compiler/,
src/bootstrap, src/stage0 and Cargo.lock. The sysroot key already named
the compiler's identity, so a sysroot built from it is a new key and is
cloned from that compiler instead of the primary's.

Nothing of the primary's is written: not its stage2, not its record, not
the rustup toyos link (a sysroot is named by directory). The global lock
is not taken for such a compiler; its own key lock (buildlock's Keyed,
now shared with sysroots) orders before the sysroot key's.
--worktree remove sweeps compilers no worktree records. hosted-rustc is
refused in a worktree building with its own compiler: the hosted rustc is
the primary's.

compiler::source now also counts files git does not track under
compiler/, which is what a new target spec is before its commit.

Negative controls, each applied as a patch, built, run and restored:
every worktree given the primary's compiler -> the coexistence test
EXIT=101; a worktree's compiler placed beside the primary's stage2 ->
EXIT=101.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
… issue files cite moved paths

Two small tooling defects found while landing the port's stage 0:

- The shared-host green arm of the ALONE line names Sched::Parallel as the
  cause, and nothing groups wide failures that share a headline: at
  f67863c one kernel defect (BackendGuard::try_lock) read as 130
  scheduling classifications.
- 46 citations in 35 issue files name paths this branch moved, beside at
  least 14 whole kernel/src paths already missing on main. Whether a
  gate belongs is the owner's: src/CLAUDE.md says documentation carries
  no gates.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
… GOT

The aarch64-unknown-toyos std and userland link through toyos-ld, and
their thread-locals are TLS descriptor sequences (adrp, ldr, add, blr):

- In an executable a descriptor for a variable it defines relaxes to
  local-exec (movz x0; movk x0; nop; nop), and one another module defines
  to initial-exec (adrp x0; ldr x0; nop; nop) over a GOT slot named by
  R_AARCH64_TLS_TPREL. The offsets are TLS variant I:
  align_up(16, TLS_ALIGN) + the variable's offset in the block, where
  TLS_ALIGN is the 64 the layout and PT_TLS already used.
- A shared library keeps the sequence, points it at a 16-byte descriptor
  pair per symbol, and asks the loader for it with R_AARCH64_TLSDESC.
- R_AARCH64_TLSIE_* and R_AARCH64_TLSLE_ADD_TPREL_* are applied; a
  local-exec reference in a shared library is refused.
- PREL64, LD_PREL_LO19, ADR_PREL_LO21, CONDBR19 and TSTBR14 are applied,
  with their ranges checked.

Two defects the AArch64 std link exposed: an AArch64 GOT relocation
looked only in the static GOT, so a shared library's import (its slot is
in dyn_got, filled by GLOB_DAT) was an undefined symbol; and the ELF
layout wrote x86-64 PLT stubs (FF 25) for every machine. The stub is
now the machine's own (adrp x16; ldr x16; br x16 on AArch64).

Tests read the outputs back with the object crate against the ABI's
numbers. Mutations, each applied, built, run and restored: the TCB gap
without the segment's alignment, EXIT=101 (the local-exec test); a
descriptor's argument not relative to the block, EXIT=101 (the shared
test); the dyn_got lookup removed, EXIT=101 (the import test).
Differential oracle: one rustc-built object with two TLSDESC sequences,
linked by LLVM's lld and by toyos-ld, is rewritten identically
(movz; movk; nop; nop); the offsets differ only by each output's PT_TLS
alignment (lld 8: 0x10/0x18; toyos-ld 64: 0x40/0x48).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…Arch64

The rust pin moves to 4874af48e26 (on toyos-inbox): the
aarch64-unknown-toyos target spec, and std's `_start` and TLS descriptor
resolver on AArch64 (variant I, `__toyos_tlsdesc_dynamic`). It is a
compiler change, so this worktree builds its own compiler
(src/compiler.rs) and the primary's is untouched.

- The build: `GUEST_TARGETS` gains aarch64-unknown-toyos, and every
  architecture has a userland, so `USERLAND_ARCHS` and the plan's
  `Userland` switch (whose `Absent` arm nothing could reach any more,
  and whose `without_userland` nothing called) are deleted. `cargo run --
  --arch aarch64 --build-only` builds std, libtoyos_c and the userland
  for AArch64 and puts them on ROOT.
- libc: `_start`, memcpy/memmove/memset and the square roots move into
  `userland/libc/src/arch/`, one module per architecture, and gain their
  AArch64 versions (pair loads and stores with a byte tail, fsqrt). The
  copies were checked on this AArch64 host against its own
  copy_within/fill over 360,000 cases (lengths 0..300, offsets 0..20,
  overlapping both ways); a copy_backward whose chunk test was 8 instead
  of 16 died on the same check (SIGBUS, exit 138).
- toyos-window's and metalprobe's scanout drain moves into an arch
  module (SFENCE; DSB ST on AArch64), and the ARCH_RULES rows for them
  and for libc become module places. toyos-window is published, so it
  bumps to 0.16.0: 0.15.0 is taken by #525 and #528.
- calc, snake and doom are left off an AArch64 ROOT, by name at build
  time (`NOT_YET_BUILT`): softbuffer's and winit's toyos forks resolve
  the published toyos-window 0.2.0, whose framebuffer is x86-64 only
  (issues/build/the-toolkit-forks-resolve-an-x86-only-toyos-window.md).
  Everything else in the userland workspace, and toyos-ld and toyos-cc
  as guest programs, builds and links for AArch64.

compiler.rs keys a compiler on the content of its sources alone:
keying on the git spelling of compiler/ made committing what had been
built as local changes a new compiler and a full rebuild.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…rough rust-lld

The owner's ruling of 2026-09-26 moves ToyOS from toyos-ld to rust-lld:
lld's output is accepted by toyos-elf and the loader, byte-reproducible
and much smaller (snake 873 KB against 1.99 MB, because toyos-ld's GC
roots every .eh_frame), and toyos-ld is frozen, to be deleted. This
branch stops extending it: every toyos-ld change it made (the machine
parameter, EM_AARCH64, R_AARCH64_RELATIVE, PE ARM64 and /MACHINE:, the
AArch64 TLS descriptor output, the AArch64 GOT and PLT fixes, and the
0.3.0 bump) is reverted by this commit, leaving toyos-ld byte for byte
main's. x86-64 links exactly as it did; the x86 switch to rust-lld is
another branch's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
By the owner's ruling of 2026-09-26. The rust pin moves to c3cb792df3f
(on toyos-inbox): aarch64-unknown-toyos names rust-lld with the GNU
flavour. The toolchain is built with `lld = true`, so every compiler and
sysroot carries rust-lld, and bootstrap is told the AArch64 userland's
linker (rust-lld, by name for the primary's stages and by path for a
sysroot's std) with `rpath = false`: bootstrap's `-Wl,-rpath` is a cc
driver's argument, and a guest std records no host library path.
`Arch::links_through_toyos_ld` says which architecture still links
through the frozen toyos-ld: x86-64, whose linking this branch does not
change.

The kernel's and the loader's AArch64 targets drop their toyos-ld
override and link with the targets' own rust-lld. The loader takes the
kernel as a static PIE at base 0, which rust-lld makes of the target's
static-model code with `-pie -z notext`; position-independent codegen
would reach symbols through GOT slots holding virtual addresses, which
the entry cannot use with the MMU off.

rust-lld's layout exposed a loader defect: the kernel's stack began at
the image's last byte, wherever the linker ended it. lld ends this kernel
at 0x…908, and on the M4 the first `stp x29, x30, [sp, #-16]!` took an SP
alignment fault (ESR_EL1 0x9A000000, read through QEMU's gdb stub on a
hardware breakpoint at firmware's synchronous vector); TCG does not
check SCTLR_EL1.SA there, so only HVF saw it. The stack now starts on
the next page.

virt_early_panic and virt_early_fault: 2 passed, EXIT=0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
AAVMF waits its platform boot timeout, five seconds, for a key before it
boots, unless QEMU hands it a menu wait through fw_cfg, which only
`-boot menu=on` does; `splash-time=0` makes that wait zero. Both launch
paths pass it for AArch64 (`Arch::boot`); x86-64's is unchanged.

Measured on this M4 Pro, QEMU's start to `EARLY PANIC:` with
test-early-panic armed, the pinned DEBUG AAVMF, three runs each,
interleaved in one session: 5.70, 5.69, 5.70 s without it (the loader's
first line at 5.32-5.33 s), 0.69, 0.69, 0.70 s with it (the loader at
0.33 s).

This is the delay the owner's ruling of 2026-09-26 meant to remove by
moving to a RELEASE build, and it is not the DEBUG build's: Debian 12's
RELEASE AAVMF (edk2 2022.11), on the boots where it reached the kernel,
took 5.71 and 6.08 s. The RELEASE builds tried did not boot reliably
either (Debian 13's 2025.02-8+deb13u1: never reached the loader in six
boots; Debian 12's 2022.11-6+deb12u2: 2 of 5), so the pinned firmware
stays.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
This branch's AAVMF entry named only BSD-2-Clause-Patent. QEMU builds
its ArmVirtQemu firmware with NETWORK_TLS_ENABLE (roms/edk2-build.config
at v11.1.0, [opts.common]), and edk2-stable202408's ArmVirt.dsc.inc links
the bundled OpenSSL with TLS on (OpensslLib) or off (OpensslLibCrypto).
The submodule pins OpenSSL 3.0.9 (openssl de90e54b, VERSION.dat), whose
LICENSE.txt is Apache-2.0 and which ships no NOTICE; the text is added as
licenses/Apache-2.0-OpenSSL.txt, and the section and both ledger rows say
BSD-2-Clause-Patent AND Apache-2.0. OVMF's record has the same gap and is
not this branch's: issues/build/ovmf-s-licence-record-names-no-openssl.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Found running the fast-tier A/B for PR #524: three suites at once in one
worktree, on main and on the branch alike, lost four of thirty runs to a
C corpus link that found no libtoyos_libc.a mid-rebuild.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Japabu and others added 8 commits September 26, 2026 21:03
At EL2 the loader reads HCR_EL2.E2H and ID_AA64MMFR4_EL1.E2H0 (by its
encoding, S3_0_C0_C7_4, which an older CPU reads as zero) before
ExitBootServices, says what it found on the firmware console, and refuses
a CPU without FEAT_E2H0, where E2H is RES1 and the kernel's drop cannot
clear it. The kernel's own tbz branch to refused_hcr_el2_e2h is deleted:
the read-back compare that follows it already refuses an E2H held set,
since the declaration has it clear, and no test could tell the two apart.
virt_early_panic and virt_el2_drop now assert the loader's line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
RelaCounts::for_executable in toyos-elf refuses a TLSDESC count, or a group
too large for one allocation, and is the only way to an ExeReservation, the
capacities parse_rela_entries reserves. The tlsdesc Vec, kept only to be
refused, and its as_relas arm are deleted; the kernel cannot skip the
refusal without losing the reservation. toyos-elf's tables test holds both
refusals. Static::variant, which nothing called, is deleted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
A compiler edit loop placed one 583 MB compiler per edit and reclaimed none
until --worktree remove. choose now sweeps after it places one; the keyed
idle guard keeps the one in use. sweep takes the rust dir it is to sweep,
so a linked worktree's call does not resolve the primary again. The test
shows a placement removing the compiler it replaced and keeping the one
another worktree names.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
… has an owner

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…s branch

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…528)

Conflicts, each resolved to carry both sides:
- bootloader/src/main.rs: the boot map's ROOT_* names with #530's update imports.
- src/image.rs: main's slotted layout, with `arch` threaded into
  create_boot_image and create_esp_volume for the removable loader's path.
- src/build.rs: main's Parts, shipped_parts and signing, keyed per
  architecture: loader_key takes the arch, the embedded key and the floor
  scope; root_image_key the plan (config and arch) and the key; build()
  writes image_for(plan.arch).
- tests/common/qemu.rs: a test's own firmware variables file where it names
  one, and otherwise the architecture's pflash.
- toyos-window is 0.19.0, past main's 0.18.0, since its scanout drain moved
  into arch modules here; both lockfiles take main's and that version.
- tests/common/update.rs names the x86-64 plan and image.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
#528's tests/iced-counter locks the path toyos-window, which the merge moved
past main's 0.18.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
fpu_isolation's probe thread is joined as NotFound on main at 17eb66a, measured
there by checking the base out in this worktree; the guest lanes leave the
toolchain tarball in their TMPDIR. quiesce_stops_the_machine and
sched_check_build recur under another worktree's load.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
@Japabu

Japabu commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator Author

Hand-back for round 2, at head 8c5be843.

BLOCKER 1, the hosted-rustc build. The hosted build now runs with lld = false. build_hosted_rustc then reruns the host-only build with lld = true, which reassembles stage2 with rust-lld and compiles nothing. The host's default-linker-linux-override is pinned "off" in every host-compiler config. Without the pin, bootstrap ties it to lld on x86_64-unknown-linux-gnu, and the toggle would rebuild the host rustc twice. assert_toolchain_is_honest refuses a toolchain with no rust-lld, by name. The rejected alternative and what #532 must adopt are in the body.

  • Negative control, this branch at 446447ca (run 36263298060): portability-linux, portability-macos and build each fail building LLVM for x86_64-unknown-toyos, reached from Assemble → LldWrapper.
  • The nightly at 8c5be843 (https://github.com/ToyOSOrg/ToyOS/actions/runs/36273557690):
    • portability-linux success, portability-macos success;
    • build success, host success;
    • the reassembly took 0:05, 0:25 and 0:07.
  • Also green before the merge, at d8bee311 (run 36266825579).

BLOCKER 2, the arch rules. A glob of core/std, or a rename of either in an import, is refused outright outside arch modules. That covers use core::*, core::{*}, use core as k, extern crate core as k, core::{self as k} and use {…, std as k}. The reviewer's plants are fixtures, and two non-imports stay green. Reverting the function hunks while keeping the fixtures gives EXIT=101.

NOTEs.

  • The loader refuses E2H RES1 at EL2 by name, and the kernel's E2H branch is deleted. Inverting the loader's check gives EXIT=1 with the refusal printed. Reverting the loader commit gives EXIT=1. HCR_EL2 declared with E2H gives EXIT=1.
  • The three EL2 writes are recorded under stage 4 of the track.
  • TLSDESC is refused on its count through RelaCounts::for_executable, the only way to the reservation. The Vec is gone. Deleting the refusal gives EXIT=101.
  • Static::variant is deleted.
  • The compiler-key issue is assigned to the ARM64 track.
  • The compiler sweep runs after every placement. Deleting it gives EXIT=101.
  • The rust-lld refusal deleted gives EXIT=101.

Merge. origin/main moved to fd62f567 (#530, #528) and is merged at a1a143f8. Seven conflicts were resolved; toyos-window is 0.19.0.

Gates. At 235c5a5b; the head adds only issues.

  • --ci host: 45 steps, EXIT=0, output read whole.
  • --ci abi-split: EXIT=0.
  • kernel-loom: 56 passed, EXIT=0.
  • virt_: 3 passed, EXIT=0.
  • --build-only: x86 EXIT=0, aarch64 EXIT=0.
  • Fast tier: 397 passed, 4 failed, EXIT=1, under another worktree's load (load average 20 to 28). Each red was green alone or is main's.

Filed. Main's defects the nightly exposed:

  • fpu_isolation's thread_join answers NotFound. It is red at main's 17eb66a4, measured by checking that base out here.
  • Every guest lane reds on the toolchain tarball left in $TMPDIR.

…he moved syscall and fpu paths

This branch lets a worktree whose fork compiler differs build its own,
and moves the syscall layer out of arch/ and fpu.rs into arch/x86_64/.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
@Japabu

Japabu commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator Author

Round-2 BLOCKERs closed and judged at 8c5be84: the hosted-rustc build runs with lld off and a host-only lld pass restores rust-lld (nightly 36273557690: portability-linux, portability-macos, build and host green; the negative control is 446447c's run 36263298060, red on all three trying to build LLVM for ToyOS); the arch rules refuse any glob or rename of core/std outside arch modules (revert EXIT=101). The guest lanes' reds trace to main (#529's TMPDIR check vs the toolchain tarball, fpu_isolation red at main's own base) and are filed. src/ and kernel/ CLAUDE.md updated by the orchestrator at 50a1939. Landing.

@Japabu
Japabu added this pull request to the merge queue Sep 26, 2026
Merged via the queue into main with commit 5e446e5 Sep 26, 2026
2 checks passed
Japabu added a commit that referenced this pull request Sep 26, 2026
#524 landed. Its move of kernel/src/arch/syscall/proc.rs to kernel/src/syscall/proc.rs carries the join fix. release.rs and sourcegate.rs merged clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Japabu added a commit that referenced this pull request Sep 26, 2026
issues/build/a-guest-lane-leaves-the-toolchain-tarball-in-its-tmpdir.md
is fixed by 80e657c: the install stages in a `toyos_tmpdir::TempDir`.

issues/kernel/fpu-isolation-s-thread-join-answers-not-found-on-main.md
is fixed by 05839b7: the join keeps the answer its collect gave. The
landing it left unbisected is #513, whose `watch::wait_until` re-runs
the predicate that collects.

Nothing cites either file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Japabu added a commit that referenced this pull request Sep 27, 2026
Every conflict carries both sides:

- rust: pinned to 209f10ded88 on the fork's wt-toyos-lld, which holds this
  branch's a84b4aa849c and main's c34ecdf0ab6 (merged as 3980fb86829, with
  none of toyos-inbox's other lines) and moves the linker, its flavor and
  -Bsymbolic into base::toyos for both ToyOS targets.
- src/toolchain.rs: #524's write_config shape (Arch::ALL guest targets,
  HOST_LINKER_PIN, the reassembly and its rust-lld assertion), with every
  userland target naming rust-lld and rpath = false; the hosted build names
  ci-llvm's lld for the hosted architecture. toyos-ld's build, witness and
  install path stay deleted.
- src/sysroot.rs: std_config(compiler, ...) names the compiler's rust-lld for
  every architecture; RECIPE keeps main's text and this branch's "linked by
  rust-lld; 3".
- src/arch.rs: links_through_toyos_ld is deleted, since no architecture does.
- src/build.rs: main's GuestEnv fields without PATH; src/release.rs: main's
  HOSTED_ARCH paths without the toyos-ld witness.
- kernel/src/loader/tls.rs, toyos-elf/src/tls.rs: main's variant I/II layout,
  with variant II's executable placed at its extent (exe_extent).
- toyos-bootmap: main's typed Plan, with the loader's image as a third input
  on both architectures, counted against one directory budget.
- bootloader/src/main.rs: main's stack rounding kept as the one fix, main's
  enter_kernel, and the loader image read off LoadedImage into the Plan.
- kernel/, bootloader/.cargo/config.toml: main's, without toyos-ld; the
  bootloader keeps /Brepro and /DEBUG:NONE.
- kernel/src/arch/x86_64/paging.rs: main's; its WindowProt comment moved to
  kernel/src/mm/policy.rs and takes this branch's wording there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Japabu added a commit that referenced this pull request Sep 27, 2026
Conflicts, each resolved onto main's layout with this branch's change kept:

- rust: pinned at 64a2050c484, this branch's 23783dc3524 merged with main's
  c34ecdf0ab6 in the fork (`wt-toyos-logtrack`), and merged into
  `toyos-inbox` (645d1c70732). `git merge-base --is-ancestor` gives 0 for
  both pins against it and for it against `origin/toyos-inbox`. Its
  `compiler/` is c34ecdf0ab6's byte for byte; the inbox head 26f662d303a was
  not taken because it also carries x86-64's switch to rust-lld, another
  branch's compiler change.
- panic.rs: main moved `halt_all_cpus` here from `apic.rs`; this branch's
  version stops the other CPUs first and has no wait for `/log`, so
  `wait_for_log_file`, `LOG_FILE_DRAIN` and `LOG_DRAIN_EXPIRED` go from
  panic.rs as they went from apic.rs, and `tests/common/power.rs`'s check for
  that string, which the kernel no longer prints, goes with them.
- serial.rs: this branch's two locks and burst writers over main's
  `arch::console_uart`, `serial_lock::BackendLock` and `IrqGuard`. The burst
  is `console_uart::TX_BURST`: 16 on the 16550, whose THRE means an empty
  FIFO, and 1 on the PL011, whose TXFF clear promises one byte.
- clock.rs: the clock page is published from main's `set_counter`.
- paging.rs: `translate_writable` on main's x86-64 `AddressSpace`, and an
  uninhabited stub on AArch64's.
- virtio.rs: `publish` and `Doorbell` on main's barriers: the doorbell is an
  `Mmio` write, which orders the idx before it.
- loader/tls.rs: main lays out variant I (TP+0 the DTV pointer, TP+8
  zeroed); `toyos_abi::TCB_TID` is TP+8 there, so the tid write does not
  touch the DTV pointer.
- toyos-window goes to 0.20.0, past main's 0.19.0; toyos-abi 0.16.0 and
  toyos 0.18.0 are already past main's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Japabu added a commit that referenced this pull request Sep 27, 2026
The merge took #524's `pub(crate)`; tests/common/compile.rs links every C test
with the sysroot's rust-lld through it, from outside the crate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Japabu added a commit that referenced this pull request Sep 27, 2026
Red on main's nightlies at e8d7c9c and fd62f56 and on #524's, in two shapes; green alone on the dev host. No issue carried it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Japabu added a commit that referenced this pull request Sep 27, 2026
…nightly and this branch's

Found in this branch's nightly run 36287592139; the same signature is in
#524's branch nightly, so it predates the linker switch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Japabu added a commit that referenced this pull request Sep 27, 2026
…, #531, #534) into the Netstack3 netd

Two conflicts. tests/common/origin.rs: main added `volumes` to the imports
this branch had split to name `segment`'s NEIGHBOUR and NEIGHBOUR_MAC; both
kept. userland/Cargo.lock: main's lock taken whole and re-locked against this
branch's manifests (`cargo metadata`), which adds the mirrored crates and their
dependencies and drops smoltcp and its defmt.

The rust gitlink fast-forwarded to main's fbf6ad143d8; this branch's pin
(7a809b7591f) is its ancestor, and the branch made no fork commit of its own.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
@Japabu
Japabu deleted the wt/toyos-arm64 branch September 28, 2026 09:46
Japabu added a commit that referenced this pull request Sep 28, 2026
…ows, and file the harness's misreport

quiesce_stops_the_machine was disabled behind a finding whose title and exit
rested on the harness's message that the guest asked for a reboot and stayed
up. PR #566's capture at 74f7d71 refutes that: writer 5's first pass ran
from 2.170 s to 7.434 s, the job printed "5 of 6 writers reached their loop
in 5s" at 6.874 s and exited 1, and it never printed "asking for the reset".
No stop began. PR #524's capture at 235c5a5 shows the same with "3 of 6",
and nightly run 36351950439 on PR #555 at d265676 with "4 of 6".

- The slow pass is the defect quiesce_dump_holds_the_stopped's issue already
  tracks, whose exit names a first write-and-fsync pass over 5 s. That issue
  is renamed to what both tests show, and gains these sightings. Both rows
  point at it.
- The stops finding is folded into it and deleted. It carried no durable line
  for a module header; its three sightings move with their evidence. Its
  a58abf5 sighting also had no stop: record, and whether that job printed
  its give-up line was not recorded.
- stopped_boot waits its whole QMP budget and then calls
  returned_to_firmware before it reads the console, so a job that never asked
  is reported as a guest that asked. In the #566 capture every scheduler
  heartbeat from 10.750 s to 253.244 s was idle, and the test went red after
  266 s. Filed as tooling, held by the orchestrator.
- The park issue is renamed: its records show 0 block operations open, so
  both threads were running, and one was the held thread, which
  last::hold keeps spinning while a sweep counts 2. It now names each
  sighting's PR and head, adds the 98e803c stop that gave up, labels the
  dispose_yield suspect as a hypothesis, and records that
  woken_by_its_threads has no enabled caller. Its exit asks for an
  instrument that names each thread still running, and for that coverage
  back.
- Deleted: the scratch log names and paths, "after a stop that reported
  every thread stopped", "on a loaded host", and the build/ park issue's
  "--known-red answers NO".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Japabu added a commit that referenced this pull request Oct 8, 2026
A compiler is bootstrap's stage 2 of compiler/rustc and library, so the
stage2 a store keeps carries a std for the host, which every guest crate's
build scripts and proc macros link, and its key read nothing of library/:
a fork commit moving only library/ kept the stored compiler with the std
of the commit before. True of a worktree's compiler since #524 and of
every compiler since the primary's became keyed. The orchestrator's
ruling: fixed here, at the price of a compiler build for a fork commit
that moves std (about 5 minutes on an idle host, on top of the
freestanding libraries and the sysroot such a commit builds already).

every_source_of_a_compiler_moves_its_key holds it. Every compiler's key
moves, and each freestanding libraries' and sysroot's with it; the LLVM's
does not.

The two refusals in sysroot.rs of a product whose sources moved while it
was built stay untested, since both run bootstrap inline: filed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
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