Skip to content

T-025 — Mmu::translate + per-task user-access translation (B6 gate #1)#39

Merged
cemililik merged 6 commits into
mainfrom
t-025-user-access-translation
May 31, 2026
Merged

T-025 — Mmu::translate + per-task user-access translation (B6 gate #1)#39
cemililik merged 6 commits into
mainfrom
t-025-user-access-translation

Conversation

@cemililik
Copy link
Copy Markdown
Collaborator

@cemililik cemililik commented May 31, 2026

T-025 — Mmu::translate + per-task user-access translation (B6 gate #1)

Closes the security-critical T-021 carry-forward gate #1 mechanism — the single most important B6 gate. Drives ADR-0038 (Accepted in this branch). Composes ADR-0009 (the Mmu trait), ADR-0033 (high-half direct map), ADR-0030 (the copy-user contract).

The threat this closes

A real EL0 task holding a debug-console capability could pass an in-window kernel VA to console_write; under B5's bounds-checked int-to-pointer deref the kernel would copy privileged memory to the console — a confused-deputy read. The range bound is necessary but not sufficient: it proves the pointer is in a range, not that it names the task's own memory.

Design

  • Mmu::translate (read-only L0→L3 walk; the deferred ADR-0009 §Open-questions "translation walk query") → (PhysFrame, MappingFlags); NotMapped/BlockMapped on absent/block leaf. QemuVirtMmu::translate reuses the audited walker read-only (UNSAFE-2026-0025 Amendment); vmsav8::descriptor_bits_to_flags is the inverse decoder (lock-shut, round-trip + property tested).
  • Two-pass copy-user (copy_from_user/copy_to_user, now generic over <M: Mmu>): pass 1 probes every spanned page (translate + require USER, +WRITE for copy_to_user); only if all pass does pass 2 copy, rebasing each frame via phys_frame_kernel_ptr. Any miss / non-USER / block page → SyscallError::FaultAddress, never a panic, zero bytes moved (all-or-nothing). The per-page USER check is the load-bearing boundary; the per-task [entry_va, stack_top_va) window is the cheap first gate.
  • SyscallContext gains mmu + task_as (generic over M); sys_console_write probes the whole range up front so a multi-chunk emit is all-or-nothing.

Smoke change (the only trace delta)

The B5 +0x200 stub's console_write passes a kernel .rodata VA → now correctly rejected by the USER-enforcing translate path (status=0x3 FaultAddress, bytes=0, no greeting). This is a positive gate-#1 demonstration (the confused-deputy defence working), not a regression — the SVC still traps + ERETs cleanly (2 SVC exceptions, zero new fault class). A real EL0 task's valid USER buffer is the B6 wire-up.

Commit arc

commit what
1ed87fd docs(adr): propose ADR-0038 + open T-025/T-026 slots
0c79533 docs(adr): review-round — 8 valid fixes from two relayed reviews (3 skipped, verified invalid)
bb1357a docs(adr): accept ADR-0038 (careful re-read)
c7f5ef1 feat(mmu): step 1-2 — Mmu::translate + vmsav8 decoder (HAL surface)
ad943e4 feat(syscall): step 3-4 — two-pass copy-user + SyscallContext<M> + BSP gate #1 wiring + smoke

The two relayed pre-implementation reviews were verified against the live tree (multi-agent workflow): 8 valid fixes applied (H1 window persistence → scheduler array deferred to T-026; H2 control-plane fail-closed; M1 sim-row reframe; M2 round-trip-over-valid-combos + lock-shut test; M4 mandatory audit AC; two-pass discipline; etc.), 3 skipped with evidence (notably MmuError::BlockMapped already exists; the rider past-tense mirrors the MapperFlush precedent).

Gates (all green)

  • cargo fmt --all --check
  • host clippy + kernel clippy -D warnings
  • host tests: kernel 252 / hal 46 / test-hal 58 / 3 doc
  • cargo kernel-build (aarch64) ✅
  • QEMU smoke PASS — boot to all tasks complete; exactly 2 SVC exceptions, clean ERET, zero new fault class; gate Development #1 rejection shown ✅
  • cargo +nightly miri test --workspace --exclude tyrne-bsp-qemu-virt0 UB

Audit

UNSAFE-2026-0030 Amendment (per-page translation supersedes the identity-map deref) + UNSAFE-2026-0025 Amendment (read-only translate caller). No new unsafe entries.

Out of scope (follow-ups)

  • Gate Development #3 (T-026) — source the running EL0 task's AS + per-task window + capability table from the scheduler (the task_user_windows/task_cap_tables parallel arrays + fail-closed syscall_entry).
  • tyrne-user + userland/hello build pipeline + the EL0 +0x400 wire-up smoke.

⚠️ Security-relevant — this is the EL0 user-access privilege boundary. Per the T-025 Definition of done it awaits an explicit EL0-boundary security review before a real EL0 task is wired (gate #3 + wire-up DoD item).

🤖 Generated with Claude Code

Summary by Sourcery

Introduce MMU-level virtual-to-physical translation and use per-task page translation to harden syscall user-memory access.

New Features:

  • Add Mmu::translate read-only walk API and implement it for QemuVirtMmu and test HAL MMUs.
  • Introduce descriptor_bits_to_flags to decode VMSAv8 leaf descriptors back into MappingFlags.
  • Add FakeUserMem test fixture to model user-mapped pages for host tests.

Enhancements:

  • Refactor copy_from_user and copy_to_user into two-pass, per-page translate-based routines generic over the Mmu trait, enforcing USER/WRITE permissions.
  • Extend SyscallContext with MMU and address-space references so syscalls resolve user pointers through the current task's translation tables.
  • Tighten UserAccessWindow semantics to act as a per-task first-gate over the task’s image+stack range instead of the whole RAM extent.
  • Update syscall boundary smoke test to demonstrate rejection of kernel virtual addresses by the new user-access gate.
  • Document ADR-0038 and related B6 gate tasks, updating roadmap and unsafe-log with the new translation-based boundary.

Tests:

  • Add comprehensive user_access and dispatch tests covering non-USER pages, unmapped pages, multi-page ranges, and all-or-nothing semantics.
  • Add tests for FakeMmu::translate and BlockMappedMmu translate behaviour, and for descriptor_bits_to_flags round-tripping and lock-shut guarantees.

Summary by CodeRabbit

  • New Features

    • Added read-only address-space translation and per-page user-access validation for syscalls; console-write now rejects kernel-VA buffers (no bytes emitted).
    • Enforced all-or-nothing semantics for user copies (probe-then-copy) to prevent partial writes.
  • Documentation

    • Added ADRs, tasks, roadmap updates, and audit entries describing the translation/query design and security rationale.
  • Tests

    • Extended unit and integration tests and host fixtures to cover translation, permission checks, multi-page behavior, and confused-deputy rejection.

cemililik and others added 5 commits May 31, 2026 15:14
…ranslation (B6 gate #1)

Adds the read-only Mmu::translate walk query (the deferred ADR-0009 "translation walk query", now with its first caller) + the per-task user-access policy closing B6 gate #1: the syscall copy path resolves each user VA through the task's own TTBR0 and enforces USER, defeating the in-window kernel-VA confused-deputy read. Opens T-025 (gate #1 mechanism) + T-026 (gate #3 plumbing) per ADR-0025 Rule 1; ADR-0009 Revision rider records the additive trait method.

Refs: ADR-0038

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…kipped)

Two relayed reviews of the Propose set, verified against the live tree (workflow). Applied 8 valid fixes: T-026 — persist the per-task window in a scheduler array (H1; the LoadedImage span is reachable nowhere today) + mandate an early short-circuit for task_yield/task_exit which bypass the table/window (H2) + de-conditionalize the mandatory UNSAFE-2026-0014 amendment (M4). T-025 — two-pass probe-then-copy for true all-or-nothing (2.1.1) + round-trip over VALID flag combos only since DEVICE|EXECUTE is rejected + a lock-shut property test (M2) + FakeMmu page-base-align/never-BlockMapped + a module-doc-correction note (L1). ADR-0038 — reframe sim row 1 (a high-half VA fails the tight per-task window, not the USER check) + add a copy_to_user WRITE row + footnote (M1/2.3.1). ADR-0009 rider — lookup->translate naming rationale + clarify BlockMapped is REUSED not added (2.2.2/2.3.4).

Skipped 3 (verified invalid): M3 (rider past-tense mirrors the MapperFlush precedent), 2.2.1 (MmuError::BlockMapped already exists — T-018), 2.4.2 (ADRs deliberately omit downstream-consumer back-links).

Refs: ADR-0038

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…anslation (B6 gate #1)

Careful re-read per write-adr §10: forward-refs ground in T-025/T-026 (real), dependency chain complete (ADR-0009/0033/0030 Accepted), negatives are real costs. The re-read tightened §Decision-outcome line 62 to the two-pass probe-then-copy model (consistency with §Simulation row 4). Flips ADR-0038 Proposed→Accepted; T-025 Draft→In Progress.

Refs: ADR-0038

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…coder (gate #1 HAL surface)

Adds the deferred ADR-0009 'translation walk query', realised per ADR-0038 as fn translate(&self, &AddressSpace, VirtAddr) -> Result<(PhysFrame, MappingFlags), MmuError>: a read-only L0->L3 walk returning the page frame + leaf flags (NotMapped on absent leaf, BlockMapped on block hit). vmsav8 gains the inverse decoder descriptor_bits_to_flags (lock-shut, round-trip + property tested). QemuVirtMmu::translate reuses the audited walker (UNSAFE-2026-0025, read-only); FakeMmu (page-base lookup, never BlockMapped) + OutOfFramesMmu/BlockMappedMmu/FailingMapMmu impls. No caller yet (gate #1 copy-user rewrite is step 3).

Refs: ADR-0038, T-025

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…SyscallContext<M> (gate #1)

Closes the B6 gate #1 mechanism (ADR-0038): copy_from_user/copy_to_user become generic over <M: Mmu>, two-pass (probe-all-then-copy) — pass 1 translates every spanned page through the task's own AS and requires USER (+WRITE for to_user), pass 2 rebases each frame via phys_frame_kernel_ptr and copies; any miss/non-USER/block-mapped page -> SyscallError::FaultAddress, never a panic. The per-page USER check is the confused-deputy boundary (a kernel/in-window VA is rejected); the per-task [entry_va,stack_top_va) window is the cheap first gate. SyscallContext gains mmu/task_as; sys_console_write probes the whole range up front (all-or-nothing emit). BSP: syscall_entry sources &MMU + the bootstrap AS (new BOOTSTRAP_AS static); the +0x200 stub console_write of a kernel VA is now correctly rejected (smoke status=0x3) — a positive gate-#1 demonstration. Shared host fixture tyrne_test_hal::FakeUserMem (page-aligned host pages mapped USER) drives the confused-deputy / unmapped / read-only / multi-page all-or-nothing tests.

Gates: fmt; host+kernel clippy -D warnings; host tests 252 kernel / 46 hal / 58 test-hal / 3 doc; kernel build; QEMU smoke PASS (2 SVC exceptions, clean ERET, zero new fault class); Miri 0 UB. UNSAFE-2026-0030 + 0025 Amendments (no new entries). Remaining for the EL0 wire-up: gate #3 (T-026) sources the running task's AS + cap table.

Refs: ADR-0038, T-025

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 31, 2026

Reviewer's Guide

Implements the B6 gate #1 user-access translation mechanism by adding a read-only Mmu::translate walk, wiring syscall copy-from/to-user to do per-page USER/WRITE checks via the task’s own address space, and updating syscall dispatch/BSP/test-hal docs and tests accordingly while preserving panic-free behaviour and tightening the privilege boundary.

Sequence diagram for console_write with per-page Mmu::translate gate

sequenceDiagram
    actor El0Task
    participant SyscallEntry
    participant SyscallDispatch
    participant SyscallContext
    participant Mmu
    participant Console

    El0Task->>SyscallEntry: SVC console_write(ptr,len,cap)
    SyscallEntry->>SyscallContext: build SyscallContext(mmu, task_as, user_window)
    SyscallEntry->>SyscallDispatch: dispatch(args)
    SyscallDispatch->>SyscallContext: sys_console_write(args)

    SyscallContext->>SyscallContext: validate_debug_console_cap
    SyscallContext->>SyscallContext: user_window.validate(ptr,len)

    alt len > 0
        SyscallContext->>Mmu: probe_user_pages(mmu, task_as, ptr, len, require_write=false)
        Mmu-->>SyscallContext: translate(task_as, page_va) for each page
        alt any translate error or !USER
            SyscallContext-->>SyscallDispatch: SyscallError::FaultAddress
            SyscallDispatch-->>SyscallEntry: SyscallReturn::error
            SyscallEntry-->>El0Task: status=FaultAddress, bytes=0
        else all pages OK
            loop chunks
                SyscallContext->>SyscallContext: copy_from_user(mmu, task_as, user_window, chunk_ptr)
                SyscallContext->>Mmu: translate(task_as, page_base)
                Mmu-->>SyscallContext: (PhysFrame, MappingFlags::USER)
                SyscallContext->>Console: write_bytes(chunk)
            end
            SyscallDispatch-->>SyscallEntry: SyscallReturn::ok(bytes)
            SyscallEntry-->>El0Task: status=0, bytes
        end
    else len == 0
        SyscallDispatch-->>SyscallEntry: SyscallReturn::ok(0)
        SyscallEntry-->>El0Task: status=0, bytes=0
    end
Loading

File-Level Changes

Change Details Files
Introduce Mmu::translate read-only walk and VMSAv8 descriptor inverse decoding, and implement concrete HAL/test-hal translations.
  • Extend Mmu trait with a translate(&self, &AddressSpace, VirtAddr) -> Result<(PhysFrame, MappingFlags), MmuError> method documented as the ADR-0009/0038 walk query.
  • Implement QemuVirtMmu::translate as a read-only L0–L3 page-table walk reusing the existing walker and decoding L3 leaf descriptors via descriptor_bits_to_flags, returning NotMapped/BlockMapped where appropriate.
  • Add vmsav8::descriptor_bits_to_flags as the inverse of flags_to_descriptor_bits with tests for round-trip over valid flag combos and a lock-shut property.
  • Implement FakeMmu/OutOfFramesMmu/BlockMappedMmu translate in test-hal, including a BlockMappedMmu that injects MmuError::BlockMapped for configured pages.
  • Add FakeUserMem fixture to test-hal to back fake user pages with real, page-aligned host memory and map them via FakeMmu for host/Miri tests.
hal/src/mmu/mod.rs
bsp-qemu-virt/src/mmu.rs
hal/src/mmu/vmsav8.rs
test-hal/src/mmu.rs
test-hal/src/lib.rs
Refactor copy_from_user/copy_to_user into a two-pass, translate-based per-page user copy that enforces USER/WRITE and uses the high-half direct map, plus shared page probing.
  • Update user_access.rs docs to describe the two-gate model (UserAccessWindow + per-page Mmu::translate) and two-pass semantics, and import MappingFlags/Mmu/VirtAddr/PAGE_SIZE plus phys_frame_kernel_ptr.
  • Introduce PAGE_MASK and probe_user_pages helper to iterate each spanned page, translate via Mmu::translate, and enforce USER (and optionally WRITE), returning SyscallError::FaultAddress on wrap, NotMapped, BlockMapped, or missing permission.
  • Change copy_from_user<M: Mmu> to validate via UserAccessWindow, short-circuit zero-length, run probe_user_pages, then loop per page: re-translate, re-check USER, compute in-page runs, rebase via phys_frame_kernel_ptr, and core::ptr::copy into dst with updated unsafe comment/audit (UNSAFE-2026-0030 amendment).
  • Similarly change copy_to_user<M: Mmu> to validate, zero-length short-circuit, probe with require_write=true, then per-page translate and write via phys_frame_kernel_ptr, enforcing USER
WRITE.
  • Expand host tests in user_access.rs to use FakeUserMem and FakeMmu for in-range, cross-page, confused-deputy, unmapped, read-only, range-gate, zero-length, and wrapping scenarios, ensuring all-or-nothing semantics and updated invariants.
  • Make SyscallContext generic over Mmu, thread mmu/task_as into syscall dispatch, and harden console_write with whole-range per-page probing and updated tests.
    • Change SyscallContext<'a> to SyscallContext<'a, M: Mmu> adding mmu: &M and task_as: &M::AddressSpace, adjust doc comments, and parameterise dispatch/sys_send/sys_recv/sys_console_write over M.
    • In sys_console_write, after cap gate and UserAccessWindow validation, add a whole-range probe_user_pages(mmu, task_as, ptr, len, false) so multi-chunk emits become all-or-nothing and in-window non-USER pages fault before emission; update per-chunk copy_from_user calls to pass mmu/task_as.
    • Refactor syscall dispatch tests to construct SyscallContext with FakeMmu/FakeAddressSpace and add run_console_write helper to wire ep_arena/queues/table/console/mmu/task_as/user_window per test.
    • Adjust all existing syscall tests to supply mmu/task_as via empty_mmu_as for non-console syscalls and to use FakeUserMem-backed buffers for console_write tests, verifying chunking, range faults, cap-gate behaviour, and new gate Development #1 semantics.
    • Add new console_write tests for non-USER pages and multipage unmapped cases to assert no output and FaultAddress status, plus release-build behaviour where console_write number decodes as bad syscall before translation.
    kernel/src/syscall/dispatch.rs
    Wire the BSP syscall path to the new translation surface and demonstrate gate #1 using the EL1 stub, including bootstrap address space storage.
    • Add BOOTSTRAP_AS StaticCell in bsp-qemu-virt, initialised in kernel_main_high alongside the bootstrap AS handle, to supply syscall_entry with a copy of the bootstrap address space root.
    • Update syscall_entry to populate SyscallContext with mmu: &MMU and task_as: &BOOTSTRAP_AS, and use the existing SYSCALL_USER_WINDOW_LEN-based UserAccessWindow as a temporary per-stub window (still fail-closed for EL0).
    • Modify syscall_boundary_smoke to call console_write with a kernel .rodata buffer, expecting gate Development #1 to reject it (status=FaultAddress, bytes=0), and adjust the log line to report the rejection instead of successful greeting emission.
    • Document the BOOTSTRAP_AS use and the gate Development #1 behaviour in comments, tying it to ADR-0038 and describing that the stub runs in a bootstrap AS with no USER mappings so kernel-VA console_write is rejected.
    • Ensure kernel-main initialisation and unsafe-cell usage are accounted for in UNSAFE-2026-0010/0029/0025 amendments and that the change passes QEMU smoke with two SVC exceptions and no new fault class.
    bsp-qemu-virt/src/main.rs
    bsp-qemu-virt/src/syscall.rs
    Update documentation (ADR-0009, ADR-0038, roadmap, unsafe-log, tasks) to record translate and gate #1, and align docs with implementation.
    • Add ADR-0038 describing Mmu::translate, per-task user-access translation, options considered, and simulation rows, and register it in decisions/README.md; add a revision note to ADR-0009 for translate.
    • Create analysis tasks T-025 and T-026 describing gate Development #1 translate work and gate Development #3 scheduler wiring, including acceptance criteria, out-of-scope items, and review history; mark T-025 as In Review and T-026 as Draft.
    • Update phase-b roadmap to mark gate Development #1 mechanism as landed and explain the remaining work for gate Development #3 and EL0 wire-up; update current.md with a B6 gate Development #1 banner summarising ADR-0038/T-025, tests, gates, and next steps.
    • Extend unsafe-log with amendments for UNSAFE-2026-0025 (read-only QemuVirtMmu::translate reuse of walker) and UNSAFE-2026-0030 (per-page translate-based copy-user replacing identity-map deref), including threat description, invariants, and smoke results.
    • Refresh user_access.rs module docs to explain the new two-layer model (UserAccessWindow + per-page translate), two-pass all-or-nothing semantics, and identity-map forward-path removal; adjust references to adr-0033/translate/gate1 links.
    docs/decisions/0038-mmu-translate-and-user-access.md
    docs/decisions/0009-mmu-trait.md
    docs/decisions/README.md
    docs/analysis/tasks/phase-b/T-025-user-access-translation.md
    docs/analysis/tasks/phase-b/T-026-current-task-cap-table.md
    docs/roadmap/current.md
    docs/roadmap/phases/phase-b.md
    docs/audits/unsafe-log.md
    kernel/src/syscall/user_access.rs

    Tips and commands

    Interacting with Sourcery

    • Trigger a new review: Comment @sourcery-ai review on the pull request.
    • Continue discussions: Reply directly to Sourcery's review comments.
    • Generate a GitHub issue from a review comment: Ask Sourcery to create an
      issue from a review comment by replying to it. You can also reply to a
      review comment with @sourcery-ai issue to create an issue from it.
    • Generate a pull request title: Write @sourcery-ai anywhere in the pull
      request title to generate a title at any time. You can also comment
      @sourcery-ai title on the pull request to (re-)generate the title at any time.
    • Generate a pull request summary: Write @sourcery-ai summary anywhere in
      the pull request body to generate a PR summary at any time exactly where you
      want it. You can also comment @sourcery-ai summary on the pull request to
      (re-)generate the summary at any time.
    • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
      request to (re-)generate the reviewer's guide at any time.
    • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
      pull request to resolve all Sourcery comments. Useful if you've already
      addressed all the comments and don't want to see them anymore.
    • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
      request to dismiss all existing Sourcery reviews. Especially useful if you
      want to start fresh with a new review - don't forget to comment
      @sourcery-ai review to trigger a new review!

    Customizing Your Experience

    Access your dashboard to:

    • Enable or disable review features such as the Sourcery-generated pull request
      summary, the reviewer's guide, and others.
    • Change the review language.
    • Add, remove or edit custom review instructions.
    • Adjust other review settings.

    Getting Help

    @coderabbitai
    Copy link
    Copy Markdown

    coderabbitai Bot commented May 31, 2026

    Review Change Stack

    No actionable comments were generated in the recent review. 🎉

    ℹ️ Recent review info
    ⚙️ Run configuration

    Configuration used: defaults

    Review profile: CHILL

    Plan: Pro

    Run ID: 92a2bc7c-b2c5-44d4-a321-1d65d27a8ebe

    📥 Commits

    Reviewing files that changed from the base of the PR and between ad943e4 and d0e5a17.

    📒 Files selected for processing (3)
    • bsp-qemu-virt/src/mmu.rs
    • docs/analysis/tasks/phase-b/T-025-user-access-translation.md
    • kernel/src/syscall/user_access.rs
    ✅ Files skipped from review due to trivial changes (1)
    • docs/analysis/tasks/phase-b/T-025-user-access-translation.md
    🚧 Files skipped from review as they are similar to previous changes (2)
    • bsp-qemu-virt/src/mmu.rs
    • kernel/src/syscall/user_access.rs

    📝 Walkthrough

    Walkthrough

    This PR implements B6 gate #1: per-task user-access translation. It adds a read-only Mmu::translate HAL trait method and refactors syscall copy helpers to perform per-page translation-based validation, replacing direct user-pointer dereferencing with two-pass probe-then-copy logic that enforces USER (and WRITE) permissions before accessing user memory.

    Changes

    B6 Gate #1: Translation-based user-access validation

    Layer / File(s) Summary
    HAL Mmu::translate trait contract
    hal/src/mmu/mod.rs
    Adds translate(&self, as_: &Self::AddressSpace, va: VirtAddr) -> Result<(PhysFrame, MappingFlags), MmuError> to the Mmu trait for read-only translation-table walks.
    VMSAv8 descriptor bits decoder
    hal/src/mmu/vmsav8.rs
    Implements descriptor_bits_to_flags, a lock-shut const fn that decodes leaf descriptor fields to MappingFlags (DEVICE/USER/WRITE/EXECUTE/GLOBAL) while ignoring other bits; includes round-trip and lock-shut tests.
    BSP QemuVirtMmu::translate read-only walk
    bsp-qemu-virt/src/mmu.rs
    Implements Mmu::translate for QemuVirtMmu: performs L0→L3 page-table walk without allocation, reads leaf descriptor via high-half direct map, validates bit patterns, and returns (PhysFrame, MappingFlags) or MmuError.
    Syscall dispatcher genericized over M: Mmu
    kernel/src/syscall/dispatch.rs (context/imports)
    SyscallContext and dispatch become generic over M: Mmu; mmu and task_as fields added to context; all handler signatures updated to accept SyscallContext<'_, M>.
    Per-page user-access validation (two-pass probe/copy)
    kernel/src/syscall/user_access.rs
    Adds probe_user_pages to validate every page via translate, requiring USER (and WRITE for writes); copy_from_user/copy_to_user become two-pass: probe all pages first, then copy only if all pages pass; returns FaultAddress on any translation or permission failure with zero bytes copied.
    Test MMU mocks with translate support and FakeUserMem
    test-hal/src/mmu.rs, test-hal/src/lib.rs
    FakeMmu, OutOfFramesMmu, and BlockMappedMmu implement translate for test scenarios; new FakeUserMem fixture allocates page-aligned host memory, maps it into a FakeMmu address space with USER|WRITE flags, and provides safe read/write accessors and metadata accessors.
    Console write syscall two-stage validation and test refactor
    kernel/src/syscall/dispatch.rs (handler/tests)
    console_write calls probe_user_pages before any output, then copy_from_user per chunk; test harness refactored with empty_mmu_as and run_console_write helpers; adds regression tests for confused-deputy (non-USER page) and all-or-nothing (later-page fault) behaviors.
    BSP bootstrap address-space wiring and smoke test update
    bsp-qemu-virt/src/main.rs, bsp-qemu-virt/src/syscall.rs
    Adds BOOTSTRAP_AS static cell to hold bootstrap QemuVirtAddressSpace; syscall_entry constructs SyscallContext with mmu from crate::MMU and task_as from crate::BOOTSTRAP_AS; syscall_boundary_smoke updated to test kernel-VA rejection (gate #1 confused-deputy defense).
    Task specs, ADRs, audits, and roadmap updates
    docs/analysis/tasks/phase-b/{T-025,T-026}.md, docs/decisions/{0009,0038}.md, docs/audits/unsafe-log.md, docs/roadmap/*, docs/decisions/README.md
    T-025 defines user-access translation acceptance criteria; T-026 specifies gate #3 scheduler wiring; ADR-0038 captures the design rationale and scenario table; ADR-0009 updated with revision note; UNSAFE-2026-0027 and UNSAFE-2026-0030 record invariant basis; roadmap and phase-b docs updated to mark gate #1 complete.

    Sequence Diagram

    sequenceDiagram
      participant App as EL0 App
      participant SyscallEntry as syscall_entry
      participant Dispatcher as dispatch
      participant ConsoleWrite as console_write
      participant Probe as probe_user_pages
      participant Copy as copy_from_user
      participant Mmu as Mmu::translate
      App->>SyscallEntry: syscall (console_write, ptr, len)
      SyscallEntry->>Dispatcher: SyscallContext{mmu, task_as, ...}
      Dispatcher->>ConsoleWrite: ctx, args
      ConsoleWrite->>Probe: mmu, task_as, ptr, len
      loop for each page in [ptr, ptr+len)
        Probe->>Mmu: translate(va)
        alt translates
          Mmu-->>Probe: PhysFrame, MappingFlags
          Probe->>Probe: check MappingFlags::USER
        else not mapped or no USER
          Mmu-->>Probe: NotMapped or missing USER
          Probe-->>ConsoleWrite: FaultAddress
          ConsoleWrite-->>App: FaultAddress (0 bytes written)
        end
      end
      alt all pages pass
        Probe-->>ConsoleWrite: Ok
        loop for each page in [ptr, ptr+len)
          ConsoleWrite->>Copy: chunk
          Copy->>Mmu: translate(page_va)
          Mmu-->>Copy: PhysFrame
          Copy->>Copy: rebase via phys_frame_kernel_ptr
          Copy->>Copy: memcpy into console buffer
        end
        ConsoleWrite-->>App: Ok (bytes written)
      end
    
    Loading

    Estimated Code Review Effort

    🎯 4 (Complex) | ⏱️ ~60 minutes

    Possibly Related PRs

    • HodeTech/Tyrne#28: Bootstrap/address-space wiring and prior BSP/TTBR setup that this PR extends to publish BOOTSTRAP_AS for syscall translation context.
    • HodeTech/Tyrne#36: High-half migration / TTBR1 setup work that the BSP smoke and high-half direct-map handling depend on.
    • HodeTech/Tyrne#23: Earlier MMU/vmsav8 trait and primitives this change builds on, extended here with translate and descriptor-decoder.

    Poem

    🐰 A rabbit hops through the kernel's defense,
    Probes each page with a careful sense;
    Translate then copy — two-pass delight,
    No confused deputy slips through the night.

    🚥 Pre-merge checks | ✅ 5
    ✅ Passed checks (5 passed)
    Check name Status Explanation
    Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
    Title check ✅ Passed The title accurately summarizes the main change: implementing Mmu::translate and per-task user-access translation for B6 gate #1, which is the primary focus of this PR.
    Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
    Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
    Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

    ✏️ Tip: You can configure your own custom pre-merge checks in the settings.

    ✨ Finishing Touches
    📝 Generate docstrings
    • Create stacked PR
    • Commit on current branch
    🧪 Generate unit tests (beta)
    • Create PR with unit tests
    • Commit unit tests in branch t-025-user-access-translation

    Comment @coderabbitai help to get the list of available commands and usage tips.

    Copy link
    Copy Markdown

    @gemini-code-assist gemini-code-assist Bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Code Review

    This pull request implements the security-critical B6 gate #1 (ADR-0038 / T-025), introducing the read-only Mmu::translate walk query to the Mmu trait and updating the syscall copy-user path (copy_from_user and copy_to_user) to use a two-pass, per-page translation check that enforces USER permissions. Feedback on the changes highlights a high-severity bug in probe_user_pages where incrementing the page address by PAGE_SIZE can overflow if the range ends at the very end of the address space, causing valid accesses to incorrectly fail.

    Comment on lines +150 to +164
    let mut page = user_ptr & !PAGE_MASK;
    while page < end {
    let (_frame, flags) = mmu
    .translate(task_as, VirtAddr(page))
    .map_err(|_| SyscallError::FaultAddress)?;
    if !flags.contains(MappingFlags::USER) {
    return Err(SyscallError::FaultAddress);
    }
    if require_write && !flags.contains(MappingFlags::WRITE) {
    return Err(SyscallError::FaultAddress);
    }
    page = page
    .checked_add(PAGE_SIZE)
    .ok_or(SyscallError::FaultAddress)?;
    }
    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    high

    In probe_user_pages, the loop increments page by PAGE_SIZE and checks page < end. If the range [user_ptr, user_ptr + len) ends in the very last page of the address space (i.e., end > usize::MAX - PAGE_SIZE), the final increment page.checked_add(PAGE_SIZE) will overflow and return None. This causes the function to incorrectly return Err(SyscallError::FaultAddress) even though the range itself is completely valid and does not wrap.

    We can fix this by using a loop and checking if the next page starts after or at end before performing the increment, avoiding the overflow error.

        let mut page = user_ptr & !PAGE_MASK;
        loop {
            let (_frame, flags) = mmu
                .translate(task_as, VirtAddr(page))
                .map_err(|_| SyscallError::FaultAddress)?;
            if !flags.contains(MappingFlags::USER) {
                return Err(SyscallError::FaultAddress);
            }
            if require_write && !flags.contains(MappingFlags::WRITE) {
                return Err(SyscallError::FaultAddress);
            }
            let next_page = page.checked_add(PAGE_SIZE);
            match next_page {
                Some(next) if next < end => {
                    page = next;
                }
                _ => break,
            }
        }

    Copy link
    Copy Markdown

    @sourcery-ai sourcery-ai Bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Hey - I've found 1 issue, and left some high level feedback:

    • The multi-page copy logic in copy_from_user and copy_to_user (page alignment, run computation, offset arithmetic, and repeated translate + flag checks) is almost identical; consider extracting this into a shared internal helper to reduce duplication and the chance of subtle divergence between the read and write paths.
    • Range arithmetic (end, checked_add, wrap handling) now appears in UserAccessWindow::validate, probe_user_pages, and the copy loops; factoring the range computation into a single helper reused across these sites would make it easier to reason about overflow/wrap behavior and avoid future inconsistencies.
    Prompt for AI Agents
    Please address the comments from this code review:
    
    ## Overall Comments
    - The multi-page copy logic in `copy_from_user` and `copy_to_user` (page alignment, `run` computation, offset arithmetic, and repeated `translate` + flag checks) is almost identical; consider extracting this into a shared internal helper to reduce duplication and the chance of subtle divergence between the read and write paths.
    - Range arithmetic (`end`, `checked_add`, wrap handling) now appears in `UserAccessWindow::validate`, `probe_user_pages`, and the copy loops; factoring the range computation into a single helper reused across these sites would make it easier to reason about overflow/wrap behavior and avoid future inconsistencies.
    
    ## Individual Comments
    
    ### Comment 1
    <location path="docs/analysis/tasks/phase-b/T-025-user-access-translation.md" line_range="8" />
    <code_context>
    +- **Dependencies:** [ADR-0038](../../../decisions/0038-mmu-translate-and-user-access.md) (drives this task — the `Mmu::translate` surface + the per-task user-access policy); [ADR-0009](../../../decisions/0009-mmu-trait.md) (the `Mmu` trait this extends; §Open-questions "Translation walk queries" is the deferred capability this realises); [ADR-0033](../../../decisions/0033-kernel-high-half-migration.md) (the high-half direct map [`phys_frame_kernel_ptr`](../../../../kernel/src/mm/mod.rs) rebases a translated frame through); [ADR-0030](../../../decisions/0030-syscall-abi.md) (the [`UserAccessWindow`](../../../../kernel/src/syscall/user_access.rs) + `SyscallError::FaultAddress` copy-user contract this plugs into); [T-016](T-016-mmu-activation.md) (the `vmsav8` encoders `descriptor_bits_to_flags` inverts); [T-019](T-019-task-loader.md) (`LoadedImage{entry_va, stack_top_va}` — the contiguous span the per-task window is derived from).
    </code_context>
    <issue_to_address>
    **issue (typo):** Clarify grammar in the phrase about `vmsav8` encoders and `descriptor_bits_to_flags`.
    
    This parenthetical is grammatically awkward and may confuse readers. Consider rephrasing to "the `vmsav8` encoders that `descriptor_bits_to_flags` inverts" or "`descriptor_bits_to_flags` inverts the `vmsav8` encoders" to clarify the subject–verb relationship.
    
    ```suggestion
    - **Dependencies:** [ADR-0038](../../../decisions/0038-mmu-translate-and-user-access.md) (drives this task — the `Mmu::translate` surface + the per-task user-access policy); [ADR-0009](../../../decisions/0009-mmu-trait.md) (the `Mmu` trait this extends; §Open-questions "Translation walk queries" is the deferred capability this realises); [ADR-0033](../../../decisions/0033-kernel-high-half-migration.md) (the high-half direct map [`phys_frame_kernel_ptr`](../../../../kernel/src/mm/mod.rs) rebases a translated frame through); [ADR-0030](../../../decisions/0030-syscall-abi.md) (the [`UserAccessWindow`](../../../../kernel/src/syscall/user_access.rs) + `SyscallError::FaultAddress` copy-user contract this plugs into); [T-016](T-016-mmu-activation.md) (the `vmsav8` encoders that `descriptor_bits_to_flags` inverts); [T-019](T-019-task-loader.md) (`LoadedImage{entry_va, stack_top_va}` — the contiguous span the per-task window is derived from).
    ```
    </issue_to_address>

    Sourcery is free for open source - if you like our reviews please consider sharing them ✨
    Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

    Comment thread docs/analysis/tasks/phase-b/T-025-user-access-translation.md Outdated
    Copy link
    Copy Markdown

    @coderabbitai coderabbitai Bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Actionable comments posted: 4

    🤖 Prompt for all review comments with AI agents
    Verify each finding against current code. Fix only still-valid issues, skip the
    rest with a brief reason, keep changes minimal, and validate.
    
    Inline comments:
    In `@bsp-qemu-virt/src/mmu.rs`:
    - Around line 325-377: translate currently accepts any L3 descriptor with V=1
    and page bit=1 even if the access flag (AF) is clear; update translate (inside
    the L3 leaf handling after reading `leaf` in function translate) to check the
    AF/access-flag bit on the L3 descriptor and return Err(MmuError::NotMapped) if
    AF==0 before constructing the PhysFrame and calling descriptor_bits_to_flags;
    ensure you reference the same bit mask used elsewhere for the AF/access flag
    (the constant used to test the AF bit) so the check mirrors how other non-usable
    descriptors are validated.
    
    In `@kernel/src/syscall/dispatch.rs`:
    - Around line 346-348: The unsafe block around calling
    FakeMmu::create_address_space(...) must be expanded to explain (a) why unsafe is
    necessary here (e.g., create_address_space needs a raw physical root frame and
    cannot be expressed safely because it performs low-level address-space setup),
    (b) the exact invariants the call upholds (the provided
    PhysFrame::from_aligned(PhysAddr(0x1000)) is valid/aligned, the FakeMmu
    implementation will store but never dereference the root, mmu remains valid for
    the lifetime used, and no aliasing/UB is possible), and (c) why a safe
    alternative was rejected (no safe constructor exists that can create an address
    space with a fake/root frame in tests or it would require changing API
    semantics). Reference the symbols FakeMmu::create_address_space,
    PhysFrame::from_aligned, PhysAddr, mmu and the local as_ binding in the comment,
    and add a corresponding audit entry to docs/audits/unsafe-log.md describing this
    justification.
    
    In `@kernel/src/syscall/user_access.rs`:
    - Around line 298-305: The unsafe write must be justified: update the
    unsafe-block comment around the kernel->user copy (the block that calls
    core::ptr::copy and complements copy_from_user) to explicitly state (a) why
    unsafe is required here — e.g., copying into user memory cannot be expressed
    with safe APIs because we must operate on raw user-page pointers obtained from
    page tables and do an in-place memmove across kernel/user boundary, (b) the
    precise invariants being upheld (range validity via window.validate and run/cur
    arithmetic, ownership/permission re-check of USER writable leaves from the
    probe, disjointness of kernel src vs user dst, and DAIF-masked SVC single-core
    protection), and (c) why safer alternatives were rejected (no existing safe
    wrapper can perform a zero-copy, checked, in-place copy into a mapped user page
    with the required permission re-checks and SVC interrupt masking). Also add an
    audit entry referencing UNSAFE-2026-0030 into docs/audits/unsafe-log.md with the
    same rationale and the function/context names (mentioning copy_from_user and the
    core::ptr::copy usage) so the audit can be traced.
    - Around line 147-163: The loop can overflow when advancing the page cursor:
    replace the checked_add that returns Err with a non-failing advance (e.g., use
    page = page.saturating_add(PAGE_SIZE)) so the page cursor clamps to usize::MAX
    instead of triggering a FaultAddress; update the code around user_ptr, len, end,
    page and PAGE_SIZE (where page =
    page.checked_add(PAGE_SIZE).ok_or(SyscallError::FaultAddress)? is used) to use
    saturating_add (or otherwise handle the overflow by setting next = usize::MAX)
    so the while page < end termination works correctly.
    
    🪄 Autofix (Beta)

    Fix all unresolved CodeRabbit comments on this PR:

    • Push a commit to this branch (recommended)
    • Create a new PR with the fixes

    ℹ️ Review info
    ⚙️ Run configuration

    Configuration used: defaults

    Review profile: CHILL

    Plan: Pro

    Run ID: 43f4e15b-253b-46a6-bf5b-58fe88bcc15c

    📥 Commits

    Reviewing files that changed from the base of the PR and between 5f17a30 and ad943e4.

    📒 Files selected for processing (18)
    • bsp-qemu-virt/src/main.rs
    • bsp-qemu-virt/src/mmu.rs
    • bsp-qemu-virt/src/syscall.rs
    • docs/analysis/tasks/phase-b/T-025-user-access-translation.md
    • docs/analysis/tasks/phase-b/T-026-current-task-cap-table.md
    • docs/audits/unsafe-log.md
    • docs/decisions/0009-mmu-trait.md
    • docs/decisions/0038-mmu-translate-and-user-access.md
    • docs/decisions/README.md
    • docs/roadmap/current.md
    • docs/roadmap/phases/phase-b.md
    • hal/src/mmu/mod.rs
    • hal/src/mmu/vmsav8.rs
    • kernel/src/obj/task_loader.rs
    • kernel/src/syscall/dispatch.rs
    • kernel/src/syscall/user_access.rs
    • test-hal/src/lib.rs
    • test-hal/src/mmu.rs

    Comment thread bsp-qemu-virt/src/mmu.rs
    Comment on lines +346 to +348
    // SAFETY: FakeMmu::create_address_space stores the root, never derefs it.
    let as_ =
    unsafe { mmu.create_address_space(PhysFrame::from_aligned(PhysAddr(0x1000)).unwrap()) };
    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

    Expand this unsafe justification.

    This note only captures one invariant. It still needs to say why the unsafe call is necessary in this helper and why a safe alternative is not viable, otherwise it doesn't meet the repo's required unsafe-block format.

    As per coding guidelines, **/*.rs: All kernel and userspace code must be written in Rust, with every unsafe block documented with comments explaining (a) why it is needed, (b) what invariants it upholds, and (c) why safer alternatives were rejected; audit entries must be logged in docs/audits/unsafe-log.md

    🤖 Prompt for AI Agents
    Verify each finding against current code. Fix only still-valid issues, skip the
    rest with a brief reason, keep changes minimal, and validate.
    
    In `@kernel/src/syscall/dispatch.rs` around lines 346 - 348, The unsafe block
    around calling FakeMmu::create_address_space(...) must be expanded to explain
    (a) why unsafe is necessary here (e.g., create_address_space needs a raw
    physical root frame and cannot be expressed safely because it performs low-level
    address-space setup), (b) the exact invariants the call upholds (the provided
    PhysFrame::from_aligned(PhysAddr(0x1000)) is valid/aligned, the FakeMmu
    implementation will store but never dereference the root, mmu remains valid for
    the lifetime used, and no aliasing/UB is possible), and (c) why a safe
    alternative was rejected (no safe constructor exists that can create an address
    space with a fake/root frame in tests or it would require changing API
    semantics). Reference the symbols FakeMmu::create_address_space,
    PhysFrame::from_aligned, PhysAddr, mmu and the local as_ binding in the comment,
    and add a corresponding audit entry to docs/audits/unsafe-log.md describing this
    justification.
    

    Comment thread kernel/src/syscall/user_access.rs Outdated
    Comment on lines +298 to +305
    // SAFETY: writes `run` bytes from the kernel `src` to the user page.
    // The invariants from [`copy_from_user`] hold with the direction
    // reversed: range validity (`window.validate` + the `run`/`cur`
    // arithmetic), ownership+permission (the probe proved every page is a
    // writable `USER` leaf; re-checked here), disjointness (`src` is a
    // kernel buffer, the destination a userspace page — distinct regions),
    // and no single-core interleaving under the `DAIF`-masked SVC handler.
    // `core::ptr::copy` is the conservative memmove. Audit: UNSAFE-2026-0030.
    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

    Document why this unsafe write is unavoidable.

    The comment covers the invariants, but it still needs the explicit “why unsafe is needed here / why a safe alternative was rejected” part to match the repo's required unsafe-block format.

    As per coding guidelines, **/*.rs: All kernel and userspace code must be written in Rust, with every unsafe block documented with comments explaining (a) why it is needed, (b) what invariants it upholds, and (c) why safer alternatives were rejected; audit entries must be logged in docs/audits/unsafe-log.md

    🤖 Prompt for AI Agents
    Verify each finding against current code. Fix only still-valid issues, skip the
    rest with a brief reason, keep changes minimal, and validate.
    
    In `@kernel/src/syscall/user_access.rs` around lines 298 - 305, The unsafe write
    must be justified: update the unsafe-block comment around the kernel->user copy
    (the block that calls core::ptr::copy and complements copy_from_user) to
    explicitly state (a) why unsafe is required here — e.g., copying into user
    memory cannot be expressed with safe APIs because we must operate on raw
    user-page pointers obtained from page tables and do an in-place memmove across
    kernel/user boundary, (b) the precise invariants being upheld (range validity
    via window.validate and run/cur arithmetic, ownership/permission re-check of
    USER writable leaves from the probe, disjointness of kernel src vs user dst, and
    DAIF-masked SVC single-core protection), and (c) why safer alternatives were
    rejected (no existing safe wrapper can perform a zero-copy, checked, in-place
    copy into a mapped user page with the required permission re-checks and SVC
    interrupt masking). Also add an audit entry referencing UNSAFE-2026-0030 into
    docs/audits/unsafe-log.md with the same rationale and the function/context names
    (mentioning copy_from_user and the core::ptr::copy usage) so the audit can be
    traced.
    

    @cemililik cemililik merged commit f21eece into main May 31, 2026
    7 checks passed
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Labels

    None yet

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    1 participant