Nothing Is Quite as It Seems
Linus Torvald’s release of Linux 7.0 announced the finalization of Rust-related projects in the kernel: From now on, the programming language is to be considered a first-class member of Linux. This should be a good motivation to take a look at the current state of Rust in the embedded systems world and muse about the applicability of the hotly discussed programming environment.
If your wife hisses about the lack of first-class lounge access, removing her from the travel cadre alleviates the noise. This concept is the core motivation for Rust. In the past, a lot of security incidents were caused by issues in the (admittedly challenging) memory management found in C and C++.
The Rust developers’ approach was as simple as it was radical. By creating a programming language with a rigid memory ownership model, developers should, in theory, have no ability to produce memory-related security errors.
Vulnerability Analysis
As with most other silver-bullet promises, practical implementation falls short. A study of real-world attack vectors reveals that memory-related vulnerabilities are far less common than one might expect. For example, a study that analyzed security problems that surfaced in 2023 and 2024 found that, of all documented vulnerabilities, only about 20% were related to memory-management problems [1]. Shockingly, these 20% were tied with another 20%, which were caused by pure carelessness on the part of the developers—think, for example, about forgetting to validate execution permissions before running potentially sensitive or dangerous commands.
This should be kept in mind later, as we proceed to studying the first Rust-born vulnerability in Linux. Before, the unsafe system deserves an honorable mention. As described in the Rust documentation [2], an unsafe block can be erected to disable most, if not all, security features of the language:
let mut num = 5;
let r1 = &raw const num;
let r2 = &raw mut num;
unsafe {
println!(“r1 is: {}”, *r1);
println!(“r2 is: {}”, *r2);
}
The availability of the unsafe block, combined with the lower barrier of entry, leads to vulnerabilities such as CVE-2025-68260. As already mentioned, memory safety cannot protect against dim programming. Reducing the burden of entry can then be argued to enable more dim programmers to interact with system code.
This trend continues in the paper “A Closer Look at the Security Risks in the Rust Ecosystem“ [3]. The computer security experts provided the table shown in Figure 1, and ended the report with a highly telling conclusion:

Memory issues represented the largest number of vulnerabilities found in the Rust ecosystem, according to experts in a 2024 study.
“Among the 17 vulnerability types we identified, the memory safety and concurrency issues account for the majority of the disclosed vulnerabilities and demonstrate the fastest growth rates over time. One-third of the vulnerabilities have no fixes released by their public disclosure, leaving a window of opportunity for potential attacker exploitation.”
Rust on ESP32: A natural match
Ever since the Linux kernel development team began endorsing the Rust programming language, microcontroller vendors have started evaluating Rust stacks. In terms of hardware manufacturers, Espressif’s support of the Rust ecosystem is second to none: Not only is the ESP32-C3-DevKit-RUST-2 [4] a development board dedicated to Rust, it also supports the complete Rust ecosystem.
The language and its development are frequently mentioned in keynotes at Espressif’s yearly developer event. Furthermore, version 1.0 of the esp-hal package [5] was officially released in November 2025, marking the first official release of a Rust SDK by an established microcontroller vendor.
Rust developers need to be careful when selecting chips. As of this writing, two microcontroller families are significant: The XTENSA-based S class (including the original ESP32), and the RISC-V-based C class. Excellent Rust support is available only for the C class.
While an unofficial LLVM backend for XTENSA is available, it is not as widely supported as the backend for RISC/V (see Figure 2) [6].

