EmbeddedRelated.com
Blogs
The 2024 Embedded Online Conference

NULL pointer protection with ARM Cortex-M MPU

Miro SamekJuly 16, 2023

The second of the Ten Commandments for C Programmers says:


2. Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

This post explains how you can set up the ARM Cortex-M MPU (Memory Protection Unit) to protect thy code from dragons, demons, core dumps, and numberless other foul creatures awaiting thee after thou dereference the NULL pointer.

This article is available in PDF format for easy printing

NULL Pointer in C

A pointer with the value NULL (which in C99 can be portably expressed as (void *)0) has in C a special meaning and denotes a pointer that points to an invalid object. NULL is commonly used to initialize pointer variables and also to indicate an invalid value of a pointer variable. Attempt to dereference a NULL pointer is considered undefined behavior in C, and should never happen in a correctly operating program.

Protection against NULL Pointer Dereferencing

Dereferencing the NULL pointer does not necessarily mean accessing only the address 0x0. For example, accessing a struct member with a NULL pointer might result in an address being offset from NULL:

struct foo {
    . . .
    uint32_t x;
};
. . .
uint32_t x = (*(struct foo *)NULL)->x; // should fail!

In that case the CPU never "sees" the address 0x0 directly. The CPU accesses only the offset of the member x from the beginning of struct foo.

Therefore, any meaningful protection against dereferencing NULL pointer must also protect the whole region around the address 0x0. The size of this region is not well defined but should be as big as the objects (structs and arrays in C) used in the program.

NULL Pointer in ARM Cortex-M

The C Standard does not actually say that the NULL pointer is the same as the pointer to memory address 0x0. However, in all C and C++ compilers for ARM Cortex-M processors, NULL pointer actually points to the address 0x0. Unfortunately, most ARM Cortex-M MCUs happily allow reads and sometimes writes to address 0x0.

In ARM Cortex-M address 0x0 is in the Flash region because that’s where the CPU starts execution out of reset. This region can be protected by the MPU (Memory Protection Unit) present in ARM Cortex-M0+/M3/M4/M7. But the standard MPU setups that you can find online [1,2] only provide protection against writing to the NULL pointer, as a side effect of setting up the whole Flash memory as a read-only region.

[1] Feabhas Sticky Bits blog post "Setting up the Cortex-M3/4 (ARMv7-M) Memory Protection Unit"

[2] Memfault Interrupt blog post "Fix Bugs and Secure Firmware with the MPU"

The MPU Setup for NULL Pointer Protection

The following MPU setting seems to work for most ARM Cortex-M MCUs (using the CMSIS):

/* Configure the MPU to prevent NULL-pointer dereferencing ... */
MPU->RBAR = 0x0U                          /* base address (NULL) */
            | MPU_RBAR_VALID_Msk          /* valid region */
            | (MPU_RBAR_REGION_Msk & 7U); /* region #7 */
MPU->RASR = (7U << MPU_RASR_SIZE_Pos)     /* 2^(7+1) region, see NOTE0 */
            | (0x0U << MPU_RASR_AP_Pos)   /* no-access region */
            | MPU_RASR_ENABLE_Msk;        /* region enable */
MPU->CTRL = MPU_CTRL_PRIVDEFENA_Msk       /* enable background region */
            | MPU_CTRL_ENABLE_Msk;        /* enable the MPU */
__ISB();
__DSB();

MPU Setup in uVision IDE

This code sets up a no-access MPU region #7 around the address 0x0 (other MPU regions will do as well, but region #7 takes precedence). Otherwise, the MPU background region is enabled (PRIVDEFENA), so that memory access to all other addresses does not trigger the MPU. Of course, you can still use the remaining MPU regions to protect other memory, such as read-only access to the ROM region, for example.

What About the Vector Table?

The NULL-region protection shown above works even for the MCUs, where the Vector Table also resides at address 0x0. Apparently, the MPU does not check access to the region by instructions other than LDR/STR, such as reading the vector address during the Cortex-M exception entry. However, in case the Vector Table resides at 0, the size of the no-access region must not contain any data that the CPU would legitimately read with the LDR instruction. This means that the size of the no-access region should be about the size of the Vector Table. In the code above, the size is set to 2^(7+1)==256 bytes, which should be fine even for relatively small vector tables.

