Skip to content
Community

Reading the MeshCore Disclosure From the Field

A heap overflow in MeshCore was fixed in v1.14.0 without an advisory. The exploit primitive, the impact tiers, and why disclosure still matters.

J
Josh
· Updated May 5, 2026 · 11 min read

A security researcher who goes by Alainx277 published a writeup at the end of April titled “MeshCore’s problem with security.” On May 3rd, Mesh America ran a follow-up under the headline “Researcher Uncovers MeshCore Security Flaw That Was Fixed and Never Disclosed.” Both pieces are circulating in mesh circles right now. The short version: a heap overflow in path-packet handling was reported to MeshCore late last year, quietly fixed in v1.14.0, and never announced through the project’s release notes or any kind of advisory.

Diagram of the MeshCore ContactInfo heap overflow showing the 64-byte out_path buffer and the adjacent contact_count, shared_secret, and other struct fields that a 65 to 127 byte memcpy can clobber

Full disclosure on the lens. I lead security at my day job and do vulnerability research on the side. So this one hits closer to home than most disclosure stories that pass through here. We’ve also been running MeshCore across our network for months. The repeater at the shop, the building nodes, the car repeaters. So when this story hit our inbox we wanted to do three things: read the technical claims with the same scrutiny we’d apply to a finding at work, map the exploit primitive honestly, and figure out how worried an operator on a hobby network should actually be.

We haven’t reproduced the bug ourselves and we don’t have the original proof of concept. Everything below comes from public sources: Alainx277’s writeup, the MeshCore commit history, the v1.14.0 release notes, and the diff itself. The technical walkthrough that follows maps the bug, not the project.

What Was Reported

Alainx277 found a missing bounds check in MeshCore’s packet path handling. When a node receives a packet of type PAYLOAD_TYPE_PATH, the code reads a single-byte length field directly from the incoming bytes and uses it in a memcpy into a fixed 64-byte buffer inside the ContactInfo struct. There was no length check. A crafted packet on the same channel could cause that copy to spill past the buffer.

The fix landed as commit ca81f64 in early March, bundled inside the multibyte-paths merge that introduced the path hash mode work. The diff replaces the raw bounds compare with a new mesh::Packet::isValidPathLen validator and a copyPath helper that returns the actual length copied. Same release that we wrote up at the time for the multibyte path features. The security fix rode along with the routing changes. Nothing in the v1.14.0 release notes mentioned it.

Here’s the shape of the disclosure complaint. Researcher reports a bug. Maintainer ships a fix two weeks before the public deadline. No SECURITY.md. No GitHub Security Advisory. No release-note mention. The researcher only learned the fix was in by reading the diff line by line.

How the Exploit Primitive Actually Works

The headline you’ll see in some circles is “DOS flaw.” That’s true for one slice of the bug. It sells the rest short. The actual primitive is CWE-122 heap-based buffer overflow with a CWE-194 unexpected sign extension quirk that controls which impact tier a crafted packet lands on. Walk it with me.

The data flow. A LoRa packet arrives at the radio. The dispatcher reads the payload type byte. For PAYLOAD_TYPE_PATH packets, the parser reads the next byte as a path length and stores it in a uint8_t. That length flows through onPeerPathRecv() and onContactPathRecv() without ever being checked against MAX_PATH_SIZE. It lands in memcpy(from.out_path, out_path, out_path_len) where out_path is a 64-byte fixed buffer inside the ContactInfo struct. No bounds check. No length validation. The packet’s claimed length is the copy size.

The integer-coercion quirk. The path length lives in a uint8_t (0 to 255). The function signatures along the path coerce it through int8_t (-128 to 127) before it reaches memcpy. memcpy’s third argument is size_t. A negative int8_t sign-extends through int and then promotes to size_t as a near-maximal positive value. On the 32-bit nRF52840 and ESP32 targets that run MeshCore, that’s around 4 billion bytes. That single sign-extension event is what splits the impact tiers.

Tier one: crash. Path length 128 to 255 sets the high bit. After sign extension, memcpy gets a multi-gigabyte size. The copy walks off the heap, hits an unmapped or read-only region, and the MCU faults (MPU violation, hardfault, or bus error depending on where the runaway copy lands). The watchdog resets the node and it comes back online in seconds. This is the “DOS” headline.

Tier two: state corruption. Path length 65 to 127 fits cleanly with the high bit clear. memcpy gets exactly that many bytes and writes them into out_path plus whatever sits adjacent in the ContactInfo struct. Adjacent fields include the contact-count for the device, GPS coordinates, last-advert timestamps, and the per-contact shared_secret. The first practical primitive is overwriting the contact-count field with zero. The node and the connected app forget every contact. The radio still works. You re-add contacts. Annoying. Not catastrophic.

Tier three: key material overwrite. Important distinction up front. The bug is a write primitive, not a read one. An attacker cannot pull existing key bytes off a remote node. They can only write attacker-controlled values. With careful byte placement inside that same 65 to 127 window, an attacker can overwrite the public-key bytes for an existing contact in the target’s address book. After a successful overwrite, the target trusts a new identity bound to that contact name. That’s the impersonation primitive Alainx277 describes. The 127-byte ceiling actually caps the damage here. Alainx277 explicitly notes the limit “prevents a more dangerous attack” that would be reachable with a larger overflow window. Reliability stays conditional. The offset to a useful key field depends on which contact slot is targeted, and the target has to have that contact stored. Alainx277 frames it as a possibility, not a demonstrated exploit. The honest read is that this is the slice worth taking seriously even if it isn’t drive-by reliable.

