Skip to content

T-017: Physical Memory Manager (PMM) bring-up — bitmap allocator + reservation tracking - #26

Merged
cemililik merged 8 commits into
mainfrom
t-017-physical-memory-manager
May 10, 2026
Merged

T-017: Physical Memory Manager (PMM) bring-up — bitmap allocator + reservation tracking#26
cemililik merged 8 commits into
mainfrom
t-017-physical-memory-manager

Conversation

@cemililik

@cemililik cemililik commented May 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements T-017 (B3 §1) per ADR-0035: bitmap-based Physical Memory Manager over QEMU virt's 128 MiB RAM extent, with reservation tracking, FrameProvider impl, and BSP boot-time wiring. The PMM is the prerequisite layer below B3's address-space abstraction (ADR-0028 + T-018, next); v1's cooperative IPC demo never calls alloc_frame, but the PMM is live and verified at every boot via the new `tyrne: pmm initialized (32604 frames available; 164 reserved)` banner line.

Five commits (four bisectable + one docs polish)

  1. `feat(kernel): T-017 commit 1 — Pmm struct + Pmm::new (kernel/src/mm/)` (2533732)
    New `kernel/src/mm/` subsystem parent + `Pmm<const N: usize, const R: usize>` struct shape + `Pmm::new(extent, reserved) -> Result<Self, PmmError>` with three fail-fast pre-mutation validations. 5 host tests. No `unsafe`. 190/190 host.

  2. `feat(kernel): T-017 commit 2 — alloc_frame / free_frame / stats + UNSAFE-2026-0026` (6500337)
    Runtime alloc / free / stats path. Frame-zeroing via `core::ptr::write_bytes` (the only `unsafe` in the PMM body — UNSAFE-2026-0026 lands as a new audit-log entry per ADR-0035 §Dependency chain step 5's adjudication-deferred verdict). 8 new host tests. 198/198 host + 13/13 Miri (Stacked Borrows clean).

  3. `feat(kernel): T-017 commit 3 — impl FrameProvider for Pmm` (fdb52d6)
    Trivial trait-impl forwarding to `Pmm::alloc_frame`. 1 new host test pinning the `&mut dyn FrameProvider` integration. 199/199 host + 14/14 Miri.

  4. `feat(bsp-qemu-virt): T-017 commit 4 — PMM wiring + boot banner + smoke` (a2ffcb6)
    `bsp-qemu-virt/src/main.rs`: PMM `StaticCell` + extern `__stack_top` linker symbol + `kernel_entry` wiring between mmu-activated print and GIC init. The kernel_entry order is now: hello → VBAR_EL1 install → boot_ns snapshot → mmu_bootstrap → "mmu activated" → PMM init → "pmm initialized (...)" → GIC init + DAIF unmask → timer banner → demo. Smoke verified.

  5. `docs: T-017 Done — PMM live; B3 §1 closed; ADR-0028 next` (6ed8a70)
    Status flips + cross-references: memory-management.md §"Frame allocation discipline" rewritten for post-T-017 reality; boot.md Stage 3 + sequence diagram extended; roadmap current.md + phase-b.md + tasks/phase-b/README.md flipped to Done; T-017 task review-history gains the closure row with the UNSAFE-2026-0026 adjudication trail.

Headline numbers

Metric Value Δ vs main
Host tests 199 / 199 +14 (all PMM)
PMM-specific Miri 14 / 14 clean NEW (PMM is new)
New unsafe audits UNSAFE-2026-0026 +1 (ADR-0035's "zero new" promise flips to "one new" per §Dependency chain step 5's adjudication-deferred caveat)
Boot trace new line `tyrne: pmm initialized (32604 frames available; 164 reserved)` +1 line
Reserved-frame breakdown 128 firmware + 36 kernel+stack = 164 frames
Free-frame count 32 604 (= 32 768 total − 164 reserved)
guest_errors window 443 pre-existing PL011 warnings (+64 ≈ bytes of new banner line) No new fault classes

§Simulation table verification

All 5 rows of ADR-0035 §Simulation are pinned by host tests:

Row State machine step Pinning test(s)
0 `Pmm::new` init + 3 fail-fast validations `new_marks_reserved_ranges_and_initialises_counters`, `new_rejects_too_many_reserved_ranges`, `new_rejects_unaligned_extent`, `new_rejects_reserved_range_outside_extent`
1 `alloc_frame` happy path + zero-fill `alloc_frame_returns_first_free_and_zeroes_payload`
2 (Critical) `free_frame` extent-bounds + reserved-range + bit checks `free_frame_clears_bit_and_rewinds_hint`, `free_frame_rejects_double_free_and_reserved`, `free_frame_reserved_check_iterates_only_populated_slots`, `free_frame_rejects_pa_outside_extent`
3 Exhaustion: `alloc_frame` returns `None` `alloc_frame_returns_none_when_exhausted`, `alloc_frame_recovers_after_free_under_exhaustion`
4 `Pmm::stats` invariants `stats_parity_with_bitmap_bit_count`

Plus `alloc_frame_implements_frame_provider` (trait-impl integration) and `extent_16f_fixture_sanity` (test fixture sanity check) bring the total to 14 PMM tests.

Audit-log adjudication

ADR-0035 §Dependency chain step 5 explicitly left the Amendment-vs-new-entry call to T-017's security review. The verdict (recorded in UNSAFE-2026-0026's §Introduced field): new entry, not an Amendment of UNSAFE-2026-0001. Reasoning walked in T-017's review-history closure row:

  • UNSAFE-2026-0001 scope: `Pl011Uart::new(PL011_UART_BASE)` — wraps the QEMU virt PL011's well-known MMIO base in a typed Console wrapper. Construction, not write.
  • PMM zero-fill scope: `core::ptr::write_bytes(pa_ptr, 0u8, PAGE_SIZE)` — writes 4 KiB of zeros to a PA derived from a just-allocated frame, with the bitmap's just-set bit as the exclusive-ownership proof. Write, not construction.

The two share surface shape ("kernel-static / identity-mapped raw-pointer") but differ on what they touch, what proves ownership, and what the failure mode is. Stretching the UNSAFE-2026-0001 umbrella over both would violate unsafe-policy.md §3's "same safety argument" requirement. The new entry's Rejected-alternatives section walks five alternatives (slice-from-raw-parts, lazy zero on page-fault, skip-zero, volatile_set_memory, new Mmu::zero_frame trait method) and explains why `write_bytes` is the honest choice.

ADR-0035 §Positive consequence "Zero new `unsafe` audits" flips to "one new entry" per the same caveat; ADR-0035 itself stays byte-stable.

Test plan

  • `cargo fmt --all -- --check` — clean
  • `cargo host-clippy` (`-D warnings`) — clean
  • `cargo kernel-clippy` (`-D warnings`) — clean
  • `cargo host-test` — 199 / 199 pass (was 185 pre-T-017)
  • `cargo +nightly miri test -p tyrne-kernel --lib mm::` — 14 / 14 clean
  • `cargo kernel-build` — clean
  • QEMU smoke — full demo through `tyrne: all tasks complete` with the new `tyrne: pmm initialized (...)` line in the expected position
  • `-d int,unimp,guest_errors` — only the pre-existing PL011 "data written to disabled UART" warning (+64 instances ≈ bytes of new banner line; no new fault classes)
  • Perf-harness re-baseline (release build, 20 iterations) — defer to next milestone closure-trio; this PR adds ~4 KiB `.bss` (PMM static) but is hot-path-free in v1 (PMM never called at runtime in cooperative demo)

What's deferred

Per T-017 §Out of scope:

  • Variable-size allocations (single-size only in v1)
  • Per-CPU caching / lock-free atomics (single-core)
  • NUMA awareness, hot-plug, swap (v1 has neither)
  • `MemoryRegionCap` capability-system integration (B5+)
  • `Pmm::release_reserved_range` recovery API
  • `Pmm::alloc_contiguous(n)` for multi-page DMA / huge-page work
  • Defragmentation (no fragmentation pressure in v1)

Next task: ADR-0028 (Address-space data structure) + T-018 (`AddressSpace` kernel object + capability-gated `Mmu::map` wrappers + activation-on-context-switch) per phase-b.md §B3 items 2–7.

Refs: ADR-0035, T-017
Audit: UNSAFE-2026-0026

Summary by Sourcery

Introduce a bitmap-based physical memory manager and wire it into the QEMU virt BSP boot sequence as the system-wide frame allocator foundation for future address-space work.

New Features:

  • Add a generic bitmap-based Physical Memory Manager (PMM) with reservation tracking and statistics, exposed via a new kernel mm subsystem.
  • Provide a concrete BSP PMM instance for QEMU virt over the 128 MiB RAM extent with firmware and kernel regions reserved, and publish it from kernel_entry.
  • Implement the FrameProvider trait for the PMM so it can back Mmu::map calls once higher-level address-space abstractions are introduced.

Enhancements:

  • Extend the boot sequence to initialise the PMM after MMU activation and log a new boot banner showing available and reserved frames.
  • Document the PMM design, boot-time integration, and unsafe audit rationale, and update roadmap and task status to mark T-017 and B3 §1 as complete.

Documentation:

  • Update memory-management and boot architecture docs to describe the live PMM, its API, and its place in the boot sequence and address-space stack.
  • Record a new UNSAFE-2026-0026 audit entry for PMM frame zeroing and update task/roadmap documents to reflect completion of the PMM bring-up task.

Tests:

  • Add a focused test suite for the PMM constructor, allocation/free semantics, stats invariants, FrameProvider integration, and Miri-backed validation of the unsafe zeroing path.

Summary by CodeRabbit

Release Notes

  • New Features

    • Implemented Physical Memory Manager with frame allocation and deallocation during kernel boot
    • Added automatic memory initialization with frame usage tracking and statistics
  • Documentation

    • Updated architecture and memory management documentation with implementation details
    • Updated project roadmap with task completion status

Review Change Stack

cemililik added 5 commits May 10, 2026 16:18
First of four bisectable commits for T-017 (B3 prep — Physical Memory
Manager per ADR-0035). Establishes the new `kernel/src/mm/` subsystem
parent + the `Pmm<N, R>` struct shape + the `Pmm::new` constructor
with three fail-fast pre-mutation validations. No `unsafe`. Tests
ride along as host-only `#[cfg(test)]` block.

What lands:

kernel/src/mm/mod.rs                                              [new]
  Memory-management subsystem parent. Hosts the PMM today; will
  host the AddressSpace data-structure module per ADR-0028
  placeholder (B3 §2) when T-018 lands.

  PhysFrameRange — half-open `[start, end)` range; carries raw
  PhysAddr values rather than PhysFrame to describe multi-page
  reservations in one entry. Includes is_aligned / len_bytes /
  frame_count / contains const-fn helpers. Used by Pmm::new for
  the BSP-provided extent + reservation list.

kernel/src/mm/pmm.rs                                              [new]
  PmmError enum (MisalignedAddress / OutOfRange /
  TooManyReservedRanges / DoubleFree); #[non_exhaustive] for
  forward-compat with future B5+ MemoryRegionCap variants.

  PmmStats struct — diagnostic snapshot for runtime self-checks +
  the boot-banner format. Counter fields share `_frames` postfix
  by design (disambiguates total/reserved/allocated/free at
  destructure sites); `clippy::struct_field_names` allowed with
  reason.

  Pmm<const N: usize, const R: usize> — bitmap allocator, where:
    - N = bitmap byte count (BSP-sized: 4096 for QEMU virt's
      32 K frames at 128 MiB / 4 KiB)
    - R = reserved-range cache capacity (BSP-sized: bsp-qemu-virt
      will pick R = 8 in commit 4 to cover its 3 v1 reservations
      plus headroom)

  Pmm::new(extent, reserved) -> Result<Self, PmmError>:
    Three fail-fast validations before any bitmap mutation:
      (i)   extent.start + extent.end page-aligned -> MisalignedAddress
      (ii)  every reserved range fits inside [extent) AND is
            page-aligned -> MisalignedAddress / OutOfRange
      (iii) reserved.len() <= R -> TooManyReservedRanges
    Plus an extent-vs-bitmap-size sanity check (extent.frame_count()
    must fit in N * 8 bits — BSP programming error surfaced as
    OutOfRange rather than silent buffer overflow).
    After all pass: walks reserved list, sets bitmap bits, copies
    list into reserved_ranges array (remainder None), computes
    counters, sets hint to first_zero_bit.

  Bitmap arithmetic (set_bit / read_bit / first_zero_bit) — pure
  safe Rust; #[deny(clippy::arithmetic_side_effects)] satisfied via
  saturating_add / saturating_sub / wrapping_div throughout.

  Pmm::extent + Pmm::stats inherent accessors. The struct's other
  fields (bitmap / reserved_ranges / hint) carry an explicit
  `#[allow(dead_code, reason = "transient until commit 2 lands
  alloc_frame / free_frame / stats")]` annotation; commit 2
  removes the allow when those methods are added.

