Buffer Overflow in Meshtastic: How One Missing Bounds Check Opens the Door to Remote Code Execution
Introduction
Mesh radio networks like Meshtastic are increasingly popular for off-grid, decentralized communication — used by hikers, emergency responders, preppers, and hobbyists worldwide. They're designed to work without internet infrastructure, relaying messages node-to-node over LoRa radio. That decentralized, open nature is a feature. But it also means any device within radio range can send packets to your node — including malicious ones.
This post breaks down a critical buffer overflow vulnerability discovered in meshtastic.cpp, explains how it could be exploited over the air, and walks through the fix. Whether you're an embedded developer, a security researcher, or just a Meshtastic user, this vulnerability is a reminder that trust boundaries matter even in hobbyist firmware.
The Vulnerability Explained
What Is a Buffer Overflow?
A buffer overflow occurs when a program writes more data into a fixed-size memory region than it was designed to hold. The excess data spills into adjacent memory, potentially overwriting other variables, control structures, or return addresses. In C and C++, this class of bug is responsible for some of the most severe exploits in computing history — from the Morris Worm in 1988 to modern IoT device compromises.
This vulnerability is classified as CWE-120: Buffer Copy without Checking Size of Input ("Classic Buffer Overflow").
Where Was the Bug?
In components/meshtastic/meshtastic.cpp, around line 449, the firmware handled incoming encrypted radio packets like this:
// VULNERABLE CODE (before fix)
memcpy(radio_buffer.payload, mp->encrypted.bytes, mp->encrypted.size);
Let's unpack what's happening here:
radio_buffer.payloadis a fixed-size buffer allocated on the stack or heap.mp->encrypted.bytesis the raw encrypted payload from an incoming radio packet.mp->encrypted.sizeis the attacker-controlled length field from that same packet.
The problem? There is no check that mp->encrypted.size is actually smaller than sizeof(radio_buffer.payload).
If an attacker sends a packet with encrypted.size set to 0xFFFF (65,535 bytes), the firmware will dutifully copy up to 65,535 bytes from the incoming packet data into a buffer that might only be 256 bytes wide. Everything past that 256-byte boundary gets overwritten — and that memory belongs to something else.
The Trust Problem: No Authentication on the Mesh
What makes this particularly dangerous is the threat model of Meshtastic itself. Mesh nodes do not authenticate each other. Any device with a compatible radio can broadcast packets that will be received and processed by nearby nodes. There's no TLS handshake, no certificate validation, no "is this a trusted sender?" check before transmit_radio_packet() is called.
This means the attack surface is:
- Any node within direct radio range (~2–15 km depending on terrain and antenna)
- Any node reachable through mesh relay — potentially much farther, as Meshtastic packets are relayed hop-by-hop
What Could an Attacker Do?
Depending on the memory layout of the target device (typically an ESP32), a successful overflow could:
- Corrupt heap metadata, causing undefined behavior or a crash (Denial of Service)
- Overwrite adjacent stack variables, changing program logic
- Overwrite a return address or function pointer, leading to arbitrary code execution
- Brick the device by corrupting critical firmware state
In the worst case, an attacker within radio range — or connected through the mesh — could achieve remote code execution on your ESP32 node without any physical access and without any credentials.
A Concrete Attack Scenario
Imagine Alice is running a Meshtastic node at a remote campsite for emergency communication. Bob, a malicious actor, is parked 3 km away with a compatible LoRa radio and a laptop. Bob crafts a raw Meshtastic packet with:
encrypted.size = 0xFFFF // 65535 bytes
encrypted.bytes = [carefully crafted payload]
Bob transmits this packet. Alice's node receives it, calls transmit_radio_packet(), and the vulnerable memcpy fires — copying 65,535 bytes into a small fixed buffer. Depending on what Bob put in those bytes, he may have just taken control of Alice's device.
The Fix
The fix is elegant in its simplicity. Before performing the memcpy, a bounds check was added:
Before (Vulnerable)
memcpy(radio_buffer.payload, mp->encrypted.bytes, mp->encrypted.size);
After (Fixed)
if (mp->encrypted.size > sizeof(radio_buffer.payload)) {
ESP_LOGE(TAG, "Encrypted payload too large: %u > %u",
mp->encrypted.size, sizeof(radio_buffer.payload));
return false;
}
memcpy(radio_buffer.payload, mp->encrypted.bytes, mp->encrypted.size);
Why This Works
The fix does three important things:
-
Validates the length before use:
mp->encrypted.sizeis compared against the actual size of the destination buffer usingsizeof(). This is a compile-time constant, so it will always reflect the true buffer size even if the code is refactored later. -
Fails safely: Instead of attempting the copy and corrupting memory, the function logs an error and returns
false. The packet is silently dropped. No crash, no corruption, no exploitation. -
Provides observability: The
ESP_LOGEcall logs the oversized packet with the actual sizes, which is invaluable for debugging and for detecting active exploitation attempts in the field.
The Diff at a Glance
+ if (mp->encrypted.size > sizeof(radio_buffer.payload)) {
+ ESP_LOGE(TAG, "Encrypted payload too large: %u > %u",
+ mp->encrypted.size, sizeof(radio_buffer.payload));
+ return false;
+ }
memcpy(radio_buffer.payload, mp->encrypted.bytes, mp->encrypted.size);
Four lines. That's all it took to close a critical remote code execution vector.
Prevention & Best Practices
This vulnerability follows a well-worn pattern. Here's how to avoid it in your own embedded C/C++ code:
1. Always Validate Lengths Before memcpy / strcpy / sprintf
Any time you copy data into a fixed-size buffer, ask: "Where does the length come from?" If it comes from user input, a network packet, or any external source — validate it first.
// Bad
memcpy(dest, src, untrusted_length);
// Good
if (untrusted_length > sizeof(dest)) {
return ERROR_TOO_LARGE;
}
memcpy(dest, src, untrusted_length);
2. Prefer sizeof() Over Magic Numbers
Using sizeof(buffer) instead of a hardcoded constant ensures your bounds check stays correct if the buffer size changes during refactoring.
// Fragile — breaks if PAYLOAD_SIZE changes
if (len > 256) { ... }
// Robust — always correct
if (len > sizeof(radio_buffer.payload)) { ... }
3. Use Safer Alternatives Where Possible
In higher-level C++ code, prefer std::vector, std::string, or std::span over raw buffers. For C-style copies, consider:
memcpy_s()(bounds-checking variant, available in C11 Annex K)std::copywith range checks- Custom wrapper functions that enforce size limits
4. Treat All Network Input as Hostile
In embedded networking code, every field in every packet is attacker-controlled. Length fields, type fields, flags — all of it. Apply the principle of "trust nothing from the network" consistently.
5. Enable Compiler and Runtime Protections
Modern compilers and runtimes offer mitigations that can reduce the impact of buffer overflows:
| Protection | How to Enable | What It Does |
|---|---|---|
| Stack canaries | -fstack-protector-all (GCC/Clang) |
Detects stack smashing at runtime |
| AddressSanitizer | -fsanitize=address (debug builds) |
Catches out-of-bounds accesses |
| FORTIFY_SOURCE | -D_FORTIFY_SOURCE=2 |
Adds bounds checks to libc functions |
| Static analysis | Clang-Tidy, Coverity, CodeQL | Finds bugs before runtime |
For ESP32/ESP-IDF projects specifically, enabling CONFIG_COMPILER_STACK_CHECK_MODE_STRONG in menuconfig adds stack overflow detection.
6. Fuzz Your Packet Parsers
Tools like AFL++ and libFuzzer can automatically generate malformed inputs to find buffer overflows before attackers do. If your firmware parses radio packets, fuzz the parser.
Security Standards & References
- CWE-120: Buffer Copy without Checking Size of Input — https://cwe.mitre.org/data/definitions/120.html
- OWASP: Buffer Overflow — https://owasp.org/www-community/vulnerabilities/Buffer_Overflow
- SEI CERT C Coding Standard: ARR38-C — Guarantee that library functions do not form invalid pointers
- MISRA C:2012: Rule 21.17 — Use of string handling functions
Conclusion
This vulnerability is a textbook example of why input validation is non-negotiable in networked embedded systems. A single missing bounds check turned a routine memcpy into a remote code execution vector exploitable by anyone with a LoRa radio.
The fix is four lines of code. The potential damage without it? Complete compromise of every vulnerable Meshtastic node within mesh reach.
Key takeaways:
- Never trust length fields from external sources — always validate against your actual buffer size.
- Fail safely — when validation fails, return an error and drop the input rather than proceeding with dangerous operations.
- Log anomalies — oversized packets are a sign of either bugs or active attacks; make them visible.
- The attack surface of mesh networks is wide — unauthenticated, multi-hop radio means your threat model must include remote, anonymous attackers.
Secure coding in embedded systems isn't glamorous, but it's essential. The next time you reach for memcpy, take two seconds to ask: "Did I check the length?" Your users — and their devices — will thank you.
This vulnerability was identified and fixed by OrbisAI Security. The fix has been verified by automated re-scan and LLM code review.