Skip to content

Development - #4

Merged
cemililik merged 10 commits into
mainfrom
development
Apr 21, 2026
Merged

Development#4
cemililik merged 10 commits into
mainfrom
development

Conversation

@cemililik

@cemililik cemililik commented Apr 21, 2026

Copy link
Copy Markdown
Collaborator

Summary by Sourcery

Introduce a cooperative, single-core scheduler with a HAL-level context-switch trait and QEMU virt CPU implementation, and wire up a two-task smoke test on the BSP, along with roadmap, ADR, and unsafe-audit documentation updates.

New Features:

  • Add ContextSwitch HAL trait and implement it for the QEMU virt CPU with an AArch64 task-context structure and inline context-switch assembly.
  • Introduce a kernel::sched module providing a bounded ready queue, per-task state tracking, and IPC-aware cooperative scheduling APIs.
  • Extend the BSP main entry to create two tasks, allocate per-task stacks, and drive the new cooperative scheduler as a QEMU smoke test.

Enhancements:

  • Refine the kernel entry API to accept both console and CPU handles in preparation for scheduler integration.
  • Generalise IrqGuard to be generic over the concrete Cpu type to avoid fat-pointer vtable issues.
  • Enable FP/SIMD in the aarch64 boot stub before BSS zeroing to support compiler-emitted NEON instructions.

Documentation:

  • Add ADRs for scheduler shape, CPU v2/context-switch trait, and formal deferral of badges and reply_recv, and document the cooperative scheduler task (T-004).
  • Update roadmap, phase-A task status, and unsafe-audit log to reflect completion of milestones A4/A5 and the new unsafe contexts in CPU and scheduler code.
  • Clarify BSP crate and kernel crate docs to describe the new CPU implementation, scheduler module, and A5 milestone goals.

Tests:

  • Add extensive unit tests for the scheduler queue and state transitions using a fake CPU/context implementation.
  • Validate the cooperative scheduler via a QEMU two-task smoke test that confirms alternating task output.

Summary by CodeRabbit

Release Notes

  • New Features

    • Implemented cooperative multitasking scheduler enabling multiple kernel tasks to run and yield execution on a single CPU core.
    • Added FP/SIMD instruction support to the boot sequence.
    • Integrated IPC primitives with task scheduling for blocking/unblocking task states.
  • Documentation

    • Added architectural decision records detailing scheduler design, CPU trait extensions, and context-switching mechanisms.
    • Updated Phase A roadmap; IPC primitives and cooperative scheduler milestones now complete.

cemililik and others added 7 commits April 21, 2026 10:12
- T-003 marked Done; PR merged to main 2026-04-21.
- A4 milestone closed; A5 (cooperative scheduler) opened.
- T-004 task file created: cooperative yield-based scheduler with
  context switch, IPC blocking integration, and QEMU smoke test.
  ADR-0019 (scheduler shape) and ADR-0020 (Cpu trait v2) required
  before implementation begins.
- current.md, phase-a.md, and tasks README updated accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settles four inter-related design axes for Milestone A5:

- Queue structure: single bounded FIFO (SchedQueue<N>, N = TASK_ARENA_CAPACITY).
- Yield semantics: yield to next ready task (FIFO order); no targeted yield in v1.
- Blocked-task state: per-task TaskState enum { Idle, Ready, Blocked { on: EndpointHandle } }
  in kernel::sched; O(N) unblock scan accepted at A5 scale (N ≤ 16).
- IPC bridge: scheduler is the orchestration layer; kernel::ipc remains free of
  scheduling concerns and its 64 tests are unaffected.

Open questions: yield_to(target) for reply_recv (deferred to ADR-0018),
initial TaskContext setup (ADR-0020), idle task for double-block deadlock (Phase B).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Formally closes the two open questions that ADR-0017 left to this ADR:

- Badge scheme: deferred (Option A). No Phase A/A6 scenario requires
  kernel-injected badges; user-space emulation via CapHandle keying is
  available today. Revisit trigger: multiplexed-server scenario or
  formal security argument requiring kernel-TCB injection.

- reply_recv fastpath: deferred (Option D). The operation is
  semantically undefined without a running scheduler (A5) and
  unmeasurable without real context switches (A6 timings). Revisit
  trigger: A5/A6 benchmarks showing the extra context switch dominates
  server-loop latency.

Message::label, Capability, SlotEntry, and all IPC operations are
unchanged; both features remain additive for a successor ADR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d Cpu v2

ADR-0019 (scheduler shape) is now Accepted: single bounded FIFO, yield-to-next-ready,
per-task TaskState enum, scheduler as IPC orchestration layer.

ADR-0020 (Proposed): introduces a separate ContextSwitch trait (preserving Cpu
object-safety), unsafe context_switch / init_context operations, and a TaskContext
associated type. Defines the aarch64 register frame (x19–x28, fp, lr, sp = 104 bytes)
and the TestHal no-op for host tests. Both ADRs are prerequisites for T-004 (A5).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ADR-0020 (`ContextSwitch` trait and `Cpu` v2) accepted: both ADRs that
gate T-004 are now Accepted. T-004 status advances from Draft → Ready;
all blocking prerequisites recorded in the task file. Roadmap pointers
updated in current.md, phase-a.md, and the task index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… scheduler

Milestone A5 implementation (T-004):

hal/src/context_switch.rs — new ContextSwitch trait (ADR-0020): TaskContext
associated type, unsafe context_switch / init_context with full Safety docs.

bsp-qemu-virt/src/cpu.rs — QemuVirtCpu implementing Cpu + ContextSwitch;
Aarch64TaskContext (repr(C), 104 bytes: x19–x28 + fp + lr + sp); inline-asm
context_switch_asm saving/restoring callee-saved registers and switching via ret.

kernel/src/sched/mod.rs — Scheduler<C: ContextSwitch + Cpu>:
  - SchedQueue<N>: bounded FIFO with wrap-around, O(1) enqueue/dequeue
  - TaskState: Idle / Ready / Blocked { on: EndpointHandle }
  - add_task, start, yield_now: cooperative context-switch operations
  - ipc_send_and_yield, ipc_recv_and_yield: IPC bridge over kernel::ipc
  - unblock_receiver_on: O(N) scan to re-enqueue blocked tasks
  - 11 host tests: queue FIFO, wrap-around, state transitions, unblock logic

docs/audits/unsafe-log.md — audit entries UNSAFE-2026-0006 through 0009.