Automatic Relocation of the Vector Table

The problem of the Vector Table also residing at address 0x0 is completely evaded in MCUs that automatically relocate the Vector Table, such as STM32. For these MCUs, the size of the no-access NULL-protection region can be increased all the way to the relocated Vector Table, like 0x0800’0000 in the case of STM32. (You could set the size to 2^(26+1)==0x0800’0000).

Manual Relocation of the Vector Table

However, sometimes the NULL-region protection in the MPU conflicts with the Vector Table also residing at address 0x0. For example, the FreeRTOS ports to ARM Cortex-M access the first word of the Vector Table (in order to read the original setting for the Stack Pointer). Such access happens via the LDR instruction, so the read from the Vector Table trips the MPU. The solution in such cases is to manually relocate the Vector Table. The pseudocode below shows an example solution:

__Vectors
[1]  ; Initial Vector Table before relocation
        DCD     __initial_sp                ; Top of Stack
        DCD     Reset_Handler               ; Reset Handler
        DCD     NMI_Handler                 ; NMI Handler
        DCD     HardFault_Handler           ; Hard Fault Handler
        DCD     MemManage_Handler           ; MPU fault handler
        DCD     BusFault_Handler            ; Bus fault handler
        DCD     UsageFault_Handler          ; Usage fault handler
        DCD     0             ; Reserved
        DCD     0             ; Reserved
        DCD     0             ; Reserved
        DCD     0             ; Reserved
        DCD     SVC_Handler                 ; SVCall handler
        DCD     DebugMon_Handler            ; Debug Monitor handler
        DCD     0            ; Reserved
        DCD     PendSV_Handler              ; PendSV handler
        DCD     SysTick_Handler             ; SysTick handler
[2]     ALIGN  256  ; Extend the initial Vector Table to the 256B boundary

[3] ; Relocated Vector Table beyond the 256B region around address 0.
    ; That region is used for NULL-pointer protection by the MPU.
__relocated_vector_table
        DCD     __initial_sp                ; Top of Stack
        DCD     Reset_Handler               ; Reset Handler
        DCD     NMI_Handler                 ; NMI Handler
        DCD     HardFault_Handler           ; Hard Fault Handler
        DCD     MemManage_Handler           ; MPU fault handler
        DCD     BusFault_Handler            ; Bus fault handler
        DCD     UsageFault_Handler          ; Usage fault handler
        DCD     0             ; Reserved
        DCD     0             ; Reserved
        DCD     0             ; Reserved
        DCD     0             ; Reserved
        DCD     SVC_Handler                 ; SVCall handler
        DCD     DebugMon_Handler            ; Debug Monitor handler
        DCD     0            ; Reserved
        DCD     PendSV_Handler              ; PendSV handler
        DCD     SysTick_Handler             ; SysTick handler

        ; IRQ handlers...
[4]     DCD     . . .

;-------------------------------------------------------------------------
Reset_Handler   PROC
        . . .
[5]     ; relocate the Vector Table
        LDR     r0, =0xE000ED08 ; System Control Block/Vector Table Offset Reg
        LDR     r1, =__relocated_vector_table
        STR     r1,[r0]         ; SCB->VTOR := __relocated_vector_table
. . .

[1] The initial Vector Table at address 0x0 contains only the initial SP and the Exception Handlers but does not contain any interrupt handlers. This initial Vector Table is used only for the first few cycles after CPU reset.

[2] The initial Vector Table is extended to the 256-byte boundary, which will be covered by the NULL-pointer protection region in the MPU.

[3] The relocated Vector Table is at address 0x100. This Vector Table is ultimately used for running the application.

[4] The relocated Vector Table contains all interrupt handlers (IRQs).

[5] The Reset_Handler relocates the Vector Table to the __relocated_vector_table.

Conclusions

Protection against NULL-pointer dereferencing is an important tool for improving the system’s robustness and even for preventing malicious attacks. I hope that this short article will help fellow embedded developers.



The 2024 Embedded Online Conference

To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.

Please login (on the right) if you already have an account on this platform.

Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: