ADR 0004 — Per-island memory isolation: fencing kernel-owned RAM + a first-class isolation attribute¶
Status: Accepted (2026-08-24) — implemented (d5d4084 D2 core + this commit’s D1). Extends
ADR 0003 (the supervisor-fence seam) and
ADR 0002 (multi-instance kernels / AMP islands).
Context¶
ADR 0003 gave each AMP island a hardware fence that binds even its kernel — a privileged store into a
neighbour island’s RAM faults. But the fence only covers the app arena an island declares with
tk_kernel_set_ram(base, size). The kernel’s own per-instance state — struct tk_kernel, which holds
that island’s tcbs[], scheduler bookkeeping, per-core idle_stack[][], and its spinlock — is not covered:
s_kernel and s_extra_kernels[] all live in one shared .privileged_bss. So today a rogue island’s
kernel can still scribble a neighbour’s TCBs or ready-state, and the freedom-from-interference claim holds
for app RAM but not for kernel RAM (backlog D2; QC3-1(i)). sizeof(struct tk_kernel) is ≈ 3 KiB
(SMP=2, 8 TCBs, 1 KiB idle stacks) — dominated by tcbs[] and idle_stack[][].
Separately (backlog D1), the mixed-criticality asymmetry “fence the safety island, leave the QM island
shared” is largely achievable today via selective set_ram: only the safety island declares an arena,
so a QM core is denied the safety arena while the QM arena stays shared. The residue is (a) a first-class,
self-documenting way to declare that intent, and (b) the one thing the current seam cannot express — a
trusted island whose core is fenced from nothing (a monitor that may read every island), because
tk_instance_fence_enter denies all neighbour arenas unconditionally.
The load-bearing constraint (found while scoping): tk_kernel_liveness_check reads a neighbour
instance’s k->ticks_ms and k->sched_progress directly — a deliberate cross-island stall-detection
channel (ADR 0002 review L3). So D2 must NOT fence the whole struct tk_kernel; that would fault the
monitor. The heartbeat fields are meant to be read across the boundary.
Decision¶
2. D1 — the TRUSTED escape hatch (kernel-RAM fencing is unconditional)¶
Implementation refined the three-mode sketch to what earns its keep. Kernel-RAM fencing (§1) is
unconditional when isolation is on — every island’s struct tk_kernel is mutually fenced, the strongest
freedom-from-interference and the right safety default (you never want an island’s scheduler state
corruptible to save a 4 KiB region). So the “SHARED vs ISOLATED” kernel distinction collapses: an island’s
mixed-criticality “sharedness” is entirely about its app arena, already opt-in per island via set_ram
(a QM island simply doesn’t call it; the asymmetry falls out — ADR 0003). That leaves ONE genuinely new
capability the deny-list seam cannot express:
TRUSTED—tk_kernel_set_trusted(handle)marks an instance whose core installs no fence (empty deny-list), so a monitor/supervisor may read every island’s RAM directly (beyond the shared heartbeat mirror, which carries only the two liveness counters). Its OWN kernel RAM is still denied to other cores, so the monitor cannot be corrupted — TRUSTED loosens only the monitor’s own view. A one-line branch intk_instance_fence_enter(if (me->trusted) return;), atrustedflag on the instance, both folded away atMAX_KERNELS==1.
(The SHARED/ISOLATED labels are dropped: with kernel RAM unconditional, “isolated” is the default and
“shared” means “no set_ram”. A future shared-kernel island — MPU-budget-driven — could revive a
per-instance opt-out; deferred, no use case today.)
3. Granularity, budget, degrade¶
Granule: paging = 4 KiB align + multiple; PMSAv7 = pow2 + natural alignment. The private-body region is placed to satisfy the coarsest.
Budget: ≤
2·(MAX_KERNELS−1)deny entries. Paging arches refine within their table budget (fine for a few KiB regions); region-MPU arches spend one MPU region per entry — fail-CLOSED (ADR 0003 §5) if a particular core runs out. Realistic island counts (2–3) fit comfortably.Degrade: unchanged from ADR 0003 §5 — loud at compile time, fail-closed at runtime.
4. Collapse safety (ADR 0002 §3, non-negotiable)¶
At TK_CONFIG_MAX_KERNELS == 1 or isolation off: no split (the shared prefix folds back so the single
instance is byte-identical), no auto-deny, no mode field, no alignment padding. A single-instance build is
byte-identical to one built without D2 — verified by the same collapse checks as ADR 0003.
Consequences¶
Freedom-from-interference becomes complete at the kernel level — a neighbour can no longer corrupt an island’s scheduler state/TCBs, only (harmlessly) its advisory heartbeat. The missing half of ADR 0003.
Cross-island monitoring is preserved by design (the heartbeat prefix stays shared), and the trusted island gives a monitor full read access when it needs more than heartbeats.
Per-instance placement + the struct split are the main churn — extracting/reordering the heartbeat fields touches
now_ms/ tick / schedule / idle / liveness; the private struct needs an aligned type and distinct per-instance objects instead ofs_extra_kernels[].Builds on Epic B — the seam is already
uintptr_t, so a 64-bit port fences kernel RAM above 4 GiB without further seam work.
Verification¶
D2 —
examples/amp_kernel_fence(arm_v8a virt SMP=2), through the realtk_kernel_define→tk_kernel_startpath: a normal island’s store into the DEFAULT kernel struct FAULTS (Data Abort, FAR inside the fenced region), an ordinary global write succeeds. Regression: all four fence demos still PASS,amp_spawn(no isolation) unchanged,check_fence_matrixPASS, collapse clean.D1 (
TRUSTED) —examples/amp_trusted_monitor: a trusted monitor island reads the default kernel struct DIRECTLY (raw word + liveticks_ms) with no fault, where the normal island above faulted.
Resolved / remaining¶
~~Shared-heartbeat mechanism (reorder vs extract).~~ → the heartbeat mirror (§1).
~~
TRUSTEDwrite access.~~ → a trusted core skips the fence entirely (if (me->trusted) return;), so it gets R/W to every island; the seam can’t express read-only holes, and R/W is fine (the monitor is trusted).Region-MPU budget for > 2 isolated islands (≤ 2 deny entries each): paging arches refine within their table budget; region-MPU arches spend one MPU region per entry and fail-CLOSED over budget. Document the per-arch ceiling when a >2-island region-MPU build is attempted — untested today.
~~First verification arch.~~ → arm_v8a virt SMP=2 (both demos above).