Demystifying ARM TrustZone for Microcontrollers (and a note on Rust support)
Introduction:
TrustZone is different from that of a separate physical security co-processor (like a TPM or a secure element) with a pre-defined set of features. You can think of it as a virtualization technology for ARM CPUs i.e. it virtualizes a physical ARM CPU core — a TrustZone enabled ARMv8 core can exist in one of 2 states Secure OR Non-Secure. This, in turn, allows us to partition all system HW and SW resources so that they exist in 1 of the 2 worlds.
TrustZone for Armv8-M has been designed for ARM microcontrollers (Cortex-M). At a high level, this variant of TrustZone is similar to the variant in Arm Cortex-A processors i.e.
- In both cases, secure and non-secure code runs on the same physical processor core.
- Execution happens in a time sliced manner (Secure <-> Normal) with non-secure software blocked from accessing secure resources directly.
- But there are differences between the 2 processor families
- TrustZone in Cortex-M has been optimized for faster context switching and low power, keeping in mind real-time processing requirements of microcontrollers. To achieve this, Cortex-M excludes the monitor mode (of Cortex-A) and the need for any secure monitor software, reducing world switch latency. For bridging software between both worlds, TrustZone for Cortex-M instead relies on a few secure function entry points. Access to/from secure function entry points is controlled via a set of special instructions: SG, BXNS, BLXNS
** TrustZone for Cortex-A processors specifies a separate processor mode called monitor mode for running a ‘secure monitor handler’ (a piece of software running in the secure world that mediates all access between worlds) as the sole entry point.
How does it work?
After a power on or reset, an Armv8-M system begins executing code in the `secure state`. This usually involves secure booting of the system along with some level of system initialization. In a TrustZone enabled system/MCU, the system needs to perform additional initialization — a few configuration routines are used to divide the system’s entire memory-map into non-overlapping secure, non-secure and non-secure-callable regions. The result is a ‘security attribution map’ (for the entire system).
Configuration of `memory security attributes` is done via 2 HW blocks called `security attribution unit` (SAU) and/or ‘implementation defined attribution unit` (IDAU).
- Implementation Defined Attribution Unit (IDAU), which is a fixed hardware unit external to the processor core that provides a fixed security status of the memory map as defined by the manufacturer. (i.e. an immutable background attribution map implemented by the vendor in hardware for their specific chip.)
- Secure Attribution Unit (SAU), which is a programmable unit integrated in the processor core used to define the security status of up to eight memory regions. Note — SAU’s registers can be set to configure non-secure memory, peripheral and interrupt access.
Key-point to remember: Security is defined by address i.e. memory security attributes are really what define security states of the processor.
After assigning ‘security attributes’ to system memory, every memory access by the processor, whether it’s a memory read, write or execute is tested for its `memory security attributes` (i.e. is it a secure or non-secure address). SAU and IDAU work together to enforce memory access restrictions at runtime. Note — While the IDAU and SAU directly enforce secure and non-secure access restrictions, they work with secure and non-secure memory protection units (MPUs) to determine the access rights associated with the target resource.
The IDAU, SAU, and MPU features of these processors provide a flexible foundation for protecting runtime execution of both system software and applications, but these capabilities are limited to the processor itself.
- In order to carry `the secure and privilege capabilities` over to other memory systems and interfaces, we use logic present in system’s bus (AMBA AHB 5/APB4) fabric i.e. the privilege attribute (HPRIV) and secure attribute (HNONSEC) are carried across the internal Advanced High-performance Bus (AHB) matrix to reach memory protection checkers (MPCs), peripheral protection checkers (PPCs), and master security wrappers (MSWs) for other bus masters.
- In other words, the core’s security state information propagates via hardware logic present in the TrustZone-enabled AMBA AHB5 / APB4 bus fabric (an extra signal (HNONSEC[1] = 0) on the AHB bus indicates a secure transaction and vice versa).
- This allows extending security to memories and peripherals through bus filters also known as TrustZone-aware peripherals which are directly connected to AHB — MPCs, PPCs, AHB/APB bridge (used as secure gate to block or propagate secure/non-secure transaction towards APB agents).
- Ensuring that no secure world resources can be accessed by the non-secure world components, enabling a strong security perimeter to be built between the 2.
Developer Workflow:
Every TrustZone implementation will have 2 separate projects
- One for the secure world and
- The other for the non-secure world
- After power on or reset, code from the secure project runs first i.e. the processor core is executing in the ‘secure state’.
- While in this state, configuration routines in the secure code project (either hand written or generated via built-in IDE tools) configure regions of memory to either be non-secure or non-secure callable. No need to explicitly attribute a ‘secure’ tag to a region of memory. Everything’s secure by default.
- As secure and non-secure projects are (pretty much) independent of each other, we must provide a way for code in one project to call code in the other. 2 compiler attributes are provided to achieve this -
(cmse_nonsecure_ entry): Functions in the secure world are decorated with this attribute to indicate that it is an entry point for non-secure calls.
(cmse_ nonsecure_call): Functions or rather function pointers in the secure world are decorated with this attribute to indicate that the secure world wants to call into the non-secure world.
Linking the secure and non-secure project:
- Any function (in the secure world) decorated with the above compiler attributes will be exported to an object file upon building the secure project. (Usually the object file is named PROJECTNAME_CMSE_lib.o)
- The non-secure project will use this object file and a `veneer_table.h` header file in its build process
In summary, we’ll need the following additions (HW +SW) to fully support TrustZone-M:
- 2 new HW blocks or on-chip peripherals called memory attribution units — SAU and IDAU (optional)
- TrustZone-aware peripherals (or controllers) to extend security to memories, peripherals and other bus masters.
- System bus AHB5/ABP4. The AHB protocol up to AMBA4 does not support security attribute (HNONSEC) signal.
- 3 new instructions — SG, BLXNS, BXNS and 2 Interrupt vector tables
Additionally, there are duplicated CPU peripherals (one for each world via register banking):
- 2 Memory Protection Units
- 2 System Control Blocks
- 2 SysTick’s: Interrupts are routed to their respective sides.
A note on Interrupt Handling:
The Nested Vectored Interrupt Controller (NVIC) is also extended for security as state transitions can also happen due to exceptions and interrupts.
- Each interrupt can be configured as Secure or Non-secure, and is determined by the Interrupt Target Non-secure (NVIC_ITNS) register, which is only programmable in the Secure world. There are no restrictions regarding whether a Non-secure or Secure interrupt can take place when the processor is running Non-secure or Secure code.
- If the arriving exception or interrupt has the same state as the current processor state, then the exception sequence is similar to the previous M-series processors.
- The main difference occurs when a non-secure interrupt takes place and is handled by the processor during the execution of secure code. In this case, the processor automatically pushes all secure information onto the secure stack and erases the contents from the register banks — this mechanism avoids any leakage of information.
**It is possible to deprioritize non-secure interrupts by setting the PRIS bit field of the Application Interrupt and Reset Control Register (AIRCR) or even avoid handling them while the secure software is running (through the PRIMASK_NS register).
Rust Support for TrustZone-M:
- For now, TrustZone-M is fully supported by a couple of semiconductor vendors (NXP’s LPCS55S69, Microchip’s SAML11, Nordic’s nRF9160, ST’s STM32L5) but all of them (only) offer ‘C’ toolchains for TZ related development.
- In order to better realize the value that TrustZone has to offer, we could combine TrustZone’s HW-based security mechanisms and Rust’s memory safety guarantees to create more secure runtime environments (or Trusted Execution Environments). As an example, think of a scenario where we could host ‘Rusty drivers for secure elements or TPMs’ in the secure world. In theory, we could write memory-safe drivers for security-sensitive applications (assuming no unsafe code is used or formal proofs for the unsafe part of your code exist.)
- Rust’s open-source community is working on adding support for the 2 compiler attributes to the Rust compiler and support for new hardware peripherals via additions to the ‘cortex-m’ crate. You can find the first pull request for (cmse_nonsecure_entry) here — https://github.com/rust-lang/rust/pull/75810.

