Protect RAM from unwanted writes

Hi,

I'd like to have a memory region with some data that should only be written by some special functions, but can be read by the whole system.

This is to make sure that no bugs in other parts can accidentally corrupt this data. In previous projects on STMs I achieved this by using the MPU and a priviledged mode to write.

What is the recommended way to do this with Nordic Connect SDK / Zephyr RTOS on the nRF5340?

I saw that using the User mode can do that, but this adds a lot of overhead to system calls and is probably not the right choice.

Best regards,

Lars

  • Sigurd Hellesvik said:
    When using the MPU as I did in the STM32, I would have to enable/disable priviledged mode. How would I do that in Zephyr?

    From searching the internet, I found some docs for the MPU at https://documentation-service.arm.com/static/5ef61f08dbdee951c1ccdd48.

    Using the hello world sample, I can read out the MPU CTRL register:

    /*
     * Copyright (c) 2012-2014 Wind River Systems, Inc.
     *
     * SPDX-License-Identifier: Apache-2.0
     */
    
    #include <zephyr/kernel.h>
    
    int main(void)
    {
    
      uint32_t i = MPU->CTRL;
    
    	printk("Hello World! %d\n", i);
    
      
    	return 0;
    }
    

    for the nrf5340dk_nrf5340_cpuapp, this prints "5", which seems to fit well with "4.2 MPU_CTRL" from the MPU docs above.
    Using the sampe method for MPU control would probably be the easiest and most direct way to do this.
    However, if the nRF Connect SDK also uses the MPU, you may get some conflicts using this method.

    For "The Zephyr way":
    I realize that I did not hit the correct link previously with the Memory map and MPU considerations. Now I see that this is more for the people writing Zephyr than actual users.

    Instead, I now think that User Mode is what would be right: "Zephyr offers the capability to run threads at a reduced privilege level which we call user mode. The current implementation is designed for devices with MPU hardware.".

    And especially the Memory Protection Design would probably be what you need.
    This is more complex, but I think it would be more "The Zephyr Way".

    Do you agree about my read on User Mode?

  • Hi Sigurd,

    thank you for your research.


    MPU

    So the nRF5340 does seem to have a MPU, though according to Driver support overview the MPU HAL is not supported for the nRF5340. As you have demonstrated, it can be accessed and configured manually - but even if I would configure it, how would I enter priviledged mode then in Zephyr?

    However, if the nRF Connect SDK also uses the MPU, you may get some conflicts using this method

    With this in addition to the missing part of entering priviledged mode (if done manually could interfere with the OS), MPU may not be the right choice.


    User Mode

    Do you agree about my read on User Mode?

    While I agree that it can be accomplished user mode, I fear the overhead that comes with that. Every syscall would need magnitudes more cycles as it is demonstrated here. Since all my threads need some form of kernel objects like mutexes or dynamic memory and regularly need to access the shared memory this thread is about (and other shared memory exposed by APIs), I'm not sure if it's worth the overhead as it's a low power, battery powered system. Please correct me if I am wrong.


    SPU

    In the meantime I managed to use the SPU to achieve what I was trying to:

    #include <hal/nrf_spu.h>
    
    /*
    The protected region has to be defined and added to the linker script 
    There are at least two options I found:
    1. Add the following to the device tree (in an overlay file)
    
        protected_sram: sram@20060000 {  // Name doesn't seem to matter
            compatible = "zephyr,memory-region", "mmio-sram";
            reg = < 0x20060000 0x2000 >;
            zephyr,memory-region = "protected"; // This will specify the name of the memory region AND the section
        };
    
    2. Add the contents to the linker script with zephyr_linker_sources() (see zephyr/cmake/modules/extensions for the functions)
    	In CMakeLists.txt: zephyr_linker_sources(RAM_SECTIONS custom_region.ld)
    	In custom_region.ld:
    		SECTION_PROLOGUE (protected, (NOLOAD), ALIGN(8192))
    		{ 
    			__protected_start = .;   
    			KEEP(*(SORT_BY_NAME("protected*")))
    			. = ALIGN(8192);
    			__protected_end = .;
    		} GROUP_DATA_LINK_IN(RAMABLE_REGION, RAMABLE_REGION) 
    */
    
    extern uint16_t __protected_start;
    extern uint16_t __protected_end;
    
    __attribute__((section("protected"))) static int testVar;
    static int otherVars[1000];
    
    int main(void)
    {
    	int regionNumber = ((uint32_t)&__protected_start - (uint32_t)_SRAM_BASE_ADDR) / 8192;
    	printk("Address of testvar: 0x%x, region no: %i, __protected_start=0x%x, __protected_end=0x%x, addr otherVars = 0x%x\n", 
    		&testVar, regionNumber, &__protected_start, &__protected_end, otherVars);
    
    	testVar = 100;
    	printk("Written 100 to testvar\n");
    
    	// Should not change anything
    	nrf_spu_ramregion_set(NRF_SPU_S, regionNumber, false, NRF_SPU_MEM_PERM_READ | NRF_SPU_MEM_PERM_WRITE, false);
    	printk("Set to r/w\n");
    	printk("Testvar = %i\n", testVar);
    	testVar = 200;
    	printk("Written 200 to testvar\n");
    
    	// Now set it as read only
    	nrf_spu_ramregion_set(NRF_SPU_S, regionNumber, false, NRF_SPU_MEM_PERM_READ, false);
    	printk("Set to ro\n");
    	printk("Testvar = %i\n", testVar); // Reading is okay, this will print
    	testVar = 300;  // This will give a bus fault since this region is now protected by SPU to be read only
    	printk("Written 300 to testvar\n"); // We'll never reach this point
    	k_sleep(K_MSEC(1000));
    	
    	return 0;
    }
    

    This indeed generates a bus fault if writing to the protected memory region is not permitted. I chose the second way with a linker script snipped to add a memory region of size 8192 (size of memory region block of SPU) where only variables with __attribute__((section("protected"))) should go. Is this the recommended way to add custom regions?

    Best regards,
    Lars

  • colar said:
    While I agree that it can be accomplished user mode, I fear the overhead that comes with that. Every syscall would need magnitudes more cycles as it is demonstrated here. Since all my threads need some form of kernel objects like mutexes or dynamic memory and regularly need to access the shared memory this thread is about (and other shared memory exposed by APIs), I'm not sure if it's worth the overhead as it's a low power, battery powered system. Please correct me if I am wrong.

    I agree that it is more overhead than using the SPU directly.

    colar said:
    This indeed generates a bus fault if writing to the protected memory region is not permitted. I chose the second way with a linker script snipped to add a memory region of size 8192 (size of memory region block of SPU) where only variables with __attribute__((section("protected"))) should go. Is this the recommended way to add custom regions?

    As long as you do not intend to use TrustZone for somehting else, such as TF-M at the same time, I think it sounds fine.

    And if you want to use TF-M after all, you can use TF-M for protected writes instead.

    Regards,
    Sigurd Hellesvik

Related