This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

Stack Guard and MPU

Hi, I'm trying to get the nrf_stack_guard and nrf_mpu libraries to catch writes past the end of the stack. My stack is 8kB in size (0x2000E000-0x20010000). This is the log output after initializing the Stack Guard module:

<debug> nrf_mpu: MPU region creating (location: 0x2000E000-0x2000E07F)
<debug> nrf_mpu: MPU region 0 created (location: 0x2000E000-0x2000E07F, access: RO/RO, type: Normal, flags: XN).
<info> stack_guard: Stack Guard: 0x2000E000-0x2000E07F (usable stack area: 8064 bytes)

I'm having a little trouble understanding how the MPU works. I'd expect a write to 0x2000E000 to trigger the HardFault_Handler, but in reality nothing is triggered. The write just happens and silentlly corrupts RAM below the stack.

I'm using the nrf gcc hardfault library implementation and am able to catch NULL dereferences and other faults, so that should be correctly set up.

What am I missing?

  • I see two possible causes

    1. The C optimizer eliminated the write completely or placed it somewhere else (where it would not be executed).
    2. The write buffer delayed the fault and you were only checking with a breakpoint directly after the write instruction.

    In any case we would need to see your source code...

  • Hi, thank you for the timely reply.

    Please see the below C++ snippet. I'm simulating this by allocating too big of a structure on the stack and writing to each element. If I break directly after this snippet, it leaves MSP at 0x2000d388 (beyond the stack limits).

    std::array<uint8_t, 8192> tmp{};
    NRF_LOG_INFO("MSP: %p", __get_MSP());
    for (int i = 0; i < 8192; ++i)
        tmp[i] = i;
    
    NRF_LOG_DEBUG("%p %p", tmp.begin(), tmp.end());
    NRF_LOG_INFO("MSP: %p", __get_MSP());
        
    illegal_write((void*) 0x2000E000);

    Then I tried directly writing to the protected stack area with the `illegal_write`, which is placed in another compilation unit. I'm running a debug build without any optimizations.

    void illegal_write(void* addr)
    {
        auto* p = (uint32_t*) addr;
        *p = 1;
    }

  • Hi,

    I do not immediately see the issue in this case. I suspect  is onto something.

    I did a tiny modification to the Command Line Interface (CLI) Example (which uses stack guard out of the box) but were not able to reproduce the behavior you see.

    diff --git a/examples/peripheral/cli/demo_cli_cmds.c b/examples/peripheral/cli/demo_cli_cmds.c
    index ac7c57e..b6ea4b2 100644
    --- a/examples/peripheral/cli/demo_cli_cmds.c
    +++ b/examples/peripheral/cli/demo_cli_cmds.c
    @@ -376,6 +376,8 @@ static void cmd_nordic(nrf_cli_t const * p_cli, size_t argc, char **argv)
                         "\n");
     
         nrf_cli_print(p_cli, "                Nordic Semiconductor              \n");
    +
    +    *(volatile uint32_t*)0x2000E000 = 0xbadeba11;
     }
     
     /* This function cannot be static otherwise it can be inlined. As a result, variable:
    

    Running the example on a nRF52 DK with the above modification I get the expected hard fault after typing "nordic" in the terminal.

  • For information on the MPU, you should consult the ARM Cortex-M4 user's guide:

    http://infocenter.arm.com/help/topic/com.arm.doc.dui0553b/DUI0553.pdf

    Cortex-M4 core itself (and its optional components) is designed by ARM Ltd. not Nordic, so the in-depth documentation for it won't be in any of the Nordic manuals.

    The MPU is basically a cut-down substitute for an MMU, though without the ability to do any virtual to physical mapping. The MPU provides a fixed number of region registers which can be used to define the following things:

    - Base address of the region

    - Size of the region

    - Region attributes

    The attributes and size are defined using a single register, called MPU_RASR (Region Attribute and Size Register, see section 4.4.5 in the manual).

    The attributes include things like access rights and cache behavior.

    The size part is what's important here: it's defined using 5 bits of the MPU_RASR register, and specifies a power of 2. Note that the actual power of 2 is the value of the size field plus 1.

    The minimum allowed value is 5, which represents 32 bytes (2^(4+1)), and the maximum value is 31, which represents 4GB (2^(31+1)).

    The debug output you included shows exactly what region is being guarded: 0x2000E000 to 0x2000E07F. This means you the stack guard has been set up with a base address of 0x2000E000 and a size of 128 bytes.

    What you need to do is make sure your that:

    a) Your test case is actually causing a load or store instruction to be executed (i.e. it's not being optimized out by the compiler -- the volatile keyword can help prevent this)

    b) The load or store is actually targeting an address _within_ that specific region.

    So for example, if your code is triggering a write to address 0x2000E100, then you're writing to memory on the other side of the stack guard instead of inside it, and you won't see any trap. If you have a function that uses a large amount of local variables, the compiler might emit code that adjusts the stack pointer so far beyond the end of the stack that you skip right over the guard.

    Note that it's possible to trigger a trap due to something other than an explicit load or store instruction. For example, if an exception occurs, like an interrupt, the CPU will push an exception frame onto the stack. If the stack pointer is at or near the stack guard when this happens, then this will also trigger a hard fault, because the CPU won't have enough room to save everything. This may happen more easily if you're using floating point math since in that case the CPU may also need to save the FPU registers as well.

    -Bill

  • Hi all, thank you for your responses. I'm a bit dumbfounded as to why I can't get the Stack Guard / MPU to work. I must me fundamentally misunderstanding something.

    In an attempt to isolate the problem, I've taken the ble_app_cli example (SDK 15.2.0), which seems to be the only example utilizing the stack guard) - and added a write to the base of the stack.

    static void core_init(void)
    {
        APP_ERROR_CHECK(NRF_LOG_INIT(app_timer_cnt_get));
    
        if (CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk)
        {
            APP_ERROR_CHECK(nrf_cli_init(&m_cli, NULL, true, true, NRF_LOG_SEVERITY_INFO));
        }
    
        nrf_drv_uart_config_t uart_config = NRF_DRV_UART_DEFAULT_CONFIG;
        uart_config.pseltxd = TX_PIN_NUMBER;
        uart_config.pselrxd = RX_PIN_NUMBER;
        uart_config.hwfc    = NRF_UART_HWFC_DISABLED;
        APP_ERROR_CHECK(nrf_cli_init(&m_cli_uart, &uart_config, true, true, NRF_LOG_SEVERITY_INFO));
    
        APP_ERROR_CHECK(nrf_drv_clock_init());
    
        nrf_drv_clock_lfclk_request(NULL);
    
        APP_ERROR_CHECK(app_timer_init());
    
        APP_ERROR_CHECK(nrf_stack_guard_init());
    
        *(volatile uint32_t*) STACK_BASE = 0xba5eba11;
    
        NRF_LOG_INFO("Written to stack base (%p): %x", STACK_BASE, *((uint32_t*)STACK_BASE));
    
        APP_ERROR_CHECK(nrf_pwr_mgmt_init());
    
        if (CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk)
        {
            APP_ERROR_CHECK(nrf_cli_task_create(&m_cli));
        }
    
        APP_ERROR_CHECK(nrf_cli_task_create(&m_cli_uart));
    }

    This code runs past the "illegal" write and continues normally. From my understanding, this should trigger a HardFault.

    Can somebody tell me what I'm missing?

Related