Memory Protection

For a system to be safe and secure, caution must be taken while designing and implementing software. But that is not sufficient, runtime checks are needed as protection from attacks or software bugs. Strict memory access control can only be enforced in hardware. Bern RTOS aims to abstract memory protection hardware and provide a generic solution to protect memory regions. In addition, the kernel was designed for process isolation from the start.

Design

The granularity of memory protection regions is much dependent on the hardware. Memory protection currently only supports the Armv7E-M architecture and was tested on an Arm Cortex-M4 core. The hardware abstraction used in Bern RTOS is partially based on [22] in which an architecture-agnostic memory protection for Tock OS was developed.

memory-protection
Figure: Memory protection seen from thread X.

An example memory protection configuration seen from thread X is shown in the figure. The thread has access to everything within its parent process. The kernel is responsible for placing variables captured by the thread closure on the stack. Some microcontrollers have a dedicated stack overflow prevention mechanism, some use a memory region. Either way, some hardware to prevent stack overflow must be present.

The kernel uses the main stack and can never be accessed from any thread directly. Isolating the kernel is crucial, as all system and thread management data could be manipulated otherwise. System calls provide an isolation barrier between threads and the kernel. A system call contains a service ID for a given request and arguments. A call triggers an exception switching to the kernel. The arguments can then be checked before the kernel processes the call with the highest privileges. Setting up a system call, parameter verification and processing takes longer than accessing the data structure directly. In the case of the kernel, execution overhead is justified by increased security.

A little bit looser is the protection on the peripherals. Here, constant copying of data is too much overhead and most microcontrollers do not have memory protection, which can adjust to regions of arbitrary size. Thus, the all-or-nothing approach applies, either a thread can access all shared memory or it cannot. Depending on the hardware in use, some peripherals can be excluded from general access permissions or multiple shared memory regions can be defined for groups of threads.

Usage

Memory protection is heavily based on linker sections and symbols. These symbols are hidden from the user in the macro used to create a new process. Further linker sections are created based on the configuration in the bern-config crate.

SECTIONS {
    .process_my_process1 : ALIGN(4096)
    {
        /* Process static memory */
        . = ALIGN(8);
        __smprocess_my_process = .;
        KEEP(*(.process.my_process2))
        . = ALIGN(8);
        __emprocess_my_process = .;

        /* Process heap */
        . = ALIGN(8);
        __shprocess_my_process = .;
        . = __smprocess_my_process + 4096;3
        __ehprocess_my_process = .;

        ASSERT(/*..*/);4
    } > RAM5 AT> FLASH
    __siprocess_my_process = LOADADDR(.process_my_process);
} INSERT AFTER .shared_global;

Listing: Linker script extract for a process.

Invoking bern_kernel::new_process!(my_process, 4096) generates the linker script extract shown in the listing. The macro creates a new linker section 1 and alignment required by the hardware. Each process section can place static data inside the process using the process name 2. The rest of the process memory is available to the process allocator. Setting an end of section symbol 3 ensures that the linker section has a fixed size. The kernel uses the start and end symbols to initialize the static data and the allocator.

If the static data is larger than the available process memory, the assertion 4 will throw an error at link time. The process memory is placed in the volatile memory selected in the RTOS configuration and initialization in flash.