From db3a66d8cdea0c7b6829fdf8760e59e091eb930c Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Sun, 31 May 2026 22:41:26 +0300 Subject: [PATCH 1/7] =?UTF-8?q?docs(adr):=20propose=20ADR-0039=20=E2=80=94?= =?UTF-8?q?=20userland=20build=20pipeline=20(B6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settles the build orchestration ADR-0029 deferred: a tools/build-userland.sh step + rust-objcopy from the pinned llvm-tools-preview (no Cargo dep) + include_bytes! of a git-ignored .bin; userland/hello + tyrne-user as default-members-excluded workspace members; a Rust-side base-VA source-of-truth. Opens T-027 (build pipeline) + T-028 (EL0 wire-up) at Draft per ADR-0025 Rule 1. cargo-xtask named as the B7+ multi-binary upgrade path. Refs: ADR-0039 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../phase-b/T-027-userland-build-pipeline.md | 52 ++++++++ .../phase-b/T-028-el0-userspace-wireup.md | 51 ++++++++ .../decisions/0039-userland-build-pipeline.md | 115 ++++++++++++++++++ docs/decisions/README.md | 1 + 4 files changed, 219 insertions(+) create mode 100644 docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md create mode 100644 docs/analysis/tasks/phase-b/T-028-el0-userspace-wireup.md create mode 100644 docs/decisions/0039-userland-build-pipeline.md diff --git a/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md b/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md new file mode 100644 index 0000000..03ed1be --- /dev/null +++ b/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md @@ -0,0 +1,52 @@ +# T-027 — `userland/hello` + `tyrne-user` + the raw-flat build pipeline (B6 step 5) + +- **Phase:** B +- **Milestone:** B6 — First userspace "hello" (step 5 of the [B6 dependency-ordered sequence](../../../roadmap/phases/phase-b.md#milestone-b6--first-userspace-hello) — `tyrne-user` crate + `userland/hello/` crate + the `cargo build → rust-objcopy -O binary → include_bytes!` pipeline + the shared base-VA source-of-truth) +- **Status:** Draft (opened in the [ADR-0039](../../../decisions/0039-userland-build-pipeline.md) Propose commit per [ADR-0025 §Rule 1](../../../decisions/0025-adr-governance-amendments.md); implementation follows ADR-0039 Accept) +- **Created:** 2026-05-31 +- **Author:** @cemililik (+ Claude Opus 4.8 agent) +- **Dependencies:** [ADR-0039](../../../decisions/0039-userland-build-pipeline.md) (the build-pipeline orchestration this implements); [ADR-0029](../../../decisions/0029-initial-userspace-image-format.md) (the raw-flat format — entry at offset 0, image `USER|EXECUTE`); [ADR-0006](../../../decisions/0006-workspace-layout.md) (the workspace-member / `tyrne-` naming conventions); [ADR-0031](../../../decisions/0031-initial-syscall-set.md) + [ADR-0030](../../../decisions/0030-syscall-abi.md) (the syscall ABI the `tyrne-user` wrappers + the SVC sequence target); [T-019](T-019-task-loader.md) (the loader that maps this image; pins `USERSPACE_IMAGE_BASE_VA`). +- **Informs:** Produces the real userspace image (replacing the hand-coded `USERSPACE_IMAGE` placeholder) so [T-028](T-028-el0-userspace-wireup.md) can load + run it in EL0. Does **not** itself run a task — the image is embedded but dormant until T-028. +- **ADRs required:** [ADR-0039](../../../decisions/0039-userland-build-pipeline.md) (must be Accepted before implementation). No new `unsafe` in the kernel; the only kernel-side change is the `include_bytes!` source of `USERSPACE_IMAGE` (data, not code). + +--- + +## User story + +As a kernel developer, I want a **real** userspace program compiled from Rust source — not a hand-coded byte literal — embedded into the kernel image through a reproducible, auditable pipeline, so that B6's first EL0 task is a maintainable program (greeting + clean exit) and every future userspace program follows the same shape. + +## Context + +ADR-0029 chose the raw-flat image format and deferred the build orchestration to B6; ADR-0039 settles that orchestration (a `tools/build-userland.sh` step + `rust-objcopy` from the pinned `llvm-tools-preview` + `include_bytes!` of a git-ignored `.bin`, with the userland crates as `default-members`-excluded workspace members). T-027 implements it. The current `USERSPACE_IMAGE` ([`bsp-qemu-virt/src/main.rs`](../../../../bsp-qemu-virt/src/main.rs)) is the placeholder `mov w0, #42; ret`; T-027 replaces it with the objcopy output of a `userland/hello` crate that, when run (T-028), will `console_write` a greeting and `task_exit`. + +## Acceptance criteria + +- [ ] **`tyrne-user` crate** (`userland/` or workspace root, package `tyrne-user`, `#![no_std]`): safe wrappers over the [ADR-0031](../../../decisions/0031-initial-syscall-set.md) syscalls needed by `hello` — at minimum `console_write(cap: CapWord, buf: &[u8]) -> Result` and `task_exit(code) -> !` — each a thin `svc #0` inline-asm shim packing the ABI ([ADR-0030](../../../decisions/0030-syscall-abi.md): `x8` = number, `x0..x5` = args; `ConsoleWrite = 5`, `TaskExit = 4`). No dependency on kernel internals (the ABI constants are restated/owned userspace-side or shared via a leaf crate). +- [ ] **`userland/hello` crate** (package `tyrne-userland-hello`, `#![no_std] #![no_main]`): a `_start` entry placed at **offset 0** of the linked image (linker `ENTRY` + a `KEEP`'d first section) that calls `tyrne_user::console_write` with a greeting string (living in the image's read-only data — reachable as a `USER` page, [T-025](T-025-user-access-translation.md) gate #1 requires only the `USER` flag for a read) then `tyrne_user::task_exit(0)`; a minimal `#[panic_handler]` that `task_exit`s or loops (no unwinder — `panic=abort` inherited). +- [ ] **Userland linker script** places `.text` (entry first) + `.rodata` contiguously at `USERSPACE_IMAGE_BASE_VA`, **no `.data`/`.bss`** (writable globals would fault — image is `USER|EXECUTE`, no `WRITE`), 16-byte aligned; produces a contiguous byte stream with no ELF artifacts under `rust-objcopy -O binary`. +- [ ] **Base-VA source-of-truth:** a single Rust-side `pub const USERSPACE_IMAGE_BASE_VA = 0x0080_0000` (read by the BSP loader call site); the userland linker script restates the literal with a documented `keep-in-sync-with` comment (LD cannot import a Rust const — per [ADR-0039](../../../decisions/0039-userland-build-pipeline.md)). +- [ ] **Build orchestration** ([`tools/build-userland.sh`](../../../../tools/build-userland.sh)): `cargo build -p tyrne-userland-hello --target aarch64-unknown-none` (release + debug as the kernel profile dictates) → `rust-objcopy -O binary` (resolved from the active toolchain's `llvm-tools` — **no Cargo dependency**, K3-8 unfired) → a git-ignored `.bin` at a stable path. Clear error if `llvm-tools` / the ELF is missing. `.gitignore` updated. +- [ ] **Workspace integration:** `userland/hello` + `tyrne-user` added to `members`, **excluded from `default-members`** (host commands skip them — mirrors `bsp-qemu-virt`); `cargo host-test` / `cargo build` / `cargo host-clippy` unaffected (verified). +- [ ] **BSP embed:** `USERSPACE_IMAGE` becomes `include_bytes!()`; [`bsp-qemu-virt/build.rs`](../../../../bsp-qemu-virt/build.rs) gains `rerun-if-changed` on the `.bin` and a `panic!` naming `tools/build-userland.sh` if it is absent. +- [ ] **`tools/smoke.sh` runs the userland build first** so the canonical smoke + CI path is unchanged (still one entry point). +- [ ] **Disassembly verification:** the objcopy'd `.bin` round-trips — a host test (or a documented `rust-objdump -d` check in the task's review-history) confirms offset 0 is the entry instruction and the SVC sequence + greeting match `hello`'s source. (The image is **not run** in T-027 — running is T-028.) +- [ ] **All gates green:** `cargo fmt --all --check`; host + kernel clippy `-D warnings`; host tests unchanged + green; `cargo kernel-build`; `tools/smoke.sh` PASS — the embedded real image loads via the existing loader smoke (maps `USER|EXECUTE` image + `USER|WRITE` stack, `LoadedImage` metadata printed), **byte-stable boot otherwise**, zero new fault class. Miri unaffected (no kernel logic change). + +## Out of scope + +- **Running the image in EL0 / the `+0x400` round-trip / `task_create_from_image` + `add_user_task` wiring** — [T-028](T-028-el0-userspace-wireup.md). +- **Seeding the task's capability table with a `DebugConsole` cap** — T-028 (the `hello` source hard-codes the *handle value* it will use; minting + inserting the cap is the wire-up's job). +- **Per-section permissions (RX `.text` / R `.rodata` / RW `.data`)** — the future ADR-0034 placeholder; v1 `hello` is code + read-only data only. +- **`cargo xtask` multi-binary orchestration** — the named B7+ upgrade ([ADR-0039](../../../decisions/0039-userland-build-pipeline.md)); v1 uses the shell script. + +## Approach + +Per [ADR-0039](../../../decisions/0039-userland-build-pipeline.md) §Decision outcome: two `default-members`-excluded crates (`tyrne-user` lib + `userland/hello` bin) targeting `aarch64-unknown-none`; a minimal userland linker script fixing the entry at `USERSPACE_IMAGE_BASE_VA` offset 0; `tools/build-userland.sh` driving `cargo build` + `rust-objcopy -O binary` (llvm-tools, no Cargo dep) to a git-ignored `.bin`; the BSP `build.rs` embedding it via `include_bytes!` with a `rerun-if-changed` + missing-file panic; `tools/smoke.sh` chaining the script. The hello program is deliberately tiny (greeting + `task_exit`) so the v1 no-`.data` constraint is non-binding. Verification is by disassembly + the existing loader smoke (the image maps cleanly); **execution is deferred to T-028**, mirroring how T-023's EL0-entry mechanism landed dormant before its wire-up. + +## Definition of done + +All acceptance criteria checked; gates green; the embedded real image replaces the placeholder and loads cleanly under `tools/smoke.sh` (dormant — not run); `current.md` + [phase-b.md §B6 step 5](../../../roadmap/phases/phase-b.md#milestone-b6--first-userspace-hello) updated. Lands **after** [ADR-0039](../../../decisions/0039-userland-build-pipeline.md) is Accepted. The EL0 wire-up + the explicit EL0-boundary security review are [T-028](T-028-el0-userspace-wireup.md). + +## Review history + +- **2026-05-31 — opened Draft** in the [ADR-0039](../../../decisions/0039-userland-build-pipeline.md) Propose commit (the ADR's dependency chain names it — step 5; [ADR-0025 §Rule 1](../../../decisions/0025-adr-governance-amendments.md)). Implementation follows the ADR Accept. diff --git a/docs/analysis/tasks/phase-b/T-028-el0-userspace-wireup.md b/docs/analysis/tasks/phase-b/T-028-el0-userspace-wireup.md new file mode 100644 index 0000000..b7a7130 --- /dev/null +++ b/docs/analysis/tasks/phase-b/T-028-el0-userspace-wireup.md @@ -0,0 +1,51 @@ +# T-028 — EL0 userspace wire-up + the `+0x400` round-trip smoke (B6 step 6) + +- **Phase:** B +- **Milestone:** B6 — First userspace "hello" (step 6 of the [B6 dependency-ordered sequence](../../../roadmap/phases/phase-b.md#milestone-b6--first-userspace-hello) — a true EL0 task takes the lower-EL `VBAR_EL1+0x400` vector, the dispatcher copies `console_write` from its `TTBR0_EL1`, `ERET` returns to EL0, and `task_exit` terminates it — the EL0↔EL1 round-trip B5's `+0x200` proxy could not prove) +- **Status:** Draft (opened in the [ADR-0039](../../../decisions/0039-userland-build-pipeline.md) Propose commit per [ADR-0025 §Rule 1](../../../decisions/0025-adr-governance-amendments.md); implementation follows [T-027](T-027-userland-build-pipeline.md)) +- **Created:** 2026-05-31 +- **Author:** @cemililik (+ Claude Opus 4.8 agent) +- **Dependencies:** [T-027](T-027-userland-build-pipeline.md) (the embedded real `hello` image this loads + runs); [T-024](T-024-task-create-from-image.md) (`task_create_from_image` — the `LoadedImage` → runnable `CapHandle{CapObject::Task}` bridge); [T-019](T-019-task-loader.md) (`load_image`); [T-023](T-023-el0-entry-context.md) / [ADR-0037](../../../decisions/0037-el0-entry-context.md) (`add_user_task` + `init_user_context` + the `enter_el0`/`ERET` trampoline + per-task `SP_EL1`); [T-025](T-025-user-access-translation.md) (gate #1 — the translate-based copy the `console_write` buffer is checked through); [T-026](T-026-current-task-cap-table.md) (gate #3 — the scheduler sources the running task's table + AS + window); [T-022](T-022-high-half-kernel-mapping.md) / [ADR-0033](../../../decisions/0033-kernel-high-half-migration.md) (the high-half regime that keeps the kernel reachable from the task's `TTBR0`). +- **Informs:** Closes B6's functional milestone — the first real userspace program runs in EL0. Triggers the **explicit EL0-boundary security review** carried forward from the [T-026 Definition of done](T-026-current-task-cap-table.md) (now that the boundary is attacker-observable). +- **ADRs required:** **None** — composes the already-Accepted mechanisms (ADR-0033 high-half, ADR-0037 EL0 entry, ADR-0038 translate, ADR-0030/0031 syscall ABI). Any new `unsafe` in the wire-up (e.g. a per-task kernel stack) gets an audit-log entry per [unsafe-policy](../../../standards/unsafe-policy.md). + +--- + +## User story + +As the kernel, I want to load the embedded `hello` image into its own address space, create a runnable EL0 task from it, schedule it, and `ERET` into it — so that the task greets via a real `console_write` syscall taking the lower-EL `+0x400` vector (copied from its own `TTBR0_EL1`, gate-#1-checked) and exits via `task_exit`, proving the EL0↔EL1 round-trip end-to-end. + +## Context + +Every B6 mechanism is in place and merged: high-half ([T-022](T-022-high-half-kernel-mapping.md)) keeps the kernel reachable from a task's `TTBR0`; the loader ([T-019](T-019-task-loader.md)) + `task_create_from_image` ([T-024](T-024-task-create-from-image.md)) turn a raw image into a runnable Task cap; `add_user_task` + `enter_el0` ([T-023](T-023-el0-entry-context.md)) seed and drop to EL0 with a register scrub; gate #1 ([T-025](T-025-user-access-translation.md)) translate-checks the `console_write` buffer; gate #3 ([T-026](T-026-current-task-cap-table.md)) sources the running task's table + AS + window. T-027 produces the real `hello` image. T-028 is the **wire-up**: it replaces the boot-time loader smoke (which loads + discards) with the full load → create → schedule → run path, and replaces the dormant `+0x200` fail-closed smoke with the live `+0x400` round-trip. + +## Acceptance criteria + +- [ ] **Load + create:** on boot, `load_image(USERSPACE_IMAGE, …)` → `task_create_from_image(&loaded, …)` mints the EL0 task cap (replacing the load-and-discard smoke). +- [ ] **Per-task capability table seeded:** a `CapabilityTable` for the EL0 task holding a **`DebugConsole`** capability (`CapRights::CONSOLE_WRITE`) at the handle the `hello` program names; bound to the task via `add_user_task`'s `cap_table` parameter (gate #3 — [T-026](T-026-current-task-cap-table.md)). The handle value `hello` hard-codes and the seeded slot must agree. +- [ ] **Per-task kernel stack:** a valid, 16-byte-aligned `SP_EL1` kernel stack (≥ the 272-byte trap frame + the gate-#1 dispatch call-tree depth) passed to `add_user_task` (the EL0→EL1 trap runs the trampoline's `sub sp, sp, #272` on `SP_EL1`, which the CPU does **not** auto-initialise — [T-023](T-023-el0-entry-context.md) gate #2). Source (BSP static vs PMM-allocated) decided in implementation; asserted non-null + aligned. +- [ ] **Schedule + run:** `add_user_task(…)` enqueues the task Ready; the scheduler `start()`/dispatch activates the task's AS (`TTBR0_EL1` installed, `EPD0` cleared — the activation hook fires by construction, [ADR-0028](../../../decisions/0028-address-space-data-structure.md)) and `enter_el0` `ERET`s into EL0 at `entry_va`. +- [ ] **`+0x400` round-trip:** the `hello` `console_write` SVC traps to the **lower-EL** sync vector (`VBAR_EL1+0x400`), `syscall_entry` resolves the task's own table + window + AS (gate #3), gate #1 translates the buffer through the task's `TTBR0`, the dispatcher copies + emits, `x0` = status, the trampoline `ERET`s back to EL0 — the round-trip B5's `+0x200` proxy could not prove. +- [ ] **Clean exit:** the `hello` `task_exit` SVC terminates the task; the kernel reports termination and continues (the cooperative demo still reaches `tyrne: all tasks complete` or the documented post-EL0 shutdown line). +- [ ] **QEMU smoke (acceptance):** the serial trace shows the kernel greeting, then **"hello from userspace"** (or the chosen greeting) in the correct order, then clean `task_exit`; `-d int` shows the `+0x400` lower-EL `SVC` exception (EL0→EL1) with a clean `ERET`; **no new fault class**. +- [ ] **Explicit EL0-boundary security review** (the [T-026](T-026-current-task-cap-table.md) carry-forward DoD): a review of the now-attacker-observable boundary covering gate #1 ([UNSAFE-2026-0030](../../../audits/unsafe-log.md)), gate #2 ([UNSAFE-2026-0032](../../../audits/unsafe-log.md)), the gate-#3 cap-table sourcing (UNSAFE-2026-0014 Amendment), and the register scrub — filed under `docs/analysis/reviews/security-reviews/`. +- [ ] **All gates green:** `cargo fmt --all --check`; host + kernel clippy `-D warnings`; host tests (+ any wire-up regression tests) green; `cargo kernel-build`; `tools/smoke.sh` PASS (the round-trip trace above); Miri 0 UB; any new `unsafe` audited. + +## Out of scope + +- **The build pipeline + the `hello`/`tyrne-user` crates** — [T-027](T-027-userland-build-pipeline.md). +- **Multiple EL0 tasks / per-task cap-table arena** — later (the BSP-static single-task shape suffices for B6; a registry is a later ADR if outgrown). +- **Per-section permissions (ADR-0034)** + **EL0 non-`SVC` fault containment** (illegal instruction / unmapped deref) — later-phase (the dispatcher is already panic-free; fault containment is Phase E / flag K3-4). +- **The B6 closure trio** (guide + performance cycle + Phase B retrospective) — [B6 step 7](../../../roadmap/phases/phase-b.md#milestone-b6--first-userspace-hello). + +## Approach + +Replace the boot-time loader smoke with the full path: `load_image` → `task_create_from_image` → seed a `CapabilityTable` with a `DebugConsole` cap at the handle `hello` names → `add_user_task` (binding the table + a per-task `SP_EL1` stack) → let the scheduler activate the task's AS and `enter_el0`. The `hello` greeting buffer is a user VA inside `[entry_va, stack_top_va)`, so gate #1 translates it through the task's own `TTBR0` and the gate-#3 window admits it. Retire the dormant `+0x200` fail-closed smoke (its property is now covered by host tests) in favour of the live `+0x400` round-trip. Because this is the first attacker-observable EL0 boundary, the explicit security review is part of the Definition of done — not deferred. + +## Definition of done + +All acceptance criteria checked; gates green (incl. Miri); the `+0x400` round-trip demonstrated in the smoke trace; the explicit EL0-boundary security review filed (**Approve** before merge); `current.md` + [phase-b.md §B6](../../../roadmap/phases/phase-b.md#milestone-b6--first-userspace-hello) updated. Lands **after** [T-027](T-027-userland-build-pipeline.md). Closes B6's functional milestone; the closure trio (step 7 — Phase B retrospective) follows. + +## Review history + +- **2026-05-31 — opened Draft** in the [ADR-0039](../../../decisions/0039-userland-build-pipeline.md) Propose commit (the ADR's dependency chain names it — step 6; [ADR-0025 §Rule 1](../../../decisions/0025-adr-governance-amendments.md)). Composes already-Accepted mechanisms; no ADR of its own. Implementation follows T-027. diff --git a/docs/decisions/0039-userland-build-pipeline.md b/docs/decisions/0039-userland-build-pipeline.md new file mode 100644 index 0000000..c06f64c --- /dev/null +++ b/docs/decisions/0039-userland-build-pipeline.md @@ -0,0 +1,115 @@ +# 0039 — Userland build pipeline (B6 — `userland/hello` + `tyrne-user` + raw-flat embed orchestration) + +- **Status:** Proposed +- **Date:** 2026-05-31 +- **Deciders:** @cemililik + +## Context + +[ADR-0029](0029-initial-userspace-image-format.md) settled the userspace image **format** (raw flat binary, entry at offset 0) and explicitly **deferred the build orchestration**: its §Decision-outcome "Build pipeline (B6)" row says B6's `userland/hello/` crate is built via `cargo build --target aarch64-unknown-none` + `objcopy -O binary` and embedded via `include_bytes!`, with "the exact path lands with B6." B4 ([T-019](../analysis/tasks/phase-b/T-019-task-loader.md)) shipped a hand-coded placeholder blob (`USERSPACE_IMAGE` at [`bsp-qemu-virt/src/main.rs`](../../bsp-qemu-virt/src/main.rs) — `mov w0, #42; ret`); that is the only userspace image in tree today, and it is loaded-then-discarded by a boot-time loader smoke (never run in EL0). + +With B6's three [T-021 carry-forward gates](../roadmap/phases/phase-b.md#t-021-carry-forward-gates-must-close-before-a-real-el0-task-runs) now closed ([T-022](../analysis/tasks/phase-b/T-022-high-half-kernel-mapping.md) high-half, [T-023](../analysis/tasks/phase-b/T-023-el0-entry-context.md) EL0-context, [T-025](../analysis/tasks/phase-b/T-025-user-access-translation.md) gate #1, [T-026](../analysis/tasks/phase-b/T-026-current-task-cap-table.md) gate #3) and `task_create_from_image` ([T-024](../analysis/tasks/phase-b/T-024-task-create-from-image.md)) merged, the remaining B6 work is to (1) produce a **real** userspace program from Rust source and embed it, and (2) wire it to run in EL0. This ADR settles the **process** ADR-0029 deferred: **how** the userland crate is built, objcopy'd, and embedded; **where** the `userland/hello` + `tyrne-user` crates live in the workspace; **which** objcopy tool is used; and **where** the userspace base VA lives so the loader and the userspace linker script cannot drift. + +The stakes are precedent, not just plumbing. This is the project's **first** cross-target build artifact (a binary compiled for a *different* privilege domain and embedded into the kernel image). Getting the orchestration wrong has three concrete failure modes: a `build.rs` that invokes `cargo` recursively can deadlock on the workspace target-directory lock; an integration that builds the bare-metal userland under host commands (`cargo test`/`cargo build`) breaks the host test suite; and "build magic" hidden in a build script is hard to audit in a security-first kernel. The shape chosen here is the shape every future userland crate (B7+) will follow. + +## Decision drivers + +- **Host commands stay untouched.** `cargo host-test` / `cargo build` / `cargo host-clippy` (workspace `default-members` = kernel, hal, test-hal) must not attempt to cross-compile the bare-metal userland — exactly as `bsp-qemu-virt` is already excluded from `default-members`. +- **No new Cargo dependency.** [infrastructure.md](../standards/infrastructure.md) §Dependency policy and the [ADR-0029](0029-initial-userspace-image-format.md) toolchain-alignment driver push to avoid `cargo-binutils` (it would trigger the **K3-8 `cargo-vet init`** flag, [phase-b §Flags](../roadmap/phases/phase-b.md#flags-to-resolve-during-b6)). The `llvm-tools-preview` component — which ships `rust-objcopy` — is **already pinned** ([`rust-toolchain.toml`](../../rust-toolchain.toml)). +- **Auditable, no magic.** A security-first kernel ([CLAUDE.md non-negotiable #1](../../CLAUDE.md)) favours an explicit, reviewable orchestrator over implicit build-script side effects that silently shell out to `cargo`. +- **Reviewer / CI ergonomics.** The canonical build + smoke path must stay simple. The repo already has [`tools/smoke.sh`](../../tools/smoke.sh) as the canonical integration entry point. +- **Smallest shape that works for v1.** B6 ships **one** userspace program. The orchestration should not pay for multi-binary generality the project does not yet have (the [ADR-0027](0027-kernel-virtual-memory-layout.md) / [ADR-0035](0035-physical-memory-manager.md) "smallest shape now, defer richness" pattern). +- **Single source-of-truth for the userspace base VA.** ADR-0029 §Consequences flagged "linker-script awareness leaks into the loader … spec drift potential." The base VA (`0x0080_0000`) is read by the kernel loader *and* by the userspace linker script; a divergence is a silent, hard-to-debug wrong-VA load. +- **Scales to B7+ without a rewrite.** The chosen shape should have a named, additive upgrade path when a second userspace program lands. + +## Considered options + +1. **`tools/build-userland.sh` pre-build step** — an explicit shell script (peer to `tools/smoke.sh`) runs `cargo build` for the userland crate + `rust-objcopy -O binary`, writing a git-ignored `.bin`; the BSP `build.rs` `include_bytes!`s it (with a clear error if absent); `tools/smoke.sh` runs the script before the kernel build. +2. **BSP `build.rs` with a separate `CARGO_TARGET_DIR`** — the BSP build script invokes `cargo build -p tyrne-userland-hello` into an isolated target dir (the standard nested-cargo lock mitigation), then objcopy + embed; `cargo kernel-build` "just works" with no pre-step. +3. **`cargo xtask` orchestrator** — a workspace `xtask` crate with a `build-userland` subcommand; `cargo` aliases chain it before the kernel build. +4. **Committed pre-built `.bin`** — check the objcopy output into the repo; the BSP `build.rs` only `include_bytes!`s it; a `tools/` script regenerates it on source change. + +## Decision outcome + +Chosen option: **Option 1 — `tools/build-userland.sh`**, with these bundled sub-decisions: + +- **Crates.** Add two workspace members under a new `userland/` directory: `userland/hello/` (package `tyrne-userland-hello`, `#![no_std] #![no_main]`, the raw-flat program) and `tyrne-user/` (package `tyrne-user`, `#![no_std]` library of **safe** syscall wrappers `console_write` / `task_exit`, which `hello` depends on). Both are added to `members` and **excluded from `default-members`** (so host commands skip them, mirroring `bsp-qemu-virt`). `tyrne-` package-name prefix per [ADR-0006](0006-workspace-layout.md). +- **objcopy.** `rust-objcopy -O binary` from the already-pinned `llvm-tools-preview` component — **no Cargo dependency**, so the K3-8 `cargo-vet` flag stays unfired. The script resolves the binary via the active toolchain (`rustc --print sysroot` + the `llvm-tools` bin dir) and fails with a clear diagnostic if the component is missing. +- **Base-VA source-of-truth.** A single `pub const USERSPACE_IMAGE_BASE_VA` lives Rust-side (read by the BSP loader call site and any kernel consumer); the userspace linker script **restates** the same literal with a `keep-in-sync-with` comment, because a linker script cannot import a Rust const. A `ld --defsym`-from-const upgrade is named for full drift-elimination when it earns its keep. +- **Artifact.** The `.bin` is **git-ignored** (regenerated from source; no binary blob in the repo — the Rust source stays the auditable truth). The BSP `build.rs` adds `rerun-if-changed` on it and `include_bytes!`s a stable path; if the file is absent it emits a `panic!` naming `tools/build-userland.sh`. `tools/smoke.sh` runs the script before `cargo kernel-build`, so the canonical path is unchanged. + +Option 1 wins on the four most load-bearing drivers: it keeps host commands untouched (the userland is never in a host build set), adds no Cargo dependency, is fully auditable (a shell script a reviewer reads at a glance — the same shape as `tools/smoke.sh`), and is the smallest shape for one binary (no new crate beyond the two the milestone requires). It avoids the nested-cargo deadlock entirely (the script runs `cargo` at top level, not inside a `build.rs`) and commits no binary blob. Its cost — a build step before a bare `cargo kernel-build` — is mitigated by the build script's clear error and by `tools/smoke.sh` chaining it, and the **`cargo xtask` pattern (Option 3) is the named, additive upgrade** when B7+ introduces multiple userspace programs (a shell `for`-loop or an `xtask build-userland` subcommand drops in without disturbing the kernel build). + +### Simulation + +**Not applicable** — this ADR settles a single-shape process / build-orchestration decision; there is no runtime state machine to simulate. (The EL0↔EL1 round-trip the resulting image exercises is the subject of [T-028](../analysis/tasks/phase-b/T-028-el0-userspace-wireup.md)'s wire-up, walked through [ADR-0030 §Simulation](0030-syscall-abi.md#simulation) and [ADR-0037](0037-el0-entry-context.md); this ADR's subject is how the bytes are produced, not how they run.) + +### Dependency chain + +For this decision to be fully in effect: + +```text +1. Raw-flat image format (entry at offset 0, USER|EXECUTE image) — ADR-0029 (Accepted) +2. rust-objcopy via the llvm-tools-preview component — rust-toolchain.toml (pinned, present) +3. Loader + LoadedImage + task_create_from_image + the syscall gates — T-019 / T-024 / T-025 / T-026 (Done / merged) +4. EL0 entry context + enter-EL0 path + add_user_task — T-023 / ADR-0037 (Done) +5. The build pipeline + userland/hello + tyrne-user crates — T-027 (Draft, opens with this ADR) +6. EL0 wire-up + the +0x400 round-trip QEMU smoke — T-028 (Draft, opens with this ADR) +``` + +T-027 closes steps 5 (the build pipeline + the two crates + the embedded real image, replacing the placeholder; dormant — not yet run). T-028 closes step 6 (load → `task_create_from_image` → `add_user_task` → the scheduler runs the task in EL0; the real round-trip) and triggers the explicit EL0-boundary security review the [T-026 Definition of done](../analysis/tasks/phase-b/T-026-current-task-cap-table.md) carries forward. Both slots are opened at `Draft` in the same commit as this ADR per [ADR-0025 §Rule 1](0025-adr-governance-amendments.md). + +## Consequences + +### Positive + +- **No new dependency, no new attack surface in the build.** `rust-objcopy` is already in the pinned toolchain; the `cargo-vet` K3-8 flag stays unfired; the orchestrator is hand-written shell + Rust the project owns end-to-end. +- **Host suite is provably unaffected.** The userland crates sit outside `default-members`; `cargo host-test` / `cargo build` / `cargo host-clippy` never touch them, exactly as `bsp-qemu-virt` is excluded today — a pattern already proven in tree. +- **Auditable.** The build order is a short shell script in `tools/`, reviewed like any source file; the embedded image is reproduced from committed Rust source, so the byte stream's provenance is the crate, not an opaque blob. +- **No nested-cargo deadlock.** The script invokes `cargo` at top level (not from inside a `build.rs`), so there is no workspace target-directory lock contention or recursive-cargo fragility. + +### Negative + +- **A bare `cargo kernel-build` needs the `.bin` present first.** On a fresh checkout, `cargo kernel-build` alone fails until `tools/build-userland.sh` has run. *Mitigation:* the BSP `build.rs` emits a `panic!` that names the script; `tools/smoke.sh` (the canonical entry point, and what CI runs) chains the script before the kernel build; the dev loop is `tools/build-userland.sh && cargo kernel-build` or simply `tools/smoke.sh`. We accept this small ergonomic cost in exchange for no build-script magic and no committed binary. +- **The base VA is stated in two places** (the Rust const and the userspace linker script), because LD cannot import a Rust const. *Mitigation:* a `keep-in-sync-with` comment on the linker-script line, the same discipline ADR-0029 already imposes ("linker-script awareness leaks into the loader"); the `ld --defsym`-from-const path is named for when full drift-elimination earns its keep (B7+ multi-program / multi-BSP). +- **The `.bin` is git-ignored, so CI must build it.** A clean CI run must execute `tools/build-userland.sh` (or `tools/smoke.sh`) before the kernel build. *Mitigation:* this is standard for embedded projects and is one line in the smoke path; the alternative (committing the blob) trades it for a binary in git history and a freshness-drift check, which we reject. + +### Neutral + +- **`cargo xtask` is the named scaling path, not a rejection.** When a second userspace program lands (B7+), the shell script either grows a loop or is replaced by an `xtask build-userland` subcommand; nothing in T-027's shape blocks that, and the kernel build is unaffected either way. +- **Reversible.** Switching to the `build.rs`-auto (Option 2) or committed-`.bin` (Option 4) shape later is a localized change to `build.rs` + `tools/`, touching no kernel or userland source. +- **The userland inherits the `[target.aarch64-unknown-none]` rustflags** (`panic=abort`, `force-frame-pointers`). Both are correct for a userspace blob (no unwinder; frame pointers are negligible overhead for a ~100-byte program); a future performance-sensitive userland can add a crate-local `.cargo/config.toml` override. + +## Pros and cons of the options + +### Option 1 — `tools/build-userland.sh` pre-build step (chosen) + +- **Pro:** Smallest shape for one binary; no new crate; explicit + auditable (peer to `tools/smoke.sh`); no nested-cargo; no Cargo dep; no committed binary; host commands untouched. +- **Pro:** `xtask` remains a clean additive upgrade for B7+ multi-binary. +- **Con:** A bare `cargo kernel-build` needs the script run first (mitigated by a clear `build.rs` error + `tools/smoke.sh` chaining). +- **Con:** Shell is less portable than Rust (the project's dev/CI hosts are Unix; `tools/smoke.sh` already assumes this). + +### Option 2 — BSP `build.rs` with a separate `CARGO_TARGET_DIR` + +- **Pro:** `cargo kernel-build` "just works" with no pre-step; the standard nested-cargo mitigation (isolated target dir) is well-trodden (bootimage, embedded Rust). +- **Con:** A build script that shells out to `cargo` is exactly the implicit "build magic" an auditable kernel should avoid; the embedded artifact is hidden under `target/**/OUT_DIR` (reviewers cannot inspect it without rebuilding); adds 5–15 s to every kernel build; couples to Cargo internals. + +### Option 3 — `cargo xtask` orchestrator + +- **Pro:** Explicit, scales cleanly to multiple userspace programs; idiomatic in the Rust ecosystem; `cargo` alias keeps the one-liner. +- **Con:** Adds a whole workspace crate for what B6 needs once — more than the "smallest shape" the single-binary milestone justifies; a cargo alias cannot itself chain a shell step, so the orchestration still needs the alias wired carefully. Reserved as the **named B7+ upgrade**, not the v1 choice. + +### Option 4 — Committed pre-built `.bin` + +- **Pro:** `cargo kernel-build` is truly standalone (the blob is in tree); reviewers can disassemble the committed artifact. +- **Con:** Puts a binary blob in git history (drift risk if source changes but the blob is not regenerated; a freshness check becomes a CI burden); a security review of a committed binary means trusting/disassembling it rather than reading the source it came from — the opposite of the "Rust source is the auditable truth" posture. + +## References + +- [ADR-0029 — Initial userspace image format](0029-initial-userspace-image-format.md) — the raw-flat **format** this ADR builds the pipeline for; its §Decision-outcome deferred the build orchestration to B6. +- [ADR-0006 — Workspace layout](0006-workspace-layout.md) — the `members` / `default-members` split and the `tyrne-` package-name prefix this ADR extends to the userland crates. +- [ADR-0027 — Kernel virtual memory layout](0027-kernel-virtual-memory-layout.md) — the `TTBR0_EL1` userspace VA range the base VA (`0x0080_0000`) sits within. +- [rust-toolchain.toml](../../rust-toolchain.toml) — the pinned `llvm-tools-preview` component providing `rust-objcopy`. +- [tools/smoke.sh](../../tools/smoke.sh) — the canonical integration entry point the build script chains into. +- [`cargo-xtask` pattern (matklad)](https://github.com/matklad/cargo-xtask) — the named B7+ multi-binary upgrade path. +- [The `embedded-bootimage` / blog_os build-step pattern](https://os.phil-opp.com/) — prior art for objcopy-and-embed orchestration of a bare-metal artifact. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 7643c02..0116335 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -66,6 +66,7 @@ Each ADR contains: | 0036 | [QEMU virt is GICv2 / no-IOMMU in v1; corrects GICv3/SMMUv3 in ADR-0004/0006/0012](0036-qemu-virt-gicv2-no-iommu-v1.md) | Accepted | 2026-05-22 | | 0037 | [EL0 entry context (B6 — userspace register file + enter-EL0/`ERET` path + per-task `SP_EL1`)](0037-el0-entry-context.md) | Accepted | 2026-05-31 | | 0038 | [`Mmu::translate` read-only walk + per-task user-access translation (B6 gate #1)](0038-mmu-translate-and-user-access.md) | Accepted | 2026-05-31 | +| 0039 | [Userland build pipeline (B6 — `userland/hello` + `tyrne-user` + raw-flat embed orchestration)](0039-userland-build-pipeline.md) | Proposed | 2026-05-31 | > **Numbering gaps.** Slot **0034** is intentionally reserved, not missing: 0034 (kernel-image section permissions) is a named-but-unallocated placeholder forward-flagged in ADR-0027. No file exists for it yet; it opens when the corresponding work surfaces (the first attacker-observable EL0 execution — likely B6). (Slot **0033** (high-half migration) was filed `Proposed` on 2026-05-29 to open B6 and `Accepted` on 2026-05-30, and is no longer a gap; slots **0030**/**0031** were filed and `Accepted` on 2026-05-29 for B5.) ADR numbers are stable history and are never renumbered. From 085112825ee5606e1f83f003f3ac08093133ee7e Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Sun, 31 May 2026 23:04:21 +0300 Subject: [PATCH 2/7] =?UTF-8?q?docs(adr):=20ADR-0039=20+=20T-027/T-028=20p?= =?UTF-8?q?re-Accept=20review-round=20=E2=80=94=208=20valid=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relayed review against the still-Proposed ADR-0039 + its two Draft task slots; verified each finding against source, all valid (1 non-defect observation skipped): - ADR §Context: 'three gates' miscount — listed 4 (T-022 is the high-half prerequisite, not a T-021 gate); reworded to gate #1/#2/#3 + the high-half prerequisite. - ADR + T-027: pinned tyrne-user to userland/tyrne-user/ (was ambiguous 'or workspace root'). - T-027 (HIGH): added the userland-unsafe-audit AC — the svc shims are unsafe; unsafe-policy applies to userspace (SAFETY + audit-log entry). - T-027: added the HELLO_CONSOLE_CAP handle-contract AC (T-027<->T-028 interface) + the explicit userland clippy gate (kernel/host clippy miss the new crates). - T-028 (HIGH): added the AS-lifetime-coupling AC (T-024 SEC-T024-01 carry-forward). - T-028: clarified the smoke is debug-profile (console_write is debug-gated) + documented release behaviour; added the tools/smoke.sh greeting-marker gate AC; pinned the kernel-stack floor at >=1 page. Skipped: the ADR build.rs-panic §Negative item (an implementation detail the reviewer agreed is non-defect). Refs: ADR-0039 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../tasks/phase-b/T-027-userland-build-pipeline.md | 7 +++++-- .../tasks/phase-b/T-028-el0-userspace-wireup.md | 11 +++++++---- docs/decisions/0039-userland-build-pipeline.md | 4 ++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md b/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md index 03ed1be..278b42b 100644 --- a/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md +++ b/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md @@ -21,8 +21,10 @@ ADR-0029 chose the raw-flat image format and deferred the build orchestration to ## Acceptance criteria -- [ ] **`tyrne-user` crate** (`userland/` or workspace root, package `tyrne-user`, `#![no_std]`): safe wrappers over the [ADR-0031](../../../decisions/0031-initial-syscall-set.md) syscalls needed by `hello` — at minimum `console_write(cap: CapWord, buf: &[u8]) -> Result` and `task_exit(code) -> !` — each a thin `svc #0` inline-asm shim packing the ABI ([ADR-0030](../../../decisions/0030-syscall-abi.md): `x8` = number, `x0..x5` = args; `ConsoleWrite = 5`, `TaskExit = 4`). No dependency on kernel internals (the ABI constants are restated/owned userspace-side or shared via a leaf crate). +- [ ] **`tyrne-user` crate** (`userland/tyrne-user/`, package `tyrne-user`, `#![no_std]`): safe wrappers over the [ADR-0031](../../../decisions/0031-initial-syscall-set.md) syscalls needed by `hello` — at minimum `console_write(cap: CapWord, buf: &[u8]) -> Result` and `task_exit(code) -> !` — each wrapping a thin `svc #0` inline-asm shim (an **`unsafe` block** — see the userland-`unsafe`-audit AC) packing the ABI ([ADR-0030](../../../decisions/0030-syscall-abi.md): `x8` = number, `x0..x5` = args; `ConsoleWrite = 5`, `TaskExit = 4`). No dependency on kernel internals (the ABI constants are restated/owned userspace-side or shared via a leaf crate). +- [ ] **Userland `unsafe` audit** (security policy applies to userspace too): the `tyrne-user` `svc #0` shims are `unsafe` (inline asm); per [unsafe-policy §Scope](../../../standards/unsafe-policy.md) ("the rules apply equally in kernel, HAL, and userspace code") **and** [CLAUDE.md non-negotiable #2](../../../../CLAUDE.md), each `unsafe` block carries a `// SAFETY:` comment (why the asm upholds the ABI contract — args in `x0..x5`/`x8`, the clobber list, no UB) **and** a dated [`docs/audits/unsafe-log.md`](../../../audits/unsafe-log.md) entry (`UNSAFE-2026-NNNN`). The workspace `clippy::undocumented_unsafe_blocks` / `missing_safety_doc` denies enforce the comment; the audit entry is the policy obligation. - [ ] **`userland/hello` crate** (package `tyrne-userland-hello`, `#![no_std] #![no_main]`): a `_start` entry placed at **offset 0** of the linked image (linker `ENTRY` + a `KEEP`'d first section) that calls `tyrne_user::console_write` with a greeting string (living in the image's read-only data — reachable as a `USER` page, [T-025](T-025-user-access-translation.md) gate #1 requires only the `USER` flag for a read) then `tyrne_user::task_exit(0)`; a minimal `#[panic_handler]` that `task_exit`s or loops (no unwinder — `panic=abort` inherited). +- [ ] **Console-cap handle contract (T-027 ↔ T-028 interface):** `hello` names its `DebugConsole` capability via a **single documented named constant** (e.g. `pub const HELLO_CONSOLE_CAP: u64` in `tyrne-user`, the `cap_word` passed in `x0` — `encode_cap_handle` of the handle the wire-up seeds, typically the root cap → index 0 / generation 0). [T-028](T-028-el0-userspace-wireup.md)'s cap-table seeding **must** insert the `DebugConsole` cap so it resolves to that exact handle. The constant is the shared interface — defined once, not duplicated by value across the two tasks. - [ ] **Userland linker script** places `.text` (entry first) + `.rodata` contiguously at `USERSPACE_IMAGE_BASE_VA`, **no `.data`/`.bss`** (writable globals would fault — image is `USER|EXECUTE`, no `WRITE`), 16-byte aligned; produces a contiguous byte stream with no ELF artifacts under `rust-objcopy -O binary`. - [ ] **Base-VA source-of-truth:** a single Rust-side `pub const USERSPACE_IMAGE_BASE_VA = 0x0080_0000` (read by the BSP loader call site); the userland linker script restates the literal with a documented `keep-in-sync-with` comment (LD cannot import a Rust const — per [ADR-0039](../../../decisions/0039-userland-build-pipeline.md)). - [ ] **Build orchestration** ([`tools/build-userland.sh`](../../../../tools/build-userland.sh)): `cargo build -p tyrne-userland-hello --target aarch64-unknown-none` (release + debug as the kernel profile dictates) → `rust-objcopy -O binary` (resolved from the active toolchain's `llvm-tools` — **no Cargo dependency**, K3-8 unfired) → a git-ignored `.bin` at a stable path. Clear error if `llvm-tools` / the ELF is missing. `.gitignore` updated. @@ -30,7 +32,7 @@ ADR-0029 chose the raw-flat image format and deferred the build orchestration to - [ ] **BSP embed:** `USERSPACE_IMAGE` becomes `include_bytes!()`; [`bsp-qemu-virt/build.rs`](../../../../bsp-qemu-virt/build.rs) gains `rerun-if-changed` on the `.bin` and a `panic!` naming `tools/build-userland.sh` if it is absent. - [ ] **`tools/smoke.sh` runs the userland build first** so the canonical smoke + CI path is unchanged (still one entry point). - [ ] **Disassembly verification:** the objcopy'd `.bin` round-trips — a host test (or a documented `rust-objdump -d` check in the task's review-history) confirms offset 0 is the entry instruction and the SVC sequence + greeting match `hello`'s source. (The image is **not run** in T-027 — running is T-028.) -- [ ] **All gates green:** `cargo fmt --all --check`; host + kernel clippy `-D warnings`; host tests unchanged + green; `cargo kernel-build`; `tools/smoke.sh` PASS — the embedded real image loads via the existing loader smoke (maps `USER|EXECUTE` image + `USER|WRITE` stack, `LoadedImage` metadata printed), **byte-stable boot otherwise**, zero new fault class. Miri unaffected (no kernel logic change). +- [ ] **All gates green:** `cargo fmt --all --check`; host + kernel clippy `-D warnings`; **the new userland crates linted** (`cargo clippy --target aarch64-unknown-none -p tyrne-userland-hello -p tyrne-user -- -D warnings` — neither `kernel-clippy` (only `-p tyrne-bsp-qemu-virt`) nor `host-clippy` (`default-members`, which excludes them) covers them, so this explicit invocation is required); host tests unchanged + green; `cargo kernel-build`; `tools/smoke.sh` PASS — the embedded real image loads via the existing loader smoke (maps `USER|EXECUTE` image + `USER|WRITE` stack, `LoadedImage` metadata printed), **byte-stable boot otherwise**, zero new fault class. Miri unaffected (no kernel logic change). ## Out of scope @@ -50,3 +52,4 @@ All acceptance criteria checked; gates green; the embedded real image replaces t ## Review history - **2026-05-31 — opened Draft** in the [ADR-0039](../../../decisions/0039-userland-build-pipeline.md) Propose commit (the ADR's dependency chain names it — step 5; [ADR-0025 §Rule 1](../../../decisions/0025-adr-governance-amendments.md)). Implementation follows the ADR Accept. +- **2026-05-31 — pre-Accept review-round (relayed, verified against source):** **(HIGH)** added the **userland-`unsafe`-audit AC** — the `tyrne-user` `svc #0` shims are `unsafe`; [unsafe-policy §Scope](../../../standards/unsafe-policy.md) applies "equally in … userspace code", so each needs a `// SAFETY:` comment + a `docs/audits/unsafe-log.md` entry (previously unstated). **(Med)** pinned the `tyrne-user` path to `userland/tyrne-user/` (was "or workspace root", contradicting the ADR). **(Med)** added the **`HELLO_CONSOLE_CAP` handle-contract AC** (the T-027↔T-028 interface — closes a silent handle-mismatch risk). **(Low)** added the explicit userland clippy gate (`-p tyrne-userland-hello -p tyrne-user` — neither `kernel-clippy` nor `host-clippy` covers the new crates). All against the still-`Proposed` ADR-0039, pre-Accept. diff --git a/docs/analysis/tasks/phase-b/T-028-el0-userspace-wireup.md b/docs/analysis/tasks/phase-b/T-028-el0-userspace-wireup.md index b7a7130..bab4d00 100644 --- a/docs/analysis/tasks/phase-b/T-028-el0-userspace-wireup.md +++ b/docs/analysis/tasks/phase-b/T-028-el0-userspace-wireup.md @@ -22,13 +22,15 @@ Every B6 mechanism is in place and merged: high-half ([T-022](T-022-high-half-ke ## Acceptance criteria - [ ] **Load + create:** on boot, `load_image(USERSPACE_IMAGE, …)` → `task_create_from_image(&loaded, …)` mints the EL0 task cap (replacing the load-and-discard smoke). -- [ ] **Per-task capability table seeded:** a `CapabilityTable` for the EL0 task holding a **`DebugConsole`** capability (`CapRights::CONSOLE_WRITE`) at the handle the `hello` program names; bound to the task via `add_user_task`'s `cap_table` parameter (gate #3 — [T-026](T-026-current-task-cap-table.md)). The handle value `hello` hard-codes and the seeded slot must agree. -- [ ] **Per-task kernel stack:** a valid, 16-byte-aligned `SP_EL1` kernel stack (≥ the 272-byte trap frame + the gate-#1 dispatch call-tree depth) passed to `add_user_task` (the EL0→EL1 trap runs the trampoline's `sub sp, sp, #272` on `SP_EL1`, which the CPU does **not** auto-initialise — [T-023](T-023-el0-entry-context.md) gate #2). Source (BSP static vs PMM-allocated) decided in implementation; asserted non-null + aligned. +- [ ] **AS-lifetime coupling** (security; the [T-024 §SEC-T024-01](T-024-task-create-from-image.md) carry-forward — "**before a real EL0 task runs**, a Task must not be able to outlive, or alias a reused slot of, its bound AS"): the minted Task stores an `AddressSpaceHandle` whose liveness is **not** coupled to the AS cap. T-028 satisfies the precondition by keeping the loaded AS cap **and** its arena slot live for the task's lifetime (the boot-created task + AS are never destroyed in v1) and **not** introducing an AS-destroy / slot-reuse path that could leave the Task's handle stale (a confused-deputy / use-after-free class). If a destroy path is added, it must check `references_object` across the task arena before freeing the AS. Called out in the security review. +- [ ] **Per-task capability table seeded:** a `CapabilityTable` for the EL0 task holding a **`DebugConsole`** capability (`CapRights::CONSOLE_WRITE`); bound to the task via `add_user_task`'s `cap_table` parameter (gate #3 — [T-026](T-026-current-task-cap-table.md)). The cap **must** be seeded so it resolves to the handle named by [T-027](T-027-userland-build-pipeline.md)'s `HELLO_CONSOLE_CAP` constant (the T-027↔T-028 interface — the seeding does not duplicate the value, it satisfies the constant). +- [ ] **Per-task kernel stack:** a valid, 16-byte-aligned `SP_EL1` kernel stack (**floor: at least one 4 KiB page**; ≥ the 272-byte trap frame + the gate-#1 dispatch call-tree depth) passed to `add_user_task` (the EL0→EL1 trap runs the trampoline's `sub sp, sp, #272` on `SP_EL1`, which the CPU does **not** auto-initialise — [T-023](T-023-el0-entry-context.md) gate #2). Source (BSP static vs PMM-allocated) decided in implementation; asserted non-null + aligned. - [ ] **Schedule + run:** `add_user_task(…)` enqueues the task Ready; the scheduler `start()`/dispatch activates the task's AS (`TTBR0_EL1` installed, `EPD0` cleared — the activation hook fires by construction, [ADR-0028](../../../decisions/0028-address-space-data-structure.md)) and `enter_el0` `ERET`s into EL0 at `entry_va`. - [ ] **`+0x400` round-trip:** the `hello` `console_write` SVC traps to the **lower-EL** sync vector (`VBAR_EL1+0x400`), `syscall_entry` resolves the task's own table + window + AS (gate #3), gate #1 translates the buffer through the task's `TTBR0`, the dispatcher copies + emits, `x0` = status, the trampoline `ERET`s back to EL0 — the round-trip B5's `+0x200` proxy could not prove. - [ ] **Clean exit:** the `hello` `task_exit` SVC terminates the task; the kernel reports termination and continues (the cooperative demo still reaches `tyrne: all tasks complete` or the documented post-EL0 shutdown line). -- [ ] **QEMU smoke (acceptance):** the serial trace shows the kernel greeting, then **"hello from userspace"** (or the chosen greeting) in the correct order, then clean `task_exit`; `-d int` shows the `+0x400` lower-EL `SVC` exception (EL0→EL1) with a clean `ERET`; **no new fault class**. -- [ ] **Explicit EL0-boundary security review** (the [T-026](T-026-current-task-cap-table.md) carry-forward DoD): a review of the now-attacker-observable boundary covering gate #1 ([UNSAFE-2026-0030](../../../audits/unsafe-log.md)), gate #2 ([UNSAFE-2026-0032](../../../audits/unsafe-log.md)), the gate-#3 cap-table sourcing (UNSAFE-2026-0014 Amendment), and the register scrub — filed under `docs/analysis/reviews/security-reviews/`. +- [ ] **QEMU smoke (acceptance, debug profile):** in a **debug build** (`console_write`, syscall number 5, is **debug-gated** — [ADR-0031](../../../decisions/0031-initial-syscall-set.md) / [`dispatch.rs`](../../../../kernel/src/syscall/dispatch.rs); in **release** it returns `BadSyscallNumber` and emits no output, so the greeting is observable only in debug) the serial trace shows the kernel greeting, then **"hello from userspace"** (the chosen greeting) in the correct order, then clean `task_exit`; `-d int` shows the `+0x400` lower-EL `SVC` exception (EL0→EL1) with a clean `ERET`; **no new fault class**. The expected **release** behaviour (greeting suppressed; `console_write` → `BadSyscallNumber`; the `+0x400` trap + `task_exit` still occur) is documented in the PR. +- [ ] **Smoke gate updated:** [`tools/smoke.sh`](../../../../tools/smoke.sh) gates (rc=1) on the **userspace greeting marker** (e.g. `grep -q "hello from userspace"`) **in addition to** the existing `"all tasks complete"` check — otherwise the greeting could be absent from the trace and the smoke would still PASS, so the acceptance above would not actually be enforced by the gate. (The greeting-marker gate is debug-profile; the release smoke gates on the trap + completion only.) +- [ ] **Explicit EL0-boundary security review** (the [T-026](T-026-current-task-cap-table.md) carry-forward DoD): a review of the now-attacker-observable boundary covering gate #1 ([UNSAFE-2026-0030](../../../audits/unsafe-log.md)), gate #2 ([UNSAFE-2026-0032](../../../audits/unsafe-log.md)), the gate-#3 cap-table sourcing (UNSAFE-2026-0014 Amendment), the register scrub, **and the AS-lifetime coupling (SEC-T024-01)** — filed under `docs/analysis/reviews/security-reviews/`. - [ ] **All gates green:** `cargo fmt --all --check`; host + kernel clippy `-D warnings`; host tests (+ any wire-up regression tests) green; `cargo kernel-build`; `tools/smoke.sh` PASS (the round-trip trace above); Miri 0 UB; any new `unsafe` audited. ## Out of scope @@ -49,3 +51,4 @@ All acceptance criteria checked; gates green (incl. Miri); the `+0x400` round-tr ## Review history - **2026-05-31 — opened Draft** in the [ADR-0039](../../../decisions/0039-userland-build-pipeline.md) Propose commit (the ADR's dependency chain names it — step 6; [ADR-0025 §Rule 1](../../../decisions/0025-adr-governance-amendments.md)). Composes already-Accepted mechanisms; no ADR of its own. Implementation follows T-027. +- **2026-05-31 — pre-Accept review-round (relayed, verified against source):** **(HIGH)** added the **AS-lifetime-coupling AC** — the [T-024 §SEC-T024-01](T-024-task-create-from-image.md) carry-forward ("before a real EL0 task runs, a Task must not outlive its bound AS") was not carried into this wire-up. **(Med)** clarified the QEMU-smoke AC is **debug-profile** (`console_write` is debug-gated — release returns `BadSyscallNumber`, suppressing the greeting) + documented the release behaviour. **(Med)** added the **`tools/smoke.sh` greeting-marker gate AC** (the existing gate only checks `"all tasks complete"`, so the greeting could be absent and smoke still PASS — the acceptance would not be enforced). **(Low)** pinned the kernel-stack floor at ≥ one 4 KiB page; linked the cap-seeding to T-027's `HELLO_CONSOLE_CAP`; added SEC-T024-01 to the security-review coverage. diff --git a/docs/decisions/0039-userland-build-pipeline.md b/docs/decisions/0039-userland-build-pipeline.md index c06f64c..2b28208 100644 --- a/docs/decisions/0039-userland-build-pipeline.md +++ b/docs/decisions/0039-userland-build-pipeline.md @@ -8,7 +8,7 @@ [ADR-0029](0029-initial-userspace-image-format.md) settled the userspace image **format** (raw flat binary, entry at offset 0) and explicitly **deferred the build orchestration**: its §Decision-outcome "Build pipeline (B6)" row says B6's `userland/hello/` crate is built via `cargo build --target aarch64-unknown-none` + `objcopy -O binary` and embedded via `include_bytes!`, with "the exact path lands with B6." B4 ([T-019](../analysis/tasks/phase-b/T-019-task-loader.md)) shipped a hand-coded placeholder blob (`USERSPACE_IMAGE` at [`bsp-qemu-virt/src/main.rs`](../../bsp-qemu-virt/src/main.rs) — `mov w0, #42; ret`); that is the only userspace image in tree today, and it is loaded-then-discarded by a boot-time loader smoke (never run in EL0). -With B6's three [T-021 carry-forward gates](../roadmap/phases/phase-b.md#t-021-carry-forward-gates-must-close-before-a-real-el0-task-runs) now closed ([T-022](../analysis/tasks/phase-b/T-022-high-half-kernel-mapping.md) high-half, [T-023](../analysis/tasks/phase-b/T-023-el0-entry-context.md) EL0-context, [T-025](../analysis/tasks/phase-b/T-025-user-access-translation.md) gate #1, [T-026](../analysis/tasks/phase-b/T-026-current-task-cap-table.md) gate #3) and `task_create_from_image` ([T-024](../analysis/tasks/phase-b/T-024-task-create-from-image.md)) merged, the remaining B6 work is to (1) produce a **real** userspace program from Rust source and embed it, and (2) wire it to run in EL0. This ADR settles the **process** ADR-0029 deferred: **how** the userland crate is built, objcopy'd, and embedded; **where** the `userland/hello` + `tyrne-user` crates live in the workspace; **which** objcopy tool is used; and **where** the userspace base VA lives so the loader and the userspace linker script cannot drift. +With B6's three [T-021 carry-forward gates](../roadmap/phases/phase-b.md#t-021-carry-forward-gates-must-close-before-a-real-el0-task-runs) now closed (gate #1 [T-025](../analysis/tasks/phase-b/T-025-user-access-translation.md), gate #2 [T-023](../analysis/tasks/phase-b/T-023-el0-entry-context.md) EL0-context, gate #3 [T-026](../analysis/tasks/phase-b/T-026-current-task-cap-table.md)), the high-half prerequisite ([T-022](../analysis/tasks/phase-b/T-022-high-half-kernel-mapping.md) / [ADR-0033](0033-kernel-high-half-migration.md)) in place, and `task_create_from_image` ([T-024](../analysis/tasks/phase-b/T-024-task-create-from-image.md)) merged, the remaining B6 work is to (1) produce a **real** userspace program from Rust source and embed it, and (2) wire it to run in EL0. This ADR settles the **process** ADR-0029 deferred: **how** the userland crate is built, objcopy'd, and embedded; **where** the `userland/hello` + `tyrne-user` crates live in the workspace; **which** objcopy tool is used; and **where** the userspace base VA lives so the loader and the userspace linker script cannot drift. The stakes are precedent, not just plumbing. This is the project's **first** cross-target build artifact (a binary compiled for a *different* privilege domain and embedded into the kernel image). Getting the orchestration wrong has three concrete failure modes: a `build.rs` that invokes `cargo` recursively can deadlock on the workspace target-directory lock; an integration that builds the bare-metal userland under host commands (`cargo test`/`cargo build`) breaks the host test suite; and "build magic" hidden in a build script is hard to audit in a security-first kernel. The shape chosen here is the shape every future userland crate (B7+) will follow. @@ -33,7 +33,7 @@ The stakes are precedent, not just plumbing. This is the project's **first** cro Chosen option: **Option 1 — `tools/build-userland.sh`**, with these bundled sub-decisions: -- **Crates.** Add two workspace members under a new `userland/` directory: `userland/hello/` (package `tyrne-userland-hello`, `#![no_std] #![no_main]`, the raw-flat program) and `tyrne-user/` (package `tyrne-user`, `#![no_std]` library of **safe** syscall wrappers `console_write` / `task_exit`, which `hello` depends on). Both are added to `members` and **excluded from `default-members`** (so host commands skip them, mirroring `bsp-qemu-virt`). `tyrne-` package-name prefix per [ADR-0006](0006-workspace-layout.md). +- **Crates.** Add two workspace members under a new `userland/` directory: `userland/hello/` (package `tyrne-userland-hello`, `#![no_std] #![no_main]`, the raw-flat program) and `userland/tyrne-user/` (package `tyrne-user`, `#![no_std]` library of **safe** syscall wrappers `console_write` / `task_exit`, which `hello` depends on). Both are added to `members` and **excluded from `default-members`** (so host commands skip them, mirroring `bsp-qemu-virt`). `tyrne-` package-name prefix per [ADR-0006](0006-workspace-layout.md). - **objcopy.** `rust-objcopy -O binary` from the already-pinned `llvm-tools-preview` component — **no Cargo dependency**, so the K3-8 `cargo-vet` flag stays unfired. The script resolves the binary via the active toolchain (`rustc --print sysroot` + the `llvm-tools` bin dir) and fails with a clear diagnostic if the component is missing. - **Base-VA source-of-truth.** A single `pub const USERSPACE_IMAGE_BASE_VA` lives Rust-side (read by the BSP loader call site and any kernel consumer); the userspace linker script **restates** the same literal with a `keep-in-sync-with` comment, because a linker script cannot import a Rust const. A `ld --defsym`-from-const upgrade is named for full drift-elimination when it earns its keep. - **Artifact.** The `.bin` is **git-ignored** (regenerated from source; no binary blob in the repo — the Rust source stays the auditable truth). The BSP `build.rs` adds `rerun-if-changed` on it and `include_bytes!`s a stable path; if the file is absent it emits a `panic!` naming `tools/build-userland.sh`. `tools/smoke.sh` runs the script before `cargo kernel-build`, so the canonical path is unchanged. From 751b7db4a1926aedd538c477d558049e75cba574 Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Sun, 31 May 2026 23:35:37 +0300 Subject: [PATCH 3/7] =?UTF-8?q?docs(adr):=20ADR-0039=20+=20T-027=20?= =?UTF-8?q?=E2=80=94=20adversarial=20review=20fix=20(CI=20build-userland?= =?UTF-8?q?=20ordering)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adversarial 5-lens pre-Accept review (81 agents, per-finding 2-skeptic verification): 38 findings, 37 refuted, 1 confirmed (governance lens fully clean). The confirmed finding: CI's kernel-build/kernel-clippy jobs run 'cargo kernel-build' directly on a clean checkout, so once USERSPACE_IMAGE becomes include_bytes! of a git-ignored .bin, the BSP build.rs missing-.bin panic would break CI. Fix: T-027 gains a CI-workflow-update AC (run tools/build-userland.sh before kernel-build); ADR-0039's stale 'what CI runs' claim corrected (CI runs kernel-build, not the smoke). The 37 refutations confirmed the prior review-round's fixes are present and the option analysis / governance / feasibility are sound. Refs: ADR-0039 Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md | 4 +++- docs/decisions/0039-userland-build-pipeline.md | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md b/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md index 278b42b..c3e566e 100644 --- a/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md +++ b/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md @@ -30,7 +30,8 @@ ADR-0029 chose the raw-flat image format and deferred the build orchestration to - [ ] **Build orchestration** ([`tools/build-userland.sh`](../../../../tools/build-userland.sh)): `cargo build -p tyrne-userland-hello --target aarch64-unknown-none` (release + debug as the kernel profile dictates) → `rust-objcopy -O binary` (resolved from the active toolchain's `llvm-tools` — **no Cargo dependency**, K3-8 unfired) → a git-ignored `.bin` at a stable path. Clear error if `llvm-tools` / the ELF is missing. `.gitignore` updated. - [ ] **Workspace integration:** `userland/hello` + `tyrne-user` added to `members`, **excluded from `default-members`** (host commands skip them — mirrors `bsp-qemu-virt`); `cargo host-test` / `cargo build` / `cargo host-clippy` unaffected (verified). - [ ] **BSP embed:** `USERSPACE_IMAGE` becomes `include_bytes!()`; [`bsp-qemu-virt/build.rs`](../../../../bsp-qemu-virt/build.rs) gains `rerun-if-changed` on the `.bin` and a `panic!` naming `tools/build-userland.sh` if it is absent. -- [ ] **`tools/smoke.sh` runs the userland build first** so the canonical smoke + CI path is unchanged (still one entry point). +- [ ] **`tools/smoke.sh` runs the userland build first** so the canonical local smoke path stays one entry point. +- [ ] **CI workflow updated:** [`.github/workflows/ci.yml`](../../../../.github/workflows/ci.yml) runs `tools/build-userland.sh` **before** its `kernel-build` / `kernel-clippy` jobs — otherwise, once `USERSPACE_IMAGE` becomes `include_bytes!` of the git-ignored `.bin`, those jobs hit the BSP `build.rs` missing-`.bin` `panic!` on a clean checkout and **CI breaks**. (The QEMU-smoke-in-CI regression gate itself stays the conditionally-deferred flag K3-7 — no QEMU-smoke CI job exists today.) - [ ] **Disassembly verification:** the objcopy'd `.bin` round-trips — a host test (or a documented `rust-objdump -d` check in the task's review-history) confirms offset 0 is the entry instruction and the SVC sequence + greeting match `hello`'s source. (The image is **not run** in T-027 — running is T-028.) - [ ] **All gates green:** `cargo fmt --all --check`; host + kernel clippy `-D warnings`; **the new userland crates linted** (`cargo clippy --target aarch64-unknown-none -p tyrne-userland-hello -p tyrne-user -- -D warnings` — neither `kernel-clippy` (only `-p tyrne-bsp-qemu-virt`) nor `host-clippy` (`default-members`, which excludes them) covers them, so this explicit invocation is required); host tests unchanged + green; `cargo kernel-build`; `tools/smoke.sh` PASS — the embedded real image loads via the existing loader smoke (maps `USER|EXECUTE` image + `USER|WRITE` stack, `LoadedImage` metadata printed), **byte-stable boot otherwise**, zero new fault class. Miri unaffected (no kernel logic change). @@ -53,3 +54,4 @@ All acceptance criteria checked; gates green; the embedded real image replaces t - **2026-05-31 — opened Draft** in the [ADR-0039](../../../decisions/0039-userland-build-pipeline.md) Propose commit (the ADR's dependency chain names it — step 5; [ADR-0025 §Rule 1](../../../decisions/0025-adr-governance-amendments.md)). Implementation follows the ADR Accept. - **2026-05-31 — pre-Accept review-round (relayed, verified against source):** **(HIGH)** added the **userland-`unsafe`-audit AC** — the `tyrne-user` `svc #0` shims are `unsafe`; [unsafe-policy §Scope](../../../standards/unsafe-policy.md) applies "equally in … userspace code", so each needs a `// SAFETY:` comment + a `docs/audits/unsafe-log.md` entry (previously unstated). **(Med)** pinned the `tyrne-user` path to `userland/tyrne-user/` (was "or workspace root", contradicting the ADR). **(Med)** added the **`HELLO_CONSOLE_CAP` handle-contract AC** (the T-027↔T-028 interface — closes a silent handle-mismatch risk). **(Low)** added the explicit userland clippy gate (`-p tyrne-userland-hello -p tyrne-user` — neither `kernel-clippy` nor `host-clippy` covers the new crates). All against the still-`Proposed` ADR-0039, pre-Accept. +- **2026-05-31 — adversarial ADR-0039 review (5-lens workflow, 81 agents, per-finding 2-skeptic verification): 38 findings, 37 refuted, 1 confirmed; governance lens fully clean.** **Confirmed (fixed):** CI's `kernel-build` job runs `cargo kernel-build` directly on a clean checkout ([`.github/workflows/ci.yml`](../../../../.github/workflows/ci.yml)), so once `USERSPACE_IMAGE` becomes `include_bytes!` of a git-ignored `.bin`, the BSP `build.rs` missing-`.bin` `panic!` would **break CI** — added the CI-workflow-update AC here + corrected ADR-0039's stale "what CI runs" claim (CI runs `kernel-build`, not the smoke). **Refuted (37):** the adversarial pass confirmed the prior review-round's fixes are present (AS-lifetime SEC-T024-01, the `HELLO_CONSOLE_CAP` contract, the no-`.data` constraint, the greeting-marker gate — all already in T-027/T-028), that the 4-option analysis is fair (Option 2/4 not strawmanned; artifact-dependencies considered + rejected as unstable-on-pinned-nightly), that governance/`write-adr` compliance is clean, and that the rust-objcopy / offset-0 / default-members feasibility holds. diff --git a/docs/decisions/0039-userland-build-pipeline.md b/docs/decisions/0039-userland-build-pipeline.md index 2b28208..78ce604 100644 --- a/docs/decisions/0039-userland-build-pipeline.md +++ b/docs/decisions/0039-userland-build-pipeline.md @@ -70,9 +70,9 @@ T-027 closes steps 5 (the build pipeline + the two crates + the embedded real im ### Negative -- **A bare `cargo kernel-build` needs the `.bin` present first.** On a fresh checkout, `cargo kernel-build` alone fails until `tools/build-userland.sh` has run. *Mitigation:* the BSP `build.rs` emits a `panic!` that names the script; `tools/smoke.sh` (the canonical entry point, and what CI runs) chains the script before the kernel build; the dev loop is `tools/build-userland.sh && cargo kernel-build` or simply `tools/smoke.sh`. We accept this small ergonomic cost in exchange for no build-script magic and no committed binary. +- **A bare `cargo kernel-build` needs the `.bin` present first.** On a fresh checkout, `cargo kernel-build` alone fails until `tools/build-userland.sh` has run. *Mitigation:* the BSP `build.rs` emits a `panic!` that names the script; `tools/smoke.sh` (the canonical **local** entry point) chains the script before the kernel build; the dev loop is `tools/build-userland.sh && cargo kernel-build` or simply `tools/smoke.sh`. (CI is a separate concern — see the next item.) We accept this small ergonomic cost in exchange for no build-script magic and no committed binary. - **The base VA is stated in two places** (the Rust const and the userspace linker script), because LD cannot import a Rust const. *Mitigation:* a `keep-in-sync-with` comment on the linker-script line, the same discipline ADR-0029 already imposes ("linker-script awareness leaks into the loader"); the `ld --defsym`-from-const path is named for when full drift-elimination earns its keep (B7+ multi-program / multi-BSP). -- **The `.bin` is git-ignored, so CI must build it.** A clean CI run must execute `tools/build-userland.sh` (or `tools/smoke.sh`) before the kernel build. *Mitigation:* this is standard for embedded projects and is one line in the smoke path; the alternative (committing the blob) trades it for a binary in git history and a freshness-drift check, which we reject. +- **The `.bin` is git-ignored, so CI must build it — and the existing CI `kernel-build` job will break without it.** CI today runs `cargo kernel-build` (and `kernel-clippy`) directly on a clean checkout ([`.github/workflows/ci.yml`](../../.github/workflows/ci.yml)); post-T-027, that job hits the BSP `build.rs` missing-`.bin` `panic!`, so **T-027 must add the userland build step to the CI workflow ahead of the `kernel-build` / `kernel-clippy` jobs** (and the maintainer-launched `tools/smoke.sh` already chains it locally). *Mitigation:* one step in the workflow; standard for embedded projects. The alternative (committing the blob) trades it for a binary in git history and a freshness-drift check, which we reject. (The QEMU-smoke-in-CI regression gate itself remains the conditionally-deferred flag K3-7 — [phase-b §Flags](../roadmap/phases/phase-b.md#flags-to-resolve-during-b6); no QEMU-smoke CI job exists today.) ### Neutral From df3fcd42d02f509092bd8703e48b53f3f6f787d4 Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Sun, 31 May 2026 23:50:24 +0300 Subject: [PATCH 4/7] =?UTF-8?q?docs(adr):=20accept=20ADR-0039=20=E2=80=94?= =?UTF-8?q?=20userland=20build=20pipeline=20(B6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Careful re-read complete (two review rounds: relayed + adversarial 5-lens/81-agent, 37/38 refuted). Flips Proposed -> Accepted in a separate commit per write-adr §10. T-027 (build pipeline + crates) implementation follows. Refs: ADR-0039 Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/decisions/0039-userland-build-pipeline.md | 2 +- docs/decisions/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/decisions/0039-userland-build-pipeline.md b/docs/decisions/0039-userland-build-pipeline.md index 78ce604..b2887d3 100644 --- a/docs/decisions/0039-userland-build-pipeline.md +++ b/docs/decisions/0039-userland-build-pipeline.md @@ -1,6 +1,6 @@ # 0039 — Userland build pipeline (B6 — `userland/hello` + `tyrne-user` + raw-flat embed orchestration) -- **Status:** Proposed +- **Status:** Accepted - **Date:** 2026-05-31 - **Deciders:** @cemililik diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 0116335..f1aeb14 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -66,7 +66,7 @@ Each ADR contains: | 0036 | [QEMU virt is GICv2 / no-IOMMU in v1; corrects GICv3/SMMUv3 in ADR-0004/0006/0012](0036-qemu-virt-gicv2-no-iommu-v1.md) | Accepted | 2026-05-22 | | 0037 | [EL0 entry context (B6 — userspace register file + enter-EL0/`ERET` path + per-task `SP_EL1`)](0037-el0-entry-context.md) | Accepted | 2026-05-31 | | 0038 | [`Mmu::translate` read-only walk + per-task user-access translation (B6 gate #1)](0038-mmu-translate-and-user-access.md) | Accepted | 2026-05-31 | -| 0039 | [Userland build pipeline (B6 — `userland/hello` + `tyrne-user` + raw-flat embed orchestration)](0039-userland-build-pipeline.md) | Proposed | 2026-05-31 | +| 0039 | [Userland build pipeline (B6 — `userland/hello` + `tyrne-user` + raw-flat embed orchestration)](0039-userland-build-pipeline.md) | Accepted | 2026-05-31 | > **Numbering gaps.** Slot **0034** is intentionally reserved, not missing: 0034 (kernel-image section permissions) is a named-but-unallocated placeholder forward-flagged in ADR-0027. No file exists for it yet; it opens when the corresponding work surfaces (the first attacker-observable EL0 execution — likely B6). (Slot **0033** (high-half migration) was filed `Proposed` on 2026-05-29 to open B6 and `Accepted` on 2026-05-30, and is no longer a gap; slots **0030**/**0031** were filed and `Accepted` on 2026-05-29 for B5.) ADR numbers are stable history and are never renumbered. From 443634bad0cce0779c73ae2509b3365ade67babf Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Mon, 1 Jun 2026 00:07:30 +0300 Subject: [PATCH 5/7] =?UTF-8?q?feat(userland):=20T-027=20=E2=80=94=20userl?= =?UTF-8?q?and/hello=20+=20tyrne-user=20+=20raw-flat=20build=20pipeline=20?= =?UTF-8?q?(B6=20step=205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements ADR-0039: Tyrne's first real userspace program, built from Rust source and embedded into the kernel image, replacing the hand-coded mov-w0-#42 placeholder. - userland/tyrne-user (no_std lib): safe console_write / task_exit wrappers over the svc #0 ABI (x8=number, x0..x5=args; ConsoleWrite=5, TaskExit=4); HELLO_CONSOLE_CAP const (the T-027<->T-028 cap-handle interface = root cap, index 0/gen 0 = 0). The two svc shims are the first userspace unsafe (UNSAFE-2026-0033). - userland/hello (no_std no_main bin): _start at .text._start (offset 0) -> console_write(greeting) -> task_exit(0); panic_handler -> task_exit. hello.ld fixes the entry at USERSPACE_IMAGE_BASE_VA (0x0080_0000), ASSERTs no .data/.bss (image maps USER|EXECUTE, no WRITE). - tools/build-userland.sh: cargo build -> rust-objcopy -O binary (from the pinned llvm-tools-preview, NO Cargo dep) -> git-ignored userland/hello/hello.bin. - Workspace: both crates added to members, excluded from default-members (host commands skip them). - BSP: USERSPACE_IMAGE -> include_bytes!(hello.bin); build.rs assert!s the .bin exists (run build-userland.sh first). - tools/smoke.sh now builds userland + kernel (one-command); .github/workflows/ci.yml installs llvm-tools-preview + builds userland before kernel-build (else the clean-checkout kernel-build job hits the missing-.bin assert). Verification: the 117-byte .bin disassembles at offset 0 to _start prologue -> x0=0 (HELLO_CONSOLE_CAP) -> adr x1 greeting -> w2=0x15 (21=len) -> bl console_write -> x0=0 -> bl task_exit. The image loads DORMANT under the smoke (entry=0x800000, image bytes 117, stack bytes 4096); running it in EL0 is T-028. Gates: fmt; host + kernel + userland clippy -D warnings; host tests 46 hal / 258 kernel / 58 test-hal / 3 doc (unchanged — userland is bare-metal); kernel build; tools/smoke.sh --int PASS (image loads, all tasks complete, 2 stub SVCs, zero new fault class); Miri unaffected (no kernel/host logic change). UNSAFE-2026-0033. Refs: T-027, ADR-0039 Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 10 +- Cargo.lock | 11 ++ Cargo.toml | 7 +- bsp-qemu-virt/build.rs | 19 +++ bsp-qemu-virt/src/main.rs | 28 ++-- .../phase-b/T-027-userland-build-pipeline.md | 29 ++-- docs/audits/unsafe-log.md | 19 +++ docs/roadmap/current.md | 2 + docs/roadmap/phases/phase-b.md | 2 +- tools/build-userland.sh | 54 ++++++++ tools/smoke.sh | 13 ++ userland/hello/Cargo.toml | 18 +++ userland/hello/build.rs | 14 ++ userland/hello/hello.ld | 54 ++++++++ userland/hello/src/main.rs | 51 +++++++ userland/tyrne-user/Cargo.toml | 14 ++ userland/tyrne-user/src/lib.rs | 130 ++++++++++++++++++ 17 files changed, 446 insertions(+), 29 deletions(-) create mode 100755 tools/build-userland.sh create mode 100644 userland/hello/Cargo.toml create mode 100644 userland/hello/build.rs create mode 100644 userland/hello/hello.ld create mode 100644 userland/hello/src/main.rs create mode 100644 userland/tyrne-user/Cargo.toml create mode 100644 userland/tyrne-user/src/lib.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1918283..ad4be72 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,7 +115,10 @@ jobs: persist-credentials: false - name: Install pinned nightly + aarch64 target run: | - rustup toolchain install $NIGHTLY_PIN --component clippy --no-self-update + # llvm-tools-preview provides rust-objcopy for the userland build + # step below (ADR-0039); it is pinned in rust-toolchain.toml but this + # explicit install does not read that file, so name it here. + rustup toolchain install $NIGHTLY_PIN --component clippy --component llvm-tools-preview --no-self-update rustup override set $NIGHTLY_PIN rustup target add aarch64-unknown-none --toolchain $NIGHTLY_PIN - name: Cache cargo registry and build @@ -129,6 +132,11 @@ jobs: key: ${{ runner.os }}-cargo-aarch64-${{ env.NIGHTLY_PIN }}-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-aarch64-${{ env.NIGHTLY_PIN }}- + - name: build userland image (ADR-0039) + # Produces userland/hello/hello.bin (rust-objcopy of the userland crate) + # which the BSP embeds via include_bytes!; MUST run before kernel-build, + # whose build.rs panics if the .bin is absent. + run: tools/build-userland.sh - name: cargo kernel-build run: cargo +$NIGHTLY_PIN kernel-build - name: cargo kernel-clippy diff --git a/Cargo.lock b/Cargo.lock index 7ac40a8..b953821 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,3 +28,14 @@ version = "0.0.1" dependencies = [ "tyrne-hal", ] + +[[package]] +name = "tyrne-user" +version = "0.0.1" + +[[package]] +name = "tyrne-userland-hello" +version = "0.0.1" +dependencies = [ + "tyrne-user", +] diff --git a/Cargo.toml b/Cargo.toml index 615f461..ea56fec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,16 @@ members = [ "hal", "bsp-qemu-virt", "test-hal", + "userland/tyrne-user", + "userland/hello", ] # Default set for workspace-wide commands (`cargo check`, `cargo test`, etc.): # everything host-buildable. The BSP is #![no_std, no_main] and requires the # explicit bare-metal target; build it via `cargo kernel-build` (or -# `cargo build --target aarch64-unknown-none -p tyrne-bsp-qemu-virt`). +# `cargo build --target aarch64-unknown-none -p tyrne-bsp-qemu-virt`). The +# userland crates (tyrne-user lib + the no_main hello bin) are likewise +# bare-metal-only — built via `tools/build-userland.sh` (ADR-0039), excluded +# here so host commands (`cargo build`/`test`/`host-clippy`) skip them. default-members = [ "kernel", "hal", diff --git a/bsp-qemu-virt/build.rs b/bsp-qemu-virt/build.rs index 1771adc..9cab460 100644 --- a/bsp-qemu-virt/build.rs +++ b/bsp-qemu-virt/build.rs @@ -4,6 +4,11 @@ //! path so resolution does not depend on the linker's working directory. //! See `docs/decisions/0012-boot-flow-qemu-virt.md` for the memory layout the //! linker script encodes. +//! +//! Also asserts the userland image (`userland/hello/hello.bin`) that +//! `main.rs` embeds via `include_bytes!` exists, and re-runs when it changes — +//! it is produced by `tools/build-userland.sh` (ADR-0039), which must run +//! before `cargo kernel-build`. fn main() { let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") @@ -14,4 +19,18 @@ fn main() { println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=src/boot.s"); println!("cargo:rerun-if-changed=src/vectors.s"); + + // Userland image (ADR-0039 / T-027): `main.rs` embeds this via + // `include_bytes!`. It is produced by `tools/build-userland.sh` + // (cargo build -> rust-objcopy) — NOT by this build script (no nested + // cargo). Fail loudly with the remedy if it is absent, rather than letting + // `include_bytes!` emit an opaque "file not found". + let hello_bin = format!("{manifest_dir}/../userland/hello/hello.bin"); + println!("cargo:rerun-if-changed={hello_bin}"); + assert!( + std::path::Path::new(&hello_bin).exists(), + "userland image not built: {hello_bin} is missing.\n \ + Run `tools/build-userland.sh` before `cargo kernel-build` \ + (ADR-0039); `tools/smoke.sh` and CI do this automatically." + ); } diff --git a/bsp-qemu-virt/src/main.rs b/bsp-qemu-virt/src/main.rs index 9524ac2..2d827f6 100644 --- a/bsp-qemu-virt/src/main.rs +++ b/bsp-qemu-virt/src/main.rs @@ -327,22 +327,26 @@ static BOOTSTRAP_AS_CAP: StaticCell = StaticCell::new(); /// untyped / memory-region authority caps. static BOOTSTRAP_AS_TABLE: StaticCell = StaticCell::new(); -// ─── T-019 task loader placeholder image (ADR-0029) ─────────────────────────── +// ─── Userland image (ADR-0029 format / ADR-0039 build pipeline) ─────────────── -/// Placeholder userspace image: 8 bytes of aarch64 `mov w0, #42; ret` -/// per [ADR-0029 §Decision outcome (Build pipeline — B4 / T-019)][adr-0029]. -/// The real B6 "hello" userspace binary lands with `userland/hello/` -/// per [ADR-0029 §Decision outcome (Build pipeline — B6)][adr-0029]; -/// T-019 ships with this hand-coded blob as the loader's smoke fixture. +/// The real B6 "hello" userspace image: the raw-flat (`rust-objcopy -O binary`) +/// output of the [`tyrne-userland-hello`] crate, embedded at compile time per +/// [ADR-0039][adr-0039]. Entry at offset 0 ([ADR-0029][adr-0029] raw-flat +/// format), mapped `USER | EXECUTE` by the loader. It replaces the B4/T-019 +/// hand-coded `mov w0, #42; ret` placeholder. /// -/// **Not executed.** T-019 produces a `LoadedImage` describing a -/// populated AS; running gates on B5 (syscall ABI per ADR-0030) + B6 -/// (first userspace "hello") which together provide the prerequisites -/// (kernel mappings in userspace AS, EL0-ready context, syscall -/// entry). +/// **Build dependency:** produced by `tools/build-userland.sh` (which the +/// BSP [`build.rs`] requires to have run — it `panic!`s if this `.bin` is +/// absent) before `cargo kernel-build`. `tools/smoke.sh` and the CI +/// kernel-build job both run that script first. +/// +/// **Not yet executed (T-027).** Embedding is T-027; loading it into a real +/// EL0 task and running the `+0x400` round-trip is the T-028 wire-up. Today the +/// boot-time loader smoke maps it (proving it loads) and the demo then idles. /// /// [adr-0029]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0029-initial-userspace-image-format.md -static USERSPACE_IMAGE: &[u8] = &[0x40, 0x05, 0x80, 0x52, 0xc0, 0x03, 0x5f, 0xd6]; +/// [adr-0039]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0039-userland-build-pipeline.md +static USERSPACE_IMAGE: &[u8] = include_bytes!("../../userland/hello/hello.bin"); /// Base VA the loader places the image at — userspace VA range per /// [ADR-0027 §Decision outcome (a)][adr-0027]'s `TTBR0_EL1` range. diff --git a/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md b/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md index c3e566e..85b9906 100644 --- a/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md +++ b/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md @@ -2,7 +2,7 @@ - **Phase:** B - **Milestone:** B6 — First userspace "hello" (step 5 of the [B6 dependency-ordered sequence](../../../roadmap/phases/phase-b.md#milestone-b6--first-userspace-hello) — `tyrne-user` crate + `userland/hello/` crate + the `cargo build → rust-objcopy -O binary → include_bytes!` pipeline + the shared base-VA source-of-truth) -- **Status:** Draft (opened in the [ADR-0039](../../../decisions/0039-userland-build-pipeline.md) Propose commit per [ADR-0025 §Rule 1](../../../decisions/0025-adr-governance-amendments.md); implementation follows ADR-0039 Accept) +- **Status:** In Review (implemented on `t-027-userland-build-pipeline` after the [ADR-0039](../../../decisions/0039-userland-build-pipeline.md) Accept; all gates green; the embedded `hello` image is **dormant** — loading + running it in EL0 is [T-028](T-028-el0-userspace-wireup.md)) - **Created:** 2026-05-31 - **Author:** @cemililik (+ Claude Opus 4.8 agent) - **Dependencies:** [ADR-0039](../../../decisions/0039-userland-build-pipeline.md) (the build-pipeline orchestration this implements); [ADR-0029](../../../decisions/0029-initial-userspace-image-format.md) (the raw-flat format — entry at offset 0, image `USER|EXECUTE`); [ADR-0006](../../../decisions/0006-workspace-layout.md) (the workspace-member / `tyrne-` naming conventions); [ADR-0031](../../../decisions/0031-initial-syscall-set.md) + [ADR-0030](../../../decisions/0030-syscall-abi.md) (the syscall ABI the `tyrne-user` wrappers + the SVC sequence target); [T-019](T-019-task-loader.md) (the loader that maps this image; pins `USERSPACE_IMAGE_BASE_VA`). @@ -21,19 +21,19 @@ ADR-0029 chose the raw-flat image format and deferred the build orchestration to ## Acceptance criteria -- [ ] **`tyrne-user` crate** (`userland/tyrne-user/`, package `tyrne-user`, `#![no_std]`): safe wrappers over the [ADR-0031](../../../decisions/0031-initial-syscall-set.md) syscalls needed by `hello` — at minimum `console_write(cap: CapWord, buf: &[u8]) -> Result` and `task_exit(code) -> !` — each wrapping a thin `svc #0` inline-asm shim (an **`unsafe` block** — see the userland-`unsafe`-audit AC) packing the ABI ([ADR-0030](../../../decisions/0030-syscall-abi.md): `x8` = number, `x0..x5` = args; `ConsoleWrite = 5`, `TaskExit = 4`). No dependency on kernel internals (the ABI constants are restated/owned userspace-side or shared via a leaf crate). -- [ ] **Userland `unsafe` audit** (security policy applies to userspace too): the `tyrne-user` `svc #0` shims are `unsafe` (inline asm); per [unsafe-policy §Scope](../../../standards/unsafe-policy.md) ("the rules apply equally in kernel, HAL, and userspace code") **and** [CLAUDE.md non-negotiable #2](../../../../CLAUDE.md), each `unsafe` block carries a `// SAFETY:` comment (why the asm upholds the ABI contract — args in `x0..x5`/`x8`, the clobber list, no UB) **and** a dated [`docs/audits/unsafe-log.md`](../../../audits/unsafe-log.md) entry (`UNSAFE-2026-NNNN`). The workspace `clippy::undocumented_unsafe_blocks` / `missing_safety_doc` denies enforce the comment; the audit entry is the policy obligation. -- [ ] **`userland/hello` crate** (package `tyrne-userland-hello`, `#![no_std] #![no_main]`): a `_start` entry placed at **offset 0** of the linked image (linker `ENTRY` + a `KEEP`'d first section) that calls `tyrne_user::console_write` with a greeting string (living in the image's read-only data — reachable as a `USER` page, [T-025](T-025-user-access-translation.md) gate #1 requires only the `USER` flag for a read) then `tyrne_user::task_exit(0)`; a minimal `#[panic_handler]` that `task_exit`s or loops (no unwinder — `panic=abort` inherited). -- [ ] **Console-cap handle contract (T-027 ↔ T-028 interface):** `hello` names its `DebugConsole` capability via a **single documented named constant** (e.g. `pub const HELLO_CONSOLE_CAP: u64` in `tyrne-user`, the `cap_word` passed in `x0` — `encode_cap_handle` of the handle the wire-up seeds, typically the root cap → index 0 / generation 0). [T-028](T-028-el0-userspace-wireup.md)'s cap-table seeding **must** insert the `DebugConsole` cap so it resolves to that exact handle. The constant is the shared interface — defined once, not duplicated by value across the two tasks. -- [ ] **Userland linker script** places `.text` (entry first) + `.rodata` contiguously at `USERSPACE_IMAGE_BASE_VA`, **no `.data`/`.bss`** (writable globals would fault — image is `USER|EXECUTE`, no `WRITE`), 16-byte aligned; produces a contiguous byte stream with no ELF artifacts under `rust-objcopy -O binary`. -- [ ] **Base-VA source-of-truth:** a single Rust-side `pub const USERSPACE_IMAGE_BASE_VA = 0x0080_0000` (read by the BSP loader call site); the userland linker script restates the literal with a documented `keep-in-sync-with` comment (LD cannot import a Rust const — per [ADR-0039](../../../decisions/0039-userland-build-pipeline.md)). -- [ ] **Build orchestration** ([`tools/build-userland.sh`](../../../../tools/build-userland.sh)): `cargo build -p tyrne-userland-hello --target aarch64-unknown-none` (release + debug as the kernel profile dictates) → `rust-objcopy -O binary` (resolved from the active toolchain's `llvm-tools` — **no Cargo dependency**, K3-8 unfired) → a git-ignored `.bin` at a stable path. Clear error if `llvm-tools` / the ELF is missing. `.gitignore` updated. -- [ ] **Workspace integration:** `userland/hello` + `tyrne-user` added to `members`, **excluded from `default-members`** (host commands skip them — mirrors `bsp-qemu-virt`); `cargo host-test` / `cargo build` / `cargo host-clippy` unaffected (verified). -- [ ] **BSP embed:** `USERSPACE_IMAGE` becomes `include_bytes!()`; [`bsp-qemu-virt/build.rs`](../../../../bsp-qemu-virt/build.rs) gains `rerun-if-changed` on the `.bin` and a `panic!` naming `tools/build-userland.sh` if it is absent. -- [ ] **`tools/smoke.sh` runs the userland build first** so the canonical local smoke path stays one entry point. -- [ ] **CI workflow updated:** [`.github/workflows/ci.yml`](../../../../.github/workflows/ci.yml) runs `tools/build-userland.sh` **before** its `kernel-build` / `kernel-clippy` jobs — otherwise, once `USERSPACE_IMAGE` becomes `include_bytes!` of the git-ignored `.bin`, those jobs hit the BSP `build.rs` missing-`.bin` `panic!` on a clean checkout and **CI breaks**. (The QEMU-smoke-in-CI regression gate itself stays the conditionally-deferred flag K3-7 — no QEMU-smoke CI job exists today.) -- [ ] **Disassembly verification:** the objcopy'd `.bin` round-trips — a host test (or a documented `rust-objdump -d` check in the task's review-history) confirms offset 0 is the entry instruction and the SVC sequence + greeting match `hello`'s source. (The image is **not run** in T-027 — running is T-028.) -- [ ] **All gates green:** `cargo fmt --all --check`; host + kernel clippy `-D warnings`; **the new userland crates linted** (`cargo clippy --target aarch64-unknown-none -p tyrne-userland-hello -p tyrne-user -- -D warnings` — neither `kernel-clippy` (only `-p tyrne-bsp-qemu-virt`) nor `host-clippy` (`default-members`, which excludes them) covers them, so this explicit invocation is required); host tests unchanged + green; `cargo kernel-build`; `tools/smoke.sh` PASS — the embedded real image loads via the existing loader smoke (maps `USER|EXECUTE` image + `USER|WRITE` stack, `LoadedImage` metadata printed), **byte-stable boot otherwise**, zero new fault class. Miri unaffected (no kernel logic change). +- [x] **`tyrne-user` crate** (`userland/tyrne-user/`, package `tyrne-user`, `#![no_std]`): safe wrappers over the [ADR-0031](../../../decisions/0031-initial-syscall-set.md) syscalls needed by `hello` — at minimum `console_write(cap: CapWord, buf: &[u8]) -> Result` and `task_exit(code) -> !` — each wrapping a thin `svc #0` inline-asm shim (an **`unsafe` block** — see the userland-`unsafe`-audit AC) packing the ABI ([ADR-0030](../../../decisions/0030-syscall-abi.md): `x8` = number, `x0..x5` = args; `ConsoleWrite = 5`, `TaskExit = 4`). No dependency on kernel internals (the ABI constants are restated/owned userspace-side or shared via a leaf crate). +- [x] **Userland `unsafe` audit** (security policy applies to userspace too): the `tyrne-user` `svc #0` shims are `unsafe` (inline asm); per [unsafe-policy §Scope](../../../standards/unsafe-policy.md) ("the rules apply equally in kernel, HAL, and userspace code") **and** [CLAUDE.md non-negotiable #2](../../../../CLAUDE.md), each `unsafe` block carries a `// SAFETY:` comment (why the asm upholds the ABI contract — args in `x0..x5`/`x8`, the clobber list, no UB) **and** a dated [`docs/audits/unsafe-log.md`](../../../audits/unsafe-log.md) entry (`UNSAFE-2026-NNNN`). The workspace `clippy::undocumented_unsafe_blocks` / `missing_safety_doc` denies enforce the comment; the audit entry is the policy obligation. +- [x] **`userland/hello` crate** (package `tyrne-userland-hello`, `#![no_std] #![no_main]`): a `_start` entry placed at **offset 0** of the linked image (linker `ENTRY` + a `KEEP`'d first section) that calls `tyrne_user::console_write` with a greeting string (living in the image's read-only data — reachable as a `USER` page, [T-025](T-025-user-access-translation.md) gate #1 requires only the `USER` flag for a read) then `tyrne_user::task_exit(0)`; a minimal `#[panic_handler]` that `task_exit`s or loops (no unwinder — `panic=abort` inherited). +- [x] **Console-cap handle contract (T-027 ↔ T-028 interface):** `hello` names its `DebugConsole` capability via a **single documented named constant** (e.g. `pub const HELLO_CONSOLE_CAP: u64` in `tyrne-user`, the `cap_word` passed in `x0` — `encode_cap_handle` of the handle the wire-up seeds, typically the root cap → index 0 / generation 0). [T-028](T-028-el0-userspace-wireup.md)'s cap-table seeding **must** insert the `DebugConsole` cap so it resolves to that exact handle. The constant is the shared interface — defined once, not duplicated by value across the two tasks. +- [x] **Userland linker script** places `.text` (entry first) + `.rodata` contiguously at `USERSPACE_IMAGE_BASE_VA`, **no `.data`/`.bss`** (writable globals would fault — image is `USER|EXECUTE`, no `WRITE`), 16-byte aligned; produces a contiguous byte stream with no ELF artifacts under `rust-objcopy -O binary`. +- [x] **Base-VA source-of-truth:** a single Rust-side `pub const USERSPACE_IMAGE_BASE_VA = 0x0080_0000` (read by the BSP loader call site); the userland linker script restates the literal with a documented `keep-in-sync-with` comment (LD cannot import a Rust const — per [ADR-0039](../../../decisions/0039-userland-build-pipeline.md)). +- [x] **Build orchestration** ([`tools/build-userland.sh`](../../../../tools/build-userland.sh)): `cargo build -p tyrne-userland-hello --target aarch64-unknown-none` (release + debug as the kernel profile dictates) → `rust-objcopy -O binary` (resolved from the active toolchain's `llvm-tools` — **no Cargo dependency**, K3-8 unfired) → a git-ignored `.bin` at a stable path (`userland/hello/hello.bin`). Clear error if `llvm-tools` / the ELF is missing. (The `.bin` is git-ignored by the repo's existing `*.bin` rule — no `.gitignore` change needed.) +- [x] **Workspace integration:** `userland/hello` + `tyrne-user` added to `members`, **excluded from `default-members`** (host commands skip them — mirrors `bsp-qemu-virt`); `cargo host-test` / `cargo build` / `cargo host-clippy` unaffected (verified). +- [x] **BSP embed:** `USERSPACE_IMAGE` becomes `include_bytes!()`; [`bsp-qemu-virt/build.rs`](../../../../bsp-qemu-virt/build.rs) gains `rerun-if-changed` on the `.bin` and a `panic!` naming `tools/build-userland.sh` if it is absent. +- [x] **`tools/smoke.sh` runs the userland build first** so the canonical local smoke path stays one entry point. +- [x] **CI workflow updated:** [`.github/workflows/ci.yml`](../../../../.github/workflows/ci.yml) runs `tools/build-userland.sh` **before** its `kernel-build` / `kernel-clippy` jobs — otherwise, once `USERSPACE_IMAGE` becomes `include_bytes!` of the git-ignored `.bin`, those jobs hit the BSP `build.rs` missing-`.bin` `panic!` on a clean checkout and **CI breaks**. (The QEMU-smoke-in-CI regression gate itself stays the conditionally-deferred flag K3-7 — no QEMU-smoke CI job exists today.) +- [x] **Disassembly verification:** the objcopy'd `.bin` round-trips — a host test (or a documented `rust-objdump -d` check in the task's review-history) confirms offset 0 is the entry instruction and the SVC sequence + greeting match `hello`'s source. (The image is **not run** in T-027 — running is T-028.) +- [x] **All gates green:** `cargo fmt --all --check`; host + kernel clippy `-D warnings`; **the new userland crates linted** (`cargo clippy --target aarch64-unknown-none -p tyrne-userland-hello -p tyrne-user -- -D warnings` — neither `kernel-clippy` (only `-p tyrne-bsp-qemu-virt`) nor `host-clippy` (`default-members`, which excludes them) covers them, so this explicit invocation is required); host tests unchanged + green; `cargo kernel-build`; `tools/smoke.sh` PASS — the embedded real image loads via the existing loader smoke (maps `USER|EXECUTE` image + `USER|WRITE` stack, `LoadedImage` metadata printed), **byte-stable boot otherwise**, zero new fault class. Miri unaffected (no kernel logic change). ## Out of scope @@ -55,3 +55,4 @@ All acceptance criteria checked; gates green; the embedded real image replaces t - **2026-05-31 — opened Draft** in the [ADR-0039](../../../decisions/0039-userland-build-pipeline.md) Propose commit (the ADR's dependency chain names it — step 5; [ADR-0025 §Rule 1](../../../decisions/0025-adr-governance-amendments.md)). Implementation follows the ADR Accept. - **2026-05-31 — pre-Accept review-round (relayed, verified against source):** **(HIGH)** added the **userland-`unsafe`-audit AC** — the `tyrne-user` `svc #0` shims are `unsafe`; [unsafe-policy §Scope](../../../standards/unsafe-policy.md) applies "equally in … userspace code", so each needs a `// SAFETY:` comment + a `docs/audits/unsafe-log.md` entry (previously unstated). **(Med)** pinned the `tyrne-user` path to `userland/tyrne-user/` (was "or workspace root", contradicting the ADR). **(Med)** added the **`HELLO_CONSOLE_CAP` handle-contract AC** (the T-027↔T-028 interface — closes a silent handle-mismatch risk). **(Low)** added the explicit userland clippy gate (`-p tyrne-userland-hello -p tyrne-user` — neither `kernel-clippy` nor `host-clippy` covers the new crates). All against the still-`Proposed` ADR-0039, pre-Accept. - **2026-05-31 — adversarial ADR-0039 review (5-lens workflow, 81 agents, per-finding 2-skeptic verification): 38 findings, 37 refuted, 1 confirmed; governance lens fully clean.** **Confirmed (fixed):** CI's `kernel-build` job runs `cargo kernel-build` directly on a clean checkout ([`.github/workflows/ci.yml`](../../../../.github/workflows/ci.yml)), so once `USERSPACE_IMAGE` becomes `include_bytes!` of a git-ignored `.bin`, the BSP `build.rs` missing-`.bin` `panic!` would **break CI** — added the CI-workflow-update AC here + corrected ADR-0039's stale "what CI runs" claim (CI runs `kernel-build`, not the smoke). **Refuted (37):** the adversarial pass confirmed the prior review-round's fixes are present (AS-lifetime SEC-T024-01, the `HELLO_CONSOLE_CAP` contract, the no-`.data` constraint, the greeting-marker gate — all already in T-027/T-028), that the 4-option analysis is fair (Option 2/4 not strawmanned; artifact-dependencies considered + rejected as unstable-on-pinned-nightly), that governance/`write-adr` compliance is clean, and that the rust-objcopy / offset-0 / default-members feasibility holds. +- **2026-06-01 — implemented (→ In Review)** on `t-027-userland-build-pipeline` (off `main` at the merged-B6-gates `a8ce11e`, after the ADR-0039 Accept `df3fcd4`). Built: `userland/tyrne-user` (safe `console_write`/`task_exit` shims + `HELLO_CONSOLE_CAP = 0` + UNSAFE-2026-0033) + `userland/hello` (`_start` at `.text._start` → greet → `task_exit`; `hello.ld` fixing the entry at offset 0 / `0x0080_0000`, ASSERT-no-`.data`/`.bss`) + `tools/build-userland.sh` (cargo → `rust-objcopy` from `llvm-tools-preview`, no Cargo dep → git-ignored `userland/hello/hello.bin`). Wired: root `Cargo.toml` members (excluded from `default-members`); BSP `USERSPACE_IMAGE` → `include_bytes!` + `build.rs` `assert!`-if-absent; `tools/smoke.sh` now builds userland + kernel (one-command); `.github/workflows/ci.yml` installs `llvm-tools-preview` + runs the userland build before `kernel-build`. **Verification:** the 117-byte `.bin` disassembles at offset 0 to the `_start` prologue → `x0 = 0` (`HELLO_CONSOLE_CAP`) → `adr x1` greeting → `w2 = 0x15` (21 = len) → `bl console_write` → `x0 = 0` → `bl task_exit`. Gates: `cargo fmt --all --check`; host + kernel clippy `-D warnings`; **userland clippy** (`-p tyrne-userland-hello -p tyrne-user`); host tests **46 hal / 258 kernel / 58 test-hal / 3 doc** (unchanged — userland is bare-metal, outside the host set); `cargo kernel-build`; **`tools/smoke.sh --int` PASS** — the real 117-byte image loads (`entry = 0x800000`, `image bytes 117`, `stack bytes 4096`), boot reaches `all tasks complete`, only the 2 expected EL1-stub SVCs, **zero new fault class**. Miri unaffected (no kernel/host logic changed — only the embedded `USERSPACE_IMAGE` bytes + build infra). The `hello` image is embedded + loads **dormant** — running it in EL0 is [T-028](T-028-el0-userspace-wireup.md). diff --git a/docs/audits/unsafe-log.md b/docs/audits/unsafe-log.md index 8287b52..bc59e5f 100644 --- a/docs/audits/unsafe-log.md +++ b/docs/audits/unsafe-log.md @@ -739,3 +739,22 @@ Neither change touches the `copy_nonoverlapping` site itself; both correct contr - **Trap EL0 FP instead of scrubbing SIMD.** Rejected for v1: `CPACR_EL1.FPEN` is global (`boot.s` enables FP at EL0+EL1); trapping EL0 FP would need a per-system CPACR change + an unhandled-trap path. Scrubbing `v0`–`v31` is self-contained and conservative. - **Reviewed by:** @cemililik (+ Claude Opus 4.8 agent). Security-sensitive (the EL1→EL0 trust-boundary first-entry primitive — a wrong `SPSR_EL1` is a privilege/isolation defect; an un-scrubbed file is a disclosure) → second-reviewer required per [unsafe-policy §Review.4](../standards/unsafe-policy.md). The 2026-05-31 review-round added the register-scrub invariant (HIGH) and corrected the SPSR write to the register form. - **Status:** Active. **Dormant in v1** — no caller yet constructs a userspace task (`Scheduler::add_user_task` is unused until the B6 `task_create_from_image` + `userland/hello` wire-up); T-023 verifies the asm assembles + the kernel boots unchanged, plus the BSP/scheduler plumbing via the `init_user_context` host tests (the *scheduler route*; the real `QemuVirtCpu` slot write + the scrub are verified by code/asm audit + `kernel-build`, the BSP being bare-metal and outside the host-test set). The runtime `ERET`-into-EL0 proof (the real `+0x400` round-trip) is the B6 wire-up task — the same staging T-021's `+0x400` handler used. + +### UNSAFE-2026-0033 — `tyrne-user` EL0 syscall `svc #0` shims + +- **Introduced:** 2026-06-01, [T-027 — userland build pipeline](../analysis/tasks/phase-b/T-027-userland-build-pipeline.md) / [ADR-0039](../decisions/0039-userland-build-pipeline.md). The **userspace-side** inline-asm wrappers a userspace program calls to invoke a syscall — the first `unsafe` in userspace code (per [unsafe-policy §Scope](../standards/unsafe-policy.md), "the rules apply equally in kernel, HAL, and userspace code"). +- **Location:** [`userland/tyrne-user/src/lib.rs`](../../userland/tyrne-user/src/lib.rs) — two `unsafe { core::arch::asm!("svc #0", …) }` blocks: `console_write` (returns) and `task_exit` (`options(noreturn)`). Linked into the `tyrne-userland-hello` raw-flat image; runs at **EL0**, not in the kernel. +- **Operation:** a single `svc #0` per wrapper, with the [ADR-0030](../decisions/0030-syscall-abi.md) register frame loaded around it: + - `console_write(cap, buf)`: `x8 = 5` (ConsoleWrite), `inout x0 = cap → status`, `inout x1 = buf.as_ptr() → bytes-written`, `in x2 = buf.len()`, `x3..x7` clobbered. Returns `Ok(written)` iff `x0 == 0`, else `Err(status)`. + - `task_exit(code)`: `x8 = 4` (TaskExit), `in x0 = code`, `options(noreturn)`. +- **Invariants relied on:** + - **The `svc` traps to EL1; the kernel side is the trusted, panic-free dispatcher.** From EL0 this `svc` is the *only* way to enter the kernel; the EL1 handler ([`syscall_entry`](../../bsp-qemu-virt/src/syscall.rs) → `dispatch`) is panic-free and resolves the call against the **running task's own** capability table + address space (gate #3, [T-026](../analysis/tasks/phase-b/T-026-current-task-cap-table.md)). The shim cannot escalate privilege: it can only name capabilities the task already holds (`cap` is a packed handle into the task's own table) and copy from buffers gate #1 ([T-025](../analysis/tasks/phase-b/T-025-user-access-translation.md)) translates through the task's own `TTBR0`. + - **No memory aliasing / no out-of-bounds.** `console_write` passes `buf.as_ptr()` + `buf.len()`; `buf` is a shared borrow read by the kernel (at most `len` bytes, USER-checked). The shim writes no memory. `task_exit` accesses no memory. + - **ABI register discipline.** The `inout`/`in`/`lateout` operands match ADR-0030 exactly; `x3..x7` are declared clobbered (the kernel may overwrite them as payload/scratch). `task_exit`'s `options(noreturn)` is sound because the kernel terminates the task and never returns to EL0 — the `-> !` signature holds. + - **Syscall numbers restated, not imported.** `4`/`5` are const in this crate (no kernel dependency, per ADR-0039); the kernel authority is `SyscallNumber::as_u64`. Drift is caught by the kernel's ABI host tests + this entry's cross-reference. +- **Rejected alternatives:** + - **Express the `svc` in safe Rust.** Impossible — a supervisor call has no safe-Rust equivalent; inline asm is the minimal architected surface, and the wrapper *is* the safe boundary the rest of userspace calls. + - **Import the kernel's `SyscallNumber`/ABI types.** Rejected per [ADR-0039](../decisions/0039-userland-build-pipeline.md): userspace must not depend on kernel internals; the contract is the stable ABI, restated userspace-side. + - **`options(nostack)` / `preserves_flags`.** Not asserted (conservative): the wrappers do not claim the asm leaves the stack/flags untouched, so no over-assertion can become UB if the contract is wrong. +- **Reviewed by:** @cemililik (+ Claude Opus 4.8 agent). The `unsafe` is confined to two tiny shims and is the *userspace* side of an already-audited trap (the EL1 handler is UNSAFE-2026-0029; the boundary is UNSAFE-2026-0014/0030/0032). The security-critical direction (privilege escalation, memory disclosure) is the **kernel→user** boundary, audited there and reviewed for T-028's first real run; this entry covers the user→kernel call shim. +- **Status:** Active. **Dormant in v1** — the `hello` image is embedded (T-027) but not yet run; the shims execute for the first time when [T-028](../analysis/tasks/phase-b/T-028-el0-userspace-wireup.md) wires the EL0 task and the `+0x400` round-trip fires. T-027 verifies the crate compiles + `rust-objcopy`s to a 117-byte raw-flat image whose offset-0 disassembly is the `_start` → `console_write` → `task_exit` sequence (the userland crates are bare-metal, outside the host-test set; verified by `cargo clippy --target aarch64-unknown-none` + disassembly). diff --git a/docs/roadmap/current.md b/docs/roadmap/current.md index ee9d3d5..e811dcb 100644 --- a/docs/roadmap/current.md +++ b/docs/roadmap/current.md @@ -4,6 +4,8 @@ A short pointer file updated as work progresses. For the full plan see [`phases/ --- +> **2026-06-01 update — B6 step 5 landed: ADR-0039 Accepted + T-027 (`userland/hello` + `tyrne-user` + the raw-flat build pipeline) implemented, In Review.** [ADR-0039](../decisions/0039-userland-build-pipeline.md) **Accepted** (the build orchestration ADR-0029 deferred — settled via two review rounds: a relayed 8-fix round + an adversarial 5-lens/81-agent pass, 37/38 refuted). **[T-027](../analysis/tasks/phase-b/T-027-userland-build-pipeline.md)** builds Tyrne's first **real** userspace program from Rust source: a `userland/tyrne-user` crate (safe `console_write`/`task_exit` shims over the `svc #0` ABI — UNSAFE-2026-0033, the first userspace `unsafe`) + a `userland/hello` `#![no_std] #![no_main]` crate (`_start` at offset 0 → greet → `task_exit`; an `hello.ld` linker script fixing the entry at `0x0080_0000`, ASSERT-no-`.data`/`.bss`). The pipeline (per ADR-0039): `tools/build-userland.sh` → `cargo build` → **`rust-objcopy -O binary`** (from the pinned `llvm-tools-preview` — **no Cargo dep**, K3-8 unfired) → a git-ignored `hello.bin`, embedded into the BSP via `include_bytes!` (replacing the hand-coded `mov w0,#42` placeholder; `build.rs` `assert!`s the `.bin` exists). Both crates are `default-members`-excluded workspace members (host commands skip them). `tools/smoke.sh` + CI build userland first (CI gains `llvm-tools-preview` + a build step — the one adversarial-confirmed finding: else the clean-checkout `kernel-build` job would hit the missing-`.bin` panic). **The 117-byte image disassembles at offset 0 to `_start` → `x0=0` (`HELLO_CONSOLE_CAP`) → greeting → `bl console_write` → `bl task_exit`**, and **loads dormant** under the smoke (`entry=0x800000`, `image bytes 117`, `stack bytes 4096`) — **running it in EL0 is [T-028](../analysis/tasks/phase-b/T-028-el0-userspace-wireup.md)** (the `+0x400` round-trip + the explicit EL0-boundary security review). Gates: **host tests 258 kernel / 46 hal / 58 test-hal / 3 doc** (unchanged — userland is bare-metal), fmt, host + kernel + **userland** clippy `-D warnings`, kernel build, `tools/smoke.sh --int` PASS (image loads, `all tasks complete`, 2 stub SVCs, **zero new fault class**), Miri unaffected. **Next:** [T-028](../analysis/tasks/phase-b/T-028-el0-userspace-wireup.md) — wire the EL0 task (load → `task_create_from_image` → seed a `DebugConsole` cap at `HELLO_CONSOLE_CAP` → `add_user_task` → run) + the `+0x400` smoke + the security review; then B6 closure (Phase B retrospective trio). This banner supersedes the gate-#3 banner below. +> > **2026-05-31 update — B6 gate #3 closed: T-026 (current-task capability table + per-task window in `syscall_entry`) implemented, In Review. All three T-021 carry-forward gates now closed.** **[T-026](../analysis/tasks/phase-b/T-026-current-task-cap-table.md)** (on `t-026-current-task-cap-table` off the merged T-025 `f21eece`; **no new ADR** — rides ADR-0030/0021/0014) makes `syscall_entry` source the **running EL0 task's** capability table + address space + user-access window from `SCHED.current` (scheduler `task_cap_tables` / `task_user_windows` parallel arrays + `current_*` accessors, written by `add_user_task`), **failing closed** when no task is current — the empty `FAILCLOSED_TABLE` (every lookup → `InvalidHandle`) + an empty window. **Control-plane** (`task_yield`/`task_exit`, which consult no capability) is gated in the dispatcher on a new `SyscallContext.has_current_task` (→ `InvalidHandle` when no current task — the H2 review fix; host-testable, since the BSP can't construct the `#[non_exhaustive]` `SyscallError`). `AddressSpace::inner()` widened to `pub` (read-only — no cap-gate bypass) so the BSP can pass the task's `&QemuVirtAddressSpace` to gate-#1 `Mmu::translate`. The `+0x200` smoke is **re-sequenced after `SCHED` init** (so `SCHED.current` is `None`) and now demonstrates gate-#3 **fail-closed** (`console_write` → `InvalidHandle` `0x102`; `bad-number` → `BadSyscallNumber`); `SYSCALL_STUB_TABLE` retired. Two relayed pre-impl reviews hardened the design (H1 window persistence, H2 control-plane, M4 mandatory audit). Mandatory **UNSAFE-2026-0014 Amendment** (the cap-table-pointer deref) + a UNSAFE-2026-0029 Amendment (syscall-arc statics + smoke re-sequence). Gates: **host tests 257 kernel / 46 hal / 58 test-hal / 3 doc**, fmt, host + kernel clippy `-D warnings`, kernel build, QEMU smoke (2 SVC, clean `ERET`, **zero new fault class**), **Miri 0 UB**. **All three [T-021 carry-forward gates](phases/phase-b.md#t-021-carry-forward-gates-must-close-before-a-real-el0-task-runs) are closed** (#1 mechanism T-025, #2 T-023, #3 T-026). **Next:** the `tyrne-user` + `userland/hello` crate + `cargo → objcopy → include_bytes!` build pipeline, then the EL0 `+0x400` wire-up smoke, then B6 closure. **Carry-forward DoD:** the explicit EL0-boundary security review (UNSAFE-2026-0030/0032 + cap-table sourcing) before a real EL0 task runs. This banner supersedes the gate-#1 banner below. > > **2026-05-31 update — B6 gate #1 mechanism landed: ADR-0038 Accepted + T-025 (`Mmu::translate` + per-page user-access translation) implemented, In Review.** [ADR-0038](../decisions/0038-mmu-translate-and-user-access.md) **Accepted** (the read-only `Mmu::translate` walk query — the realised [ADR-0009](../decisions/0009-mmu-trait.md) §Open-questions "translation walk query" — + the per-task user-access policy; arc on `t-025-user-access-translation`: propose → review-round (8 valid / 3 skipped across two relayed reviews) → accept). **[T-025](../analysis/tasks/phase-b/T-025-user-access-translation.md)** implements gate #1's **mechanism** (the security-critical [T-021 carry-forward gate #1](phases/phase-b.md#t-021-carry-forward-gates-must-close-before-a-real-el0-task-runs)): `copy_from_user` / `copy_to_user` become **two-pass** (probe-all-then-copy), generic over ``, resolving **every** user page through the task's own address space and **requiring `USER`** (`FaultAddress` on any miss / non-`USER` / block-mapped page — the confused-deputy defence, never a panic), behind a per-task `[entry_va, stack_top_va)` window first-gate; `SyscallContext` gains `mmu` / `task_as`. `vmsav8` gains the inverse decoder `descriptor_bits_to_flags` (lock-shut). The B5 `+0x200` stub `console_write` of a **kernel** VA is now correctly **rejected** (smoke: `status=0x3 bytes=0`, no greeting emitted) — a positive gate-#1 demonstration and the only smoke-trace change. Gates: **host tests 252 kernel / 46 hal / 58 test-hal / 3 doc**, `cargo fmt`, host + kernel clippy `-D warnings`, kernel build, QEMU smoke (exactly 2 SVC exceptions, clean `ERET`, **zero new fault class**), **Miri 0 UB**. UNSAFE-2026-0030 + 0025 Amendments (per-page translation + read-only `translate` caller — no new entries). **Next:** gate #3 ([T-026](../analysis/tasks/phase-b/T-026-current-task-cap-table.md)) sources the running EL0 task's AS + capability table from the scheduler, then the `tyrne-user` + `userland/hello` build pipeline + the EL0 `+0x400` wire-up smoke. This banner supersedes the B6-opening banner below. diff --git a/docs/roadmap/phases/phase-b.md b/docs/roadmap/phases/phase-b.md index c35d8ec..1784fdc 100644 --- a/docs/roadmap/phases/phase-b.md +++ b/docs/roadmap/phases/phase-b.md @@ -251,7 +251,7 @@ A real userspace task, loaded by B4, running in EL0 in its own address space, ma 2. **EL0 task context register file + the enter-EL0 path + per-task `SP_EL1`** (closes [T-021 carry-forward gate #2](#milestone-b6--first-userspace-hello)). 3. **`task_create_from_image`** — `LoadedImage` → runnable `CapHandle{CapObject::Task(...)}` (composes steps 1 + 2; the deferred [§B4 §3](#milestone-b4--task-loader) bridge). 4. **Close the remaining T-021 carry-forward gates:** the per-task `console_write` window + per-page user-VA → kernel-VA translation returning `FaultAddress` (**gate #1 — security-critical**; without it an EL0 debug-console-cap holder reads arbitrary kernel memory), and `SYSCALL_STUB_TABLE` → the scheduler's current-task table (**gate #3**). -5. **`tyrne-user` crate** (safe wrappers) + **`userland/hello/` crate** + the `cargo build → objcopy -O binary → include_bytes!` pipeline + the shared `userland-layout` source-of-truth ([ADR-0029 §"Build pipeline (B6)"](../../decisions/0029-initial-userspace-image-format.md)). *(§Sub-breakdown items 1 + 3.)* +5. ✅ **`tyrne-user` crate** (safe wrappers) + **`userland/hello/` crate** + the `cargo build → rust-objcopy -O binary → include_bytes!` pipeline — **landed (2026-06-01, [T-027](../../analysis/tasks/phase-b/T-027-userland-build-pipeline.md) / [ADR-0039](../../decisions/0039-userland-build-pipeline.md), In Review).** `tools/build-userland.sh` (rust-objcopy from the pinned `llvm-tools-preview`, no Cargo dep) produces the git-ignored `hello.bin`; the BSP embeds it via `include_bytes!` (`build.rs` asserts it exists); `smoke.sh` + CI build userland first; UNSAFE-2026-0033 for the userspace `svc` shims. The real 117-byte image **loads dormant** (entry `0x800000`); **running** it is step 6. *(§Sub-breakdown items 1 + 3.)* 6. **Wire-up + QEMU smoke** *(§Sub-breakdown items 2 + 4)*: a true EL0 task takes the lower-EL `+0x400` vector, the dispatcher copies `console_write` from the task's `TTBR0_EL1`, `ERET` returns to EL0, and `task_exit` terminates it — the EL0↔EL1 round-trip B5's `+0x200` proxy could not prove. 7. **Closure = the Phase B retrospective** *(§Sub-breakdown items 5–7)*: the guide, the first hypothesis-driven performance cycle (real EL0 round-trip / IPC / context-switch vs the A6 baseline — and a same-host control, given the [B5 perf leg](../../analysis/reviews/performance-optimization-reviews/2026-05-29-B5-closure.md)'s finding that the harness is nearing its resolving floor), and a security review of the now-attacker-observable boundary. diff --git a/tools/build-userland.sh b/tools/build-userland.sh new file mode 100755 index 0000000..dbf9ffd --- /dev/null +++ b/tools/build-userland.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Build the Tyrne userland program(s) and strip to raw flat binaries (ADR-0039). +# +# Produces userland/hello/hello.bin (git-ignored via the repo's `*.bin` rule) +# by compiling the `tyrne-userland-hello` crate for aarch64-unknown-none and +# stripping it with `rust-objcopy -O binary` from the pinned `llvm-tools-preview` +# component (NO Cargo dependency — keeps the K3-8 cargo-vet flag unfired). +# +# Must run BEFORE `cargo kernel-build`: the BSP embeds the .bin via +# `include_bytes!`, and its build.rs panics if the .bin is absent. `tools/smoke.sh` +# and the CI kernel-build job both run this first. +# +# Usage: +# tools/build-userland.sh — debug profile (matches a debug kernel) +# tools/build-userland.sh --release — release profile (matches --release kernel) +set -euo pipefail + +PROFILE="debug" +case "${1:-}" in + --release) PROFILE="release"; shift ;; + "") ;; + -h|--help) sed -n '2,/^set -/p' "$0" | sed 's/^# \{0,1\}//;/^set -/d' >&2; exit 0 ;; + *) echo "error: unknown argument: $1 (usage: $0 [--release])" >&2; exit 2 ;; +esac + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +echo "build-userland: cargo build -p tyrne-userland-hello --target aarch64-unknown-none (${PROFILE})" >&2 +if [[ "$PROFILE" == "release" ]]; then + cargo build -p tyrne-userland-hello --target aarch64-unknown-none --release +else + cargo build -p tyrne-userland-hello --target aarch64-unknown-none +fi + +# Resolve rust-objcopy from the active toolchain's llvm-tools (no Cargo dep). +SYSROOT="$(rustc --print sysroot)" +OBJCOPY="$(find "$SYSROOT" -type f -name 'rust-objcopy' 2>/dev/null | head -n1)" +if [[ -z "$OBJCOPY" ]]; then + echo "error: rust-objcopy not found under $SYSROOT" >&2 + echo " install the llvm-tools-preview component (pinned in rust-toolchain.toml):" >&2 + echo " rustup component add llvm-tools-preview" >&2 + exit 1 +fi + +ELF="target/aarch64-unknown-none/${PROFILE}/hello" +BIN="userland/hello/hello.bin" +if [[ ! -f "$ELF" ]]; then + echo "error: expected userland ELF not found at $ELF" >&2 + exit 1 +fi + +"$OBJCOPY" -O binary "$ELF" "$BIN" +echo "build-userland: wrote $BIN ($(wc -c < "$BIN" | tr -d ' ') bytes)" >&2 diff --git a/tools/smoke.sh b/tools/smoke.sh index cdf9c5a..b906152 100755 --- a/tools/smoke.sh +++ b/tools/smoke.sh @@ -43,6 +43,19 @@ if ! [[ "$TO" =~ ^[1-9][0-9]*$ ]]; then exit 2 fi +# When no explicit ELF is given, build the userland image first (ADR-0039: its +# raw-flat .bin must exist before the BSP embeds it via include_bytes! — the BSP +# build.rs panics otherwise) and then the kernel, making this the one-command +# integration entry point. An explicit ELF argument is used as-is (no build). +if [[ -z "$KERNEL" ]]; then + REL_FLAG="" + [[ "$PROFILE" == "release" ]] && REL_FLAG="--release" + # shellcheck disable=SC2086 # word-split the optional --release flag + "$(dirname "$0")/build-userland.sh" $REL_FLAG + # shellcheck disable=SC2086 + cargo kernel-build $REL_FLAG +fi + [[ -z "$KERNEL" ]] && KERNEL="target/aarch64-unknown-none/${PROFILE}/tyrne-bsp-qemu-virt" if [[ ! -f "$KERNEL" ]]; then echo "error: kernel image not found at $KERNEL (run 'cargo kernel-build' first)" >&2 diff --git a/userland/hello/Cargo.toml b/userland/hello/Cargo.toml new file mode 100644 index 0000000..11736a4 --- /dev/null +++ b/userland/hello/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "tyrne-userland-hello" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +description = "Tyrne first userspace program — greets via console_write, exits via task_exit (B6)." + +[[bin]] +name = "hello" +path = "src/main.rs" + +[dependencies] +tyrne-user = { path = "../tyrne-user" } + +[lints] +workspace = true diff --git a/userland/hello/build.rs b/userland/hello/build.rs new file mode 100644 index 0000000..53394e8 --- /dev/null +++ b/userland/hello/build.rs @@ -0,0 +1,14 @@ +//! Build script for `tyrne-userland-hello`. +//! +//! Passes the userland linker script (`hello.ld`) to the linker with an +//! absolute path, mirroring `bsp-qemu-virt/build.rs`. The script fixes the +//! image at the userspace base VA with the entry at offset 0 (ADR-0029/0039). + +fn main() { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") + .expect("CARGO_MANIFEST_DIR must be set by Cargo when running build scripts"); + + println!("cargo:rustc-link-arg=-T{manifest_dir}/hello.ld"); + println!("cargo:rerun-if-changed=hello.ld"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/userland/hello/hello.ld b/userland/hello/hello.ld new file mode 100644 index 0000000..874d3f6 --- /dev/null +++ b/userland/hello/hello.ld @@ -0,0 +1,54 @@ +/* + * Userspace linker script for the v1 "hello" raw-flat image (ADR-0029 / ADR-0039). + * + * The loader (kernel/src/obj/task_loader.rs) maps the objcopy'd bytes at a + * fixed userspace VA with the entry instruction at OFFSET 0. This script: + * - fixes the image at USERSPACE_IMAGE_BASE_VA (0x0080_0000), + * - places `_start` (in `.text._start`) FIRST in `.text` so offset 0 is the + * entry, then the rest of `.text`, then `.rodata` (the greeting), + * - forbids `.data` / `.bss`: the single image region maps USER|EXECUTE (no + * WRITE), so a writable byte would fault on first write — the ASSERTs below + * fail the build LOUDLY rather than silently embed unwritable globals. + * + * USERSPACE_IMAGE_BASE_VA must be kept in sync with the Rust-side constant + * `USERSPACE_IMAGE_BASE_VA` in bsp-qemu-virt/src/main.rs (a linker script + * cannot import a Rust const — ADR-0039 §Decision outcome; the `--defsym` + * upgrade is named there for full drift-elimination if it earns its keep). + */ + +USERSPACE_IMAGE_BASE_VA = 0x00800000; + +ENTRY(_start) + +SECTIONS { + . = USERSPACE_IMAGE_BASE_VA; + + .text : { + KEEP(*(.text._start)) /* _start first => offset 0 is the entry */ + *(.text .text.*) + } + + .rodata : ALIGN(8) { + *(.rodata .rodata.*) + } + + /* v1 forbids writable data (image is USER|EXECUTE, no WRITE). Collect any + * stray .data/.bss so the ASSERTs can prove they are empty. */ + .data : { *(.data .data.*) } + .bss : { *(.bss .bss.* COMMON) } + + ASSERT(SIZEOF(.data) == 0, + "userland hello: .data must be empty (image maps USER|EXECUTE, no WRITE)") + ASSERT(SIZEOF(.bss) == 0, + "userland hello: .bss must be empty (image maps USER|EXECUTE, no WRITE)") + + /* Drop metadata/unwind/debug sections — not part of the raw-flat image. */ + /DISCARD/ : { + *(.comment) + *(.note .note.*) + *(.eh_frame .eh_frame_hdr) + *(.ARM.exidx .ARM.exidx.*) + *(.ARM.attributes) + *(.debug .debug.*) + } +} diff --git a/userland/hello/src/main.rs b/userland/hello/src/main.rs new file mode 100644 index 0000000..97221ce --- /dev/null +++ b/userland/hello/src/main.rs @@ -0,0 +1,51 @@ +//! # tyrne-userland-hello +//! +//! Tyrne's first real userspace program (B6). It runs in EL0 in its own address +//! space, greets through a `console_write` syscall, and exits via `task_exit` — +//! the EL0↔EL1 round-trip the B5 EL1-stub proxy could not prove. +//! +//! This is a `#![no_std] #![no_main]` raw-flat image per [ADR-0029][adr-0029]: +//! the loader maps the objcopy'd bytes at a fixed userspace VA with the entry +//! instruction at **offset 0**, so [`_start`] is placed first via the +//! `.text._start` section + the `hello.ld` linker script. There is no `.data` +//! or `.bss` — the image region maps `USER | EXECUTE` (no `WRITE`), so the +//! greeting is a read-only string literal (`.rodata`, in-image) and there are +//! no writable globals. The build pipeline (cargo → `rust-objcopy -O binary` → +//! `include_bytes!`) is [ADR-0039][adr-0039] / T-027. +//! +//! [adr-0029]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0029-initial-userspace-image-format.md +//! [adr-0039]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0039-userland-build-pipeline.md + +#![no_std] +#![no_main] + +use tyrne_user::{console_write, task_exit, HELLO_CONSOLE_CAP}; + +/// The greeting emitted via `console_write`. A read-only `.rodata` literal that +/// lives inside the mapped image region (`USER | EXECUTE`) — gate #1 admits a +/// read of a `USER` page, so the buffer pointer translates and the bytes copy. +static GREETING: &[u8] = b"hello from userspace\n"; + +/// Userspace entry point. Placed at **offset 0** of the raw-flat image (the +/// loader sets `ELR_EL1` to the image base, so the first instruction executed +/// is this function's first instruction). Greets, then exits — never returns. +/// +/// `#[no_mangle]` gives the linker the bare `_start` symbol (the `hello.ld` +/// `ENTRY`); `#[link_section = ".text._start"]` + the script's `KEEP` place it +/// first in `.text` so offset 0 is the entry. +#[no_mangle] +#[link_section = ".text._start"] +pub extern "C" fn _start() -> ! { + // Ignore the result: the v1 demo has no fallback path if the console write + // is rejected — it exits cleanly either way (the kernel reports the exit). + let _ = console_write(HELLO_CONSOLE_CAP, GREETING); + task_exit(0) +} + +/// Panic handler (required for `#![no_std]`). Unwinding is disabled +/// (`panic=abort` for the bare-metal target), so this just exits the task with +/// a distinct non-zero code rather than attempting to unwind. +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + task_exit(101) +} diff --git a/userland/tyrne-user/Cargo.toml b/userland/tyrne-user/Cargo.toml new file mode 100644 index 0000000..9d69221 --- /dev/null +++ b/userland/tyrne-user/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tyrne-user" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +description = "Tyrne userspace syscall wrappers — safe shims over the EL0 SVC ABI (ADR-0030/0031)." + +[lib] +name = "tyrne_user" + +[lints] +workspace = true diff --git a/userland/tyrne-user/src/lib.rs b/userland/tyrne-user/src/lib.rs new file mode 100644 index 0000000..b32cb24 --- /dev/null +++ b/userland/tyrne-user/src/lib.rs @@ -0,0 +1,130 @@ +//! # tyrne-user +//! +//! Safe userspace wrappers over the Tyrne EL0 syscall ABI. This crate is what a +//! userspace program (e.g. [`tyrne-userland-hello`]) links against to invoke +//! syscalls without writing its own `unsafe` inline assembly. +//! +//! Each wrapper packs the [ADR-0030][adr-0030] register convention (`x8` = +//! number, `x0`–`x5` = arguments; `x0` = status, `x1`–`x7` = payload) around a +//! single `svc #0`, the [ADR-0031][adr-0031] trap instruction. The `unsafe` +//! surface is confined to the `svc` shims here; callers stay in safe Rust. +//! +//! The crate is `#![no_std]` and carries **no dependency on the kernel** — the +//! syscall numbers are restated here per [ADR-0031][adr-0031] (the contract is +//! the ABI, not a shared type). The host-side authority is +//! `tyrne_kernel::syscall::abi::SyscallNumber::as_u64`; if these drift, the +//! kernel's ABI host tests and this crate's numbers must be reconciled. +//! +//! [adr-0030]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0030-syscall-abi.md +//! [adr-0031]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0031-initial-syscall-set.md + +#![no_std] + +use core::arch::asm; + +/// `task_exit` syscall number (`x8`), per [ADR-0031][adr-0031]. Restated +/// userspace-side; the kernel authority is `SyscallNumber::TaskExit as 4`. +/// +/// [adr-0031]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0031-initial-syscall-set.md +const SYS_TASK_EXIT: u64 = 4; + +/// `console_write` syscall number (`x8`), per [ADR-0031][adr-0031]. **Debug-gated** +/// in the kernel (number `5` decodes to a syscall only in a debug build; a +/// release kernel returns `BadSyscallNumber` and emits nothing). Restated +/// userspace-side; the kernel authority is `SyscallNumber::ConsoleWrite as 5`. +/// +/// [adr-0031]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0031-initial-syscall-set.md +const SYS_CONSOLE_WRITE: u64 = 5; + +/// The capability word the first userspace task ([`tyrne-userland-hello`]) names +/// for its debug console — the **T-027 ↔ T-028 interface** (per +/// [ADR-0039][adr-0039]). It is the packed handle of the **root** capability of +/// a freshly created table: index `0`, generation `0`, so the ABI packing +/// `(generation << 16) | index` yields `0`. The EL0 wire-up (T-028) **must** +/// seed the task's `DebugConsole` capability so it resolves to this handle +/// (`insert_root` into a fresh table yields index 0 / generation 0). Defined +/// once here; not duplicated by value across the program and the seeding site. +/// +/// [adr-0039]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0039-userland-build-pipeline.md +pub const HELLO_CONSOLE_CAP: u64 = 0; + +/// A syscall rejection: the non-zero kernel status word returned in `x0`. `0` +/// means success and is never wrapped in this type (the wrappers return `Ok`). +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct SyscallError( + /// The raw kernel status word (`x0`); the low/high blocks encode the + /// kernel's `SyscallError` taxonomy ([ADR-0030][adr-0030]). + /// + /// [adr-0030]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0030-syscall-abi.md + pub u64, +); + +/// Write `buf` to the debug console named by the capability word `cap`. +/// +/// Returns the number of bytes the kernel accepted on success, or the raw +/// kernel status word on rejection (an absent/wrong capability, an +/// out-of-window or untranslatable buffer, etc. — the kernel dispatcher is +/// panic-free and fails closed). `cap` is a packed handle into the **caller's +/// own** capability table (e.g. [`HELLO_CONSOLE_CAP`]); it grants nothing the +/// task does not already hold. +/// +/// # Errors +/// +/// Returns [`SyscallError`] wrapping the non-zero `x0` status when the kernel +/// rejects the call. +#[allow( + clippy::cast_possible_truncation, + reason = "the returned byte count is <= buf.len() <= usize::MAX; the u64 -> usize \ + cast is lossless on every supported (64-bit) target" +)] +pub fn console_write(cap: u64, buf: &[u8]) -> Result { + let status: u64; + let written: u64; + // SAFETY: `svc #0` is the EL0→EL1 syscall trap. We load the ABI registers + // exactly per ADR-0030 — x8 = the console_write number, x0 = the capability + // word, x1 = the buffer base pointer, x2 = the length — and read back + // x0 = status and x1 = the accepted byte count. The kernel dispatcher is + // panic-free and, post-gate-#1 (T-025), translates [x1, x1+x2) through the + // caller's own address space requiring USER, copying at most `buf.len()` + // bytes; it never writes through these pointers and never touches userspace + // memory outside `buf`. `buf` is only read (a shared borrow). x3..x7 are + // marked clobbered (caller-saved scratch/payload). No flags or stack state + // are relied upon across the trap. Audit: UNSAFE-2026-0033. + unsafe { + asm!( + "svc #0", + in("x8") SYS_CONSOLE_WRITE, + inout("x0") cap => status, + inout("x1") buf.as_ptr() as u64 => written, + in("x2") buf.len() as u64, + lateout("x3") _, + lateout("x4") _, + lateout("x5") _, + lateout("x6") _, + lateout("x7") _, + ); + } + if status == 0 { + Ok(written as usize) + } else { + Err(SyscallError(status)) + } +} + +/// Terminate the current task with exit `code`. Does not return — the kernel +/// removes the task from the scheduler and never re-enters it. +pub fn task_exit(code: u64) -> ! { + // SAFETY: `svc #0` with x8 = the task_exit number and x0 = the exit code. + // task_exit acts on the caller's own task identity (no capability), and the + // kernel terminates the task without returning to EL0, so the + // `options(noreturn)` contract holds. Only x0 is passed; no memory is + // accessed. Audit: UNSAFE-2026-0033. + unsafe { + asm!( + "svc #0", + in("x8") SYS_TASK_EXIT, + in("x0") code, + options(noreturn), + ); + } +} From ce257276ae46e9a1b7c1b34523109be1a651472f Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Mon, 1 Jun 2026 00:47:36 +0300 Subject: [PATCH 6/7] =?UTF-8?q?fix(userland):=20T-027=20PR=20#41=20review-?= =?UTF-8?q?round=20=E2=80=94=20cfg-gate=20for=20--workspace/Miri=20+=20lin?= =?UTF-8?q?ker/script=20hardening?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relayed PR #41 bot review; verified each finding against source. HIGH (fixed) — the userland crates broke --workspace host builds: cargo check --workspace --exclude tyrne-bsp-qemu-virt (the Miri-equivalent) failed on tyrne-user's aarch64 inline asm (a regression my original 'Miri unaffected' claim missed). Cfg-gated the svc asm + no_std/no_main to target_arch=aarch64 with degenerate host stubs (never run), keeping ADR-0039's root-members structure. Verified: cargo check --workspace --exclude tyrne-bsp-qemu-virt clean and cargo +nightly miri test --workspace --exclude tyrne-bsp-qemu-virt now compiles the userland crates + passes (kernel 258 / hal 46 / test-hal 58, 0 UB). LOW (fixed): hello.ld also ASSERTs .got empty (non-PIC, catches stray PIC artifacts); tools/build-userland.sh resolves rust-objcopy deterministically via /Users/dev/.rustup/toolchains/nightly-2026-01-15-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib (was a brittle/slow sysroot find) + respects CARGO_TARGET_DIR; the T-027 'denies enforce' typo fixed; UNSAFE-2026-0033 notes the asm is aarch64-gated (host stubs carry no unsafe). Skipped (with reason): the 'future-dated 2026-06-01' findings — today IS 2026-06-01 (date confirmed), the entries are accurate; build.rs format!-> Path::join — the / paths work on Tyrne's Unix-only dev/CI (macOS + ubuntu), not a bug; a console message in the panic_handler — over-engineering for the trivial v1 hello; SyscallError: Display — unused in v1 (YAGNI). Gates re-run green: fmt; host + kernel + userland clippy -D warnings; host tests 46/258/58/3; kernel build; tools/smoke.sh --int PASS (image loads, all tasks complete, 2 stub SVCs, zero new fault); Miri --workspace --exclude 0 UB. Refs: T-027, ADR-0039 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../phase-b/T-027-userland-build-pipeline.md | 5 ++-- docs/audits/unsafe-log.md | 2 +- tools/build-userland.sh | 18 +++++++++---- userland/hello/hello.ld | 9 +++++-- userland/hello/src/main.rs | 17 ++++++++++-- userland/tyrne-user/src/lib.rs | 26 +++++++++++++++++++ 6 files changed, 65 insertions(+), 12 deletions(-) diff --git a/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md b/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md index 85b9906..76eb48f 100644 --- a/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md +++ b/docs/analysis/tasks/phase-b/T-027-userland-build-pipeline.md @@ -22,13 +22,13 @@ ADR-0029 chose the raw-flat image format and deferred the build orchestration to ## Acceptance criteria - [x] **`tyrne-user` crate** (`userland/tyrne-user/`, package `tyrne-user`, `#![no_std]`): safe wrappers over the [ADR-0031](../../../decisions/0031-initial-syscall-set.md) syscalls needed by `hello` — at minimum `console_write(cap: CapWord, buf: &[u8]) -> Result` and `task_exit(code) -> !` — each wrapping a thin `svc #0` inline-asm shim (an **`unsafe` block** — see the userland-`unsafe`-audit AC) packing the ABI ([ADR-0030](../../../decisions/0030-syscall-abi.md): `x8` = number, `x0..x5` = args; `ConsoleWrite = 5`, `TaskExit = 4`). No dependency on kernel internals (the ABI constants are restated/owned userspace-side or shared via a leaf crate). -- [x] **Userland `unsafe` audit** (security policy applies to userspace too): the `tyrne-user` `svc #0` shims are `unsafe` (inline asm); per [unsafe-policy §Scope](../../../standards/unsafe-policy.md) ("the rules apply equally in kernel, HAL, and userspace code") **and** [CLAUDE.md non-negotiable #2](../../../../CLAUDE.md), each `unsafe` block carries a `// SAFETY:` comment (why the asm upholds the ABI contract — args in `x0..x5`/`x8`, the clobber list, no UB) **and** a dated [`docs/audits/unsafe-log.md`](../../../audits/unsafe-log.md) entry (`UNSAFE-2026-NNNN`). The workspace `clippy::undocumented_unsafe_blocks` / `missing_safety_doc` denies enforce the comment; the audit entry is the policy obligation. +- [x] **Userland `unsafe` audit** (security policy applies to userspace too): the `tyrne-user` `svc #0` shims are `unsafe` (inline asm); per [unsafe-policy §Scope](../../../standards/unsafe-policy.md) ("the rules apply equally in kernel, HAL, and userspace code") **and** [CLAUDE.md non-negotiable #2](../../../../CLAUDE.md), each `unsafe` block carries a `// SAFETY:` comment (why the asm upholds the ABI contract — args in `x0..x5`/`x8`, the clobber list, no UB) **and** a dated [`docs/audits/unsafe-log.md`](../../../audits/unsafe-log.md) entry (`UNSAFE-2026-NNNN`). The workspace `clippy::undocumented_unsafe_blocks` and `clippy::missing_safety_doc` lints (set to `deny`) require this comment; the audit entry is the policy obligation. - [x] **`userland/hello` crate** (package `tyrne-userland-hello`, `#![no_std] #![no_main]`): a `_start` entry placed at **offset 0** of the linked image (linker `ENTRY` + a `KEEP`'d first section) that calls `tyrne_user::console_write` with a greeting string (living in the image's read-only data — reachable as a `USER` page, [T-025](T-025-user-access-translation.md) gate #1 requires only the `USER` flag for a read) then `tyrne_user::task_exit(0)`; a minimal `#[panic_handler]` that `task_exit`s or loops (no unwinder — `panic=abort` inherited). - [x] **Console-cap handle contract (T-027 ↔ T-028 interface):** `hello` names its `DebugConsole` capability via a **single documented named constant** (e.g. `pub const HELLO_CONSOLE_CAP: u64` in `tyrne-user`, the `cap_word` passed in `x0` — `encode_cap_handle` of the handle the wire-up seeds, typically the root cap → index 0 / generation 0). [T-028](T-028-el0-userspace-wireup.md)'s cap-table seeding **must** insert the `DebugConsole` cap so it resolves to that exact handle. The constant is the shared interface — defined once, not duplicated by value across the two tasks. - [x] **Userland linker script** places `.text` (entry first) + `.rodata` contiguously at `USERSPACE_IMAGE_BASE_VA`, **no `.data`/`.bss`** (writable globals would fault — image is `USER|EXECUTE`, no `WRITE`), 16-byte aligned; produces a contiguous byte stream with no ELF artifacts under `rust-objcopy -O binary`. - [x] **Base-VA source-of-truth:** a single Rust-side `pub const USERSPACE_IMAGE_BASE_VA = 0x0080_0000` (read by the BSP loader call site); the userland linker script restates the literal with a documented `keep-in-sync-with` comment (LD cannot import a Rust const — per [ADR-0039](../../../decisions/0039-userland-build-pipeline.md)). - [x] **Build orchestration** ([`tools/build-userland.sh`](../../../../tools/build-userland.sh)): `cargo build -p tyrne-userland-hello --target aarch64-unknown-none` (release + debug as the kernel profile dictates) → `rust-objcopy -O binary` (resolved from the active toolchain's `llvm-tools` — **no Cargo dependency**, K3-8 unfired) → a git-ignored `.bin` at a stable path (`userland/hello/hello.bin`). Clear error if `llvm-tools` / the ELF is missing. (The `.bin` is git-ignored by the repo's existing `*.bin` rule — no `.gitignore` change needed.) -- [x] **Workspace integration:** `userland/hello` + `tyrne-user` added to `members`, **excluded from `default-members`** (host commands skip them — mirrors `bsp-qemu-virt`); `cargo host-test` / `cargo build` / `cargo host-clippy` unaffected (verified). +- [x] **Workspace integration:** `userland/hello` + `tyrne-user` added to `members`, **excluded from `default-members`** (host commands skip them — mirrors `bsp-qemu-virt`); `cargo host-test` / `cargo build` / `cargo host-clippy` unaffected (verified). The crates additionally **cfg-gate their aarch64 asm / `no_main`** (degenerate host stubs, never run) so the explicit `--workspace` commands — notably `cargo miri test --workspace --exclude tyrne-bsp-qemu-virt` — host-compile them rather than choking on aarch64 asm (verified: `cargo check --workspace --exclude tyrne-bsp-qemu-virt` clean). - [x] **BSP embed:** `USERSPACE_IMAGE` becomes `include_bytes!()`; [`bsp-qemu-virt/build.rs`](../../../../bsp-qemu-virt/build.rs) gains `rerun-if-changed` on the `.bin` and a `panic!` naming `tools/build-userland.sh` if it is absent. - [x] **`tools/smoke.sh` runs the userland build first** so the canonical local smoke path stays one entry point. - [x] **CI workflow updated:** [`.github/workflows/ci.yml`](../../../../.github/workflows/ci.yml) runs `tools/build-userland.sh` **before** its `kernel-build` / `kernel-clippy` jobs — otherwise, once `USERSPACE_IMAGE` becomes `include_bytes!` of the git-ignored `.bin`, those jobs hit the BSP `build.rs` missing-`.bin` `panic!` on a clean checkout and **CI breaks**. (The QEMU-smoke-in-CI regression gate itself stays the conditionally-deferred flag K3-7 — no QEMU-smoke CI job exists today.) @@ -56,3 +56,4 @@ All acceptance criteria checked; gates green; the embedded real image replaces t - **2026-05-31 — pre-Accept review-round (relayed, verified against source):** **(HIGH)** added the **userland-`unsafe`-audit AC** — the `tyrne-user` `svc #0` shims are `unsafe`; [unsafe-policy §Scope](../../../standards/unsafe-policy.md) applies "equally in … userspace code", so each needs a `// SAFETY:` comment + a `docs/audits/unsafe-log.md` entry (previously unstated). **(Med)** pinned the `tyrne-user` path to `userland/tyrne-user/` (was "or workspace root", contradicting the ADR). **(Med)** added the **`HELLO_CONSOLE_CAP` handle-contract AC** (the T-027↔T-028 interface — closes a silent handle-mismatch risk). **(Low)** added the explicit userland clippy gate (`-p tyrne-userland-hello -p tyrne-user` — neither `kernel-clippy` nor `host-clippy` covers the new crates). All against the still-`Proposed` ADR-0039, pre-Accept. - **2026-05-31 — adversarial ADR-0039 review (5-lens workflow, 81 agents, per-finding 2-skeptic verification): 38 findings, 37 refuted, 1 confirmed; governance lens fully clean.** **Confirmed (fixed):** CI's `kernel-build` job runs `cargo kernel-build` directly on a clean checkout ([`.github/workflows/ci.yml`](../../../../.github/workflows/ci.yml)), so once `USERSPACE_IMAGE` becomes `include_bytes!` of a git-ignored `.bin`, the BSP `build.rs` missing-`.bin` `panic!` would **break CI** — added the CI-workflow-update AC here + corrected ADR-0039's stale "what CI runs" claim (CI runs `kernel-build`, not the smoke). **Refuted (37):** the adversarial pass confirmed the prior review-round's fixes are present (AS-lifetime SEC-T024-01, the `HELLO_CONSOLE_CAP` contract, the no-`.data` constraint, the greeting-marker gate — all already in T-027/T-028), that the 4-option analysis is fair (Option 2/4 not strawmanned; artifact-dependencies considered + rejected as unstable-on-pinned-nightly), that governance/`write-adr` compliance is clean, and that the rust-objcopy / offset-0 / default-members feasibility holds. - **2026-06-01 — implemented (→ In Review)** on `t-027-userland-build-pipeline` (off `main` at the merged-B6-gates `a8ce11e`, after the ADR-0039 Accept `df3fcd4`). Built: `userland/tyrne-user` (safe `console_write`/`task_exit` shims + `HELLO_CONSOLE_CAP = 0` + UNSAFE-2026-0033) + `userland/hello` (`_start` at `.text._start` → greet → `task_exit`; `hello.ld` fixing the entry at offset 0 / `0x0080_0000`, ASSERT-no-`.data`/`.bss`) + `tools/build-userland.sh` (cargo → `rust-objcopy` from `llvm-tools-preview`, no Cargo dep → git-ignored `userland/hello/hello.bin`). Wired: root `Cargo.toml` members (excluded from `default-members`); BSP `USERSPACE_IMAGE` → `include_bytes!` + `build.rs` `assert!`-if-absent; `tools/smoke.sh` now builds userland + kernel (one-command); `.github/workflows/ci.yml` installs `llvm-tools-preview` + runs the userland build before `kernel-build`. **Verification:** the 117-byte `.bin` disassembles at offset 0 to the `_start` prologue → `x0 = 0` (`HELLO_CONSOLE_CAP`) → `adr x1` greeting → `w2 = 0x15` (21 = len) → `bl console_write` → `x0 = 0` → `bl task_exit`. Gates: `cargo fmt --all --check`; host + kernel clippy `-D warnings`; **userland clippy** (`-p tyrne-userland-hello -p tyrne-user`); host tests **46 hal / 258 kernel / 58 test-hal / 3 doc** (unchanged — userland is bare-metal, outside the host set); `cargo kernel-build`; **`tools/smoke.sh --int` PASS** — the real 117-byte image loads (`entry = 0x800000`, `image bytes 117`, `stack bytes 4096`), boot reaches `all tasks complete`, only the 2 expected EL1-stub SVCs, **zero new fault class**. Miri unaffected (no kernel/host logic changed — only the embedded `USERSPACE_IMAGE` bytes + build infra). The `hello` image is embedded + loads **dormant** — running it in EL0 is [T-028](T-028-el0-userspace-wireup.md). +- **2026-06-01 — PR #41 review-round (relayed bots, verified against source):** **(High, fixed)** the userland crates broke `--workspace` host builds — `cargo check --workspace --exclude tyrne-bsp-qemu-virt` (the Miri-equivalent) failed on `tyrne-user`'s aarch64 asm. Cfg-gated the `svc` asm + `no_std`/`no_main` to `target_arch = "aarch64"` with degenerate host stubs (never run), so `--workspace` / Miri host-compile cleanly while keeping the ADR-0039 root-members structure (`cargo check --workspace --exclude tyrne-bsp-qemu-virt` now clean). UNSAFE-2026-0033 noted the asm is aarch64-gated. **(Low, fixed)** `hello.ld` now also ASSERTs `.got` empty (non-PIC); `tools/build-userland.sh` resolves `rust-objcopy` deterministically via `rustc --print target-libdir` (was a brittle sysroot `find`) + respects `CARGO_TARGET_DIR`; the §AC "denies enforce" typo fixed. **Skipped:** the "future-dated 2026-06-01" findings (today **is** 2026-06-01 — the dates are accurate, not future); the `build.rs` `format!`→`Path::join` idiom (the `/` paths work on Tyrne's Unix-only dev/CI — macOS + ubuntu — not a bug); a console message in the `panic_handler` (over-engineering for the trivial v1 `hello` — the exit code is the signal); `SyscallError: Display` (unused in v1 — YAGNI). Gates re-run green. diff --git a/docs/audits/unsafe-log.md b/docs/audits/unsafe-log.md index bc59e5f..cc7f389 100644 --- a/docs/audits/unsafe-log.md +++ b/docs/audits/unsafe-log.md @@ -757,4 +757,4 @@ Neither change touches the `copy_nonoverlapping` site itself; both correct contr - **Import the kernel's `SyscallNumber`/ABI types.** Rejected per [ADR-0039](../decisions/0039-userland-build-pipeline.md): userspace must not depend on kernel internals; the contract is the stable ABI, restated userspace-side. - **`options(nostack)` / `preserves_flags`.** Not asserted (conservative): the wrappers do not claim the asm leaves the stack/flags untouched, so no over-assertion can become UB if the contract is wrong. - **Reviewed by:** @cemililik (+ Claude Opus 4.8 agent). The `unsafe` is confined to two tiny shims and is the *userspace* side of an already-audited trap (the EL1 handler is UNSAFE-2026-0029; the boundary is UNSAFE-2026-0014/0030/0032). The security-critical direction (privilege escalation, memory disclosure) is the **kernel→user** boundary, audited there and reviewed for T-028's first real run; this entry covers the user→kernel call shim. -- **Status:** Active. **Dormant in v1** — the `hello` image is embedded (T-027) but not yet run; the shims execute for the first time when [T-028](../analysis/tasks/phase-b/T-028-el0-userspace-wireup.md) wires the EL0 task and the `+0x400` round-trip fires. T-027 verifies the crate compiles + `rust-objcopy`s to a 117-byte raw-flat image whose offset-0 disassembly is the `_start` → `console_write` → `task_exit` sequence (the userland crates are bare-metal, outside the host-test set; verified by `cargo clippy --target aarch64-unknown-none` + disassembly). +- **Status:** Active. **Dormant in v1** — the `hello` image is embedded (T-027) but not yet run; the shims execute for the first time when [T-028](../analysis/tasks/phase-b/T-028-el0-userspace-wireup.md) wires the EL0 task and the `+0x400` round-trip fires. T-027 verifies the crate compiles + `rust-objcopy`s to a 117-byte raw-flat image whose offset-0 disassembly is the `_start` → `console_write` → `task_exit` sequence (the userland crates are bare-metal, outside the host-test set; verified by `cargo clippy --target aarch64-unknown-none` + disassembly). The `svc` asm is `#[cfg(target_arch = "aarch64")]`-gated; non-aarch64 (`--workspace` / Miri host) builds get safe `unimplemented!()` stubs with **no `unsafe`**, so the `unsafe` this entry covers exists only on the aarch64 target. diff --git a/tools/build-userland.sh b/tools/build-userland.sh index dbf9ffd..6f979ae 100755 --- a/tools/build-userland.sh +++ b/tools/build-userland.sh @@ -34,16 +34,24 @@ else fi # Resolve rust-objcopy from the active toolchain's llvm-tools (no Cargo dep). -SYSROOT="$(rustc --print sysroot)" -OBJCOPY="$(find "$SYSROOT" -type f -name 'rust-objcopy' 2>/dev/null | head -n1)" -if [[ -z "$OBJCOPY" ]]; then - echo "error: rust-objcopy not found under $SYSROOT" >&2 +# Resolve rust-objcopy deterministically: target-libdir is +# .../rustlib//lib, whose sibling bin/ holds the llvm-tools binaries. +# Fall back to a sysroot search for unusual layouts. +OBJCOPY="$(rustc --print target-libdir)/../bin/rust-objcopy" +if [[ ! -x "$OBJCOPY" ]]; then + OBJCOPY="$(find "$(rustc --print sysroot)" -type f -name 'rust-objcopy' 2>/dev/null | head -n1)" +fi +if [[ -z "$OBJCOPY" || ! -x "$OBJCOPY" ]]; then + echo "error: rust-objcopy not found in the active toolchain" >&2 echo " install the llvm-tools-preview component (pinned in rust-toolchain.toml):" >&2 echo " rustup component add llvm-tools-preview" >&2 exit 1 fi -ELF="target/aarch64-unknown-none/${PROFILE}/hello" +# Respect CARGO_TARGET_DIR (set in some CI / nested-build layouts); default to +# the workspace `target/` dir cargo uses otherwise. +TARGET_DIR="${CARGO_TARGET_DIR:-target}" +ELF="${TARGET_DIR}/aarch64-unknown-none/${PROFILE}/hello" BIN="userland/hello/hello.bin" if [[ ! -f "$ELF" ]]; then echo "error: expected userland ELF not found at $ELF" >&2 diff --git a/userland/hello/hello.ld b/userland/hello/hello.ld index 874d3f6..1bbdaea 100644 --- a/userland/hello/hello.ld +++ b/userland/hello/hello.ld @@ -32,15 +32,20 @@ SECTIONS { *(.rodata .rodata.*) } - /* v1 forbids writable data (image is USER|EXECUTE, no WRITE). Collect any - * stray .data/.bss so the ASSERTs can prove they are empty. */ + /* v1 forbids writable data (image is USER|EXECUTE, no WRITE) and is + * non-PIC (fixed VA — no relocations). Collect any stray .data/.bss/.got so + * the ASSERTs can prove they are empty rather than let a writable byte or a + * PIC artifact silently slip into the raw-flat image. */ .data : { *(.data .data.*) } .bss : { *(.bss .bss.* COMMON) } + .got : { *(.got .got.plt) } ASSERT(SIZEOF(.data) == 0, "userland hello: .data must be empty (image maps USER|EXECUTE, no WRITE)") ASSERT(SIZEOF(.bss) == 0, "userland hello: .bss must be empty (image maps USER|EXECUTE, no WRITE)") + ASSERT(SIZEOF(.got) == 0, + "userland hello: .got must be empty (v1 is non-PIC, fixed-VA — no relocations)") /* Drop metadata/unwind/debug sections — not part of the raw-flat image. */ /DISCARD/ : { diff --git a/userland/hello/src/main.rs b/userland/hello/src/main.rs index 97221ce..a792c09 100644 --- a/userland/hello/src/main.rs +++ b/userland/hello/src/main.rs @@ -16,14 +16,19 @@ //! [adr-0029]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0029-initial-userspace-image-format.md //! [adr-0039]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0039-userland-build-pipeline.md -#![no_std] -#![no_main] +// `no_std` / `no_main` apply on the real (aarch64 EL0) target. The crate is a +// `--workspace` member, so host tooling (`cargo check --workspace`, Miri) +// compiles it for the host; there it is an ordinary `std` binary with an empty +// `main` (see the host stub below) so it builds (it is never run on the host). +#![cfg_attr(target_arch = "aarch64", no_std, no_main)] +#[cfg(target_arch = "aarch64")] use tyrne_user::{console_write, task_exit, HELLO_CONSOLE_CAP}; /// The greeting emitted via `console_write`. A read-only `.rodata` literal that /// lives inside the mapped image region (`USER | EXECUTE`) — gate #1 admits a /// read of a `USER` page, so the buffer pointer translates and the bytes copy. +#[cfg(target_arch = "aarch64")] static GREETING: &[u8] = b"hello from userspace\n"; /// Userspace entry point. Placed at **offset 0** of the raw-flat image (the @@ -33,6 +38,7 @@ static GREETING: &[u8] = b"hello from userspace\n"; /// `#[no_mangle]` gives the linker the bare `_start` symbol (the `hello.ld` /// `ENTRY`); `#[link_section = ".text._start"]` + the script's `KEEP` place it /// first in `.text` so offset 0 is the entry. +#[cfg(target_arch = "aarch64")] #[no_mangle] #[link_section = ".text._start"] pub extern "C" fn _start() -> ! { @@ -45,7 +51,14 @@ pub extern "C" fn _start() -> ! { /// Panic handler (required for `#![no_std]`). Unwinding is disabled /// (`panic=abort` for the bare-metal target), so this just exits the task with /// a distinct non-zero code rather than attempting to unwind. +#[cfg(target_arch = "aarch64")] #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { task_exit(101) } + +// Host stub: this program targets aarch64 EL0; the host build exists only so +// `cargo check --workspace` / Miri can include the crate (it is never run on +// the host — the real entry is `_start`, above). +#[cfg(not(target_arch = "aarch64"))] +fn main() {} diff --git a/userland/tyrne-user/src/lib.rs b/userland/tyrne-user/src/lib.rs index b32cb24..f49439e 100644 --- a/userland/tyrne-user/src/lib.rs +++ b/userland/tyrne-user/src/lib.rs @@ -20,12 +20,19 @@ #![no_std] +// The `svc` shims are aarch64-only. The crate is a `--workspace` member, so +// host tooling (`cargo check --workspace`, Miri) compiles it for the host — +// where aarch64 inline asm does not assemble. Gate the asm to aarch64 and give +// the host degenerate stubs (never run; the crate only executes in EL0 on +// aarch64). Mirrors the kernel's `cfg_attr(not(test), no_std)` discipline. +#[cfg(target_arch = "aarch64")] use core::arch::asm; /// `task_exit` syscall number (`x8`), per [ADR-0031][adr-0031]. Restated /// userspace-side; the kernel authority is `SyscallNumber::TaskExit as 4`. /// /// [adr-0031]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0031-initial-syscall-set.md +#[cfg(target_arch = "aarch64")] const SYS_TASK_EXIT: u64 = 4; /// `console_write` syscall number (`x8`), per [ADR-0031][adr-0031]. **Debug-gated** @@ -34,6 +41,7 @@ const SYS_TASK_EXIT: u64 = 4; /// userspace-side; the kernel authority is `SyscallNumber::ConsoleWrite as 5`. /// /// [adr-0031]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0031-initial-syscall-set.md +#[cfg(target_arch = "aarch64")] const SYS_CONSOLE_WRITE: u64 = 5; /// The capability word the first userspace task ([`tyrne-userland-hello`]) names @@ -72,6 +80,7 @@ pub struct SyscallError( /// /// Returns [`SyscallError`] wrapping the non-zero `x0` status when the kernel /// rejects the call. +#[cfg(target_arch = "aarch64")] #[allow( clippy::cast_possible_truncation, reason = "the returned byte count is <= buf.len() <= usize::MAX; the u64 -> usize \ @@ -111,8 +120,18 @@ pub fn console_write(cap: u64, buf: &[u8]) -> Result { } } +// Host stub for `console_write` (see the crate-level note): aarch64-EL0-only; +// the host build exists solely so `cargo check --workspace` / Miri can compile +// the crate. Never run on the host. `#[doc(hidden)]` keeps it out of rustdoc. +#[cfg(not(target_arch = "aarch64"))] +#[doc(hidden)] +pub fn console_write(_cap: u64, _buf: &[u8]) -> Result { + unimplemented!("tyrne-user runs only at aarch64 EL0") +} + /// Terminate the current task with exit `code`. Does not return — the kernel /// removes the task from the scheduler and never re-enters it. +#[cfg(target_arch = "aarch64")] pub fn task_exit(code: u64) -> ! { // SAFETY: `svc #0` with x8 = the task_exit number and x0 = the exit code. // task_exit acts on the caller's own task identity (no capability), and the @@ -128,3 +147,10 @@ pub fn task_exit(code: u64) -> ! { ); } } + +// Host stub for `task_exit` (see the crate-level note): aarch64-EL0-only. +#[cfg(not(target_arch = "aarch64"))] +#[doc(hidden)] +pub fn task_exit(_code: u64) -> ! { + unimplemented!("tyrne-user runs only at aarch64 EL0") +} From 68f07ec54e2de93e0af2d1ee13717e69ab6ac49c Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Mon, 1 Jun 2026 01:09:15 +0300 Subject: [PATCH 7/7] =?UTF-8?q?fix(userland):=20gate=20hello.ld=20to=20aar?= =?UTF-8?q?ch64=20=E2=80=94=20unbreak=20the=20CI=20coverage=20(llvm-cov)?= =?UTF-8?q?=20host=20link?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CI coverage job (cargo llvm-cov --workspace --exclude tyrne-bsp-qemu-virt) failed: it does a real instrumented running 46 tests test mmu::vmsav8::tests::block_descriptor_drops_low_bits_for_unaligned_pa ... ok test mmu::vmsav8::tests::block_descriptor_v1_device_block_encoding ... ok test mmu::vmsav8::tests::descriptor_bits_to_flags_is_lock_shut ... ok test mmu::vmsav8::tests::block_descriptor_v1_kernel_ram_block_encoding ... ok test mmu::vmsav8::tests::device_flag_picks_device_attr_index ... ok test mmu::vmsav8::tests::empty_flags_kernel_ro_normal_no_execute_global_inverted ... ok test mmu::vmsav8::tests::descriptor_bits_to_flags_round_trips_valid_flags ... ok test mmu::vmsav8::tests::flags_to_descriptor_bits_ignores_bits_above_four ... ok test mmu::vmsav8::tests::global_flag_clears_ng_bit ... ok test mmu::vmsav8::tests::mair_value_attr0_device_attr1_normal_others_zero ... ok test mmu::vmsav8::tests::page_descriptor_drops_low_bits_for_unaligned_pa ... ok test mmu::vmsav8::tests::page_descriptor_v1_kernel_rw_page_encoding ... ok test mmu::vmsav8::tests::sctlr_mmu_enable_mask_sets_m_c_i_only ... ok test mmu::vmsav8::tests::table_descriptor_carries_valid_and_table_bits_and_address ... ok test mmu::vmsav8::tests::table_descriptor_drops_low_bits_for_unaligned_address ... ok test mmu::vmsav8::tests::tcr_high_half_clears_only_epd1 ... ok test mmu::vmsav8::tests::tcr_value_carries_t0sz_16_and_ips_2_and_epd1_set ... ok test mmu::vmsav8::tests::user_execute_yields_user_ro_pxn_one_uxn_zero ... ok test mmu::vmsav8::tests::user_write_yields_user_rw_no_execute ... ok test mmu::vmsav8::tests::write_alone_yields_kernel_rw_no_execute ... ok test mmu::vmsav8::tests::write_plus_execute_yields_kernel_rwx_uxn_pxn_zero ... ok test timer::tests::ns_to_ticks_one_second_yields_frequency_at_any_freq ... ok test timer::tests::ns_to_ticks_round_trips_against_ticks_to_ns_at_qemu_frequency ... ok test timer::tests::ns_to_ticks_rounds_up_on_subtick ... ok test timer::tests::ns_to_ticks_saturates_at_u64_max ... ok test timer::tests::ns_to_ticks_zero_ns_is_zero ... ok test timer::tests::ns_to_ticks_panics_on_zero_frequency - should panic ... ok test timer::tests::resolution_clamps_to_one_above_2ghz ... ok test timer::tests::resolution_const_fn_works_in_const_context ... ok test timer::tests::resolution_floor_vs_round_difference_documented ... ok test timer::tests::resolution_one_gigahertz_is_one_ns ... ok test timer::tests::resolution_ns_for_freq_panics_on_zero_frequency - should panic ... ok test timer::tests::resolution_qemu_virt_is_16_ns ... ok test timer::tests::resolution_round_to_nearest_for_non_divisor ... ok test timer::tests::resolution_two_ghz_is_one_ns_exactly ... ok test timer::tests::ticks_to_ns_const_fn_works_in_const_context ... ok test timer::tests::ticks_to_ns_high_frequency_one_gigahertz ... ok test timer::tests::ticks_to_ns_is_monotonic_across_frequencies ... ok test timer::tests::ticks_to_ns_no_silent_wrap_at_64bit_boundary ... ok test timer::tests::ticks_to_ns_pi3_class_non_divisor ... ok test timer::tests::ticks_to_ns_plateaus_at_u64_max_after_saturation ... ok test timer::tests::ticks_to_ns_panics_on_zero_frequency - should panic ... ok test timer::tests::ticks_to_ns_qemu_virt_one_second ... ok test timer::tests::ticks_to_ns_qemu_virt_single_tick ... ok test timer::tests::ticks_to_ns_saturates_at_u64_max ... ok test timer::tests::ticks_to_ns_zero_count_is_zero ... ok test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s running 258 tests test cap::rights::tests::difference_clears_bits ... ok test cap::rights::tests::empty_contains_empty_but_nothing_else ... ok test cap::rights::tests::bitor_assign_adds_bits ... ok test cap::rights::tests::from_raw_masks_unknown_bits ... ok test cap::rights::tests::from_raw_and_raw_round_trip ... ok test cap::rights::tests::intersection_narrows ... ok test cap::rights::tests::union_and_contains ... ok test cap::table::tests::cap_copy_narrows_rights ... ok test cap::table::tests::cap_copy_of_root_then_revoke_root_leaves_peer_alive ... ok test cap::table::tests::cap_copy_on_stale_handle_returns_invalid_handle ... ok test cap::table::tests::cap_copy_rejects_widened_rights ... ok test cap::table::tests::cap_copy_with_same_rights_succeeds ... ok test cap::table::tests::cap_copy_without_duplicate_right_fails ... ok test cap::table::tests::cap_derive_creates_child_with_narrower_rights ... ok test cap::table::tests::cap_derive_enforces_depth_cap ... ok test cap::table::tests::cap_derive_on_full_table_returns_caps_exhausted ... ok test cap::table::tests::cap_derive_rejects_widened_rights ... ok test cap::table::tests::cap_derive_without_derive_right_fails ... ok test cap::table::tests::cap_drop_on_interior_node_returns_has_children ... ok test cap::table::tests::cap_revoke_cascades_depth_three ... ok test cap::table::tests::cap_revoke_clears_references_object ... ok test cap::table::tests::cap_revoke_on_leaf_is_a_noop ... ok test cap::table::tests::cap_revoke_on_stale_handle_fails ... ok test cap::table::tests::cap_revoke_removes_only_descendants ... ok test cap::table::tests::cap_revoke_without_revoke_right_fails ... ok test cap::table::tests::cap_take_middle_sibling_preserves_list_integrity ... ok test cap::table::tests::cap_take_on_node_with_children_fails ... ok test cap::table::tests::cap_take_returns_capability_and_invalidates_handle ... ok test cap::table::tests::cap_take_slot_reusable_with_bumped_generation ... ok test cap::table::tests::cap_take_stale_handle_fails ... ok test cap::table::tests::copy_of_a_child_shares_parent ... ok test cap::table::tests::drop_first_child_updates_parent_first_child_pointer ... ok test cap::table::tests::drop_invalidates_handle ... ok test cap::table::tests::drop_middle_sibling_preserves_list_integrity ... ok test cap::table::tests::drop_peer_does_not_affect_other_peer ... ok test cap::table::tests::drop_twice_returns_invalid_handle ... ok test cap::table::tests::freed_slot_is_reused_with_bumped_generation ... ok test cap::table::tests::is_full_transitions ... ok test cap::table::tests::lookup_on_stale_handle_returns_invalid_handle ... ok test cap::table::tests::new_table_can_accept_one_root ... ok test cap::table::tests::references_object_sees_live_caps_only ... ok test cap::table::tests::slot_entry_size_matches_adr_0023 ... ok test cap::table::tests::table_exhaustion_returns_caps_exhausted ... ok test cap::tests::capobject_debug_redacts_handle_but_shows_kind ... ok test cap::tests::debug_redacts_named_object_but_keeps_rights ... ok test ipc::tests::cancel_recv_clears_recv_waiting_back_to_idle ... ok test ipc::tests::blocked_sender_delivered_on_subsequent_recv ... ok test ipc::tests::cancel_recv_is_idempotent ... ok test ipc::tests::cancel_recv_on_idle_is_noop ... ok test ipc::tests::cancel_recv_on_recv_complete_does_not_drop_message_or_cap ... ok test ipc::tests::cancel_recv_on_send_pending_does_not_drop_message ... ok test ipc::tests::cancel_recv_to_destroyed_endpoint_returns_stale_handle ... ok test ipc::tests::cancel_recv_with_wrong_object_kind_returns_wrong_object_kind ... ok test ipc::tests::cancel_recv_without_recv_right_fails ... ok test ipc::tests::notify_sets_bits ... ok test ipc::tests::notify_with_stale_handle_after_slot_reuse_fails ... ok test ipc::tests::notify_with_wrong_object_kind_returns_wrong_object_kind ... ok test ipc::tests::notify_without_notify_right_fails ... ok test ipc::tests::receiver_first_delivers_on_send ... ok test ipc::tests::receiver_first_then_send_with_cap ... ok test ipc::tests::recv_with_full_table_preserves_pending_cap ... ok test ipc::tests::recv_with_wrong_object_kind_returns_wrong_object_kind ... ok test ipc::tests::recv_without_recv_right_fails ... ok test ipc::tests::second_recv_when_waiting_fails ... ok test ipc::tests::second_send_when_pending_fails ... ok test ipc::tests::send_to_destroyed_endpoint_returns_stale_handle ... ok test ipc::tests::send_transfers_cap_atomically ... ok test ipc::tests::send_with_bad_transfer_cap_preserves_recv_waiting ... ok test ipc::tests::send_with_dropped_cap_handle_returns_stale_handle ... ok test ipc::tests::send_with_wrong_object_kind_returns_wrong_object_kind ... ok test ipc::tests::send_without_send_right_fails ... ok test ipc::tests::send_without_transfer_right_on_xfer_cap_fails ... ok test ipc::tests::sender_first_delivers_on_recv ... ok test ipc::tests::stale_queue_state_reset_on_slot_reuse ... ok test ipc::tests::stale_recv_waiting_resets_silently ... ok test ipc::tests::stale_send_pending_without_cap_resets_silently ... ok test mm::address_space::tests::arena_alloc_returns_distinct_handles ... ok test ipc::tests::stale_send_pending_with_some_cap_panics_in_debug - should panic ... ok test mm::address_space::tests::arena_full_returns_arena_full_error ... ok test mm::address_space::tests::arena_get_with_stale_handle_returns_none ... ok test mm::address_space::tests::cap_create_address_space_consumes_one_pmm_frame_and_mints_cap ... ok test mm::address_space::tests::cap_create_address_space_rejects_missing_derive ... ok test mm::address_space::tests::cap_create_address_space_rejects_widened_rights ... ok test mm::address_space::tests::cap_create_address_space_rejects_wrong_parent_kind ... ok test mm::address_space::tests::cap_create_address_space_returns_out_of_frames_on_pmm_exhaustion ... ok test mm::address_space::tests::cap_create_rejects_too_deep_parent_without_consuming_pmm ... ok test mm::address_space::tests::cap_create_uses_cap_derive_so_revoke_parent_invalidates_child ... ok test mm::address_space::tests::cap_map_installs_mapping_and_flushes_tlb ... ok test mm::address_space::tests::cap_map_propagates_block_mapped_and_leaves_no_mapping ... ok test mm::address_space::tests::cap_map_propagates_intermediate_out_of_frames_and_does_not_consume_pa ... ok test mm::address_space::tests::cap_map_rejects_wrong_kind ... ok test mm::address_space::tests::cap_map_wraps_mmu_error_passthrough ... ok test mm::address_space::tests::cap_unmap_returns_unmapped_frame ... ok test mm::address_space::tests::cap_unmap_wraps_mmu_error_passthrough ... ok test mm::address_space::tests::destroy_with_stale_handle_returns_stale_handle_error ... ok test mm::address_space::tests::inner_accessors_provide_borrow_and_borrow_mut ... ok test mm::address_space::tests::resolve_address_space_cap_returns_handle_on_correct_kind ... ok test mm::address_space::tests::resolve_address_space_cap_returns_wrong_kind_on_endpoint_cap ... ok test mm::address_space::tests::wrap_bootstrap_returns_address_space_with_root ... ok test mm::pmm::tests::alloc_frame_implements_frame_provider ... ok test mm::pmm::tests::alloc_frame_recovers_after_free_under_exhaustion ... ok test mm::pmm::tests::alloc_frame_returns_none_when_exhausted ... ok test mm::pmm::tests::alloc_frame_returns_first_free_and_zeroes_payload ... ok test mm::pmm::tests::could_yield_pa_overlapping_interval_equals_perframe ... ok test mm::pmm::tests::could_yield_pa_overlapping_treats_allocated_frame_as_yieldable ... ok test mm::pmm::tests::extent_4f_fixture_sanity ... ok test mm::pmm::tests::free_frame_clears_bit_and_rewinds_hint ... ok test mm::pmm::tests::free_frame_rejects_double_free_and_reserved ... ok test mm::pmm::tests::free_frame_rejects_pa_outside_extent ... ok test mm::pmm::tests::free_frame_reserved_check_iterates_only_populated_slots ... ok test mm::pmm::tests::new_marks_reserved_ranges_and_initialises_counters ... ok test mm::pmm::tests::new_rejects_extent_larger_than_bitmap ... ok test mm::pmm::tests::new_rejects_overlapping_reserved_ranges ... ok test mm::pmm::tests::new_rejects_reserved_range_outside_extent ... ok test mm::pmm::tests::new_rejects_too_many_reserved_ranges ... ok test mm::pmm::tests::new_rejects_unaligned_extent ... ok test mm::pmm::tests::stats_parity_with_bitmap_bit_count ... ok test obj::arena::tests::allocate_and_get_round_trip ... ok test obj::arena::tests::empty_capacity_arena_has_no_free_slot ... ok test obj::arena::tests::exhaustion_returns_none ... ok test obj::arena::tests::free_invalidates_id ... ok test obj::arena::tests::free_middle_then_allocate_reuses_that_slot ... ok test obj::arena::tests::free_then_allocate_bumps_generation ... ok test obj::arena::tests::get_mut_permits_mutation ... ok test obj::endpoint::tests::create_destroy_round_trip ... ok test obj::notification::tests::destroy_invalidates_handle ... ok test obj::notification::tests::set_and_consume_round_trip ... ok test obj::task::tests::address_space_handle_round_trips ... ok test obj::task::tests::arena_exhaustion_returns_arena_full ... ok test obj::task::tests::create_then_get_round_trip ... ok test obj::task::tests::destroy_invalidates_handle ... ok test obj::task_loader::tests::accepts_image_disjoint_from_pmm_extent ... ok test obj::task_loader::tests::accepts_image_base_va_exactly_at_userspace_va_limit_minus_span ... ok test obj::task_loader::tests::intermediate_frame_count_8mib_image_one_stack_page_crosses_five_l2 ... ok test obj::task_loader::tests::frame_budget_includes_root_plus_intermediates ... ok test obj::task_loader::tests::intermediate_frame_count_l1_boundary_crossing ... ok test obj::task_loader::tests::intermediate_frame_count_minimal_single_l2_slot ... ok test obj::task_loader::tests::intermediate_frame_count_saturated_total_pages ... ok test obj::task_loader::tests::intermediate_frame_count_zero_span_defensive ... ok test obj::task_loader::tests::load_error_frame_budget_exceeded_fields_round_trip ... ok test obj::task_loader::tests::load_error_variants_are_distinct ... ok test obj::task_loader::tests::load_error_variants_pattern_match_exhaustively ... ok test obj::task_loader::tests::loaded_image_distinguishes_different_field_values ... ok test obj::task_loader::tests::loaded_image_struct_literal_round_trips_through_copy_and_eq ... ok test obj::task_loader::tests::maps_stack_with_user_write_flags ... ok test obj::task_loader::tests::maps_image_pages_with_user_execute_flags ... ok test obj::task_loader::tests::missing_derive_surfaces_via_address_space_creation_failed ... ok test obj::task_loader::tests::rejects_empty_image ... ok test obj::task_loader::tests::rejects_image_base_va_past_userspace_va_limit ... ok test obj::task_loader::tests::mints_address_space_cap_with_requested_non_empty_rights ... ok test obj::task_loader::tests::rejects_image_base_va_saturating_overflow ... ok test obj::task_loader::tests::rejects_invalid_parent_cap_lookup ... ok test obj::task_loader::tests::rejects_invalid_parent_cap_wrong_kind ... ok test obj::task_loader::tests::rejects_misaligned_image_base_va_with_pmm_byte_stable ... ok test obj::task_loader::tests::rejects_when_image_overlaps_allocatable_memory ... ok test obj::task_loader::tests::rejects_when_pmm_budget_exceeded ... ok test obj::task_loader::tests::rejects_zero_stack ... ok test obj::task_loader::tests::returns_loaded_image_with_correct_metadata ... ok test obj::task_loader::tests::rolls_back_on_block_mapped_mid_image_loop ... ok test obj::task_loader::tests::rollback_helper_zero_pages_only_drops_cap ... ok test obj::task_loader::tests::rolls_back_on_cap_map_failure_mid_image_loop ... ok test obj::task_loader::tests::rolls_back_on_cap_map_failure_mid_stack_loop ... ok test obj::task_loader::tests::rolls_back_on_intermediate_out_of_frames_mid_image_loop ... ok test obj::task_loader::tests::rolls_back_on_pmm_exhausted_mid_image_loop ... ok test obj::task_loader::tests::stack_top_va_is_one_past_highest_mapped ... ok test obj::task_loader::tests::tail_zeroing_on_partial_last_page ... ok test obj::task_loader::tests::task_create_from_image_mints_task_cap_bound_to_the_loaded_as ... ok test obj::task_loader::tests::task_create_from_image_rejects_stale_as_cap ... ok test obj::task_loader::tests::task_create_from_image_rejects_when_task_arena_full ... ok test obj::task_loader::tests::task_create_from_image_rejects_wrong_kind_as_cap ... ok test obj::task_loader::tests::task_create_from_image_rolls_back_task_on_cap_table_exhausted ... ok test obj::task_loader::tests::va_range_preflight_runs_before_frame_budget ... ok test obj::task_loader::tests::widened_rights_surfaces_via_address_space_creation_failed ... ok test sched::tests::add_task_sets_ready_state_and_stores_handle ... ok test sched::tests::add_user_task_seeds_el0_context_and_enqueues_ready ... ok test sched::tests::address_space_activation_target_pure_function ... ok test sched::tests::current_accessors_resolve_running_task_bindings_or_none ... ok test sched::tests::dispatcher_picks_idle_only_when_ready_queue_empty ... ok test sched::tests::ipc_recv_and_yield_deadlock_rolls_back_endpoint_state ... ok test sched::tests::ipc_recv_and_yield_resume_pending_returns_typed_err ... ok test sched::tests::ipc_recv_and_yield_returns_deadlock_when_ready_queue_empty ... ok test sched::tests::ipc_recv_and_yield_with_idle_as_current_returns_deadlock ... ok test sched::tests::ipc_recv_and_yield_with_no_current_task_leaves_endpoint_idle ... ok test sched::tests::ipc_send_and_yield_delivered_unblocks_receiver_and_yields ... ok test sched::tests::ipc_send_and_yield_enqueued_does_not_yield ... ok test sched::tests::ipc_send_and_yield_send_error_preserves_scheduler_state ... ok test sched::tests::queue_empty_dequeue_is_none ... ok test sched::tests::queue_enqueue_dequeue_fifo_order ... ok test sched::tests::queue_full_returns_error ... ok test sched::tests::queue_len_and_is_empty ... ok test sched::tests::queue_wraps_around ... ok test sched::tests::register_idle_stores_handle_in_idle_slot_and_not_in_ready_queue ... ok test sched::tests::start_prelude_dispatches_head_and_marks_ready ... ok test sched::tests::task_state_variants_are_distinct ... ok test sched::tests::start_prelude_panics_on_empty_ready_queue - should panic ... ok test sched::tests::unblock_after_yield_dispatches_unblocked_receiver_not_idle ... ok test sched::tests::unblock_receiver_on_moves_task_to_ready ... ok test sched::tests::unblock_receiver_on_wrong_ep_is_noop ... ok test sched::tests::yield_now_activates_when_tasks_differ_in_address_space ... ok test sched::tests::yield_now_masks_irqs_across_switch_and_restores_on_return ... ok test sched::tests::yield_now_skips_activation_when_tasks_share_address_space ... ok test sched::tests::yield_now_switches_context_and_updates_current ... ok test sched::tests::yield_now_with_no_current_returns_error ... ok test syscall::abi::tests::as_u64_round_trips_recognised_numbers ... ok test syscall::abi::tests::console_write_is_a_syscall_in_debug_builds ... ok test syscall::abi::tests::decode_maps_v1_numbers ... ok test syscall::abi::tests::decode_out_of_range_is_none ... ok test syscall::abi::tests::decode_send_message_reads_label_and_params ... ok test syscall::abi::tests::decode_zero_is_reserved_invalid ... ok test syscall::abi::tests::none_round_trips_through_null_sentinel ... ok test syscall::abi::tests::recv_pending_packs_pending_code_and_zeroes_rest ... ok test syscall::abi::tests::recv_received_packs_message_and_cap ... ok test syscall::abi::tests::recv_received_without_cap_packs_null_sentinel ... ok test syscall::abi::tests::required_handle_decode_ignores_sentinel_semantics ... ok test syscall::abi::tests::send_outcome_codes ... ok test syscall::abi::tests::some_handle_round_trips ... ok test syscall::abi::tests::syscall_return_with_payload_sets_indexed_word ... ok test syscall::dispatch::tests::bad_number_zero_returns_bad_syscall_number_touching_nothing ... ok test syscall::dispatch::tests::console_write_cap_ok_but_non_user_page_emits_nothing ... ok test syscall::dispatch::tests::console_write_emits_buffer_and_returns_byte_count ... ok test syscall::dispatch::tests::console_write_exactly_one_chunk_emits_all_bytes ... ok test syscall::dispatch::tests::console_write_multipage_second_page_unmapped_emits_nothing ... ok test syscall::dispatch::tests::console_write_out_of_range_buffer_faults_without_output ... ok test syscall::dispatch::tests::console_write_spanning_multiple_chunks_emits_all_bytes ... ok test syscall::dispatch::tests::console_write_with_no_cap_returns_cap_invalid_handle_no_output ... ok test syscall::dispatch::tests::console_write_with_wrong_kind_cap_returns_cap_wrong_kind ... ok test syscall::dispatch::tests::console_write_without_write_right_returns_insufficient_rights ... ok test syscall::dispatch::tests::incomplete_binding_context_fails_closed_on_both_planes ... ok test syscall::dispatch::tests::out_of_range_number_returns_bad_syscall_number ... ok test syscall::dispatch::tests::recv_of_enqueued_message_unpacks_into_registers ... ok test syscall::dispatch::tests::recv_with_no_sender_returns_pending_packing ... ok test syscall::dispatch::tests::send_with_no_receiver_enqueues_and_returns_ok ... ok test syscall::dispatch::tests::send_with_stale_transfer_handle_returns_invalid_transfer_cap ... ok test syscall::dispatch::tests::send_with_transfer_cap_then_recv_returns_cap_in_x6 ... ok test syscall::dispatch::tests::send_without_send_right_returns_typed_ipc_missing_right ... ok test syscall::dispatch::tests::task_exit_routes_to_terminate_with_code ... ok test syscall::dispatch::tests::task_exit_with_no_current_task_fails_closed ... ok test syscall::dispatch::tests::task_yield_routes_to_reschedule ... ok test syscall::dispatch::tests::task_yield_with_no_current_task_fails_closed ... ok test syscall::error::tests::cap_and_ipc_status_blocks_do_not_collide ... ok test syscall::error::tests::cap_error_from_round_trips_and_encodes_in_cap_block ... ok test syscall::error::tests::ipc_error_from_round_trips_and_encodes_in_ipc_block ... ok test syscall::error::tests::ok_status_is_zero_and_no_error_encodes_to_it ... ok test syscall::error::tests::top_level_status_codes_are_stable ... ok test syscall::user_access::tests::copy_from_user_block_mapped_page_faults ... ok test syscall::user_access::tests::copy_from_user_faults_on_unmapped_page ... ok test syscall::user_access::tests::copy_from_user_multipage_second_page_unmapped_copies_nothing ... ok test syscall::user_access::tests::copy_from_user_out_of_window_faults_before_translate ... ok test syscall::user_access::tests::copy_from_user_overrun_past_window_end_faults ... ok test syscall::user_access::tests::copy_from_user_range_ending_in_top_page_does_not_spuriously_fault ... ok test syscall::user_access::tests::copy_from_user_rejects_in_window_non_user_page ... ok test syscall::user_access::tests::copy_from_user_spanning_two_pages_copies_all ... ok test syscall::user_access::tests::copy_from_user_translates_and_copies_a_user_page ... ok test syscall::user_access::tests::copy_to_user_in_range_moves_bytes ... ok test syscall::user_access::tests::copy_to_user_rejects_read_only_user_page ... ok test syscall::user_access::tests::validate_exact_window_fit_is_ok ... ok test syscall::user_access::tests::wrapping_range_faults ... ok test syscall::user_access::tests::zero_length_copy_is_ok_even_for_unmapped_pointer ... ok test result: ok. 258 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s running 58 tests test context_switch::tests::context_switch_marks_current_and_counts ... ok test console::tests::fmt_writer_produces_formatted_output ... ok test console::tests::captures_successive_byte_writes ... ok test context_switch::tests::default_context_is_uninitialized_and_unswitched ... ok test console::tests::default_fake_console_is_empty ... ok test context_switch::tests::init_context_records_entry_stack_and_marks_initialized ... ok test context_switch::tests::init_context_clears_prior_user_markers_on_reuse ... ok test context_switch::tests::init_user_context_records_user_entry_sp_kernel_stack_and_marks_is_user ... ok test cpu::tests::default_cpu_reports_core_zero_with_irqs_enabled ... ok test cpu::tests::disable_irqs_masks_and_returns_previous_state ... ok test cpu::tests::instruction_barrier_increments_count ... ok test cpu::tests::irq_guard_enters_and_exits_critical_section ... ok test cpu::tests::irq_state_uses_daif_polarity_zero_means_enabled ... ok test cpu::tests::nested_irq_guards_restore_outer_state ... ok test cpu::tests::wait_for_interrupt_increments_count ... ok test cpu::tests::with_core_id_sets_reported_id ... ok test irq_controller::tests::ack_eoi_cycle_leaves_clean_state ... ok test irq_controller::tests::acknowledge_returns_none_when_queue_empty ... ok test irq_controller::tests::acknowledge_returns_pending_fifo ... ok test irq_controller::tests::disable_removes_enabled_state ... ok test irq_controller::tests::disabled_irq_can_still_be_injected_for_test_purposes ... ok test irq_controller::tests::enable_is_idempotent ... ok test irq_controller::tests::disable_panics_on_out_of_range_irq - should panic ... ok test irq_controller::tests::enable_marks_line_as_enabled ... ok test irq_controller::tests::end_of_interrupt_records_irq_in_order ... ok test mmu::tests::activate_records_root ... ok test irq_controller::tests::enable_panics_on_out_of_range_irq - should panic ... ok test mmu::tests::block_mapped_mmu_delegates_unblocked_addresses ... ok test mmu::tests::block_mapped_mmu_injects_block_mapped_on_map_and_unmap ... ok test mmu::tests::block_mapped_mmu_translate_injects_block_mapped ... ok test mmu::tests::bulk_map_with_ignore_then_invalidate_tlb_all ... ok test mmu::tests::create_address_space_stores_root ... ok test mmu::tests::double_map_returns_already_mapped ... ok test mmu::tests::fake_translate_returns_mapping_and_aligns_interior_offset ... ok test mmu::tests::fake_translate_unmapped_is_not_mapped ... ok test mmu::tests::map_rejects_device_plus_execute ... ok test mmu::tests::map_rejects_unaligned_va ... ok test mmu::tests::map_returns_token_with_mapped_va ... ok test mmu::tests::map_unmap_round_trip ... ok test mmu::tests::mapper_flush_carries_virt_addr ... ok test mmu::tests::mapper_flush_flush_invokes_invalidate_tlb_address ... ok test mmu::tests::mapper_flush_ignore_is_documented_noop ... ok test mmu::tests::mapping_flags_difference_clears_bits ... ok test mmu::tests::mapping_flags_union_and_contains ... ok test mmu::tests::out_of_frames_mmu_maps_while_frames_available ... ok test mmu::tests::out_of_frames_mmu_returns_out_of_frames_when_provider_empty ... ok test mmu::tests::phys_frame_rejects_unaligned ... ok test mmu::tests::tlb_invalidations_recorded_in_order ... ok test mmu::tests::unmap_missing_returns_not_mapped ... ok test mmu::tests::unmap_rejects_unaligned_va ... ok test mmu::tests::unmap_returns_token_with_unmapped_va_and_frame ... ok test timer::tests::advance_moves_clock_forward ... ok test timer::tests::arm_deadline_records_value ... ok test timer::tests::arm_deadline_replaces_previous ... ok test timer::tests::cancel_clears_deadline_and_counts ... ok test timer::tests::default_has_one_nanosecond_resolution ... ok test timer::tests::new_starts_at_zero_with_given_resolution ... ok test timer::tests::set_now_overrides_clock ... ok test result: ok. 58 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s running 2 tests test hal/src/console.rs - console::FmtWriter (line 51) ... ignored test hal/src/cpu.rs - cpu::IrqGuard (line 108) ... ignored test result: ok. 0 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 0.00s running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s running 3 tests test test-hal/src/context_switch.rs - context_switch::FakeContextSwitch (line 60) ... ok test test-hal/src/mmu.rs - mmu::OutOfFramesMmu (line 312) ... ok test test-hal/src/mmu.rs - mmu::BlockMappedMmu (line 446) ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.91s build that LINKS the host test binary of the hello bin crate, and hello/build.rs passed -T hello.ld unconditionally — so the bare-metal script's no-.data/.bss/.got ASSERTs fired against the host std + coverage artifacts at link time. (My earlier review-round cfg-gated the SOURCE so --workspace host-COMPILES, and the miri job passed because Miri interprets rather than links — but llvm-cov links, and I had validated with (no link) + running 46 tests test mmu::vmsav8::tests::block_descriptor_drops_low_bits_for_unaligned_pa ... ok test mmu::vmsav8::tests::descriptor_bits_to_flags_round_trips_valid_flags ... ok test mmu::vmsav8::tests::block_descriptor_v1_device_block_encoding ... ok test mmu::vmsav8::tests::descriptor_bits_to_flags_is_lock_shut ... ok test mmu::vmsav8::tests::block_descriptor_v1_kernel_ram_block_encoding ... ok test mmu::vmsav8::tests::device_flag_picks_device_attr_index ... ok test mmu::vmsav8::tests::empty_flags_kernel_ro_normal_no_execute_global_inverted ... ok test mmu::vmsav8::tests::flags_to_descriptor_bits_ignores_bits_above_four ... ok test mmu::vmsav8::tests::global_flag_clears_ng_bit ... ok test mmu::vmsav8::tests::mair_value_attr0_device_attr1_normal_others_zero ... ok test mmu::vmsav8::tests::page_descriptor_drops_low_bits_for_unaligned_pa ... ok test mmu::vmsav8::tests::page_descriptor_v1_kernel_rw_page_encoding ... ok test mmu::vmsav8::tests::sctlr_mmu_enable_mask_sets_m_c_i_only ... ok test mmu::vmsav8::tests::table_descriptor_carries_valid_and_table_bits_and_address ... ok test mmu::vmsav8::tests::table_descriptor_drops_low_bits_for_unaligned_address ... ok test mmu::vmsav8::tests::tcr_high_half_clears_only_epd1 ... ok test mmu::vmsav8::tests::tcr_value_carries_t0sz_16_and_ips_2_and_epd1_set ... ok test mmu::vmsav8::tests::user_execute_yields_user_ro_pxn_one_uxn_zero ... ok test mmu::vmsav8::tests::user_write_yields_user_rw_no_execute ... ok test mmu::vmsav8::tests::write_alone_yields_kernel_rw_no_execute ... ok test mmu::vmsav8::tests::write_plus_execute_yields_kernel_rwx_uxn_pxn_zero ... ok test timer::tests::ns_to_ticks_one_second_yields_frequency_at_any_freq ... ok test timer::tests::ns_to_ticks_round_trips_against_ticks_to_ns_at_qemu_frequency ... ok test timer::tests::ns_to_ticks_rounds_up_on_subtick ... ok test timer::tests::ns_to_ticks_saturates_at_u64_max ... ok test timer::tests::ns_to_ticks_panics_on_zero_frequency - should panic ... ok test timer::tests::ns_to_ticks_zero_ns_is_zero ... ok test timer::tests::resolution_clamps_to_one_above_2ghz ... ok test timer::tests::resolution_const_fn_works_in_const_context ... ok test timer::tests::resolution_floor_vs_round_difference_documented ... ok test timer::tests::resolution_one_gigahertz_is_one_ns ... ok test timer::tests::resolution_ns_for_freq_panics_on_zero_frequency - should panic ... ok test timer::tests::resolution_qemu_virt_is_16_ns ... ok test timer::tests::resolution_round_to_nearest_for_non_divisor ... ok test timer::tests::resolution_two_ghz_is_one_ns_exactly ... ok test timer::tests::ticks_to_ns_const_fn_works_in_const_context ... ok test timer::tests::ticks_to_ns_high_frequency_one_gigahertz ... ok test timer::tests::ticks_to_ns_is_monotonic_across_frequencies ... ok test timer::tests::ticks_to_ns_no_silent_wrap_at_64bit_boundary ... ok test timer::tests::ticks_to_ns_panics_on_zero_frequency - should panic ... ok test timer::tests::ticks_to_ns_pi3_class_non_divisor ... ok test timer::tests::ticks_to_ns_plateaus_at_u64_max_after_saturation ... ok test timer::tests::ticks_to_ns_qemu_virt_one_second ... ok test timer::tests::ticks_to_ns_qemu_virt_single_tick ... ok test timer::tests::ticks_to_ns_saturates_at_u64_max ... ok test timer::tests::ticks_to_ns_zero_count_is_zero ... ok test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s running 258 tests test cap::rights::tests::bitor_assign_adds_bits ... ok test cap::rights::tests::difference_clears_bits ... ok test cap::rights::tests::empty_contains_empty_but_nothing_else ... ok test cap::rights::tests::from_raw_and_raw_round_trip ... ok test cap::rights::tests::from_raw_masks_unknown_bits ... ok test cap::rights::tests::intersection_narrows ... ok test cap::rights::tests::union_and_contains ... ok test cap::table::tests::cap_copy_narrows_rights ... ok test cap::table::tests::cap_copy_of_root_then_revoke_root_leaves_peer_alive ... ok test cap::table::tests::cap_copy_on_stale_handle_returns_invalid_handle ... ok test cap::table::tests::cap_copy_rejects_widened_rights ... ok test cap::table::tests::cap_copy_with_same_rights_succeeds ... ok test cap::table::tests::cap_copy_without_duplicate_right_fails ... ok test cap::table::tests::cap_derive_creates_child_with_narrower_rights ... ok test cap::table::tests::cap_derive_enforces_depth_cap ... ok test cap::table::tests::cap_derive_on_full_table_returns_caps_exhausted ... ok test cap::table::tests::cap_derive_rejects_widened_rights ... ok test cap::table::tests::cap_derive_without_derive_right_fails ... ok test cap::table::tests::cap_drop_on_interior_node_returns_has_children ... ok test cap::table::tests::cap_revoke_cascades_depth_three ... ok test cap::table::tests::cap_revoke_clears_references_object ... ok test cap::table::tests::cap_revoke_on_leaf_is_a_noop ... ok test cap::table::tests::cap_revoke_on_stale_handle_fails ... ok test cap::table::tests::cap_revoke_removes_only_descendants ... ok test cap::table::tests::cap_revoke_without_revoke_right_fails ... ok test cap::table::tests::cap_take_middle_sibling_preserves_list_integrity ... ok test cap::table::tests::cap_take_on_node_with_children_fails ... ok test cap::table::tests::cap_take_returns_capability_and_invalidates_handle ... ok test cap::table::tests::cap_take_slot_reusable_with_bumped_generation ... ok test cap::table::tests::cap_take_stale_handle_fails ... ok test cap::table::tests::copy_of_a_child_shares_parent ... ok test cap::table::tests::drop_first_child_updates_parent_first_child_pointer ... ok test cap::table::tests::drop_invalidates_handle ... ok test cap::table::tests::drop_middle_sibling_preserves_list_integrity ... ok test cap::table::tests::drop_peer_does_not_affect_other_peer ... ok test cap::table::tests::drop_twice_returns_invalid_handle ... ok test cap::table::tests::freed_slot_is_reused_with_bumped_generation ... ok test cap::table::tests::is_full_transitions ... ok test cap::table::tests::lookup_on_stale_handle_returns_invalid_handle ... ok test cap::table::tests::new_table_can_accept_one_root ... ok test cap::table::tests::references_object_sees_live_caps_only ... ok test cap::table::tests::slot_entry_size_matches_adr_0023 ... ok test cap::table::tests::table_exhaustion_returns_caps_exhausted ... ok test cap::tests::capobject_debug_redacts_handle_but_shows_kind ... ok test cap::tests::debug_redacts_named_object_but_keeps_rights ... ok test ipc::tests::blocked_sender_delivered_on_subsequent_recv ... ok test ipc::tests::cancel_recv_clears_recv_waiting_back_to_idle ... ok test ipc::tests::cancel_recv_is_idempotent ... ok test ipc::tests::cancel_recv_on_idle_is_noop ... ok test ipc::tests::cancel_recv_on_recv_complete_does_not_drop_message_or_cap ... ok test ipc::tests::cancel_recv_on_send_pending_does_not_drop_message ... ok test ipc::tests::cancel_recv_to_destroyed_endpoint_returns_stale_handle ... ok test ipc::tests::cancel_recv_with_wrong_object_kind_returns_wrong_object_kind ... ok test ipc::tests::cancel_recv_without_recv_right_fails ... ok test ipc::tests::notify_sets_bits ... ok test ipc::tests::notify_with_stale_handle_after_slot_reuse_fails ... ok test ipc::tests::notify_with_wrong_object_kind_returns_wrong_object_kind ... ok test ipc::tests::notify_without_notify_right_fails ... ok test ipc::tests::receiver_first_delivers_on_send ... ok test ipc::tests::receiver_first_then_send_with_cap ... ok test ipc::tests::recv_with_full_table_preserves_pending_cap ... ok test ipc::tests::recv_with_wrong_object_kind_returns_wrong_object_kind ... ok test ipc::tests::recv_without_recv_right_fails ... ok test ipc::tests::second_recv_when_waiting_fails ... ok test ipc::tests::second_send_when_pending_fails ... ok test ipc::tests::send_to_destroyed_endpoint_returns_stale_handle ... ok test ipc::tests::send_transfers_cap_atomically ... ok test ipc::tests::send_with_bad_transfer_cap_preserves_recv_waiting ... ok test ipc::tests::send_with_dropped_cap_handle_returns_stale_handle ... ok test ipc::tests::send_with_wrong_object_kind_returns_wrong_object_kind ... ok test ipc::tests::send_without_send_right_fails ... ok test ipc::tests::send_without_transfer_right_on_xfer_cap_fails ... ok test ipc::tests::sender_first_delivers_on_recv ... ok test ipc::tests::stale_queue_state_reset_on_slot_reuse ... ok test ipc::tests::stale_recv_waiting_resets_silently ... ok test ipc::tests::stale_send_pending_without_cap_resets_silently ... ok test mm::address_space::tests::arena_alloc_returns_distinct_handles ... ok test mm::address_space::tests::arena_full_returns_arena_full_error ... ok test ipc::tests::stale_send_pending_with_some_cap_panics_in_debug - should panic ... ok test mm::address_space::tests::arena_get_with_stale_handle_returns_none ... ok test mm::address_space::tests::cap_create_address_space_consumes_one_pmm_frame_and_mints_cap ... ok test mm::address_space::tests::cap_create_address_space_rejects_missing_derive ... ok test mm::address_space::tests::cap_create_address_space_rejects_widened_rights ... ok test mm::address_space::tests::cap_create_address_space_rejects_wrong_parent_kind ... ok test mm::address_space::tests::cap_create_address_space_returns_out_of_frames_on_pmm_exhaustion ... ok test mm::address_space::tests::cap_create_rejects_too_deep_parent_without_consuming_pmm ... ok test mm::address_space::tests::cap_create_uses_cap_derive_so_revoke_parent_invalidates_child ... ok test mm::address_space::tests::cap_map_installs_mapping_and_flushes_tlb ... ok test mm::address_space::tests::cap_map_propagates_block_mapped_and_leaves_no_mapping ... ok test mm::address_space::tests::cap_map_propagates_intermediate_out_of_frames_and_does_not_consume_pa ... ok test mm::address_space::tests::cap_map_rejects_wrong_kind ... ok test mm::address_space::tests::cap_map_wraps_mmu_error_passthrough ... ok test mm::address_space::tests::cap_unmap_returns_unmapped_frame ... ok test mm::address_space::tests::cap_unmap_wraps_mmu_error_passthrough ... ok test mm::address_space::tests::destroy_with_stale_handle_returns_stale_handle_error ... ok test mm::address_space::tests::inner_accessors_provide_borrow_and_borrow_mut ... ok test mm::address_space::tests::resolve_address_space_cap_returns_handle_on_correct_kind ... ok test mm::address_space::tests::resolve_address_space_cap_returns_wrong_kind_on_endpoint_cap ... ok test mm::address_space::tests::wrap_bootstrap_returns_address_space_with_root ... ok test mm::pmm::tests::alloc_frame_implements_frame_provider ... ok test mm::pmm::tests::alloc_frame_recovers_after_free_under_exhaustion ... ok test mm::pmm::tests::alloc_frame_returns_none_when_exhausted ... ok test mm::pmm::tests::alloc_frame_returns_first_free_and_zeroes_payload ... ok test mm::pmm::tests::could_yield_pa_overlapping_interval_equals_perframe ... ok test mm::pmm::tests::extent_4f_fixture_sanity ... ok test mm::pmm::tests::could_yield_pa_overlapping_treats_allocated_frame_as_yieldable ... ok test mm::pmm::tests::free_frame_rejects_double_free_and_reserved ... ok test mm::pmm::tests::free_frame_clears_bit_and_rewinds_hint ... ok test mm::pmm::tests::free_frame_rejects_pa_outside_extent ... ok test mm::pmm::tests::free_frame_reserved_check_iterates_only_populated_slots ... ok test mm::pmm::tests::new_marks_reserved_ranges_and_initialises_counters ... ok test mm::pmm::tests::new_rejects_extent_larger_than_bitmap ... ok test mm::pmm::tests::new_rejects_overlapping_reserved_ranges ... ok test mm::pmm::tests::new_rejects_reserved_range_outside_extent ... ok test mm::pmm::tests::new_rejects_too_many_reserved_ranges ... ok test mm::pmm::tests::new_rejects_unaligned_extent ... ok test mm::pmm::tests::stats_parity_with_bitmap_bit_count ... ok test obj::arena::tests::allocate_and_get_round_trip ... ok test obj::arena::tests::empty_capacity_arena_has_no_free_slot ... ok test obj::arena::tests::exhaustion_returns_none ... ok test obj::arena::tests::free_invalidates_id ... ok test obj::arena::tests::free_middle_then_allocate_reuses_that_slot ... ok test obj::arena::tests::free_then_allocate_bumps_generation ... ok test obj::arena::tests::get_mut_permits_mutation ... ok test obj::endpoint::tests::create_destroy_round_trip ... ok test obj::notification::tests::destroy_invalidates_handle ... ok test obj::notification::tests::set_and_consume_round_trip ... ok test obj::task::tests::address_space_handle_round_trips ... ok test obj::task::tests::arena_exhaustion_returns_arena_full ... ok test obj::task::tests::create_then_get_round_trip ... ok test obj::task::tests::destroy_invalidates_handle ... ok test obj::task_loader::tests::accepts_image_base_va_exactly_at_userspace_va_limit_minus_span ... ok test obj::task_loader::tests::accepts_image_disjoint_from_pmm_extent ... ok test obj::task_loader::tests::intermediate_frame_count_8mib_image_one_stack_page_crosses_five_l2 ... ok test obj::task_loader::tests::frame_budget_includes_root_plus_intermediates ... ok test obj::task_loader::tests::intermediate_frame_count_l1_boundary_crossing ... ok test obj::task_loader::tests::intermediate_frame_count_minimal_single_l2_slot ... ok test obj::task_loader::tests::intermediate_frame_count_saturated_total_pages ... ok test obj::task_loader::tests::intermediate_frame_count_zero_span_defensive ... ok test obj::task_loader::tests::load_error_frame_budget_exceeded_fields_round_trip ... ok test obj::task_loader::tests::load_error_variants_are_distinct ... ok test obj::task_loader::tests::load_error_variants_pattern_match_exhaustively ... ok test obj::task_loader::tests::loaded_image_distinguishes_different_field_values ... ok test obj::task_loader::tests::loaded_image_struct_literal_round_trips_through_copy_and_eq ... ok test obj::task_loader::tests::maps_stack_with_user_write_flags ... ok test obj::task_loader::tests::maps_image_pages_with_user_execute_flags ... ok test obj::task_loader::tests::missing_derive_surfaces_via_address_space_creation_failed ... ok test obj::task_loader::tests::rejects_empty_image ... ok test obj::task_loader::tests::mints_address_space_cap_with_requested_non_empty_rights ... ok test obj::task_loader::tests::rejects_image_base_va_past_userspace_va_limit ... ok test obj::task_loader::tests::rejects_image_base_va_saturating_overflow ... ok test obj::task_loader::tests::rejects_invalid_parent_cap_lookup ... ok test obj::task_loader::tests::rejects_invalid_parent_cap_wrong_kind ... ok test obj::task_loader::tests::rejects_misaligned_image_base_va_with_pmm_byte_stable ... ok test obj::task_loader::tests::rejects_when_image_overlaps_allocatable_memory ... ok test obj::task_loader::tests::rejects_when_pmm_budget_exceeded ... ok test obj::task_loader::tests::rejects_zero_stack ... ok test obj::task_loader::tests::returns_loaded_image_with_correct_metadata ... ok test obj::task_loader::tests::rollback_helper_zero_pages_only_drops_cap ... ok test obj::task_loader::tests::rolls_back_on_block_mapped_mid_image_loop ... ok test obj::task_loader::tests::rolls_back_on_cap_map_failure_mid_image_loop ... ok test obj::task_loader::tests::rolls_back_on_cap_map_failure_mid_stack_loop ... ok test obj::task_loader::tests::rolls_back_on_intermediate_out_of_frames_mid_image_loop ... ok test obj::task_loader::tests::rolls_back_on_pmm_exhausted_mid_image_loop ... ok test obj::task_loader::tests::tail_zeroing_on_partial_last_page ... ok test obj::task_loader::tests::stack_top_va_is_one_past_highest_mapped ... ok test obj::task_loader::tests::task_create_from_image_mints_task_cap_bound_to_the_loaded_as ... ok test obj::task_loader::tests::task_create_from_image_rejects_stale_as_cap ... ok test obj::task_loader::tests::task_create_from_image_rejects_when_task_arena_full ... ok test obj::task_loader::tests::task_create_from_image_rejects_wrong_kind_as_cap ... ok test obj::task_loader::tests::task_create_from_image_rolls_back_task_on_cap_table_exhausted ... ok test obj::task_loader::tests::va_range_preflight_runs_before_frame_budget ... ok test obj::task_loader::tests::widened_rights_surfaces_via_address_space_creation_failed ... ok test sched::tests::add_task_sets_ready_state_and_stores_handle ... ok test sched::tests::add_user_task_seeds_el0_context_and_enqueues_ready ... ok test sched::tests::address_space_activation_target_pure_function ... ok test sched::tests::current_accessors_resolve_running_task_bindings_or_none ... ok test sched::tests::dispatcher_picks_idle_only_when_ready_queue_empty ... ok test sched::tests::ipc_recv_and_yield_deadlock_rolls_back_endpoint_state ... ok test sched::tests::ipc_recv_and_yield_resume_pending_returns_typed_err ... ok test sched::tests::ipc_recv_and_yield_returns_deadlock_when_ready_queue_empty ... ok test sched::tests::ipc_recv_and_yield_with_idle_as_current_returns_deadlock ... ok test sched::tests::ipc_recv_and_yield_with_no_current_task_leaves_endpoint_idle ... ok test sched::tests::ipc_send_and_yield_delivered_unblocks_receiver_and_yields ... ok test sched::tests::ipc_send_and_yield_enqueued_does_not_yield ... ok test sched::tests::ipc_send_and_yield_send_error_preserves_scheduler_state ... ok test sched::tests::queue_empty_dequeue_is_none ... ok test sched::tests::queue_enqueue_dequeue_fifo_order ... ok test sched::tests::queue_full_returns_error ... ok test sched::tests::queue_len_and_is_empty ... ok test sched::tests::queue_wraps_around ... ok test sched::tests::register_idle_stores_handle_in_idle_slot_and_not_in_ready_queue ... ok test sched::tests::start_prelude_dispatches_head_and_marks_ready ... ok test sched::tests::task_state_variants_are_distinct ... ok test sched::tests::unblock_after_yield_dispatches_unblocked_receiver_not_idle ... ok test sched::tests::start_prelude_panics_on_empty_ready_queue - should panic ... ok test sched::tests::unblock_receiver_on_moves_task_to_ready ... ok test sched::tests::unblock_receiver_on_wrong_ep_is_noop ... ok test sched::tests::yield_now_activates_when_tasks_differ_in_address_space ... ok test sched::tests::yield_now_masks_irqs_across_switch_and_restores_on_return ... ok test sched::tests::yield_now_skips_activation_when_tasks_share_address_space ... ok test sched::tests::yield_now_switches_context_and_updates_current ... ok test sched::tests::yield_now_with_no_current_returns_error ... ok test syscall::abi::tests::as_u64_round_trips_recognised_numbers ... ok test syscall::abi::tests::console_write_is_a_syscall_in_debug_builds ... ok test syscall::abi::tests::decode_maps_v1_numbers ... ok test syscall::abi::tests::decode_out_of_range_is_none ... ok test syscall::abi::tests::decode_send_message_reads_label_and_params ... ok test syscall::abi::tests::decode_zero_is_reserved_invalid ... ok test syscall::abi::tests::none_round_trips_through_null_sentinel ... ok test syscall::abi::tests::recv_pending_packs_pending_code_and_zeroes_rest ... ok test syscall::abi::tests::recv_received_packs_message_and_cap ... ok test syscall::abi::tests::recv_received_without_cap_packs_null_sentinel ... ok test syscall::abi::tests::required_handle_decode_ignores_sentinel_semantics ... ok test syscall::abi::tests::send_outcome_codes ... ok test syscall::abi::tests::some_handle_round_trips ... ok test syscall::abi::tests::syscall_return_with_payload_sets_indexed_word ... ok test syscall::dispatch::tests::bad_number_zero_returns_bad_syscall_number_touching_nothing ... ok test syscall::dispatch::tests::console_write_cap_ok_but_non_user_page_emits_nothing ... ok test syscall::dispatch::tests::console_write_emits_buffer_and_returns_byte_count ... ok test syscall::dispatch::tests::console_write_exactly_one_chunk_emits_all_bytes ... ok test syscall::dispatch::tests::console_write_multipage_second_page_unmapped_emits_nothing ... ok test syscall::dispatch::tests::console_write_out_of_range_buffer_faults_without_output ... ok test syscall::dispatch::tests::console_write_spanning_multiple_chunks_emits_all_bytes ... ok test syscall::dispatch::tests::console_write_with_no_cap_returns_cap_invalid_handle_no_output ... ok test syscall::dispatch::tests::console_write_with_wrong_kind_cap_returns_cap_wrong_kind ... ok test syscall::dispatch::tests::console_write_without_write_right_returns_insufficient_rights ... ok test syscall::dispatch::tests::incomplete_binding_context_fails_closed_on_both_planes ... ok test syscall::dispatch::tests::out_of_range_number_returns_bad_syscall_number ... ok test syscall::dispatch::tests::recv_of_enqueued_message_unpacks_into_registers ... ok test syscall::dispatch::tests::recv_with_no_sender_returns_pending_packing ... ok test syscall::dispatch::tests::send_with_no_receiver_enqueues_and_returns_ok ... ok test syscall::dispatch::tests::send_with_stale_transfer_handle_returns_invalid_transfer_cap ... ok test syscall::dispatch::tests::send_with_transfer_cap_then_recv_returns_cap_in_x6 ... ok test syscall::dispatch::tests::send_without_send_right_returns_typed_ipc_missing_right ... ok test syscall::dispatch::tests::task_exit_routes_to_terminate_with_code ... ok test syscall::dispatch::tests::task_exit_with_no_current_task_fails_closed ... ok test syscall::dispatch::tests::task_yield_routes_to_reschedule ... ok test syscall::dispatch::tests::task_yield_with_no_current_task_fails_closed ... ok test syscall::error::tests::cap_and_ipc_status_blocks_do_not_collide ... ok test syscall::error::tests::cap_error_from_round_trips_and_encodes_in_cap_block ... ok test syscall::error::tests::ipc_error_from_round_trips_and_encodes_in_ipc_block ... ok test syscall::error::tests::ok_status_is_zero_and_no_error_encodes_to_it ... ok test syscall::error::tests::top_level_status_codes_are_stable ... ok test syscall::user_access::tests::copy_from_user_block_mapped_page_faults ... ok test syscall::user_access::tests::copy_from_user_faults_on_unmapped_page ... ok test syscall::user_access::tests::copy_from_user_multipage_second_page_unmapped_copies_nothing ... ok test syscall::user_access::tests::copy_from_user_out_of_window_faults_before_translate ... ok test syscall::user_access::tests::copy_from_user_overrun_past_window_end_faults ... ok test syscall::user_access::tests::copy_from_user_range_ending_in_top_page_does_not_spuriously_fault ... ok test syscall::user_access::tests::copy_from_user_rejects_in_window_non_user_page ... ok test syscall::user_access::tests::copy_from_user_spanning_two_pages_copies_all ... ok test syscall::user_access::tests::copy_from_user_translates_and_copies_a_user_page ... ok test syscall::user_access::tests::copy_to_user_in_range_moves_bytes ... ok test syscall::user_access::tests::copy_to_user_rejects_read_only_user_page ... ok test syscall::user_access::tests::validate_exact_window_fit_is_ok ... ok test syscall::user_access::tests::wrapping_range_faults ... ok test syscall::user_access::tests::zero_length_copy_is_ok_even_for_unmapped_pointer ... ok test result: ok. 258 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s running 58 tests test context_switch::tests::context_switch_marks_current_and_counts ... ok test console::tests::default_fake_console_is_empty ... ok test console::tests::captures_successive_byte_writes ... ok test console::tests::fmt_writer_produces_formatted_output ... ok test context_switch::tests::default_context_is_uninitialized_and_unswitched ... ok test context_switch::tests::init_context_clears_prior_user_markers_on_reuse ... ok test context_switch::tests::init_context_records_entry_stack_and_marks_initialized ... ok test context_switch::tests::init_user_context_records_user_entry_sp_kernel_stack_and_marks_is_user ... ok test cpu::tests::default_cpu_reports_core_zero_with_irqs_enabled ... ok test cpu::tests::disable_irqs_masks_and_returns_previous_state ... ok test cpu::tests::instruction_barrier_increments_count ... ok test cpu::tests::irq_guard_enters_and_exits_critical_section ... ok test cpu::tests::irq_state_uses_daif_polarity_zero_means_enabled ... ok test cpu::tests::nested_irq_guards_restore_outer_state ... ok test cpu::tests::wait_for_interrupt_increments_count ... ok test cpu::tests::with_core_id_sets_reported_id ... ok test irq_controller::tests::ack_eoi_cycle_leaves_clean_state ... ok test irq_controller::tests::acknowledge_returns_none_when_queue_empty ... ok test irq_controller::tests::acknowledge_returns_pending_fifo ... ok test irq_controller::tests::disable_removes_enabled_state ... ok test irq_controller::tests::disabled_irq_can_still_be_injected_for_test_purposes ... ok test irq_controller::tests::enable_is_idempotent ... ok test irq_controller::tests::disable_panics_on_out_of_range_irq - should panic ... ok test irq_controller::tests::enable_marks_line_as_enabled ... ok test irq_controller::tests::end_of_interrupt_records_irq_in_order ... ok test irq_controller::tests::enable_panics_on_out_of_range_irq - should panic ... ok test mmu::tests::activate_records_root ... ok test mmu::tests::block_mapped_mmu_delegates_unblocked_addresses ... ok test mmu::tests::block_mapped_mmu_injects_block_mapped_on_map_and_unmap ... ok test mmu::tests::block_mapped_mmu_translate_injects_block_mapped ... ok test mmu::tests::bulk_map_with_ignore_then_invalidate_tlb_all ... ok test mmu::tests::create_address_space_stores_root ... ok test mmu::tests::double_map_returns_already_mapped ... ok test mmu::tests::fake_translate_returns_mapping_and_aligns_interior_offset ... ok test mmu::tests::fake_translate_unmapped_is_not_mapped ... ok test mmu::tests::map_rejects_device_plus_execute ... ok test mmu::tests::map_rejects_unaligned_va ... ok test mmu::tests::map_returns_token_with_mapped_va ... ok test mmu::tests::map_unmap_round_trip ... ok test mmu::tests::mapper_flush_carries_virt_addr ... ok test mmu::tests::mapper_flush_flush_invokes_invalidate_tlb_address ... ok test mmu::tests::mapper_flush_ignore_is_documented_noop ... ok test mmu::tests::mapping_flags_difference_clears_bits ... ok test mmu::tests::mapping_flags_union_and_contains ... ok test mmu::tests::out_of_frames_mmu_maps_while_frames_available ... ok test mmu::tests::out_of_frames_mmu_returns_out_of_frames_when_provider_empty ... ok test mmu::tests::phys_frame_rejects_unaligned ... ok test mmu::tests::tlb_invalidations_recorded_in_order ... ok test mmu::tests::unmap_missing_returns_not_mapped ... ok test mmu::tests::unmap_rejects_unaligned_va ... ok test mmu::tests::unmap_returns_token_with_unmapped_va_and_frame ... ok test timer::tests::advance_moves_clock_forward ... ok test timer::tests::arm_deadline_records_value ... ok test timer::tests::arm_deadline_replaces_previous ... ok test timer::tests::cancel_clears_deadline_and_counts ... ok test timer::tests::default_has_one_nanosecond_resolution ... ok test timer::tests::new_starts_at_zero_with_given_resolution ... ok test timer::tests::set_now_overrides_clock ... ok test result: ok. 58 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s running 2 tests test hal/src/console.rs - console::FmtWriter (line 51) ... ignored test hal/src/cpu.rs - cpu::IrqGuard (line 108) ... ignored test result: ok. 0 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 0.00s running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s running 3 tests test test-hal/src/context_switch.rs - context_switch::FakeContextSwitch (line 60) ... ok test test-hal/src/mmu.rs - mmu::OutOfFramesMmu (line 312) ... ok test test-hal/src/mmu.rs - mmu::BlockMappedMmu (line 446) ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.04s (default-members, excludes userland), missing the --workspace test-link path.) Fix: hello/build.rs applies -T hello.ld only when CARGO_CFG_TARGET_ARCH == aarch64. Host builds (coverage / check / miri / ) link the hello bin as an ordinary std stub (no script); the aarch64 build still gets the script + its ASSERTs (verified: build-userland.sh still emits the 117-byte raw-flat image). Also excluded the userland crates from the llvm-cov job (coverage of aarch64-only stubs is meaningless). Verified: running 46 tests test mmu::vmsav8::tests::block_descriptor_drops_low_bits_for_unaligned_pa ... ok test mmu::vmsav8::tests::block_descriptor_v1_device_block_encoding ... ok test mmu::vmsav8::tests::block_descriptor_v1_kernel_ram_block_encoding ... ok test mmu::vmsav8::tests::descriptor_bits_to_flags_is_lock_shut ... ok test mmu::vmsav8::tests::device_flag_picks_device_attr_index ... ok test mmu::vmsav8::tests::empty_flags_kernel_ro_normal_no_execute_global_inverted ... ok test mmu::vmsav8::tests::descriptor_bits_to_flags_round_trips_valid_flags ... ok test mmu::vmsav8::tests::flags_to_descriptor_bits_ignores_bits_above_four ... ok test mmu::vmsav8::tests::global_flag_clears_ng_bit ... ok test mmu::vmsav8::tests::mair_value_attr0_device_attr1_normal_others_zero ... ok test mmu::vmsav8::tests::page_descriptor_drops_low_bits_for_unaligned_pa ... ok test mmu::vmsav8::tests::page_descriptor_v1_kernel_rw_page_encoding ... ok test mmu::vmsav8::tests::sctlr_mmu_enable_mask_sets_m_c_i_only ... ok test mmu::vmsav8::tests::table_descriptor_carries_valid_and_table_bits_and_address ... ok test mmu::vmsav8::tests::table_descriptor_drops_low_bits_for_unaligned_address ... ok test mmu::vmsav8::tests::tcr_high_half_clears_only_epd1 ... ok test mmu::vmsav8::tests::tcr_value_carries_t0sz_16_and_ips_2_and_epd1_set ... ok test mmu::vmsav8::tests::user_execute_yields_user_ro_pxn_one_uxn_zero ... ok test mmu::vmsav8::tests::user_write_yields_user_rw_no_execute ... ok test mmu::vmsav8::tests::write_alone_yields_kernel_rw_no_execute ... ok test mmu::vmsav8::tests::write_plus_execute_yields_kernel_rwx_uxn_pxn_zero ... ok test timer::tests::ns_to_ticks_one_second_yields_frequency_at_any_freq ... ok test timer::tests::ns_to_ticks_round_trips_against_ticks_to_ns_at_qemu_frequency ... ok test timer::tests::ns_to_ticks_rounds_up_on_subtick ... ok test timer::tests::ns_to_ticks_saturates_at_u64_max ... ok test timer::tests::ns_to_ticks_panics_on_zero_frequency - should panic ... ok test timer::tests::ns_to_ticks_zero_ns_is_zero ... ok test timer::tests::resolution_clamps_to_one_above_2ghz ... ok test timer::tests::resolution_const_fn_works_in_const_context ... ok test timer::tests::resolution_floor_vs_round_difference_documented ... ok test timer::tests::resolution_one_gigahertz_is_one_ns ... ok test timer::tests::resolution_qemu_virt_is_16_ns ... ok test timer::tests::resolution_ns_for_freq_panics_on_zero_frequency - should panic ... ok test timer::tests::resolution_round_to_nearest_for_non_divisor ... ok test timer::tests::resolution_two_ghz_is_one_ns_exactly ... ok test timer::tests::ticks_to_ns_const_fn_works_in_const_context ... ok test timer::tests::ticks_to_ns_high_frequency_one_gigahertz ... ok test timer::tests::ticks_to_ns_is_monotonic_across_frequencies ... ok test timer::tests::ticks_to_ns_no_silent_wrap_at_64bit_boundary ... ok test timer::tests::ticks_to_ns_pi3_class_non_divisor ... ok test timer::tests::ticks_to_ns_panics_on_zero_frequency - should panic ... ok test timer::tests::ticks_to_ns_plateaus_at_u64_max_after_saturation ... ok test timer::tests::ticks_to_ns_qemu_virt_one_second ... ok test timer::tests::ticks_to_ns_qemu_virt_single_tick ... ok test timer::tests::ticks_to_ns_saturates_at_u64_max ... ok test timer::tests::ticks_to_ns_zero_count_is_zero ... ok test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s running 258 tests test cap::rights::tests::bitor_assign_adds_bits ... ok test cap::rights::tests::difference_clears_bits ... ok test cap::rights::tests::empty_contains_empty_but_nothing_else ... ok test cap::rights::tests::from_raw_and_raw_round_trip ... ok test cap::rights::tests::from_raw_masks_unknown_bits ... ok test cap::rights::tests::intersection_narrows ... ok test cap::rights::tests::union_and_contains ... ok test cap::table::tests::cap_copy_narrows_rights ... ok test cap::table::tests::cap_copy_of_root_then_revoke_root_leaves_peer_alive ... ok test cap::table::tests::cap_copy_on_stale_handle_returns_invalid_handle ... ok test cap::table::tests::cap_copy_rejects_widened_rights ... ok test cap::table::tests::cap_copy_with_same_rights_succeeds ... ok test cap::table::tests::cap_copy_without_duplicate_right_fails ... ok test cap::table::tests::cap_derive_creates_child_with_narrower_rights ... ok test cap::table::tests::cap_derive_enforces_depth_cap ... ok test cap::table::tests::cap_derive_on_full_table_returns_caps_exhausted ... ok test cap::table::tests::cap_derive_rejects_widened_rights ... ok test cap::table::tests::cap_derive_without_derive_right_fails ... ok test cap::table::tests::cap_drop_on_interior_node_returns_has_children ... ok test cap::table::tests::cap_revoke_cascades_depth_three ... ok test cap::table::tests::cap_revoke_clears_references_object ... ok test cap::table::tests::cap_revoke_on_leaf_is_a_noop ... ok test cap::table::tests::cap_revoke_on_stale_handle_fails ... ok test cap::table::tests::cap_revoke_removes_only_descendants ... ok test cap::table::tests::cap_revoke_without_revoke_right_fails ... ok test cap::table::tests::cap_take_middle_sibling_preserves_list_integrity ... ok test cap::table::tests::cap_take_on_node_with_children_fails ... ok test cap::table::tests::cap_take_returns_capability_and_invalidates_handle ... ok test cap::table::tests::cap_take_slot_reusable_with_bumped_generation ... ok test cap::table::tests::cap_take_stale_handle_fails ... ok test cap::table::tests::copy_of_a_child_shares_parent ... ok test cap::table::tests::drop_first_child_updates_parent_first_child_pointer ... ok test cap::table::tests::drop_invalidates_handle ... ok test cap::table::tests::drop_middle_sibling_preserves_list_integrity ... ok test cap::table::tests::drop_peer_does_not_affect_other_peer ... ok test cap::table::tests::drop_twice_returns_invalid_handle ... ok test cap::table::tests::freed_slot_is_reused_with_bumped_generation ... ok test cap::table::tests::is_full_transitions ... ok test cap::table::tests::lookup_on_stale_handle_returns_invalid_handle ... ok test cap::table::tests::new_table_can_accept_one_root ... ok test cap::table::tests::references_object_sees_live_caps_only ... ok test cap::table::tests::slot_entry_size_matches_adr_0023 ... ok test cap::table::tests::table_exhaustion_returns_caps_exhausted ... ok test cap::tests::capobject_debug_redacts_handle_but_shows_kind ... ok test cap::tests::debug_redacts_named_object_but_keeps_rights ... ok test ipc::tests::blocked_sender_delivered_on_subsequent_recv ... ok test ipc::tests::cancel_recv_clears_recv_waiting_back_to_idle ... ok test ipc::tests::cancel_recv_is_idempotent ... ok test ipc::tests::cancel_recv_on_idle_is_noop ... ok test ipc::tests::cancel_recv_on_recv_complete_does_not_drop_message_or_cap ... ok test ipc::tests::cancel_recv_on_send_pending_does_not_drop_message ... ok test ipc::tests::cancel_recv_to_destroyed_endpoint_returns_stale_handle ... ok test ipc::tests::cancel_recv_with_wrong_object_kind_returns_wrong_object_kind ... ok test ipc::tests::cancel_recv_without_recv_right_fails ... ok test ipc::tests::notify_sets_bits ... ok test ipc::tests::notify_with_stale_handle_after_slot_reuse_fails ... ok test ipc::tests::notify_with_wrong_object_kind_returns_wrong_object_kind ... ok test ipc::tests::notify_without_notify_right_fails ... ok test ipc::tests::receiver_first_delivers_on_send ... ok test ipc::tests::receiver_first_then_send_with_cap ... ok test ipc::tests::recv_with_full_table_preserves_pending_cap ... ok test ipc::tests::recv_with_wrong_object_kind_returns_wrong_object_kind ... ok test ipc::tests::recv_without_recv_right_fails ... ok test ipc::tests::second_recv_when_waiting_fails ... ok test ipc::tests::second_send_when_pending_fails ... ok test ipc::tests::send_to_destroyed_endpoint_returns_stale_handle ... ok test ipc::tests::send_transfers_cap_atomically ... ok test ipc::tests::send_with_bad_transfer_cap_preserves_recv_waiting ... ok test ipc::tests::send_with_dropped_cap_handle_returns_stale_handle ... ok test ipc::tests::send_with_wrong_object_kind_returns_wrong_object_kind ... ok test ipc::tests::send_without_send_right_fails ... ok test ipc::tests::send_without_transfer_right_on_xfer_cap_fails ... ok test ipc::tests::sender_first_delivers_on_recv ... ok test ipc::tests::stale_queue_state_reset_on_slot_reuse ... ok test ipc::tests::stale_recv_waiting_resets_silently ... ok test ipc::tests::stale_send_pending_without_cap_resets_silently ... ok test mm::address_space::tests::arena_alloc_returns_distinct_handles ... ok test mm::address_space::tests::arena_full_returns_arena_full_error ... ok test ipc::tests::stale_send_pending_with_some_cap_panics_in_debug - should panic ... ok test mm::address_space::tests::arena_get_with_stale_handle_returns_none ... ok test mm::address_space::tests::cap_create_address_space_consumes_one_pmm_frame_and_mints_cap ... ok test mm::address_space::tests::cap_create_address_space_rejects_missing_derive ... ok test mm::address_space::tests::cap_create_address_space_rejects_widened_rights ... ok test mm::address_space::tests::cap_create_address_space_rejects_wrong_parent_kind ... ok test mm::address_space::tests::cap_create_address_space_returns_out_of_frames_on_pmm_exhaustion ... ok test mm::address_space::tests::cap_create_rejects_too_deep_parent_without_consuming_pmm ... ok test mm::address_space::tests::cap_create_uses_cap_derive_so_revoke_parent_invalidates_child ... ok test mm::address_space::tests::cap_map_installs_mapping_and_flushes_tlb ... ok test mm::address_space::tests::cap_map_propagates_block_mapped_and_leaves_no_mapping ... ok test mm::address_space::tests::cap_map_propagates_intermediate_out_of_frames_and_does_not_consume_pa ... ok test mm::address_space::tests::cap_map_rejects_wrong_kind ... ok test mm::address_space::tests::cap_map_wraps_mmu_error_passthrough ... ok test mm::address_space::tests::cap_unmap_returns_unmapped_frame ... ok test mm::address_space::tests::cap_unmap_wraps_mmu_error_passthrough ... ok test mm::address_space::tests::destroy_with_stale_handle_returns_stale_handle_error ... ok test mm::address_space::tests::inner_accessors_provide_borrow_and_borrow_mut ... ok test mm::address_space::tests::resolve_address_space_cap_returns_handle_on_correct_kind ... ok test mm::address_space::tests::resolve_address_space_cap_returns_wrong_kind_on_endpoint_cap ... ok test mm::address_space::tests::wrap_bootstrap_returns_address_space_with_root ... ok test mm::pmm::tests::alloc_frame_implements_frame_provider ... ok test mm::pmm::tests::alloc_frame_recovers_after_free_under_exhaustion ... ok test mm::pmm::tests::alloc_frame_returns_none_when_exhausted ... ok test mm::pmm::tests::alloc_frame_returns_first_free_and_zeroes_payload ... ok test mm::pmm::tests::could_yield_pa_overlapping_interval_equals_perframe ... ok test mm::pmm::tests::extent_4f_fixture_sanity ... ok test mm::pmm::tests::could_yield_pa_overlapping_treats_allocated_frame_as_yieldable ... ok test mm::pmm::tests::free_frame_rejects_double_free_and_reserved ... ok test mm::pmm::tests::free_frame_clears_bit_and_rewinds_hint ... ok test mm::pmm::tests::free_frame_rejects_pa_outside_extent ... ok test mm::pmm::tests::free_frame_reserved_check_iterates_only_populated_slots ... ok test mm::pmm::tests::new_marks_reserved_ranges_and_initialises_counters ... ok test mm::pmm::tests::new_rejects_extent_larger_than_bitmap ... ok test mm::pmm::tests::new_rejects_overlapping_reserved_ranges ... ok test mm::pmm::tests::new_rejects_reserved_range_outside_extent ... ok test mm::pmm::tests::new_rejects_too_many_reserved_ranges ... ok test mm::pmm::tests::new_rejects_unaligned_extent ... ok test mm::pmm::tests::stats_parity_with_bitmap_bit_count ... ok test obj::arena::tests::allocate_and_get_round_trip ... ok test obj::arena::tests::empty_capacity_arena_has_no_free_slot ... ok test obj::arena::tests::exhaustion_returns_none ... ok test obj::arena::tests::free_invalidates_id ... ok test obj::arena::tests::free_middle_then_allocate_reuses_that_slot ... ok test obj::arena::tests::free_then_allocate_bumps_generation ... ok test obj::arena::tests::get_mut_permits_mutation ... ok test obj::endpoint::tests::create_destroy_round_trip ... ok test obj::notification::tests::destroy_invalidates_handle ... ok test obj::notification::tests::set_and_consume_round_trip ... ok test obj::task::tests::address_space_handle_round_trips ... ok test obj::task::tests::arena_exhaustion_returns_arena_full ... ok test obj::task::tests::create_then_get_round_trip ... ok test obj::task::tests::destroy_invalidates_handle ... ok test obj::task_loader::tests::accepts_image_base_va_exactly_at_userspace_va_limit_minus_span ... ok test obj::task_loader::tests::accepts_image_disjoint_from_pmm_extent ... ok test obj::task_loader::tests::intermediate_frame_count_8mib_image_one_stack_page_crosses_five_l2 ... ok test obj::task_loader::tests::frame_budget_includes_root_plus_intermediates ... ok test obj::task_loader::tests::intermediate_frame_count_l1_boundary_crossing ... ok test obj::task_loader::tests::intermediate_frame_count_minimal_single_l2_slot ... ok test obj::task_loader::tests::intermediate_frame_count_saturated_total_pages ... ok test obj::task_loader::tests::intermediate_frame_count_zero_span_defensive ... ok test obj::task_loader::tests::load_error_frame_budget_exceeded_fields_round_trip ... ok test obj::task_loader::tests::load_error_variants_are_distinct ... ok test obj::task_loader::tests::load_error_variants_pattern_match_exhaustively ... ok test obj::task_loader::tests::loaded_image_distinguishes_different_field_values ... ok test obj::task_loader::tests::loaded_image_struct_literal_round_trips_through_copy_and_eq ... ok test obj::task_loader::tests::maps_stack_with_user_write_flags ... ok test obj::task_loader::tests::maps_image_pages_with_user_execute_flags ... ok test obj::task_loader::tests::missing_derive_surfaces_via_address_space_creation_failed ... ok test obj::task_loader::tests::rejects_empty_image ... ok test obj::task_loader::tests::rejects_image_base_va_past_userspace_va_limit ... ok test obj::task_loader::tests::mints_address_space_cap_with_requested_non_empty_rights ... ok test obj::task_loader::tests::rejects_image_base_va_saturating_overflow ... ok test obj::task_loader::tests::rejects_invalid_parent_cap_lookup ... ok test obj::task_loader::tests::rejects_invalid_parent_cap_wrong_kind ... ok test obj::task_loader::tests::rejects_misaligned_image_base_va_with_pmm_byte_stable ... ok test obj::task_loader::tests::rejects_when_image_overlaps_allocatable_memory ... ok test obj::task_loader::tests::rejects_when_pmm_budget_exceeded ... ok test obj::task_loader::tests::rejects_zero_stack ... ok test obj::task_loader::tests::returns_loaded_image_with_correct_metadata ... ok test obj::task_loader::tests::rollback_helper_zero_pages_only_drops_cap ... ok test obj::task_loader::tests::rolls_back_on_block_mapped_mid_image_loop ... ok test obj::task_loader::tests::rolls_back_on_cap_map_failure_mid_image_loop ... ok test obj::task_loader::tests::rolls_back_on_cap_map_failure_mid_stack_loop ... ok test obj::task_loader::tests::rolls_back_on_intermediate_out_of_frames_mid_image_loop ... ok test obj::task_loader::tests::rolls_back_on_pmm_exhausted_mid_image_loop ... ok test obj::task_loader::tests::stack_top_va_is_one_past_highest_mapped ... ok test obj::task_loader::tests::tail_zeroing_on_partial_last_page ... ok test obj::task_loader::tests::task_create_from_image_mints_task_cap_bound_to_the_loaded_as ... ok test obj::task_loader::tests::task_create_from_image_rejects_stale_as_cap ... ok test obj::task_loader::tests::task_create_from_image_rejects_when_task_arena_full ... ok test obj::task_loader::tests::task_create_from_image_rejects_wrong_kind_as_cap ... ok test obj::task_loader::tests::task_create_from_image_rolls_back_task_on_cap_table_exhausted ... ok test obj::task_loader::tests::va_range_preflight_runs_before_frame_budget ... ok test obj::task_loader::tests::widened_rights_surfaces_via_address_space_creation_failed ... ok test sched::tests::add_task_sets_ready_state_and_stores_handle ... ok test sched::tests::add_user_task_seeds_el0_context_and_enqueues_ready ... ok test sched::tests::address_space_activation_target_pure_function ... ok test sched::tests::current_accessors_resolve_running_task_bindings_or_none ... ok test sched::tests::dispatcher_picks_idle_only_when_ready_queue_empty ... ok test sched::tests::ipc_recv_and_yield_deadlock_rolls_back_endpoint_state ... ok test sched::tests::ipc_recv_and_yield_resume_pending_returns_typed_err ... ok test sched::tests::ipc_recv_and_yield_returns_deadlock_when_ready_queue_empty ... ok test sched::tests::ipc_recv_and_yield_with_idle_as_current_returns_deadlock ... ok test sched::tests::ipc_recv_and_yield_with_no_current_task_leaves_endpoint_idle ... ok test sched::tests::ipc_send_and_yield_delivered_unblocks_receiver_and_yields ... ok test sched::tests::ipc_send_and_yield_enqueued_does_not_yield ... ok test sched::tests::ipc_send_and_yield_send_error_preserves_scheduler_state ... ok test sched::tests::queue_empty_dequeue_is_none ... ok test sched::tests::queue_enqueue_dequeue_fifo_order ... ok test sched::tests::queue_full_returns_error ... ok test sched::tests::queue_len_and_is_empty ... ok test sched::tests::queue_wraps_around ... ok test sched::tests::register_idle_stores_handle_in_idle_slot_and_not_in_ready_queue ... ok test sched::tests::start_prelude_dispatches_head_and_marks_ready ... ok test sched::tests::task_state_variants_are_distinct ... ok test sched::tests::unblock_after_yield_dispatches_unblocked_receiver_not_idle ... ok test sched::tests::start_prelude_panics_on_empty_ready_queue - should panic ... ok test sched::tests::unblock_receiver_on_moves_task_to_ready ... ok test sched::tests::unblock_receiver_on_wrong_ep_is_noop ... ok test sched::tests::yield_now_activates_when_tasks_differ_in_address_space ... ok test sched::tests::yield_now_masks_irqs_across_switch_and_restores_on_return ... ok test sched::tests::yield_now_skips_activation_when_tasks_share_address_space ... ok test sched::tests::yield_now_switches_context_and_updates_current ... ok test sched::tests::yield_now_with_no_current_returns_error ... ok test syscall::abi::tests::as_u64_round_trips_recognised_numbers ... ok test syscall::abi::tests::console_write_is_a_syscall_in_debug_builds ... ok test syscall::abi::tests::decode_maps_v1_numbers ... ok test syscall::abi::tests::decode_out_of_range_is_none ... ok test syscall::abi::tests::decode_send_message_reads_label_and_params ... ok test syscall::abi::tests::decode_zero_is_reserved_invalid ... ok test syscall::abi::tests::none_round_trips_through_null_sentinel ... ok test syscall::abi::tests::recv_pending_packs_pending_code_and_zeroes_rest ... ok test syscall::abi::tests::recv_received_packs_message_and_cap ... ok test syscall::abi::tests::recv_received_without_cap_packs_null_sentinel ... ok test syscall::abi::tests::required_handle_decode_ignores_sentinel_semantics ... ok test syscall::abi::tests::send_outcome_codes ... ok test syscall::abi::tests::some_handle_round_trips ... ok test syscall::abi::tests::syscall_return_with_payload_sets_indexed_word ... ok test syscall::dispatch::tests::bad_number_zero_returns_bad_syscall_number_touching_nothing ... ok test syscall::dispatch::tests::console_write_cap_ok_but_non_user_page_emits_nothing ... ok test syscall::dispatch::tests::console_write_emits_buffer_and_returns_byte_count ... ok test syscall::dispatch::tests::console_write_exactly_one_chunk_emits_all_bytes ... ok test syscall::dispatch::tests::console_write_multipage_second_page_unmapped_emits_nothing ... ok test syscall::dispatch::tests::console_write_out_of_range_buffer_faults_without_output ... ok test syscall::dispatch::tests::console_write_spanning_multiple_chunks_emits_all_bytes ... ok test syscall::dispatch::tests::console_write_with_no_cap_returns_cap_invalid_handle_no_output ... ok test syscall::dispatch::tests::console_write_with_wrong_kind_cap_returns_cap_wrong_kind ... ok test syscall::dispatch::tests::console_write_without_write_right_returns_insufficient_rights ... ok test syscall::dispatch::tests::incomplete_binding_context_fails_closed_on_both_planes ... ok test syscall::dispatch::tests::out_of_range_number_returns_bad_syscall_number ... ok test syscall::dispatch::tests::recv_of_enqueued_message_unpacks_into_registers ... ok test syscall::dispatch::tests::recv_with_no_sender_returns_pending_packing ... ok test syscall::dispatch::tests::send_with_no_receiver_enqueues_and_returns_ok ... ok test syscall::dispatch::tests::send_with_stale_transfer_handle_returns_invalid_transfer_cap ... ok test syscall::dispatch::tests::send_with_transfer_cap_then_recv_returns_cap_in_x6 ... ok test syscall::dispatch::tests::send_without_send_right_returns_typed_ipc_missing_right ... ok test syscall::dispatch::tests::task_exit_routes_to_terminate_with_code ... ok test syscall::dispatch::tests::task_exit_with_no_current_task_fails_closed ... ok test syscall::dispatch::tests::task_yield_routes_to_reschedule ... ok test syscall::dispatch::tests::task_yield_with_no_current_task_fails_closed ... ok test syscall::error::tests::cap_and_ipc_status_blocks_do_not_collide ... ok test syscall::error::tests::cap_error_from_round_trips_and_encodes_in_cap_block ... ok test syscall::error::tests::ipc_error_from_round_trips_and_encodes_in_ipc_block ... ok test syscall::error::tests::top_level_status_codes_are_stable ... ok test syscall::error::tests::ok_status_is_zero_and_no_error_encodes_to_it ... ok test syscall::user_access::tests::copy_from_user_block_mapped_page_faults ... ok test syscall::user_access::tests::copy_from_user_faults_on_unmapped_page ... ok test syscall::user_access::tests::copy_from_user_multipage_second_page_unmapped_copies_nothing ... ok test syscall::user_access::tests::copy_from_user_out_of_window_faults_before_translate ... ok test syscall::user_access::tests::copy_from_user_overrun_past_window_end_faults ... ok test syscall::user_access::tests::copy_from_user_range_ending_in_top_page_does_not_spuriously_fault ... ok test syscall::user_access::tests::copy_from_user_rejects_in_window_non_user_page ... ok test syscall::user_access::tests::copy_from_user_spanning_two_pages_copies_all ... ok test syscall::user_access::tests::copy_from_user_translates_and_copies_a_user_page ... ok test syscall::user_access::tests::copy_to_user_in_range_moves_bytes ... ok test syscall::user_access::tests::copy_to_user_rejects_read_only_user_page ... ok test syscall::user_access::tests::validate_exact_window_fit_is_ok ... ok test syscall::user_access::tests::wrapping_range_faults ... ok test syscall::user_access::tests::zero_length_copy_is_ok_even_for_unmapped_pointer ... ok test result: ok. 258 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s running 58 tests test console::tests::default_fake_console_is_empty ... ok test console::tests::captures_successive_byte_writes ... ok test console::tests::fmt_writer_produces_formatted_output ... ok test context_switch::tests::context_switch_marks_current_and_counts ... ok test context_switch::tests::default_context_is_uninitialized_and_unswitched ... ok test context_switch::tests::init_context_clears_prior_user_markers_on_reuse ... ok test context_switch::tests::init_context_records_entry_stack_and_marks_initialized ... ok test context_switch::tests::init_user_context_records_user_entry_sp_kernel_stack_and_marks_is_user ... ok test cpu::tests::default_cpu_reports_core_zero_with_irqs_enabled ... ok test cpu::tests::disable_irqs_masks_and_returns_previous_state ... ok test cpu::tests::instruction_barrier_increments_count ... ok test cpu::tests::irq_guard_enters_and_exits_critical_section ... ok test cpu::tests::irq_state_uses_daif_polarity_zero_means_enabled ... ok test cpu::tests::nested_irq_guards_restore_outer_state ... ok test cpu::tests::wait_for_interrupt_increments_count ... ok test cpu::tests::with_core_id_sets_reported_id ... ok test irq_controller::tests::ack_eoi_cycle_leaves_clean_state ... ok test irq_controller::tests::acknowledge_returns_none_when_queue_empty ... ok test irq_controller::tests::acknowledge_returns_pending_fifo ... ok test irq_controller::tests::disable_removes_enabled_state ... ok test irq_controller::tests::disabled_irq_can_still_be_injected_for_test_purposes ... ok test irq_controller::tests::enable_is_idempotent ... ok test irq_controller::tests::enable_marks_line_as_enabled ... ok test irq_controller::tests::disable_panics_on_out_of_range_irq - should panic ... ok test irq_controller::tests::end_of_interrupt_records_irq_in_order ... ok test irq_controller::tests::enable_panics_on_out_of_range_irq - should panic ... ok test mmu::tests::activate_records_root ... ok test mmu::tests::block_mapped_mmu_delegates_unblocked_addresses ... ok test mmu::tests::block_mapped_mmu_injects_block_mapped_on_map_and_unmap ... ok test mmu::tests::block_mapped_mmu_translate_injects_block_mapped ... ok test mmu::tests::bulk_map_with_ignore_then_invalidate_tlb_all ... ok test mmu::tests::create_address_space_stores_root ... ok test mmu::tests::double_map_returns_already_mapped ... ok test mmu::tests::fake_translate_returns_mapping_and_aligns_interior_offset ... ok test mmu::tests::fake_translate_unmapped_is_not_mapped ... ok test mmu::tests::map_rejects_device_plus_execute ... ok test mmu::tests::map_rejects_unaligned_va ... ok test mmu::tests::map_returns_token_with_mapped_va ... ok test mmu::tests::map_unmap_round_trip ... ok test mmu::tests::mapper_flush_carries_virt_addr ... ok test mmu::tests::mapper_flush_flush_invokes_invalidate_tlb_address ... ok test mmu::tests::mapper_flush_ignore_is_documented_noop ... ok test mmu::tests::mapping_flags_difference_clears_bits ... ok test mmu::tests::mapping_flags_union_and_contains ... ok test mmu::tests::out_of_frames_mmu_maps_while_frames_available ... ok test mmu::tests::out_of_frames_mmu_returns_out_of_frames_when_provider_empty ... ok test mmu::tests::phys_frame_rejects_unaligned ... ok test mmu::tests::tlb_invalidations_recorded_in_order ... ok test mmu::tests::unmap_missing_returns_not_mapped ... ok test mmu::tests::unmap_rejects_unaligned_va ... ok test mmu::tests::unmap_returns_token_with_unmapped_va_and_frame ... ok test timer::tests::advance_moves_clock_forward ... ok test timer::tests::arm_deadline_records_value ... ok test timer::tests::arm_deadline_replaces_previous ... ok test timer::tests::cancel_clears_deadline_and_counts ... ok test timer::tests::default_has_one_nanosecond_resolution ... ok test timer::tests::new_starts_at_zero_with_given_resolution ... ok test timer::tests::set_now_overrides_clock ... ok test result: ok. 58 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s running 2 tests test hal/src/console.rs - console::FmtWriter (line 51) ... ignored test hal/src/cpu.rs - cpu::IrqGuard (line 108) ... ignored test result: ok. 0 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 0.00s running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s running 3 tests test test-hal/src/context_switch.rs - context_switch::FakeContextSwitch (line 60) ... ok test test-hal/src/mmu.rs - mmu::OutOfFramesMmu (line 312) ... ok test test-hal/src/mmu.rs - mmu::BlockMappedMmu (line 446) ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.36s running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s links + passes (9 suites, 0 failures); build-userland.sh emits the 117-byte image; fmt clean. Refs: T-027, ADR-0039 Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 6 +++++- userland/hello/build.rs | 17 +++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad4be72..bad4547 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -270,4 +270,8 @@ jobs: restore-keys: | ${{ runner.os }}-llvmcov-${{ env.NIGHTLY_PIN }}- - name: cargo llvm-cov --summary-only - run: cargo llvm-cov --workspace --exclude tyrne-bsp-qemu-virt --summary-only + # Exclude the bare-metal crates: the BSP is no_std/no_main (cannot host- + # build) and the userland crates (tyrne-user / hello) host-compile only + # as aarch64-gated stubs (ADR-0039) — coverage of never-run stubs is + # meaningless and would only dilute the metric. + run: cargo llvm-cov --workspace --exclude tyrne-bsp-qemu-virt --exclude tyrne-user --exclude tyrne-userland-hello --summary-only diff --git a/userland/hello/build.rs b/userland/hello/build.rs index 53394e8..cdc217d 100644 --- a/userland/hello/build.rs +++ b/userland/hello/build.rs @@ -1,14 +1,23 @@ //! Build script for `tyrne-userland-hello`. //! -//! Passes the userland linker script (`hello.ld`) to the linker with an -//! absolute path, mirroring `bsp-qemu-virt/build.rs`. The script fixes the -//! image at the userspace base VA with the entry at offset 0 (ADR-0029/0039). +//! Passes the userland linker script (`hello.ld`) to the linker — **only when +//! building for the real aarch64 target**. The script fixes the image at the +//! userspace base VA with the entry at offset 0 (ADR-0029/0039) and ASSERTs no +//! `.data`/`.bss`/`.got`. On a HOST build (the `--workspace` member is host- +//! compiled by `cargo check`/`miri`/`llvm-cov`), those ASSERTs would fire +//! against the host std + coverage-instrumentation artifacts at link time, so +//! the script must NOT be applied there — the host build links as an ordinary +//! `std` stub (see `src/main.rs`) and is never run. fn main() { let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") .expect("CARGO_MANIFEST_DIR must be set by Cargo when running build scripts"); - println!("cargo:rustc-link-arg=-T{manifest_dir}/hello.ld"); + // CARGO_CFG_TARGET_ARCH reflects the *target* being built for (aarch64 for + // the real image; the host arch for coverage/miri/check of the workspace). + if std::env::var("CARGO_CFG_TARGET_ARCH").as_deref() == Ok("aarch64") { + println!("cargo:rustc-link-arg=-T{manifest_dir}/hello.ld"); + } println!("cargo:rerun-if-changed=hello.ld"); println!("cargo:rerun-if-changed=build.rs"); }