Memory Safety Is Still the Bug Class That Owns Your Firmware.
Decades on, the overwhelming majority of exploitable firmware bugs are still memory-safety bugs — on devices with no ASLR, no sandbox, and often no MMU to catch them.
It is faintly embarrassing, as an industry, that the single most common way firmware gets exploited is the same way it got exploited decades ago: a program writes past the end of a buffer. Memory-safety bugs — buffer overflows, use-after-free, out-of-bounds reads and writes — have been understood for a very long time, and they remain, year after year, the bug class behind the majority of serious, exploitable firmware vulnerabilities. The exploitation techniques have grown sophisticated. The root cause has not changed at all.
On embedded devices the situation is worse than on the servers and desktops that have spent those same decades accreting defences. The firmware is written in C, the mitigations that make memory bugs survivable elsewhere are frequently absent, and the attack surface — every byte of input the device parses — is both large and often exposed to untrusted sources. The bug class that the rest of computing has spent enormous effort learning to contain is, on a microcontroller, close to as dangerous as it was the day it was first described.
The oldest bug class, still winning
Memory-safety bugs come from a language that permits a program to touch memory it was never meant to. Write more bytes than a buffer holds and you corrupt whatever sat beside it. Keep using a pointer after its memory is freed and you operate on something that has since moved or changed. Index an array out of bounds and you quietly read or write a neighbour. In a memory-safe language these are impossible by construction, or caught the instant they happen. In C and C++ they are simply what occurs, silently, until something downstream breaks — or until an attacker learns to steer the corruption on purpose.
What makes this class the attacker's favourite is not merely that it is common, though it is. It is that these bugs convert cleanly into control. A carefully shaped overflow does not just crash the device; it overwrites a return address, a function pointer, or a length field, and turns "this input was a little too long" into "the device now executes code of the attacker's choosing." That conversion from memory corruption to code execution is the whole game, and memory bugs hand it over more reliably than any other category of defect.
Why firmware is a soft target
On a general-purpose operating system, a memory bug runs headlong into a wall of mitigations built up over many years. Address-space layout randomisation means the attacker cannot predict where anything lives. Non-executable data means an overwritten buffer cannot simply be run as code. Stack canaries notice an overwrite before a function returns. Process isolation means one corrupted program does not take the whole system with it. None of these are individually perfect, but together they downgrade a great many memory bugs from reliable exploits to mere crashes.
A typical microcontroller has few of these defences, or none. Often there is no address-space randomisation — the memory map is fixed, documented, and identical on every unit shipped. Frequently there is no MMU at all, and therefore no process isolation and no hardware concept of "memory this code may not touch." Code and data may share one flat address space the firmware reads and writes at will, with everything running at a single privilege level. The consequence is stark: a memory bug that would be a contained crash on a server becomes, on the microcontroller, a direct and often dependable path to full control — because the attacker knows exactly where everything is, having read the same datasheet you did.
The attack surface is every parser
The place memory bugs actually get reached is input parsing, and a connected device parses far more than its builders usually enumerate. Every network packet, every field in an update-image header, every reading from a sensor, every byte arriving over USB or a serial link, every entry in a stored configuration blob — each is untrusted input processed by code, and each piece of processing is a place a malformed input can trip a memory bug. The input does not have to be large or exotic. It only has to be unexpected in a way the code did not handle.
The dangerous parsers are often the ones nobody thinks of as parsers: the routine that reads a length field and then copies that many bytes, the one that walks a received structure assuming it is well-formed, the one that trusts a sensor to report a value within range. Attackers go straight for these, because a parser handling untrusted input is precisely where their bytes meet your memory. Any input that crosses from the outside world into one of your buffers is attack surface, whether or not the code that handles it was ever dignified with the name parser.
On a server a buffer overflow is usually a crash and occasionally an exploit. On a bare-metal microcontroller with a fixed memory map and no isolation, it is usually an exploit and occasionally a crash. The same bug, with the odds inverted.
What the MPU can and can't do
Many microcontrollers that lack a full MMU nonetheless include a Memory Protection Unit, and it is among the most underused defences in all of embedded. An MPU cannot give you virtual memory or per-process address spaces. What it can do is mark regions with permissions — this region is read-only, this one is no-execute, this one is off-limits to unprivileged code — and raise a fault the moment code violates them. That single capability turns a silent out-of-bounds write into a trap the device can detect and act on, instead of a corruption that proceeds entirely unnoticed until it surfaces as something worse.
Used well, an MPU enforces boundaries that are otherwise purely honour-system. It makes the stack non-executable, write-protects the bootloader and vector table, and isolates less-trusted code — a network stack, a parser for untrusted input — from the rest, so that a bug in one cannot freely rewrite everything else. This is not memory safety; a bug can still corrupt within a region it is allowed to touch. But it converts a large share of whole-device compromises into localised faults, and on a part with no MMU it is often the only hardware boundary on offer. Leaving it switched off — as a great many shipped products do — throws away exactly the containment the silicon was prepared to provide at no cost.
Memory-safe languages at the edge
The durable fix for a bug class rooted in the language is to use a language that does not have it. Memory-safe languages — Rust being the one with real traction on constrained targets — make the entire category impossible for the great majority of code, enforcing at compile time the rules that C merely trusts the programmer to follow. Bare-metal support has matured to the point where new firmware, and new components inside existing firmware, can be written this way without surrendering the low-level control that embedded work genuinely requires.
This is not a demand to rewrite a decade of working, debugged C overnight; that is rarely justified and still more rarely funded. It is a case for writing new code — and above all new parsers of untrusted input — in a language where the most dangerous bug class simply cannot occur, and for migrating the highest-risk components first. The highest-risk component is almost always the one interpreting bytes from a source you do not control, which is exactly where the switch to a memory-safe language repays its cost fastest and most visibly.
Defense in depth for the C you can't replace
Most firmware will keep a large body of C for a long time, and the realistic goal is to make its inevitable memory bugs expensive to exploit and survivable when they fire. That is a stack of deliberate, individually modest choices.
- Turn on every mitigation the part offers — stack canaries, non-executable data, and MPU-enforced permissions where they exist; these are routinely present in the toolchain and the silicon and left disabled, and each one raises the cost of turning a bug into an exploit.
- Put the MPU to work — a non-executable stack and heap, a write-protected bootloader and vector table, and isolation walls around any code that touches untrusted input, so that a memory bug faults instead of spreading across the device.
- Fuzz the parsers — feed malformed and hostile inputs to every routine that handles outside data, because fuzzing reaches the out-of-bounds cases that hand-written tests, built as they are around valid inputs, almost never exercise.
- Minimise and simplify the attack surface — fewer parsers, simpler formats, and strict length and bounds checks at every boundary; the input you refuse to accept at all is the input that can never trip a bug.
- Isolate what you cannot trust — run the riskiest code at the least privilege and the tightest memory permissions the hardware allows, so that compromising it is contained rather than total.
Memory safety is not an unsolved problem in embedded so much as a mostly-unapplied one. The defences exist, the safe languages exist, the hardware boundaries frequently exist and sit switched off — and meanwhile the same overflow that was dangerous decades ago remains the most likely way any given device is taken over. When 6QURE reviews firmware, the memory-safety question has two halves: where does untrusted input meet C, and what catches the bug when — not if — one of those parsers turns out to be wrong. A device that can answer the second half has turned its inevitable memory bugs into faults it survives. A device that cannot has turned them into somebody else's remote code execution.
Facing something like this in your own environment?