Skip to content

Commit 99d9464

Browse files
committed
Protect _client ptr usage in cases where a concurrent call to onDisconnnect could set it to null
1 parent 55a5709 commit 99d9464

1 file changed

Lines changed: 46 additions & 25 deletions

File tree

src/AsyncWebSocket.cpp

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -325,14 +325,12 @@ void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) {
325325
_controlQueue.pop_front();
326326
_status = WS_DISCONNECTED;
327327
async_ws_log_v("[%s][%" PRIu32 "] ACK WS_DISCONNECTED", _server->url(), _clientId);
328-
if (_client) {
329-
/*
330-
Unlocking has to be called before return execution otherwise std::unique_lock ::~unique_lock() will get an exception pthread_mutex_unlock.
331-
Due to _client->close() shall call the callback function _onDisconnect()
332-
The calling flow _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient()
333-
*/
328+
// Capture _client before unlocking: _client->close() triggers the _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient() chain,
329+
// so we must not access any member after unlock.
330+
AsyncClient *c = _client;
331+
if (c) {
334332
lock.unlock();
335-
_client->close();
333+
c->close();
336334
}
337335
return;
338336
}
@@ -472,18 +470,16 @@ bool AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint
472470
if (closeWhenFull) {
473471
_status = WS_DISCONNECTED;
474472

475-
if (_client) {
476-
/*
477-
Unlocking has to be called before return execution otherwise std::unique_lock ::~unique_lock() will get an exception pthread_mutex_unlock.
478-
Due to _client->close() shall call the callback function _onDisconnect()
479-
The calling flow _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient()
480-
*/
473+
async_ws_log_w("[%s][%" PRIu32 "] Too many messages queued: closing connection", _server->url(), _clientId);
474+
475+
// Capture _client before unlocking: _client->close() triggers the _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient() chain,
476+
// so we must not access any member after unlock.
477+
AsyncClient *c = _client;
478+
if (c) {
481479
lock.unlock();
482-
_client->close();
480+
c->close();
483481
}
484482

485-
async_ws_log_w("[%s][%" PRIu32 "] Too many messages queued: closing connection", _server->url(), _clientId);
486-
487483
} else {
488484
async_ws_log_w("[%s][%" PRIu32 "] Too many messages queued: discarding new message", _server->url(), _clientId);
489485
}
@@ -531,8 +527,13 @@ void AsyncWebSocketClient::close(uint16_t code, const char *message) {
531527
return;
532528
} else {
533529
async_ws_log_e("Failed to allocate");
534-
if (_client) {
535-
_client->abort();
530+
// Reads _client, then dereference it without any lock.
531+
// A concurrent _onDisconnect could null + delete the client between the check and the use.
532+
// Local capture ensures the pointer is read exactly once, eliminating the null-dereference.
533+
// (TOCTOU)
534+
AsyncClient *c = _client;
535+
if (c) {
536+
c->abort();
536537
}
537538
}
538539
}
@@ -548,17 +549,27 @@ void AsyncWebSocketClient::_onError(int8_t err) {
548549
}
549550

550551
void AsyncWebSocketClient::_onTimeout(uint32_t time) {
551-
if (!_client) {
552+
// Reads _client, then dereference it without any lock.
553+
// A concurrent _onDisconnect could null + delete the client between the check and the use.
554+
// Local capture ensures the pointer is read exactly once, eliminating the null-dereference.
555+
// (TOCTOU)
556+
AsyncClient *c = _client;
557+
if (!c) {
552558
return;
553559
}
554560
async_ws_log_v("[%s][%" PRIu32 "] TIMEOUT %" PRIu32, _server->url(), _clientId, time);
555-
_client->close();
561+
c->close();
556562
}
557563

558564
void AsyncWebSocketClient::_onDisconnect() {
559565
async_ws_log_v("[%s][%" PRIu32 "] DISCONNECT", _server->url(), _clientId);
560566
_status = WS_DISCONNECTED;
561-
_client = nullptr;
567+
{
568+
// Every queue method (_queueControl, _queueMessage, _runQueue, _onPoll, _onAck) reads _client while holding _queue_lock.
569+
// For those guarded reads to be meaningful, the write must also be synchronized. This doesn't change _queue_lock's purpose — it still guards queue integrity — but ensures the "is client alive?" checks that protect queue operations see a consistent value.
570+
asyncsrv::lock_guard_type lock(_queue_lock);
571+
_client = nullptr;
572+
}
562573
_server->_handleDisconnect(this);
563574
}
564575

@@ -951,17 +962,27 @@ bool AsyncWebSocketClient::binary(const __FlashStringHelper *data, size_t len) {
951962
#endif
952963

953964
IPAddress AsyncWebSocketClient::remoteIP() const {
954-
if (!_client) {
965+
// Reads _client, then dereference it without any lock.
966+
// A concurrent _onDisconnect could null + delete the client between the check and the use.
967+
// Local capture ensures the pointer is read exactly once, eliminating the null-dereference.
968+
// (TOCTOU)
969+
AsyncClient *c = _client;
970+
if (!c) {
955971
return IPAddress((uint32_t)0U);
956972
}
957-
return _client->remoteIP();
973+
return c->remoteIP();
958974
}
959975

960976
uint16_t AsyncWebSocketClient::remotePort() const {
961-
if (!_client) {
977+
// Reads _client, then dereference it without any lock.
978+
// A concurrent _onDisconnect could null + delete the client between the check and the use.
979+
// Local capture ensures the pointer is read exactly once, eliminating the null-dereference.
980+
// (TOCTOU)
981+
AsyncClient *c = _client;
982+
if (!c) {
962983
return 0;
963984
}
964-
return _client->remotePort();
985+
return c->remotePort();
965986
}
966987

967988
/*

0 commit comments

Comments
 (0)