ESP32 Wi-Fi Vulns: Coordinated Disclosure Is Broken by AI 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. This article was originally published on my personal blog: https://fughil.li/blog/esp32-c6-wifi-audit/ Two 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. Scope - 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 . - 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. - Both were reported together, both are fixed, and Espressif assessed both as duplicates of an earlier report. Neither received a CVE or an advisory. Why audit a binary blob at all This 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. That 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. The 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. Espressif 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. So 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. Sibling-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. 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 . ESP-001 — a heap overflow in MBSSID beacon reconstruction The feature Multiple-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. The 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. The bug The reconstruction writes into a fixed pooled esf buf , appending each merged information element in a loop with no destination-bounds check: // paraphrased from decompilation for src = first ie; src = ie end; src += src 1 + 2 { ... memcpy dst, chosen ie, chosen ie 1 + 2 ; // no dst < ebuf end check dst += chosen ie 1 + 2; } Because 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. The sibling that proves it Elsewhere 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: if accumulated + frag len < pTxRx + 0x400 - 0x5c memcpy reasm + accumulated, frag, frag len ; // bounded 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. Nailing the numbers on silicon The 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. From 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. 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. Delivering an oversized beacon An 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. A 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. Impact Remote, 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. ESP-002 — the hardware-GCM path drops an output-size check The contract mbedtls 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. The bug ESP-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: // hardware path: output length = input length; // no output size < input length guard esp aes crypt ctr &ctx- aes ctx, input length, ..., input, output ; // writes input length bytes Reachability This 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. This is a hardening / robustness issue, not a remote RCE. Verifying the fixes with no source ESP-002 is open source , so its fix is simply readable. Commit 7462e3c