FABRIC.md: insert 4.3.5a -- riscv64 has no software-controlled paging

Discovered live while implementing what was 4.3.5a (PLIC bring-up,
renumbered 4.3.5b): riscv64 never activates its own page table
(load_cr3() is a no-op outside __x86_64__, arch.c's Sv39/SATP setup is
an explicit stub), so firmware's own mapping -- which doesn't cover the
PLIC's MMIO window -- is what's live for the kernel's whole lifetime.
Confirmed via a live store-page-fault, not inferred. Per Captain Bob's
ruling: stop, scope as its own item, decide the fix (Bare mode vs. real
Sv39) in a future session rather than embedded in the PLIC item.
4.3.5b-4.3.5e renumbered accordingly (4.3.5f was 4.3.5e); no items were
executed under the old numbering, so this is free to do cleanly now.

Documentation only. PLIC driver code (plic.c/plic.h, interrupts.c/
apic.c wiring, Makefile.starkernel) is written and reviewed but stays
uncommitted -- it cannot be verified until 4.3.5a unblocks it, and
riscv64 currently hangs in the fatal exception handler with it applied.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-08 00:47:31 -04:00
co-authored by Claude Sonnet 5
parent 6a97fa4c98
commit e01c4e33e4
+53 -19
View File
@@ -3919,30 +3919,59 @@ document and committing that amendment as its own item.*
> aarch64, `logs/20260808-001637` riscv64) — the feature is amd64-only this item, so
> aarch64/riscv64 are a regression check, not a keyboard test.
- [ ] **4.3.5a — riscv64: PLIC bring-up (external interrupt controller).** Does not exist
- [ ] **4.3.5a — riscv64: minimal paging/addressing bring-up (prerequisite, blocks
4.3.5b).** Discovered live 2026-08-08 while implementing 4.3.5b, not anticipated when
4.3.5a-4.3.5f were originally scoped. **riscv64 has no software-controlled paging at
all.** `arch/riscv64/arch.c`'s own comment is explicit: "Full Sv39/Sv48 page-table setup
(SATP, PMP, etc.) is deferred to a later milestone" — it is a stub, `satp` is never
written by this kernel. Separately, `memory/vmm.c` (header comment: "4-level paging
Virtual Memory Manager (x86_64)") runs its `vmm_init()` unconditionally on all three
architectures and builds a real x86-64-style page table in memory, but its activation
step (`load_cr3()`) compiles to a no-op outside `__x86_64__` — so on riscv64 that table
is dead, unused data. Whatever `satp` firmware (EDK2 RISC-V) left active at
`ExitBootServices()` is what's live for the kernel's entire lifetime, and there is no
documented or safe way for us to add entries to firmware's own page table post-handoff.
This surfaced because 4.3.5b's PLIC threshold register (`PLIC_BASE + 0x201000`, deep
into the PLIC's 0x600000-byte MMIO window) is not covered by firmware's mapping —
confirmed via a live store-page-fault (`scause=0xF`) at exactly that address, not
inferred. **Not fixed as part of surfacing it** — Captain Bob's ruling 2026-08-08: stop,
scope as its own item, decide the approach (Bare-mode `satp=0` vs. real Sv39 bring-up)
in a future session with fresh context, rather than deciding it embedded inside a PLIC
item. 4.3.5b's driver code (`plic.c`/`plic.h`, `interrupts.c`/`apic.c` wiring,
`Makefile.starkernel`) exists, written and reviewed, but **uncommitted** — it cannot be
verified until this item unblocks it, and 4.3.5b's own acceptance ("three-arch QEMU boot
unaffected") is currently violated (riscv64 hangs in the fatal exception handler).
*Done when:* riscv64 has some working address-translation story (either genuinely
activated Sv39 paging with a kernel-owned page table, or an explicit, deliberate switch
to Bare mode) sufficient for `plic_init()`'s existing MMIO writes to succeed without
faulting, three-arch QEMU boot clean, log committed.
*Refs:* §27.5.
- [ ] **4.3.5b — riscv64: PLIC bring-up (external interrupt controller).** Does not exist
anywhere in this tree — Phase 0 (0.2/0.3) only ever enabled the S-mode *timer* interrupt
(`sie.STIE`); external interrupts (`sie.SEIE`, bit 9) were never touched, and
`interrupts.c`'s own comment ("the PLIC replaces both") describes the architecture, not
anything implemented. This item enables `sie.SEIE`, programs the PLIC's per-source
priority, the hart context's threshold and enable bits, and wires the trap handler's
external-interrupt case to `PLIC claim` → dispatch → `PLIC complete`. Prerequisite for
4.3.5b; no keyboard code in this item.
4.3.5c; no keyboard code in this item. **Blocked on 4.3.5a** (see that item) — driver
code already written, not yet verifiable.
*Done when:* a synthetic/known external interrupt source claims and completes correctly
through the PLIC on riscv64, three-arch QEMU boot unaffected, log committed.
*Refs:* §27.5.
- [ ] **4.3.5b — riscv64: virtio-keyboard-device, interrupt-driven.** Add
- [ ] **4.3.5c — riscv64: virtio-keyboard-device, interrupt-driven.** Add
`virtio-keyboard-device` (MMIO virtio, matching the existing MMIO virtio-blk pattern on
this board) to the riscv64 `qemu`/`qemu-esp` targets in `Makefile.starkernel`. New driver
under `src/starkernel/virtio/` for the virtio-input device class — decode `EV_KEY` events
off the input event virtqueue. Unlike `virtio_blk.c`'s synchronous poll-the-used-ring
pattern, this device must be interrupt-driven end to end: the used-ring notification
arrives via the PLIC path from 4.3.5a, not a poll loop. Built on 4.3.5a.
arrives via the PLIC path from 4.3.5b, not a poll loop. Built on 4.3.5b.
*Done when:* a keypress in QEMU on riscv64 produces a captured `EV_KEY` event via the
interrupt path with no polling loop, three-arch QEMU boot unaffected, log committed.
*Refs:* §27.5.
- [ ] **4.3.5c — aarch64: GIC SPI wiring for virtio-input.** Item 0.6 scoped the GIC to
- [ ] **4.3.5d — aarch64: GIC SPI wiring for virtio-input.** Item 0.6 scoped the GIC to
"one interrupt" (the timer PPI) on purpose and explicitly called a general GIC driver
out of scope. A PCI-attached `virtio-keyboard-pci` device signals via legacy INTx, which
is an SPI, not a PPI — a different GIC distributor path (SPI target-CPU and priority
@@ -3953,18 +3982,18 @@ document and committing that amendment as its own item.*
PCI device, three-arch QEMU boot unaffected, log committed.
*Refs:* §27.5.
- [ ] **4.3.5d — aarch64: virtio-keyboard-pci, interrupt-driven.** Add `virtio-keyboard-pci`
- [ ] **4.3.5e — aarch64: virtio-keyboard-pci, interrupt-driven.** Add `virtio-keyboard-pci`
to the aarch64 `qemu`/`qemu-esp` targets — the existing `virtio-blk-pci` device proves the
PCI bus is already enumerated (`pci.c`) on this board, so this rides the same bus, a new
device class. Same virtio-input driver shape as 4.3.5b (shared code where the transport
device class. Same virtio-input driver shape as 4.3.5c (shared code where the transport
allows — MMIO vs. PCI config-space discovery differs, the event-queue/`EV_KEY` decode
should not). Built on 4.3.5c.
should not). Built on 4.3.5d.
*Done when:* a keypress in QEMU on aarch64 produces a captured `EV_KEY` event via the
interrupt path with no polling loop, three-arch QEMU boot unaffected, log committed.
*Refs:* §27.5.
- [ ] **4.3.5e — Checkpoint: one keyboard abstraction, three architectures, no polling.**
Converge 4.3.5/4.3.5b/4.3.5d behind a single scancode/keycode interface so the REPL
- [ ] **4.3.5f — Checkpoint: one keyboard abstraction, three architectures, no polling.**
Converge 4.3.5/4.3.5c/4.3.5e behind a single scancode/keycode interface so the REPL
keyboard-input work (M8, outside Stadium) has one thing to call, not three. Stop and
review here — same posture as 4.3.4 — before scoping REPL wiring.
*Done when:* a keypress on all three architectures produces the same shape of event at
@@ -3973,7 +4002,7 @@ document and committing that amendment as its own item.*
three-arch QEMU boot, logs committed.
*Refs:* §27.5.
> **Sequencing noted 2026-08-07, not yet scoped:** keyboard input (4.3.54.3.5e) →
> **Sequencing noted 2026-08-07, not yet scoped:** keyboard input (4.3.54.3.5f) →
> glyph rendering (stroke-based font capsules) → REPL — in that order, before the 4.4
> Artemis boundary. Recorded so the order is not lost between sessions; neither glyph
> rendering nor REPL has *Done when* criteria yet. Per §25.0 rule 2, each gets scoped in
@@ -4352,7 +4381,7 @@ not a quick add. Stop and review here before scoping the next 4.3.x item. Out-of
for 4.3.14.3.4 is recorded once, at the 4.3 punch-list entry itself (§25.5), not repeated
per sub-item.
### 27.5 Keyboard input, interrupt-driven (4.3.54.3.5e)
### 27.5 Keyboard input, interrupt-driven (4.3.54.3.5f)
Scoped 2026-08-07, next after the 4.3.4 checkpoint. Purpose: get a real keypress into the
kernel with no polling anywhere in the path, so the REPL keyboard-input work (M8, tracked
@@ -4360,7 +4389,7 @@ outside Stadium — see `.claude/CLAUDE.md`'s roadmap section) has something to
section is groundwork only, same as 27.127.4 was for the framebuffer — it stops at "a
keypress produces an event at a shared interface," not REPL line-editing, not a scancode-
to-ASCII layout, not repeat/modifier-key semantics. Those are later items, scoped once
4.3.5e is reviewed.
4.3.5f is reviewed.
**Why this split is bigger than 4.3.3's three-way split.** 4.3.3/4.3.3a/4.3.3b all built on
interrupt/timer substrate Phase 0 already finished. Keyboard input does not have that
@@ -4373,11 +4402,16 @@ luxury on two of three architectures:
- **riscv64** has never enabled external interrupts at all — Phase 0 (0.2/0.3) scoped
strictly to the S-mode timer interrupt (`sie.STIE`). The PLIC (`sie.SEIE`, bit 9) is
unimplemented; `interrupts.c`'s existing comment naming the PLIC describes the
architecture, not working code. 4.3.5a is that bring-up, split out as its own item because
it is substrate, not keyboard-specific — same shape as Phase 0's per-ISA items.
architecture, not working code. 4.3.5b is that bring-up, split out as its own item because
it is substrate, not keyboard-specific — same shape as Phase 0's per-ISA items. **Found
2026-08-08 while implementing 4.3.5b: that substrate assumption itself had a substrate
gap underneath it** — riscv64 has no software-controlled paging at all (see 4.3.5a,
inserted ahead of the PLIC item once this surfaced). Phase 0 never needed to touch memory
translation because the timer only ever touches CSRs and SBI calls, never MMIO; PLIC
bring-up is the first riscv64 item to need a real MMIO write, and that is what exposed it.
- **aarch64**'s GIC (item 0.6) was deliberately scoped to one PPI (the timer) and explicitly
ruled a general GIC driver out of scope. A PCI `virtio-keyboard-pci` device signals via an
SPI, a distinct GIC path 0.6 never touched. 4.3.5c is the minimal SPI extension, held to
SPI, a distinct GIC path 0.6 never touched. 4.3.5d is the minimal SPI extension, held to
the same narrow-scope discipline 0.6 used.
**Hardware path differs by architecture, same as the framebuffer did.** amd64's `q35`
@@ -4385,13 +4419,13 @@ machine has a real i8042 PS/2 controller (QEMU default, IRQ1) — 4.3.5. Neither
(aarch64, riscv64) has PS/2 hardware; both get a `virtio-keyboard` device added to their
QEMU command lines and a new virtio-input driver (this tree has none — `virtio_blk.c` is
the only existing virtio driver, and it is fully synchronous/polled, so it is a transport
reference only, not an interrupt-handling one) — 4.3.5b (riscv64, MMIO transport) and 4.3.5d
reference only, not an interrupt-handling one) — 4.3.5c (riscv64, MMIO transport) and 4.3.5e
(aarch64, PCI transport, riding the bus `pci.c` already enumerates for `virtio-blk-pci`).
**No polling, anywhere, in any of 4.3.5/4.3.5b/4.3.5d.** This is the one hard constraint
**No polling, anywhere, in any of 4.3.5/4.3.5c/4.3.5e.** This is the one hard constraint
carried through every sub-item — restated per item because it is the actual point of doing
interrupt bring-up first rather than reading `0x64`'s status bit or spinning on a used-ring
index, which would have been the fast/wrong way to get a key on screen quickly.
4.3.5e is the checkpoint, same posture as 4.3.4: converge the three architecture-specific
4.3.5f is the checkpoint, same posture as 4.3.4: converge the three architecture-specific
paths behind one interface before anything scopes REPL line input on top of it.