All checks clean: cargo fmt, cargo host-clippy, cargo host-test (75 passed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e passes

Three bugs prevented task_a from executing after `start()` transferred control.
All three are fixed in this commit; QEMU now shows interleaved output from both
tasks across all three iterations.

Bug 1 — IrqGuard fat-pointer vtable (hal/src/cpu.rs)
  `IrqGuard::new(cpu)` where `cpu: &QemuVirtCpu` was coerced to `&dyn Cpu`.
  The compiler placed the vtable adjacent to a panic string in .rodata; the
  vtable offset landed on ASCII bytes, so `blr` jumped into garbage.
  Fix: make `IrqGuard` generic — `IrqGuard<'a, C: Cpu>` takes `cpu: &'a C`
  directly, eliminating the fat-pointer coercion site.

Bug 2 — CPACR_EL1.FPEN not initialised (bsp-qemu-virt/src/boot.s)
  QEMU virt drops the kernel to EL1. CPACR_EL1.FPEN resets to 0b00, which
  traps FP/SIMD at EL1 as Undefined Instruction. The compiler emits
  `movi v0.2d, #0` for zero-initialising the throwaway scheduler context;
  this trapped immediately. No VBAR was set, so the exception vector at 0x200
  fetched zeros → silent hang.
  Fix: set CPACR_EL1.FPEN = 0b11 (bits[21:20] = 0x300000) in boot.s before
  zeroing BSS, followed by ISB.

Bug 3 — context_switch_asm compiler prologue corrupts saved sp
  (bsp-qemu-virt/src/cpu.rs)
  Even with `#[inline(never)]`, the compiler generated a standard function
  prologue (`stp x29, x30, [sp, #-0x10]!`) before our asm, adjusting sp by
  -16 before we saved it. On context restore, sp was 16 bytes too low; the
  caller's (yield_now) epilogue read callee-saved registers from the wrong
  stack addresses, then `ret`-ed to garbage.
  Fix: convert `context_switch_asm` to `#[unsafe(naked)] extern "C"` with
  `naked_asm!`. No prologue/epilogue is generated; sp is saved and restored
  exactly as the caller left it.

QEMU smoke output (debug build):
  umbrix: hello from kernel_main
  umbrix: starting cooperative scheduler
  umbrix: task A — iteration 0
  umbrix: task B — iteration 0
  umbrix: task A — iteration 1
  umbrix: task B — iteration 1
  umbrix: task A — iteration 2
  umbrix: task B — iteration 2
  umbrix: task A done; spinning

Also: remove debug raw volatile UART writes from task_a; update T-004 → Done;
advance current.md and phase-a.md to A6.

Audit entries: UNSAFE-2026-0006 through UNSAFE-2026-0009 (existing).
Host tests: 75 passed (umbrix-kernel --lib).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Apr 21, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces the A5 cooperative scheduler milestone: adds a generic scheduler over a new ContextSwitch HAL trait, implements QemuVirtCpu with context-switch assembly on aarch64, wires two kernel tasks and their stacks into the BSP entry for a QEMU smoke test, fixes IrqGuard to avoid fat-pointer issues, and updates roadmap, ADRs, and task docs to reflect completion and unsafe-audit entries.

Class diagram for new ContextSwitch, QemuVirtCpu, and Scheduler

classDiagram
    direction LR

    class Cpu {
        <<trait>>
        +current_core_id() CoreId
        +disable_irqs() IrqState
        +restore_irq_state(state IrqState) void
        +wait_for_interrupt() void
        +instruction_barrier() void
    }

    class ContextSwitch {
        <<trait>>
        +context_switch(current TaskContext, next TaskContext) void
        +init_context(ctx TaskContext, entry fn() -> !, stack_top *mut u8) void
        <<associatedtype>> TaskContext
    }

    class IrqGuard~C~ {
        -cpu &C
        -prev IrqState
        +new(cpu &C) IrqGuard~C~
        -drop() void
    }

    class QemuVirtCpu {
        -_priv ()
        +new() QemuVirtCpu
        +current_core_id() CoreId
        +disable_irqs() IrqState
        +restore_irq_state(state IrqState) void
        +wait_for_interrupt() void
        +instruction_barrier() void
        +context_switch(current &mut Aarch64TaskContext, next &Aarch64TaskContext) void
        +init_context(ctx &mut Aarch64TaskContext, entry fn() -> !, stack_top *mut u8) void
    }

    class Aarch64TaskContext {
        <<reprC>>
        +x19_x28 u64[10]
        +fp u64
        +lr u64
        +sp u64
        +new() Aarch64TaskContext
    }

    class SchedQueue~N~ {
        -buf Option~TaskHandle~[N]
        -head usize
        -len usize
        +new() SchedQueue~N~
        +enqueue(handle TaskHandle) Result~(), TaskHandle~
        +dequeue() Option~TaskHandle~
        +len() usize
        +is_empty() bool
    }

    class TaskState {
        <<enum>>
        Idle
        Ready
        Blocked
        +on EndpointHandle
    }

    class SchedError {
        <<enum>>
        NoCurrentTask
        QueueFull
        Ipc
    }

    class Scheduler~C~ {
        -ready SchedQueue~TASK_ARENA_CAPACITY~
        -task_states TaskState[TASK_ARENA_CAPACITY]
        -task_handles Option~TaskHandle~[TASK_ARENA_CAPACITY]
        -current Option~TaskHandle~
        -contexts C::TaskContext[TASK_ARENA_CAPACITY]
        +new() Scheduler~C~
        +add_task(cpu &C, handle TaskHandle, entry fn() -> !, stack_top *mut u8) Result~(), SchedError~
        +start(cpu &C) void
        +yield_now(cpu &C) Result~(), SchedError~
        +ipc_send_and_yield(cpu &C, ep_arena &mut EndpointArena, queues &mut IpcQueues, caller_table &mut CapabilityTable, ep_cap CapHandle, msg Message, transfer Option~CapHandle~) Result~SendOutcome, SchedError~
        +ipc_recv_and_yield(cpu &C, ep_arena &mut EndpointArena, queues &mut IpcQueues, caller_table &mut CapabilityTable, ep_cap CapHandle) Result~RecvOutcome, SchedError~
        -resolve_ep_cap(caller_table &CapabilityTable, ep_cap CapHandle) Result~EndpointHandle, SchedError~
        -unblock_receiver_on(ep EndpointHandle) void
    }

    class StaticCell~T~ {
        -inner UnsafeCell~MaybeUninit~T~~
        +new() StaticCell~T~
    }

    class TaskStack {
        <<reprC_align16>>
        -inner UnsafeCell~[u8;4096]~
        +new() TaskStack
        +top() *mut u8
    }

    class TaskHandle
    class EndpointHandle
    class CapabilityTable
    class IpcQueues
    class Message
    class SendOutcome
    class RecvOutcome
    class CapHandle

    %% Trait implementations and relationships
    QemuVirtCpu ..|> Cpu
    QemuVirtCpu ..|> ContextSwitch

    ContextSwitch <|.. Scheduler
    Cpu <|.. IrqGuard

    Scheduler "1" o-- "1" SchedQueue
    Scheduler "1" o-- "TASK_ARENA_CAPACITY" TaskState
    Scheduler "1" o-- "TASK_ARENA_CAPACITY" Aarch64TaskContext : contexts
    Scheduler ..> TaskHandle
    Scheduler ..> EndpointHandle
    Scheduler ..> CapabilityTable
    Scheduler ..> IpcQueues
    Scheduler ..> Message

    IrqGuard ..> IrqState

    StaticCell "1" o-- "1" QemuVirtCpu
    StaticCell "1" o-- "1" Scheduler
    StaticCell "1" o-- "1" Console

    TaskStack <.. TASK_A_STACK : uses
    TaskStack <.. TASK_B_STACK : uses

    class Console
Loading

File-Level Changes

Change Details Files
Implement a cooperative scheduler in the kernel crate with IPC integration and unit tests.
  • Add SchedQueue bounded FIFO of TaskHandle with enqueue/dequeue/len/is_empty operations
  • Introduce TaskState enum and SchedError error type to track task readiness and IPC failures
  • Implement generic Scheduler<C: ContextSwitch + Cpu> managing ready queue, per-task state, current task, and per-task contexts
  • Provide add_task, start, yield_now methods that initialise task contexts and perform context switches under IrqGuard
  • Add ipc_send_and_yield and ipc_recv_and_yield bridge methods that call ipc_send/ipc_recv, park/unpark tasks on endpoints, and yield accordingly
  • Introduce FakeCpu and FakeCtx plus helpers to host-test scheduler and queue behaviour without real assembly
kernel/src/sched/mod.rs
Extend the HAL with a ContextSwitch trait and make IrqGuard generic over concrete Cpu types.
  • Add ContextSwitch trait with associated TaskContext, unsafe context_switch and init_context methods plus detailed safety docs
  • Re-export ContextSwitch from hal::lib for downstream use
  • Change IrqGuard from trait-object based (&dyn Cpu) to generic IrqGuard<'a, C: Cpu> to avoid vtable/fat-pointer issues and update its Drop impl and constructor
hal/src/context_switch.rs
hal/src/lib.rs
hal/src/cpu.rs
Provide a QEMU aarch64 Cpu and ContextSwitch implementation with inline and naked assembly, plus associated unsafe-audit entries.
  • Introduce zero-sized QemuVirtCpu type with new() constructor and unsafe Send/Sync markers justified for single-core use
  • Implement Cpu for QemuVirtCpu using asm! to read MPIDR_EL1, mask/restore DAIF, execute WFI, and ISB
  • Define Aarch64TaskContext #[repr(C)] with x19–x28, fp, lr, and sp fields matching assembly layout
  • Implement naked context_switch_asm that saves/restores callee-saved registers and sp between two Aarch64TaskContext pointers
  • Implement ContextSwitch for QemuVirtCpu by delegating to context_switch_asm and initialising lr/sp in init_context
  • Add unsafe-log entries UNSAFE-2026-0006 through -0009 documenting Send/Sync, inline assembly, context_switch, and init_context invariants
bsp-qemu-virt/src/cpu.rs
docs/audits/unsafe-log.md
Wire the cooperative scheduler and QemuVirtCpu into the QEMU BSP entrypoint with static stacks, StaticCell globals, and two smoke-test tasks.
  • Introduce StaticCell as a Sync wrapper around UnsafeCell<MaybeUninit> for write-once globals and TaskStack with 16-byte-aligned 4 KiB stacks and top() helper
  • Add TASK_A_STACK, TASK_B_STACK, SCHED, CPU, and CONSOLE statics to hold stacks, scheduler, CPU, and console for tasks
  • Implement task_a and task_b functions that print iteration messages via FmtWriter and cooperatively yield via Scheduler::yield_now using SCHED and CPU cells, then spin when done
  • Refactor kernel_entry to construct Pl011Uart and QemuVirtCpu, publish them into StaticCells, create two Task objects in the arena, add them to the Scheduler with proper stack tops, publish SCHED, and call Scheduler::start
  • Clean up panic_handler comments to match new FmtWriter handling
bsp-qemu-virt/src/main.rs
Adjust the early boot assembly to enable FP/SIMD and document EL1 assumptions for NEON-using codegen.
  • Update boot.s comments to mention enabling FP/SIMD and new step ordering
  • Insert CPACR_EL1.FPEN = 0b11 write via msr cpacr_el1 plus ISB before BSS zeroing so compiler-generated NEON instructions do not trap
  • Clarify that QEMU virt starts at EL1 and DTB pointer in x0 is ignored
bsp-qemu-virt/src/boot.s
Update kernel entry API to accept Cpu as well as Console and adjust documentation.
  • Extend umbrix_kernel::run signature to be generic over Console and Cpu, taking both references
  • Update doc comment to describe current A5 behaviour (print greeting and idle) and future wiring of scheduler and IPC
kernel/src/lib.rs
Update roadmap, phase-A docs, task records, and ADR index to reflect completion of A4/A5 and add new ADRs for scheduler and Cpu v2.
  • Mark current roadmap active milestone as A6, last completed milestone as A5, and describe T-003 and T-004 deliverables and bugs fixed
  • Mark T-003 status as Done and add final review log entry; add T-004 task file describing cooperative scheduler scope and completion notes
  • Mark Phase A Milestone A5 as done, add T-004 under A5, and move T-003 to Done
  • Extend decisions README with ADR-0018, ADR-0019, and ADR-0020 entries
  • Add ADR-0018 deferring badge scheme and reply_recv, ADR-0019 specifying scheduler shape, and ADR-0020 defining Cpu v2/ContextSwitch design
docs/roadmap/current.md
docs/roadmap/phases/phase-a.md
docs/analysis/tasks/phase-a/README.md
docs/analysis/tasks/phase-a/T-003-ipc-primitives.md
docs/analysis/tasks/phase-a/T-004-cooperative-scheduler.md
docs/decisions/README.md
docs/decisions/0018-badge-scheme-and-reply-recv-deferral.md
docs/decisions/0019-scheduler-shape.md
docs/decisions/0020-cpu-trait-v2-context-switch.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

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@cemililik has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 57 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 7 minutes and 57 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5158c2b4-0f41-4a8d-9976-c613ef1131b0

📥 Commits

Reviewing files that changed from the base of the PR and between 4fe547c and b4879c7.

📒 Files selected for processing (15)
  • .claude/skills/README.md
  • .claude/skills/add-bsp/SKILL.md
  • bsp-qemu-virt/src/boot.s
  • bsp-qemu-virt/src/cpu.rs
  • bsp-qemu-virt/src/main.rs
  • docs/analysis/tasks/phase-a/README.md
  • docs/analysis/tasks/phase-a/T-004-cooperative-scheduler.md
  • docs/audits/unsafe-log.md
  • docs/decisions/0019-scheduler-shape.md
  • docs/standards/bsp-boot-checklist.md
  • docs/standards/unsafe-policy.md
  • hal/src/cpu.rs
  • kernel/src/lib.rs
  • kernel/src/sched/mod.rs
  • tools/run-qemu.sh
📝 Walkthrough

Walkthrough

This PR implements Phase A5 cooperative scheduler with context switching support. It introduces a ContextSwitch HAL trait, implements context-switch assembly in the QEMU BSP, and adds a kernel scheduler managing a FIFO ready queue, task states (Idle/Ready/Blocked), and IPC-aware yield operations. The boot flow enables FP/SIMD before BSS zeroing, and the BSP integrates the scheduler into the main kernel entry point.

Changes

Cohort / File(s) Summary
Boot-time FP/SIMD Setup
bsp-qemu-virt/src/boot.s
Updated boot flow to enable FP/SIMD (CPACR_EL1 with FPEN=0b11) before BSS zeroing via new runtime ISB instruction. Adjusted step numbering and documentation to reflect this ordering change.
QEMU BSP CPU & Context Switching
bsp-qemu-virt/src/cpu.rs
New CPU backend module implementing QemuVirtCpu with Cpu and ContextSwitch traits. Includes unsafe inline-assembly for DAIF/MPIDR_EL1/WFI/ISB, Aarch64TaskContext register layout (repr(C)), and a naked unsafe assembly stub context_switch_asm for register save/restore and stack switching.
BSP Main & Scheduler Integration
bsp-qemu-virt/src/main.rs
Refactored from simple umbrix_kernel::run() handoff to cooperative multitasking bootstrap. Adds CPU module, creates TaskArena with two tasks, initializes Scheduler<QemuVirtCpu>, publishes it globally, and invokes start(cpu). Includes new StaticCell and TaskStack write-once static storage types and two task entry functions that yield in loops.
HAL Trait Additions
hal/src/context_switch.rs, hal/src/lib.rs
New ContextSwitch trait defining TaskContext associated type and two unsafe methods (context_switch, init_context) for context save/restore and task initialization. Comprehensive safety documentation for interrupt-disabling requirements and context lifetime constraints. Module added to HAL and re-exported.
HAL CPU Trait Refactoring
hal/src/cpu.rs
Changed IrqGuard<'a> from holding &'a dyn Cpu trait object to concrete generic type IrqGuard<'a, C: Cpu>. Updated new() and Drop implementations accordingly, improving type safety and inlining potential.
Kernel Entrypoint & Scheduler Module
kernel/src/lib.rs, kernel/src/sched/mod.rs
Updated run() signature to accept Cpu in addition to Console. New 655-line scheduler module implementing SchedQueue (heap-free FIFO), TaskState enum (Idle/Ready/Blocked), and Scheduler<C: ContextSwitch> with lifecycle operations (add_task, start, yield_now) and IPC orchestration (ipc_send_and_yield, ipc_recv_and_yield with blocking/unblocking logic). Includes FakeCpu test implementation and comprehensive unit tests.
Task & Phase Tracking
docs/analysis/tasks/phase-a/README.md, docs/analysis/tasks/phase-a/T-003-ipc-primitives.md, docs/analysis/tasks/phase-a/T-004-cooperative-scheduler.md
Updated T-003 status to "Done" and added new T-004 entry ("Cooperative scheduler", milestone A5, status "In Progress"). New T-004 task document specifying scheduler requirements, FIFO/heap-free design, yield_now semantics, IPC blocking/unblocking bridge, and CPU trait extensions with audit/safety policies.
Architecture Decision Records
docs/decisions/0018-badge-scheme-and-reply-recv-deferral.md, docs/decisions/0019-scheduler-shape.md, docs/decisions/0020-cpu-trait-v2-context-switch.md, docs/decisions/README.md
Added three ADRs: ADR-0018 documents deferral of kernel badge injection and reply_recv fastpath; ADR-0019 specifies Phase A5 scheduler shape (cooperative, FIFO, O(N) unblock, no yield_to); ADR-0020 defines ContextSwitch trait v2 as object-safe extension with Aarch64TaskContext layout and assembly boundary. Updated ADR index.
Roadmap & Phase Status
docs/roadmap/current.md, docs/roadmap/phases/phase-a.md
Updated current roadmap from A4 (T-003 in review) to A6 (T-004 completed). Marked T-003 and T-004 as "Done" with completion date 2026-04-21. Added "Tasks under A5" section and revised review triggers to include code/security review before A6 landing.
Unsafe Code Audit
docs/audits/unsafe-log.md
Added four new unsafe-audit entries (UNSAFE-2026-0006 through -0009) documenting: unsafe impl Send/Sync for QemuVirtCpu, unsafe inline-assembly register access (DAIF, MPIDR_EL1, WFI, ISB), unsafe naked assembly context_switch_asm and scheduler callers, and unsafe context initialization. Each entry records invariants, alternatives, and reviewer attribution.

Sequence Diagrams

sequenceDiagram
    participant Boot as Boot Sequence
    participant CPU as QemuVirtCpu
    participant Sched as Scheduler
    participant Task as Task A
    
    Boot->>Boot: Enable FP/SIMD (CPACR_EL1)
    Boot->>Boot: Zero BSS
    Boot->>Sched: kernel_entry()<br/>Initialize Scheduler
    Sched->>CPU: new()
    Sched->>Sched: TaskArena.add(Task A)<br/>TaskArena.add(Task B)
    Sched->>Sched: add_task(Task A, entry_fn,<br/>stack_top)
    CPU->>CPU: init_context()<br/>Set lr=entry, sp=stack_top
    Sched->>Sched: add_task(Task B, ...)
    Sched->>Sched: start()
    Sched->>CPU: context_switch_asm<br/>(current=idle, next=Task A)
    CPU->>CPU: Restore Task A registers<br/>Switch SP, ret to Task A.lr
    Task->>Task: Execute (print, yield)
Loading
sequenceDiagram
    participant Task_A as Task A
    participant Sched as Scheduler
    participant CPU as QemuVirtCpu
    participant Task_B as Task B
    
    Task_A->>Sched: yield_now()
    Sched->>Sched: Re-enqueue Task A<br/>Dequeue Task B
    Sched->>Sched: Update TaskState<br/>(A→Ready, B→Running)
    Sched->>CPU: disable_irqs() → save DAIF
    Sched->>CPU: context_switch_asm<br/>(current=Task A ctx,<br/>next=Task B ctx)
    CPU->>CPU: Save Task A callee-saved<br/>(x19–x28, fp, lr, sp)
    CPU->>CPU: Restore Task B callee-saved<br/>Switch SP, ret to Task B.lr
    Task_B->>Task_B: Execute (print, yield)
    Sched->>CPU: restore_irq_state(saved_DAIF)
    Note over Sched,Task_B: Task B now running
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Poem

🐰 A scheduler hops into place,
Context-switching at a brisk pace,
Two tasks yield and play,
Round-robin all day,
While IPC blocks with charm and grace! 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Development' is vague and generic, failing to convey meaningful information about the substantial changeset spanning scheduler implementation, IPC integration, boot flow updates, and architectural documentation. Replace with a specific, descriptive title that captures the primary change, such as 'Implement A5 cooperative scheduler with context switching and IPC integration' or 'Add Phase A5 cooperative scheduler and CPU HAL context-switch support'.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch development

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Cooperative scheduler with context switching and IPC integration (A5 milestone completion)

✨ Enhancement 🐞 Bug fix 📝 Documentation

Grey Divider

Walkthroughs

Description
• Implements cooperative scheduler with bounded FIFO queue (SchedQueue<N>) providing O(1)
  enqueue/dequeue operations
• Introduces ContextSwitch HAL trait as separate extension to preserve Cpu object-safety, with
  associated TaskContext type for register state storage
• Implements aarch64 context-switch for QEMU virt BSP with Aarch64TaskContext struct and naked
  assembly context_switch_asm function
• Adds TaskState enum (Idle, Ready, Blocked) for per-task scheduling state tracking and IPC
  bridge methods (ipc_send_and_yield(), ipc_recv_and_yield())
• Provides yield_now() for cooperative context switching with comprehensive unit tests
• Fixes IrqGuard vtable dispatch issue by changing from dynamic dispatch to generic `IrqGuard<'a,
  C: Cpu>`
• Enables FP/SIMD in boot sequence by initializing CPACR_EL1.FPEN to prevent NEON instruction traps
• Adds smoke-test tasks demonstrating cooperative yielding and scheduler initialization in kernel
  entry
• Documents design decisions in ADR-0018 (badge scheme deferral), ADR-0019 (scheduler shape), and
  ADR-0020 (context-switch trait)
• Records T-004 task completion with three implementation bugs fixed and unsafe audit log entries
• Updates roadmap marking A5 milestone complete and activating A6 milestone
Diagram
flowchart LR
  A["Cpu trait<br/>object-safe"] -->|"separate trait"| B["ContextSwitch trait<br/>with TaskContext"]
  C["SchedQueue<N><br/>bounded FIFO"] -->|"manages"| D["Scheduler<br/>ready queue + state"]
  D -->|"uses"| B
  E["aarch64 assembly<br/>context_switch_asm"] -->|"implements"| B
  D -->|"provides"| F["yield_now<br/>cooperative switch"]
  D -->|"bridges"| G["IPC send/recv<br/>with yield"]
  H["IrqGuard generic<br/>no vtable"] -->|"fixes dispatch"| A
  I["CPACR_EL1.FPEN<br/>boot init"] -->|"prevents"| J["NEON traps"]
Loading

Grey Divider

File Changes

1. kernel/src/sched/mod.rs ✨ Enhancement +655/-0

Cooperative scheduler with FIFO queue and IPC integration

• Introduces SchedQueue<N>, a bounded FIFO queue for task handles with O(1) enqueue/dequeue
 operations
• Defines TaskState enum (Idle, Ready, Blocked) to track per-task scheduling state
• Implements Scheduler<C: ContextSwitch> with ready queue, per-task state tracking, and saved task
 contexts
• Provides yield_now() for cooperative context switching and IPC bridge methods
 ipc_send_and_yield() / ipc_recv_and_yield()
• Includes comprehensive unit tests for queue operations and scheduler state transitions

kernel/src/sched/mod.rs


2. bsp-qemu-virt/src/cpu.rs ✨ Enhancement +236/-0

aarch64 context-switch implementation for QEMU virt BSP

• Implements QemuVirtCpu struct providing Cpu trait methods (interrupt masking, core ID, WFI,
 ISB)
• Defines Aarch64TaskContext struct holding callee-saved registers (x19–x28, fp, lr, sp) for task
 context storage
• Implements ContextSwitch trait with context_switch_asm naked assembly function for register
 save/restore
• Provides init_context() to initialize task entry point and stack pointer

bsp-qemu-virt/src/cpu.rs


3. hal/src/context_switch.rs ✨ Enhancement +70/-0

New ContextSwitch HAL trait for cooperative task switching

• Introduces ContextSwitch trait as a separate extension to preserve Cpu object-safety
• Defines associated type TaskContext for concrete register state storage
• Specifies unsafe fn context_switch() and unsafe fn init_context() with detailed safety
 contracts
• Trait is generic (not dynamic) to avoid vtable dispatch and enable monomorphization

hal/src/context_switch.rs


View more (15)
4. bsp-qemu-virt/src/main.rs ✨ Enhancement +224/-12

Smoke-test tasks and scheduler initialization in kernel entry

• Adds StaticCell<T> wrapper for write-once global initialization without static mut
• Defines TaskStack with 16-byte alignment and Sync marker for safe stack storage
• Implements two smoke-test tasks (task_a, task_b) that yield cooperatively and print to console
• Rewrites kernel_entry() to initialize scheduler, register tasks, and transfer control via
 start()
• Updates module documentation to reflect A5 milestone additions

bsp-qemu-virt/src/main.rs


5. kernel/src/lib.rs ✨ Enhancement +9/-6

Expose scheduler module and update kernel documentation

• Adds pub mod sched to expose the scheduler subsystem
• Updates module documentation to reference T-004 and ADR-0019/ADR-0020
• Modifies run() signature to accept generic Cpu parameter (previously unused)

kernel/src/lib.rs


6. hal/src/cpu.rs 🐞 Bug fix +9/-5

Make IrqGuard generic to fix vtable dispatch issues

• Changes IrqGuard from dynamic dispatch (&dyn Cpu) to generic IrqGuard<'a, C: Cpu> to avoid
 fat-pointer vtable issues
• Updates IrqGuard::new() to accept concrete CPU reference instead of trait object
• Adds documentation note explaining generic approach avoids vtable dispatch near large constants

hal/src/cpu.rs


7. hal/src/lib.rs ✨ Enhancement +4/-1

Export ContextSwitch trait from HAL crate

• Exports new ContextSwitch trait from context_switch module
• Updates documentation to list ContextSwitch (ADR-0020) among accepted HAL traits

hal/src/lib.rs


8. bsp-qemu-virt/src/boot.s 🐞 Bug fix +17/-6

Enable FP/SIMD in boot sequence to prevent NEON traps

• Adds CPACR_EL1.FPEN initialization to enable FP/SIMD at EL1 before BSS zeroing
• Prevents NEON instruction traps from compiler-generated zero-initialization code
• Updates comments to reflect new boot step and clarify EL1 entry assumption

bsp-qemu-virt/src/boot.s


9. docs/decisions/0020-cpu-trait-v2-context-switch.md 📝 Documentation +322/-0

ADR-0020: CPU trait v2 context-switch extension design

• Settles design of ContextSwitch trait as separate from Cpu to preserve object-safety
• Documents three design options and rationale for choosing separate trait with associated
 TaskContext type
• Specifies aarch64 concrete type (Aarch64TaskContext) and assembly requirements
• Details scheduler integration, TestHal fake implementation, and consequences

docs/decisions/0020-cpu-trait-v2-context-switch.md


10. docs/decisions/0019-scheduler-shape.md 📝 Documentation +218/-0

ADR-0019: Scheduler shape and queue structure design

• Settles scheduler data structure as single bounded FIFO queue with O(1) operations
• Chooses yield-to-next-ready semantics and per-task TaskState enum for blocked tracking
• Establishes scheduler as IPC orchestration layer to keep ipc module scheduling-agnostic
• Documents design options, rationale, and forward-compatibility with priority queues

docs/decisions/0019-scheduler-shape.md


11. docs/analysis/tasks/phase-a/T-004-cooperative-scheduler.md 📝 Documentation +95/-0

T-004 task definition and completion record for A5 scheduler

• Defines T-004 task for A5 milestone: cooperative scheduler with context switching
• Lists acceptance criteria including ADR-0019/ADR-0020 acceptance, assembly implementation, and
 QEMU smoke test
• Documents scope constraints (no timer, no preemption) and approach sketch
• Records completion with three bugs fixed (IrqGuard vtable, CPACR_EL1.FPEN, naked assembly)

docs/analysis/tasks/phase-a/T-004-cooperative-scheduler.md


12. docs/decisions/0018-badge-scheme-and-reply-recv-deferral.md 📝 Documentation +98/-0

ADR-0018: Formal deferral of badge scheme and reply_recv

• Formally defers kernel-injected badge scheme to successor ADR pending concrete use case
• Defers reply_recv fastpath to after A5 scheduler is proven and A6 provides measurements
• Documents decision drivers and revisit triggers for both features
• Notes user-space badge emulation is available immediately without kernel changes

docs/decisions/0018-badge-scheme-and-reply-recv-deferral.md


13. docs/audits/unsafe-log.md 📝 Documentation +56/-0

Unsafe audit log entries for A5 context-switch implementation

• Adds five new audit entries (UNSAFE-2026-0006 through UNSAFE-2026-0009) for context-switch
 implementation
• Documents invariants for QemuVirtCpu Send/Sync markers, inline assembly, context-switch
 assembly, and context initialization
• Records safety rationale and rejected alternatives for each unsafe block

docs/audits/unsafe-log.md


14. docs/roadmap/current.md 📝 Documentation +10/-9

Roadmap update: A5 completion and A6 milestone activation

• Updates active milestone from A4 to A6 and marks T-004 as Done
• Records T-003 (A4) completion with 64 tests and T-004 (A5) completion with 75 tests
• Documents three implementation bugs fixed in A5 (IrqGuard, CPACR_EL1, naked assembly)
• Notes Phase A stack is complete; A6 two-task IPC demo is next

docs/roadmap/current.md


15. docs/analysis/tasks/phase-a/T-003-ipc-primitives.md 📝 Documentation +2/-1

Mark T-003 IPC primitives task as Done

• Updates T-003 status from "In Review" to "Done" with merge date 2026-04-21
• Records completion of IPC primitives subsystem with 55/55 tests passing

docs/analysis/tasks/phase-a/T-003-ipc-primitives.md


16. docs/roadmap/phases/phase-a.md 📝 Documentation +6/-2

Update Phase A milestones and task completion status

• Updated T-003 (IPC primitives) status from "In Review" to "Done"
• Marked Milestone A5 as completed with date 2026-04-21
• Added new "Tasks under A5" section documenting T-004 (Cooperative scheduler) as "Done"

docs/roadmap/phases/phase-a.md


17. docs/decisions/README.md 📝 Documentation +3/-0

Add three new accepted architecture decision records

• Added three new Architecture Decision Records (ADRs) to the table
• ADR 0018: Badge scheme and reply_recv fastpath formal deferral (Accepted 2026-04-21)
• ADR 0019: Scheduler shape (Accepted 2026-04-21)
• ADR 0020: ContextSwitch trait and Cpu v2 (Accepted 2026-04-21)

docs/decisions/README.md


18. docs/analysis/tasks/phase-a/README.md 📝 Documentation +2/-1

Update Phase A task statuses and add new task

• Updated T-003 (IPC primitives) status from "In Review" to "Done"
• Added new task T-004 (Cooperative scheduler) for Milestone A5 with "In Progress" status

docs/analysis/tasks/phase-a/README.md


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Apr 21, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. New code added in boot.s 📘 Rule violation ⛨ Security
Description
The PR adds new boot logic in bsp-qemu-virt/src/boot.s (AArch64 assembly) rather than Rust. This
violates the requirement that kernel/userspace implementation code be written in Rust to preserve
memory-safety and consistency guarantees.
Code

bsp-qemu-virt/src/boot.s[R30-36]

+    /* Enable FP/SIMD at EL1 and EL0 (CPACR_EL1.FPEN = 0b11 = bits[21:20]).
+     * 0x300000 = 3 << 20.  CPACR_EL1 resets to zero, so only FPEN needs
+     * setting; all other fields remain 0 (no ZEN, no TTA traps).
+     * ISB ensures the write is visible before the first NEON instruction. */
+    mov     x0, #0x300000
+    msr     cpacr_el1, x0
+    isb
Evidence
PR Compliance ID 2 forbids adding kernel/userspace implementation code in non-Rust languages. The
added FP/SIMD enablement sequence is new assembly instructions added directly to boot.s.

CLAUDE.md
bsp-qemu-virt/src/boot.s[30-36]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR adds new boot-time functionality in an assembly source file (`boot.s`). This violates the rule requiring kernel/userspace implementation code to be written in Rust.
## Issue Context
The added instructions configure `CPACR_EL1.FPEN` to avoid NEON traps during early boot. This is legitimate functionality, but it should be expressed from Rust source (e.g., via a Rust `#[naked]` entry stub using `core::arch::asm!`/`naked_asm!`, or another Rust-owned early-init path) rather than adding new `.s` implementation code.
## Fix Focus Areas
- bsp-qemu-virt/src/boot.s[30-36]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. unsafe comments miss alternatives📘 Rule violation ⚙ Maintainability
Description
Multiple newly added unsafe regions include // SAFETY: notes but do not document why safer
alternatives were rejected. This violates the requirement that each unsafe use have adjacent
invariant documentation including rejected safer approaches for auditability.
Code

bsp-qemu-virt/src/main.rs[R69-72]

+// SAFETY: Umbrix v1 is single-core and cooperative. No two tasks ever run
+// simultaneously, so there are no data races on `StaticCell` contents.
+// Audit: UNSAFE-2026-0001.
+unsafe impl<T> Sync for StaticCell<T> {}
Evidence
PR Compliance ID 3 requires each unsafe block to have adjacent justification covering necessity,
invariants, and rejected safer alternatives. The added unsafe impl Sync for StaticCell (and other
added unsafe in the PR) documents invariants but does not include rejected safer alternatives in
the adjacent comment.

CLAUDE.md
bsp-qemu-virt/src/main.rs[69-72]
bsp-qemu-virt/src/cpu.rs[46-54]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New `unsafe` regions added by this PR have adjacent `// SAFETY:` commentary, but the commentary does not consistently include (c) why safer alternatives were rejected.
## Issue Context
The compliance rule requires every `unsafe` use to have adjacent documentation explaining: (a) why `unsafe` is needed, (b) what invariants it relies on, and (c) why safer alternatives were rejected. The PR often includes (a) and (b), but omits (c) in-code (even if an external audit log exists).
## Fix Focus Areas
- bsp-qemu-virt/src/main.rs[69-72]
- bsp-qemu-virt/src/cpu.rs[46-54]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Aliased &mut scheduler🐞 Bug ≡ Correctness
Description
Tasks take &mut Scheduler from a global StaticCell and call yield_now/start, which performs a
context switch before returning. This leaves a live &mut Scheduler in the suspended task’s stack
while another task can create a second &mut Scheduler, causing undefined behavior and potential
scheduler corruption.
Code

bsp-qemu-virt/src/main.rs[R141-150]

+        // SAFETY: SCHED and CPU are fully initialised before `start()`.
+        // Cooperative scheduling ensures no concurrent mutation.
+        // Audit: UNSAFE-2026-0001.
+        let sched = unsafe { (*SCHED.0.get()).assume_init_mut() };
+        // SAFETY: same invariants as SCHED above. Audit: UNSAFE-2026-0001.
+        let cpu = unsafe { (*CPU.0.get()).assume_init_ref() };
+        // yield_now returns Err only if current == None, which cannot
+        // happen once the scheduler has started.
+        let _ = sched.yield_now(cpu);
+    }
Evidence
task_a/task_b obtain a mutable reference to the global scheduler via assume_init_mut() and then
call yield_now. yield_now (and start) performs cpu.context_switch(...), which transfers
control to another task before the original call returns; the original task’s stack frame still
holds the mutable borrow, so the next task can create an aliasing mutable borrow via the same
assume_init_mut() pattern—this violates Rust’s exclusive &mut aliasing rules even on a single
core.

