Skip to content

T-026 — current-task capability table + per-task window in syscall_entry (B6 gate #3)#40

Merged
cemililik merged 6 commits into
mainfrom
t-026-current-task-cap-table
May 31, 2026
Merged

T-026 — current-task capability table + per-task window in syscall_entry (B6 gate #3)#40
cemililik merged 6 commits into
mainfrom
t-026-current-task-cap-table

Conversation

@cemililik
Copy link
Copy Markdown
Collaborator

@cemililik cemililik commented May 31, 2026

T-026 — current-task capability table + per-task window in syscall_entry (B6 gate #3)

Closes the last T-021 carry-forward gategate #3. With gate #1 (T-025) + gate #2 (T-023) already merged, all three are now closed, so the B6 wire-up can run a real EL0 task. No new ADR (rides ADR-0030 §step-7 + ADR-0021 + ADR-0014).

What it does

syscall_entry no longer resolves against a fixed kernel-stub table + the whole-RAM window. It sources the running EL0 task's own capability table, address space, and user-access window from the scheduler's current task, and fails closed when there is none:

  • Scheduler bindings (kernel/src/sched/mod.rs): two parallel arrays mirroring task_address_space_handlestask_cap_tables: [Option<*mut CapabilityTable>; N] (raw ptr, BSP-owned, ADR-0021 bridge) + task_user_windows: [Option<UserAccessWindow>; N] ([entry_va, stack_top_va)), both written by add_user_task (new cap_table param). New current_user_table() / current_address_space_handle() / current_user_window() accessors return None (the fail-closed signal) for no-current / unbound slots. The raw ptr makes Scheduler !Send/!Sync — absorbed by the BSP's unconditionally-Sync StaticCell<Scheduler>, reached via the ADR-0021 *mut Scheduler bridge.
  • syscall_entry rewire (bsp-qemu-virt/src/syscall.rs): sources table/AS/window from SCHED.current; with no current task → the empty FAILCLOSED_TABLE (every lookup → InvalidHandle) + an empty window + a never-dereferenced bootstrap-AS placeholder. AddressSpace::inner() widened to pub (read-only — grants no mutation, so no cap-gate bypass) so the BSP can pass the task's &QemuVirtAddressSpace to gate-Development #1 Mmu::translate.
  • Control-plane fail-closed (H2) lands in the dispatcher (kernel/src/syscall/dispatch.rs), not a BSP short-circuit — the BSP can't construct the #[non_exhaustive] SyscallError, and the dispatcher version is host-testable: SyscallContext gains has_current_task: bool; task_yield/task_exit (which consult no capability, so the empty table can't guard them) → InvalidHandle when it is false.

Smoke (maintainer chose: fail-closed demo)

The +0x200 stub smoke is re-sequenced after SCHED is published (so SCHED.current is None) and now demonstrates gate-#3 fail-closed: console_writeInvalidHandle (status 0x102), bad-numberBadSyscallNumber; 2 SVC exceptions, clean ERET, zero new fault class. SYSCALL_STUB_TABLE retired → the empty FAILCLOSED_TABLE. (The B5 cap-mint + greeting are superseded — a syscall with no running task being rejected is the security property worth showing; the real positive EL0 +0x400 round-trip is the wire-up.)

Pre-implementation reviews (relayed, verified against the live tree)

Two reviews hardened the design before/early implementation — applied: H1 persist the per-task window in a scheduler array (the LoadedImage span was reachable nowhere); H2 control-plane fail-closed (above); M4 mandatory (not conditional) UNSAFE-2026-0014 amendment. This PR also carries a small T-025 gate-#1 follow-up (commit 6cbb684): a BlockMapped copy-path test + an AF-check audit note (2 valid; the DEVICE|USER finding forward-flagged to cap_map).

Commit arc

commit what
54a1795 step A — scheduler task_cap_tables / task_user_windows + accessors + add_user_task param
6cbb684 T-025 gate-#1 review follow-up (BlockMapped test + AF audit note)
15a9d80 AddressSpace::inner()pub (read-only; gate-#3 prep)
(this) step B+C — syscall_entry rewire + fail-closed + control-plane gate + smoke re-sequence + tests + audit amendments + docs

Gates (all green)

  • cargo fmt --all --check
  • host + kernel clippy -D warnings
  • host tests: kernel 257 / hal 46 / test-hal 58 / 3 doc ✅ (new: current_accessors_resolve_running_task_bindings_or_none, task_{yield,exit}_with_no_current_task_fails_closed)
  • cargo kernel-build
  • QEMU smoke PASS — boot to all tasks complete; 2 SVC exceptions, clean ERET, zero new fault class; gate-Development #3 fail-closed shown ✅
  • cargo +nightly miri test --workspace --exclude tyrne-bsp-qemu-virt0 UB

Audit

UNSAFE-2026-0014 Amendment (mandatory — the syscall_entry &mut *table_ptr cap-table deref; second-reviewer required) + UNSAFE-2026-0029 Amendment (the syscall-arc statics change + smoke re-sequence). No new unsafe entries.

Out of scope (follow-ups)

  • tyrne-user + userland/hello + the cargo → objcopy → include_bytes! build pipeline (ADR-0029 §Build pipeline).
  • The EL0 +0x400 wire-up smoke + B6 closure (Phase B retrospective).

⚠️ Security-relevant — capability-table sourcing + the EL0 syscall boundary. Per the Definition of done it awaits the explicit EL0-boundary security review (with gate #1's UNSAFE-2026-0030 / gate #2's UNSAFE-2026-0032) before a real EL0 task is wired.

🤖 Generated with Claude Code

Summary by Sourcery

Wire syscall handling to use the scheduler’s current EL0 task context (capability table, address space, and user window) and make syscalls fail-closed when no task is running, completing gate #3 for the B6 EL0 wire-up.

Enhancements:

  • Track per-task capability tables and user-access windows in the scheduler and expose current-task accessors for syscall handling.
  • Rewrite syscall_entry to derive caller capability table, address space, and user-access window from the current task, with an empty fail-closed table and window when no task is current, and make AddressSpace::inner publicly readable for MMU translation.
  • Add a dispatcher-level has_current_task guard so task_yield and task_exit reject with InvalidHandle when no EL0 task is current, preserving control-plane fail-closed semantics.
  • Replace the old syscall stub capability table with an empty fail-closed table and update the syscall smoke test to run after scheduler init and demonstrate the no-current-task fail-closed behaviour.
  • Extend the unsafe-audit log and documentation to cover the new cap-table pointer dereference and scheduler-sourced syscall context, including updated roadmap/task status.

Documentation:

  • Update T-026 task analysis, roadmap, and unsafe-log documentation to reflect the new current-task-based syscall sourcing, fail-closed behaviour, and the closure of T-021 gate Development #3.

Tests:

  • Add scheduler and syscall-dispatch tests for current-task accessors and control-plane fail-closed behaviour, and extend user-access tests to cover block-mapped translation faults in the copy-from-user path.

Summary by CodeRabbit

  • Bug Fixes

    • Syscalls now reliably fail closed when no user task is active; control-plane syscalls are blocked in that case.
  • Refactor

    • Scheduler now records per-task capability/address-space context and exposes it for syscall handling.
    • Fail-closed fallback table is initialized earlier to ensure safe behavior during startup.
  • Documentation

    • Roadmap, task spec, and unsafe-audit records updated to reflect these security/safety changes.
  • Tests

    • New and expanded unit and smoke tests validate fail-closed behavior and user-copy error handling.

cemililik and others added 4 commits May 31, 2026 17:41
…ings + accessors (gate #3)

Adds two scheduler parallel arrays (mirroring task_address_space_handles): task_cap_tables (Option<*mut CapabilityTable> — raw ptr, BSP-owned, ADR-0021 bridge; the H1 review fix) + task_user_windows (Option<UserAccessWindow> = [entry_va, stack_top_va), built from add_user_task's existing user_entry/user_sp). add_user_task gains a cap_table param and records both bindings. New pub accessors current_user_table() / current_address_space_handle() / current_user_window() resolve self.current -> slot -> the arrays, returning None (the fail-closed signal) for no-current / unbound slots. The raw ptr makes Scheduler !Send/!Sync; it only ever lives inside the BSP's unconditionally-Sync StaticCell, reached via the *mut Scheduler bridge — no Send/Sync bound broken. No consumer yet (the syscall_entry rewire + fail-closed dispatch is step B).

Refs: T-026

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… test + AF-check audit note

Post-merge security review of gate #1 (PR #39) verdict: confused-deputy closed, all-or-nothing clean, lock-shut decoder, read-only translate, fail-closed — all verified. Two minor findings actioned: (BULGU-1) add copy_from_user_block_mapped_page_faults — the probe maps every translate error (incl. BlockMapped) to FaultAddress, but only the NotMapped arm had a copy-path test; uses the existing BlockMappedMmu decorator. (BULGU-3) note in the UNSAFE-2026-0025 amendment that the AF=0->NotMapped filter (d0e5a17) is a pure read-only guard (no write site / frame alloc / new invariant). Skipped (BULGU-2): Mmu::map allowing DEVICE|USER is not a defect here — the copy-time USER check is correct; the proper fix is at map-time (cap_map rejecting DEVICE|USER), a future B6+ item, and no v1 leaf is DEVICE|USER.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…late consumers (T-026 gate #3 prep)

syscall_entry (gate #3) needs the running task's concrete &M::AddressSpace to pass to the gate-#1 Mmu::translate copy-user path. inner() is &self (shared) so it grants no mutation — all map/unmap still flow through the cap-gated wrappers via inner_mut; exposing the immutable view bypasses no capability check.

Refs: T-026

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

syscall_entry sources the running EL0 task's capability table + address space + user-access window from SCHED.current (scheduler accessors landed in step A), failing closed to the empty FAILCLOSED_TABLE + empty window when no task is current. AddressSpace::inner() widened to pub (read-only) so the BSP can pass the task's &QemuVirtAddressSpace to gate-#1 Mmu::translate. The H2 control-plane fail-closed lands in the dispatcher via SyscallContext.has_current_task (host-testable; the BSP can't construct the #[non_exhaustive] SyscallError): task_yield/task_exit -> InvalidHandle when no task is current. The +0x200 smoke is re-sequenced after SCHED init (current=None) and now demonstrates gate-#3 fail-closed (console_write -> InvalidHandle 0x102); SYSCALL_STUB_TABLE retired -> the empty FAILCLOSED_TABLE. Closes the last T-021 carry-forward gate (#1 T-025 + #2 T-023 + #3 T-026 all done).

Gates: fmt; host+kernel clippy -D warnings; host tests kernel 257 / hal 46 / test-hal 58 / 3 doc (new: current_accessors_resolve_running_task_bindings_or_none, task_{yield,exit}_with_no_current_task_fails_closed); kernel build; QEMU smoke PASS (2 SVC, clean ERET, zero new fault class); Miri 0 UB. Mandatory UNSAFE-2026-0014 Amendment (cap-table deref) + UNSAFE-2026-0029 Amendment (statics + smoke re-sequence).

Refs: T-026

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

Rewires syscall_entry to source capability tables, address spaces, and per-task user-access windows from the scheduler’s current EL0 task instead of a fixed kernel stub table, introduces per-task cap-table and window bindings in the scheduler, adds dispatcher-level fail-closed handling for control-plane syscalls when no task is current, retires the stub syscall table in favor of an empty fail-closed table, and updates tests, unsafe audit entries, and documentation to reflect gate #3 closure.

Sequence diagram for syscall_entry sourcing current-task capabilities and failing closed

sequenceDiagram
    participant Task as EL0_or_stub_caller
    participant SE as syscall_entry
    participant S as Scheduler
    participant ASArena as AS_ARENA
    participant FCT as FAILCLOSED_TABLE
    participant BOOTAS as BOOTSTRAP_AS
    participant D as dispatch

    Task->>SE: SVC #0
    SE->>S: current_user_table()
    SE->>S: current_address_space_handle()
    SE->>S: current_user_window()

    alt current task bound
        SE->>ASEntry: get_address_space(AS_ARENA, handle)
        ASEntry-->>SE: Some(AddressSpace) or None
        alt AddressSpace found
            SE->>D: dispatch(SyscallContext {\n  caller_table: &mut *table_ptr,\n  user_window: window,\n  task_as: as.inner(),\n  has_current_task: true\n})
        else AddressSpace missing
            SE->>BOOTAS: assume_init_ref()
            SE->>D: dispatch(SyscallContext {\n  caller_table: &mut *table_ptr,\n  user_window: UserAccessWindow::empty(),\n  task_as: &*BOOTSTRAP_AS,\n  has_current_task: true\n})
        end
    else no current task
        SE->>FCT: assume_init_mut()
        SE->>BOOTAS: assume_init_ref()
        SE->>D: dispatch(SyscallContext {\n  caller_table: &mut FAILCLOSED_TABLE,\n  user_window: UserAccessWindow::empty(),\n  task_as: &*BOOTSTRAP_AS,\n  has_current_task: false\n})
    end

    D-->>SE: SyscallEffect
    SE-->>Task: resume / reschedule / terminate based on SyscallEffect
Loading

File-Level Changes

Change Details Files
Add per-task capability-table and user-window bindings plus accessors to the Scheduler and thread them through add_user_task.
  • Introduce task_cap_tables and task_user_windows arrays parallel to task_address_space_handles, initialized to None.
  • Extend unsafe add_user_task to accept a mut CapabilityTable, store it in task_cap_tables, and derive a UserAccessWindow from user_entry/user_sp into task_user_windows.
  • Add current_user_table, current_address_space_handle, and current_user_window accessors that resolve the current TaskHandle to the corresponding per-task bindings or None.
  • Update scheduler tests to assert that add_user_task records the cap-table pointer and user window, and that the current_ accessors return None without a current task and Some(...) once current is set.
kernel/src/sched/mod.rs
Rewire syscall_entry to use the current task’s capability table, address space, and per-task user window, and introduce a fail-closed path with an empty table and window when no task is current.
  • Remove the global SYSCALL_STUB_TABLE and the fixed whole-RAM SYSCALL_USER_WINDOW_LEN; add a global empty FAILCLOSED_TABLE instead.
  • In syscall_entry, read SCHED.current_user_table/current_address_space_handle/current_user_window, resolve the address space via AS_ARENA, and construct SyscallContext with caller_table = &mut *table_ptr or FAILCLOSED_TABLE, user_window from the current window or UserAccessWindow::empty(), task_as from the current address space or BOOTSTRAP_AS, and has_current_task set from the presence of a current table.
  • Extend the syscall_entry safety and invariants docs to cover the new statics (SCHED, MMU, AS_ARENA, FAILCLOSED_TABLE, BOOTSTRAP_AS) and explicitly describe the cap-table-pointer dereference as an UNSAFE-2026-0014 amendment.
  • Adjust SyscallEffect handling comments for TaskYield/Terminate to clarify they are only reachable with a current task and remain dormant until the B6 wire-up.
bsp-qemu-virt/src/syscall.rs
bsp-qemu-virt/src/main.rs
Move control-plane fail-closed behavior for task_yield/task_exit into the dispatcher, keyed off a new has_current_task flag in SyscallContext.
  • Extend SyscallContext with a has_current_task: bool field documenting its role as the control-plane gate for task_yield/task_exit.
  • Change dispatch so TaskYield and TaskExit return Reschedule/Terminate only when has_current_task is true; otherwise they return Resume(SyscallReturn::error(CapError::InvalidHandle)).
  • Update existing syscall dispatch tests to populate has_current_task = true and add new tests that assert task_yield/task_exit with has_current_task = false yield Resume with InvalidHandle rather than Reschedule/Terminate.
kernel/src/syscall/dispatch.rs
Re-sequence and repurpose the syscall boundary smoke test to demonstrate fail-closed behavior with no current task, and wire it after SCHED initialization.
  • Replace the stub-focused syscall_boundary_smoke with a fail-closed demo: initialise FAILCLOSED_TABLE as an empty CapabilityTable, issue console_write and a bad-number syscall from EL1 with SCHED.current == None, and assert the statuses are InvalidHandle and BadSyscallNumber, respectively, updating the console log message accordingly.
  • Move syscall_boundary_smoke invocation from before scheduler setup to immediately after SCHED is published but before start(), ensuring syscall_entry sees a valid scheduler with no current task.
  • Update module-level docs in syscall.rs and main.rs to describe the new fail-closed smoke semantics and its relationship to B6’s future EL0 +0x400 wire-up.
bsp-qemu-virt/src/main.rs
bsp-qemu-virt/src/syscall.rs
Expose AddressSpace::inner() publicly (read-only) so BSP code can pass concrete address-space references to Mmu::translate without bypassing capability checks.
  • Change AddressSpace::inner from pub(crate) const fn to pub const fn and update its documentation to clarify that it returns a shared reference suitable for Mmu::translate/activate and does not grant mutation (which still flows through inner_mut via caps).
kernel/src/mm/address_space.rs
Tighten user-access copy tests and add coverage for BlockMapped translation results via a BlockMappedMmu helper.
  • Introduce a new test using BlockMappedMmu to ensure copy_from_user treats a block-mapped leaf as FaultAddress and copies no bytes.
  • Import BlockMappedMmu and supporting types (Mmu, PhysAddr, PhysFrame, VirtAddr) into syscall::user_access tests to support the new coverage.
kernel/src/syscall/user_access.rs
Update T-026 analysis, unsafe audit log, and roadmap docs to reflect the implemented design, gate #3 closure, and new unsafe-site amendments.
  • Mark all T-026 acceptance criteria as completed, and expand the implementation section to detail the scheduler bindings, syscall_entry rewire, dispatcher has_current_task gating, smoke decision, and mandatory UNSAFE-2026-0014/0029 amendments.
  • Add an UNSAFE-2026-0014 amendment describing the syscall_entry cap-table-pointer dereference and its invariants, and extend UNSAFE-2026-0029 to cover the new statics and smoke re-sequencing.
  • Update current roadmap and phase-b documentation with a gate Development #3 closure banner and notes that syscall_entry now sources per-task state from the scheduler and uses FAILCLOSED_TABLE when no task is current.
docs/analysis/tasks/phase-b/T-026-current-task-cap-table.md
docs/audits/unsafe-log.md
docs/roadmap/current.md
docs/roadmap/phases/phase-b.md

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: 0aea91ae-c02c-4bf2-b254-50a438c4c65a

📥 Commits

Reviewing files that changed from the base of the PR and between f6bc9db and a76d52d.

📒 Files selected for processing (1)
  • kernel/src/syscall/dispatch.rs

📝 Walkthrough

Walkthrough

This PR implements B6 gate #3: Scheduler records per-task capability-table pointers and user windows; syscall_entry sources caller table/address-space/window from Scheduler and fails closed to an empty table/window when no task is current; dispatcher gates control-plane syscalls on task presence; boot smoke and tests verify fail-closed behavior.

Changes

Gate #3: Current Task Capability Table Sourcing and Fail-Closed Behavior

Layer / File(s) Summary
Scheduler per-task capability-table and user-window storage
kernel/src/sched/mod.rs
Scheduler<C> gains per-slot optional raw CapabilityTable pointers and optional UserAccessWindow; add_user_task(cap_table: *mut CapabilityTable) records bindings (null → None), initializes arrays to None, and exposes current_user_table(), current_address_space_handle(), and current_user_window(); tests validate registration and accessors.
Syscall entry gate #3 rewiring (scheduler → current-task state)
kernel/src/mm/address_space.rs, bsp-qemu-virt/src/syscall.rs
AddressSpace::inner() made pub for read-only access; syscall_entry now sources caller capability-table pointer, user window, and address-space handle from Scheduler current-task accessors, falling back to FAILCLOSED_TABLE, UserAccessWindow::empty(), and BOOTSTRAP_AS when incomplete; SyscallContext includes has_current_task; docs updated.
Dispatcher control-plane syscall fail-closed gating
kernel/src/syscall/dispatch.rs
SyscallContext adds has_current_task: bool; TaskYield and TaskExit return scheduling directives only when has_current_task is true, otherwise return Resume with InvalidHandle; tests updated and new tests added for fail-closed control-plane behavior.
Boot-sequence fail-closed smoke test
bsp-qemu-virt/src/main.rs
Adds FAILCLOSED_TABLE static; rewrites syscall_boundary_smoke to exercise gate #3 fail-closed paths (two SVCs) and assert non-OK_STATUS; initializes FAILCLOSED_TABLE during kernel setup and repositions smoke to run after SCHED publication but before start().
Documentation, audit, and test coverage updates
docs/analysis/tasks/phase-b/T-026-current-task-cap-table.md, docs/audits/unsafe-log.md, docs/roadmap/current.md, docs/roadmap/phases/phase-b.md, kernel/src/syscall/user_access.rs
T-026 moved to In Review with security-review flag and acceptance criteria; UNSAFE amendments recorded for cap-table pointer dereference, MMU translate read-only + AF filter, and syscall_entry sourcing; roadmap updated; new unit test ensures copy_from_user faults on block-mapped L3 leaves.

Sequence Diagram(s)

sequenceDiagram
  Participant EL0 as EL0 (user)
  Participant EL1 as syscall_entry (EL1)
  Participant Sched as Scheduler
  Participant Dispatch as dispatch
  Participant Table as FAILCLOSED_TABLE / CapabilityTable

  EL0->>EL1: SVC (syscall)
  EL1->>Sched: read current_user_table(), current_user_window(), current_address_space_handle()
  Sched-->>EL1: Option<cap_table>, Option<window>, Option<AS>
  EL1->>Table: choose FAILCLOSED_TABLE or deref cap_table
  EL1->>Dispatch: dispatch(SyscallContext{has_current_task})
  Dispatch-->>EL1: SyscallEffect (Resume/Reschedule/Terminate / InvalidHandle)
  EL1-->>EL0: return status in x0
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • HodeTech/Tyrne#36: Overlaps boot/runtime and syscall plumbing (kernel_main_high sequencing and syscall dispatcher wiring) that this PR builds upon.
  • HodeTech/Tyrne#28: Related address-space infrastructure and address-space resolution used by syscall_entry.
  • HodeTech/Tyrne#37: Related changes to add_user_task and EL0 entry context wiring used by this PR.

Poem

🐰 A table waits where tasks might be,
when none hop in the gate stays free—
an empty shelf, a guarded door,
safe calls fail-closed forevermore. 🥕

🚥 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 directly and specifically describes the main change: rewiring syscall entry to use current-task capability table and per-task window for B6 gate #3.
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-026-current-task-cap-table

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 T-026 (gate #3) fail-closed fallback mechanism, transitioning the syscall entry path to resolve capability tables, address spaces, and user-access windows dynamically from the scheduler's current task instead of using a static stub table. When no task is running, the system fails closed using an empty fallback table and window, and control-plane syscalls are rejected. However, a critical security issue was identified: initializing the fallback FAILCLOSED_TABLE inside the diagnostic syscall_boundary_smoke function couples a core security mechanism to a test. If the smoke test is disabled or removed, the table will remain uninitialized, leading to undefined behavior during a fail-closed syscall. This initialization should be moved to the core kernel setup.

Comment thread bsp-qemu-virt/src/main.rs Outdated
Comment on lines +739 to +743
// SAFETY: `FAILCLOSED_TABLE` lives in `.bss`; this is its single write,
// before any `SVC` issues. Audit: UNSAFE-2026-0010 (StaticCell pattern).
unsafe {
(*FAILCLOSED_TABLE.0.get()).write(CapabilityTable::new());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-high high

Initializing FAILCLOSED_TABLE inside syscall_boundary_smoke couples a critical kernel security fallback mechanism to a diagnostic smoke test. If syscall_boundary_smoke is ever disabled, bypassed, or removed in production, FAILCLOSED_TABLE will remain uninitialized, leading to undefined behavior (reading uninitialized memory via assume_init_mut()) on any fail-closed syscall.

FAILCLOSED_TABLE should be initialized as part of the core kernel/BSP setup in kernel_main_high alongside other system structures (like SCHED and EP_ARENA), rather than inside a smoke test.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in f6bc9db. FAILCLOSED_TABLE is now initialised in core kernel_main_high setup (alongside EP_ARENA/IPC_QUEUES), not inside syscall_boundary_smoke, with an explicit ordering invariant. The security fallback is now live independent of whether the diagnostic smoke runs — closing exactly the "smoke removed → uninitialised .bss read via assume_init_mut()" UB you flagged. Good catch, thank you.

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 left some high level feedback:

  • The logic in syscall_entry that pulls current_user_table / current_address_space_handle / current_user_window and wires up the fail-closed defaults is getting dense; consider factoring that into a small helper that returns { caller_table, task_as, user_window, has_current_task } so the unsafe deref + invariants are localized and easier to audit.
  • has_current_task is derived from current_user_table().is_some(), which effectively equates "running EL0 task" with "has a cap table"; if you ever introduce task types that should be allowed to task_yield/task_exit without a cap table, this will silently treat them as "no current task"—it may be worth basing this flag on an explicit "is EL0"/"is schedulable" property instead of the cap-table binding.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The logic in `syscall_entry` that pulls `current_user_table` / `current_address_space_handle` / `current_user_window` and wires up the fail-closed defaults is getting dense; consider factoring that into a small helper that returns `{ caller_table, task_as, user_window, has_current_task }` so the unsafe deref + invariants are localized and easier to audit.
- `has_current_task` is derived from `current_user_table().is_some()`, which effectively equates "running EL0 task" with "has a cap table"; if you ever introduce task types that should be allowed to `task_yield`/`task_exit` without a cap table, this will silently treat them as "no current task"—it may be worth basing this flag on an explicit "is EL0"/"is schedulable" property instead of the cap-table binding.

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.

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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
bsp-qemu-virt/src/main.rs (1)

735-795: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Assert the expected fail-closed results.

Line 794 reports success unconditionally. If either SVC starts returning the wrong status, this smoke still prints ok, so it won’t catch a gate #3 regression.

Suggested fix
     unsafe {
         core::arch::asm!(
             "svc `#0`",
             in("x8") 0u64,
             out("x0") bad_status,
             out("x1") _,
             out("x2") _,
             out("x3") _,
             out("x4") _,
             out("x5") _,
             out("x6") _,
             out("x7") _,
         );
     }
+
+    assert_eq!(
+        status, 0x102,
+        "syscall smoke: console_write without a current task must fail closed with InvalidHandle",
+    );
+    assert_eq!(
+        bad_status, 0x1,
+        "syscall smoke: reserved syscall number must return BadSyscallNumber",
+    );
 
     let mut w = FmtWriter(console);
     let _ = writeln!(
         w,
         "tyrne: syscall smoke ok (gate `#3` fail-closed — no current task: console_write status={status:`#x`}; bad-number status={bad_status:`#x`})"
🤖 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 `@bsp-qemu-virt/src/main.rs` around lines 735 - 795, The test prints "ok"
unconditionally; update syscall_boundary_smoke to assert the SVC results instead
of always reporting success: after the first SVC compare status against the
expected fail-closed error constant (the InvalidHandle/appropriate SyscallError
value used by the dispatcher) and fail/log/panic if it differs, and after the
second SVC compare bad_status against the SyscallError::BadSyscallNumber value
and fail/log/panic if it differs; use the existing local variables status and
bad_status and keep the final writeln only after both checks pass so the message
truthfully reflects success.
🧹 Nitpick comments (1)
kernel/src/sched/mod.rs (1)

444-453: ⚡ Quick win

Add a debug-time null check for cap_table.

add_user_task stores cap_table unconditionally, but syscall_entry later treats any Some(table_ptr) as dereferenceable. A null pointer here becomes “current task present” plus immediate UB on the first syscall.

Suggested change
 pub unsafe fn add_user_task(
     &mut self,
     cpu: &C,
     handle: TaskHandle,
     address_space_handle: AddressSpaceHandle,
     user_entry: usize,
     user_sp: usize,
     kernel_stack_top: *mut u8,
     cap_table: *mut CapabilityTable,
 ) -> Result<(), SchedError> {
+    debug_assert!(
+        !cap_table.is_null(),
+        "add_user_task: cap_table must be non-null",
+    );
     let idx = handle.slot().index() as usize;
🤖 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/sched/mod.rs` around lines 444 - 453, In add_user_task, add a
debug-time null-check for the cap_table pointer (e.g.
debug_assert!(!cap_table.is_null(), "cap_table is null in add_user_task")) and
also defensively handle a null pointer when storing it so syscall_entry won't
treat a null as a valid dereferenceable pointer; either return an
Err(SchedError::...) on null or store None instead of Some(cap_table) (update
the field usage in syscall_entry to accept Option<*mut CapabilityTable>
accordingly). Ensure references to add_user_task and syscall_entry are updated
so the runtime never unconditionally treats a null cap_table as dereferenceable.
🤖 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/syscall.rs`:
- Around line 165-194: The syscall context currently mixes live and fallback
pieces; change the logic so the "current-task binding" is all-or-nothing:
compute a single boolean (e.g., has_full_binding) that is true only if
current_table.is_some(), current_window.is_some(), and the AS lookup (the match
using current_as -> tyrne_kernel::mm::get_address_space(arena, h)) succeeds; if
has_full_binding is false, set caller_table to FAILCLOSED_TABLE
(assume_init_mut), user_window to UserAccessWindow::empty(), task_as to
BOOTSTRAP_AS (assume_init_ref), and has_current_task to false, otherwise use the
existing live values (caller_table from current_table, user_window from
current_window.unwrap(), task_as from the successful AS lookup, and
has_current_task = true). Update references around
current_table/current_window/current_as, task_as, caller_table, and
has_current_task accordingly.

In `@kernel/src/syscall/user_access.rs`:
- Around line 508-526: The test fails to compile because the call to
BlockMappedMmu::create_address_space is missing a closing parenthesis; update
the test so the unsafe block that assigns `as_` properly closes the
create_address_space(...) call and the unsafe block (i.e., ensure the expression
`unsafe {
mmu.create_address_space(PhysFrame::from_aligned(PhysAddr(0x1000)).unwrap()) }`
is syntactically complete), referencing the `BlockMappedMmu`,
`create_address_space`, `PhysFrame::from_aligned`, and local variable `as_`.

---

Outside diff comments:
In `@bsp-qemu-virt/src/main.rs`:
- Around line 735-795: The test prints "ok" unconditionally; update
syscall_boundary_smoke to assert the SVC results instead of always reporting
success: after the first SVC compare status against the expected fail-closed
error constant (the InvalidHandle/appropriate SyscallError value used by the
dispatcher) and fail/log/panic if it differs, and after the second SVC compare
bad_status against the SyscallError::BadSyscallNumber value and fail/log/panic
if it differs; use the existing local variables status and bad_status and keep
the final writeln only after both checks pass so the message truthfully reflects
success.

---

Nitpick comments:
In `@kernel/src/sched/mod.rs`:
- Around line 444-453: In add_user_task, add a debug-time null-check for the
cap_table pointer (e.g. debug_assert!(!cap_table.is_null(), "cap_table is null
in add_user_task")) and also defensively handle a null pointer when storing it
so syscall_entry won't treat a null as a valid dereferenceable pointer; either
return an Err(SchedError::...) on null or store None instead of Some(cap_table)
(update the field usage in syscall_entry to accept Option<*mut CapabilityTable>
accordingly). Ensure references to add_user_task and syscall_entry are updated
so the runtime never unconditionally treats a null cap_table as dereferenceable.
🪄 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: 09ab1e28-c5aa-4ca8-9b00-d277ac6d60e7

📥 Commits

Reviewing files that changed from the base of the PR and between f21eece and 385f4b4.

📒 Files selected for processing (10)
  • bsp-qemu-virt/src/main.rs
  • bsp-qemu-virt/src/syscall.rs
  • docs/analysis/tasks/phase-b/T-026-current-task-cap-table.md
  • docs/audits/unsafe-log.md
  • docs/roadmap/current.md
  • docs/roadmap/phases/phase-b.md
  • kernel/src/mm/address_space.rs
  • kernel/src/sched/mod.rs
  • kernel/src/syscall/dispatch.rs
  • kernel/src/syscall/user_access.rs

Comment thread bsp-qemu-virt/src/syscall.rs Outdated
Comment thread kernel/src/syscall/user_access.rs
…ILCLOSED in core setup + smoke asserts + null guard

Addresses the PR #40 review (4 valid fixes; 2 findings skipped with reason):

- syscall_entry: fold has_current_task into the same all-or-nothing match as the
  data-plane triple (table + user window + generation-checked AS). An incomplete
  running-task binding now also sets has_current_task=false, so the control-plane
  syscalls (task_yield/task_exit, which consult no capability) are rejected too —
  not just the data-plane. The whole caller context is all-or-nothing fail-closed.
- main.rs: relocate FAILCLOSED_TABLE init from syscall_boundary_smoke into core
  kernel_main_high setup (alongside EP_ARENA/IPC_QUEUES), with an explicit
  ordering invariant, so the security fallback is live independent of whether the
  diagnostic smoke runs (removes a 'smoke removed -> uninit .bss UB' footgun).
- main.rs: syscall_boundary_smoke now ASSERTS both SVC statuses != OK_STATUS
  before printing (a fail-closed regression would over-grant and report OK,
  panicking the boot before 'all tasks complete') instead of printing 'ok'
  unconditionally.
- sched::add_user_task: debug_assert!(!cap_table.is_null()) + defensive
  (!cap_table.is_null()).then_some(cap_table) store, so a # Safety-contract
  violation degrades to None -> fail-closed instead of a Some(null) deref.
- dispatch.rs: refresh the SyscallContext.has_current_task doc to the
  all-or-nothing semantics.

Skipped: (a) 'BlockMapped test missing paren / won't compile' — the test compiles
and passed; (b) 'base has_current_task on an explicit is-EL0 property' — in v1
only EL0 tasks (which always have a cap table) issue syscalls, so the equivalence
holds (forward-flagged).

Adversarially verified before commit: a 5-lens review workflow (all-or-nothing
correctness, init-ordering, null-store, smoke-assert, audit-accuracy) found no
real issues — the all-or-nothing lens cleared all 8 binding-state combinations.

UNSAFE-2026-0014 + 0029 review-round amendments. Gates: fmt; host+kernel clippy
-D warnings; host tests kernel 257 / hal 46 / test-hal 58 / 3 doc; kernel build;
QEMU smoke PASS (asserts hold: console_write 0x102, bad-number 0x1; 2 SVC, clean
ERET, zero new fault); Miri 0 UB (kernel 257).

Refs: T-026

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

Review-round resolution (f6bc9db)

Thanks all. Verified each finding against the current tree; 4 fixed, 2 skipped with reason, plus 2 doc-accuracy nits closed.

Fixed

  • FAILCLOSED_TABLE init coupling (gemini, security-high) → relocated to core kernel_main_high setup with an explicit ordering invariant; live independent of the smoke.
  • All-or-nothing binding (coderabbit, Major) → syscall_entry resolves table + window + generation-checked AS as one unit; any miss drops to empty table + empty window + has_current_task = false (control-plane rejected too).
  • Smoke assertssyscall_boundary_smoke now assert!s both SVC statuses != OK_STATUS (a fail-closed regression returning OK would panic the boot) instead of printing 'ok' unconditionally.
  • add_user_task null guarddebug_assert!(!cap_table.is_null()) + defensive (!cap_table.is_null()).then_some(cap_table) store (null → None → fail-closed, never Some(null)).

Skipped (with reason)

  • "Syntax error: missing closing paren" in the BlockMapped test (coderabbit, Critical) → false positive; the test compiles and passes host-test + Miri (replied inline).
  • Base has_current_task on an explicit is-EL0 property (sourcery) → in v1 only EL0 tasks (which always have a cap table via add_user_task) issue syscalls, so cap-table-presence ≡ schedulable-EL0; forward-flagged for if/when a cap-table-less schedulable task type is introduced.

On the "factor the binding into a helper" note (sourcery): the single all-or-nothing match now localizes the whole resolution (table/window/AS + has_current_task) in one place; a separate fn would have to thread the &mut/lifetime of the table-pointer deref and the static borrows through its signature, so the inline match reads cleaner here. The unsafe deref + its invariants are documented in syscall_entry's # Safety and the UNSAFE-2026-0014 amendment.

Verification: before committing I ran an adversarial 5-lens review pass (all-or-nothing correctness, init-ordering, null-store chain, smoke-assert soundness, audit accuracy) — no real issues; the all-or-nothing lens cleared all 8 binding-state combinations. Gates: fmt; host + kernel clippy -D warnings; host tests kernel 257 / hal 46 / test-hal 58 / 3 doc; kernel build; QEMU smoke PASS (asserts hold — console_write 0x102, bad-number 0x1; 2 SVC, clean ERET, zero new fault); Miri 0 UB.

…closed on both planes

Closes the one 'Kısmi' (partial) item from the review verification: there was
no host test directly modelling the context the BSP syscall_entry produces on an
incomplete running-task binding (empty FAILCLOSED_TABLE + empty window +
has_current_task=false). The BSP match that assembles it is no_std/no_main and
not host-testable directly, but the dispatcher's handling of that exact context
is — and the prior suite only covered the planes separately (console_write with
has_current_task=true; task_yield with has_current_task=false), never the
combined fail-closed context.

The new test builds that exact context once and asserts BOTH planes fail closed
in it: data-plane console_write -> InvalidHandle (no output, via the empty
table) and control-plane task_yield -> InvalidHandle (via the has_current_task
gate, not Reschedule). debug-gated (console_write number 5 is debug-only).

Gates: fmt; host + kernel clippy -D warnings; kernel tests 258 (+1); Miri 0 UB
on the new test.

Refs: T-026

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cemililik cemililik merged commit a8ce11e 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