Exploit characteristics. No authentication required. No network adjacency required beyond RF range. Single packet, no protocol state. Fits in a normal LoRa frame. Trivially scriptable on a $30 dev board on the same regional preset. Once a working PoC exists, distribution cost is zero.

So it’s a real bug with a real fix. The crash tier is genuinely hard to get worked up about on a public channel. The state-corruption and key-overwrite tiers are not. Same packet. Different outcomes.

Why a DOS on a Public Hobby Mesh Is a Strange Bar to Clear

Here’s the part that gets glossed over when these stories make the rounds. A public LoRa channel is open spectrum. License-free ISM. Anyone with a $30 board and the same regional preset can transmit on it. You don’t need a vulnerability to take down a hobby mesh. You need a transmitter and bad intent.

We’ve watched a single misconfigured repeater quietly degrade a region of the LoRa mesh by flooding with bad max-hops. No exploit. No CVE. Just a config error.

A motivated attacker with a Heltec V3 and a script can saturate a public channel within duty-cycle limits and bring traffic to a crawl. Same effect as the tier-one crash, basically. The difference is the crash is a packet, and the flood is a transmitter. The flood is also harder to stop, because the attacker doesn’t need any specific bug to be present.

That’s why “remote crash on public channel” is structurally weak as a threat model on hobby gear. The channel is the threat surface. The bug just adds one more way to misbehave on a surface that’s already wide open.

If our network were running on private channels with PSKs, a remote crash that someone could trigger without the key would be a much bigger deal. On a public channel, it’s another flavor of jamming.

What Would Actually Worry Us

Some bug classes deserve the alarm bell on a hobby network. A momentary crash isn’t one of them. Here’s where we draw the line.

Persistent corruption that survives a reboot. If a packet writes to flash in a way that requires re-flashing or a factory reset to recover, that’s a different conversation. Our solar repeaters don’t have a maintenance window. Climbing a building or pole to re-flash is the actual cost.

Key exfiltration on private channels. Private channels are how operators put real traffic on the network. A bug that leaks the PSK or breaks the encryption guarantee belongs in a different tier than a crash.

Spoofing or impersonation that defeats sender attestation. A packet that lets an attacker masquerade as a known contact would erode the trust model the network is built on. The tier-three theoretical key overwrite in this disclosure points at this, and it’s the part of the writeup we read carefully.

One-packet brick that needs physical recovery. This is the doomsday scenario for a deployed solar node. Nothing in this disclosure indicates that’s the actual impact. We checked.

The current bug doesn’t put us in any of those tiers in a clean, demonstrated way. The headline impact is a crash and a contact wipe. We can live with both.

Where the Disclosure Question Actually Has Teeth

The technical bug is patched. The disclosure handling is the part that still matters.

A SECURITY.md file in the repository costs nothing. A GitHub Security Advisory is a free feature on every public repo. A line in the release notes that says “this release contains a fix for a remotely-triggerable buffer overflow, please update” takes thirty seconds to write. None of that happened for this fix. The researcher reported the bug, suggested the project enable advisories, was told “that’s not a bad idea,” and then had to read the diff manually to confirm the fix had landed.

That part is fair criticism. A hobby project isn’t held to the same disclosure cadence as enterprise software, but the bar isn’t zero either. The same researcher previously published CVE-2025-24797 for a similar buffer overflow in Meshtastic, and that one was handled with a proper GHSA, an affected-version table, and a clear advisory. That’s the template. It’s free. The MeshCore project knows how it’s supposed to look because it’s right there in the sister project.

Reading this in the wider context of the recent MeshCore project split, the disclosure failure tracks. A project that’s been sorting out governance, trademark, and AI-code-attribution questions all at once isn’t going to nail security ops on the first try. That doesn’t excuse the gap. It explains it. And the path forward is the same either way: write the SECURITY.md, file the GHSA, mention security fixes in release notes plainly.

The disclosure handling is the visible symptom. The underlying cause is a codebase that leans heavily on raw C arrays, manual byte-level packet parsing, and bounds-check-by-convention. Alainx277 flagged that pattern in the writeup and he’s not wrong. The path-length bug is the kind of finding that AddressSanitizer, UBSan, and a coverage-guided fuzzing harness pointed at the packet parser would have surfaced before any human had to read the diff. None of that’s exotic. It’s the standard appsec playbook for any project shipping code that takes packets off the wire.

What We’d Like to See

A SECURITY.md at the root of meshcore-dev/MeshCore with a private reporting channel and an explicit timeline. GitHub Security Advisories enabled and used for the next user-impacting fix, with affected versions, fixed version, and a one-line impact summary. Release notes that flag security-shaped fixes alongside the feature notes. None of this is exotic. It’s the same bar we’d want from any open project we run gear on.

On the engineering side, the path is just as boring. Sanitizer-enabled CI builds catch the integer-coercion class of bug at compile and run time. A small fuzzing harness around the packet parser, run on every PR, catches the unchecked-length class. Both run free on GitHub Actions. Both would have caught this finding without needing a researcher to ride a 90-day clock. The disclosure side and the engineering side reinforce each other. You can have one without the other, but the project is stronger when both are in place.

We’d also like operators to update. If you’re on anything older than v1.14.0, the v1.14.1 and v1.15.0 releases both carry the fix. Roll forward. The bug is real, the patch is in, and there’s no good reason to be on a vulnerable build at this point.

Hobby gear running on hobby firmware doesn’t have to mean hobby disclosure practices. The fix is already shipped. The next disclosure can ride a better track.

We’ll keep watching how the project handles the next finding.

#meshcore #security #disclosure #vulnerability #heap-overflow #lora #responsible-disclosure #meshamerica #alainx277 #opinion

Comments