Programming for the Xtensa in Rust involves delving into forked development versions and more complicated toolchains than required for programming for the ESP32.
As a result, the following steps will be performed on an ESP32-C6. As a workstation, an Ubuntu 24.04-based machine is used. Due to the proximity between the Rust and the Linux developer communities, Linux can be considered the first-class development environment for Rust-related tasks.
The first step is to deploy a Rust development environment on the workstation. This can be done by visiting the website https://rustup.rs. It will analyze the browser to determine the operating system and usually provide a command that can be entered at the command line.
For the Ubuntu 24 workstation, the invocation is curl –proto ‘=https’ –tlsv1.2 -sSf https://sh.rustup.rs | sh.
After execution, a few questions will pop up, which can be ignored by pressing enter—the default installation of Rust is enough for most embedded experiments.
When execution is complete, the installer will provide the message, “Rust is installed now. Great!” and provide some additional information about the path settings. This additional information comes in the messages shown in Figure 3.
After the main toolchain is deployed, users of the ESP32-C6 and ESP32-H2 need to run the following two commands. Their job is the deployment of microcontroller-specific elements:
tamhan@TAMHAN18:~$ rustup toolchain install stable –component rust-src
tamhan@TAMHAN18:~$ rustup target add riscv32imac-unknown-none-elf
When using the ESP32-C2 and ESP32-C3 course, execute the command rustup target add riscv32imc-unknown-none-elf instead—their core does not support as many of the RISC/V expansions. Make sure to select the stable version—the nightly build might provide more features, but it is derived from the HEAD and can contain all kinds of nasty bugs.
Finally, a few optional, but highly recommended modules should be downloaded:
tamhan@TAMHAN18:~$ cargo install esp-generate –locked
. . .
Installing /home/tamhan/.cargo/bin/esp-generate
Installed package `esp-generate v1.2.0` (executable `esp-generate`)
tamhan@TAMHAN18:~$ cargo install espflash –locked
tamhan@TAMHAN18:~$ cargo install esp-config –features=tui –locked
Do not be concerned if the deployment process takes more time than seems reasonable in the modern age. The Rust package manager, known as Cargo, will compile command-line utilities using your workstation’s compiler. When done, it will inform about the location where the binary file was placed. Finally, note that the individual expansions are referred to as crates in the Rust ecosystem.
Rust-based ESP32 project skeleton
Given that we downloaded the esp-config utility, we can proceed to create a working directory. In it, execute the following:
tamhan@TAMHAN18:~$ mkdir esp32rustp1
tamhan@TAMHAN18:~$ cd esp32rustp1/
tamhan@TAMHAN18:~/esp32rustp1$ esp-generate
The generator will prompt for the project name and the type of microcontroller to support. After that, a semi-graphical environment will pop up. It allows adjusting ESP32-specific settings; the defaults are sufficient for our experiments.
Afterward, press the S key on the keyboard to start generating the project skeleton. When complete, the system will provide output as shown in Figure 4.

A complete Rust environment installation is indicated with a presentation of the component version numbers installed.
At this point, a new directory called my-esp32-demo1 has been created. It contains a variety of code files that will make up our applications. In principle, .rs files contain Rust code.
In our example, the file ~/esp32rustp1/my-esp32-demo1/src/bin/main.rs is particularly important. Its structure, as shown in Listing 1, starts with the #![no_std] and #![no_main] attributes. The first is particularly important, as it declares that this project can do without the standard library.
Listing 1
The original program framework for our Rust project.
#![no_std]#![no_main]#![deny( . . .use esp_hal::clock::CpuClock;use esp_hal::main;use esp_hal::time::{Duration, Instant};#[panic_handler]. . .#[main]fn main() -> ! { let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); let _peripherals = esp_hal::init(config); loop { let delay_start = Instant::now(); while delay_start.elapsed() < Duration::from_millis(500) {} }}
Before proceeding to the Rust example, a quick discussion about some of the things that happened because of the instructions just executed is required. The invocation no_std declared that our embedded system works without the standard library.
— ADVERTISMENT—
—Advertise Here—
In a fashion similar to the C standard library, the Rust development team has a set of commonly used functions and data structures. Most notably, this includes basic linked lists and related types, which are commonly used in third-party crates [7].
Not using the standard library, as in our example, leads to a smaller executable size. On the other hand, developers seeking to integrate their Rust code with crates from repositories are encouraged to use the standard library, as it simplifies code reuse. We will remain with the non-standard version here.
After that, the various use declarations include parts of the HAL crate. It is an Espressif-provided library that simplifies access to the chip’s peripherals.
The block with the panic header, available in the online resource files, establishes a few additional constructs required for the program’s successful execution. Finally, the main block creates the actual runner. It first invokes configuration methods and afterward executes an endless loop.
Rust on an ESP32
Before running the code, connect the ESP32 development board to the workstation. The jumper shown in Figure 5 is required to supply power to the MCU; it allows the loop-in of an ammeter, which can be useful for power consumption measurements. However, a missing jumper and ammeter mean no power will be supplied to the microcontroller.

The ESP-32 board must include the noted jumper in order to power the board from the development workstation.
The connection between the workstation and the development board must now be made via the port labeled “UART.” Running dmesg or lsusb, then, permits an efficient checking of the connection stability.
The execution of our program can be handled via cargo run. When the command runs for the first time, libraries will be downloaded:
tamhan@TAMHAN18:~/esp32rustp1/my-esp32-demo1$ cargo run –release
Updating crates.io index
Locking 103 packages to latest Rust 1.88 compatible versions
After that, the program will start just like any other ESP32 application. The monitor can be closed by pressing CTRL+C. Interestingly, the startup process emits the line “App/part. size: 11,152/4,128,768 bytes, 0.27%“, which informs us that about 11kB worth of code is required for what is essentially an empty Rust project skeleton’s binary file.
System Performance and ESP_IDF
At this point, it is time to run an experiment to determine the operating frequency of the GPIO subsystem. Because we selected the No_STD environment, the APIs available in the ESP_IDF environment are not available. Instead, the API documented under the on the Expressis website [8] must be used.
Modifying our program starts with the use declarations. They must look as follows:
use esp_hal::{
gpio::{Level, Output, OutputConfig},
main,
};
Careful observers notice the removal of timing-related use declarations. This is necessary because the Rust compiler treats unused use declarations as errors, which block compilation. The rest of the program behaves like any other MSR application:
fn main() -> ! {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
— ADVERTISMENT—
—Advertise Here—
let mut led = Output::new(peripherals.GPIO7, Level::Low, OutputConfig::default());
led.set_high();
loop {
led.set_high();
led.set_low();
led.set_high();
led.set_low();
}
}
One interesting aspect involves the change in the declaration of the peripherals. Called _peripherals before, the variable now goes by the name peripherals. This change is required because the underscore in front of the name indicates that the object is unused; it is no longer needed, since the object is used to create the output. Another compile is required. Memory consumption is now shown as “11,248/4,128,768 bytes“.
At this point in time, the ESP32 board can be connected to both the modulation domain analyzer and the digital oscilloscope. Both Figure 6 and Figure 7 show that the Rust-based application demonstrates exceedingly stable waveform output.

The modulation domain analyzer indicates no meaningful modulation in the signal produced by the program running on the ESP-32.
Even though our Rust project skeleton does not use ESP-IDF or its accompanying real-time operating system, a brief comparison shall be performed. The first act involves downloading the same version of the IDF, which can be accomplished by running the parameters shown:
tamhan@TAMHAN18:~/esp551$ git clone -b v5.5.1 –recursive https://github.com/espressif/esp-idf.git
tamhan@TAMHAN18:~/esp551$ cd esp-idf/
tamhan@TAMHAN18:~/esp551/esp-idf$ ./install.sh esp32c6
— ADVERTISMENT—
—Advertise Here—
tamhan@TAMHAN18:~/esp551/esp-idf$ . ./export.sh
After that, the project skeleton found at: ~/esp551/esp-idf/examples/get-started/blink$ needs to be gutted. Listing 2 shows a minimal version, which can then be run as any other ESP32 program. When complete, the results shown in Figure 8 and Figure 9 will pop up.
Listing 2
This is the revised program after the complete rebuild to take an RTOS into account.
#include <stdio.h>#include “freertos/FreeRTOS.h”#include “freertos/task.h”#include “driver/gpio.h”#include “esp_log.h”#include “sdkconfig.h”static const char *TAG = “example”;static uint8_t s_led_state = 0;static void configure_led(void){ gpio_reset_pin(7); gpio_set_direction(7, GPIO_MODE_OUTPUT);}void app_main(void){ configure_led(); while (1) { gpio_set_level(7, 1); gpio_set_level(7, 0); gpio_set_level(7, 1); gpio_set_level(7, 0); }}

