Memory Protection
Memory is crucial concerning safety and security of an embedded system.
Functional safety is compromised if memory is manipulated by accident leading to a malfunction of a critical part of the software. Issues regarding memory can arise when:
- Data is passed between parts of the software and cast into the wrong type
- Data is written and read without synchronization (data race)
- Data is deallocated too early or multiple times (dangling pointer)
- Software reads or writes outside intended address spaces (overflow)
Rust solves 1. with strong typing and generics, 2. by enforcing synchronization with Sync
traits and 3. with ownership and lifetime checks when compiling code.
- is in part solved in the Rust language, as arrays and strings store their length with the data. Thus, copying strings will not write beyond bounds. Stack overflows, on the other hand, can still occur. One possible solution to reduce the risk of stack overflows is to use only a single stack. In the case of an RTOS, threads must either be cooperative (exiting the thread entry function every time) or hardware interrupts must be used for context switching, as in the RTIC real-time framework [43]. Another solution is to detect a stack overflow by setting and checking watermark patterns or by trapping an overflow in hardware.
Security is compromised if an attacker can read or can manipulate data on purpose. In microcontroller projects, security is often not regarded as a concern as these systems often run without connection to the outside world. With increased connectivity of embedded systems, a newly developed RTOS must have security in mind to avoid obsolescence in the coming years.
To prevent stack overflows and provide a secure system, Bern RTOS will rely on memory protection hardware. The main approach for safety and security is to form processes by splitting the application by criticality. Bern RTOS has the following use cases in mind:
Strict Allocating everything statically is the safest option as memory is guaranteed to be available at compile time. The kernel should allow for threads, stacks and sync primitives to be allocated statically, so that they can run for sure. Threads should run in isolation and the kernel should copy data between threads. This is the safest and most secure option.
Relaxed Making threads safe adds overhead at runtime, if a thread is less critical, it should be allocated in a fixed size pool of threads. To speed-up IPC, the kernel should provide a shared memory section where multiple threads have access to the same resource.
Dynamic Allocating memory dynamically simplifies the handling of dynamic loads of communication interfaces. It is the most memory efficient solution because memory is reserved only if it is used. However, an overload of a communication interface might block the system by using all of its memory.
A combination of the three use cases above should be able to run on the same system.
On a microcontroller, flash memory, SRAM, peripherals and other memory components are mapped to a single address space. To isolate and protect parts, it must be placed into sections, as shown in the figure.
Figure: Memory Protection: Memory Layout.
Different threads have different access privileges to different sections. Insecure network stacks can, for example, be denied access to peripherals, except the network interface. If threads cannot access a memory section directly, they must ask the kernel for help via a system call (syscall).
The most secure system would allow every peripheral access via a syscall, so that the kernel can check the access permission with fine granularity. This is done in Tock OS [56] but implies high overhead and HAL implementation by the OS maintainers. As a compromise, peripherals in the Bern RTOS should be protected by either allowing full access to parts of the memory section or none. That way peripherals can be somewhat protected, but the kernel is still independent of a HAL and access is efficient.
In terms of flexibility, the aim is for the Bern RTOS to use some of the hardware memory protection capabilities to make the kernel safe and secure, and to leave some to the user to use for application-specific purposes. For example, a user might want to restrict access to encryption keys.