Chapter 7 TrustZone-M runtime view (CMSIS Core(M))
7.1 Introduction
In this chapter we look into how secure and non-secure function calls are handled during runtime. In chapter 6 we saw how parameters are passed into functions, whereas the following chapter focuses only on peculiarities of secure and non-secure function calls. We will also look into how SAU regions are set up during system reset.
7.2 SAU initialization (CMSIS)
More on Security Attribution Unit:
More on Implementation Defined Attribution Unit:
More on TrustZone-M core components:
Let’s quickly recall the boot process examined in chapter 5.3 (startup_\<device\>.s
):
A CMSIS project contains a header file named partition_<device>.h
which contains the initial configuration of the TrustZone components. This file implements the function TZ_SAU_Setup()
called from SystemInit()
. In the following chapter we are looking into the details of the “initialize SAU” call - which is the function TZ_SAU_Setup()
- in :SystemInit
shown in figure 7.1.
In partition_<device>.h
a macro is defined to ease the configuration of SAU regions: SAU_INIT_REGION(n)
and SAU_INIT_END(n)
. Using these macros a SAU region can be configured by defining a start and an end address together with the region number. The macro then sets up the appropriate registers using these values. Then the security attributes of the region number are set using SAU_INIT_NSC(n)
.
→ example-stm32-p1(Secure/Core/Inc/partition_stm32l552xx.h)
#define SAU_INIT_REGION0 1
/*
// <o>Start Address <0-0xFFFFFFE0>
*/
#define SAU_INIT_START0 0x0C03E000 /* start address of SAU region 0 */
/*
// <o>End Address <0x1F-0xFFFFFFFF>
*/
#define SAU_INIT_END0 0x0C03FFFF /* end address of SAU region 0 */
/*
// <o>Region is
// <0=>Non-Secure
// <1=>Secure, Non-Secure Callable
*/
#define SAU_INIT_NSC0 1
The example shows the setup of a NSC region (SAU_INIT_NSC0 0
)from 0x0C03E000 to 0x0C03FFFF. In example-stm32-p1 6 SAU regions are setup up. For each SAU region the security attribute is set using the SAU_INIT_NSC(n)
macro, where 0 means the region is non-secure and 1 that it is non-secure callable.
Next in TZ_SAU_Setup()
the SAU is enabled. When SAU is disabled the default security attribution of all memory can be defined in the ALLNS
bit (bit[1]) of SAU_CTRL
register:
- bit[1] (
ALLNS
): All memory is marked as secure, when 0. When 1, all memory is marked as non-secure. (if SAU is disabled)
The following code shows how CMSIS project enable and set up the ALLNS
bit of the SAU_CTRL
register:
→ example-stm32-p1(Secure/Core/Inc/partition_stm32l552xx.h)
#if defined (SAU_INIT_CTRL) && (SAU_INIT_CTRL == 1U)
SAU->CTRL = ((SAU_INIT_CTRL_ENABLE << SAU_CTRL_ENABLE_Pos) & SAU_CTRL_ENABLE_Msk) |
((SAU_INIT_CTRL_ALLNS << SAU_CTRL_ALLNS_Pos) & SAU_CTRL_ALLNS_Msk) ;
#endif
This enables the SAU and marks all memory as secure, when SAU is disabled (ALLNS
).
7.2.1 SAU configuration registers
The configuration registers for the SAU are located in the system control space of the core starting at 0xEDD0.EDD0
. The number of regions configurable by the developer is implementation defined by the vendor. The configuration of SAU is done in the following registers:
SAU_CTRL
(0xE000EDD0): Enable / Disable SAU andALLNS
To set up a SAU region the following procedure happens:
SAU_RNR
(0xE000EDD8, Region Number Register): Write the selected region number intoSAU_RNR
.SAU_RBAR
(0xE000EDDC, Region Base Address Register): Write the base address of the region selected inSAU_RNR
intoSAU_RBAR
SAU_RLAR
(0xE000EDE0, Region Limit Address Register): Write the highest address of the region selected inSAU_RNR
intoSAU_RBAR
. To mark a region as non-secure callable write 1 in bit[1] fromSAU_RLAR
. 0 indicates that the region is non-secure.
7.3 Secure function call
More on Secure function calls:
A secure function call is a call from non-secure world to secure world. When a state transition happens the stack registers are banked, which means there are at least two separate stacks available, one for secure and one for non-secure world. If MSP
(Main Stack Pointer) and PSP
(Process Stack Pointer) are used, there might even be four stacks: handler mode stack, thread mode stack and both available in each world.
More on Arm Privilege Levels:
In chapter 4.6.2 and chapter 5.2.3 we learned which instructions must be used to transition from non-secure to secure world and how secure gateways veneers must be placed in non-secure callable regions. What we ignored until now is how arguments are passed into secure world. Non-secure world is neither aware it called a secure world function (even more, it is a requirements that non-secure firmware can be developed in a IDE, which has no Cortex-M Security Extension (CMSE) awareness) nor can non-secure world write into secure world. So how are arguments passed from non-secure to secure world?
In chapter 6 we looked into Procedure Call Standard for Arm Architecture (AAPCS) and explained how arguments are passed between functions in in regular Arm compiled binaries. Since non-secure world is not aware of calling a secure world as a consequence the same subroutine call rules apply: Parameters are passed in R0-R3 and on stack, the same applies to return values (see chapter 6 for details).
Except SP
no other general-purpose register are banked, so passing parameters using R0-R3 works the same way in intra- as for inter-world function calls. Things get interesting when stack must be used to pass parameters, since stack is separated between worlds.
a
[7] defines requirements which development tools like compilers and linkers must implement to be CMSE compatible. To handle arguments on stack, compiler developers are given two options:
- Generate code, which reads and writes arguments from and to non-secure stack
- Constrain the parameters and return allowed in the function during compile time, to make sure no stack is used.
Let’s look at some examples. The first one is from chapter 6 and we adapt it a little bit and define sum()
as an entry function.
arm-none-eabi-gcc -specs=nano.specs -specs=nosys.specs -mcmse -march=armv8-m.main -T STM32L552ZETXQ_FLASH.ld main.c
struct s {
int a;
int b;
};
struct s __attribute__((cmse_nonsecure_entry)) sum(struct s a){
return (struct s) {a.a + 3, a.b + 4};
}
int main(void){
struct s mySt;
.a = 1;
mySt.b = 2;
myStstruct s mySt2 = sum(mySt);
return mySt.a + mySt.b + mySt2.a + mySt2.b;
}
Result:
main2.c:10:59: error: 'cmse_nonsecure_entry' attribute not available to functions that return value on the stack
10 | struct s __attribute__((cmse_nonsecure_entry)) sum(struct s a){
|
Recap from 6: The parameter struct s
from sum()
is splitted into it’s fundamental data types, which are then passed separately in R1
and R2
. For the return struct on the other side a reference to reserved space on main()
s stack is passed in R0
. sum()
writes into that reserved space and returns the reference to main()
in R0
.
As you can see in the error message GCC decided to implement the first of the mentioned two options: Do not allow function, which need arguments to be passed on stack. That’s why we got an error when compiling the example: The return value would be passed into sum()
on non-secure stack and GCC decided not to implement any code to copy values from non-secure to secure stack, and back.
There might be some compilers, which copy values from non-secure to secure stack. Examining those might be done in the future.
The second example is modified, so that only registers are used:
arm-none-eabi-gcc -specs=nano.specs -specs=nosys.specs -mcmse -march=armv8-m.main -T STM32L552ZETXQ_FLASH.ld main.c
struct s {
int a;
int b;
};
int __attribute__((cmse_nonsecure_entry)) sum(struct s a){
return a.a + a.b;
}
int main(void){
struct s mySt;
.a = 1;
mySt.b = 2;
myStint mySt2 = sum(mySt);
return mySt2;
}
The example above only uses R0
, R1
and R2
to pass and return parameters: struct s a
is splitted into its fundamental types, the return value of sum()
is already a fundamental type and thus returned in R0
. Lets disassemble sum()
and the __acle_se_sum()
:
arm-none-eabi-objdump --disassemble=sum main
arm-none-eabi-objdump --disassemble=__acle_se_sum main
main: file format elf32-littlearm
.text:
Disassembly of section
:
Disassembly of section .gnu.sgstubs
<sum>:
0c03e000 c03e000: e97f e97f sg
c03e004: f7c2 b856 b.w c0000b4 <__acle_se_sum>
main: file format elf32-littlearm
.text:
Disassembly of section
<__acle_se_sum>:
0c0000b4 c0000b4: b480 push {r7}
c0000b6: b083 sub sp, #12
c0000b8: af00 add r7, sp, #0
c0000ba: 463b mov r3, r7
c0000bc: e883 0003 stmia.w r3, {r0, r1}
c0000c0: 683a ldr r2, [r7, #0]
c0000c2: 687b ldr r3, [r7, #4]
c0000c4: 4413 add r3, r2
c0000c6: 4618 mov r0, r3
c0000c8: 370c adds r7, #12
c0000ca: 46bd mov sp, r7
c0000cc: bc80 pop {r7}
c0000ce: 4671 mov r1, lr
c0000d0: 4672 mov r2, lr
c0000d2: 4673 mov r3, lr
c0000d4: 46f4 mov ip, lr
c0000d6: f38e 8800 msr CPSR_f, lr
c0000da: 4774 bxns lr
The symbol sym.sum
points to the secure gateway veneer for sum()
. sym.__acle_se_sum
is the actual implementation of sum()
in secure world. We see that the compiler reserves some space on secure stack (referenced by R7
) and then stores the two arguments from R0
and R1
at [R7]
and [R7+4]
. Then these values are loaded into R2
and R3
, summed up and moved from R3
into R0
. The rest is epilogue and the secure sum()
returns to non-secure world.
7.4 Non-secure function call
More on Nonsecure function calls:
A non-secure function call is a call from secure world to non-secure world. As a consequence of separating secure and non-secure worlds into different executable object files, non-secure function calls calls can only happen using function pointers (details: 4.6.2). The previous chapter on secure function calls showed that GCC does not allow the usage of stack to pass arguments between worlds. The same applies to non-secure function calls.
Let’s try an example where a struct is returned from non-secure to secure world:
arm-none-eabi-gcc -specs=nano.specs -specs=nosys.specs -mcmse -march=armv8-m.main -T STM32L552ZETXQ_FLASH.ld main.c`
struct s {
int a;
int b;
};
typedef struct s __attribute__((cmse_nonsecure_call)) nsfunc(struct s);
int secure(nsfunc* callback){
struct s a = {1,2};
struct s b = callback(a);
return a.a + b.a + a.b + b.b;
}
A secure function secure()
is defined, which gets a non-secure function pointer as an argument (named callback()
) and calls it. callback()
has a struct as return value. What would happen normally (when not dealing with secure / non-secure calls): secure()
would reserve space on it’s stack for a struct s and would call callback()
by setting up the call in the following way:
- R0: pointer to the reserved space on secure stack.
- R1: 1
- R2: 2
Since we deal with a non-secure function call non-secure world would need to write to secure stack! That is obviously not possible, and GCC gives us, as expected an error:
main3.c:7:69: error: 'cmse_nonsecure_call' attribute not available to functions that return value on the stack
7 | typedef struct s __attribute__((cmse_nonsecure_call)) nsfunc(struct s);
|
Let’s modify the example now in a way stack is not used:
arm-none-eabi-gcc -specs=nano.specs -specs=nosys.specs -mcmse -march=armv8-m.main -T STM32L552ZETXQ_FLASH.ld main.c
struct s {
int a;
int b;
};
typedef int __attribute__((cmse_nonsecure_call)) nsfunc(struct s);
int secure(nsfunc* callback){
struct s a;
.a = 1;
a.b = 2;
aint b = callback(a);
return a.a + b + a.b + b;
}
void main(void){}
The return type is nsfunc
is changed to return only a int
.
Disassembly of secure()
:
arm-none-eabi-objdump --disassemble=secure main
a.out: file format elf32-littlearm
Disassembly of section .text:
0c0000b4 <secure>:
c0000b4: b590 push {r4, r7, lr} // prologue
c0000b6: b087 sub sp, #28
c0000b8: af00 add r7, sp, #0
c0000ba: 6078 str r0, [r7, #4] // callback
c0000bc: 2301 movs r3, #1
c0000be: 60fb str r3, [r7, #12] // [R7+12]: 1
c0000c0: 2302 movs r3, #2
c0000c2: 613b str r3, [r7, #16] // [R7+16]: 2
c0000c4: 687a ldr r2, [r7, #4] // load callback into R2
c0000c6: f107 030c add.w r3, r7, #12
c0000ca: e893 0003 ldmia.w r3, {r0, r1} // load 1 and 2 into R0 and R1 from [R7+12] and[R7+16]
c0000ce: 4614 mov r4, r2 // move callback into from R2 into R4
c0000d0: 0864 lsrs r4, r4, #1 // clearing of ..
c0000d2: 0064 lsls r4, r4, #1 // ... bit[0] in R4
c0000d4: 4622 mov r2, r4 // clear R2
c0000d6: 4623 mov r3, r4 // clear R2
c0000d8: f000 f812 bl c000100 <__gnu_cmse_nonsecure_call>
c0000dc: 6178 str r0, [r7, #20] // get the return value from non-secure world
c0000de: 68fa ldr r2, [r7, #12] // sum everything up
c0000e0: 697b ldr r3, [r7, #20]
c0000e2: 441a add r2, r3
c0000e4: 693b ldr r3, [r7, #16]
c0000e6: 441a add r2, r3
c0000e8: 697b ldr r3, [r7, #20]
c0000ea: 4413 add r3, r2
c0000ec: 4618 mov r0, r3 // return from secure() with sum in R0.
c0000ee: 371c adds r7, #28 // epilogue
c0000f0: 46bd mov sp, r7
c0000f2: bd90 pop {r4, r7, pc}
Please note the inline-comments explaining the different instructions. When a function is attributed as cmse_nonsecure_call
, the compiler adds code to prepare the transition to non-secure:
- a subroutine
__gnu_cmse_nonsecure_call
is added. In this subroutine is responsible to bank and clean variable registers (R5-R11) and co-processor registers. Banking happens on secure stack. - Before calling the
__gnu_cmse_nonsecure_call
the caller is responsible to clear / set up argument registers (R0-R3). - The callback address in non-secure world is expected by
__gnu_cmse_nonsecure_call
to be in R4.
The clearing of bit[0] in R4
at 0xc0000d4
indicates to BLXNS
(in __gnu_cmse_nonsecure_call
) a transition from secure to non-secure world.
Let’s look into __gnu_cmse_nonsecure_call
:
arm-none-eabi-objdump --disassemble=__gnu_cmse_nonsecure_call main
.text:
Disassembly of section
<__gnu_cmse_nonsecure_call>:
0c000100 c000100: e92d 4fe0 stmdb sp!, {r5, r6, r7, r8, r9, sl, fp, lr} / save R5-R11, LR on secure stack
c000104: 4627 mov r7, r4 //clear registers
c000106: 46a0 mov r8, r4
c000108: 46a1 mov r9, r4
c00010a: 46a2 mov sl, r4
c00010c: 46a3 mov fp, r4
c00010e: 46a4 mov ip, r4
c000110: b0a2 sub sp, #136 // reserve space on secure stack to store the secure floating point registers
c000112: ec2d 0a00 vlstm sp // actually store the registers
c000116: f384 8800 msr CPSR_f, r4 / clear APSR register
c00011a: 4625 mov r5, r4 // clear more registes
c00011c: 4626 mov r6, r4
c00011e: 47a4 blxns r4 // call non-secure world
c000120: ec3d 0a00 vlldm sp // restore secure floating pointer registers
c000124: b022 add sp, #136
c000126: e8bd 8fe0 ldmia.w sp!, {r5, r6, r7, r8, r9, sl, fp, pc} // restore core registers
0x0c00.0100
to0x0c00.010e
bank and clean the variable registers (overwrite with R4)0x0c00.0110
to0x0c00.0112
reserves space for floating point co-processor registers and stores them.
BLXNS
stores the return address, in the example 0x0c00.0120
, the contents of IPSR (Interrupt Program Status Register) register and CONTROL.SFPA
(Secure Floating Point Active bit) on secure stack. LR
is updated to contain the special value FNC_RETURN
.
When the non-secure world function returns and the core detects FNC_RETURN
in PC
(e.g. after the non-secure function executed BX LR
), the previously stored return address is popped from secure stack into PC and execution continues in secure world. In the example that would be 0x0c00.0120
. From there the previously banked co-processor registers are restored, just as the variable registers (0x0c00.0126
).
7.5 Secure and non-secure exceptions
More on Arm Exception System:
- Chapter 3.6.2: Arm Architecture:NVIC
- Chapter 3.6.3: Arm Architecture: Masking
- Chapter 6.3: Exception Entry and Return
- Chapter 10.2.5.2: STM32L5: TrustZone Illegal Access Controller (TZIC)
- Chapter 3.4: Arm Architecture: Execution Modes and Privilege Levels
- Chapter 3.6: Arm Architecture:Exceptions, Interrupts, Faults
- Chapter 7.5: Secure and non-secure exceptions
When security extensions are enabled in a M-profile core, exceptions either target a specific world (secure or non-secure) or are banked, meaning they are available in both worlds.
Exception Name | Exception Number | Banked |
---|---|---|
Reset | 1 | No |
NMI | 2 | No |
HardFault | 3 | Yes (cond.) |
MemManage fault | 4 | Yes |
BusFault | 5 | No |
UsageFault | 6 | Yes |
SecureFault | 7 | No |
Reserved | 8-10 | - |
SVCall | 11 | Yes |
DebugMonitor | 12 | No |
Reserved | 13 | - |
PendSV | 14 | Yes |
SysTick | 15 | Yes |
External interrupt N | 16+N | No |
The SecureFault exception always targets secure world and is taken, when a security violation occurs in the core, which could be for example:
- A change to secure world was requested, but the first instruction was not
SG
- SAU violation
From a systems perspective violations occurred at a security gate do not trigger a SecureFault. In STM32L5 for example, an GTZC exception is implemented as an external interrupt. Security violations occurring at a security gate are reported to the GTZC and then forwarded to the NVIC to trigger an external interrupt.
7.5.1 Exception Entry with transition (S to NS)
Since an exception is a sudden interruption of the program execution, the core automatically stores the current program context when an exception is taken and restores it, when the exception returns. As already mentioned in chapter 6.3 the state context is 8 32-bit words:
- RETPSR (Combined Exception Return Program Status Registers)
- Return Address
- LR
- R12
- R0-R3
More on Arm Registers:
The state context is stored on the current stack. In an core with floating point extensions enabled the state context is one of four frames, which the core could save on the stack on exception entry:
- state context
- additional state context
- floating point context
- additional floating point context
The additional state contexts are stored on stack, when the taken exception targets non-secure world and the background state was secure, i.e. a transition from secure to non-secure is done. Whether the additional floating point context is stored on transition is depends on the settings of the FPCCR_S.TS
register bit, which determines whether the floating pointer registers should be treated as secure (1) or or non-secure (0).
When FPCCR_S.TS
is 1 and the exception is taken from secure to non-secure state all four mentioned frames are saved on stack:
The exception return phase is started by the core, when PC
is set to EXC_RETURN
by the exception handler (remember: On exception entry LR
was set to EXC_RETURN
). During the return phase the saved contexts are restored and the execution resumes execution where is was interrupted before the exception, triggered by setting PC
to the stored Return Address from stack.
The EXC_RETURN in LR holds some important information used for the return routine:
- EXC_RETURN.S: Holds Information on the stack, which should be used to restore the previously stored contexts.
- 0 = Non-Secure
- 1 = Secure
- EXC_RETURN.MODE: Shows the privilege level the core was when the exception happened
- 0 = Handler
- 1 = Thread
- EXC_RETURN.SPSEL: Stack Pointer selection: Stores the Stack Pointer for the security Domain, which was active when the exception was taken.
- 0: MSP
- 1: PSP
- EXC_RETURN.ES: Shows in which Security Domain the Exception is
- 0: Non-Secure
- 1: Secure
The return routine takes four major steps:
- Check whether the current security state corresponds with the value stored in EXC_RETURN.ES
- If yes, restore EXC_RETURN.SPSEL to CONTROL.SPSEL depended on the current security domain. CONTROL.SPSEL is zeroed out on Exception Entry to select MSP in Handler Mode.
- Previous Security Domain is restored, as indicated by EXC_RETURN.S
- Contexts are restored based on the values EXC_RETURN.MODE and EXC_RETURN.S
7.5.2 Exception Target Security State Configuration
The world targeted by an exception can be configured for all exceptions which are not banked. If the bit AIRCR.BFHFNMINS
is 1, NMI and BusFault target secure world. The target for DebugMonitor can be configured by setting DEMCR.SDME
to 1 (secure).
For external interrupts managed by NVIC the target security level is configured in the NVIC_ITNSn
register.