{"slug": "esp32-wi-fi-vulns-coordinated-disclosure-is-broken-by-ai", "title": "ESP32 Wi-Fi Vulns: Coordinated Disclosure Is Broken by AI", "summary": "A developer auditing the closed-source Wi-Fi stack on the ESP32-C6 found two vulnerabilities: ESP-001, a remote pre-association heap out-of-bounds write in the 802.11 Multiple-BSSID beacon-reconstruction path of the binary-only libnet80211.a, and ESP-002, a missing output-buffer bounds check in the hardware-GCM path (esp_aes_gcm_update) that diverges from the upstream mbedtls contract. Both were reported to Espressif under coordinated disclosure and fixed in shipping ESP-IDF, but Espressif assessed them as duplicates of an earlier report, so neither received a CVE or advisory. The work grew out of memory-budgeting for the splanc computer-vision LED-mapping platform, whose nodes run on the ESP32-C6.", "body_md": "This article was originally published on my personal blog: https://fughil.li/blog/esp32-c6-wifi-audit/\n\nTwo findings in the ESP32-C6, both reported to Espressif under coordinated disclosure and both now fixed in shipping ESP-IDF. One is a remote, pre-association heap overflow inside a binary-only Wi-Fi blob; the other is a quieter divergence from the mbedtls crypto contract. This is about how you find a bug with no source, how you prove a fix landed with no source, and what the paper trail around the fix does — and doesn’t — tell the people shipping the part.\n\nScope\n\n- **ESP-001** — a remote, pre-association**heap out-of-bounds write** in the 802.11 Multiple-BSSID (MBSSID) beacon-reconstruction path, in the closed-source Wi-Fi library (`libnet80211.a` ).\n- **ESP-002** — a missing output-buffer bounds check in the hardware-GCM path (`esp_aes_gcm_update` ), a divergence from the upstream mbedtls contract. A hardening issue, not a remote RCE.\n- Both were reported together, both are fixed, and Espressif assessed both as duplicates of an earlier report. Neither received a CVE or an advisory.\n\n## Why audit a binary blob at all\n\nThis started as a side quest, and not a security one. I’ve been building [splanc](https://github.com/fughilli/splanc) — a computer-vision LED-mapping and lighting-control platform — whose nodes run on the ESP32-C6, and I was fighting for memory. The vendor’s Wi-Fi stack allocates from the same heap my application needs, and it exposes no way to constrain its heap usage at the granularity a reliable real-time system requires. To budget memory at all, I had to understand where the closed stack was spending it — which meant reading a blob I would rather not have had to read.\n\nThat reading turned into a different question. If I can’t see how this stack manages memory for something as ordinary as a buffer pool, what does it look like where memory management is security-critical — in the code that parses frames from strangers? The two bugs below are what fell out of following that question.\n\nThe ESP32-C6’s 802.11 stack ships as proprietary static libraries. There is no source. And yet it parses fully attacker-controlled radio frames from anyone in range, before any association or key exchange. That combination — no source, maximum attacker reach — is the highest-value surface on the part, and precisely the part you cannot read.\n\nEspressif is not unusual in this. Shipping the connectivity stack as a closed binary is the industry default, and it runs the whole length of the hardware spectrum. On Linux-class parts it is the norm — Broadcom’s `brcmfmac`, Qualcomm Atheros’s `ath10k`, and Realtek’s `rtw88` drivers all load vendor-supplied Wi-Fi firmware with no source, [aggregated](https://wiki.archlinux.org/title/Linux_firmware) in the non-free `linux-firmware` collection precisely because there is nothing to compile. It is just as true one tier down, on the microcontroller-class SoCs inside so much of the IoT: the ESP32 is one, and the Raspberry Pi Pico W is another — its Infineon CYW43439 Wi-Fi boots a [firmware](https://github.com/georgerobotics/cyw43-driver) whose source is proprietary, shipped only as a binary you fetch and are expected to treat as a black box. The method that follows isn’t really about one vendor, or even one class of chip — it’s about a surface the whole industry has agreed to keep closed.\n\nSo you read it anyway. The method is unglamorous: decompile the blob, rank functions by memory-safety red flags, and read the top candidates. The single highest-signal move is to look for a *sibling function that already bounds the same buffer*. A copy that omits a bound its neighbor enforces is the strongest static signal you can get in a codebase you can’t otherwise trust — the authors clearly knew the bound was necessary somewhere, and simply didn’t apply it here.\n\nSibling-hunting is only one way in, and not an obvious one. It was a direction the agents chose, not one I prescribed — and it is one of many ways to interrogate a binary this size, most of which remain unexplored, none of which needs a human to ideate or to drive.\n\n*The audit loop, start to finish, against a binary with no source. The highlighted step — finding a neighboring function that already bounds the same buffer — is what turns a hunch into a provable claim. On this part, the whole loop ran largely under autonomous-agent control (more on that below).*\n\n## ESP-001 — a heap overflow in MBSSID beacon reconstruction\n\n### The feature\n\nMultiple-BSSID (802.11 element ID `0x47`) lets one physical access point advertise many virtual BSSIDs compactly: a single *transmitted* BSSID carries a compressed list of *nontransmitted* profiles. To hand each virtual AP to the rest of the stack, the receiver synthesizes a full standalone beacon for every nontransmitted profile — expanding the compressed profile back into a complete beacon in a pooled buffer, then feeding it back through the normal beacon parser.\n\nThe relevant function is `ieee80211_parse_mbssid()` in `libnet80211`, reached from `hostap_recv_mgmt` through the beacon-parse path. No association, no key, no user interaction — just a frame on the air.\n\n### The bug\n\nThe reconstruction writes into a fixed pooled `esf_buf`, appending each merged information element in a loop with no destination-bounds check:\n\n```\n// paraphrased from decompilation\nfor (src = first_ie; src != ie_end; src += src[1] + 2) {\n    ...\n    memcpy(dst, chosen_ie, chosen_ie[1] + 2);   // no `dst < ebuf_end` check\n    dst += chosen_ie[1] + 2;\n}\n```\n\nBecause reconstruction *expands* a compressed profile into a full beacon, the output can exceed even a near-maximum-size input frame — and thus exceed the fixed pool buffer it is being written into.\n\n### The sibling that proves it\n\nElsewhere in the same codebase, `ppRxFragmentProc` (in `libpp`, RX defragmentation) reassembles into the *same* kind of buffer and gates every append on the buffer’s capacity:\n\n```\nif (accumulated + frag_len < *(_pTxRx + 0x400) - 0x5c)\n    memcpy(reasm + accumulated, frag, frag_len);   // bounded\n```\n\n`*(_pTxRx + 0x400)` is the exact value the MBSSID path passes to `ic_ebuf_alloc(0, 7, ...)` as the buffer size. The codebase already knows to bound writes against this capacity. The MBSSID reconstruction simply omits the check its neighbor performs.\n\n### Nailing the numbers on silicon\n\nThe pool-buffer capacity isn’t a source constant you can look up — it’s inside the blob — so I measured it directly, reading the field over JTAG on a running C6: `*(TxRxCxt + 0x400) = 0x6a4 = 1700 bytes`. Writes begin at offset `0x80`, which puts the overflow threshold at `1700 − 0x80 = 1572` reconstructed bytes.\n\nFrom there the model is concrete: a receivable beacon near 1700 bytes reconstructs to roughly 1644 bytes — about a **72-byte heap out-of-bounds write**. And below the threshold it behaves exactly as predicted: a 1490-byte beacon reconstructs to ~1414 bytes and does not overflow, with the heap-integrity check staying green. The sub-threshold case matching the model is what turns “probably unbounded” into a measured boundary.\n\n*The measured picture. The pooled buffer is 1700 bytes (read over JTAG); writes begin at offset* `0x80`*, leaving 1572 bytes before the end. A near-maximum beacon reconstructs to about 1644 bytes of information elements, so the final ~72 bytes land past the buffer, in adjacent heap. Below the threshold the model predicts no overflow — and on silicon, it doesn’t.*\n\n### Delivering an oversized beacon\n\nAn ESP-based injector cannot send the trigger. `esp_wifi_80211_tx` caps frames at 1500 bytes, below the ~1572-byte threshold. *That is a limit of the sender, not of the vulnerability.*\n\nA monitor-mode Wi-Fi adapter has no such cap. Using one on a test bench, a 1680–1690-byte crafted MBSSID beacon was put on the air, and a stock ESP32-C6 received and parsed it — removing the “can the frame even be delivered?” question for the reconstruction path. The exploitability case then rests on four independent legs: the JTAG-measured 1700-byte buffer, the decompiled unchecked append loop, the sibling `ppRxFragmentProc` that *does* bound the same buffer, and the on-silicon sub-threshold model match. A live crash additionally requires transmitting a ~1650-byte frame, which a monitor-mode card does trivially.\n\n### Impact\n\nRemote, pre-association heap corruption on any C6 that is scanning or connected within radio range. The attacker controls the overflow length and partially its contents. At minimum this is a denial of service; depending on adjacent heap layout it is a candidate for more.\n\n## ESP-002 — the hardware-GCM path drops an output-size check\n\n### The contract\n\nmbedtls 3.x added an `output_size` parameter to `mbedtls_gcm_update()` so the implementation can reject an undersized output buffer: `output_size < input_length → MBEDTLS_ERR_GCM_BAD_INPUT_DATA`. Callers are entitled to rely on that rejection.\n\n### The bug\n\nESP-IDF replaces the reference implementation with a hardware-accelerated port, `esp_aes_gcm_update()` in `components/mbedtls/port/aes/esp_aes_gcm.c`. It accepts `output_size` but only honors it on the software-fallback branch; the hardware path writes `input_length` bytes regardless:\n\n```\n// hardware path:\n*output_length = input_length;          // no `output_size < input_length` guard\nesp_aes_crypt_ctr(&ctx->aes_ctx, input_length, ..., input, output);  // writes input_length bytes\n```\n\n### Reachability\n\nThis is *not* reachable through mbedtls’s own TLS record layer, which sizes the GCM output buffer equal to the input length, so `output_size >= input_length` always holds. It *is* reachable by portable application or library code that calls `mbedtls_gcm_update()` directly with a fixed output buffer and trusts the documented rejection. Such code is safe on stock mbedtls and silently overflows on ESP targets. That divergence — the same code being safe on one implementation and unsafe on another — is the risk.\n\nThis is a hardening / robustness issue, not a remote RCE.\n\n## Verifying the fixes with no source\n\n**ESP-002 is open source**, so its fix is simply readable. Commit `7462e3c`<sup>[1]</sup> restores the exact `output_size < input_length` guard in `esp_aes_gcm_update()`. You can see it in the diff.\n\n**ESP-001 has no source diff.** The fix ships as a Wi-Fi blob bump — a new `libnet80211`. So I verified it the only way available: by disassembling the library before and after the bump.<sup>[2]</sup>\n\n``` php\nieee80211_parse_mbssid   size:            0x4a6  ->  0x4e2   (+60 bytes)\n                         conditional br:  27     ->  31\nloads of capacity field *(ctx+0x400):    1      ->  3\n```\n\nThe two new capacity loads are the bounds logic: a `bltu a5, s2, <drop>` comparison (capacity below running length → drop the frame) and a `capacity − 0x80` computation forming the append limit — exactly the `0x80` headroom the report had reverse-engineered. The drop path calls `ic_ebuf_recycle_rx`: on overflow the RX buffer is recycled rather than written past. You do not need source to confirm a bounds check landed; you need the two disassemblies.\n\n## Disclosure, versions, and a note on what the paper trail conveys\n\nThe reports went to Espressif’s bug bounty program, PGP-encrypted, with full root-cause analysis and proof-of-concept. Espressif replied promptly, assessed both findings as duplicates of an earlier report, and cited the two fix commits. I independently verified both fixes — ESP-002 at source level, ESP-001 at binary level — and asked about CVE IDs, version ranges, and whether an advisory was planned.<sup>[3]</sup>\n\nThe response: ESP-001 was confirmed as a valid pre-association issue, characterized as availability-only (a device crash), and slated for the normal release cycle — no CVE, no advisory. ESP-002 was classified as a hardening fix reachable only by an incorrectly-written caller — no CVE, no advisory. Espressif provided per-branch version tables; ESP-001 is fixed across six supported release branches (v5.2 through v6.1), ESP-002 across the active ones.<sup>[4]</sup>\n\nA few facts about how the ESP-001 fix reaches the people who ship the part:\n\n- Neither issue received a CVE or a security advisory.\n- The fix reaches downstream integrators only as an opaque `libnet80211` blob bump, pulled in under an ESP-IDF commit titled for an unrelated ESPTouch v2 change. The underlying blob commit message reads, in full,*“fixed the buffer overflow issues.”*\n- Because it is a binary blob, a downstream maintainer cannot inspect the diff to gauge severity. The commit text is all they have, and it does not convey that this is a remotely reachable, pre-association memory-safety fix.\n\nThe practical consequence is simple: if you ship an ESP32-C6, you likely want this update regardless of how the changelog reads — and working that out took the analysis above rather than reading an advisory. A CVE and a short advisory would have made the decision obvious to every downstream maintainer, none of whom can disassemble a blob to find out what changed.\n\nI don’t think this is anyone at Espressif acting in bad faith. I think it’s what the incentives produce. A vendor bears the cost of issuing an advisory — support load, optics, buyer questions — while the benefit of that transparency accrues mostly to downstream integrators and their users. When the party who pays for disclosure isn’t the party who benefits from it, “availability-only, normal release cycle, no advisory” is the rational path, and memory-safety fixes travel silently. That gap between who bears the cost of protection and who reaps it is the thing I keep circling back to<sup>[5]</sup> — and it’s why I’ve started sketching [a way to realign those incentives directly](http://substack.website.claude.localhost:8484/security-exchange/index.html).\n\n## Where this ships, and who’s choosing\n\nIt is worth being concrete about the stakes, because this is not a hobby part. Espressif has shipped more than a billion wireless-connectivity chips<sup>[6]</sup>, and its own documentation lists the ESP32’s intended application scenarios as “Smart Home, Industrial Automation, Health Care, Consumer Electronics, … POS Machines, …”<sup>[7]</sup> — health care and payment terminals on the same line as smart bulbs. I did not find an FDA-cleared medical device built on the part, and I make no such claim; but the research and prototype literature is full of ESP32-based patient-monitoring and vital-signs systems<sup>[8]</sup>, and the vendor is openly courting that market. A remotely reachable, pre-association memory-safety bug in the Wi-Fi stack of a part with that reach affects a very large deployed base.\n\nNow look at the decision that produced all those deployments. A builder choosing a connectivity SoC is optimizing something — and if the only objective is cost, the ESP32 is extraordinarily attractive: cheap, capable, superbly supported, already everywhere. Nothing in that calculus surfaces what I had to disassemble a blob to learn — how the closed stack handles hostile input, whether a remote memory-safety fix shipped silently, how the vendor classifies and discloses what it finds. That information exists; I just spent a while producing it by hand. But it isn’t available at the point of decision, so it doesn’t enter the decision. The builder who cares about their users has no way to price security in; the builder who doesn’t is never asked to.\n\nThis is the gap the rest of my writing is aimed at. Everyone who bears the consequences — researchers, users, enterprise buyers, and the device builders standing at the vendor-selection fork — should be able to see and price the *actual* security of what they depend on, so the choice can respect all of their interests instead of collapsing to whoever is cheapest. Visibility of the kind this post produced, made routine and comparable across vendors, is exactly the input that decision has been missing — and building the market that supplies it is [what I’m working toward next](https://fughilli.substack.com/p/a-security-exchange-that-rewards).\n\n## Found by machines\n\nOne fact about how this was done changes what the rest of it means. I did not spend weeks at a bench with a logic analyzer. This audit was carried out largely by autonomous agents driving hardware-in-the-loop rigs — real ESP32-C6 devices wired into test benches, flashed, exercised, instrumented, and measured under machine control, with me stepping in only at the edges. The decompilation triage, the search for a bounding sibling, the JTAG measurement, the before-and-after diff of the patched blob: that loop runs for long stretches with no human in it.\n\nThe capability is symmetric. If a memory-safety bug in a billion-device Wi-Fi stack can be found, characterized, and its fix independently verified by agents with minimal supervision, the same machinery points the other way just as well. Discovery, weaponization, and exploitation of this class of flaw are becoming things that happen at machine speed and machine scale, no longer gated by scarce human expertise. This isn’t speculation about the future; it’s how this post was produced.\n\nWhich is the reality the [last piece in this series](http://substack.website.claude.localhost:8484/security-exchange/index.html) is a response to. When both attack and defense are accelerating, what decides the outcome is whether the incentives, the evidence, and the money can move as fast as the machines. Right now they can’t — and closing that gap is the entire point.\n\n## Takeaways\n\n- “Find the sibling that bounds the same buffer” is a high-signal way to audit a binary-only parser — a missing check reads loudest against a neighbor that has it.\n- Measuring a pool-buffer capacity over JTAG turns a “probably unbounded” static finding into a concrete overflow threshold and a testable model.\n- You can verify a vendor’s fix inside a closed blob by diffing the disassembly. Source is convenient, not required.\n- Reachability scoping matters: ESP-002 is hardening, not RCE, and the ESP-001 injector cap is a sender limit, not the bug.\n\n## Notes\n\n1. ESP-IDF commit `7462e3c30a357d4018f70ac3192684e5ac83645e` , “fix(mbedtls): validate crypto input lengths” (2026-06-26). Open-source component; the guard is directly inspectable in the diff.\n2. The blob bump ships in ESP-IDF commit `ade59336b6a500618cee60ee698e4947efd35af1` , which advances the`esp_wifi` library from`c703130767` to`4e2d48c7ee` ; the underlying`esp32-wifi-lib` step is titled “fix(wifi): fixed the buffer overflow issues” (2026-07-21).\n3. Hardware validation was done with a monitor-mode Wi-Fi adapter on a test bench and field reads taken over JTAG on a running C6; specifics of the bench are omitted deliberately.\n4. Per-branch affected/fixed tables were provided by Espressif. ESP-001 is fixed across release branches v5.2–v6.1; ESP-002 across the active branches (v5.2 reached end-of-life on 2026-08-16 with no backport planned).\n5. The same question — who is responsible for protecting a party who cannot protect themselves, and what makes that responsibility binding rather than optional — is the one I wrote about in [The Monolith](http://substack.website.claude.localhost:8484/the-monolith/index.html) . This post is the small, concrete version of it.\n6. Espressif reports surpassing one billion cumulative shipments of its wireless-connectivity chips as of September 2023, since the 2014 launch of the ESP8266 — and the number has kept climbing. [Espressif, “Over 1 Billion Shipments Worldwide.”](https://www.espressif.com/en/news/1_Billion_Chip_Sales)\n7. The ESP32’s typical application scenarios, per Espressif’s own hardware design guidelines, are “Smart Home, Industrial Automation, Health Care, Consumer Electronics, Smart Agriculture, POS Machines, Service Robot, Audio Devices, …” [ESP32 product overview.](https://docs.espressif.com/projects/esp-hardware-design-guidelines/en/latest/esp32/product-overview.html)\n8. ESP32-based health monitoring is common in the research and prototype literature — see, for example, an [intelligent wearable system for measuring the vital signs of admitted patients](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC8399336/) . These are research and prototype systems; I found no FDA-cleared commercial medical device built on the part.", "url": "https://wpnews.pro/news/esp32-wi-fi-vulns-coordinated-disclosure-is-broken-by-ai", "canonical_source": "https://fughilli.substack.com/p/auditing-the-closed-esp32-c6-wi-fi", "published_at": "2026-09-17 21:08:31+00:00", "updated_at": "2026-09-17 21:25:32.600951+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools"], "entities": ["Espressif", "ESP32-C6", "ESP-IDF", "mbedtls", "splanc", "libnet80211.a", "Raspberry Pi Pico W", "Infineon CYW43439"], "alternates": {"html": "https://wpnews.pro/news/esp32-wi-fi-vulns-coordinated-disclosure-is-broken-by-ai", "markdown": "https://wpnews.pro/news/esp32-wi-fi-vulns-coordinated-disclosure-is-broken-by-ai.md", "text": "https://wpnews.pro/news/esp32-wi-fi-vulns-coordinated-disclosure-is-broken-by-ai.txt", "jsonld": "https://wpnews.pro/news/esp32-wi-fi-vulns-coordinated-disclosure-is-broken-by-ai.jsonld"}}