1. Security state is a property of the address, not a mode bit

This is the single idea that everything else follows from, and it is the one that trips up engineers coming from Cortex-A or from a supervisor/user mental model.

On Armv8-M the core is in Secure state because the program counter is pointing at an address that is attributed Secure. There is no SECURE bit you flip. Change the PC to a non-secure address through a legal path and the core is now non-secure - the transition is a side effect of the branch, not an instruction that costs cycles.

fig01-security-states.png
Two consequences worth internalising:

Privilege is orthogonal. You get four combinations, not two: secure-privileged, secure-unprivileged, non-secure-privileged, non-secure-unprivileged. A non-secure privileged RTOS kernel has full authority over its own world and exactly zero authority over the secure one. Your secure side can - and for a real root of trust, should - run its own services unprivileged behind its own MPU_S.

The state is banked, not saved and restored. MSP, PSP, their stack limit registers, VTOR, the MPU, SysTick, CONTROL, the system handler priority registers (SHPR1-3, SHCSR) and the priority masks (PRIMASK, BASEPRI, FAULTMASK) all exist twice. The secure stack pointer is not something the non-secure side can read, corrupt, or even name. This is why a world switch is cheap: there is no context to copy, the hardware just selects the other bank.

Note what is not banked: NVIC_IPRn. External interrupts are assigned to a state through NVIC_ITNS, but their priority registers are shared - the one place where the two worlds still have to agree on a number.

There are two vector tables and two SysTicks. If you have ever tried to run two independent schedulers on one Cortex-M4, you know what that is worth.


2. Every resource is decoded twice

ST's implementation of the security attribution is deliberately simple, and once you see it the memory maps in the reference manual stop looking arbitrary.

Every internal resource is decoded at two addresses. Flash lives at 0x0800_0000 non-secure and 0x0C00_0000 secure. SRAM at 0x2000_0000 and 0x3000_0000. Peripherals at 0x4000_0000 and 0x5000_0000. Same silicon, two addresses.

A detail worth getting right, because it is widely repeated wrong: this is not a single address bit. SRAM and peripherals are aliased +256 MB apart (bit 28), but flash is +64 MB apart (bit 26). The IDAU decodes a fixed region table - 0x0800_0000 to 0x0BFF_FFFF Non-secure, 0x0C00_0000 to 0x0FFF_FFFF Non-secure-callable, and so on. Do not build a pointer-conversion macro around a single-bit XOR.

STM32H573 address map

The attribution itself comes from two units:

  • IDAU - Implementation Defined Attribution Unit. Fixed in silicon, not configurable. It is the region table above. On ST parts the low alias comes out Non-secure and the high alias comes out Non-secure-callable.
  • SAU - Security Attribution Unit. Eight regions, writable by secure privileged code only. This is where you promote a range to Secure, demote one to Non-secure, or carve out the NSC window.

The result is the stricter of the two, with the ordering Secure > NSC > Non-secure. You cannot use the SAU to weaken what the IDAU declared secure. That asymmetry is the point: the SAU is under secure control, but even a bug in your SAU setup cannot open a hole the IDAU closed.

A practical note that costs people an afternoon: the same physical flash sector reached through 0x0800_0000 and 0x0C00_0000 is the same bytes, but the two accesses are not equivalent. The linker script, the SAU regions, and the flash secure watermark must all agree on which alias a given function lives at, or you get a fault at the first call.


3. Two independent gates, and DMA only meets one of them

Attribution alone would only protect against the CPU. Real systems have DMA controllers, an Ethernet MAC, a graphics engine - masters that issue bus transactions without going anywhere near the core pipeline. So the check happens twice, in two structurally different places.

Two gates check every transaction
Core side. The IDAU/SAU attribution decides whether the access is even legal for the current state, then the banked MPU applies privilege and execute-never on top. A mismatch here is a SecureFault or a MemManage fault. The status register is SCB->SFSR - note it lives in the System Control Block, not in the SAU, despite what half the forum posts call it: INVEP for a bad entry point, INVTRAN for an illegal state transition, AUVIOL for an attribution violation. The transaction never reaches the bus.

