Skip to content

fix(http): prevent use-after-free from synchronous abort/close() - v2#466

Closed
mathieucarbou wants to merge 2 commits into
mainfrom
fix/use-after-free-shared_ptr
Closed

fix(http): prevent use-after-free from synchronous abort/close() - v2#466
mathieucarbou wants to merge 2 commits into
mainfrom
fix/use-after-free-shared_ptr

Conversation

@mathieucarbou

Copy link
Copy Markdown
Member

Same as #465 but a commit was added for further fixes to be able to test the result.

willmmiles and others added 2 commits July 21, 2026 22:11
…ncTCP 3.5.0)

Ensure that AsyncWebRequest objects are held in scope until the end of
their handler functions, avoiding any possible use-after-free cases
when aborted or closed.  This is managed by using `std::shared_ptr` to
allow a scoped lifecycle "lock" to be taken without an OS mutex.
…ared_ptr lifecycle

Four fixes to the shared_ptr-based request lifecycle introduced in the
previous commit:

1. Double-free in WebSocket / EventSource handoff (CRITICAL)

   create() sets _this as an owning shared_ptr, but AsyncWebSocket::_newClient()
   and AsyncEventSourceClient's constructor still called `delete request`
   directly.  When the shared_ptr later dropped its last reference it deleted
   the already-freed request a second time → heap corruption:

     CORRUPT HEAP assertion in std::list<AsyncWebParameter> destroyed a
     second time → multi_heap_free()

   Fix: added a public release() method that calls _this.reset().  Replaced
   both `delete request` calls with `request->release()` so destruction goes
   through the shared_ptr exactly once.  If a local shared_ptr hold (e.g. in
   _onData/_onPoll/_onAck) is on the stack, the request stays alive until that
   hold goes out of scope.

2. _onTimeout missing shared_ptr hold

   _onTimeout() called _client->close() without holding a local shared_ptr.
   close() triggers a synchronous _onDisconnect() → _this.reset() which can
   drop the last reference and destruct the request (and delete the
   AsyncClient) while close() is still on the stack.  Added the same
   `std::shared_ptr<AsyncWebServerRequest> self = _this;` hold used by
   _onData/_onPoll/_onAck.

3. _onAck shared_ptr hold placed after early return

   The `self = _this` hold was after the `if (!_response) return;` check,
   unlike _onData and _onPoll where it is at the top.  Moved it to the top
   of the function for consistency and to guard the early-return path
   against future regressions.

4. _onDisconnect() self-destruction during member execution

   _onDisconnect() did `this->_this.reset()` as its last statement.  If
   _this was the last owning shared_ptr, reset() destructed *this while the
   member function was still executing (undefined behavior).  Fix: take a
   local copy `self = _this` first, run the user callback with _this still
   valid, then reset the member.  The local copy keeps the object alive
   until the function returns, at which point the shared_ptr destructor runs
   safely.
@mathieucarbou
mathieucarbou requested a review from willmmiles July 22, 2026 10:54
@mathieucarbou mathieucarbou changed the title fix(http): prevent use-after-free from synchronous abort/close() fix(http): prevent use-after-free from synchronous abort/close() - v2 Jul 22, 2026
@mathieucarbou mathieucarbou added the Type: Bug Something isn't working label Jul 22, 2026
Comment thread src/WebRequest.cpp
}

void AsyncWebServerRequest::_onTimeout(uint32_t time) {
std::shared_ptr<AsyncWebServerRequest> self = _this; // Ensure we stay in scope over the function

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a waste of resources and should be omitted as it is not necessary for this function.

Comment thread src/WebRequest.cpp
Comment on lines +284 to +293
// Take a local copy of _this before resetting the member: if _this is the
// last owning shared_ptr, resetting it would destruct *this while this
// member function is still executing. The local copy keeps us alive until
// the function returns, then the shared_ptr destructor runs safely.
std::shared_ptr<AsyncWebServerRequest> self = _this;
if (_onDisconnectfn) {
_onDisconnectfn();
}
this->_this.reset(); // release shared pointer to this request, allowing for object destruction
_this.reset(); // release shared pointer to this request, allowing for object destruction
// 'self' goes out of scope here — if it was the last owner, the request is deleted

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect and unnecessary. Sorry I have not had a chance to post the detailed explanation yet.

Comment thread src/WebRequest.cpp Outdated
Comment on lines -249 to -255
std::shared_ptr<AsyncWebServerRequest> self = _this; // Ensure we stay in scope over the function
// os_printf("a:%u:%u\n", len, time);
if (!_response) {
return;
}

std::shared_ptr<AsyncWebServerRequest> self = _this; // Ensure we stay in scope over the function

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was intentional to avoid the cost of unnecessarily locking.

@mathieucarbou
mathieucarbou marked this pull request as draft July 22, 2026 13:11
@mathieucarbou

Copy link
Copy Markdown
Member Author

Closing this PR: it was only raised to allow a user to test the fixes introduced in the second commit.

PR #465 is the original one and @willmmiles will bring the necessary fixes into it.

@mathieucarbou
mathieucarbou deleted the fix/use-after-free-shared_ptr branch July 22, 2026 13:21
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.

2 participants