-
Notifications
You must be signed in to change notification settings - Fork 0
B5 syscall boundary: error taxonomy (T-020) + EL0→EL1 SVC dispatch (T-021) #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
476710b
docs(adr): propose ADR-0030/0031 — syscall ABI + initial syscall set
cemililik 93d5960
docs(adr): accept ADR-0030/0031 after careful re-read + maintainer re…
cemililik d20e6d0
feat(ipc): split IpcError::InvalidCapability into three typed variants
cemililik 324457a
feat(cap): redact Capability and CapObject Debug to hide object identity
cemililik 4777f9a
test(ipc): pin StaleHandle + WrongObjectKind on ipc_cancel_recv
cemililik 1df1b52
docs(roadmap): T-020 In Review; narrow B5 acceptance to current-EL proxy
cemililik 7b35ed6
test(ipc): make wrong-kind tests actually prove kind-before-rights
cemililik 806c966
feat(syscalls): EL0→EL1 SVC dispatch — trampoline, panic-free dispatc…
cemililik 5145d4d
test(syscalls): T-021 review-round follow-up — dispatch tests + compi…
cemililik d448540
docs(roadmap): record PR #34 (combined T-020 + T-021 B5 review)
cemililik 2c713c0
fix(syscalls): T-021 review-round 2 — overlap-safe copy-user + scope/…
cemililik 1a7deab
audit(syscalls): correct UNSAFE-2026-0030 amendment — disjointness is…
cemililik File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,214 @@ | ||
| //! BSP-side syscall glue: the `SVC` trap frame and the Rust entry the | ||
| //! `vectors.s` sync trampoline calls. | ||
| //! | ||
| //! The architecture-agnostic, panic-free dispatch logic lives in the kernel | ||
| //! ([`tyrne_kernel::syscall`]). This module owns only the **hardware-facing** | ||
| //! half: | ||
| //! | ||
| //! - [`SyscallTrapFrame`] — the `#[repr(C)]` mirror of the register frame the | ||
| //! `tyrne_sync_trampoline` in `vectors.s` saves (`x0`–`x30` + `SP_EL0` + | ||
| //! `ELR_EL1` + `SPSR_EL1`); its field order and offsets must match the asm | ||
| //! `stp` sequence byte-for-byte (a compile-time `size_of` guard catches drift). | ||
| //! - [`syscall_entry`] — reads the syscall number + arguments from the saved | ||
| //! frame, builds a [`SyscallContext`] from the BSP statics, calls | ||
| //! [`tyrne_kernel::syscall::dispatch`], and applies the returned | ||
| //! [`SyscallEffect`] by writing the status + payload back into the frame. | ||
| //! | ||
| //! ## B5 scope and the `0x200` / `0x400` split | ||
| //! | ||
| //! The shared trampoline is installed at **both** sync vector slots — current-EL | ||
| //! (`VBAR_EL1 + 0x200`) and lower-EL-AArch64 (`VBAR_EL1 + 0x400`) — because the | ||
| //! save → dispatch → `ERET` mechanism is privilege-entry-agnostic. In B5 the | ||
| //! only `SVC` comes from an **EL1 kernel-stub** (see `kernel_entry`'s syscall | ||
| //! smoke), which — executing at the *current* EL — takes the `0x200` vector, | ||
| //! **not** the lower-EL `0x400` vector. A real EL0 task taking the `0x400` | ||
| //! vector (with the EL0↔EL1 privilege transition and copy-user against a | ||
| //! separate userspace `TTBR0_EL1`) is verified at runtime in **B6**, per | ||
| //! [ADR-0030 §Simulation row-to-verification mapping][adr-0030]. The `0x400` | ||
| //! handler is installed now so B6 adds only the EL0 task, not new trap plumbing. | ||
| //! | ||
| //! `caller_table` is a dedicated **kernel-stub** capability table in B5 | ||
| //! ([`crate::SYSCALL_STUB_TABLE`]); B6 replaces it with the scheduler's | ||
| //! current-task table once a real EL0 task exists. | ||
| //! | ||
| //! Audit: UNSAFE-2026-0029 (the trap-frame asm + this entry's frame | ||
| //! reads/writes). | ||
| //! | ||
| //! [adr-0030]: https://github.com/HodeTech/Tyrne/blob/main/docs/decisions/0030-syscall-abi.md | ||
|
|
||
| use tyrne_kernel::syscall::{ | ||
| dispatch, SyscallArgs, SyscallContext, SyscallEffect, UserAccessWindow, | ||
| }; | ||
|
|
||
| /// Saved-register frame the `tyrne_sync_trampoline` in `vectors.s` populates | ||
| /// before branching into [`syscall_entry`] on an `SVC`. | ||
| /// | ||
| /// `#[repr(C)]` is **mandatory**: the field order and byte offsets must match | ||
| /// the asm `stp` sequence in `vectors.s` exactly. The frame is 272 bytes total | ||
| /// (`x0`–`x29` as 15 pairs, then `x30`/`SP_EL0`, then `ELR_EL1`/`SPSR_EL1`), | ||
| /// 16-byte SP-aligned. Unlike the IRQ [`TrapFrame`][crate::exceptions::TrapFrame] | ||
| /// (which saves only the AAPCS64 caller-saved set), the syscall frame saves the | ||
| /// **full** general-purpose register file plus `SP_EL0` so it is a complete | ||
| /// snapshot of the trapped context — the shape a real EL0 task (B6) and any | ||
| /// future preemption arc require. | ||
| /// | ||
| /// Fields are private: the only reader/writer is [`syscall_entry`] in this | ||
| /// module, and keeping the raw register snapshot un-`pub` avoids exposing | ||
| /// (or accidentally logging) trapped register contents elsewhere. | ||
| #[repr(C)] | ||
| pub struct SyscallTrapFrame { | ||
| // `x0`–`x29` saved as 15 consecutive pairs at offsets 0x00..0xF0. | ||
| x0_x1: [u64; 2], | ||
| x2_x3: [u64; 2], | ||
| x4_x5: [u64; 2], | ||
| x6_x7: [u64; 2], | ||
| x8_x9: [u64; 2], | ||
| x10_x11: [u64; 2], | ||
| x12_x13: [u64; 2], | ||
| x14_x15: [u64; 2], | ||
| x16_x17: [u64; 2], | ||
| x18_x19: [u64; 2], | ||
| x20_x21: [u64; 2], | ||
| x22_x23: [u64; 2], | ||
| x24_x25: [u64; 2], | ||
| x26_x27: [u64; 2], | ||
| x28_x29: [u64; 2], | ||
| /// `x30` (LR) at 0xF0 and `SP_EL0` at 0xF8. | ||
| x30_sp_el0: [u64; 2], | ||
| /// `ELR_EL1` (return address) at 0x100 and `SPSR_EL1` (saved PSTATE) at 0x108. | ||
| elr_spsr: [u64; 2], | ||
| } | ||
|
|
||
| // The trampoline reserves exactly 272 bytes and writes through fixed offsets | ||
| // mirroring the field order above. A size/layout drift between the asm and this | ||
| // `#[repr(C)]` would corrupt saved registers on every syscall; this guard fails | ||
| // the build before that can ship. (Mirrors the `TrapFrame` 192-byte guard.) | ||
| const _: () = assert!(core::mem::size_of::<SyscallTrapFrame>() == 272); | ||
|
|
||
| /// Length of the syscall copy-from/to-user window in B5: the whole | ||
| /// identity-mapped RAM extent the bootstrap address space covers. | ||
| /// | ||
| /// The B5 EL1 kernel-stub runs on the bootstrap AS, which identity-maps the | ||
| /// managed extent (per [ADR-0027 §Decision outcome (a)]), so the stub's buffer | ||
| /// — a `.rodata`-resident `&[u8]` in the kernel image — is in range. B6's real | ||
| /// EL0 task derives a tighter window from its own mapped region (see | ||
| /// [`UserAccessWindow`]'s module docs). The subtraction is a `const`, so it | ||
| /// cannot wrap at runtime: const-eval rejects an underflow at **build time** | ||
| /// (an inverted extent is a hard compile error, never a release wrap). The | ||
| /// explicit assertion below makes that invariant — and its failure message — | ||
| /// unambiguous rather than relying on a raw "subtract with overflow" const-eval | ||
| /// error. | ||
| const _: () = assert!( | ||
| crate::PMM_EXTENT_END >= crate::PMM_EXTENT_START, | ||
| "PMM extent must be non-inverted: PMM_EXTENT_END >= PMM_EXTENT_START" | ||
| ); | ||
| const SYSCALL_USER_WINDOW_LEN: usize = crate::PMM_EXTENT_END - crate::PMM_EXTENT_START; | ||
|
|
||
| /// Rust entry for the `SVC` sync trampoline (`vectors.s`). | ||
| /// | ||
| /// Reads the syscall number (`x8`) and arguments (`x0`–`x5`) from the saved | ||
| /// `frame`, dispatches through [`tyrne_kernel::syscall::dispatch`], and applies | ||
| /// the resulting [`SyscallEffect`] by writing the status (`x0`) and payload | ||
| /// (`x1`–`x7`) back into the frame. Returns to the trampoline, which restores | ||
| /// the (now result-bearing) frame and `ERET`s. | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// `extern "C"` so the asm trampoline can `bl` it. `frame` is guaranteed valid | ||
| /// by the trampoline (constructed via `stp` immediately before the `bl`, on the | ||
| /// kernel stack); this function dereferences it only inside `unsafe` blocks. | ||
| /// | ||
| /// **Why `unsafe` is required.** The function reads and writes the saved | ||
| /// register frame through a raw `*mut SyscallTrapFrame` (the asm calling | ||
| /// convention passes a pointer, not a `&mut`), and it materialises momentary | ||
| /// references to the write-once BSP statics via `assume_init_{mut,ref}`. | ||
| /// **Invariants upheld.** (1) The four statics it reaches | ||
| /// (`EP_ARENA` / `IPC_QUEUES` / `SYSCALL_STUB_TABLE` / `CONSOLE`) are all | ||
| /// written before the syscall smoke issues any `SVC`; (2) v1 is single-core and | ||
| /// the `SVC` handler runs with interrupts masked (exception entry masks `DAIF`), | ||
| /// so no peer aliases them mid-call; (3) the momentary `&mut`s are scoped to the | ||
| /// single `dispatch` call and do not cross a context switch — the data-plane | ||
| /// syscalls do not switch and the control-plane ones return a directive *before* | ||
| /// any switch, honouring the [ADR-0021] discipline; (4) the frame writes touch | ||
| /// only `x0`–`x7`, leaving the trampoline's restore of `x8`–`x30` + `SP_EL0` + | ||
| /// `ELR_EL1` + `SPSR_EL1` intact. **Rejected alternatives.** Passing a `&mut | ||
| /// SyscallTrapFrame` from the asm is impossible (asm has no Rust references); | ||
| /// holding the BSP statics behind a lock would deadlock the interrupts-masked | ||
| /// handler with no soundness gain under single-core cooperative semantics. | ||
| /// | ||
| /// Audit: UNSAFE-2026-0029 (trap-frame asm + frame access) + UNSAFE-2026-0010 | ||
| /// (`StaticCell` pattern) + UNSAFE-2026-0014 (momentary `&mut` to kernel state). | ||
| #[unsafe(no_mangle)] | ||
| pub unsafe extern "C" fn syscall_entry(frame: *mut SyscallTrapFrame) { | ||
| // SAFETY: `frame` is valid per the trampoline contract above; read the | ||
| // syscall number (x8) and argument words (x0..x5) out of the saved frame. | ||
| // Audit: UNSAFE-2026-0029. | ||
| let args = unsafe { | ||
| let f = &*frame; | ||
| SyscallArgs { | ||
| number: f.x8_x9[0], | ||
| args: [ | ||
| f.x0_x1[0], f.x0_x1[1], f.x2_x3[0], f.x2_x3[1], f.x4_x5[0], f.x4_x5[1], | ||
| ], | ||
| } | ||
| }; | ||
|
|
||
| // SAFETY: build the dispatch context from the write-once BSP statics. All | ||
| // four are initialised in `kernel_entry` before the syscall smoke runs; | ||
| // single-core + interrupts-masked-in-handler means no aliasing; the | ||
| // momentary `&mut`s drop at the end of the `dispatch` call and never cross a | ||
| // switch. Audit: UNSAFE-2026-0010 (StaticCell) + UNSAFE-2026-0014 (momentary | ||
| // `&mut` to kernel state) + UNSAFE-2026-0029 (the syscall arc). | ||
| let effect = unsafe { | ||
| let mut ctx = SyscallContext { | ||
| ep_arena: (*crate::EP_ARENA.0.get()).assume_init_mut(), | ||
| queues: (*crate::IPC_QUEUES.0.get()).assume_init_mut(), | ||
| caller_table: (*crate::SYSCALL_STUB_TABLE.0.get()).assume_init_mut(), | ||
| console: (*crate::CONSOLE.0.get()).assume_init_ref(), | ||
| user_window: UserAccessWindow::new(crate::PMM_EXTENT_START, SYSCALL_USER_WINDOW_LEN), | ||
| }; | ||
| dispatch(&mut ctx, args) | ||
| }; | ||
|
|
||
| match effect { | ||
| SyscallEffect::Resume(r) => { | ||
| // SAFETY: write the status (x0) + payload (x1..x7) back into the | ||
| // saved frame; the trampoline restores them on `ERET`. Touches only | ||
| // x0..x7. Audit: UNSAFE-2026-0029. | ||
| unsafe { | ||
| let f = &mut *frame; | ||
| f.x0_x1[0] = r.status; // x0 = status | ||
| f.x0_x1[1] = r.payload[0]; // x1 | ||
| f.x2_x3[0] = r.payload[1]; // x2 | ||
| f.x2_x3[1] = r.payload[2]; // x3 | ||
| f.x4_x5[0] = r.payload[3]; // x4 | ||
| f.x4_x5[1] = r.payload[4]; // x5 | ||
| f.x6_x7[0] = r.payload[5]; // x6 | ||
| f.x6_x7[1] = r.payload[6]; // x7 | ||
| } | ||
| } | ||
| SyscallEffect::Reschedule => { | ||
| // task_yield. v1 B5 stand-in: there is no scheduler-resident EL0 | ||
| // task issuing this (the smoke runs the stub before `start()`), so | ||
| // the real `yield_now` wiring lands in B6 once the caller is an EL0 | ||
| // task. The dispatcher-level routing (number 3 → Reschedule) is | ||
| // host-tested; here we resume with `Ok` (x0 = 0) — task_yield | ||
| // "always succeeds in v1" per ADR-0031. | ||
| // SAFETY: write x0 only. Audit: UNSAFE-2026-0029. | ||
| unsafe { | ||
| (*frame).x0_x1[0] = tyrne_kernel::syscall::OK_STATUS; | ||
| } | ||
| } | ||
| SyscallEffect::Terminate(_code) => { | ||
| // task_exit. The ABI says "does not return", but v1 has no EL0 | ||
| // context register file to drop — real termination lands in B6. The | ||
| // dispatcher-level routing (number 4 → Terminate) is host-tested; | ||
| // here we defensively resume with `Ok` so a stray kernel-stub | ||
| // task_exit cannot wedge the boot before B6 wires real termination. | ||
| // SAFETY: write x0 only. Audit: UNSAFE-2026-0029. | ||
| unsafe { | ||
| (*frame).x0_x1[0] = tyrne_kernel::syscall::OK_STATUS; | ||
| } | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.