bsp-qemu-virt/src/main.rs[132-176]
bsp-qemu-virt/src/main.rs[262-266]
kernel/src/sched/mod.rs[233-254]
kernel/src/sched/mod.rs[265-301]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Scheduler::start` / `Scheduler::yield_now` context-switch away before returning, but callers currently obtain an `&mut Scheduler` from a global `StaticCell` and keep that mutable borrow live across the switch. When another task later obtains its own `&mut Scheduler`, you get aliased mutable references (undefined behavior).
### Issue Context
This is *not* a data-race problem; it is a Rust aliasing/lifetime problem caused by suspending a stack frame that holds an `&mut` while other code can create a new `&mut` to the same object.
### Fix Focus Areas
- bsp-qemu-virt/src/main.rs[132-176]
- bsp-qemu-virt/src/main.rs[262-266]
- kernel/src/sched/mod.rs[233-304]
### Suggested remediation direction
- Do **not** expose `&mut Scheduler` to tasks across a context switch.
- Move the scheduler mutation behind an API that does not create an `&mut Scheduler` that can live across the switch, e.g.:
- Make the yield entrypoint a free function that operates on a raw pointer/`UnsafeCell` internally and carefully ensures any temporary `&mut` borrows end *before* calling `context_switch`, or
- Re-architect so tasks trap into a scheduler-owned loop (syscall-style) where the scheduler holds the only mutable access and tasks never hold a direct `&mut` to it.
- Add a safety comment/audit entry documenting how aliasing is prevented (not just “single-core”).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. SIMD context not saved🐞 Bug ≡ Correctness
Description
Boot enables FP/SIMD (CPACR_EL1.FPEN=0b11) but the context switch saves/restores only GPR
callee-saved state (x19–x30, sp). Any use of callee-saved SIMD/FP registers (v8–v15/d8–d15) in task
code can be corrupted/leaked across yields.
Code

bsp-qemu-virt/src/cpu.rs[R175-201]

+    naked_asm!(
+        // ── save current (x0) ───────────────────────────────────────────
+        "stp x19, x20, [x0,  #0]",
+        "stp x21, x22, [x0,  #16]",
+        "stp x23, x24, [x0,  #32]",
+        "stp x25, x26, [x0,  #48]",
+        "stp x27, x28, [x0,  #64]",
+        "stp x29, x30, [x0,  #80]",   // fp, lr
+        "mov x8,  sp",
+        "str x8,  [x0, #96]",          // sp
+
+        // ── restore next (x1) ───────────────────────────────────────────
+        "ldr x8,  [x1, #96]",          // sp
+        "mov sp,  x8",
+        "ldp x29, x30, [x1, #80]",    // fp, lr
+        "ldp x27, x28, [x1, #64]",
+        "ldp x25, x26, [x1, #48]",
+        "ldp x23, x24, [x1, #32]",
+        "ldp x21, x22, [x1, #16]",
+        "ldp x19, x20, [x1,  #0]",
+
+        // ret jumps to the lr just loaded from `next`.
+        // On a task's first run that lr was set by init_context to the
+        // entry function; on subsequent runs it is the return address
+        // stored by the `bl context_switch_asm` in the previous yield.
+        "ret",
+    );
Evidence
The reset stub explicitly enables FP/SIMD. The aarch64 context structure and context_switch_asm
only cover GPRs and SP/LR/FP; the project’s own ADR notes that callee-saved NEON registers are not
saved in v1, which becomes a more practical hazard once FP/SIMD is enabled.

bsp-qemu-virt/src/boot.s[30-37]
bsp-qemu-virt/src/cpu.rs[120-202]
docs/decisions/0020-cpu-trait-v2-context-switch.md[302-307]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
FP/SIMD is enabled at boot, but the cooperative context switch does not preserve callee-saved SIMD/FP state. This can break task correctness if LLVM/Rust uses v8–v15 across yield boundaries.
### Issue Context
Even if kernel code avoids explicit floats, the compiler may still allocate SIMD registers for some integer/vectorized operations. If it uses callee-saved SIMD regs, the context switch must preserve them to meet the ABI assumptions across a `yield` call.
### Fix Focus Areas
- bsp-qemu-virt/src/boot.s[30-37]
- bsp-qemu-virt/src/cpu.rs[120-202]
- docs/decisions/0020-cpu-trait-v2-context-switch.md[302-307]
### Suggested remediation direction
Pick one (and document/enforce it):
1) **Preserve SIMD context**: extend `Aarch64TaskContext` to include v8–v15 (or d8–d15) and update `context_switch_asm` to save/restore them.
2) **Enforce “no SIMD callee-saves”**: keep FP/SIMD enabled only if you can *prove* (via build flags/lints/CI disassembly checks) that generated code won’t rely on callee-saved SIMD regs; document the restriction at the context switch boundary.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

5. CPACR_EL1 overwrites fields🐞 Bug ☼ Reliability
Description
The boot stub writes a constant to CPACR_EL1 rather than setting FPEN via read-modify-write. If boot
conditions ever change such that CPACR_EL1 is preconfigured, this will clear unrelated fields
unexpectedly.
Code

bsp-qemu-virt/src/boot.s[R30-36]

+    /* Enable FP/SIMD at EL1 and EL0 (CPACR_EL1.FPEN = 0b11 = bits[21:20]).
+     * 0x300000 = 3 << 20.  CPACR_EL1 resets to zero, so only FPEN needs
+     * setting; all other fields remain 0 (no ZEN, no TTA traps).
+     * ISB ensures the write is visible before the first NEON instruction. */
+    mov     x0, #0x300000
+    msr     cpacr_el1, x0
+    isb
Evidence
The code sets CPACR_EL1 by writing 0x300000 directly, which unconditionally clears all other
CPACR_EL1 fields. While this is likely fine for QEMU virt reset state as documented, it is brittle
if reused under different boot/firmware conditions.

bsp-qemu-virt/src/boot.s[30-36]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Directly writing a constant to CPACR_EL1 can clobber other configuration bits.
### Issue Context
Even if CPACR_EL1 resets to 0 on your current QEMU path, a defensive RMW makes the stub more robust and self-contained.
### Fix Focus Areas
- bsp-qemu-virt/src/boot.s[30-36]
### Suggested change
Replace the constant write with:
- `mrs x0, cpacr_el1`
- `orr x0, x0, #(3 << 20)`
- `msr cpacr_el1, x0`
- `isb`

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The safety contract in QemuVirtCpu::new (“there must be at most one instance per core”) doesn’t match it being a safe const fn; either make the constructor unsafe or relax/rephrase the documented invariant so callers aren’t relying on an unenforced safety guarantee.
  • The ad‑hoc unsafe access patterns on StaticCell (e.g., (*CONSOLE.0.get()).assume_init_ref()) are replicated in several places; consider adding safe-ish helper methods like fn init(&self, T) and fn get(&self) -> &T/&mut T (still unsafe) to centralise the invariants and avoid directly reaching into the inner UnsafeCell at each call site.
  • In Scheduler::ipc_recv_and_yield, the deadlock path panics with a fixed message; it might be useful to log which endpoint and task index caused the condition (or at least the fact that all tasks are blocked) to make debugging such a fatal condition easier when it is hit on real hardware.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The safety contract in `QemuVirtCpu::new` (“there must be at most one instance per core”) doesn’t match it being a safe `const fn`; either make the constructor `unsafe` or relax/rephrase the documented invariant so callers aren’t relying on an unenforced safety guarantee.
