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

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.