Performance reduced by the introduction of an RTOS is shown on the oscilloscope screen when running the second version of the software.
The slower performance can be explained by the greater amount of code in the ESP-IDF project skeleton. Not only is a real-time operating system started, but the program also loads library code that is unneeded in a bare-metal project. This leads to a total memory consumption of 0x1e3e0 bytes.
The extremely high stability observed in the modulation domain indicates that Rust’s memory management does not incur the usual latency penalties of classic garbage collection.
The Rust approach to memory management
Even though an in-depth analysis of Rust’s hardware access capabilities would be challenging, understanding how Rust handles memory safety is more important for an engineer seeking a qualified opinion.
Rust’s memory management differs from that in systems such as Java in that there is no asynchronous garbage collector. Instead, like Qt, the actual memory management is handled via ownership. A complete discussion of Rust’s memory management approach is not possible in a single article. Fortunately, documentation is available that provides an excellent overview of the key points [9].
Rust’s core idea is that any object gets destroyed when its reference is removed—this is done by invoking the drop function on it automatically as the reference disappears. An interesting challenge is raised by the code shown below, whose compilation will fail with an error along the lines of “error[E0382]: borrow of moved value: `s1`”:
let s1 = String::from(“hello”);
let s2 = s1;
println!(“{s1}, world!”);
A careful look at the code shows that the string—created by the from method—is passed onto the variable s2. This means that the reference in s1 becomes stale, and can no longer be used. Instead, a clone needs to be created like so:
let s1 = String::from(«hello»);
let s2 = s1.clone();
println!(«s1 = {s1}, s2 = {s2}»);
This version of the code invokes the clone() method which does what one can expect from its name. When the code has finished running, both s1 and s2 are removed, leading to the freeing of their respective memory allocations.
An interesting challenge occurs when passing complex objects—such as strings—into functions. This is best accomplished by passing in a reference in the fashion shown here:
fn main() {
let s1 = String::from(“hello”);
let len = calculate_length(&s1);
println!(“The length of ‘{s1}’ is {len}.”);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
Another interesting question involves returning values from a function. In theory, the code below could be used:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from(“hello”);
&s
}
Compiling it, sadly, fails with an error along the lines of “error[E0106]: missing lifetime specifier”. The reason for this is that the code returns a reference (line &s) to the string s, which is owned by the dangle function. As the dangle function ends, the compiler will remove the string via its drop function, leading to a reference pointing into the void.
The best way to handle the issue is to simply return a non-reference object as per the snippet shown here:
fn no_dangle() -> String {
let s = String::from(“hello”);
s
}
This way, the method no_dangle returns the ownership of the string to the caller—the error mentioned here will disappear.
The Outlook for Rust
The experiments performed have clearly confirmed the feasibility of using Rust on microcontrollers. From a technical point of view, embedded systems can be developed with a programming language. However, Rust is and remains but one of many C alternatives. In a greenfield design, a single engineer can choose the programming language that best suits his tastes and whims.
In real-world projects, a variety of constraints must be considered. The best programming language is worth nothing if no engineers are available. In Central Europe, Pascal was used purely because trained specialist engineers were available. A second important criterion is logical coupling: if an embedded device integrates into a larger codebase, reusing server code reduces software coupling. [10]
Finally, code quality can also be achieved without Rust: advanced static analysis tools and a strict programming standard, such as MISRA, can, in many ways, provide similar levels of safety while not requiring developers to learn a completely new language.
RESOURCES
Espressif | espressif.com
Rust | rust-lang.org
Ubuntu | ubuntu.com
REFERENCES
[1] https://horizon3.ai/attack-research/attack-blogs/analysis-of-2023s-known-exploited-vulnerabilities/
[2] https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html
[3] https://arxiv.org/abs/2308.15046
[4] https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32c3/esp32-c3-devkit-rust-2/index.html
[5] https://developer.espressif.com/blog/2025/10/esp-hal-1
[6] https://users.rust-lang.org/t/rust-support-for-esp32s3-in-vs-code/134377/2
[7] https://doc.rust-lang.org/std/
[8] https://docs.espressif.com/projects/rust/esp-hal/1.0.0/esp32c6/esp_hal/index.html
[9] https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
[10] https://www.youtube.com/watch?v=R6X30t_T5xo
Marvel at the Known Exploited Vulnerabilities Catalog
If an in-depth listing of security vulnerabilities is desired, I recommend visiting the website:
https://www.cisa.gov/known-exploited-vulnerabilities-catalog. The US government maintains a publicly documented list of known security vulnerabilities across all kinds of software.
PUBLISHED IN CIRCUIT CELLAR MAGAZINE • JULY 2026 #432 – Get a PDF of the issue
Sponsor this ArticleBefore entering the mobile market in 2004, Tam Hanna earned himself some money in the embedded design space...only to return to embedded design again with a lot of mobile phone programming experience. Find out more about my work and my cigar smoking habit at my Instagram account at https://www.instagram.com/tam.hanna/ - given that 20 000 people follow the Crazy Electronics Lab, you will be in good company - and do not forget that we smoke the bad cigars so that you don't have to!



