← Writing

Building Trustworthy Embedded Systems with Rust

2025-06-16 rust · embedded · memory


In this article, I will explore:

🟠 If you work with hardware, write firmware, or are simply interested in the topic of memory safety, then this text is for you.


    The cost of a defect

The better we become as developers, the less we are willing to tolerate bugs. Users even more so. Today, no one is willing to wait, especially if money has been paid for the software. Everything must work stably, instantly and always.

But the reality is more complicated. The world of software, electronics and network communications is a huge, multi-layered organism. Errors in it are almost inevitable, especially when there is a person in the chain. And there is nothing more dangerous than self-confidence: “everything is stable, there are no defects.” In practice, they always exist. Some of them just haven’t fired yet.

The hardest thing to catch and predict are memory errors. They can be microscopic — imperceptible leaks that after 100,000 iterations lead to a crash or, worse, to undefined behavior.

And here the question becomes relevant: what the cost of one bug?
For a startup, it could be a microservice failure in the middle of a sale on a new mainstream toy, leading to the company’s closure.
For a corporation, it could be millions in losses due to a vulnerability in production.
And for an embedded system in an airplane with a glitched flap firmware, it could all end in a fatal crash.

We are used to seeing bugs in applications, the web, interfaces. But this is just the tip of the iceberg.
Under the hood of the modern world are complex systems invisible to the user: digital electronics, POS terminals, escalator controllers, microcontrollers in medicine, transport, and defense. They work quietly and ensure our everyday life. We simply do not notice them — until a failure occurs.

Developing for such systems is not just “another type of programming.” It is working with limited resources, strict standards, and zero tolerance for errors.
For many years, C/C++ reigned here: fast, flexible, but also extremely dangerous when working with memory. Segfault, use-after-free, race conditions — all these are the consequences of unsafe memory work, the price of which can be human lives.

Rust is a game changer.
It provides low-level control and deterministic behavior, but at the same time does not allow the developer to shoot himself in the foot.
Memory safety is built into the language architecture itself.
Therefore, Rust’s unique value proposition fully meets the needs for safe and modern software for critical systems.


    1. A quick overview of Rust’s memory fundamentals

ConceptDescriptionKey LayoutKey Benefit for Embedded Systems
OwnershipEach value has a single owner; memory is automatically freed when the owner goes out of scope.Compile TimeEliminates memory leaks, double frees, use-after-frees.
BorrowAllows temporary access to data (immutable or mutable) without transferring ownership; compiler ensures safety.Compile TimePrevents race conditions; ensures thread safety.
LifetimeEnsures that references are valid as long as the data they point to.Compile TimePrevents dangling pointers.
StackMemory for local variables and function call frames; LIFO; fixed size, known at compile time.Random Access Memory (RAM)Extremely fast and predictable allocation; minimizes fragmentation.
HeapMemory for dynamically allocated data; size determined at run time.Random Access Memory (RAM)Flexibility for dynamic structures (if explicitly managed); slower than stack.

    2. no_std: the foundation for embedded Rust

The no_std attribute specifies that the crate will reference libcore instead of libstd.

ModuleAvailable?Notes
core✅Core - arithmetic, Option, Result, iterators
core::fmt✅Formatting via core::write! and Debug
core::ops✅Operators: Add, Deref, Drop, etc.
core::cmp✅Comparison, PartialEq, Ord, etc.
core::ptr✅Working with pointers
core::mem✅Sizes, allocation, MaybeUninit
core::slice✅Working with slices
core::str✅Support for string slices
CategoryIn stdIn no_stdPossible alternative
PreludeFull std::preludecore::prelude, limitedby core and alloc::prelude
Panic (panic!)Yes, calls std::panic()Requires custom implementation#[panic_handler]
Hip/dynamicsYes Box, Vec, StringNo - no allocAdd alloc and allocator
Default allocatorYesNowee_alloc, dlmalloc, buddy_alloc, etc.
Files and File Systemstd::fs❌ NoNot supported
Networkstd::net❌ NoUse HAL/RTOS or smoltcp
Threadsstd::thread❌ NoRTOS / embassy / RTIC
SynchronizationMutex, RwLock, Condvar❌ Nospin, critical-section, cortex-m
Clocks and Timersstd::time❌ Almost everythingHardware timers, HAL
Standard Outputprintln!, std::io❌ Nodefmt, UART/ITM over HAL
I/Ostd::io::Read/Write❌ NoHAL drivers, embedded-hal
Formattingformat!, std::fmt✅ Partial (core::fmt)core::fmt without allocation
Standard collectionsHashMap, Vec, String❌ Noheapless, arrayvec, alloc
Error trait)std::error::Error❌ NoCustom traits or core::fmt::Debug
C FFI✅ Yes✅ Yesextern "C" works
Signals, processes, envstd::env, std::process❌ NoNot applicable
Randomnessrand, std::rand❌ Norand_core, hardware generator

Optional allocator

Although the no_std environment removes the default allocator, it does not completely disable dynamic memory allocation capabilities (e.g. Vec, String).
To reintroduce this capability, the controlled alloc gateway from no_std must be used.
After hooking into this gateway, a custom allocator must be implemented and passed to it alloc from the existing crates specifically designed for this purpose, dlmalloc, buddy_alloc or wee_alloc. These allocators typically implement the core::alloc::GlobalAlloc trait.


    3. Memory allocation strategies in constrained environments

3.1 Static vs Dynamic Memory Allocation

3.2 Heap in no_std

⭐ Best practices for heap management:

3.3 Stack in no_std

The most common problem that happens with the stack is stack overflows, which causes the program to overwrite adjacent memory, which can lead to UB or crashes.

⭐ Best practices for stack management:

⭐ Monitoring stack memory usage:

A general recommendation for managing problems like stack overflows is to bring these errors into the realm of complete determinism, from UB to guaranteed and predictable failure.


    4. Hardware Memory Architectures and Protection

4.1 Microcontrollers Memory

Memory TypeVolatilityPrimary UsageTypical Write CyclesAccess Speed ​​(Relative)
FlashNon-VolatileProgram Code, Constants1,000 - 10,000Fast Reads, Slow Writes
SRAMVolatileStack, VariablesBillionsVery Fast
EEPROMNon-volatileConfiguration data, rarely changed100,000Slower than SRAM

4.2 Memory Layout and Sections

While Rust provides high-level memory safety, the linker script is the critical low-level component that translates these abstractions into physical memory addresses.
In the absence of OS, linker dictates where code and data segments are located in Flash and RAM, and provides the ability to precisely define stack, heap, and static data regions to ensure that Rust’s memory model is correctly implemented on the target hardware.

With #[export_name], #[no_mangle], and #[link_section], you literally control how the compiled Rust code fits into memory, and what becomes the entry point for the hardware. Without this, the firmware won’t work.

You can 📖 read more about the role of the linker and the distribution of data across memory sections in my recent article.

4.3 Memory Protection Units (MPUs) and Memory Management Units (MMUs)

Rust’s memory safety guarantees are mostly provided at compile time.
However, hardware features such as the MPU and stack limit registers provide a crucial runtime layer of protection, especially against logic errors that can occur in unsafe code.


    5. Concurrency and Shared State Management

5.1 Send and Sync concurrency Primitives.

These are marker traits with no methods, which are automatically inferred if the type consists entirely of Send or Sync types.

They report thread safety information at the type system level.
However, there are exceptions to the rule:

In embedded systems, concurrency often arises from interrupts (ISRs) and real-time tasks.
Applying Send and Sync to static mut variables and shared resources, in combination with critical sections or atomic operations, ensures that even in bare-metal or RTOS environments, race conditions are prevented at compile time.

5.2 Smart pointers in embedded systems

Smart pointers provide abstractions for managing memory and shared state at the cost of some overhead.
In resource-constrained embedded systems, these overheads require careful consideration.

PrimitiveThread-SafeMemory OverheadPerformance ImpactPrimary Use CaseKey Limitation/Feature
Rc<T>NoneLow (reference count)FastShared ownership in single-threaded scenariosNot thread-safe
RefCell<T>NoneLowFastInternal mutability in single-threaded scenariosNot thread-safe
Arc<T>YesModerate (atomic count)Fast (for cloning), low (for dereferencing)Shared ownership in multi-threaded scenariosDefault data immutability
Mutex<T>YesModerate (lock structure)Potential contention/blockingExclusive access to shared mutable dataMay lead to deadlocks; locking overhead

The preference for heapless collections and static memory allocation over heap-based Arc and Box in many embedded contexts indicates a conscious trade-off: raw efficiency is prioritized unless the complexity of shared ownership requires absolutely smart pointers.

5.3 Critical Sections and Atomic Operations

Critical sections are a basic primitive for concurrency control, literally a global mutex that can only be acquired by one thread/context at a time.

The critical-section crate provides a generic API for critical sections across various target platforms.
It provides safe access to shared mutable data (e.g. Mutex<Cell<u32>>) in an interrupt-safe context.

PrimitiveThread-SafeMemory OverheadPerformance ImpactPrimary Use CaseKey Limitation/Feature
Critical Section (disable interrupts)Yes (on single core)LowHigh interrupt latency, jitterProtects shared data from interruptsDoes not guarantee exclusivity on multi-core systems
Atomic OperationsYesLowMinimal overheadSafe read-modify-write operations; inter-core synchronizationDepends on hardware support; more complex memory model

The choice between disabling interrupts and using fine-grained atomic operations reflects a tradeoff between simplicity/guarantees and performance/latency.

5.4 RTOS and concurrency frameworks

Rust supports integration with real-time kernels and concurrency frameworks for multitasking.


    6. Some approaches to memory optimization

6.1. DST and fat pointers

References or raw pointers to a DST are fat pointers, and take up 2x more memory than regular pointers (data + vtable).

Trait objects (dyn Trait) are a common DST. They provide dynamic dispatch (runtime method resolution via vtables). Dynamic dispatch via vtables also incurs a small runtime overhead compared to static dispatch.

In resource-constrained embedded systems, the cost of flexibility from using DSTs and fat pointers becomes more obvious. The choice involves a tradeoff between

6.2. Move vs Copy

In Rust, by default values ​​are “moved” when assigned or passed to a function.

Although Copy and Move are often equivalent in performance for small types.
For large fixed-size data structures that could be Copy (e.g. [u8; 1024]), the decision whether to implement Copy directly affects whether a full memcpy will occur on assignment.

6.3. Memory Alignment Considerations

Data alignment ensures that data structures are allocated at memory addresses that are multiples of their size or the specified alignment.

This is critical for performance (CPUs often read data in machine word sized chunks) and correctness (unaligned accesses can throw exceptions or be slower).
The Rust (and LLVM) compilers handle alignment automatically. Struct fields are padded to align with the largest alignment of any of their fields.

size_of::<(char, u8, i32)>();   // 12
align_of::<(char, u8, i32)>();  // 4

By deliberately designing structs with alignment in mind (e.g. by ordering fields by size or using #[repr(align)]), to ensure optimal cache usage and single-cycle memory accesses, even though the compiler will eventually align the data anyway.

6.4. Best practices for minimizing memory usage and optimizing performance


    7. Mission Critical Systems and Certification

7.1. Rust for Mission Critical

AspectRust Approach (Built-in Guarantees)C/C++ Approach (Optional Safeguards)
Memory Safety and OwnershipGuaranteed by compiler ownership modelManually managed; depends on developer/tooling discipline
Garbage CollectorNone (managed at compile time)None (manually managed)
Buffer Overflow PreventionPrevented at compile timeDepends on developer discipline, tools, and runtime checks
Use-After-Free PreventionGuaranteed by ownership modelDepends on developer discipline, tools, and runtime checks
Race Condition PreventionEnforced by type system (Send/Sync)Manually managed thread safety; error prone
Stack and Heap ExploitsMitigated by structured memory safety mechanismsGoverned by coding conventions and checks
unsafe code annotationExplicitly marked (unsafe blocks)Undefined behavior is governed by coding conventions and checks

7.2. ISome Industry initiatives

Safety Critical Rust Consortium: The Rust Foundation, in partnership with AdaCore, Arm, Ferrous Systems, and Woven by Toyota, formed this consortium to support the responsible use of the Rust language in mission-critical software. Their focus includes developing guidelines, linters, libraries, static analysis tools, formal methods, and language subsets to meet industry and legal requirements.

Ferrocene: A key development is Ferrocene, a qualified open-source Rust compiler toolchain for mission-critical systems.
It is qualified for automotive (ISO 26262 ASIL D), industrial (IEC 61508 SIL 4) and medical (IEC 62304 Class C) design.