Portable security & safety API — the explicit-control model¶
Status: design doc / north star (proposed 2026-08-31). Defines the application-facing vocabulary the
kernel exposes for hardware security & safety features, the division of labour between the vendor SDK and
the kernel, and the rule for hardware that lacks a given feature. Extends
sketches/amp/mcxn947/protection-api-discussion.md
and lowering_prototype.c by adding world
(Secure/Non-secure) as a first-class axis and making the kernel — not the SDK — own the hardware setup.
Mechanisms already HW-proven: ADR 0003 (MPU fence),
ADR 0005 (TrustZone/AHBSC fence), G1/G2a
(examples/frdm_mcxn947/).
1. Principle¶
An application engineer controls the hardware’s security & safety features through explicit, visible, human-readable kernel API — in terms of kernels, threads, privilege, cores, affinity, world — not through a vendor config tool that programs the silicon behind the scenes. The same API compiles and runs on any supported architecture (ARM, RISC-V, TriCore, …); each hardware feature (MPU, TrustZone, PMP, …) is an optional capability the API drives where present and refuses or ignores where absent. The kernel owns the programming of that hardware; the vendor SDK is reduced to bare boot.
The invariant is visibility and control, not kernel-exclusivity: the engineer chooses who configures
the silicon — the kernel from main(), the SDK’s GUI tools, or their own pre-kernel register code (§2). What
must never happen is a security-relevant setting buried in a generated file the engineer cannot see, replace,
or override. And the default vocabulary is security-concept-centric and architecture-agnostic — arch-
specific features (e.g. TrustZone’s second world) are explicit, capability-gated extensions, not the base.
2. Who owns the setup — a spectrum, the engineer chooses¶
The kernel does not demand to own the hardware setup; the engineer decides who does. Three modes, all first-class:
Mode |
Who programs SAU/AHBSC/MPU/ITNS/core-release |
Kernel’s role |
|---|---|---|
Kernel-owns |
the kernel, from the app’s API declarations |
owner: programs the partition and takes responsibility for enforcing it |
App/SDK-owns |
the vendor SDK (incl. its GUI config tools) or the app’s own bare-metal register code, before kernel init |
guest: runs on the pre-configured environment, uses what it is given, verifies it (below) |
Hybrid |
some pre-configured, the rest by the kernel |
owner of what it was asked to own; guest for the rest |
So “the kernel owns the programming” means it can — not that it must. Hand it a pre-configured world (an
SDK that generated BOARD_InitTrustZone + per-core main_coreN() entries, à la the TriCore tooling; or
hand-written register setup) and the kernel is a guest: it can be instantiated with no settings at all
— just create threads and schedule them — running within the given partition. Boot minimally instead (clock
reach
main()) and ask the kernel to configure everything, and it takes ownership: it programs the partition from the API declarations and owns responsibility for it. The vendor SDK is reducible to bare boot — not required to be. Either way the partition is visible and controllable — in the engineer’smain()(kernel-owns) or in the engineer’s own pre-kernel setup (app-owns) — never hidden.
Prerequisites: the kernel is given the memory map — it does not invent it¶
In every mode the kernel needs the memory layout handed to it, not guessed. The .ld linker script
defines the areas (secure code, secure RAM, NS code, NS RAM, per-island arenas, the shared window); these
reach the kernel as addresses or linker symbols. The kernel’s job is to enforce the partition on the
hardware and to verify the layout meets its requirements — never to choose the layout. Verification is
fail-fast:
compile time where possible —
static_asserton sizes / alignments / region relationships (a 4 KiB-aligned per-island kernel region; an arena that fits its bank; a shared window outside every arena);runtime, on first execution for what the linker cannot check — probe the actual hardware/security state and report any inconsistency immediately (fail loud), before scheduling a single thread.
So a prerequisite mismatch (a missing symbol, a misaligned region, an arena overlapping the shared window, a
requested world with no linker-provided NS image) surfaces at build or at bring-up — never as a mysterious
fault later. When the kernel takes ownership it also absorbs the job of the SDK’s BOARD_InitTrustZone()
(SAU + AHBSC + ITNS), programming it from these given areas at a kernel init/start hook (linchpin: §9).
3. The vocabulary (first-class axes)¶
Each axis is a named concept with an API surface and a backing hardware capability. Real names exist today unless marked NEW (proposed).
core — a physical execution unit¶
TK_CORE(n) (a mask), arch_core_id(), tk_kernel_owned_cores(k). Each core carries capability flags the
kernel queries (has an MPU? a security extension? a PMP?). Cores are owned by kernels (below), never
addressed directly by app logic beyond affinity.
kernel (instance) — a scheduling domain owning a set of cores¶
tk_kernel_init() (the default instance), tk_kernel_define(cores, name, &err), tk_kernel_start(),
tk_kernel_self(). One core → an AMP island; several cores → an SMP cluster; the two compose (an AMP island
beside an SMP cluster — HW-proven on AURIX TC275). The set of instances is the SMP-vs-AMP decision,
expressed, not configured elsewhere.
thread — a unit of execution inside one kernel¶
tk_thread_create_in(k, fn, stack, size, prio), tk_thread_create_attr_in, tk_thread_spawn_in. A thread
lives in exactly one instance’s pool for its whole life and runs on that instance’s cores (per affinity).
affinity — which of its kernel’s cores a thread may run on¶
tk_thread_set_affinity(t, TK_CORE(..)), tk_thread_pin(t, core). Default = the creating core (a new
thread cannot be grabbed by a peer before it is placed).
privilege — a thread is privileged or unprivileged (capability: MPU / PMP)¶
tk_thread_set_privileged(t); unprivileged is the default under TK_CONFIG_UNPRIV_THREADS. An unprivileged
thread is confined by the core’s MPU/PMP (cannot reach kernel RAM or peripherals; must syscall). Within
one kernel — the boundary between a thread and its own kernel. Enforced identically by whatever unit the
arch has (ARMv8-M MPU, ARMv7-M MPU+SRD, RISC-V PMP+U-mode); the app writes one call, unaware which.
world — Secure or Non-secure (capability: TrustZone-M / TrustZone-A) NEW¶
tk_kernel_set_world(k, TK_WORLD_SECURE | TK_WORLD_NONSECURE) (or a tk_kernel_define argument). A
security domain, assigned to an instance. The kernel programs the SAU + system security controller
(AHBSC) so that instance’s cores execute in that world and its memory carries that attribution. Between
kernels — the coarsest boundary, orthogonal to privilege. The trusted (Secure) instance can observe the
Non-secure one; the Non-secure one cannot reach Secure memory (asymmetric — ADR 0005). This is the axis the
engineer was missing: “this island runs Secure, that one Non-secure,” said in one call.
Naming / scope (DECIDED 2026-08-31: keep
world).worldis intrinsically a second security dimension, orthogonal to privilege — and most arches have only privilege (RISC-V M/S/U, TriCore user/supervisor are privilege ladders, not a second world). Soworldis a capability-specific axis, not a universal one: the vocabulary stays security-concept-centric and arch-agnostic by default, andworldis the deliberate arch-specific extension, capability-gated (§4). The termworldis kept (oversecurity_domain/trust_domain): abstracting the name buys nothing on arches with no second domain, and naming the thing for what it is — TrustZone — is clearer and honest.
isolation — freedom-from-interference between kernels (capability: MPU / paging / world)¶
tk_kernel_set_fence(k, view) / today’s tk_kernel_set_ram(k, base, size); posture ISOLATED / TRUSTED
/ OPEN (see lowering_prototype.c). Declares that an instance’s private RAM must be unreachable by other
instances’ kernels. The kernel lowers the intent onto whatever mechanism the cores have: a per-core
MPU deny-list (symmetric), paging, or — when a peer core has no unit — the world mechanism (mark the
protected instance Secure). So world and isolation meet here: on TrustZone hardware, assigning worlds is
how isolation is delivered; on symmetric hardware, the MPU delivers it and world is absent.
interrupt targeting — which world/privilege an ISR runs in (capability: NVIC-ITNS / TrustZone) NEW¶
tk_irq_set_world(irqn, world). Which world handles an interrupt (NVIC->ITNS), owned by the kernel rather
than the SDK’s generated table. Absent TrustZone, collapses to “all interrupts one world.”
4. The capability rule — loud vs collapse¶
Every axis binds to a hardware capability that may be absent. Two distinct behaviours, never a silent lie:
You explicitly request what the silicon cannot do → LOUD.
tk_thread_set_privilegedon a core with no MPU, ortk_kernel_set_world(SECURE)on RISC-V/TriCore, returnsTK_ERR_UNSUPPORTED; a requested island fence that cannot be installed fails closed (tk_instance_fence_reject). The app learns the truth.You don’t use an axis → COLLAPSE. No world declared → no TrustZone programming, byte-identical build. No fence declared → no fence. The feature adds nothing when unused. (This is the kernel’s existing collapse property +
ARCH_HAS_*capability flags.)
Fail vs ignore is the engineer’s call, per intent (the open question you raised). For cross-architecture source, “request an arch-specific feature on a part that lacks it” should sometimes be fatal and sometimes a graceful skip — only the engineer knows whether the feature is load-bearing for their safety case:
a required intent (
world = SECUREbecause the partition is only safe with TrustZone) → LOUD: the setup call reportsTK_ERR_UNSUPPORTED, and if the engineer relies on it the enforcement point fails closed. Running weaker-than-asked is never silent.a best-effort intent (
use TrustZone if present, else the MPU fence, else run flat) → graceful no-op; the kernel delivers the strongest boundary the part supports and reports what it did.
DECIDED (2026-09-01): the fallback flag names the OUTCOME, tk_fallback_t = TK_FALLBACK_ABORT / DEGRADE.
The earlier BEST_EFFORT wording was rejected as actively wrong for this domain: it implies a probabilistic
boundary (“we’ll try to keep you isolated”), whereas a security/safety boundary is always present (1) or
absent (0) — a TK_FEAT_ISOLATION-style binary, never a maybe. This flag is NOT fuzzy enforcement; it is a
setup-time decision over the platform’s BINARY answer to “can I provide this boundary?”. A capability-gated
call takes it explicitly:
TK_FALLBACK_ABORT(the default — fail-safe: forget to think about it and you get the safe behaviour): the platform’s “no” is a hard stop —TK_ERR_UNSUPPORTEDat setup, fail-closed (park the core) atstart. Safety-load-bearing calls use this. (HW-proven: W-fail.)TK_FALLBACK_DEGRADE(opt-in for portable source): the platform’s “no” means run in a defined degraded mode and emit a diagnostic — never silent. The achieved state is queryable (tk_kernel_worldreturns what the platform actually delivered, never the requested world), so the boundary is honestly 0, not pretended 1. (HW-proven: W-besteffort.)
The boundary itself, and the achieved state, stay binary and readable at all times; the flag only chooses what
to do about a platform “no”. The raw status return remains for engineers who want to branch themselves; and
any declared isolation guarantee that cannot be delivered fails closed at start under ABORT.
So “any ARM hardware that supports (or optionally supports) MPU / TrustZone” is exactly this: the API is one surface; each call is honoured, refused loud, or collapsed, per the core’s capability flags — never mis-mapped to a weaker mechanism.
5. The kernel owns the hardware setup¶
The kernel programs the security hardware from the declared configuration, through arch/board seams, at
tk_kernel_start() (and a pre-start setup hook) — not a generated SystemInitHook. Seams:
arch seam (portable, per-arch):
arch_instance_fence_apply(...)(per-core MPU/PMP/paging deny),arch_thread_region_set(...)(per-thread privilege region), andarch_self_is_unprivileged()etc.board/SoC seam (non-architectural silicon): a
board_security_setup(config)+board_instance_fence_*that programs SoC IP — on MCXN947 the SAU + AHBSC + NVIC-ITNS + core-release, i.e. whatBOARD_InitTrustZonedoes today, but driven by the kernel from the app’sworld/shared/isolation declaration. No MCXN947 register poking enters the portable kernel/arch (ADR 0005 §3).
The vendor SDK’s BOARD_InitTrustZone() is thereby retired from the boot path; its knowledge moves into the
board seam the kernel calls. The engineer sees the partition declared in main(); the kernel makes it real.
6. One image or two — the build-shape a world split forces¶
world is the one axis that can change the number of binaries, because a memory region is Secure or
Non-secure and a core executes in the world of the code it fetches:
No
worldsplit (symmetric board, or all-Secure): ONE image. SMP or multi-instance AMP live in a single binary with a singlemain(); the kernel schedules/partitions inside it. (Plain MCXN947 SMP/AMP demos; every non-TrustZone arch.)A
worldsplit (TrustZone, Secure + Non-secure instances): TWO images — a Secure image (the trusted instance, owns the setup + programs the partition) and a Non-secure image (the confined instance’s threads). The Securemain()declares the whole partition and releases the NS core into the NS image (G2a). The API spans two binaries of one project; it does not pretend they are one.
The API is identical in both shapes; only the build/link output differs, decided by whether a world split
was declared. This is a property to surface, not hide.
7. Worked examples (the engineer’s scenarios)¶
(a) SMP with privileged + unprivileged threads — any MPU arch, one image
main() { // one entry; kernel schedules across cores
tk_kernel_init(); // one kernel, owns all cores (SMP)
t = tk_thread_create(worker, ...); // unprivileged by default (UNPRIV build)
tk_thread_set_privileged(mon); // this one privileged
tk_kernel_start(); // MPU/PMP enforces privilege per thread
}
Runs unchanged on ARMv7-M, ARMv8-M, RISC-V — the arch’s unit implements privilege; the app is unaware which.
(b) AMP — one kernel per core (+ optional SMP cluster), one image
tk_kernel_init(); // island A: core 0
B = tk_kernel_define(TK_CORE(1), "B", 0); // island B: core 1 (AMP)
C = tk_kernel_define(TK_CORE(2)|TK_CORE(3), "C", 0); // island C: cores 2-3 (SMP cluster)
tk_kernel_set_ram(A, arenaA, sizeA); // isolation: each island's private RAM
tk_kernel_set_ram(B, arenaB, sizeB);
tk_kernel_start(); // per-core MPU fences applied
(c) AMP + TrustZone — Secure + Non-secure islands, two images
// Secure image, core 0 — the ONLY security-relevant boot is: hand control to the kernel
main() {
tk_kernel_init(); // island A: core 0
B = tk_kernel_define(TK_CORE(1), "B", 0); // island B: core 1
tk_kernel_set_world(A, TK_WORLD_SECURE); // explicit: A is Secure
tk_kernel_set_world(B, TK_WORLD_NONSECURE); // explicit: B is Non-secure
tk_kernel_add_shared(mailbox, mbsize); // the open window
tk_kernel_start(); // kernel programs SAU+AHBSC+ITNS, marks A Secure, releases core1 into the NS image
}
On a board without TrustZone the two set_world calls return TK_ERR_UNSUPPORTED (loud) — the app knows
this partition needs TrustZone. On MCXN947 the kernel owns every register BOARD_InitTrustZone touches.
8. Portability matrix (illustrative)¶
arch |
privilege (MPU/PMP) |
world (TrustZone) |
isolation mechanism |
|---|---|---|---|
ARM v8-M (M33) |
✅ PMSAv8 MPU |
✅ TrustZone-M + SAU |
MPU deny, or world (AHBSC) for a unit-less peer |
ARM v7-M (M3/4/7) |
✅ PMSAv7 MPU |
❌ ( |
MPU deny |
ARM v6-M (M0+) |
❌ ( |
❌ |
none (collapse) |
ARM-A / R (VMSA / PMSAv8-R) |
✅ |
⚠ TrustZone-A (different) |
paging / region deny |
RISC-V |
✅ PMP + U-mode |
❌ |
PMP/paging deny |
TriCore (AURIX) |
✅ DPR + User mode |
❌ |
DPR deny |
C2000 / MSP430 / host |
❌ |
❌ |
none (collapse) |
The app writes the same calls; this table is what each honours, refuses, or collapses.
9. Where this stands — proven, and the linchpin¶
Proven mechanisms: privilege via MPU/PMP (multi-arch, HW); MPU island fence (ADR 0003, 4 arches HW); a kernel running in the Secure world + marking AHBSC rules at runtime (G1, HW); a Secure core releasing the other core into Non-secure code that runs (G2a, HW).
The linchpin — PROVEN (GL, HW): the kernel owns the TrustZone setup —
BOARD_InitTrustZoneruns frommain(), not the SDK’sSystemInitHook(self-verified: MISC_CTRL0x86AA→0x86A6). “The SDK secretly configures security” is now “the kernel controls it, visibly, frommain().”The
worldaxis — PROVEN end-to-end (W3/W4, HW 2026-08-31). The axis is now real calls:tk_kernel_set_world(k, TK_WORLD_SECURE|NONSECURE, flags)and — for a dual-image confined secondary that runs its OWN image —tk_kernel_launch_secondary(phys_core, entry, flags). Attk_kernel_startthe kernel lowers them onto two weak board seams (board_world_protect= the self-mark;board_core_release= the secondary release), the single portable-kernel change (ADR 0005 §3), capability-gated + collapse-safe (fence matrix PASS, RISC-V fence demo still holds,TK_FEAT_WORLDoff = byte-identical). On FRDM-MCXN947: the Secure island A marks its own RAM and launches the Non-secure CPU1 into its image with zero AHBSC in the app; a benign CPU1 island coexists indefinitely (W3), and a rogue CPU1’s Non-secure write to A’s Secure block is fenced with A’s data INTACT (W4). ⭐ A HW run also corrected the model: a confined secondary is NOT atk_kernel_defineinstance (that would carve a scheduler core of the secure kernel’s own machine) — it is an external secondary the secure kernel launches + confines, so the secure kernel stays single-core.Remaining:
tk_kernel_add_shared/tk_irq_set_worldas first-class calls; graceful containment of a violation (today the part takes an ITRC fail-secure reset — W5/ITRC follow-on); kernel-owned SAU region programming (the checking enable stays boot-gated per the GLIKEY findings).
The measure of done: an engineer partitions a multicore ARM part across Secure/Non-secure worlds, SMP/AMP instances, and privileged/unprivileged threads entirely through this vocabulary, and the same source compiles to a weaker-but-honest configuration on a part that lacks MPU or TrustZone — with the vendor SDK doing nothing but booting the clock.