# ADR 0005 — System-side (TrustZone-AMP) fence for a secondary core that has no MPU of its own **Status:** Accepted (2026-08-31) — **mechanism AND kernel graft HW-PROVEN on FRDM-MCXN947**; **design reasoning settled** (pressure-tested in `sketches/amp/mcxn947/lowering_prototype.c` §7–§9 and `protection-api-discussion.md`). Realized through the portable **`world` axis** (`docs/architecture/ security-safety-api.md`): an app declares `tk_kernel_set_world(A, TK_WORLD_SECURE)` + `tk_kernel_launch_secondary(CPU1, entry)`, and the kernel lowers that onto the board seams — no AHBSC in the app. **Extends** [ADR 0003](0003-supervisor-fence-seam.md) (the per-core supervisor fence) by adding a THIRD fence mechanism class for the case ADR 0003 §consequences flagged as a hard limit: a secondary AMP core with no privilege-binding memory-protection unit. ## Context ADR 0003 fences an AMP island by programming **that core's own** memory-protection unit (region-MPU: ARM PMSAv8/v7, AURIX DPR; or paging: RISC-V Sv32, ARM VMSA) so even the island's *kernel* is bound out of a neighbour's RAM. It is **symmetric and per-core**: every island's core installs its own fence, keyed off a deny-list of the *other* islands' arenas (`arch_instance_fence_apply(deny_base, deny_size, ndeny)`). That model assumes each core **has** such a unit. On the FRDM-MCXN947 (dual Cortex-M33) it holds for the primary **CPU0** (8-region PMSAv8 MPU + TrustZone) but **not** for the secondary **CPU1** — HW-confirmed by `examples/frdm_mcxn947/mcxn947_core1_probe_demo.c`: CPU1 is a reduced M33 with **no MPU, no TrustZone, no SAU** (`ID_PFR1[7:4]=0`, `MPU_TYPE=0`, `SAU_TYPE=0`). There is no per-core unit to program, so `arch_instance_fence_apply` correctly fails loud (`tk_instance_fence_reject`). ADR 0003 documented this as a hardware limit and pointed at "a board-specific, system-side mechanism (the AHB Secure Controller)." This ADR is that mechanism, now that it is proven. ## Decision ### 1. A third mechanism class: system-side security-attribution (bus-master ownership) The fence for a unit-less secondary core is enforced **at the system bus**, not at the core. On MCXN947 the **AHB Secure Controller (AHBSC)** attaches a security level to each memory region and treats **CPU1 as a permanent Non-secure bus master**; a Non-secure transaction to a region marked **Secure** is denied at the bus, before it reaches the RAM. This is a distinct class from ADR 0003's two: | Class | Binds by | Programmed | Symmetry | Examples | |---|---|---|---|---| | region-MPU (ADR 0003) | privilege, per core | each core, its own MPU | symmetric | PMSAv8/v7, AURIX DPR | | paging (ADR 0003) | address translation, per core | each core, its own tables | symmetric | RISC-V Sv32, ARM VMSA | | **system-side attribution (this ADR)** | **bus-master security level** | **the trusted (Secure) core, once, at the system controller** | **asymmetric** | **MCXN947 AHBSC**; (cf. C2000 owner/master, backlog G1) | ### 2. Asymmetric trust: a Secure core marks; the Non-secure core is confined Unlike ADR 0003's every-core-fences-itself model, this is **trusted vs untrusted**: - The **trusted** core (CPU0, Secure) programs the AHBSC to mark each **protected island's arena Secure**. It is not "deny me the neighbours" but "**mark mine Secure**" — the inverse framing of the ADR 0003 deny-list, because a security attribution is a property of the *protected* region, not of the accessing core. - The **untrusted** secondary core (CPU1) is a permanent Non-secure master; it needs no fence *installed on it* — it is confined by the marking, unable to reach any Secure region. It cannot lift its own confinement (it has no SAU / no way to issue Secure transactions). So a two-island MCXN947 partition is: island A on CPU0 = the trusted/secure island; island B on CPU1 = the confined/non-secure island. A's arena is Secure → B cannot touch it. (B's arena stays Non-secure/shared; protecting A *from* B is the freedom-from-interference that matters — a rogue B cannot corrupt A.) **What the asymmetry BUYS — security, not merely safety.** This is the deepest reason it is a separate class, beyond "CPU1 has no MPU." ADR 0003's symmetric deny is enforced by *each neighbour's own* unit: "A is safe from B" holds only because B's core installs — and never reprograms — a region denying A. That protects against a *cooperating-but-buggy* neighbour (B's wild write is faulted by B's own MPU), but an **unmanaged** B (never installs its cage) or a **malicious** B-kernel (turns its MPU off) can still reach A. That is **fault isolation**. This ADR's mark is installed *by A itself*, unilaterally, and B **cannot lift it** (no SAU; the AHBSC is Secure-only) — so it holds no matter what B does or fails to do. That is **security isolation** of a trusted island from a *non-cooperating* core. The two fence classes deliberately defend **different threat models** — cooperating-and-buggy (0003) vs untrusted-and-hostile (0005). (Pressure-tested in `sketches/amp/mcxn947/lowering_prototype.c` §9.) ### 3. The seam lives at the board/SoC layer, not the portable arch The AHBSC is an **NXP SoC peripheral**, not part of the Cortex-M33 architecture — so its fence does **not** belong in `arch/arm_v8m` (which stays the portable per-core PMSAv8 fence of ADR 0003). Instead: - The portable arch keeps its `arch_instance_fence_apply` (per-core MPU). On a core with no MPU it returns 0 → the kernel consults a **board-level system fence** before failing loud. - A **board hook** (e.g. `board_instance_fence_apply(...)`, weak default = "not handled") lets a board like MCXN947 supply the AHBSC implementation: mark the trusted islands' arenas Secure via the `RAMx_MEM_RULE` registers. The board hook, not the arch, owns the SoC-specific register programming and the secure/non-secure alias map. **No MCXN947-specific code enters the portable kernel/arch.** **The app never calls this hook.** It declares mechanism-neutral **intent** at the kernel level — an island's *posture* (isolated / trusted; a shared window is a separate region, §6) — and the kernel **lowers** that intent onto whichever mechanism the running core actually has: the per-core deny (arch seam) or this system-side mark (board hook). So deny-list-vs-mark-list is an internal lowering target, not an application concern. The intent→mechanism lowering, and the pressure-testing that validated it, are worked in `sketches/amp/mcxn947/protection-api-discussion.md` §7 and `lowering_prototype.c` (§5 lowering, §7–§8 pressure tests). ### 4. Runtime rules, boot-time enable HW-established (below): the AHBSC's per-region **RULE registers are writable at runtime** (the kernel marks an island's arena Secure when the island is defined), but the **global secure-checker must be enabled at secure boot** (it cannot be toggled at runtime in a non-secure/SAU-off context — that resets the chip). So the integration contract is: **the board's secure boot enables checking (as the SDK's `BOARD_InitTrustZone` does); the kernel manages the per-region rules at runtime.** This means the k2 MCXN947 integration must run the kernel as a **Secure TrustZone image** on CPU0 (a change from today's non-TZ approach-2 drop-in). ### 5. The system-side class enforces exactly TWO domains (its defining limit) A region-MPU or paging fence gives each core an N-way choice: it can deny any subset of peers, so N mutually-isolated islands cost N carve-outs and just work. The system-side class does **not**. A security-attribution controller partitions memory into exactly **two** domains — **Secure** and **Non-secure** — and each bus master belongs to one of them. The pressure test (`sketches/amp/mcxn947/lowering_prototype.c` §7–§8) made three consequences concrete: - **Three peers cannot be mutually isolated on this class.** Two Non-secure islands share one domain; marking one's arena Secure to hide it from the other would also hide it from *itself* (Secure RAM is invisible to an NS master). "≥3 mutually-`ISOLATED` islands" is therefore unachievable — the kernel must reject it **loud**, with a precise cause, not silently under-deliver. - **A shared window visible to a subset that excludes the trusted core is impossible.** The Secure master can read *all* NS RAM, so an NS window shared between two NS islands cannot be hidden from the Secure island. Subset-sharing works on region/paging; not here. - **The achievable partition is exactly one trusted (Secure) set + one confined (NS) set.** That is the whole expressive range — enough for the mixed-criticality goal (protect the critical island from the untrusted one), and no more. This is a property of the **silicon mechanism**, not of this ADR's design. The kernel's obligation is to enforce it honestly: deliver the two-domain partition, and reject anything that would need a third. ### 6. Shared memory, and how AMP/SMP compose - **A shared window is a REGION, not an island.** Model an inter-island shared segment as a first-class shared region (granted by every island's fence), never as a degenerate "open island" with no core — the latter fights the island-owns-cores model and conflates "my arena is shared" with "I am unconfined." On this class the shared window simply **stays Non-secure and unmarked**: every master reaches unmarked NS RAM, so the Secure and the confined island both see it with zero special-casing and zero region cost (a grant is the *absence* of a hole). It is a **sanctioned interference channel** — the trusted island validates whatever the confined island writes into it, exactly like an MMIO mailbox. - **AMP and SMP compose for free, because the fence is per-core keyed on ISLAND identity.** An SMP island owning several cores is one *instance*: each of its cores installs the same "deny the peers" fence and they share the island's own arena; an AMP island beside it denies, and is denied by, that whole instance. Nothing in the fence layer distinguishes AMP from SMP. Even a *heterogeneous* SMP island straddling a unit-less core composes — its unit-bearing cores enforce via their MPUs while its unit-less core is confined by the bus — though real SMP's homogeneity needs (coherency, shared translation) are a separate concern living above this layer. (A thread cannot migrate *across* islands: its TCB lives in its home island's kernel RAM, which every other island's fence denies, so a foreign core faults on the thread's own control block — isolation and no-migration are one mechanism. `lowering_prototype.c` §9 Attack A.) - **The composition assumes every powered core maps to exactly ONE island.** The default instance sweeps up every core no `tk_kernel_define` carved, so normally none is unowned. A powered-but-**unowned** core (mis-enumeration, or a vendor-released secondary the boot code left running) is an **unfenced bus master** — full memory reach, cooperating with nothing. The honest defense mirrors `tk_instance_fence_reject`: a **`tk_core_unclaimed(core)` fail-loud** at bring-up, plus holding unclaimed cores in reset. This is a general AMP bring-up requirement, not MCXN947-specific — but the §2 safety-vs-security split is why it bites *harder* on a symmetric board: there an unowned neighbour defeats a peer's deny-based protection, whereas a Secure island here protects itself regardless of what the secondary core does. (§9 Attack B.) ## Verification (mechanism) `examples/frdm_mcxn947/mcxn947_ahbsc_fence_s.c`, run on FRDM-MCXN947 in the SECURE world (checking enabled at boot via `BOARD_InitTrustZone`), 2026-08-25: - **Runtime rule change works with checking on:** `RAMH_MEM_RULE.RULE0` written Secure at runtime (read back `0x2`). - **Secure access allowed, Non-secure access blocked:** CPU0's access to the marked SRAMH block via the **Secure alias `0x30060000`** returns the sentinel; its access via the **Non-secure alias `0x20060000`** (same physical bank, but a Non-secure transaction — exactly what CPU1 can only ever issue) is **refused** by the AHBSC. The refusal is a security bus fault whose violating address is `0x20060000`. A permanent Non-secure master (CPU1) can only issue the Non-secure kind, so this demonstrates the CPU1 fence without releasing CPU1. Register reference: AHBSC Secure base `0x50120000`; `RAMH_MEM_RULE` @ +0x180; `ENABLE_SECURE_CHECKING` `0b01`=on; RULE `0b1x` = Secure-required. Details: `examples/frdm_mcxn947/README.md` "E1 — AHBSC TrustZone-AMP fence". ## Verification (kernel graft — the `world` axis, HW-PROVEN 2026-08-31) The fence above is now driven **entirely by the portable kernel API**, with the AHBSC confined to a board file (`examples/frdm_mcxn947/mcxn947_world_board.c`). Island A's Secure image (`mcxn947_world_islandA_s.c`) declares only intent: ```c tk_kernel_set_world(A, TK_WORLD_SECURE, TK_FALLBACK_ABORT); // A protects its own RAM tk_kernel_launch_secondary(1, NS_IMAGE_BASE, TK_FALLBACK_ABORT); // CPU1 = confined external NS image tk_kernel_start(); // tk_world_realize(): board_world_protect(A) then board_core_release(CPU1) ``` - **W3 (benign CPU1 island):** `set_ram`/`set_world`/`launch_secondary` all return OK; the board marks A's SRAMH sub-region Secure and releases CPU1 into its own NS image; the benign island's heartbeat climbs and **the system runs indefinitely with no reset** (SRS = `0x110` WARM|PIN) — the mark+release are correct and a Non-secure island coexists with no false violation. - **W4 (rogue CPU1 island):** the rogue's Non-secure write to A's Secure block (`0x20060000`) at 2 s is **fenced** — `A.sentinel` stays INTACT (which *proves the API-installed mark is active*: a missing mark would let the write land → CORRUPTED, and no violation) — and the part takes its **ITRC security-violation reset** (SRS = `0x40000030` WARM|FATAL|SECVIO), the fail-secure response first seen in G3.2. ⭐ **The unfenceable CPU1 cannot corrupt the trusted island, and the fence is installed with ZERO AHBSC register writes in the application** — the ADR 0005 goal, realized through the portable `world` API. ⚠ **The confined secondary is NOT a `tk_kernel_define` instance.** A hardware run proved why: the secure image is a single-core kernel, so `tk_kernel_define(TK_CORE(1))` rightly rejects CPU1 as off-machine. In dual-image AMP the secondary runs its **own** image — the secure kernel only *launches + confines* it, never schedules it. Hence `tk_kernel_launch_secondary(phys_core, entry, flags)` (a small "external secondary" record), not a scheduler instance; the secure kernel stays genuinely single-core. Data-integrity fence = proven end-to-end through the API. **Graceful containment** (catch the violation, keep A running, quarantine B — instead of the ITRC reset) remains the follow-on: the reset is a separate ITRC tamper-response, plausibly GLIKEY-locked (see the E1 findings), and is arguably the correct fail-secure default. ## Consequences - **The ADR 0003 secondary-core gap is closed in principle** — a unit-less AMP core CAN be fenced, by the system-side class, for silicon that provides one (MCXN947 AHBSC). - **The kernel graft is board-gated and asymmetric** — it needs the board hook (§3), the secure-boot enable + runtime-rule contract (§4), and the k2 MCXN947 integration to become a Secure TrustZone image. That is a distinct implementation phase, deliberately kept out of the portable kernel/arch. - **Security isolation, not merely fault isolation** — because A installs its own protection unilaterally and B cannot lift it, the trusted island is safe from an *unmanaged or malicious* secondary, not only a cooperating-but-buggy one. ADR 0003 and this ADR defend **different threat models** (§2); that is what makes the mixed-criticality guarantee here stronger than a symmetric deny among mutually-trusted islands. - **Different threat model → different, both-correct failure response (W5 conclusion).** The two fence classes fail differently ON PURPOSE: - ADR 0003 **safety** among cooperating islands — a per-core-MPU violation is a **local fault**, caught by the fault handler and contained (`TK_CONFIG_FAULT_CONTAIN` terminates the offending thread), and the system **runs on**. Graceful, app-visible handling — works today on any MPU core, incl. MCXN947 CPU0. - ADR 0005 **security** from a non-cooperating core — a Non-secure-into-Secure access has its data **blocked** at the bus AND trips the part's **ITRC fail-secure reset** (HW-proven, W4/G3.2). For a genuine attack from an untrusted master, fail-secure is the **correct** response, not a limitation. So "catch it gracefully" and "reset fail-secure" are not competing options to reconcile — they are the right answers to two different questions. Containment of a *security* violation (keep A up, quarantine B) would trade fail-secure for availability and is a deliberate, board-specific opt-in, not a default. The ITRC mechanism to attempt it exists (`OUT_SEL` remap → `ITRC0_IRQn`) but is left unpursued by choice. - **The class is two-domain — by silicon, not by choice** (§5): it delivers exactly one trusted + one confined set. ≥3 mutually-isolated islands, and subset-sharing that excludes the trusted core, are architecturally impossible and must be rejected loud, never silently under-delivered. - **Shared memory and AMP/SMP compose cleanly** (§6): the shared window is unmarked NS RAM reached by all; the per-core-keyed-on-island fence makes AMP-beside-SMP (even a heterogeneous SMP island) need no special handling. ## Resolved / remaining 1. ~~Is the secondary core fenceable at all?~~ → not per-core (no MPU); yes system-side (AHBSC), PROVEN. 2. ~~**Kernel integration** — the board hook + Secure-TZ image + island→security mapping + a real 2-island run.~~ → **DONE + HW-PROVEN 2026-08-31** via the portable `world` axis (`tk_kernel_set_world` + `tk_kernel_launch_secondary` → weak seams `board_world_protect` / `board_core_release`); see Verification (kernel graft). The realization is dual-image (Secure CPU0 + separate NS CPU1) and the confined secondary is an external-secondary record, not a scheduler instance. 3. **Generalization** — the seams are mechanism-neutral so a future ownership-class part (C2000 owner/master, backlog G1) can reuse the board-hook path; only MCXN947 AHBSC is proven today. 4. **Two-domain ceiling** — a documented, hard property of the class (§5), not a gap to close: the kernel delivers one trusted + one confined set and rejects any partition that needs a third. 5. ~~**Graceful containment of the security violation**~~ → **RESOLVED as fail-secure-by-design (W5, 2026-09-01).** A Non-secure-into-Secure attack correctly triggers the part's ITRC fail-secure reset; this is the right response to a genuine attack, not a gap (see the failure-response Consequence above). Graceful, app-visible handling already exists where it belongs — for *safety* faults among cooperating islands (ADR 0003 local fault + `TK_CONFIG_FAULT_CONTAIN`). A board-specific opt-in to contain a *security* violation instead of resetting remains possible (ITRC `OUT_SEL` remap → `ITRC0_IRQn`) but is deliberately unpursued: it trades fail-secure for availability on a detected attack. 6. **Kernel-owned SAU regions (stretch, open)** — the kernel can program SAU *regions* at runtime, but the secure-checking *enable* stays boot-gated (GLIKEY / secure boot). A precise boundary, not a blocker.