Skip to content

Fix request processing failures caused by failed allocations - #501

Open
mathieucarbou wants to merge 2 commits into
mainfrom
issue/500
Open

mathieucarbou wants to merge 2 commits into
mainfrom
issue/500

Conversation

@mathieucarbou

Copy link
Copy Markdown
Member

Summary

Fixes #500 and the other places where request processing broke when a heap allocation failed.

An Arduino String whose allocation fails is left empty, and its c_str() returns NULL on ESP32. Depending on where that happened, the library crashed, silently mis-parsed the request, corrupted the HTTP wire format, or served the wrong file.

Why

Issue #500 reported a crash in _parseReqHeader(): the Accept value was copied into a new String, the copy failed under heap pressure, and strstr(NULL, ...) hard-faulted in the async_tcp task. The same pattern existed in several other places, with different (but equally wrong) outcomes.

What changed

Header parsing (WebRequest.cpp, AsyncWebHeader.cpp)

  • New AsyncWebHeader::tryParse() returns ParseStatus::{Ok,Invalid,NoMemory} so a failed name/value copy fails the request rather than silently dropping the header. A dropped Content-Length or Transfer-Encoding desynchronizes the body parser.
  • Accept and Transfer-Encoding are now scanned in place — the lowercased copies are gone entirely, so the Null dereference in _parseReqHeader() when the Accept header copy fails to allocate (crash instead of rejecting the request) #500 crash cannot occur and chunked framing can no longer be silently missed.
  • Failed copies into _host, _contentType and _authorization abort the request instead of handing handlers corrupt state.
  • Failed header-line accumulation in _onData() and failed byte accumulation in the plain/multipart body parsers abort instead of truncating data.
  • The 100 Continue reply is written straight from the literal (the String copy exceeded the small-buffer size, and an OOM one silently stalled the client).

Responses (WebResponses.cpp, AsyncWebSocket.cpp, AsyncEventSource.cpp)

  • Every concat() in _assembleHead() is checked; on OOM the buffer is emptied and all four _respond() callers close the connection instead of writing a truncated head.
  • AsyncWebSocketResponse sets RESPONSE_FAILED when it cannot build Sec-WebSocket-Accept, and _sourceValid() reflects it, so the upgrade is refused instead of completing with a garbage accept header.

Allocation helpers

  • beginResponse*(), AsyncStaticWebHandler and AsyncEventSource use std::nothrow, making their existing null checks live instead of unreachable (plain new aborts the device on OOM). send() / _send() now handle a null response.
  • generateBasicHash() and the WebSocket printf helpers use malloc where the null checks were dead code.

Static file handler (WebHandlers.cpp)

  • If the request path cannot be built, canHandle() returns false so the request falls through to another handler / 404 instead of serving the default or a wrong file.

Known limitation (not addressed here)

std::list::emplace_back (headers, params), std::vector, std::make_shared (WebSocket message buffers) and the cbuf allocated inside AsyncResponseStream still use throwing-new, so an OOM there aborts the device. Fixing that needs custom allocators/containers and is out of scope for this change.

Validation

  • Builds: arduino-3, esp8266, raspberrypi, libretiny, arduino-2-esp-idf-log (core 2.x), no-json, regex, no-chunk-inflight, latest-asynctcp — all pass.
    • Note: the arduino-2 env itself fails before compiling any source because PlatformIO no longer serves the pinned tool-esptoolpy@^5.3.0; this happens on unmodified main too. The arduino-2-esp-idf-log env covers the same core.
  • cpplint with the CI filter set: 0 errors. codespell: clean. Formatted with the pinned clang-format 18.1.3.

Opened as a draft for review.

Abort the request instead of crashing, corrupting the wire format or
silently mis-parsing it when a heap allocation fails.

Arduino String leaves a failed string empty, and its c_str() is null on
ESP32.  Sites that copied header or body data and then passed c_str() to
C string functions could crash (issue #500), and sites that stored the
copy in request state silently lost or corrupted data.

Header parsing:
- Add AsyncWebHeader::tryParse() reporting ParseStatus::NoMemory so a
  failed name/value copy fails the request instead of dropping the header
  (a lost Content-Length desynchronizes the body parser).
- Scan Accept and Transfer-Encoding in place: the lowercased copies are
  gone, so issue #500 cannot happen and chunked framing is no longer
  silently missed.
- Abort on failed copies into _host, _contentType, _authorization and on
  failed header-line/body accumulation in the plain and multipart parsers.
- Write the 100-continue reply from the literal: the String copy exceeded
  the small-buffer size and an OOM one silently stalled the client.

Responses:
- Check every concat() in _assembleHead(); callers now close the
  connection instead of writing a truncated head (basic, abstract,
  WebSocket and SSE responses).
- AsyncWebSocketResponse flags RESPONSE_FAILED when it cannot build
  Sec-WebSocket-Accept, and _sourceValid() reflects it, so the upgrade is
  refused rather than completing with a garbage accept header.

Allocation helpers:
- Use std::nothrow for response objects created by beginResponse*(),
  AsyncStaticWebHandler and AsyncEventSource so failures are observable
  instead of aborting the device; send()/_send() handle a null response.
- Replace plain new with malloc in generateBasicHash() and the WebSocket
  printf helpers, where the null checks were dead code.

Static file handler:
- Stop and fall through to 404 when the path cannot be built, rather than
  serving the default or a wrong file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mathieucarbou mathieucarbou self-assigned this Oct 28, 2026
@mathieucarbou mathieucarbou added the Type: Bug Something isn't working label Oct 28, 2026
@mathieucarbou

mathieucarbou commented Oct 28, 2026 •

Copy link
Copy Markdown
Member Author

@willmmiles : this is a starting point, not fully tested yet.
I intend to strip the comments out (some comments are not useful).
Feel free to add more commits, cleanups, etc. We can work together on this one if you want to or see things missing.
Thanks!

Keep the code self-explanatory: the allocation change does not need an
inline comment in each of the call sites.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Null dereference in _parseReqHeader() when the Accept header copy fails to allocate (crash instead of rejecting the request)

1 participant