/* ============================================================================ * sketches/amp/mcxn947/lowering_prototype.c * * DISCUSSION PSEUDO-CODE — NOT built, NOT wired into the tree, NO register pokes. * Companion to protection-api-discussion.md. Its job is to make ONE idea concrete: * * the application declares INTENT (a tk_fence_view per island); * the KERNEL lowers that intent onto whatever mechanism the core it is * actually running on happens to have — region-MPU, paging, system-side * bus attribution, or nothing — and fails LOUD when the hardware cannot * deliver what was asked. * * The same app code (§6) lands as a mutual per-core deny on a symmetric board and * as an asymmetric "mark-my-arena-Secure" on the MCXN947 (CPU1 has no MPU). The app * never mentions AHBSC, MPU, PMP, SAU, or Secure aliases — that vocabulary lives only * in the backends (§4). * * Real symbols referenced as they exist in the tree today: * struct tk_kernel { ... uintptr_t fence_base, fence_size; unsigned char trusted; } * int arch_instance_fence_apply(const uintptr_t* deny_base, const uintptr_t* deny_size, uint32_t ndeny) * void tk_instance_fence_reject(uint32_t core) // weak, fail-closed * uint32_t arch_core_id(void) * Names introduced by THIS sketch (proposed, do not exist yet) are prefixed /*NEW*/. * ============================================================================ */ #include #include /* =========================================================================== * 1. APP-FACING INTENT (kernel-level API — the ONLY thing the app sees) * =========================================================================== * A posture is a RELATIONSHIP, not an operation. It says nothing about MPU regions * or Secure bits — it says what this island's trust stance toward its peers IS. The * kernel derives the mechanism (§5) from posture + the core's capabilities (§2). */ typedef enum { TK_FENCE_OPEN = 0, /* "the street": shared, no isolation. Collapse default. */ TK_FENCE_ISOLATED, /* mutual freedom-from-interference with peers — the portable ideal */ TK_FENCE_TRUSTED /* may OBSERVE all peers; own arena still protected FROM them */ /* (a monitor island; or the Secure bus-master on an asym. board) */ /* Deliberately NOT an app posture: "CONFINED". Confinement is a LOWERING OUTCOME * (§5), not an intent — a core discovers it can only be fenced from the outside. * Keeping it out of the enum is what keeps the app portable: island B asks for * ISOLATED on every board; the kernel decides B ends up confined on the MCXN947. */ } /*NEW*/ tk_fence_posture_t; typedef struct /*NEW*/ tk_fence_view { void* arena_base; /* the RAM this island owns (32-byte aligned) */ size_t arena_size; tk_fence_posture_t posture; /* INTENT. never a mechanism knob. */ /* Deliberately tiny. Every mechanism-specific attribute (cacheability, shareability, * TrustZone NSC, PMP granularity, MMU domain id, DPR index...) stays INSIDE the * backend that understands it — never in this crossing struct. Bloat here is the * single failure mode that turns this from a real API into a lab curiosity. */ } tk_fence_view_t; /* [setup] Record island k's fence intent. A strict generalization of today's * tk_kernel_set_ram(k, base, size), which is exactly this with posture=ISOLATED. * Records into the kernel struct now; NOTHING touches hardware until start (§5). */ /*NEW*/ int tk_kernel_set_fence(struct tk_kernel* k, const tk_fence_view_t* v); /* =========================================================================== * 2. WHAT THE RUNNING CORE CAN DO (capability, queried at start — never #ifdef) * =========================================================================== * The lowering pass is written ONCE and is arch-blind; it asks the core what class of * unit it has. Each port fills this in. This is how "consider every architecture" * becomes data, not a switch statement in the kernel. */ typedef enum { TK_FCLASS_NONE = 0, /* this core has no unit that can bind its own kernel */ TK_FCLASS_REGION, /* base/limit protection regions (small fixed budget) */ TK_FCLASS_PAGING /* address translation (effectively unlimited "regions") */ } /*NEW*/ tk_fence_class_t; typedef struct /*NEW*/ tk_core_caps { tk_fence_class_t per_core; /* the core's OWN unit — REGION / PAGING / NONE */ uint32_t region_budget; /* usable regions for REGION class (else ~inf) */ unsigned char system_side; /* a board/SoC bus-attribution mechanism exists */ unsigned char secure_master; /* THIS core may MARK regions (trusted attributor) */ unsigned char confined_ns; /* THIS core is a permanent, unit-less NS master */ } tk_core_caps_t; /* Filled by the port for the calling core. (Board caps — system_side/secure_master/ * confined_ns — come from a board hook the port defers to; arch caps from the CPU.) */ /*NEW*/ tk_core_caps_t arch_core_caps(void); /* --------------------------------------------------------------------------- * The table this query stands in for, across EVERY supported target: * * arch / target per_core how the backend (§4) actually enforces a deny * ------------------ -------- --------------------------------------------- * arm_v8m (M33) REGION(8) PMSAv8 RBAR/RLAR, MAIR attrs, no PRIVDEFENA * └ MCXN947 CPU0 REGION(8) ...AND secure_master=1 (owns AHBSC, Secure world) * └ MCXN947 CPU1 NONE MPU_TYPE=0 → confined_ns=1 (permanent NS master) * arm_v7m (M3/4/7) REGION(8) PMSAv7 regions + SRD sub-region disable + zones * arm_v8r (R52) REGION PMSAv8-R PRBAR/PRLAR @EL2 (⚠ AP=0b00 quirk) * tricore (TC275) REGION(16) DPR data-protection ranges, class-1 trap * risc_v (RV32) PAGING Sv32 page tables (or PMP → REGION on no-MMU parts) * arm_v7a (A-class) PAGING VMSA tables + DACR domains (⚠ domain0=client) * arm_v8a (A-class) PAGING VMSA tables, map-all then punch 4 KiB holes * arm_v6m (M0+) NONE no MPU on this part → cannot bind its own kernel * c2000 (C28x) NONE single-core, owner/master only (backlog G1) * host / pthread NONE no HW boundary at all * * system_side is a SEPARATE axis owned by the board, not the CPU: * └ MCXN947 board system_side=1 AHBSC RAMx_MEM_RULE (bus-master attribution) * └ every other board system_side=0 (none proven yet; C2000 owner/master is a future one) * --------------------------------------------------------------------------- */ /* =========================================================================== * 3. MECHANISM-NEUTRAL PLAN (what the kernel hands the backends) * =========================================================================== * The kernel translates postures into two facts a backend can act on WITHOUT knowing * postures: a deny-set (regions THIS core must be kept out of) and self (this island's * own arena, for the mark-based path). Both directions of protection are named here. */ typedef struct { uintptr_t base, size; } tk_region_t; typedef struct /*NEW*/ tk_fence_plan { /* Direction 1 — "keep ME out of peers": regions this core must not reach. * Peers' app arenas (set_fence) AND their kernel structs (D2/ADR 0004). */ tk_region_t deny[8]; uint32_t ndeny; /* Direction 2 — "keep PEERS out of ME": my own regions, and whether they need a * SYSTEM-SIDE mark because some untrusted peer sits on a unit-less core and so * cannot fence itself away from me. BOTH the app arena AND the kernel struct must be * markable — protecting the arena but not the TCBs (D2/ADR 0004) would leave a rogue * NS core free to corrupt scheduler state (§7 finding). */ tk_region_t self_arena; /* pseudo: = me->fence_base / fence_size */ tk_region_t self_kstruct; /* pseudo: = &me / TK_CONFIG_KERNEL_REGION_SIZE (4 KiB) */ unsigned char self_needs_mark; /* set by build_plan() when an untrusted peer is unit-less */ unsigned char self_open; /* posture==OPEN → self wants no protection at all */ } tk_fence_plan_t; /* =========================================================================== * 4. THE TWO BACKENDS (the seams — the ONLY place mechanism words appear) * ===========================================================================*/ /* 4a. PER-CORE seam (symmetric). This is today's arch_instance_fence_apply, restated * against the plan. Delivers Direction 1: keep this core out of plan->deny. */ static int arch_fence_install(const tk_fence_plan_t* p, const tk_core_caps_t* caps) { if (p->ndeny == 0u) return 1; /* nothing to keep me out of → trivially done */ if (caps->per_core == TK_FCLASS_NONE) return 0; /* no unit → I cannot fence myself (fall to §4b) */ if (caps->per_core == TK_FCLASS_REGION && p->ndeny > caps->region_budget) return 0; /* Q1 bites HERE: more neighbour arenas than region slots → honest 0, not silent drop */ switch (caps->per_core) { case TK_FCLASS_REGION: /* v8-M : for each deny[i] write an RBAR/RLAR pair, no-access, MAIR-indexed. * v7-M : cover SRAM with one region, DISABLE the sub-regions that overlap deny[i]. * v8-R : PRBAR/PRLAR @EL2 (AP=0b00). * tricore: program a DPR range per deny[i], set the range's disable bits. * === this is literally the body of the existing arch_instance_fence_apply === */ /* arch_instance_fence_apply(&p->deny[..].base, &p->deny[..].size, p->ndeny); */ return 1; case TK_FCLASS_PAGING: /* risc_v Sv32 / arm VMSA: identity-map, then UNMAP (punch a hole for) each deny[i]. * No small budget → the ndeny>budget check above never trips for PAGING. */ return 1; default: return 0; } } /* 4b. SYSTEM-SIDE seam (asymmetric). BOARD/SoC layer ONLY (AHBSC is NXP IP, not ARMv8-M — * it must NOT enter portable arch). The first draft was ONE overloaded call; the §7 * pressure test showed that hid a whole direction, so it is split in two. Weak defaults * return 0 = "this board has no such mechanism". */ /* Direction 2, for a Secure attributor (MCXN947 CPU0): MARK my arena AND my kernel struct * Secure, so a permanent-NS peer faults at the bus reaching EITHER. Marking only the arena * would leave the TCBs/scheduler (D2/ADR 0004) exposed to a rogue NS core. */ static int board_mark_self_secure(const tk_fence_plan_t* p, const tk_core_caps_t* caps) { if (!caps->system_side || !caps->secure_master) return 0; /* AHBSC: RAMx_MEM_RULE sub-regions covering p->self_arena AND p->self_kstruct → RULE * 0b1x (Secure). enable-at-secure-boot / rules-at-runtime (ADR 0005 §4, fence_s demo). * The D2 kernel region is already 4 KiB-aligned → lands on the AHBSC rule granularity. */ (void)p; return 1; } /* Direction 1, for a unit-less confined core (MCXN947 CPU1): I install nothing (no unit), * but I am only TRULY confined if EVERY region in my deny-set is Secure-marked by its owner. * A deny target whose owner is NOT a Secure attributor is NOT held away from me → return 0 * so the caller fails loud instead of trusting an unenforced boundary. */ static int board_confines_me(const tk_fence_plan_t* p, const tk_core_caps_t* caps) { if (!caps->system_side || !caps->confined_ns) return 0; for (uint32_t i = 0u; i < p->ndeny; i++) { /* if (!ahbsc_region_is_secure(p->deny[i])) return 0; — a non-Secure deny target * means nothing holds me out of it; refuse to pretend I'm confined from it. */ } return 1; /* every deny target is Secure; the bus confines me. */ } /* =========================================================================== * 5. THE LOWERING PASS (the heart — written once, arch-blind) * =========================================================================== * Replaces the single arch_instance_fence_apply() call inside tk_instance_fence_enter(). * Runs ON each core as it enters its island, before it schedules any thread. */ /* helpers the real version reads out of the instance table (sketched): */ extern struct tk_kernel* s_all_islands[]; /* default + every tk_kernel_define'd island */ extern uint32_t s_island_count; extern tk_fence_posture_t island_posture(const struct tk_kernel*); /* from tk_kernel_set_fence */ extern int island_core_is_unit_less(const struct tk_kernel*); /* its core has per_core==NONE */ extern uint32_t arch_core_id(void); extern void tk_instance_fence_reject(uint32_t core); /* Turn every island's POSTURE into the mechanism-neutral plan for the core entering `me`. */ static void build_plan(const struct tk_kernel* me, tk_fence_plan_t* p) { tk_fence_posture_t my_posture = island_posture(me); p->ndeny = 0u; p->self_needs_mark = 0u; p->self_open = (unsigned char)(my_posture == TK_FENCE_OPEN); p->self_arena.base = 0u; p->self_arena.size = 0u; /* pseudo: = me->fence_base / fence_size */ p->self_kstruct.base = 0u; p->self_kstruct.size = 0u; /* pseudo: = (uintptr_t)me / KERNEL_REGION_SIZE */ for (uint32_t i = 0u; i < s_island_count; i++) { const struct tk_kernel* k = s_all_islands[i]; if (k == me) continue; tk_fence_posture_t kp = island_posture(k); /* Direction 1: I must be kept OUT OF peer k — unless I am TRUSTED (I may see all), * OR k is OPEN (k's arena is a SHARED WINDOW everyone may reach — §8). Posture thus * matters on BOTH sides: mine (the accessor) AND k's (the target). Deny k's app arena * (if any) AND its kernel struct (D2 — always); never deny a shared window. */ if (my_posture != TK_FENCE_TRUSTED && kp != TK_FENCE_OPEN) { /* p->deny[p->ndeny++] = k->fence_base/size (if k->fence_size) */ /* p->deny[p->ndeny++] = { (uintptr_t)k, TK_CONFIG_KERNEL_REGION_SIZE } (always) */ p->ndeny += 1u; /* pseudo-count; real code appends the two regions above */ } /* Direction 2: is peer k UNTRUSTED and on a UNIT-LESS core? Then k cannot fence * itself away from me, so *I* must be protected system-side (self-mark). */ if (my_posture != TK_FENCE_OPEN && kp != TK_FENCE_TRUSTED && island_core_is_unit_less(k)) { p->self_needs_mark = 1u; } } } static void tk_instance_fence_enter(const struct tk_kernel* me) { tk_core_caps_t caps = arch_core_caps(); tk_fence_plan_t plan; build_plan(me, &plan); int d1_required = (plan.ndeny > 0u); /* keep ME out of peers */ int d2_required = (plan.self_needs_mark != 0u); /* keep unit-less peers out of ME */ /* Collapse: no isolation holes needed on this core. (PAGING caveat, §7: "no fence" is * NOT "no page table" — a VMSA arch still needs its base identity map just to run; that * belongs to the arch backend, not to this lowering.) */ if (!d1_required && !d2_required) { return; } /* ---- Direction 1: keep ME out of peers ---- */ int d1_ok = 1; if (d1_required) { d1_ok = arch_fence_install(&plan, &caps); /* my own unit denies them */ if (!d1_ok && caps.confined_ns) { /* No unit — but if every region I must avoid is held Secure by its owner, the * BUS keeps me out (MCXN947 CPU1). Deliverable only if that actually holds. */ d1_ok = board_confines_me(&plan, &caps); } } /* ---- Direction 2: keep unit-less peers out of ME ---- */ int d2_ok = 1; if (d2_required) { /* ONLY a Secure attributor can mark my regions Secure. A plain NS core — WITH a unit * or without — cannot, because marking its own RAM Secure would lock ITSELF out * (Secure is invisible to an NS master). THIS is the 2-domain limit (§7 Case 1). */ d2_ok = caps.secure_master ? board_mark_self_secure(&plan, &caps) : 0; } /* Honest floor: EACH required direction must be delivered ON ITS OWN. They protect * DIFFERENT things (peers-from-me vs me-from-peers) and are NOT substitutable — the §7 * fix. A successful d1 must NEVER mask a failed-but-required d2 (that was the bug: a * third NS island whose deny installs fine yet cannot be protected from a rogue B). */ if ((d1_required && !d1_ok) || (d2_required && !d2_ok)) { tk_instance_fence_reject(arch_core_id()); } /* NOTE (honest-reporting, Q4): island B on MCXN947 asked ISOLATED but only gets * "B cannot reach A"; "A cannot reach B" is not provided (A is the trusted master). * Partial delivery, not failure — a real build reports it "downgraded", never pretends. */ } /* =========================================================================== * 6. WORKED EXAMPLE — the SAME app source, two very different boards * ===========================================================================*/ /* --- (a) MCXN947: dual M33, CPU1 has no MPU/SAU/TrustZone ------------------- * * A = default island on CPU0 (secure_master), B = island on CPU1 (confined_ns). * The app writes pure INTENT. It contains no AHBSC, no Secure alias, no "mark". */ extern struct tk_kernel* tk_kernel_default(void); extern struct tk_kernel* tk_kernel_define(uint32_t owned_cores, const char* name, void* out); extern void tk_kernel_init(void); extern void tk_kernel_start(void); #define TK_CORE(n) (1u << (n)) static uint8_t arenaA[4096], arenaB[4096]; static void amp_main_mcxn947(void) { tk_kernel_init(); struct tk_kernel* A = tk_kernel_default(); struct tk_kernel* B = tk_kernel_define(TK_CORE(1), "B", 0); tk_fence_view_t va = { arenaA, sizeof arenaA, TK_FENCE_ISOLATED }; tk_fence_view_t vb = { arenaB, sizeof arenaB, TK_FENCE_ISOLATED }; tk_kernel_set_fence(A, &va); /* both just say "ISOLATED" — no board knowledge */ tk_kernel_set_fence(B, &vb); /* tk_thread_create_in(A, ...); tk_thread_create_in(B, ...); */ tk_kernel_start(); /* HOW IT LOWERS (from §5, given the §2 caps): * core CPU0 enters A: caps={REGION(8), secure_master=1, system_side=1} * build_plan(A): my_posture ISOLATED; peer B untrusted & unit-less * → ndeny += B(arena+kstruct); self_needs_mark = 1 * d1: arch_fence_install → REGION denies B from A (d1_ok=1) * d2: board_mark_self_secure → MARK A's arena+kstruct Secure (d2_ok=1) * ⇒ A kept out of B by its MPU, AND A protected from B at the bus. * core CPU1 enters B: caps={NONE, confined_ns=1, system_side=1} * build_plan(B): peer A is ISOLATED → ndeny += A. self_needs_mark=0 * (A has a unit, so A fences itself; no unit-less untrusted peer). * d1: arch_fence_install → per_core NONE → 0; confined_ns → * board_confines_me: A's arena+kstruct held Secure by A (d1_ok=1) * d2: not required. * ⇒ B confined by the bus; NO reject. B cannot touch A. * Result: A is safe from a rogue B. (B is NOT protected from A — the asymmetric * truth of this silicon; §5 would report B's ISOLATED as downgraded.) */ } /* --- (b) a symmetric board (e.g. dual-core RV32, or an M7 SMP-subset) -------- * * BYTE-IDENTICAL app source. Only the caps differ, so the lowering differs. */ static void amp_main_symmetric(void) /* same body as above, different target */ { /* core X enters A: caps={PAGING or REGION, secure_master=0, system_side=0} * build_plan(A): peer B untrusted, NOT unit-less → self_needs_mark=0; ndeny += B * arch_fence_install: deny B from A (d1_ok=1) → done * core Y enters B: symmetric — deny A from B (d1_ok=1) → done * Result: MUTUAL isolation. board_fence_install never even consulted. * The app said "ISOLATED / ISOLATED" and got the full, symmetric guarantee. */ (void)amp_main_mcxn947; /* silence unused in this pseudo-file */ } /* =========================================================================== * 7. PRESSURE TEST — does posture{OPEN,ISOLATED,TRUSTED} + the lowering survive * contact with harder partitions? One case found a real bug; one held. * =========================================================================== * * CASE 1 — THREE islands; a third that also needs protection from the unit-less core. * Hypothetical 3-core security-attribution SoC, all three declared ISOLATED: * A = CPU0 (REGION, secure_master) B = CPU1 (NONE, confined_ns) * C = CPU2 (REGION, NS, secure_master=0) * Lowering C: d1 (deny A,B on C's own MPU) SUCCEEDS; d2 REQUIRED (B is an untrusted * unit-less peer) but C is not a Secure attributor → d2 UNDELIVERABLE. * • BUG FOUND: the first draft rejected only on (!d1_ok && !d2_ok), so C's good d1 * MASKED its failed d2 — C would have run UNPROTECTED from a rogue B. FIX (now §5): * the two directions are INDEPENDENT; reject if EITHER required one is undelivered. * C now correctly rejects. * • DEEPER TRUTH: a security-attribution mechanism has exactly TWO domains (Secure * vs NS). Two NS islands (B and C) CANNOT be isolated from each other — marking C's * arena Secure to hide it from B would also hide it from C itself. So "three * mutually-ISOLATED islands" is UNACHIEVABLE on this class; the honest result is a * reject with a precise DET ("system-side fence = 2 domains"). REGION/PAGING have no * such limit — each core denies all N peers. → this belongs in the DET text and docs. * * CASE 2 — a TRUSTED monitor that must read every island. HELD, on both board types: * Symmetric board: build_plan(M) → ndeny=0 (TRUSTED sees all), self_needs_mark=0 (no * unit-less peer) → COLLAPSE; M installs nothing, yet the peers still deny M's kstruct * when THEY enter, so M's own state stays protected. ✓ (this is today's set_trusted.) * MCXN947: M on CPU0 (secure_master), B on CPU1 (unit-less). ndeny=0 BUT self_needs_mark=1 * → does NOT collapse; d2 marks M's arena+kstruct Secure so a rogue B cannot corrupt the * monitor. ✓ Validates the collapse guard being (ndeny==0 && !self_needs_mark), not * just (ndeny==0) — the monitor is the case that would break the simpler guard. * * TWO SMALLER THINGS THE TEST SURFACED: * (i) caps.confined_ns is DERIVABLE = (per_core==NONE && !secure_master); it is kept as a * field only for readability. The genuinely independent axes are "has a per-core * unit?" and "can it mark / what domain is it?" — keep them orthogonal, never rebundle. * (ii) the self-mark MUST cover arena AND kernel struct (fixed in §3/§4b); marking only the * arena would leave the TCBs open. The 4 KiB D2 kernel region conveniently aligns to * the AHBSC rule granularity, so both are markable at the same sub-region size. * =========================================================================== */ /* =========================================================================== * 8. PRESSURE TEST II — a shared window (4th posture), and exotic AMP/SMP topologies. * =========================================================================== * * (4th posture) SHARED WINDOW — an OPEN region that BOTH A and B must reach. * • MODEL SMELL: a shared window is passive MEMORY, not a scheduling instance — yet posture * rides on an ISLAND (which owns cores + threads). Declaring the window as an "OPEN island * with no core" fights the island-owns-cores model. Cleaner: a first-class shared region, * e.g. tk_kernel_add_shared(base, size), granted by every island's fence — NOT an island. * (Left as an OPEN posture below only to show the lowering fix it forced.) * • BUG FOUND: build_plan built the deny-list from the ACCESSOR's posture alone and denied * EVERY peer's arena — so it would have walled everyone off from the shared window. FIX * (§5): an OPEN peer's arena is a shared window → excluded from every deny-list. Posture * now matters on BOTH sides. (Symmetric to §7's "directions are independent" fix: §7 was * the reject logic, this is the deny-list construction.) * • ASYMMETRIC BOARD: the window "just works" by staying NS. You only ever MARK Secure, * never mark NS; A(Secure) and B(NS) both reach unmarked NS RAM. No special-casing, and * ZERO region cost — a grant is the ABSENCE of a hole, not a region. * • SECURITY NOTE: a shared window is a SANCTIONED interference channel. FFI protects * PRIVATE memory only; the trusted island must treat whatever B writes into S as * untrusted input (validate on read), exactly like an MMIO mailbox. * • OPEN CONFLATES TWO AXES: "my arena is shared" (visibility) vs "I am unconfined" * (access). The three postures are 3 corners of {PRIVATE|SHARED} x {CONFINED|UNCONFINED}: * ISOLATED = private + confined OPEN = shared + confined * TRUSTED = private + unconfined (4th corner shared+unconfined = a flat/legacy core) * Harmless while OPEN is only a PASSIVE window; if an OPEN island ever gets threads, * decide explicitly that it stays CONFINED (a non-critical island must not scribble a * critical one). Another reason to model the window as a region, not a posture. * * EXOTIC TOPOLOGY — core0 AMP-isolated + cores 1/2/3 as ONE SMP island. * P = island{core0}, ISOLATED; Q = island{core1,core2,core3}, ISOLATED. * • HOLDS, unchanged. The fence is PER-CORE but keyed on ISLAND identity: cores 1/2/3 each * run build_plan(Q=me) → they SHARE Q's arena (self, never denied) and each denies P; * core0 runs build_plan(P) → denies Q. Mutual isolation, SMP cores share freely. * • WHY IT COMPOSES FOR FREE: s_all_islands is per-INSTANCE, so an SMP island is ONE peer * entry no matter how many cores it owns — the loop already does the right thing. * • PAGING note: SMP cores usually share one TTBR → per-core install is idempotent (same * table); REGION cores each own their MPU → per-core install is required. Both are correct. * * EXOTIC + ASYMMETRIC — an SMP island STRADDLING a unit-less core. * P = core0 (REGION, secure_master, ISOLATED); * Q = {core1 REGION, core2 REGION, core3 NONE/confined_ns}, ISOLATED. * • Q's cores enforce Q-vs-P DIFFERENTLY yet coherently: core1/2 deny P via their MPUs; * core3 (unit-less) is confined from P BY THE BUS — because Q's unit-less core3 makes * self_needs_mark(P)=1, so P marks itself Secure. Mixed enforcement inside ONE island * composes. (Real SMP wants homogeneous cores — coherency, shared tables — a constraint * that lives ABOVE this fence layer; the lowering itself raises no objection.) * * SELECTIVE SHARING (a window visible to a SUBSET) — the 2-domain wall returns: * region/paging give N domains, so S_AB (only A,B) and S_BC (only B,C) each cost a hole and * work. On the system-side class a Secure master sees ALL NS RAM, so you CANNOT hide an NS * shared window from the Secure island — subset-sharing that excludes the trusted core is * unachievable there. Same ceiling as §7 Case 1, now for sharing instead of isolation. * =========================================================================== */ /* =========================================================================== * 9. PRESSURE TEST III — the per-core-keyed-on-island invariant ITSELF. * Two attacks: a thread that migrates islands, and a core that owns no island. * The invariant HELD — but stressing it exposed WHY it holds, and a sharp * safety-vs-security split between the two fence classes. * =========================================================================== * * INVARIANT: which fence a core installs is fixed by WHICH ISLAND IT RUNS — computed once * at start from the static island→core ownership, never re-derived per context switch. * * ATTACK A — a thread migrates from island A to island B at runtime. * The fence is keyed on the CORE's island, not on the thread's claimed membership, so: * • A thread's TCB lives in its HOME island's kernel RAM. Every OTHER island's fence * DENIES that RAM (D2/ADR 0004). So the instant a foreign island's core tries to * schedule the migrated thread, it FAULTS reading the thread's own control block. * ⇒ cross-island migration is IMPOSSIBLE BY CONSTRUCTION — not a runtime check. The * very fence that isolates islands hides each thread's bookkeeping from the others; * isolation and no-migration are the SAME mechanism. (Same reason a runtime * cross-island SPAWN or JOIN is confined — each would touch a foreign TCB.) * • Intra-island migration across an SMP island's OWN cores is FINE: they share one * island key, one kstruct, one arena. The invariant permits it. (This is why §8's * AMP+SMP composition is safe — SMP load-balancing never crosses the key.) * ASYMMETRIC CAVEAT (MCXN947): the structural guard WEAKENS. A trusted Secure core can * read a foreign NS island's TCB+stack, so hardware does NOT fault a B→A move — only * scheduler POLICY keeps threads home. So "a trusted core installs no fence" must pair * with "a trusted core schedules only ITS OWN threads": the trust is for OBSERVATION, * never for RUNNING foreign code (an NS thread with Secure reach = privilege escalation). * * ATTACK B — a core that owns no island. * Normally impossible: the default instance sweeps up every core no tk_kernel_define * carved, so every powered core maps to exactly one island. A powered-but-UNOWNED core * (mis-enumeration, or a vendor-released secondary) is an UNFENCED hole — full memory * reach. Defense = fail LOUD at bring-up (a tk_core_unclaimed() mirror of * tk_instance_fence_reject) + hold unclaimed cores in reset. * * THE DEEP FINDING — a ROBUSTNESS INVERSION between the two fence classes: * • Symmetric deny (ADR 0003): "A is safe from B" is enforced by B's OWN unit denying A. * It DEPENDS ON B COOPERATING — installing its cage at bring-up and not reprogramming * it. An unmanaged B (never installs) or a malicious B-kernel (turns its MPU off) can * reach A. ⇒ FAULT isolation among trusted-but-buggy islands (a SAFETY property). * • Asymmetric mark (ADR 0005): "A is safe from B" is installed UNILATERALLY by A (mark A * Secure); B cannot lift it (no SAU; the AHBSC is Secure-only). It holds no matter what * B does — even an unmanaged or malicious B. ⇒ SECURITY isolation of a trusted island * from an untrusted secondary. * So the classes defend DIFFERENT THREAT MODELS: 0003 = freedom-from-interference among * cooperating islands; 0005 = confinement of a NON-cooperating core. That is the sharpest * reason 0005 is a separate class — and it means an unowned CPU1 cannot harm a Secure A * (A protects itself), whereas an unowned neighbour on a SYMMETRIC board DOES defeat A's * protection (A relied on that neighbour to cage itself). * =========================================================================== */ /* ============================================================================ * TAKEAWAYS for the open questions (protection-api-discussion.md §6) * Q1 region budget: the ndeny>region_budget check in §4a arch_fence_install is * exactly where "N arenas per island" would start rejecting on v8-M; PAGING * archs never hit it. → decide N with eyes open. * Q2 resolved as your option (c): app sees INTENT; the two mechanism-shaped seams * (deny / mark) are internal lowering targets, not app surface. * Q3 posture is the declaration. TRUSTED + secure_master(derived from owning the * Secure core) is what makes A the attributor — no AHBSC in the app. * Q4 set_fence(B) is ACCEPTED; B's ISOLATED is delivered as far as the silicon * allows (confined, not mutually isolated) and REPORTED as downgraded — the * honest middle between silent-noop and hard-reject. * Q5 set_ram → tk_kernel_set_fence reads as intent at the call site; set_ram * survives as the posture=ISOLATED shorthand. * ============================================================================ */