Conversation
When a single TCP delivery ends on exactly one byte of the next frame's header, the STATE_FRAME_START branch reads fdata[1] out of bounds and computes plen -= 2 on plen == 1, underflowing size_t to SIZE_MAX; the parser then walks wild memory until the device panics. The v3.12 websocket refactor (ESP32Async#462) added careful torn-frame handling for the mask bytes and the payload, but the base 2-byte header consume is still only guarded by 'while (plen > 0)'. Drop the un-parseable tail instead of crashing: the connection desynchronizes and is closed by the client, the same trade-off the adjacent extended-length guards already make.
|
Tested
Connection-survival check (the differentiator vs dropping): after each torn header, a follow-up frame is answered with telemetry within 400 ms — 5/5. The parser reassembles and keeps serving on the same connection; no desync, no drop. 420 s of continuous heartbeats across the whole run, no reboots, graceful accounting for the junk fragments. Inbound-performance note (per the earlier question in this thread): at our telemetry pull rate (10 Hz small frames) plus the 64-injection load, loop-task jitter was imperceptible in heartbeat cadence. We could not get clean numbers for high-rate inbound flood because of an unrelated baseline issue below. One caveat that is not this PR's fault: the v3.12.x base still carries the per-request heap decline + burst wedge we reported in #482, so our device stays on the 3.6.0 tag with our 2-line guard until that lands. The parser fixes themselves are verified good from our side. Closing this PR as superseded by #473 — thanks for the pointer and for the proper fix! |
Summary
When a single TCP delivery ends on exactly one byte of the next frame's 2-byte header, the
STATE_FRAME_STARTbranch ofAsyncWebSocketClient::_onData():fdata[1]out of bounds (only 1 byte is valid), anddata += 2; plen -= 2;onplen == 1, underflowingsize_ttoSIZE_MAX,after which the parser walks wild memory until the device panics.
The v3.12 websocket refactor (#462) added careful torn-frame handling for the mask bytes (
STATE_FRAME_MASK) and the payload, but the base 2-byte header consume is still only guarded bywhile (plen > 0). This PR adds the missing guard.Reproduction
Naturally occurs under retransmission/load (a segment happens to split at a frame boundary). Deterministic trigger with a raw-socket client that writes one header byte, flushes, waits, then writes the rest:
Evidence
Load access faultpanic atAsyncWebSocket.cpp:538, 3/3 runs under loadGuru Meditation Error: Core 0 panic'ed (Store access fault)-> reboot; occasionally the accept path stalls for seconds afterwardsTested on ESP32-C3 (arduino-esp32 3.2.1 / IDF 5.4, AsyncTCP 3.5.0); plain WS echo scenario, no TLS.
The fix
Dropping the un-parseable tail makes the connection desynchronize and get closed by the client - the same trade-off the adjacent
plen >= 2/plen >= 8/plen >= 4extended-length and mask guards already make.A larger alternative would accumulate the base header across deliveries with a new
STATE_FRAME_HEADERstate, mirroring theSTATE_FRAME_MASKmachinery - happy to rework in that direction if you prefer it. The minimal guard is offered first because it is trivially reviewable and the desync outcome is already how the other partial-header paths behave.Testing