5 host tests (all in kernel/src/mm/pmm.rs::tests under
#[cfg(test)] + #[allow(clippy::{arithmetic_side_effects, unwrap_used,
expect_used, panic})] per the existing kernel-test convention):

  - new_marks_reserved_ranges_and_initialises_counters
  - new_rejects_too_many_reserved_ranges
  - new_rejects_unaligned_extent (start + end + reserved-range
    bound variants)
  - new_rejects_reserved_range_outside_extent (entirely-above /
    partially-exceeding / below variants)
  - extent_16f_fixture_sanity (sanity-check the test fixture
    itself; pins the fixture's frame_count + is_aligned + contains
    invariants for use by future commits' tests)

The 5 tests pin every error path in Pmm::new + the happy path's
counter+hint+reserved_ranges initialisation per ADR-0035
§Simulation §Step 0.

kernel/src/lib.rs
  Adds `pub mod mm;` to the top-level module declaration list.
  Module slot reserved between `ipc` and `obj` alphabetically.

Verification:
  - cargo fmt --check           clean
  - cargo clippy (host)         clean -D warnings
  - cargo kernel-clippy         clean -D warnings
  - cargo test                  190/190 host (was 185; +5 PMM
                                construction tests)
  - cargo kernel-build          clean

The four-commit chain proceeds:
  - commit 2: alloc_frame / free_frame / stats + frame-zeroing
              unsafe + 8 more host tests + audit-log entry
  - commit 3: impl FrameProvider for Pmm + 1 test
  - commit 4: bsp-qemu-virt PMM publication + smoke verification

Refs: ADR-0035, T-017
…AFE-2026-0026

Second of four bisectable commits for T-017. Adds the runtime PMM
alloc / free path with the frame-zeroing unsafe block, plus a new
audit-log entry per ADR-0035 §Dependency-chain step 5's
adjudication-deferred verdict (PMM frame-zeroing is semantically
distant from UNSAFE-2026-0001's PL011 MMIO base blessing).

What lands:

kernel/src/mm/pmm.rs
  - Pmm::alloc_frame() -> Option<PhysFrame>:
      Forward-from-hint linear bitmap scan for the first 0 bit;
      on hit sets the bit, advances hint, decrements free_count,
      increments allocated_count, zero-fills the 4 KiB frame
      contents via core::ptr::write_bytes, returns Some(frame).
      None on bitmap fully set. The wrap-then-scan-prefix step is
      retained for forward-compat with SMP (per ADR-0035 §Simulation
      §Step 3 forward-compat note); structurally unreachable in v1's
      single-core cooperative discipline.
  - Pmm::free_frame(frame) -> Result<(), PmmError>:
      Three-stage validation in order — extent-bounds (returns
      OutOfRange if PA outside [extent.start, extent.end)),
      defensive reserved-range scan over Some(_) slots only
      (returns DoubleFree if PA falls in any cached reserved
      range), bitmap-bit check (returns DoubleFree if already
      Free). On all-pass: clears bit, rewinds hint = min(hint, idx),
      counters updated. Each error path leaves bitmap + counter
      state byte-stable.
  - clear_bit private helper (companion to set_bit / read_bit).
  - PhysFrame import from tyrne_hal.
  - Removed the commit-1 #[allow(dead_code)] on the Pmm struct
    (bitmap / reserved_ranges / hint are now read by alloc / free).

8 new host tests (kernel/src/mm/pmm.rs::tests):
  - alloc_frame_returns_first_free_and_zeroes_payload — uses
    a host-allocated PAGE_SIZE-aligned Vec<u8> as backing storage,
    pre-poisons it with 0xA5, allocs, asserts every byte of the
    returned frame is 0u8 (pins the FrameProvider zero-init
    contract).
  - free_frame_clears_bit_and_rewinds_hint — alloc x3, free f1,
    asserts hint rewinds to 0 and the next alloc returns f1
    again.
  - free_frame_rejects_double_free_and_reserved — pins the
    DoubleFree path for both Reserved-range frames and never-
    allocated frames (bitmap unchanged on rejection).
  - alloc_frame_returns_none_when_exhausted — pins §Simulation
    §Step 3.
  - alloc_frame_recovers_after_free_under_exhaustion — pins
    that the rewind discipline reclaims a freed frame even
    after exhaustion.
  - stats_parity_with_bitmap_bit_count — pins the cached-
    counter ↔ bitmap-popcount invariant.
  - free_frame_reserved_check_iterates_only_populated_slots —
    pins the Critical-row defensive scan's contract that
    populated Some(_) slots are iterated and None slots are
    skipped (regression guard against accidental Wildcard
    interpretation of None).
  - free_frame_rejects_pa_outside_extent — pins the §Step 2
    extent-bounds fail-fast for PAs below extent.start and at /
    above extent.end (counters unchanged).

Total kernel test count rises 100 → 113 (+13 PMM tests so far;
commits 3 + 4 won't add more). Workspace total 190 → 198.

docs/audits/unsafe-log.md
  - **UNSAFE-2026-0026** introduced (NEW ENTRY, not Amendment):
    PMM frame-zeroing via core::ptr::write_bytes. ADR-0035
    §Dependency-chain step 5 explicitly left this call to T-017's
    security-review verdict; the verdict (recorded in the entry's
    Introduced field) is that UNSAFE-2026-0001's original scope
    (PL011 MMIO base address blessing in a typed Console wrapper) is
    semantically distant enough from PMM RAM zero-fill that an
    Amendment would stretch the umbrella beyond unsafe-policy.md
    §3's "same safety argument" requirement. The two operations
    differ on what they touch (typed MMIO base vs. RAM zero),
    what proves ownership (well-known QEMU virt PL011 base vs.
    just-set bitmap bit), and what the failure mode is (silent
    firmware confusion vs. bitmap-or-extent corruption). The new
    entry follows the Operation / Invariants / Rejected-alternatives
    shape per unsafe-policy.md §3.
  - Five Invariants explicitly named:
      1. Page-alignment (Pmm::new validation (i) propagates).
      2. Exclusive ownership at write time (just-set bitmap bit).
      3. Identity mapping post-MMU (ADR-0027 §Decision outcome (a)).
      4. Bitmap math overflow-free (saturating_*, wrapping_div).
      5. write_bytes ordering (single-core; no peer reader).
  - Five Rejected alternatives walked: slice-from-raw-parts-mut
    (same unsafe, different syntax); lazy zero on page-fault
    (no v1 page-fault routing); skip zero (FrameProvider contract
    + B5+ isolation hazard); volatile_set_memory (no peer
    observer); new Mmu::zero_frame trait method (relocates audit
    point, doesn't remove it).
  - Status: Active; v1 site is reached on every boot (PMM
    constructed in commit 4) but the zero-fill itself is
    unexercised in v1's demo — host-test coverage + Miri Stacked
    Borrows clean is the v1 evidence base. ADR-0035's "zero new
    audit entries" §Positive consequence flips to "one new entry"
    per the adjudication caveat; ADR-0035 itself stays unchanged.

Verification:
  - cargo fmt --check                clean
  - cargo clippy (host)              clean -D warnings
  - cargo kernel-clippy              clean -D warnings
  - cargo test                       198/198 host (was 190; +8 PMM
                                     alloc/free/stats tests)
  - cargo +nightly miri test         13/13 PMM tests clean (Stacked
                                     Borrows + raw-pointer discipline)
  - cargo kernel-build               clean

Refs: ADR-0035, T-017
Audit: UNSAFE-2026-0026
Third of four bisectable commits for T-017. Layers the
tyrne_hal::FrameProvider trait impl on top of Pmm::alloc_frame so
the PMM can be passed directly to Mmu::map as &mut dyn FrameProvider.
Trivial — the impl forwards to the inherent method. Pure safe Rust;
no audit-log change.

What lands:

kernel/src/mm/pmm.rs
  - FrameProvider import added (`tyrne_hal::FrameProvider`).
  - `impl<const N: usize, const R: usize> FrameProvider for Pmm<N, R>`
    block: single fn alloc_frame(&mut self) -> Option<PhysFrame>
    that delegates to Pmm::alloc_frame. Doc-comment names the
    Mmu::map contract (None propagates as MmuError::OutOfFrames per
    ADR-0009 + ADR-0035 §Decision drivers).
  - 1 new host test: alloc_frame_implements_frame_provider —
    exercises the trait method via &mut dyn FrameProvider so the
    impl integration is pinned (regression guard against accidental
    signature drift between Pmm::alloc_frame and
    FrameProvider::alloc_frame). Allocates 4 frames + asserts the
    5th is None.

Workspace test count rises 198 → 199.

Verification:
  - cargo fmt --check                clean
  - cargo clippy (host)              clean -D warnings
  - cargo kernel-clippy              clean -D warnings
  - cargo test                       199/199 host (was 198; +1
                                     FrameProvider impl test)
  - cargo kernel-build               clean

Refs: ADR-0035, T-017
Final of four bisectable commits for T-017. Wires the PMM into the
QEMU virt BSP boot sequence — instantiates `Pmm<4096, 8>` over the
128 MiB physical-RAM extent, marks two reserved regions (QEMU
firmware + kernel image / `.bss` / `.boot_pt` / boot stack), and
prints the `tyrne: pmm initialized (...)` banner immediately after
`tyrne: mmu activated` and before GIC init. v1's cooperative IPC
demo never calls `alloc_frame` so the PMM stays unused at runtime;
init success + counter values are the v1 smoke evidence.

What lands:

bsp-qemu-virt/src/main.rs (constants + types):
  - PMM_EXTENT_START = 0x4000_0000 (QEMU virt RAM origin per
    ADR-0012).
  - PMM_EXTENT_END = 0x4800_0000 (128 MiB above).
  - KERNEL_IMAGE_START = 0x4008_0000 (linker.ld -kernel load
    address; the 512 KiB below is QEMU-firmware-reserved).
  - PMM_BITMAP_BYTES = (extent / PAGE_SIZE / 8) = 4096 bytes for
    32 768 frames.
  - PMM_RESERVED_RANGES = 8 (v1 uses 2; headroom for future BSP
    layouts with DTB / ATF / ACPI / initrd / framebuffer
    reservations).
  - type BspPmm = Pmm<PMM_BITMAP_BYTES, PMM_RESERVED_RANGES> —
    per-BSP-const-generic discipline of ADR-0035; future BSPs
    instantiate their own (Pi 4 with 4 GiB → N = 131072, etc.).

bsp-qemu-virt/src/main.rs (linker symbols):
  - Added extern "C" { static __stack_top: u8; }. linker.ld
    aligns this to 16 bytes (post-`ALIGN(16); . = . + 64K`); the
    BSP rounds it up to 4 KiB at PMM-init time via
    `addr.saturating_add(PAGE_SIZE - 1) & !(PAGE_SIZE - 1)`. The
    few-byte slack falls into the kernel-reserved range so the
    round-up cannot collide with a valid runtime allocation.

bsp-qemu-virt/src/main.rs (static PMM cell):
  - static PMM: StaticCell<BspPmm> = StaticCell::new();
    Published before any post-bootstrap Mmu::map caller could
    use it (none in v1; first caller is B3+ AddressSpace
    bring-up via T-018). Sized ~4.1 KiB of .bss per ADR-0035
    §Consequences §Bounded metadata: 4 KiB bitmap + 8 ×
    Option<PhysFrameRange> reserved-range cache (~144 bytes
    with alignment) + 5 × usize counters.

bsp-qemu-virt/src/main.rs (kernel_entry wiring):
  - Insert PMM init between "tyrne: mmu activated" print and
    GIC init. Three steps:
    1. Compute rounded-up stack-top from the __stack_top linker
       symbol.
    2. Build the two reserved-range entries: firmware region
       [0x4000_0000, 0x4008_0000) + kernel-everything region
       [0x4008_0000, __stack_top_aligned_up).
    3. BspPmm::new(extent, &reserved).expect("...") +
       StaticCell::write. The .expect() is structurally
       unreachable in v1 (the static reservation list is
       well-formed at compile time; R=8 is much greater than the
       2-range count); the message documents the
       kernel-discipline contract per CLAUDE.md non-negotiable
       rules.
    4. Print "tyrne: pmm initialized (N frames available; M
       reserved)" via FmtWriter — the only new UART line in
       v1's smoke trace.

The kernel_entry full order is now:
  hello → VBAR_EL1 install → boot_ns snapshot → mmu_bootstrap
  → "tyrne: mmu activated" → PMM init → "tyrne: pmm initialized
  (N available; M reserved)" → GIC init + DAIF unmask → timer
  banner → kernel-object setup → IPC + scheduler → start().

Verification:

  - cargo fmt --check                clean
  - cargo clippy (host)              clean -D warnings
  - cargo kernel-clippy              clean -D warnings
  - cargo test                       199/199 host (unchanged —
                                     BSP wiring touches no test
                                     code; the 14 PMM tests
                                     landed in commits 1+2+3)
  - cargo +nightly miri test         14/14 PMM tests clean
  - cargo kernel-build               clean
  - QEMU smoke trace                 (verbatim):
      tyrne: hello from kernel_main
      tyrne: mmu activated
      tyrne: pmm initialized (32604 frames available; 164 reserved)
      tyrne: timer ready (62500000 Hz, resolution 16 ns)
      tyrne: starting cooperative scheduler
      tyrne: task B — waiting for IPC
      tyrne: task A -- sending IPC
      tyrne: task B — received IPC (label=0xaaaa); replying
      tyrne: task A — received reply (label=0xbbbb); done
      tyrne: all tasks complete
      tyrne: boot-to-end elapsed = 7430992 ns

    Total frames 32768 = free 32604 + reserved 164 ✓ (sanity:
    164 frames × 4 KiB = 656 KiB of reservation = 128 frames /
    512 KiB firmware + 36 frames / 144 KiB kernel image + .bss
    + .boot_pt 16 KiB + 64 KiB stack + alignment slack).

  - QEMU `-d int,unimp,guest_errors`:
      443 instances of pre-existing "PL011 data written to
      disabled UART" warning (was 379 post-T-016; +64 = bytes
      of the new "tyrne: pmm initialized (...)" line). No new
      fault classes; no Translation Faults; no Permission
      Faults; no Unallocated Instructions.

T-017 implementation chain complete. Remaining work (separate
commit on this branch): docs updates (memory-management.md,
boot.md, roadmap banners, T-017 status flip In Progress → Done).

Refs: ADR-0035, T-017
Audit: UNSAFE-2026-0001
Documentation polish + status flips after the four-commit T-017
implementation chain (2533732 / 6500337 / fdb52d6 / a2ffcb6).
No code change; gates remain at 199/199 host + 14/14 PMM-miri +
clippy / fmt / kernel-build clean; smoke trace stable.

Updates:

- docs/analysis/tasks/phase-b/T-017-physical-memory-manager.md
    Status flipped `In Progress → Done`. Review-history gains
    three new rows: 2026-05-09 ADR-0035-Accepted +
    Draft-to-InProgress flip; 2026-05-10 In-Progress-to-Done
    flip with headline numbers + the UNSAFE-2026-0026
    new-entry adjudication trail (per ADR-0035 §Dependency
    chain step 5's deferred verdict — PL011 MMIO base
    blessing vs. PMM RAM zero-fill semantic distance forced a
    fresh entry rather than an Amendment to UNSAFE-2026-0001).

- docs/architecture/memory-management.md
    §"Frame allocation discipline" rewritten from the "PMM is
    not part of T-016" forward-looking framing to the post-T-017
    reality. New content: bitmap allocator design recap (N
    bitmap bytes + R reserved-range cache capacity as per-BSP
    const generics per ADR-0035); API contract (Pmm::new +
    alloc_frame + free_frame + extent + stats); FrameProvider
    integration (one v1 caller — none in runtime; first runtime
    exercise B3+); the v1 smoke-trace banner with the 32 604 +
    164 = 32 768 frame sanity-check; audit-log surface
    (UNSAFE-2026-0026's five invariants); forward-portability
    note (bitmap regime-independent; frame-zeroing needs a
    phys_to_virt helper post-high-half).

- docs/architecture/boot.md
    Stage 3 description extended with the PMM-init step; the
    Mermaid sequence diagram refreshed with the new
    `Pmm::new` + `tyrne: pmm initialized (...)` arrows. No
    other diagram or invariant changes.

- docs/roadmap/current.md
    Active-milestone line updated: B3 §1 closed 2026-05-10;
    next is ADR-0028 + T-018. Active-task line: none — T-017
    moved to Done. Headline numbers + UNSAFE-2026-0026
    adjudication summary inline.

- docs/roadmap/phases/phase-b.md
    B3 §Status block flipped to "B3 §1 closed 2026-05-10".
    Tasks-under-B3 row for T-017 updated to "Done (2026-05-10;
    4 bisectable commits on branch t-017-physical-memory-manager;
    UNSAFE-2026-0026 introduced as new entry)".

- docs/analysis/tasks/phase-b/README.md
    T-017 row flipped to "Done (2026-05-10)".

ADR-0035 itself stays byte-stable post-T-017 per its §Dependency
chain step 5's adjudication-deferred contract — the "zero new
audit entries" → "one new entry" promotion is recorded in
T-017's review-history + this commit body + UNSAFE-2026-0026's
own §Introduced field. No retroactive ADR edit needed.

Verification:
  - cargo fmt --check               clean
  - cargo clippy (host)             clean -D warnings
  - cargo kernel-clippy             clean -D warnings
  - cargo test                      199/199 host (unchanged —
                                    docs only)
  - cargo kernel-build              clean
  - QEMU smoke                      full demo through "tyrne:
                                    all tasks complete"; the
                                    "tyrne: pmm initialized
                                    (32604 frames available;
                                    164 reserved)" line in
                                    the expected position

T-017 arc complete. Branch is mergeable; PR opens next.

Refs: ADR-0035, T-017
@qodo-code-review

Copy link
Copy Markdown
ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

@sourcery-ai

sourcery-ai Bot commented May 10, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces a bitmap-based Physical Memory Manager (PMM) in the kernel, wires it into the QEMU-virt BSP boot path as the concrete FrameProvider implementation over the 128 MiB RAM extent with reservation tracking, adds targeted tests and a new unsafe audit entry for frame zeroing, and updates documentation/roadmap to reflect T-017 completion and PMM behaviour.

Sequence diagram for kernel_entry boot with PMM initialization

sequenceDiagram
    actor FW as Firmware
    participant ST as StartStub
    participant KE as kernel_entry
    participant MMU as mmu_bootstrap
    participant PMM as Pmm
    participant GIC as GicInit
    participant U as Console

    FW->>ST: jump to _start (EL1/EL2)
    ST->>ST: setup stack, zero BSS
    ST->>KE: call kernel_entry()

    KE->>U: write_bytes("tyrne: hello ...")
    KE->>KE: install EL1 vector table
    KE->>KE: boot_ns = cpu.now_ns()

    KE->>MMU: mmu_bootstrap()
    MMU-->>KE: MMU activated (identity map)
    KE->>U: write_bytes("tyrne: mmu activated\n")

    KE->>KE: compute stack_top_aligned_up
    KE->>KE: build PhysFrameRange extent
    KE->>KE: build reserved[0], reserved[1]
    KE->>PMM: Pmm::new(extent, &reserved)
    PMM-->>KE: Pmm instance

    KE->>PMM: stats()
    PMM-->>KE: PmmStats
    KE->>U: write_bytes("tyrne: pmm initialized (... )\n")

    KE->>GIC: init GIC + unmask DAIF.I
    GIC-->>KE: ready
    KE->>U: write_bytes("tyrne: timer ready (...)\n")
    KE->>KE: setup kernel objects, IPC, scheduler
Loading

Updated class diagram for the Physical Memory Manager and related types

classDiagram
    class PmmError {
      <<enum>>
      MisalignedAddress
      OutOfRange
      TooManyReservedRanges
      DoubleFree
    }

    class PmmStats {
      +usize total_frames
      +usize reserved_frames
      +usize allocated_frames
      +usize free_frames
    }

    class PhysFrameRange {
      +PhysAddr start
      +PhysAddr end
      +new(start: PhysAddr, end: PhysAddr) PhysFrameRange
      +is_aligned() bool
      +len_bytes() usize
      +frame_count() usize
      +contains(pa: PhysAddr) bool
    }

    class FrameProvider {
      <<trait>>
      +alloc_frame() PhysFrame
    }

    class Pmm_N_R {
      <<Pmm<N,R>>
      -u8[N] bitmap
      -PhysFrameRange extent
      -PhysFrameRange[R] reserved_ranges
      -usize hint
      -usize free_count
      -usize reserved_count
      -usize allocated_count
      +new(extent: PhysFrameRange, reserved: PhysFrameRange[]) Result~Pmm_N_R, PmmError~
      +extent() PhysFrameRange
      +stats() PmmStats
      +alloc_frame() PhysFrame
      +free_frame(frame: PhysFrame) Result~void, PmmError~
    }

    class PhysAddr
    class PhysFrame

    PhysFrameRange --> PhysAddr : uses
    Pmm_N_R --> PhysFrameRange : manages
    Pmm_N_R --> PmmStats : returns
    Pmm_N_R --> PmmError : errors
    Pmm_N_R --> PhysFrame : allocates
    FrameProvider <|.. Pmm_N_R : implements
Loading

File-Level Changes

Change Details Files
Add generic bitmap-based Physical Memory Manager with reservation tracking and FrameProvider implementation.
  • Introduce kernel::mm module with PhysFrameRange helper type and re-exported PMM types.
  • Implement Pmm<N,R> bitmap allocator over a configurable physical extent, including constructor validation, alloc/free logic with hint-based scanning, stats counters, and internal bitmap helpers.
  • Implement FrameProvider for Pmm and comprehensive host tests plus Miri coverage for constructor, allocation, freeing, and stats invariants.
kernel/src/lib.rs
kernel/src/mm/mod.rs
kernel/src/mm/pmm.rs
Wire PMM into QEMU-virt BSP boot sequence over 128 MiB RAM with reserved firmware and kernel regions and boot-time stats banner.
  • Define BSP-local constants for PMM-managed RAM extent, kernel image start, bitmap size, and reserved-range capacity, with a BspPmm type alias.
  • Add a StaticCell-backed global PMM instance, obtain __stack_top from the linker, compute an aligned kernel-reserved range, and construct the Pmm in kernel_entry after mmu_bootstrap.
  • Publish a boot banner using Pmm::stats and ensure PMM is accessible as a &mut dyn FrameProvider for future Mmu::map callers.
bsp-qemu-virt/src/main.rs
Document PMM design, boot integration, and unsafe audit entry; mark T-017 and B3 §1 as Done in roadmap files.
  • Update memory-management architecture chapter to describe the live PMM, its API (new/alloc_frame/free_frame/stats), FrameProvider integration, and smoke trace line.
  • Add UNSAFE-2026-0026 audit-log entry describing the frame-zeroing write_bytes site, invariants, and rejected alternatives.
  • Flip T-017 status to Done and adjust current/phase-b roadmap files and task README to reflect B3 §1 closure and next steps (ADR-0028/T-018).
  • Extend boot.md to include PMM init in the kernel_entry description and sequence diagram.
docs/architecture/memory-management.md
docs/audits/unsafe-log.md
docs/analysis/tasks/phase-b/T-017-physical-memory-manager.md
docs/architecture/boot.md
docs/roadmap/current.md
docs/roadmap/phases/phase-b.md
docs/analysis/tasks/phase-b/README.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 May 10, 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 33 minutes and 38 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ 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: dc77c4da-12f8-4155-b124-543aa9dede2d

📥 Commits

Reviewing files that changed from the base of the PR and between 6ed8a70 and b4633ae.

📒 Files selected for processing (6)
  • bsp-qemu-virt/src/main.rs
  • docs/analysis/tasks/phase-b/T-017-physical-memory-manager.md
  • docs/architecture/memory-management.md
  • docs/audits/unsafe-log.md
  • docs/roadmap/current.md
  • kernel/src/mm/pmm.rs
📝 Walkthrough

Walkthrough

This PR implements a bitmap-based Physical Memory Manager (PMM) for the kernel with frame allocation, deallocation, and reserved-range tracking. The PMM is initialized during kernel boot after MMU activation, wired into the BSP, exported from the kernel crate, and documented with comprehensive unsafe audits and task tracking.

Changes

Physical Memory Manager (PMM) Implementation

Layer / File(s) Summary
Data Contracts
kernel/src/mm/mod.rs
PhysFrameRange type with start/end bounds and helpers for alignment checks, byte length, frame counting, and membership tests. PmmError enum with variants for misaligned addresses, out-of-range extents, too-many reserved ranges, and double-free detection. PmmStats struct with total, reserved, allocated, and free frame counters. Pmm<const N, const R> struct holding bitmap, extent, hint pointer, reserved ranges, and cached statistics.
PMM Core Implementation
kernel/src/mm/pmm.rs
Pmm::new validates extent/reserved alignment and capacity, initializes bitmap marking reserved frames, computes initial hint via first-zero-bit search, and caches counters. alloc_frame scans bitmap from hint with wrap-around fallback, marks frame allocated, zero-initializes via unsafe write_bytes, updates counters, returns PhysFrame. free_frame validates extent membership, rejects reserved and already-free frames via bitmap/reserved-range scans, clears bitmap bit, rewinds hint when beneficial, updates counters. FrameProvider trait impl delegates to alloc_frame. Private bitmap helpers for bit operations.
Test Suite
kernel/src/mm/pmm.rs
Host tests validate Pmm::new rejection of misaligned extents and reserved bounds, reserved-list capacity overflow, and unaligned reserved ranges. Tests confirm bitmap initialization marks reserved frames correctly, sets proper initial hint, maintains counter parity, handles allocation exhaustion and hint rewinding, and rejects double-free attempts.
Kernel Module Setup
kernel/src/lib.rs, kernel/src/mm/mod.rs
Kernel crate publicly exports new mm subsystem module. mm/mod.rs declares pmm submodule, imports PhysAddr and PAGE_SIZE from HAL, and re-exports Pmm, PmmError, PmmStats for public use.
BSP Integration & Initialization
bsp-qemu-virt/src/main.rs
QEMU virt BSP defines RAM extent constants (128 MiB), kernel image load address, bitmap size, and reserved-range capacity. Declares extern __stack_top linker symbol. Creates global PMM static via StaticCell<BspPmm>. During kernel_entry after MMU activation: reads __stack_top, aligns stack boundary to 4 KiB, computes two reserved ranges (firmware + kernel/bss/stack), constructs BspPmm via Pmm::new, writes to PMM static, prints frame statistics (total, reserved, free).
Documentation & Task Tracking
docs/analysis/tasks/phase-b/*, docs/architecture/*, docs/audits/unsafe-log.md, docs/roadmap/*
Task T-017 status changed from Draft to Done with dated review history. boot.md expanded with kernel_entry PMM initialization narrative and sequence-diagram step. memory-management.md replaces "PMM not implemented" placeholder with live PMM section covering location, bitmap-hint allocator design, reserved-range initialization, full API surface (constructor, allocation, deallocation, accessors, FrameProvider impl), runtime Mmu::map integration, boot-trace output, frame-count sanity check, and reserved-frame decomposition. unsafe-log.md adds UNSAFE-2026-0026 documenting write_bytes frame zeroing with required invariants (page alignment, exclusive ownership, identity mapping post-MMU, overflow-free bitmap math) and rejected alternatives. Roadmaps updated: B3 §1 marked closed with T-017 completion; active task switched to ADR-0028/T-018 follow-up.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • cemililik/Tyrne#23: Adds MMU bootstrap and activation in kernel_entry; T-017 PMM initialization is sequenced after this MMU activation point.
  • cemililik/Tyrne#15: Refactors BSP kernel_entry post-start handling; both PRs modify kernel_entry static and symbol management patterns.
  • cemililik/Tyrne#6: Adds scheduler StaticCell initialization in kernel_entry; both PRs extend kernel_entry boot sequence with new subsystem initialization statics.

🐰 A bitmap stands tall and true,
Four kilobytes get zeroed anew,
Reserved ranges kept apart,
Frame allocation's beating heart,
Boot-time freedom, fresh and free!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and specifically summarizes the main change: implementing the Physical Memory Manager (PMM) with bitmap allocator and reservation tracking for T-017, which is precisely what the changeset accomplishes.
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 t-017-physical-memory-manager

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.

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

  • In Pmm::alloc_frame, if PhysFrame::from_aligned ever returned None you would already have mutated the bitmap and counters; consider either constructing the PhysFrame before updating state or at least enforcing the invariant with an explicit debug_assert!/expect so a violation can’t silently desynchronise internal bookkeeping.
  • The PMM StaticCell in kernel_entry is manipulated via repeated unsafe { (*PMM.0.get())... } blocks; it would be cleaner and less error‑prone to wrap this pattern in a small helper on StaticCell (or a BSP-local wrapper) so future uses don’t need to reach into its internals or duplicate the raw‑pointer logic.
  • In the test fixture extent_16f, the doc comment claims "16 frames (16 KiB)" but the range 0x4000_0000..0x4000_4000 is 16 KiB = 4 frames and the test asserts frame_count() == 4; updating the comment to match the actual size will avoid confusion when reading these tests.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `Pmm::alloc_frame`, if `PhysFrame::from_aligned` ever returned `None` you would already have mutated the bitmap and counters; consider either constructing the `PhysFrame` before updating state or at least enforcing the invariant with an explicit `debug_assert!/expect` so a violation can’t silently desynchronise internal bookkeeping.
- The `PMM` `StaticCell` in `kernel_entry` is manipulated via repeated `unsafe { (*PMM.0.get())... }` blocks; it would be cleaner and less error‑prone to wrap this pattern in a small helper on `StaticCell` (or a BSP-local wrapper) so future uses don’t need to reach into its internals or duplicate the raw‑pointer logic.
- In the test fixture `extent_16f`, the doc comment claims "16 frames (16 KiB)" but the range `0x4000_0000..0x4000_4000` is 16 KiB = 4 frames and the test asserts `frame_count() == 4`; updating the comment to match the actual size will avoid confusion when reading these tests.

## Individual Comments

### Comment 1
<location path="kernel/src/mm/pmm.rs" line_range="201-210" />
<code_context>
+        let mut reserved_count: usize = 0;
</code_context>
<issue_to_address>
**issue (bug_risk):** Overlapping reserved ranges double-count frames and can make stats inconsistent with the bitmap/extents.

This relies on reserved ranges being non-overlapping. If a BSP passes overlapping or off‑by‑one-touching ranges, the bitmap remains correct but `reserved_count` will be inflated and can even exceed `total_frames`, making `stats()` inconsistent (`total_frames` > `extent.frame_count()`, `free_frames` clamped from a negative value). Either validate and reject overlapping ranges in `Pmm::new`, or derive `reserved_count` from the bitmap after applying all ranges (e.g., by counting set bits once).
</issue_to_address>

### Comment 2
<location path="kernel/src/mm/pmm.rs" line_range="246-259" />
<code_context>
+    /// Return a diagnostic snapshot of the PMM's frame-state
+    /// counters.
+    #[must_use]
+    pub fn stats(&self) -> PmmStats {
+        PmmStats {
+            total_frames: self
+                .free_count
+                .saturating_add(self.reserved_count)
+                .saturating_add(self.allocated_count),
+            reserved_frames: self.reserved_count,
+            allocated_frames: self.allocated_count,
+            free_frames: self.free_count,
+        }
+    }
</code_context>
<issue_to_address>
**suggestion:** Deriving `total_frames` from cached counters risks divergence from the managed extent.

Currently `stats().total_frames` is derived from `free_count + reserved_count + allocated_count`, which can diverge from `extent.frame_count()` if any counter drifts from the bitmap/extent. Using the extent as the single source of truth for `total_frames` avoids this class of inconsistency:

```rust
pub fn stats(&self) -> PmmStats {
    PmmStats {
        total_frames: self.extent.frame_count(),
        reserved_frames: self.reserved_count,
        allocated_frames: self.allocated_count,
        free_frames: self.free_count,
    }
}
```

You can still detect counter drift by asserting `free + reserved + allocated == extent.frame_count()` in tests or debug builds, while callers always see a consistent `total_frames`.

```suggestion
    /// Return a diagnostic snapshot of the PMM's frame-state
    /// counters.
    #[must_use]
    pub fn stats(&self) -> PmmStats {
        PmmStats {
            total_frames: self.extent.frame_count(),
            reserved_frames: self.reserved_count,
            allocated_frames: self.allocated_count,
            free_frames: self.free_count,
        }
    }
```
</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/mm/pmm.rs
Comment on lines +201 to +210
let mut reserved_count: usize = 0;
for (idx, range) in reserved.iter().enumerate() {
// Mark every covered frame as Reserved.
//
// Saturating sub keeps `clippy::arithmetic_side_effects`
// happy; validation (ii) above guarantees
// `range.start >= extent.start` so the saturation never
// truncates in well-formed input.
let start_idx = range
.start

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Overlapping reserved ranges double-count frames and can make stats inconsistent with the bitmap/extents.

This relies on reserved ranges being non-overlapping. If a BSP passes overlapping or off‑by‑one-touching ranges, the bitmap remains correct but reserved_count will be inflated and can even exceed total_frames, making stats() inconsistent (total_frames > extent.frame_count(), free_frames clamped from a negative value). Either validate and reject overlapping ranges in Pmm::new, or derive reserved_count from the bitmap after applying all ranges (e.g., by counting set bits once).

Comment thread kernel/src/mm/pmm.rs
Comment on lines +246 to +259
/// Return a diagnostic snapshot of the PMM's frame-state
/// counters.
#[must_use]
pub fn stats(&self) -> PmmStats {
PmmStats {
total_frames: self
.free_count
.saturating_add(self.reserved_count)
.saturating_add(self.allocated_count),
reserved_frames: self.reserved_count,
allocated_frames: self.allocated_count,
free_frames: self.free_count,
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Deriving total_frames from cached counters risks divergence from the managed extent.

Currently stats().total_frames is derived from free_count + reserved_count + allocated_count, which can diverge from extent.frame_count() if any counter drifts from the bitmap/extent. Using the extent as the single source of truth for total_frames avoids this class of inconsistency:

pub fn stats(&self) -> PmmStats {
    PmmStats {
        total_frames: self.extent.frame_count(),
        reserved_frames: self.reserved_count,
        allocated_frames: self.allocated_count,
        free_frames: self.free_count,
    }
}

You can still detect counter drift by asserting free + reserved + allocated == extent.frame_count() in tests or debug builds, while callers always see a consistent total_frames.

Suggested change
/// Return a diagnostic snapshot of the PMM's frame-state
/// counters.
#[must_use]
pub fn stats(&self) -> PmmStats {
PmmStats {
total_frames: self
.free_count
.saturating_add(self.reserved_count)
.saturating_add(self.allocated_count),
reserved_frames: self.reserved_count,
allocated_frames: self.allocated_count,
free_frames: self.free_count,
}
}
/// Return a diagnostic snapshot of the PMM's frame-state
/// counters.
#[must_use]
pub fn stats(&self) -> PmmStats {
PmmStats {
total_frames: self.extent.frame_count(),
reserved_frames: self.reserved_count,
allocated_frames: self.allocated_count,
free_frames: self.free_count,
}
}

@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 the Physical Memory Manager (PMM) as a bitmap-based allocator, fulfilling the requirements of ADR-0035. The implementation includes the Pmm struct in the kernel crate, which handles frame allocation, deallocation, and zero-initialization, and is integrated into the bsp-qemu-virt boot sequence. Review feedback identifies a potential bug where overlapping reserved ranges could lead to incorrect frame counts, concerns regarding stack overflow when initializing large bitmaps, and the architectural debt introduced by assuming identity mapping when zeroing frames.

Comment thread kernel/src/mm/pmm.rs
Comment on lines +215 to +219
for off in 0..frames_in_range {
let frame_idx = start_idx.saturating_add(off);
set_bit(&mut bitmap, frame_idx);
}
reserved_count = reserved_count.saturating_add(frames_in_range);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The reserved_count is calculated by summing the lengths of all provided reserved ranges. If any of these ranges overlap, the same frames will be counted multiple times, leading to an incorrect reserved_count and consequently an incorrect free_count. While the bitmap itself handles overlaps correctly (bits are set idempotently), the cached counters will become inconsistent with the actual state of the bitmap. This will result in incorrect values being reported by stats() and may cause free_count to saturate prematurely. Consider calculating the count based on whether a bit was actually flipped.

            for off in 0..frames_in_range {
                let frame_idx = start_idx.saturating_add(off);
                if !read_bit(&bitmap, frame_idx) {
                    set_bit(&mut bitmap, frame_idx);
                    reserved_count = reserved_count.saturating_add(1);
                }
            }

Comment thread kernel/src/mm/pmm.rs
}

// All validations passed; construct + populate.
let mut bitmap = [0u8; N];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The bitmap array is constructed on the stack in Pmm::new. Since Pmm<N, R> is generic over the bitmap size N, large values of N (e.g., for systems with several GiB of RAM) will lead to significant stack usage. For instance, a 4 GiB RAM extent requires a 128 KiB bitmap, which exceeds the 64 KiB boot stack defined in the BSP. This limits the portability of the PMM to larger memory configurations. Consider an initialization pattern that avoids placing the entire Pmm struct on the stack, such as initializing the static storage in-place or using a separate static for the bitmap data.

Comment thread kernel/src/mm/pmm.rs
// structurally provable.
let pa_off = idx.saturating_mul(PAGE_SIZE);
let pa_usize = self.extent.start.0.saturating_add(pa_off);
let pa_ptr = pa_usize as *mut u8;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The alloc_frame implementation directly casts a physical address (pa_usize) to a raw pointer (*mut u8) for zero-filling. This assumes an identity mapping where physical addresses are valid virtual addresses in the kernel's address space. While this holds true for the current v1 identity layout on QEMU virt, it violates the board-agnostic nature of the kernel crate. Future configurations (like a high-half kernel) will break this assumption. It would be more architectural to abstract this through a HAL-provided translation helper or a zeroing primitive, even if it's a simple identity pass-through for now.

@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: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bsp-qemu-virt/src/main.rs`:
- Around line 700-707: The snippet around __stack_top (stack_top_addr,
stack_top_aligned_up, PAGE_SIZE) and the pmm_extent construction
(PhysFrameRange::new, PhysAddr, PMM_EXTENT_START, PMM_EXTENT_END) is not
rustfmt-formatted; run cargo fmt (or rustfmt) on bsp-qemu-virt/src/main.rs to
reformat this hunk so the alignment and spacing match project style and the CI
rustfmt check passes.
- Around line 82-86: PMM_BITMAP_BYTES is truncating when the number of managed
frames isn't divisible by 8, underallocating the bitmap and causing Pmm::new to
fail for valid extents; change the const to compute the frame count first as
(PMM_EXTENT_END - PMM_EXTENT_START) / PAGE_SIZE and then round up to bytes using
ceiling division by 8 (e.g., (frame_count + 7) / 8) so any partial final byte is
allocated.

In `@docs/analysis/tasks/phase-b/T-017-physical-memory-manager.md`:
- Line 5: The document T-017-physical-memory-manager contains stale
pre-adjudication text referencing "UNSAFE-2026-0001 amendment / no new audit
entry" while the final outcome recorded is "UNSAFE-2026-0026" and the status is
"Done"; update all earlier sections (including the Status line and any mentions
in the intro/summary) to a single consistent audit result matching the final
outcome (UNSAFE-2026-0026 new audit entry) so there are no contradictory
statements about audit entry or status.

In `@docs/audits/unsafe-log.md`:
- Line 555: Update the contradictory sentence in the UNSAFE-2026-0026 status so
it clearly states that PMM initialization (the unsafe site) is reached on every
v1 boot but the zero-fill operation inside alloc_frame is not exercised in v1;
reword the line to say the PMM is constructed and exposed in v1 (so the site is
reached) while Pmm::alloc_frame (the zero-fill unsafe operation validated later
by the test Pmm::alloc_frame_returns_first_free_and_zeroes_payload and runtime
B3+ work) is not actually called in v1.

In `@docs/roadmap/current.md`:
- Around line 45-46: The "Current-focus" / "next steps" text still references
the older T-016/B2 closure; update all downstream "next task" and "review
trigger" entries to match the new active milestone and next tasks: ADR-0028 and
T-018 (AddressSpace kernel object + capability-gated Mmu::map wrappers +
activation-on-context-switch), remove or replace any mentions of T-016/B2, and
ensure the "Active milestone" / "Active task" header (B3 / T-017) is the
authoritative source so the document consistently points to ADR-0028 + T-018 as
the next items.

In `@kernel/src/mm/pmm.rs`:
- Around line 154-157: Run rustfmt on the file to fix formatting issues causing
CI failure: format kernel/src/mm/pmm.rs (including the public function new with
signature pub fn new(extent: PhysFrameRange, reserved: &[PhysFrameRange]) ->
Result<Self, PmmError>) by running `cargo fmt` (or your editor's Rust
formatting) so the file conforms to rustfmt rules before merging.
- Around line 185-195: The reserved-range loop currently allows
overlapping/duplicate ranges which double-counts reserved_count and causes
stats() to mismatch the bitmap; update the logic in the reservation path (the
loop over reserved ranges and the code that increments reserved_count) to first
validate and reject overlaps (by checking each incoming range against
previously-accepted ranges or by scanning the bitmap for already-set bits)
before accepting them, or alternatively derive reserved_count from the bitmap
after marking to ensure only unique bits are counted; reference the reserved
slice iteration, the reserved_count field, the bitmap marking code, and callers
like stats() / alloc_frame() when making the change so counting and bitmap stay
consistent.
- Around line 317-338: Extend the unsafe-block safety comment around the
core::ptr::write_bytes call to also state why unsafe is required (we are writing
to a physical address pointer pa_ptr for a 4 KiB frame and cannot obtain a safe
&mut reference/slice to that PA), enumerate the invariants already listed
(pa_ptr page-aligned, frame exclusively owned per bitmap after alloc_frame,
identity-mapped PA==VA in mmu_bootstrap, PAGE_SIZE fits), and explicitly state
why safe alternatives were rejected (cannot use safe slice/ptr APIs because no
valid Rust &mut [u8] or typed frame wrapper exists for this physical address at
this callsite, and higher-level zeroing helpers would either allocate or assume
different ownership/alignment guarantees that we cannot provide here); reference
symbols pa_ptr, PAGE_SIZE, and the allocation path (e.g., Pmm::new /
alloc_frame) so reviewers can locate the site.
🪄 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: ca0a2808-0f3e-4191-8147-646a280dc113

📥 Commits

Reviewing files that changed from the base of the PR and between fe0e106 and 6ed8a70.

📒 Files selected for processing (11)
  • bsp-qemu-virt/src/main.rs
  • docs/analysis/tasks/phase-b/README.md
  • docs/analysis/tasks/phase-b/T-017-physical-memory-manager.md
  • docs/architecture/boot.md
  • docs/architecture/memory-management.md
  • docs/audits/unsafe-log.md
  • docs/roadmap/current.md
  • docs/roadmap/phases/phase-b.md
  • kernel/src/lib.rs
  • kernel/src/mm/mod.rs
  • kernel/src/mm/pmm.rs

Comment thread bsp-qemu-virt/src/main.rs Outdated
Comment thread bsp-qemu-virt/src/main.rs Outdated
Comment thread docs/analysis/tasks/phase-b/T-017-physical-memory-manager.md
Comment thread docs/audits/unsafe-log.md Outdated
Comment thread docs/roadmap/current.md
Comment thread kernel/src/mm/pmm.rs Outdated
Comment thread kernel/src/mm/pmm.rs
Comment thread kernel/src/mm/pmm.rs Outdated
cemililik and others added 3 commits May 10, 2026 23:26
3-bot review round (sourcery + gemini + coderabbit; qodo rate-
limited). 13 inline findings on the T-017 implementation arc.
Triage: 1 build failure (fmt), 4 Major (1 from 3 bots, 1 from 2,
2 standalone), 6 Minor, 2 Medium-to-defer.

Verified each against current code:

Applied:

1. **CI fmt failure** — `cargo fmt --all` reformatted 9 sites in
   bsp-qemu-virt/src/main.rs + kernel/src/mm/pmm.rs where I'd
   manually broken lines that the rust-toolchain.toml-pinned
   nightly-2026-01-15 rustfmt collapses to single lines (e.g.,
   `let x = a.method().method()` style). No semantic change;
   pure whitespace.

2. **Overlapping reserved ranges in Pmm::new** (Major; flagged
   independently by sourcery + gemini + coderabbit). If two
   reserved ranges overlap, the per-range bit-set loop sets the
   shared frame's bit once but each entry increments
   `reserved_count` separately, leaving the cached counter
   inconsistent with the bitmap. `stats().free_frames` could
   report 0 while `alloc_frame()` still finds a free frame —
   classic counter-drift. Added:
   - **PmmError::OverlappingReservedRanges** variant
     (`#[non_exhaustive]`, additive).
   - **O(R²) pairwise overlap check** in `Pmm::new` after the
     in-extent / alignment validations, before any bitmap
     mutation. Two half-open ranges [a, b) and [c, d) overlap
     iff `a < d && c < b`. For R ≤ 8 (v1), ≤ 28 comparisons —
     trivially cheap.
   - **`new_rejects_overlapping_reserved_ranges`** host test
     covers three scenarios: classic-overlap (rejected),
     duplicate-range (rejected), touching-but-not-overlapping
     half-open ranges `[a, b) + [b, c)` (correctly NOT rejected
     — boundary semantics).

3. **stats().total_frames anchored against extent** (sourcery
   Medium). Previously derived as `free_count + reserved_count +
   allocated_count`; if any sub-counter drifts, the total
   silently re-establishes internal consistency rather than
   surfacing the drift. Now derived from `self.extent.frame_count()`
   directly so counter bugs become stats-vs-bitmap mismatches
   the existing `stats_parity_with_bitmap_bit_count` test
   catches.

4. **PMM_BITMAP_BYTES round-up** (coderabbit Minor). For QEMU
   virt's 32 768 frames the old `/8` and new `.div_ceil(8)`
   produce the same 4 096 bytes (no v1 bug). Forward-defensive
   for future BSPs with non-multiple-of-8 frame counts where the
   floor form would underallocate the last bitmap byte. Used
   stable `usize::div_ceil` directly per clippy::manual_div_ceil
   suggestion.

5. **alloc_frame SAFETY block — inline rejected-alternatives**
   (coderabbit Major). Per CLAUDE.md unsafe-policy.md §1: every
   unsafe block must state (a) why unsafe is needed, (b)
   invariants upheld, (c) why safer alternatives were rejected.
   The previous SAFETY comment had (a) + (b) but punted (c) to
   the audit-log entry. Now inlines a four-alternative summary
   (slice-from-raw-parts-mut + lazy-zero-on-page-fault +
   skip-zero + volatile_set_memory) with one-line rejection
   reasons each. The fifth alternative (Mmu::zero_frame trait
   method) stays in UNSAFE-2026-0026's full Rejected-alternatives
   discussion to keep the inline block bounded; the SAFETY
   comment names the audit entry for the full record.

6. **T-017 stale UNSAFE outcome text** (coderabbit Minor).
   Lines 20 / 87 / 88 / 124 / 134 still claimed
   "UNSAFE-2026-0001 Amendment / no new audit entry" — pre-
   adjudication wording from the ADR-0035 §Dependency chain
   step 5's deferred-verdict era. Now uniformly "UNSAFE-2026-0026
   (new audit-log entry per the adjudication-deferred verdict)"
   with the same Rejected-alternatives summary the new entry
   carries.

7. **UNSAFE-2026-0026 contradictory verification sentence**
   (coderabbit Minor). Status line said both "v1's demo never
   calls `alloc_frame`" AND "the site is reached on every boot".
   The two refer to different "sites": Pmm::new construction
   (reached every boot, prints the banner) vs. the
   `core::ptr::write_bytes` zero-fill block (the actual unsafe
   site, runtime-unexercised because no v1 caller hits
   `alloc_frame`). Reworded to make the construction-vs-zero-
   fill distinction explicit per coderabbit's suggestion.

8. **current.md downstream "next steps" sections** (coderabbit
   Minor). Lines 78-79 ("Next task to open: T-016 implementation"
   + "Next review trigger: B2 closure trio") were stale —
   T-016 closed two PRs ago, B2 closed too. Updated to point
   at ADR-0028 + T-018 (next task) + B3 milestone closure trio
   (next review trigger) with the correct forward-flag (T-018
   will be UNSAFE-2026-0026's first runtime exercise).

Skipped with reason:

A. **`bitmap` array on the function stack in `Pmm::new`**
   (gemini Medium). `let mut bitmap = [0u8; N]` creates a stack
   frame proportional to `N`. For v1 N = 4096 → trivially fits
   in the 64 KiB boot stack; for future Pi-4-class BSPs with
   N = 131072 (4 GiB RAM → 128 KiB bitmap), this overflows the
   v1 stack. The fix is an in-place `Pmm::init_in(slot: &mut
   MaybeUninit<Self>, ...)` API that writes through the
   StaticCell directly rather than via a stack-allocated value.
   That's a larger API change; v1 doesn't need it and the cost
   of bundling it here is non-trivial. Tracked as a known
   future-BSP concern in T-017's review-history; opens with the
   first BSP whose `N` exceeds the boot stack (likely the
   Pi-4 BSP arc).

B. **PA → kernel pointer cast in alloc_frame** (gemini Medium).
   The `(extent.start.0 + idx * PAGE_SIZE) as *mut u8` cast
   relies on the identity-mapped v1 layout (ADR-0027). The
   SAFETY block + UNSAFE-2026-0026's invariant (3) already
   document this explicitly. The high-half migration
   (ADR-0033 placeholder) will need a `phys_to_virt` helper at
   this exact site — the placeholder ADR's §Simulation table
   will name it as the first migration step. No code change in
   T-017's scope; the identity-only assumption is the v1
   design.

Test count rises 15 → 16 (new `new_rejects_overlapping_reserved_ranges`
pinning sourcery+gemini+coderabbit's #1 finding). Workspace
total 199 → 200 host tests.

Verification:
  - cargo fmt --check               clean (CI ran with
                                    nightly-2026-01-15;
                                    matches local now)
  - cargo clippy (host)             clean -D warnings
  - cargo kernel-clippy             clean -D warnings (used
                                    `usize::div_ceil` per
                                    clippy::manual_div_ceil)
  - cargo test                      200/200 host (was 199; +1
                                    overlap test)
  - cargo +nightly miri test        15/15 PMM tests clean
                                    (Stacked Borrows clean
                                    against the unchanged
                                    zero-fill path)
  - cargo kernel-build              clean
  - QEMU smoke                      stable; new banner reads
                                    "tyrne: pmm initialized
                                    (32603 frames available;
                                    165 reserved)" — the +1
                                    reserved-frame delta vs.
                                    prior commit (32604 + 164)
                                    is the kernel-image-section
                                    growth from this commit's
                                    code additions rounding up
                                    one more frame at
                                    __stack_top_aligned

Refs: ADR-0035, T-017
Audit: UNSAFE-2026-0026
…erred)

External "Approve with nits" review on the T-017 implementation
arc. 4 actionable findings (3 Minor + 1 Nit) + 1 preexisting
out-of-scope drift. Triage:

Applied:

1. **T-017 §DoD stale UNSAFE-2026-0001 Amendment references**
   (Minor; Axis 12.1). Four sites still carried pre-adjudication
   "UNSAFE-2026-0001 Amendment / no new audit entry" wording
   that contradicted §Acceptance criteria + §Approach commit 2
   + Review history + the actual UNSAFE-2026-0026 entry:

   - §DoD line 135: "UNSAFE-2026-0001 gains the PMM frame-
     zeroing Amendment" → "UNSAFE-2026-0026 introduced as a
     new entry (NOT a UNSAFE-2026-0001 Amendment) per ADR-0035
     §Dependency-chain step 5's adjudication-deferred verdict".
   - §DoD line 136: "No new audit-log entry — confirms ADR-0035's
     'zero new audit entries' promise byte-for-byte" → "One new
     audit-log entry (UNSAFE-2026-0026) — ADR-0035 §Positive
     consequence 'Zero new unsafe audits' flips to 'one new
     entry' per the same adjudication caveat".
   - §DoD line 141 commit-trailer guidance:
     "Audit: UNSAFE-2026-0001 trailer where applicable" →
     "Audit: UNSAFE-2026-0026 trailer for commits that introduce
     or extend the PMM frame-zeroing site".
   - §References line 161: "UNSAFE-2026-0001 gains the PMM
     frame-zeroing Amendment" → "UNSAFE-2026-0026 covers the
     PMM frame-zeroing site (new entry per the §Dependency-
     chain-step-5 adjudication)".

   Same-file internal contradiction; a future reader walking
   T-017 top-to-bottom can now trace the adjudication outcome
   without cross-referencing the audit log.

2. **`Pmm::new` doc-comment "three" → "five" validations**
   (Minor; Axis 12.2). The doc-comment listed three checks
   (extent-aligned, ranges-in-extent, list-fits-in-R) but the
   code has five distinct validation steps. Missing entries
   in the doc:

   - Bitmap-size check (`total_frames > N * 8` → OutOfRange).
   - Per-range alignment check (`!range.is_aligned()` →
     MisalignedAddress).
   - Per-range non-inverted check (`range.end < range.start`
     → OutOfRange).
   - Pairwise-overlap check (→ OverlappingReservedRanges, the
     round-1 addition).

   Replaced the three-item enumeration with the full five-item
   list. Same "three fail-fast validations" drift fixed in
   memory-management.md §"API".

3. **`extent_16f()` → `extent_4f()` test fixture rename**
   (Nit; Axis 13.1). The fixture was named "16 frames" and
   doc-commented "16 frames (16 KiB)" but the actual range
   `PhysAddr(0x4000_0000)..PhysAddr(0x4000_4000)` is 16 KiB
   = 4 frames (at 4 KiB/frame). The sanity-check test asserts
   `frame_count() == 4` (matching the code, contradicting the
   name). Rename + corrected doc-comment ("4 frames (16 KiB)";
   "Bitmap holds 4 bits → 1 byte (rounded up); N = 2 is the
   smallest non-trivial PMM with headroom for the sanity
   check"). Sanity test renamed `extent_16f_fixture_sanity` →
   `extent_4f_fixture_sanity`. Pure cosmetic; no test logic
   changes; fixture is referenced only by its own sanity test
   (every other test uses `pmm_over_backing` directly).

4. **`alloc_frame` doc-comment on the unreachable-leak path**
   (Minor; Axis 3.1). The previous doc-comment claimed
   "we keep the path total via `?` chaining on an Option to
   avoid an unwrap" — but there is no `?` chain in the
   actual code; `PhysFrame::from_aligned(...)` returns the
   Option directly. A future maintainer reading the
   doc-comment would expect a rollback path that doesn't
   exist.

   Replaced with an honest description: (a) why `from_aligned`
   is provably-Some by construction (Pmm::new validation (i)
   + idx * PAGE_SIZE alignment preservation), (b) the
   unreachable-leak caveat (bitmap mutation happens BEFORE
   the call, so if a future change ever weakens the alignment
   proof and `from_aligned` returns None, the frame is
   permanently leaked), (c) explicit guidance for a future
   maintainer who alters Pmm::new's validation set ("either
   preserve the alignment proof or move the mutation block
   below this call"). Doc-only fix; the structural unreachability
   in v1 makes the leak path inert today; the smaller doc fix
   is defensible vs. the 4-line refactor (move mutation
   post-from_aligned) because the refactor would add a
   rollback-on-None branch for a structurally impossible case.

Skipped with reason:

A. **BSP `Audit: UNSAFE-2026-0001` tag drift on PMM StaticCell
   writes at main.rs:706, 722, 729** (Nit; Axis 9.1; preexisting,
   out-of-scope). The new PMM-related unsafe blocks cite
   UNSAFE-2026-0001, but UNSAFE-2026-0001's actual scope is
   `Pl011Uart::new(PL011_UART_BASE)` (typed MMIO wrapper
   construction); the StaticCell unsafe impl Sync pattern is
   UNSAFE-2026-0010. The drift is preexisting (other StaticCell
   writes for CONSOLE / CPU already cite UNSAFE-2026-0001
   incorrectly); this PR perpetuates but doesn't introduce the
   pattern. Worth a separate hygiene PR that retags the
   StaticCell-pattern unsafe blocks across main.rs to
   UNSAFE-2026-0010 — not a T-017 remediation. Queued.

Verification:
  - cargo fmt --check               clean
  - cargo host-clippy               clean -D warnings
  - cargo kernel-clippy             clean -D warnings
  - cargo test                      200/200 host (unchanged;
                                    only test name renamed)
  - cargo kernel-build              clean

Refs: ADR-0035, T-017
Audit: UNSAFE-2026-0026

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mmError to round-2 reality

Round-3 review (parallel reviewer 1) flagged §Acceptance criteria
line 41 still says "three fail-fast validations" — the only
remaining drift inside T-017 after the round-2 fixup. Walking
the file once more surfaced three sibling drifts in the same
file, all from the same root cause (pre-adjudication / pre-
overlap-check wording that the round-2 fixup didn't sweep
because it focused on §DoD + §References):

1. **§Acceptance criteria line 41** (`Pmm::new` constructor
   description) — "three fail-fast validations" → "five
   fail-fast validations" with the full (i)..(v) enumeration
   matching the doc-comment at pmm.rs:136-165: extent-aligned,
   bitmap-size, R-cap, per-range (alignment + bounds + non-
   inverted), pairwise-overlap. Each validation now names its
   error variant and the round-1-PR-#26 provenance for the
   overlap gate.

2. **§Acceptance criteria line 45** (`PmmError` enum
   description) — added `OverlappingReservedRanges` variant
   to the documented variant list with its trigger condition
   ("two reserved ranges share at least one frame, which would
   cause bitmap-vs-counter drift"). Also extended `OutOfRange`'s
   description to cover the bitmap-size-exceeds-capacity
   condition and the reserved-range-out-of-extent /
   inverted-range conditions, mirroring how Pmm::new actually
   uses the variant.

3. **§Approach line 123** (commit 1 description) — "three
   fail-fast validations (extent-alignment, reserved-in-extent,
   R-cap)" → "five fail-fast validations (extent-alignment,
   bitmap-size, R-cap, per-range {alignment + bounds + non-
   inverted}, pairwise-overlap — the last gate added in
   PR #26 round-1 review)". Also added
   `new_rejects_overlapping_reserved_ranges` to the
   constructor-commit host test list (the test exists at
   pmm.rs:613 but the §Approach narrative didn't yet name it).

4. **§Background line 22 + §Design notes line 146** — "audit-
   log Amendment" → "new audit-log entry (UNSAFE-2026-0026,
   per the §Dependency-chain-step-5 adjudication-deferred
   verdict)". Same pre-adjudication wording the round-2 fixup
   purged from §DoD + §References; these two §Background /
   §Design-notes sites slipped through that sweep.

All four mentions matching `grep -n "Amendment"` that remain
post-this-commit are legitimate negation-form phrasings
("NOT a UNSAFE-2026-0001 Amendment", "not a UNSAFE-2026-0001
Amendment") in §Acceptance criteria line 87, §DoD line 135,
and Review history line 175 — all correctly framing the
adjudication outcome rather than asserting an Amendment.

Verification:
  - grep -n "three\|Amendment\|zero new audit"
    docs/analysis/tasks/phase-b/T-017-physical-memory-manager.md
    returns only the four legitimate negation-form lines (87, 135,
    146 [audit-log entry], 175 [Review history audit trail]); no
    remaining "three fail-fast validations" hits.
  - cargo fmt --check                 clean (doc-only change)
  - cargo host-clippy                  clean -D warnings
  - cargo test                         200/200 host (unchanged
                                       — no code touched)

Refs: ADR-0035, T-017

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