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

Parents
  • Hi Lars,

    Could you clarify further what you want to protect this RAM region against? Is it against certain malicious attack?

    If security of the data is your concern, then you can consider the PSA Protected Storage. My colleague Sigurd has a blog on persistent storage here:  Persistent storage of keys and data using the nRF Connect SDK .

    The blog is on persistent storage, but PSA Protected Storage also supports volatile storage. The information therefore will still be applicable.

    Please reply and let me know if it this suits your need. I will actually be out of office from tomorrow, but another engineer will be assigned if you need any further help.

    Best regards,

    Hieu

  • Hi Hieu,

    thanks for your answer.

    My main goal is not to protect against malicious attacks, but rather to protect special memory regions against some bugs I create elsewhere. For example a thread that has a buffer overflow or is writing to an invalid location should not be able to overwrite some important data (to guarantee this, data shall only be manipulated by functions with some priviledged mode).

    All code running on the device can be trusted as it is written by myself, though of course it may contain afromentioned bugs. So user mode is probably not the right choice due to its overhead with syscalls that I use extensively.

    Best regards,

    Lars

  • Seems the MPU is a feature of our chips I did not know so much about yet.
    Thank you for giving some pushback here, so I found out about this feature.

    From what I understand, we have not documented the MPU in our Product Specification.
    That is because we still use a Cortex-M. So I asked a collegue about this and he says that the MPU in our chips should be the same as the one in STM, since both use Cortex-M.
    However, we have an MPU HAL available.

    In addition, have a look at Zephyrs Memory map and MPU considerations.
    Also see the Memory Protection Unit (MPU) Sample for this.

    Then I think you should be able to use one of those, whichever you find fits you best.

    Is this what you are looking for?

    colar said:
    With the SPU in nRF5340, I only see the option to specifiy permissions for secure and non-secure domains, not for priviledged/inpriviledged, so to use that feature I MUST use secure/non-secure domains. Am I right with that?

    I think that the SPU does not have to depend on domains. From https://infocenter.nordicsemi.com/topic/ps_nrf5340/spu.html?cp=4_0_0_6_31_8_21#register.RAMREGION.PERM:

    So the Secure/Non-secure attribute is separate from the Write attribute.
    Therefore, I think you can use this for only Write protect.

    By the way, If you build without TF-M, the whole application runs as "Secure" from the SPUs perspective.

    colar said:
    And to read/write from non-secure code (no matter if with the application RoT services or not), all accesses have to go over an API, then IPC from non-secure to secure image and then back, right?

    Yes, but only if you have TF-M enabled. See my previous sentence.

    colar said:
    Are there other options to that or have I misunderstood some aspect of the nRF5340's features?

    I started this comment with describing the MPU, see that.

  • Hi,

    In addition, have a look at Zephyrs Memory map and MPU considerations.
    Also see the Memory Protection Unit (MPU) Sample for this.

    so the nRF5340 actually has an MPU and a SPU? I thought, since it was not documented, only the SPU was available in this Chip (HAL may be for other chips?).

    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?

    So the Secure/Non-secure attribute is separate from the Write attribute.
    Therefore, I think you can use this for only Write protect.

    By the way, If you build without TF-M, the whole application runs as "Secure" from the SPUs perspective.

    And when using the SPU, how would I then write to the protected memory? Simply temporarily disable the protection?

    Best regards,

    Lars

  • colar said:
    so the nRF5340 actually has an MPU and a SPU? I thought, since it was not documented, only the SPU was available in this Chip (HAL may be for other chips?).

    It is documented exactly here, as far as I know: CPU and support module configuration.

    colar said:
    And when using the SPU, how would I then write to the protected memory? Simply temporarily disable the protection?

    I think so yes. Might not be that easy, but probably worth a try.
    From SPU docs, I see they specify "For each region, permissions can be set and then locked to prevent subsequent modifications by using the RAMREGION[n].PERM.LOCK bit.".
    This implies that as long as you do not lock this, you can change permissions.

    colar 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?

    I will look into this and return with more info early next week.

  • 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

Reply
  • 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

Children
  • 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