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 theRAMx_MEM_RULEregisters. 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-
ISOLATEDislands” 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.
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.RULE0written Secure at runtime (read back0x2).Secure access allowed, Non-secure access blocked: CPU0’s access to the marked SRAMH block via the Secure alias
0x30060000returns the sentinel; its access via the Non-secure alias0x20060000(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 is0x20060000.
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_secondaryall 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 =0x110WARM|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.sentinelstays 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 =0x40000030WARM|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_CONTAINterminates 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_SELremap →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¶
~~Is the secondary core fenceable at all?~~ → not per-core (no MPU); yes system-side (AHBSC), PROVEN.
~~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
worldaxis (tk_kernel_set_world+tk_kernel_launch_secondary→ weak seamsboard_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.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.
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.
~~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 (ITRCOUT_SELremap →ITRC0_IRQn) but is deliberately unpursued: it trades fail-secure for availability on a detected attack.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.