System side. The transaction goes out on the AHB matrix carrying a security signal (HNONSEC at the Arm level; ST's own documents just call it the multilayer AHB bus matrix) that reflects the state it was issued from. At the target, GTZC1 checks it again.

GTZC is composed of three sub-blocks - a point that is easy to get wrong:

Sub-block Protects Granularity
GTZC1_TZSC peripherals per peripheral, secure and privileged attributes
GTZC1_MPCBB1..3 internal SRAM1/2/3 512-byte blocks (32 blocks = one 16 KB superblock), volatile
GTZC1_TZIC nothing - it reports per-source illegal-access flags

The watermark protection (MPCWM) that people often list as a fourth block is not one: it is a register set inside TZSC (MPCWM1ACFGR to MPCWM4BR) covering the memory-mapped external memories on OCTOSPI1/2 and FMC at 128 KB granularity, plus BKPSRAM at 32-byte granularity.

The asymmetry that matters operationally: a bus master that is not the core never passes the core-side filter at all. GPDMA, HPDMA, the Ethernet DMA - for them GTZC is the only thing standing between a mis-programmed descriptor and your key material. If you configure the SAU beautifully and leave GTZC at reset, you have built a system that a single wrong DMA destination address defeats.

Note also how a GTZC violation reports, because it is not uniform. ST documents three distinct responses depending on the resource: read-as-zero / write-ignore silently; RAZ/WI plus an illegal-access event; or a bus error. Check the "illegal access definition" table in the GTZC chapter of RM0481 for the resource you care about rather than assuming. Where an event is raised, TZIC latches a per-source flag and asserts GTZC_IRQn - you have to wire that interrupt up and decide what it means. Silently swallowing it is a design decision, and usually the wrong one.


If the secure world were simply unreachable it would also be useless. The controlled entry mechanism is what makes TrustZone-M a partition rather than a wall.

A third attribution exists alongside Secure and Non-secure: Non-secure callable. NSC memory is secure memory that non-secure code is permitted to branch to - and only to branch to - at addresses that begin with an SG (Secure Gateway) instruction.

Anatomy of a non-secure to secure call
The sequence, and why each step exists:

  1. Non-secure code does an ordinary BL to a veneer address in the NSC region. Nothing special about the call site - this is why the C API on the non-secure side looks like any other function call.
  2. The first halfword at that address is SG. Executing SG while in non-secure state, at an address attributed NSC is the only way non-secure code can call into the secure world. (Secure exceptions targeted via NVIC_ITNS, and reset, also enter Secure state - but those are not something non-secure code initiates.) The core switches state here.
  3. The veneer then branches into the real secure function, which is in ordinary Secure memory. That branch is now legal because the core is already secure.
  4. The secure function returns with BXNS lr. On this path LR simply holds the ordinary non-secure return address; BXNS is what makes the branch also switch state back.

The mirror-image path is the one with the hardware protection: when secure code calls a non-secure function with BLXNS, the real return address and xPSR are pushed onto the secure stack and LR is loaded with the magic FNC_RETURN value (0xFEFF_FFFF). That is what stops non-secure code from fabricating a return that lands somewhere convenient inside the secure world.

Try to branch straight at the secure function and you get a SecureFault with SFSR.INVEP - invalid entry point. The SG is not decoration; it is the token that proves this address was intended as an entry point by whoever built the secure image.

Three things fall out of this that shape how you design the secure API:

  • The NSC region is your entire attack surface. Every veneer in it is a callable entry. Keep it small and audit it as a unit - this is the one part of the secure image an attacker gets to poke at directly.
  • Arguments arrive in registers from an untrusted caller. The hardware validated the entry point, not the arguments. Every pointer that crosses the boundary must be checked against the non-secure memory map before you dereference it - the TT/TTA instructions exist exactly for this, and the CMSE intrinsics (cmse_check_address_range, cmse_check_pointed_object) wrap them. A secure function that memcpy's through a caller-supplied pointer without checking it hands the whole partition away.
  • Non-secure callbacks are a deliberate hole. Passing a non-secure function pointer into the secure world and calling it via BLXNS is supported and sometimes necessary, but you have just given non-secure code control of the PC inside a secure call frame. Treat every such callback as a design review item.

5. What this buys you, stated honestly

TrustZone-M gives you a hardware-enforced partition with a single auditable entry surface, at low runtime cost - a world switch is a branch plus a veneer, not an exception entry and not a context save. It is not literally free: the compiler's cmse_nonsecure_entry prologue and epilogue clear caller-saved registers, and BLXNS pushes return state to the secure stack. Budget a handful of cycles per crossing, not a microsecond.

What it does not give you:

  • It is not a defence against physical attack. Glitching, side channels, and decapsulation are a different discipline; on the H5 that conversation involves the product state, HDPL hide-protection levels, and the DHUK-derived keys, not the SAU.
  • It does not protect a secure world that trusts its inputs. Most published TrustZone-M failures are not silicon failures; they are unvalidated pointers crossing an NSC veneer.
  • It is not free architecturally. You now have two link maps, two vector tables, two RTOS-visible SysTicks, and a build that produces a secure image and a non-secure image that must agree on the memory partition. Getting that agreement wrong is the single largest source of "it faults at startup" in this ecosystem - which is exactly what Part 2 is about.

Coming in Part 2 - Configuration

Turning the architecture into a working partition on an STM32H573: TZEN and the product state machine, flash secure watermarks (FLASH_SECWM1R_CUR/_PRG, FLASH_SECWM2R_CUR/_PRG) and HDP, MPCBB block maps for SRAM, the GTZC peripheral assignment, and how the linker scripts and the _CMSE_Lib.o import library keep the two images consistent.

Part 3 - Practice: the OEMiRoT / STiROT boot chain, provisioning, debug authentication, and a signed firmware update that survives a power cut.


Going further

Our one-day course ARM TrustZone for Cortex-M based devices (SEC7) covers this on a Cortex-M33 board: Armv8-M security extension, SAU and IDAU configuration, Cortex-M33 MPU and PMSA, exceptions across the boundary, security access faults, secure software design. STM32H5 (STR12), three days, takes it to the silicon: secure and non-secure images, NSC veneers, GTZC isolation patterns, TF-M and STM32Trust Secure Manager, provisioning and dual-bank update.

Sessions run online in both European and US time zones. See the course pages for the next dates, or request a session on your own dates, online or on-site.

SEC7 · STR12 · SEC5 · STR19 · STR13


References

  • ST - RM0481, STM32H563/H573/H562 reference manual - GTZC and FLASH chapters, the authoritative H5 source
  • ST - PM0264, STM32 Cortex-M33 MCUs programming manual - SFSR, banking, SG/BXNS/BLXNS
  • ST - AN5347, Introduction to Arm TrustZone features on STM32L5, STM32U5 and STM32U3 MCUs - covers the L5/U5/U3 families rather than H5, but the IDAU/SAU/GTZC model is the same
  • ST wiki - Security features on STM32H5 MCUs
  • Arm - Armv8-M Architecture Reference Manual, security extension