Chapter 8 TrustZone-M runtime view (CMSIS Core(M))

8.1 Introduction

In this chapter we look into how secure and non-secure function calls are handled during runtime. In chapter 7 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.

8.2 SAU initialization (CMSIS)

More on Security Attribution Unit:

  • Chapter 5.5: IDAU and SAU: Security attribution
  • Chapter 6.3: CMSIS: ROM segment and Boot Process
  • Chapter 8.2: SAU Initialization
  • Chapter 9.4: STM32L5: Memory Map

More on Implementation Defined Attribution Unit:

  • Chapter 5.5: IDAU and SAU: Security attribution
  • Chapter 9.4: STM32L5: Memory Map

More on TrustZone-M core components:

  • Chapter 5: Basics: TrustZone-M
  • Chapter 5.4: Two worlds: Secure and non-secure
  • Chapter 5.5: IDAU and SAU: Security attribution

Let’s quickly recall the boot process examined in chapter 6.3 (startup_\<device\>.s):

boot process

Figure 8.1: boot process

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 8.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 appopriate 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 und marks all memory as secure, when SAU is disabled (ALLNS).

8.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 and ALLNS

To set up a SAU region the following procedure happens:

  1. SAU_RNR (0xE000EDD8, Region Number Register): Write the selected region number into SAU_RNR.
  2. SAU_RBAR (0xE000EDDC, Region Base Address Register): Write the base address of the region selected in SAU_RNR into SAU_RBAR
  3. SAU_RLAR (0xE000EDE0, Region Limit Address Register): Write the highest address of the region selected in SAU_RNR into SAU_RBAR. To mark a region as non-secure callable write 1 in bit[1] from SAU_RLAR. 0 indicates that the region is non-secure.

8.3 Secure function call

More on Secure function calls:

  • Chapter 6.4: CMSIS: Non-Secure Callable segment
  • Chapter 7.2: AAPCS: Subroutine Call
  • Chapter 5.4.1: Banked Registers
  • Chapter 8.3: Details: Secure function call
  • Chapter 5.6.1: Overview: Secure function call

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:

  • Chapter 5.2: Execution Modes and Privilege Levels (with TrustZone)
  • Chapter 4.4: Arm Architecture: Execution Modes and Privilege Levels

In chapter 5.6.2 and chapter 6.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 calles 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 7 we looked into Procedure Call Standard for Arm Architecutre (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 7 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 7 and we adapt it a little bit and define sum() as a entry function.

→ chapter 8, example 1

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;
        mySt.a = 1;
        mySt.b = 2;
        struct 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 7: 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:

→ chapter 8, example 2

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;
        mySt.a = 1;
        mySt.b = 2;
        int 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

Disassembly of section .text:

Disassembly of section .gnu.sgstubs:

0c03e000 <sum>:
 c03e000:       e97f e97f       sg
 c03e004:       f7c2 b856       b.w     c0000b4 <__acle_se_sum>

main:     file format elf32-littlearm

Disassembly of section .text:

0c0000b4 <__acle_se_sum>:
 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.

8.4 Non-secure function call

More on Nonsecure function calls:

  • Chapter 7.2: AAPCS: Subroutine Call
  • Chapter 5.4.1: Banked Registers
  • Chapter 5.6.2: Overview: Non-Secure function call
  • Chapter 8.4: Details: Non-secure function call

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: 5.6.2). The previous chapter on secure function calls showed that GCC does not allow the useage 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:

→ chapter 8, example 3

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 a 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 exected 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:

→ chapter 8, example 4

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.a = 1;
        a.b = 2;
        int b = callback(a);
        return a.a + b + a.b + b;
}

void main(void){}

The return type os 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 comiler 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
Disassembly of section .text:                                                                     

0c000100 <__gnu_cmse_nonsecure_call>:
 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 to 0x0c00.010e bank and clean the variable registers (overwrite with R4)
  • 0x0c00.0110 to 0x0c00.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).

8.5 Secure and non-secure exceptions

More on Arm Exception System:

  • Chapter 4.6.2: Arm Architecture:NVIC
  • Chapter 4.6.3: Arm Architecture: Masking
  • Chapter 7.3: Exception Entry and Return
  • Chapter 9.5.2: STM32L5: TrustZone Illegal Access Controller (TZIC)
  • Chapter 4.4: Arm Architecture: Execution Modes and Privilege Levels
  • Chapter 4.6: Arm Architecture:Exceptions, Interrupts, Faults
  • Chapter 8.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.

Table 8.1: ArmV8-M Exceptions with banking status
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 voilation occours 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 occoured at a security gate do not trigger a SecureFault. In STM32L5 for example, an GTZC exception is implemented as an external interrupt. Security voilations occouring at a security gate are reported to the GTZC and then forwarded to the NVIC to trigger an external interrupt.

8.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 execption is taken and restores it, when the exception returns. As already mentioned in chapter 7.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:

  • Chapter 7.2: AAPCS: Subroutine Call
  • Chapter 5.4.1: Banked Registers
  • Chapter 4.7: Arm Architecture: General-Purpose registers, special-purpose 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:

Exception Entry Stack

Figure 8.2: Exception Entry 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 exeption, 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 happend
    • 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:

  1. Check whether the current security state corresponds with the value stored in EXC_RETURN.ES
  2. 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.
  3. Previous Security Domain is restored, as indicated by EXC_RETURN.S
  4. Contexts are restored based on the values EXC_RETURN.MODE and EXC_RETURN.S

8.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 ist configured in the NVIC_ITNSn register.

References

[7] Arm Ltd, “ARMv8-M Security Extensions Requirements on Development Tools.” 2019, [Online]. Available: https://developer.arm.com/docs/ecm0359818/latest.