- The ad‑hoc `unsafe` access patterns on `StaticCell` (e.g., `(*CONSOLE.0.get()).assume_init_ref()`) are replicated in several places; consider adding safe-ish helper methods like `fn init(&self, T)` and `fn get(&self) -> &T`/`&mut T` (still `unsafe`) to centralise the invariants and avoid directly reaching into the inner `UnsafeCell` at each call site.
- In `Scheduler::ipc_recv_and_yield`, the deadlock path panics with a fixed message; it might be useful to log which endpoint and task index caused the condition (or at least the fact that all tasks are blocked) to make debugging such a fatal condition easier when it is hit on real hardware.

## Individual Comments

### Comment 1
<location path="kernel/src/sched/mod.rs" line_range="424-431" />
<code_context>
+    /// Scan `task_states` for a task blocked on `ep` and re-enqueue it.
+    ///
+    /// O(N) scan — acceptable at `TASK_ARENA_CAPACITY ≤ 16` (ADR-0019).
+    fn unblock_receiver_on(&mut self, ep: EndpointHandle) {
+        for idx in 0..TASK_ARENA_CAPACITY {
+            if let TaskState::Blocked { on } = self.task_states[idx] {
+                if on == ep {
+                    if let Some(handle) = self.task_handles[idx] {
+                        self.task_states[idx] = TaskState::Ready;
+                        let _ = self.ready.enqueue(handle);
+                        return;
+                    }
+                }
</code_context>
<issue_to_address>
**question:** Clarify single-receiver unblocking semantics or consider unblocking all receivers on an endpoint.

This helper currently wakes only the first task blocked on the endpoint and returns, which effectively enforces single-waiter semantics. If multiple receivers per endpoint are possible, either document that only one receiver is woken per send, or adjust the loop to wake all matching tasks to avoid potential starvation or surprising behavior.
</issue_to_address>

### Comment 2
<location path="bsp-qemu-virt/src/cpu.rs" line_range="72-81" />
<code_context>
+        id
+    }
+
+    fn disable_irqs(&self) -> IrqState {
+        let daif: usize;
+        // SAFETY: `MRS x, DAIF` reads the current interrupt mask; `MSR DAIF, #0xF`
+        // masks all DAIF bits (D, A, I, F). Both are EL1-privileged register
+        // operations. The returned `IrqState` captures the prior value so that
+        // `restore_irq_state` can restore it exactly. Audit: UNSAFE-2026-0007.
+        unsafe {
+            asm!(
+                "mrs {daif}, daif",
+                "msr daifset, #0xf",
+                daif = out(reg) daif,
+                options(nostack, nomem),
</code_context>
<issue_to_address>
**nitpick:** Align comments with the actual DAIF-masking instruction used.

The comment mentions `MSR DAIF, #0xF`, but the code uses `msr daifset, #0xf`. Please update the comment to describe the actual `DAIFSet` instruction and its semantics, so it’s clear which masking mechanism is relied on for this operation and for future audits.
</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 kernel/src/sched/mod.rs
Comment on lines +424 to +431
fn unblock_receiver_on(&mut self, ep: EndpointHandle) {
for idx in 0..TASK_ARENA_CAPACITY {
if let TaskState::Blocked { on } = self.task_states[idx] {
if on == ep {
if let Some(handle) = self.task_handles[idx] {
self.task_states[idx] = TaskState::Ready;
let _ = self.ready.enqueue(handle);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

question: Clarify single-receiver unblocking semantics or consider unblocking all receivers on an endpoint.

This helper currently wakes only the first task blocked on the endpoint and returns, which effectively enforces single-waiter semantics. If multiple receivers per endpoint are possible, either document that only one receiver is woken per send, or adjust the loop to wake all matching tasks to avoid potential starvation or surprising behavior.

Comment thread bsp-qemu-virt/src/cpu.rs
Comment on lines +72 to +81
fn disable_irqs(&self) -> IrqState {
let daif: usize;
// SAFETY: `MRS x, DAIF` reads the current interrupt mask; `MSR DAIF, #0xF`
// masks all DAIF bits (D, A, I, F). Both are EL1-privileged register
// operations. The returned `IrqState` captures the prior value so that
// `restore_irq_state` can restore it exactly. Audit: UNSAFE-2026-0007.
unsafe {
asm!(
"mrs {daif}, daif",
"msr daifset, #0xf",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nitpick: Align comments with the actual DAIF-masking instruction used.

The comment mentions MSR DAIF, #0xF, but the code uses msr daifset, #0xf. Please update the comment to describe the actual DAIFSet instruction and its semantics, so it’s clear which masking mechanism is relied on for this operation and for future audits.

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

Copy link
Copy Markdown

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 a cooperative scheduler and aarch64 context switching for the QEMU virt target. Key changes include the introduction of the ContextSwitch HAL trait, a FIFO-based scheduler in the kernel, and naked assembly for register save/restore. Additionally, the boot sequence was updated to enable FP/SIMD at EL1 to prevent traps during initialization, and IrqGuard was refactored to use generics to avoid vtable dispatch issues. I have no feedback to provide.

Comment thread bsp-qemu-virt/src/boot.s
Comment on lines +30 to +36
/* Enable FP/SIMD at EL1 and EL0 (CPACR_EL1.FPEN = 0b11 = bits[21:20]).
* 0x300000 = 3 << 20. CPACR_EL1 resets to zero, so only FPEN needs
* setting; all other fields remain 0 (no ZEN, no TTA traps).
* ISB ensures the write is visible before the first NEON instruction. */
mov x0, #0x300000
msr cpacr_el1, x0
isb

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. New code added in boot.s 📘 Rule violation ⛨ Security

The PR adds new boot logic in bsp-qemu-virt/src/boot.s (AArch64 assembly) rather than Rust. This
violates the requirement that kernel/userspace implementation code be written in Rust to preserve
memory-safety and consistency guarantees.
Agent Prompt
## Issue description
The PR adds new boot-time functionality in an assembly source file (`boot.s`). This violates the rule requiring kernel/userspace implementation code to be written in Rust.

## Issue Context
The added instructions configure `CPACR_EL1.FPEN` to avoid NEON traps during early boot. This is legitimate functionality, but it should be expressed from Rust source (e.g., via a Rust `#[naked]` entry stub using `core::arch::asm!`/`naked_asm!`, or another Rust-owned early-init path) rather than adding new `.s` implementation code.

## Fix Focus Areas
- bsp-qemu-virt/src/boot.s[30-36]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread bsp-qemu-virt/src/main.rs Outdated
Comment thread bsp-qemu-virt/src/main.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

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

⚠️ Outside diff range comments (2)
docs/roadmap/phases/phase-a.md (1)

129-153: ⚠️ Potential issue | 🟡 Minor

T-004 status is inconsistent between this roadmap and the task index.

Line 129 marks milestone A5 as ✓ (done 2026-04-21) and Line 153 lists T-004 as Done, but docs/analysis/tasks/phase-a/README.md Line 12 still shows T-004 as In Progress. One of the two is stale — please reconcile before merging so the roadmap, task index, and the T-004 task doc itself agree on A5/T-004 being either Done or still In Progress.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/roadmap/phases/phase-a.md` around lines 129 - 153, Milestone A5 and the
T-004 task state disagree: update the status so Milestone A5 (heading "Milestone
A5 — Cooperative scheduler and context switch") and the T-004 entry in the
phase-a task index (the README that lists T-004) and the T-004 task doc all show
the same state (either "Done" or "In Progress"); pick the correct final state,
change the text labeled "✓ (done 2026-04-21)" or the T-004 line in the phase-a
README to match, and ensure the T-004 task document itself reflects that same
status so A5, T-004, and the task doc are consistent.
kernel/src/lib.rs (1)

52-68: ⚠️ Potential issue | 🟡 Minor

kernel::run() is dead code — either remove or rewire the BSP to use it.

No call to kernel::run() exists anywhere in the codebase. The BSP's kernel_entry() (lines 203+) initializes the console, CPU, scheduler, and tasks directly, then transfers control to the scheduler. This function never reaches the run() function marked as the "portable kernel entry."

The function's doc comment claims it handles A5 scheduler wiring, but that work happens in kernel_entry() instead. The added _cpu parameter is unused. Either:

  • Delete run() and clarify the doc on kernel_entry() as the actual entry point, or
  • Move scheduler/task initialization into kernel::run() and have kernel_entry() call it, so the "portable entry" claim becomes truthful.

As written, the crate exposes a dead public API whose doc is out of sync with reality.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@kernel/src/lib.rs` around lines 52 - 68, The public function kernel::run and
its docs are dead — either remove it or make it the real portable entry: Option
A: delete pub fn run<Con: Console, C: Cpu>(...) entirely, remove its docs and
update the kernel_entry() doc comment to state it is the BSP entry point; Option
B: refactor so kernel_entry() delegates to kernel::run by moving scheduler and
task initialization out of kernel_entry() into run (and then remove the unused
_cpu parameter or use it), ensuring run performs console setup, scheduler wiring
and never returns; pick one approach and update docs accordingly.
♻️ Duplicate comments (1)
docs/analysis/tasks/phase-a/README.md (1)

11-12: ⚠️ Potential issue | 🟡 Minor

T-004 status contradicts the phase roadmap.

This index lists T-004 as In Progress, while docs/roadmap/phases/phase-a.md (Lines 129, 153) now marks Milestone A5 and T-004 as Done (2026-04-21). Pick one source of truth and align the other (and the T-004 task markdown) so readers can't land on contradicting status values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/analysis/tasks/phase-a/README.md` around lines 11 - 12, The README table
entry for T-004 ("Cooperative scheduler") currently shows "In Progress" which
contradicts the phase roadmap entry marking Milestone A5/T-004 as "Done"; update
the T-004 status so both sources match (choose the canonical source of truth and
align the other), e.g., change the table row for T-004 in
docs/analysis/tasks/phase-a/README.md to "Done (2026-04-21)" and ensure the
T-004 task markdown (T-004-cooperative-scheduler.md) and the Milestone A5
reference reflect the same final status and date.
🧹 Nitpick comments (8)
hal/src/cpu.rs (1)

86-119: Monomorphizing IrqGuard over C: Cpu looks correct.

Removes the &dyn Cpu coercion at guard construction, so the scheduler's hot paths (yield_now, IPC handoff) go through static dispatch and no vtable is materialized. Lifetime/Drop signatures are in order.

One small note: the doc rationale on Lines 86-88 describes a fairly specific codegen pathology ("incorrect vtable references when the coercion site is inlined near large .rodata constants"). If that's tied to a reproducer or upstream issue, a link in the doc (or in ADR-0020) would make the justification easier to revisit later; otherwise the more generic "avoid dynamic dispatch on critical-section paths" framing might age better.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hal/src/cpu.rs` around lines 86 - 119, The doc comment on IrqGuard currently
includes a specific codegen pathology claim; either add a reference link to the
reproducer or upstream issue that demonstrates "incorrect vtable references when
the coercion site is inlined near large `.rodata` constants" or reword the
rationale to a more generic explanation such as "avoid dynamic dispatch on
critical-section paths" to keep the justification maintainable; update the doc
comment above the IrqGuard struct (the text that references Cpu and vtable
coercion) accordingly.
bsp-qemu-virt/src/boot.s (1)

30-36: FP/SIMD enable is correct.

CPACR_EL1.FPEN = 0b11 at bits [21:20] (3 << 20 == 0x300000) disables EL0/EL1 SIMD/FP traps, and the trailing ISB correctly flushes the pipeline before any NEON-emitting Rust code runs. Placing this ahead of the BSS loop is conservative (the BSS zeroing itself only uses scalar str xzr), but it keeps the "no traps by the time we enter Rust" invariant unambiguous.

One optional refinement: the comment on line 31 states CPACR_EL1 resets to zero. Per the Arm ARM (DDI0601/2023-06), the FPEN field is architecturally UNKNOWN on reset and only happens to be zero on QEMU. If this code runs on real hardware in the future, consider rewording to: "We do not rely on the reset value; an explicit write sets FPEN = 0b11 and leaves all other fields zero."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bsp-qemu-virt/src/boot.s` around lines 30 - 36, Update the comment above the
FP/SIMD enable sequence (the mov x0,`#0x300000` / msr cpacr_el1, x0 / isb block)
to avoid asserting CPACR_EL1 resets to zero; instead state that we do not rely
on the reset value and explicitly write CPACR_EL1.FPEN = 0b11 (bits[21:20])
leaving other fields zero, so FPEN is set and EL0/EL1 SIMD/FP traps are disabled
before Rust/NEON code runs.
bsp-qemu-virt/src/cpu.rs (1)

34-41: Clarify the singleton invariant or make the constructor unsafe.

The new() function is marked safe but its Safety doc requires "at most one QemuVirtCpu instance" to prevent inconsistent DAIF state. Since QemuVirtCpu is a zero-sized type with no interior mutability, duplicate instances are structurally harmless — the actual invariant concerns disable/restore pairing (each disable_irqs() call must be paired with the correct restore_irq_state()), not instance count.

Either:

  1. Make the constructor unsafe to enforce the singleton invariant at the type level.
  2. Remove the "at most one instance" requirement from the Safety doc and clarify that multiple zero-sized handles are safe as long as disable/restore calls are paired correctly on the same core.

Current construction (single site in kernel_entry) follows the stated requirement, but the type system does not enforce it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bsp-qemu-virt/src/cpu.rs` around lines 34 - 41, The constructor
QemuVirtCpu::new currently is safe but its Safety doc requires a singleton;
change this to make the constructor unsafe to reflect the invariant at the type
level: update the function signature to pub const unsafe fn new() -> Self,
update the doc comment to state that callers must ensure at most one QemuVirtCpu
per physical core (or otherwise uphold disable_irqs()/restore_irq_state()
pairing on that core), and adjust call sites (e.g., kernel_entry) to call unsafe
{ QemuVirtCpu::new() }; ensure references to disable_irqs and restore_irq_state
and DAIF behavior remain in the Safety text so the invariant is explicit.
kernel/src/sched/mod.rs (5)

148-166: Struct bound differs from impl bounds and doc-comment claim.

Struct declaration at line 153 bounds only C: ContextSwitch, while the impls at 168/174 additionally require Cpu, and the doc at line 150 states "Generic over C: ContextSwitch + Cpu". Consider tightening the struct to Scheduler<C: ContextSwitch + Cpu> for consistency — otherwise a caller can instantiate Scheduler::<SomeCtxOnlyCpu> fields (though not new()), which creates a surprising API surface.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@kernel/src/sched/mod.rs` around lines 148 - 166, The struct's generic bound
is too weak: update the Scheduler declaration to require both traits so it
matches the impl blocks and docstring — change the declaration of Scheduler<C:
ContextSwitch> to Scheduler<C: ContextSwitch + Cpu> so the type parameter used
by fields like contexts: [C::TaskContext; ...] and the impls (the impl blocks
referencing Cpu) are consistent with Scheduler::new / impl Scheduler methods and
the doc comment.

276-282: Subtle self-yield path silently drains the ready queue.

When current is the only task in the system, line 273 enqueues it, then this dequeue() returns Some(current_handle) which falls through to the _ arm and returns Ok(()). The queue is now empty (the single task was consumed by dequeue and not put back), and self.current remains the same task. Functionally correct — the next yield_now will re-enqueue and repeat — but it means the ready queue is transiently out-of-sync with self.current in a way that's easy to miss when debugging. A short comment capturing this invariant ("after self-yield fast path, queue is empty and current is unchanged; next yield re-establishes [current]") would help future readers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@kernel/src/sched/mod.rs` around lines 276 - 282, The self-yield fast-path
silently dequeues the lone task (via self.ready.dequeue returning
Some(current_handle)) and returns early, leaving the ready queue empty while
self.current still points to the same task; add a concise comment near this
match (referencing self.ready.dequeue, current_handle, and self.current)
explaining the invariant: after this fast-path the ready queue is transiently
empty and self.current is unchanged and the next yield will re-enqueue the
current task, so readers understand the temporary desynchronization.

408-419: resolve_ep_cap collapses all lookup errors into InvalidCapability.

Line 414 maps any lookup error to IpcError::InvalidCapability, losing the distinction between a genuinely invalid handle, a stale generation, or other future variants. Since this helper is called on both IPC paths, the resulting SchedError::Ipc(InvalidCapability) may mask real failure modes. If CapabilityTable::lookup returns a typed error convertible to IpcError, forwarding it would give more accurate diagnostics.


203-222: add_task leaves partially-registered state if enqueue fails.

Lines 217–218 write task_states[idx] = Ready and task_handles[idx] = Some(handle) before the fallible enqueue call at 219–221. On the QueueFull path the slot is marked Ready with a stored handle but the task is not in the run queue — an orphaned state that the scheduler can't reach. The doc correctly notes this can't happen when TASK_ARENA_CAPACITY slots back the queue, but the ordering makes the invariant depend on a capacity argument rather than on local atomicity.

♻️ Suggested fix: enqueue first, then commit state
         unsafe {
             cpu.init_context(&mut self.contexts[idx], entry, stack_top);
         }
-        self.task_states[idx] = TaskState::Ready;
-        self.task_handles[idx] = Some(handle);
         self.ready
             .enqueue(handle)
-            .map_err(|_| SchedError::QueueFull)
+            .map_err(|_| SchedError::QueueFull)?;
+        self.task_states[idx] = TaskState::Ready;
+        self.task_handles[idx] = Some(handle);
+        Ok(())
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@kernel/src/sched/mod.rs` around lines 203 - 222, The current add_task sets
task_states[idx] and task_handles[idx] before the fallible
self.ready.enqueue(handle), which can leave a partially-registered orphan on
QueueFull; change the order so you call self.ready.enqueue(handle) first and
only on Ok(()) set self.task_states[idx] = TaskState::Ready and
self.task_handles[idx] = Some(handle) and then return Ok(()), mapping enqueue
errors to SchedError::QueueFull; keep the unsafe cpu.init_context(&mut
self.contexts[idx], entry, stack_top) as-is (it can run before enqueue) but
ensure no mutations to task_states/task_handles occur until enqueue succeeds.

367-400: Resumption path re-calls ipc_recv but doesn't handle a second Pending.

After resuming from the block (line 398), ipc_recv is called again and its result is returned directly. If — due to a bug elsewhere or a racing unblock — ipc_recv returns RecvOutcome::Pending again, this function returns Ok(Pending) to the caller, but task_states[current_idx] was set to Ready by the unblocker at line 429 and self.current was restored implicitly by the context switch. Semantically the caller now sees Pending for a task that is Ready and running — inconsistent with the contract that Pending means "blocked until resumed".

Given the design invariant that unblock only fires on SendOutcome::Delivered (guaranteeing a message is waiting), a second Pending should be impossible; but a debug_assert!(!matches!(outcome, RecvOutcome::Pending)) on the resumed path would catch violations during testing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@kernel/src/sched/mod.rs` around lines 367 - 400, Add a debug-only assertion
on the resumed-path call to ipc_recv to ensure it never returns
RecvOutcome::Pending: after the context switch resumes and before returning the
ipc_recv result, call ipc_recv into a local (e.g., let outcome =
ipc_recv(...).map_err(SchedError::Ipc)? or capture the RecvOutcome result) and
insert debug_assert!(!matches!(outcome, RecvOutcome::Pending)), then return
outcome (or map it) as before; reference symbols: ipc_recv,
RecvOutcome::Pending, task_states/current/current_idx (resumed path after
context_switch and before return).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bsp-qemu-virt/src/cpu.rs`:
- Around line 120-136: The AAPCS64 callee-saved SIMD registers d8–d15 are not
preserved: update Aarch64TaskContext and the assembly context switch to either
(preferred) extend the saved context with d8–d15 and implement corresponding
save/restore in context_switch_asm, or (alternate) force kernel-wide
soft-float/disable-NEON codegen so LLVM cannot allocate d8–d15; if you choose
the extend approach, add fields for d8–d15 (match their size/alignment), update
the total size comment and any code that assumes 104 bytes, and modify
context_switch_asm to store/restore those SIMD registers in the same layout and
adjust the stack offsets so the Rust struct and assembly layout remain
identical.

In `@bsp-qemu-virt/src/main.rs`:
- Around line 245-251: The calls to sched.add_task(...) are currently discarding
errors via .ok(), which can hide registration failures; change both
sched.add_task(cpu, handle_a, task_a, TASK_A_STACK.top()) and
sched.add_task(cpu, handle_b, task_b, TASK_B_STACK.top()) to handle the Result
instead (e.g., call .expect(...) or match and panic on Err) so setup will panic
with a clear message if task registration fails rather than silently continuing.
- Around line 69-72: The unsafe impl for StaticCell<T> and the unsafe
abstraction TaskStack must get their own dedicated audit entries and expanded
comments: create unique audit IDs (e.g. UNSAFE-2026-StaticCell-0001 and
UNSAFE-2026-TaskStack-0001), replace the current UNSAFE-2026-0001 reference on
StaticCell and the corresponding lines for TaskStack, and add an audit comment
block immediately above each unsafe impl/abstraction that (a) states why unsafe
is required, (b) lists the invariants the code upholds (concurrency, aliasing,
lifetime, initialization/Drop guarantees), and (c) explains why safer
alternatives (Mutex/RefCell/atomic types, stack-allocated arrays, or runtime
checks) were rejected; also record the new audit IDs in the repository's audit
tracking per docs/standards.
- Around line 141-149: The current code creates long-lived &mut Scheduler via
assume_init_mut() (SCHED) and then performs context switches
(start()/yield_now()), which can suspend a task while that mutable borrow is
live and lead to overlapping &mut references; change usages in the blocks that
call SCHED.0.get().assume_init_mut() / CPU.0.get().assume_init_ref() (occurring
around start(), yield_now(), and the other occurrences noted) to instead obtain
a raw *mut Scheduler / *const Cpu pointer from the UnsafeCell and call new
unsafe helper functions on Scheduler (e.g., scheduler_enter_raw(ptr) /
scheduler_yield_raw(ptr, cpu_ptr)) that operate via raw pointers so no &mut
Scheduler is materialized across context switches; ensure all places that
previously used sched.yield_now(cpu) or similar are routed through these
raw-pointer APIs and remove assume_init_mut() lifetimes that span calls that can
switch contexts.

In `@docs/analysis/tasks/phase-a/T-004-cooperative-scheduler.md`:
- Around line 24-27: The doc incorrectly references ADR-0020 as adding
Cpu::save_context / Cpu::restore_context and links a non-existent file; update
the text to state ADR-0020 defines a separate ContextSwitch trait with methods
context_switch and init_context (not Cpu::save_context/restore_context), remove
or correct the contradictory sentence about design on the later line, and fix
the broken link to point to the actual ADR/filename for ADR-0020; search for
symbols "ADR-0020", "ContextSwitch", "context_switch", "init_context", and
"Cpu::save_context"/"Cpu::restore_context" to locate and update the affected
lines.

In `@docs/audits/unsafe-log.md`:
- Around line 64-119: The pull request added several unsafe boundaries in the
BSP bootstrap that are not individually audited: add separate audit entries and
code comments for each unsafe in the bootstrap (rather than reusing
UNSAFE-2026-0001). Specifically, create new audit entries documenting why and
which invariants hold for unsafe impl Sync for StaticCell<T>, unsafe impl Sync
for TaskStack, unsafe fn TaskStack::top, and the static-cell write/assume_init_*
access blocks; then update the corresponding unsafe code comments in the BSP
bootstrap to include (a) why the unsafe is needed, (b) the invariants upheld,
and (c) why safer alternatives were rejected, referencing the same unique
symbols (StaticCell<T>, TaskStack, TaskStack::top, write, assume_init_*), so the
audit trail maps one-to-one to each unsafe boundary per docs/standards.

In `@docs/decisions/0019-scheduler-shape.md`:
- Line 23: Update the ADR text and API sketch to reference the separate
ContextSwitch trait instead of Cpu: replace mentions that say "context-switch
assembly lives in the BSP, exposed through the `Cpu` trait" with wording that it
lives in the BSP exposed through the `ContextSwitch` trait, and update the API
sketch to show `Scheduler<C: ContextSwitch>` (and any examples/signatures)
instead of tying context switching to `Cpu`; ensure all occurrences (including
the API sketch and the paragraph(s) around the current CPU reference) are
changed consistently so implementers are pointed to `ContextSwitch` per
ADR-0020.

In `@docs/decisions/0020-cpu-trait-v2-context-switch.md`:
- Around line 304-306: CPACR_EL1.FPEN is being enabled while context_switch_asm
only saves general-purpose callee-saved registers (x19–x28, x29, x30, sp), which
violates AAPCS64 because NEON/SIMD callee-saved registers d8–d15 are not
preserved; either add a global compile flag to disable SIMD codegen for kernel
tasks (e.g., rustflags/target-feature to force -mno-simd or equivalent) or
modify the context switch assembly (context_switch_asm) to save/restore d8–d15
across task switches and update ADR-0020 to document the change and rationale.
Ensure the chosen fix references CPACR_EL1.FPEN, context_switch_asm and d8–d15
so reviewers can find and verify the change.

In `@kernel/src/sched/mod.rs`:
- Around line 570-595: The test uses plain [u8; 512] buffers whose end pointer
(stack.as_mut_ptr_range().end) may not be 16-byte aligned, violating the safety
contract of ContextSwitch::init_context called by Scheduler::add_task; change
the tests (e.g., in yield_now_switches_context_and_updates_current and the
earlier test creating `stack`) to allocate a stack with explicit 16-byte
alignment (for example via a small wrapper type or helper like AlignedStack<512>
or using core::mem::align_of/#[repr(align(16))]) and pass its end pointer to
add_task; update the SAFETY comments to justify why the alignment is satisfied
(reference FakeCpu, add_task, and ContextSwitch::init_context) rather than
relying on FakeCpu being a no-op.
- Around line 233-254: The scheduler disables IRQs via an IrqGuard in
Scheduler::start but switches to a task stack that never drops that guard,
leaving IRQs disabled on task entry; either document this invariant on
Scheduler::start and Cpu::context_switch (and mention that Aarch64TaskContext
does not save PSTATE.DAIF so IRQ state does not follow tasks, and that yield_now
relies on guards on task stacks) or implement saving/restoring PSTATE.DAIF in
Aarch64TaskContext so Cpu::context_switch preserves IRQ state across switches;
pick one option, update the doc comments on the referenced functions/types
(Scheduler::start, Cpu::context_switch, Aarch64TaskContext, and yield_now) or
modify Aarch64TaskContext to include DAIF and adjust init_context/context_switch
paths accordingly to save/restore it.

---

Outside diff comments:
In `@docs/roadmap/phases/phase-a.md`:
- Around line 129-153: Milestone A5 and the T-004 task state disagree: update
the status so Milestone A5 (heading "Milestone A5 — Cooperative scheduler and
context switch") and the T-004 entry in the phase-a task index (the README that
lists T-004) and the T-004 task doc all show the same state (either "Done" or
"In Progress"); pick the correct final state, change the text labeled "✓ (done
2026-04-21)" or the T-004 line in the phase-a README to match, and ensure the
T-004 task document itself reflects that same status so A5, T-004, and the task
doc are consistent.

In `@kernel/src/lib.rs`:
- Around line 52-68: The public function kernel::run and its docs are dead —
either remove it or make it the real portable entry: Option A: delete pub fn
run<Con: Console, C: Cpu>(...) entirely, remove its docs and update the
kernel_entry() doc comment to state it is the BSP entry point; Option B:
refactor so kernel_entry() delegates to kernel::run by moving scheduler and task
initialization out of kernel_entry() into run (and then remove the unused _cpu
parameter or use it), ensuring run performs console setup, scheduler wiring and
never returns; pick one approach and update docs accordingly.

---

Duplicate comments:
In `@docs/analysis/tasks/phase-a/README.md`:
- Around line 11-12: The README table entry for T-004 ("Cooperative scheduler")
currently shows "In Progress" which contradicts the phase roadmap entry marking
Milestone A5/T-004 as "Done"; update the T-004 status so both sources match
(choose the canonical source of truth and align the other), e.g., change the
table row for T-004 in docs/analysis/tasks/phase-a/README.md to "Done
(2026-04-21)" and ensure the T-004 task markdown
(T-004-cooperative-scheduler.md) and the Milestone A5 reference reflect the same
final status and date.

---

Nitpick comments:
In `@bsp-qemu-virt/src/boot.s`:
- Around line 30-36: Update the comment above the FP/SIMD enable sequence (the
mov x0,`#0x300000` / msr cpacr_el1, x0 / isb block) to avoid asserting CPACR_EL1
resets to zero; instead state that we do not rely on the reset value and
explicitly write CPACR_EL1.FPEN = 0b11 (bits[21:20]) leaving other fields zero,
so FPEN is set and EL0/EL1 SIMD/FP traps are disabled before Rust/NEON code
runs.

In `@bsp-qemu-virt/src/cpu.rs`:
- Around line 34-41: The constructor QemuVirtCpu::new currently is safe but its
Safety doc requires a singleton; change this to make the constructor unsafe to
reflect the invariant at the type level: update the function signature to pub
const unsafe fn new() -> Self, update the doc comment to state that callers must
ensure at most one QemuVirtCpu per physical core (or otherwise uphold
disable_irqs()/restore_irq_state() pairing on that core), and adjust call sites
(e.g., kernel_entry) to call unsafe { QemuVirtCpu::new() }; ensure references to
disable_irqs and restore_irq_state and DAIF behavior remain in the Safety text
so the invariant is explicit.

In `@hal/src/cpu.rs`:
- Around line 86-119: The doc comment on IrqGuard currently includes a specific
codegen pathology claim; either add a reference link to the reproducer or
upstream issue that demonstrates "incorrect vtable references when the coercion
site is inlined near large `.rodata` constants" or reword the rationale to a
more generic explanation such as "avoid dynamic dispatch on critical-section
paths" to keep the justification maintainable; update the doc comment above the
IrqGuard struct (the text that references Cpu and vtable coercion) accordingly.

In `@kernel/src/sched/mod.rs`:
- Around line 148-166: The struct's generic bound is too weak: update the
Scheduler declaration to require both traits so it matches the impl blocks and
docstring — change the declaration of Scheduler<C: ContextSwitch> to
Scheduler<C: ContextSwitch + Cpu> so the type parameter used by fields like
contexts: [C::TaskContext; ...] and the impls (the impl blocks referencing Cpu)
are consistent with Scheduler::new / impl Scheduler methods and the doc comment.
- Around line 276-282: The self-yield fast-path silently dequeues the lone task
(via self.ready.dequeue returning Some(current_handle)) and returns early,
leaving the ready queue empty while self.current still points to the same task;
add a concise comment near this match (referencing self.ready.dequeue,
current_handle, and self.current) explaining the invariant: after this fast-path
the ready queue is transiently empty and self.current is unchanged and the next
yield will re-enqueue the current task, so readers understand the temporary
desynchronization.
- Around line 203-222: The current add_task sets task_states[idx] and
task_handles[idx] before the fallible self.ready.enqueue(handle), which can
leave a partially-registered orphan on QueueFull; change the order so you call
self.ready.enqueue(handle) first and only on Ok(()) set self.task_states[idx] =
TaskState::Ready and self.task_handles[idx] = Some(handle) and then return
Ok(()), mapping enqueue errors to SchedError::QueueFull; keep the unsafe
cpu.init_context(&mut self.contexts[idx], entry, stack_top) as-is (it can run
before enqueue) but ensure no mutations to task_states/task_handles occur until
enqueue succeeds.
- Around line 367-400: Add a debug-only assertion on the resumed-path call to
ipc_recv to ensure it never returns RecvOutcome::Pending: after the context
switch resumes and before returning the ipc_recv result, call ipc_recv into a
local (e.g., let outcome = ipc_recv(...).map_err(SchedError::Ipc)? or capture
the RecvOutcome result) and insert debug_assert!(!matches!(outcome,
RecvOutcome::Pending)), then return outcome (or map it) as before; reference
symbols: ipc_recv, RecvOutcome::Pending, task_states/current/current_idx
(resumed path after context_switch and before return).
🪄 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: a9f441a3-0e87-4266-966f-73b58b421ee3

📥 Commits

Reviewing files that changed from the base of the PR and between 7378cb6 and 4fe547c.

📒 Files selected for processing (18)
  • bsp-qemu-virt/src/boot.s
  • bsp-qemu-virt/src/cpu.rs
  • bsp-qemu-virt/src/main.rs
  • docs/analysis/tasks/phase-a/README.md
  • docs/analysis/tasks/phase-a/T-003-ipc-primitives.md
  • docs/analysis/tasks/phase-a/T-004-cooperative-scheduler.md
  • docs/audits/unsafe-log.md
  • docs/decisions/0018-badge-scheme-and-reply-recv-deferral.md
  • docs/decisions/0019-scheduler-shape.md
  • docs/decisions/0020-cpu-trait-v2-context-switch.md
  • docs/decisions/README.md
  • docs/roadmap/current.md
  • docs/roadmap/phases/phase-a.md
  • hal/src/context_switch.rs
  • hal/src/cpu.rs
  • hal/src/lib.rs
  • kernel/src/lib.rs
  • kernel/src/sched/mod.rs

Comment thread bsp-qemu-virt/src/cpu.rs
Comment thread bsp-qemu-virt/src/main.rs Outdated
Comment thread bsp-qemu-virt/src/main.rs Outdated
Comment on lines +141 to +149
// SAFETY: SCHED and CPU are fully initialised before `start()`.
// Cooperative scheduling ensures no concurrent mutation.
// Audit: UNSAFE-2026-0001.
let sched = unsafe { (*SCHED.0.get()).assume_init_mut() };
// SAFETY: same invariants as SCHED above. Audit: UNSAFE-2026-0001.
let cpu = unsafe { (*CPU.0.get()).assume_init_ref() };
// yield_now returns Err only if current == None, which cannot
// happen once the scheduler has started.
let _ = sched.yield_now(cpu);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect global scheduler mutable borrows and switching call sites.
# Expectation: no `assume_init_mut()`-derived `&mut Scheduler` remains live across `start`/`yield_now`.

rg -n -C4 '\(\*SCHED\.0\.get\(\)\)\.assume_init_mut\(\)|\.start\(cpu\)|\.yield_now\(cpu\)|fn (start|yield_now)\b|unsafe fn (start|yield_now)\b' \
  bsp-qemu-virt/src/main.rs kernel/src/sched/mod.rs

Repository: cemililik/UmbrixOS

Length of output: 4426


Fix overlapping mutable borrows to scheduler across context switches.

assume_init_mut() creates an exclusive &mut Scheduler reference that persists across context switches. When start() or yield_now() switches away, the borrow remains live on the suspended task's stack. When another task resumes and calls assume_init_mut() on the same UnsafeCell, overlapping &mut Scheduler references exist simultaneously, violating Rust's aliasing model.

This occurs at lines 141–149, 171–175, and 265. Route all scheduler entry and yield operations through an unsafe raw-pointer API that avoids materializing overlapping mutable references across task context switches.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bsp-qemu-virt/src/main.rs` around lines 141 - 149, The current code creates
long-lived &mut Scheduler via assume_init_mut() (SCHED) and then performs
context switches (start()/yield_now()), which can suspend a task while that
mutable borrow is live and lead to overlapping &mut references; change usages in
the blocks that call SCHED.0.get().assume_init_mut() /
CPU.0.get().assume_init_ref() (occurring around start(), yield_now(), and the
other occurrences noted) to instead obtain a raw *mut Scheduler / *const Cpu
pointer from the UnsafeCell and call new unsafe helper functions on Scheduler
(e.g., scheduler_enter_raw(ptr) / scheduler_yield_raw(ptr, cpu_ptr)) that
operate via raw pointers so no &mut Scheduler is materialized across context
switches; ensure all places that previously used sched.yield_now(cpu) or similar
are routed through these raw-pointer APIs and remove assume_init_mut() lifetimes
that span calls that can switch contexts.

Comment thread bsp-qemu-virt/src/main.rs Outdated
Comment thread docs/analysis/tasks/phase-a/T-004-cooperative-scheduler.md
Comment thread docs/audits/unsafe-log.md
Comment thread docs/decisions/0019-scheduler-shape.md Outdated
Comment thread docs/decisions/0020-cpu-trait-v2-context-switch.md
Comment thread kernel/src/sched/mod.rs
Comment thread kernel/src/sched/mod.rs Outdated
cemililik and others added 3 commits April 21, 2026 16:42
…-int-log

Three silent hangs during A5 debugging revealed gaps in documented boot
requirements and unsafe coding rules. This commit closes those gaps so the
same bugs cannot repeat unnoticed on future BSP targets.

docs/standards/bsp-boot-checklist.md (new)
  Six-item ordered checklist for every new BSP: EL verification, CPACR_EL1
  FP/SIMD enable, VBAR, SP alignment, BSS zeroing, and the naked-asm rule for
  context-switch functions. Each item explains what goes wrong if skipped and
  how to diagnose it. Includes a diagnostic cheat sheet mapping symptom →
  first check, and the QEMU -d int invocation.

docs/standards/unsafe-policy.md — new §5a
  Codifies that any function saving/restoring SP in inline asm must be
  #[unsafe(naked)] with naked_asm!. Shows a correct and a broken example.
  Explains why #[inline(never)] is not sufficient (compiler still emits the
  stp x29,x30,[sp,#-N]! prologue).

tools/run-qemu.sh — --int-log flag
  Passes -d int -D /tmp/qemu_int.log to QEMU when requested. Eliminates the
  need to remember or look up the QEMU exception-log flags mid-debug session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All findings from the post-A5 review verified against current code and
fixed or explicitly documented. Summary by category:

Safety / correctness
  - bsp-qemu-virt/src/cpu.rs: extend Aarch64TaskContext with d8_d15 [u64;8]
    and update context_switch_asm to save/restore AAPCS64 callee-saved SIMD
    registers d8–d15. Total context size: 104 → 168 bytes. Without this fix
    a task that causes NEON register allocation in d8–d15 would corrupt state
    across a cooperative yield.
  - kernel/src/sched/mod.rs: reorder add_task so ready.enqueue() is called
    before task_states/task_handles are set; a QueueFull error no longer
    leaves partial registration.
  - bsp-qemu-virt/src/main.rs: change add_task .ok() to .expect() so setup
    failures panic with a message instead of silently continuing.
  - bsp-qemu-virt/src/main.rs: restructure task_a/task_b yield calls so
    &mut Scheduler is not bound to a named variable that persists as a loop
    local across context switches. Add UNSAFE-2026-0012 audit entry documenting
    the deliberate aliasing relaxation under the cooperative model.

API / type correctness
  - bsp-qemu-virt/src/cpu.rs: make QemuVirtCpu::new() unsafe to match its
    # Safety invariant (at most one instance per core).
  - kernel/src/sched/mod.rs: add Cpu bound to Scheduler<C> struct declaration
    to match the existing impl<C: ContextSwitch + Cpu> blocks.

Documentation / comments
  - bsp-qemu-virt/src/cpu.rs: fix disable_irqs SAFETY comment — the
    instruction is MSR DAIFSet, not MSR DAIF.
  - bsp-qemu-virt/src/cpu.rs: add (c) "why safer alternatives rejected" to
    Send/Sync SAFETY comments.
  - bsp-qemu-virt/src/boot.s: remove "CPACR_EL1 resets to zero" assertion;
    state we write FPEN explicitly without relying on reset value.
  - bsp-qemu-virt/src/main.rs: add (c) to StaticCell and TaskStack SAFETY
    comments; update audit IDs to UNSAFE-2026-0010 and UNSAFE-2026-0011.
  - hal/src/cpu.rs: reword IrqGuard doc comment to avoid an overly specific
    codegen claim about .rodata layout.
  - kernel/src/sched/mod.rs: add IRQ-state-on-entry doc to Scheduler::start;
    add self-yield fast-path invariant comment in yield_now; add single-waiter
    semantics doc to unblock_receiver_on.
  - docs/analysis/tasks/phase-a/T-004-cooperative-scheduler.md: fix
    ADR-0020 API references (save_context/restore_context → context_switch/
    init_context; Cpu trait → ContextSwitch trait); update frame size comment.
  - docs/decisions/0019-scheduler-shape.md: replace "Cpu trait" with
    "ContextSwitch trait" in constraints and references sections.
  - docs/analysis/tasks/phase-a/README.md: T-004 status → Done (2026-04-21).

Tests
  - kernel/src/sched/mod.rs: replace plain [u8; 512] stacks with
    AlignedStack<512> (#[repr(C, align(16))]) in Scheduler tests so the
    stack_top pointer satisfies init_context's 16-byte alignment contract.
  - kernel/src/sched/mod.rs: add debug_assert in ipc_recv_and_yield that
    the second ipc_recv call after resume never returns Pending.

Audit log
  - docs/audits/unsafe-log.md: add UNSAFE-2026-0010 (StaticCell<T> Sync),
    UNSAFE-2026-0011 (TaskStack Sync), UNSAFE-2026-0012 (&mut Scheduler
    aliasing across cooperative yield).

Dead code
  - kernel/src/lib.rs: remove the stale pub fn run<Con, C>() stub, which
    printed a greeting and spun. kernel_entry() is the real entry point and
    the stub was never called.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers the full procedure for adding a new BSP crate to the workspace:
boot-flow ADR first, crate skeleton, the six-item boot checklist from
bsp-boot-checklist.md, cpu.rs with naked context switch and d8-d15,
console.rs, linker script, run script with --int-log, and smoke test
verification. Acceptance criteria require the naked-fn rule and SIMD
register coverage to be met before the task is considered done.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cemililik

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cemililik
cemililik merged commit cb93322 into main Apr 21, 2026
2 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Apr 21, 2026
cemililik added a commit that referenced this pull request Apr 27, 2026
…le SAFETY)

Verified each finding; all five applied as suggested.

Inline #1 — boot.md halt-loop syntax in Mermaid diagram + body text
- Line 41 (Mermaid sequence diagram): `halt: wfe; b .-` →
  `halt_unsupported_el: wfe ; b halt_unsupported_el`. The named-label
  form matches the actual asm in `bsp-qemu-virt/src/boot.s` and
  removes the malformed `.-` token.
- Line 16 (text description): `wfe; b .-` halt loop wording corrected
  in two places — the EL3 path now reads "named-label `wfe`-loop
  (`halt_unsupported_el: wfe ; b halt_unsupported_el`)" and the
  kernel_entry-return defensive halt now reads `wfe ; b 2b` (matching
  the local-label loop at the bottom of `_start`).

Inline #2 — UNSAFE-2026-0017 GAS halt-loop syntax (Amendment)
- §Operation said "halt via `wfe; b -1b`". `-1b` is not valid GAS
  syntax — `1b` is the back-reference to local label `1:`, but a
  leading `-` is meaningless there. Real asm uses
  `b halt_unsupported_el`.
- §Rejected alternatives → "Halt on EL3 with a panic frame" said
  "`wfe; b .-` is the visible silence". `b .-` is a similar
  malformed token (`.` is the current address; `b .-` with no offset
  is not a valid branch target).
- Both occurrences corrected via an Amendment block per
  unsafe-policy.md §3 (the entry was committed in f289d4d / T-013;
  in-place edits to a committed entry's body are forbidden). The
  behaviour the audit describes is unchanged; only the prose's asm
  rendering was wrong.

Outside-diff #4 — bsp-qemu-virt/src/cpu.rs SAFETY blocks for
CNTFRQ_EL0 (`new()`) + CNTVCT_EL0 (`now_ns()`)
- Both blocks claimed "boot.s performs no EL transition" — true
  before T-013, stale after. Updated to reflect ADR-0024: `boot.s`
  drives an EL2 → EL1 transition when the firmware/emulator delivers
  at EL2, falls through at EL1, halts at EL3. The runtime EL-check
  via `tyrne_hal::cpu::current_el()` (audited under
  UNSAFE-2026-0018) is the checked invariant pinning `CurrentEL == 1`
  at this point. Both SAFETY paragraphs now name UNSAFE-2026-0017
  (the boot.s sequence) explicitly, and document (a) why-unsafe-is-
  required, (b) invariants, (c) rejected alternatives per
  CLAUDE.md rule 2.

Nitpick — UNSAFE-2026-0018 cfg-gating wording (Amendment)
- §Invariants relied on → "Cfg-gating" said "user code reading
  `CurrentEL` would trap or yield `EL0` with no useful information".
  The "or yield `EL0`" alternative is wrong: per ARM ARM §D11.2 /
  §C5.2, `MRS x, CurrentEL` at EL0 is undefined — the system register
  is not accessible at EL0 and the read raises an Undefined
  Instruction exception (which becomes `SIGILL` on hosted Unix-like
  targets such as `aarch64-apple-darwin`). There is no fallback EL0
  read. Corrected via Amendment block (same §3 reasoning as
  UNSAFE-2026-0017 above).

Nitpick — T-008 task file:120 review-history release-note structure
- Single dense paragraph split into structured bullets using
  `<br/>•` separators inside the table cell: New docs / Updated docs
  / Scope discipline / Verification / State transition. Same factual
  content, scannable.

Verification: cargo fmt clean; host-clippy / kernel-clippy / kernel-
build clean; host-test 143 / 143 green.

Refs: ADR-0024
Audit: UNSAFE-2026-0015, UNSAFE-2026-0017, UNSAFE-2026-0018

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cemililik added a commit that referenced this pull request Apr 28, 2026
Two Düşük findings from the T-012-arc approval review:

1. `bsp-qemu-virt/src/main.rs::idle_entry` — function-level doc-comment
   was stale: still described the body as `spin_loop` + `yield_now`
   ("not `wfi` + `yield_now` — until a timer IRQ source lands (T-009)")
   and projected `wait_for_interrupt` as future work. The body itself
   landed in commit b4ed68c using `cpu.wait_for_interrupt()`; only the
   doc-comment lagged. The PR-#9 post-fix-sweep DoD rule
   (code-review.md item 7 — `git grep -F` after a behaviour change)
   would have caught this. Rewritten to describe the now-current
   behaviour: WFI + yield, ADR-0022 first-rider Sub-rider closed,
   v1's IPC demo never reaching idle in practice (so WFI is
   structurally unreachable in the demo path).

2. `docs/decisions/0021-raw-pointer-scheduler-ipc-bridge.md:123` —
   Amendment header read "2026-04-27" but the cited commit `28c5ce9`
   landed 2026-04-28; the matching UNSAFE-2026-0014 Amendment in
   `docs/audits/unsafe-log.md:225` already had the correct
   2026-04-28 date. One-character fix bringing the two Amendments
   into agreement on when the discipline-extension landed.

Bilgi #3 (TrapFrame `_reserved` never written by trampoline) and
Bilgi #4 (`compiler_fence` on spurious-acknowledge path is defensive)
are noted but not actioned — observation-grade only.

Gates: cargo fmt / host-clippy / kernel-clippy / kernel-build clean.
148/148 host tests unchanged (no code paths touched).

Refs: T-012, ADR-0021 (Amendment), ADR-0022 (Sub-rider closure)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cemililik added a commit that referenced this pull request Apr 28, 2026
Code (5 fixes):

1. `bsp-qemu-virt/src/exceptions.rs` — irq_entry and panic_entry are now
   `unsafe extern "C" fn` (was `extern "C" fn`). Both have caller-side
   safety preconditions (frame validity for irq_entry; class validity
   for panic_entry) already documented in `# Safety` doc-comments;
   making them `unsafe fn` enforces the contract at the type level for
   any future Rust caller. The asm trampolines `bl` them unchanged.

2. `bsp-qemu-virt/src/gic.rs` — enable/disable now assert
   `irq.0 < GIC_MAX_IRQ` (1020) per ARM IHI 0048B §4.3.2 before
   computing distributor offsets. Without the check, an out-of-range
   IrqNumber would compute an MMIO offset outside the distributor
   window even with saturating arithmetic — writing to reserved or
   wrong-window addresses. The IrqController trait returns no Result,
   so panic is the right mechanism for kernel-internal contract
   violations. SAFETY comments updated to cite the range invariant.

3. `hal/src/timer.rs` — ns_to_ticks switched from floor to ceiling
   division (via `u128::div_ceil`). ADR-0010 §Decision outcome
   specifies `arm_deadline` semantics as "When `now_ns()` reaches or
   exceeds `deadline_ns`, the hardware timer IRQ fires"; flooring
   could arm the comparator at a tick whose time-equivalent is
   sub-tick *before* deadline_ns, violating the contract. Ceiling
   guarantees the comparator's tick-equivalent is ≥ deadline_ns. New
   `# Rounding` doc section explains the choice. Existing tests
   (round-trip at 62.5 MHz divisor frequency, one-second-yields-
   frequency at multiple frequencies, zero-ns, panic-on-zero-frequency)
   all still pass — they exercise frequencies that divide evenly into
   1e9 ns/s, where ceiling and floor agree.

4. `hal/src/timer.rs::ns_to_ticks_saturates_at_u64_max` — added two
   over-boundary cases (`freq = 1_000_000_001` and `freq = u64::MAX`)
   so the saturation *branch* (not just the boundary) is exercised.
   The original test hit only the exact boundary
   (`huge_ns * 1e9 = u64::MAX * 1e9` divides exactly).

5. `bsp-qemu-virt/src/exceptions.rs::TrapFrame` — no code change;
   the doc-comment's "Padding" claim about `_reserved` was already
   correct. Reviewer's Bilgi #3 was an observation, not a fix
   request.

Documentation (8 fixes):

6. `.claude/skills/conduct-approval-review/SKILL.md` — Turkish quote
   in line 30 translated to English ("kabul edilen bulguların tamamı
   düzeltildi mi?" → "Have all accepted findings been fixed?");
   severity labels in the output-format template translated
   (`Yüksek`/`Orta`/`Düşük` → `High`/`Medium`/`Low`); the line-53
   "Yüksek finding" prose translated; the example fenced code block
   gained a `markdown` language tag. CLAUDE.md §3 mandates English in
   all committed artefacts.

7. `docs/analysis/reviews/business-reviews/2026-04-27-B0-closure.md` —
   "Five `unsafe` regions" → "Seven" to match the actual table count
   (UNSAFE-2026-0012 through 0018 inclusive = 7 entries).

8. `docs/analysis/tasks/phase-b/README.md` — T-012 row status changed
   from `In Progress (design-first 2026-04-28)` to
   `In Review (2026-04-28)` to match the task file's header.

9. `docs/architecture/exceptions.md` — IRQ dispatch flow Mermaid
   diagram updated to reflect shipped behaviour: timer-IRQ branch
   now shows "msr CNTV_CTL_EL0 → gic.end_of_interrupt → return"
   (ack-and-ignore for v1) instead of the placeholder
   sched::on_timer_irq call. Spurious-INTID arm added (returns
   without EOI per GICv2 architecture spec). Saved-register list
   corrected: trampoline saves `x0..x18, x30` (not `x0..x29`); the
   prose now explicitly states that `x19..x29` are AAPCS64
   callee-saved and preserved by the Rust handler, not pushed by
   the trampoline.

10. `docs/decisions/0021-raw-pointer-scheduler-ipc-bridge.md` — the
    Amendment paragraph that said "When the first scheduler-touching
    IRQ handler lands, ... UNSAFE-2026-0014 ... gains an Amendment"
    reworded to reflect that the Amendment **already exists** (commit
    `28c5ce9`) and v1's body has no live citation yet because
    `irq_entry` is ack-and-ignore. Future scheduler-touching arcs
    add a follow-up Amendment recording the activation.

11. `docs/roadmap/current.md` — pre-T-011 baseline coverage numbers
    reconciled with the canonical B0 closure retro: sched/mod.rs was
    `83.93%` → `84.16%`; workspace was `96.33%` → `96.37%`. The post-
    T-011 numbers (93.97% / 96.37%) were already correct.

12. `docs/roadmap/phases/phase-b.md` — "T-012 closed" → "T-012
    delivered (pending verification)". The bullet's existing
    parenthetical already explained that closure waits on QEMU
    verification; the lead label is now consistent with that.

13. `docs/analysis/tasks/phase-b/T-012-exception-and-irq-infrastructure.md`
    — Dependencies field: T-009 status `In Review` → `Done` (closed
    2026-04-27 via PR #9); ADR-0024 mention sharpened to record its
    Accept date (2026-04-27).

Bilgi findings #3 (`TrapFrame._reserved` never written) and #4
(spurious-acknowledge `compiler_fence` is defensive-not-load-bearing)
intentionally not actioned per their "no action today" tags.

Gates: cargo fmt / host-clippy / kernel-clippy / kernel-build all
clean. host-test 148 / 148 unchanged (24 hal + 90 kernel + 34
test-hal); the new ns_to_ticks_saturates_at_u64_max assertions
extend an existing test rather than add a new one.

Refs: T-012, ADR-0010 (rounding clarification rider candidate),
ADR-0021 (Amendment reword), ADR-0024 (precondition language)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cemililik added a commit that referenced this pull request May 8, 2026
… 2026-05-08 + 2026-05-07 follow-ups

Closes the 12 remaining items flagged by the 2026-05-08 multi-axis
review's §Follow-up backlog (3 Track-2 Majors, 1 Track-3 Major fix
already closed in `59c08e9`, 6 Track-3 / Track-2 Minors, 2 Track-4
Minors, 2 Track-2 Nits) plus the carry-forward 2026-05-07 Track-H
NIT-1.

**Track 2 Majors (forward-flagged → ADR-0027 / phase-b ledger riders):**
- M1 (escape-hatch doc): ADR-0027 §Decision outcome (c) gains a bullet
  documenting `mem::forget` / `ManuallyDrop` / `let _ = ...` as
  deliberate-but-rare escape hatches, mirroring the
  `x86_64::structures::paging::MapperFlush` precedent.
- M2 (MMU-instance binding): ADR-0027 §Decision outcome (c) gains a
  bullet noting `MapperFlush::flush(self, mmu: &impl Mmu)` accepts
  any `Mmu`; multi-`Mmu` deployments (B3+ per-task `AddressSpace`,
  Phase C multi-CPU) will need a stronger token type. Out of scope
  for v1.
- M3 (ADR-0034 placeholder): ADR-0027 §Decision outcome adds an
  "ADR-0034 (kernel-image section permissions) placeholder" block
  alongside ADR-0033, and `phase-b.md` ADR ledger gains rows for
  both ADR-0033 and ADR-0034 with named-but-unallocated discipline.

**Track 3 Minors (governance / wording polish):**
- m1 + m2 (current.md L52): drop "Phase-2" prefix on "§Simulation
  table" (the table walks Steps 0–4, not a "Phase 2"); "Accept will
  be" → "Accept landed as" + actual commit SHA `bb0a6ba`.
- m3 (commit-style.md PR-numbering rider): new §"PR-number references
  in committed artefacts" subsection naming the recurrence (PR #18 +
  PR #20 each had a one-commit PR-number fix-up) and codifying three
  acceptable disciplines (defer banner authoring; reference branch
  slug; or use commit SHA).
- n1 (framing alignment): "first to apply Simulation forward" wording
  in current.md (banner + Active decisions row) and phase-b.md §B2
  status block aligned to the precise "first non-recovery-primitive
  state-machine ADR drafted under §Simulation" phrasing — ADR-0032's
  Propose did land with a table; the prior framing was technically
  defensible only under a narrow reading of "retro-extracted".

**Track 2 Nits (substance riders in ADR-0027):**
- #4 (DSB ISH vs DSB NSH rationale): §Simulation gains a rationale
  paragraph after the table — `ISH` is forward-compatible with the
  eventual SMP boot, sub-microsecond cost on single-core, matches
  Linux aarch64 `arch/arm64/mm/proc.S` for the same reason.
- #5 (TCR_EL1.AS wording tighten): line 59 reworded — `AS = 0`
  selects 8-bit ASID *size*, not "the ASID value is 0" (the value
  is `TTBR0_EL1.ASID = 0` and is what's "globally used in v1").
- #7 (line 17 §-citation precision) + Track-3 n1: "first
  non-recovery-primitive state-machine" framing now precise.
- #8 (memory-management.md L88 page-table descriptor cosmetic):
  ASCII bit-field diagram redrawn to match L2 block-descriptor
  reality (OutputAddress[47:21], not [47:12]); explanatory note
  added for L1-block / L3-page variants.

**Track 4 Minors (perf-harness.sh):**
- #1 (Ctrl-C cleanup trap): new `cleanup_in_flight` shell function +
  `trap '...' EXIT INT TERM` that kills any in-flight QEMU + watchdog
  PIDs tracked in `CURRENT_CMD_PID` / `CURRENT_WATCHDOG_PID` shell
  globals. `run_with_timeout` updates the globals at every call so
  the trap addresses whichever pair is currently in flight; clears
  them at every clean exit so the trap is a no-op outside iterations.
- #2 (p99 small-N reporting hygiene): generated baseline report
  Methodology section gains a "**Note on p99 at small `n`**" paragraph
  explaining that under nearest-rank `p99 == max` for `n < 100` and
  callers should not over-read it as a tail-latency signal until
  `n >= 100`.

**Track 4 Nit #3 (read_stats refactor):** **Already closed by PR #21
review-round commit `ef30b5c`** — the 8 `echo | awk` parses became a
single `while read` loop. Verified at HEAD (`grep -c "while read -r
key val" tools/perf-harness.sh` → 1 hit).

**2026-05-07 Track-H NIT-1 (Pending Amendment closure-path indexing):**
UNSAFE-2026-0019 / 0020 / 0021 each gain a 2026-05-08 "closure-path
indexed" Amendment naming the canonical clearance trigger (B5
Milestone, ADR-0030 entry-point, deadline-arming syscall) explicitly,
so a future reader of `unsafe-log.md` alone has the full picture
without leaving the file. No semantic change; co-locates information
that was previously distributed across `phase-b.md` cross-references.

Verification gates re-run on the integration branch:
- `cargo fmt --all -- --check` clean
- `cargo host-test` 159/159 (25 + 100 + 34)
- `cargo host-clippy` clean (-D warnings)
- `cargo kernel-clippy` clean
- `cargo kernel-build` clean
- `tools/perf-harness.sh --iterations=3` runs end-to-end with the new
  trap + Methodology note + while-read parsing intact

This commit + the prior 2026-05-08 review's recommendations close all
follow-ups identified by both 2026-05-07 and 2026-05-08 multi-axis
reviews. Forward-flagged items (10/12/13 from 2026-05-07) and any
review-round bot input on this integration branch remain the only
open items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cemililik added a commit that referenced this pull request May 9, 2026
Doc-only fixup commit on the B3-prep PR. Reviewer Approve-with-nits
verdict; all 10 actionable findings applied. No merge-blocker;
cargo gates unchanged from main (185/185 host; clippy / fmt /
kernel-build clean).

Applied (Minor):

1. **Finding 3.1 — §Simulation row 3 wrap-scan rationale.** The
   "two-pass scan ensures correctness even under hint-past-last-
   free" claim describes a state that cannot occur in v1 (the
   unconditional rewind in row 2 keeps `hint ≤ lowest-free-index`).
   Reworded row 3 to honestly frame the wrap as forward-compat
   scaffolding for the future SMP extension where per-core caches
   may leave the global hint stale.

2. **Finding 4.1 — UNSAFE-2026-0001 umbrella adjudication-deferred.**
   The reviewer correctly noted that UNSAFE-2026-0001's actual
   scope (PL011 MMIO base blessing) is semantically distant from
   PMM frame-zeroing (raw write_bytes over normal-cached RAM).
   Added an explicit "adjudication-deferred to T-017's security
   review" soft-flag in §Dependency chain step 5, propagated to
   §Decision drivers + §Decision outcome + Option A pros + T-017
   §Approach commit 2. The Amendment-vs-new-entry call is now
   honestly framed as a borderline judgment for T-017's review;
   either outcome is bounded.

3. **Finding 8.1 — repurposed T-017 test #10.** The original
   `free_frame_rejects_pa_in_reserved_range` overlapped with test
   #4 (which already pinned `free_frame(reserved)` → `DoubleFree`).
   Repurposed to `free_frame_reserved_check_iterates_only_populated_slots`
   to pin the populated-slots-only contract on the defensive scan
   (genuinely O(populated-entries), not O(R)). Combined coverage
   is now: test #4 = "reserved-PA correctly rejected"; test #10
   = "non-reserved-PA correctly accepted under partially-populated
   array".

4. **Finding 8.2 — `MAX_RESERVED_RANGES` per-BSP const generic.**
   The hardcoded `8` was unjustified and risked TooManyReservedRanges
   on future BSPs whose typical aarch64 boot-stack reservations
   (DTB + ATF + ACPI + initrd + framebuffer) could land 7–9
   ranges. Promoted to a per-BSP const generic `R` consistent with
   the existing `PMM_BITMAP_BYTES` `N` parameterisation: the kernel
   crate exposes `Pmm<const N: usize, const R: usize>`; v1's
   `bsp-qemu-virt` picks `R = 8`; future BSPs pick what they need.
   The `expect("...")` BSP panic is structurally unreachable when
   `R` is sized per the BSP's static reservation list.

5. **Finding 10.1 — Option A regime-independence qualifier.**
   The bare "regime-independent (high-half-portable)" claim in
   §Pros didn't carry the qualifier from §Decision outcome / §Positive
   ("metadata addressing"). Added the same qualifier to §Pros and
   noted that the frame-zeroing step's `*mut u8` from PA is
   identity-mapping-dependent today — a `phys_to_virt` helper is
   needed post-high-half (but the same helper would be needed for
   the rejected Option C, so Option A's regime-independence
   advantage at the algorithm-and-metadata level still stands).

Applied (Nit):

6. **Finding 3.2 — §Simulation row 0 `.bss` zero-fill prose.** The
   "bitmap zero-initialised by `.bss` zero-fill" precondition was
   technically true but not load-bearing — `Pmm::new` constructs
   a fully-initialised value. Reworded row 0 to acknowledge this:
   "the zero-init is precondition-only, not load-bearing".

7. **Finding 5.1 — Option B u32/u16 indices math.** The 128 KiB
   metadata claim assumed `u32` indices; `u16` halves to 64 KiB
   for v1's 32 K frames. Tightened the math with a parenthetical
   ("64 KiB with `u16` since 32 K fits in 15 bits — but `u32`
   is forward-portable to BSPs with > 64 K frames such as Pi 4
   with 1 M frames at 4 GiB"). Qualitative judgment unchanged
   (still 16–32× the bitmap).

8. **Finding 8.3 — §Approach commit 1 test set.** Moved
   `new_rejects_too_many_reserved_ranges` from "implicit later
   commit" to commit 1 alongside `new_marks_reserved_ranges_and_initialises_counters`
   (both pin `Pmm::new`, so they belong in the constructor commit).

9. **Finding 9 — UNSAFE-2026-0001 umbrella name consistency.** The
   ADR's prose drifted between "kernel-static-buffer raw-pointer
   write to identity-mapped memory" (L35) and "MMIO + kernel-static-
   buffer raw-pointer" (L58). Picked the former as the canonical
   phrasing and aligned both surfaces.

10. **Finding 12.1 — T-016 §"Why one bundled task" disambiguation.**
    The line "instead of T-016 + T-017 + T-018?" was written when
    those numbers were hypothetical sub-task splits of T-016 itself.
    Post-T-017 (PMM) + T-018 (AddressSpace, future) numbering, the
    line risked confusion. Added a parenthetical clarifying it's
    about a hypothetical 3-way split of T-016, not the actual
    post-T-016 numbering.

Bonus — Finding 12.2 (kernel-internal framing) — slight cosmetic
qualifier added: "kernel-internal at runtime" + a parenthetical
clarifying that the BSP touches the PMM only at construction time
(linker-symbol → Pmm::new → StaticCell publication) and never
afterwards.

Verification:
  - cargo fmt --check           clean
  - cargo test                  185/185 host (unchanged — docs only)
  - cargo kernel-build          clean

Files touched:
  - docs/decisions/0035-physical-memory-manager.md (+30 / -20)
  - docs/analysis/tasks/phase-b/T-017-physical-memory-manager.md (+18 / -10)
  - docs/analysis/tasks/phase-b/T-016-mmu-activation.md (+1 / -1)

This commit completes the PR #25 review-round; all reviewer-flagged
findings closed. The Accept commit (2de67f1) carrying ADR-0035 →
Accepted stays as the load-bearing flip; this fixup commit is the
honesty / consistency polish pass on top.

Refs: ADR-0035, T-017
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