From 87f5219f181535859edb8339f895275569a0213d Mon Sep 17 00:00:00 2001 From: Deqing Sun Date: Thu, 4 Dec 2025 22:29:37 -0500 Subject: [PATCH 1/2] deal with safari fragmented pong data Pong in Chrome 8A 80 8B 9B 7E 64 Pong in Safari 8A 80 46 DC 9F 25 This commit addresses the pointer corruption that occurs when Safari sends a Pong. Without this change, the library will use the mask as beginning of the data packet and misbehave. Use _pinfo.masked as a counter to minimize change of the code. --- src/AsyncWebSocket.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/AsyncWebSocket.cpp b/src/AsyncWebSocket.cpp index 1bd0a160..065a8e01 100644 --- a/src/AsyncWebSocket.cpp +++ b/src/AsyncWebSocket.cpp @@ -515,7 +515,7 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { _pinfo.index = 0; _pinfo.final = (fdata[0] & 0x80) != 0; _pinfo.opcode = fdata[0] & 0x0F; - _pinfo.masked = (fdata[1] & 0x80) != 0; + _pinfo.masked = ((fdata[1] & 0x80) != 0) ? 1 : 0; _pinfo.len = fdata[1] & 0x7F; // async_ws_log_d("WS[%" PRIu32 "]: _onData: %" PRIu32, _clientId, plen); @@ -536,12 +536,20 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { data += 8; plen -= 8; } + } - if (_pinfo.masked - && plen >= 4) { // if ws.close() is called, Safari sends a close frame with plen 2 and masked bit set. We must not decrement plen which is already 0. - memcpy(_pinfo.mask, data, 4); - data += 4; - plen -= 4; + if (_pinfo.masked > 0 && _pinfo.masked < 5) { + // Handle fragmented mask data - Safari may split the 4-byte mask across multiple packets + while (_pinfo.masked < 5) { + if (plen == 0) { + //wait for more data + _pstate = 1; + return; + } + _pinfo.mask[_pinfo.masked - 1] = data[0]; + data += 1; + plen -= 1; + _pinfo.masked++; } } From 3c9bb75df032a916e1539dc44852394b3282f6da Mon Sep 17 00:00:00 2001 From: Mathieu Carbou Date: Fri, 5 Dec 2025 13:23:08 +0100 Subject: [PATCH 2/2] Restore _pinfo.masked to a boolean state (0 or 1) to keep backward compatibility. --- src/AsyncWebSocket.cpp | 53 +++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/src/AsyncWebSocket.cpp b/src/AsyncWebSocket.cpp index 065a8e01..08775828 100644 --- a/src/AsyncWebSocket.cpp +++ b/src/AsyncWebSocket.cpp @@ -518,9 +518,12 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { _pinfo.masked = ((fdata[1] & 0x80) != 0) ? 1 : 0; _pinfo.len = fdata[1] & 0x7F; - // async_ws_log_d("WS[%" PRIu32 "]: _onData: %" PRIu32, _clientId, plen); - // async_ws_log_d("WS[%" PRIu32 "]: _status = %" PRIu32, _clientId, _status); - // async_ws_log_d("WS[%" PRIu32 "]: _pinfo: index: %" PRIu64 ", final: %" PRIu8 ", opcode: %" PRIu8 ", masked: %" PRIu8 ", len: %" PRIu64, _clientId, _pinfo.index, _pinfo.final, _pinfo.opcode, _pinfo.masked, _pinfo.len); + // async_ws_log_w("WS[%" PRIu32 "]: _onData: %" PRIu32, _clientId, plen); + // async_ws_log_w("WS[%" PRIu32 "]: _status = %" PRIu32, _clientId, _status); + // async_ws_log_w( + // "WS[%" PRIu32 "]: _pinfo: index: %" PRIu64 ", final: %" PRIu8 ", opcode: %" PRIu8 ", masked: %" PRIu8 ", len: %" PRIu64, _clientId, _pinfo.index, + // _pinfo.final, _pinfo.opcode, _pinfo.masked, _pinfo.len + // ); data += 2; plen -= 2; @@ -538,24 +541,48 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { } } - if (_pinfo.masked > 0 && _pinfo.masked < 5) { - // Handle fragmented mask data - Safari may split the 4-byte mask across multiple packets - while (_pinfo.masked < 5) { - if (plen == 0) { - //wait for more data + if (_pinfo.masked) { + // Read mask bytes (may be fragmented across packets in Safari) + size_t mask_offset = 0; + + // If we're resuming from a previous fragmented read, check _pinfo.index + if (_pstate == 1 && _pinfo.index < 4) { + mask_offset = _pinfo.index; + } + + // Read as many mask bytes as available + while (mask_offset < 4 && plen > 0) { + _pinfo.mask[mask_offset++] = *data++; + plen--; + } + + // Check if we have all 4 mask bytes + if (mask_offset < 4) { + // Incomplete mask + if (_pinfo.opcode == WS_DISCONNECT && plen == 0) { + // Safari close frame edge case: masked bit set but no mask data + // async_ws_log_w("WS[%" PRIu32 "]: close frame with incomplete mask, treating as unmasked", _clientId); + _pinfo.masked = 0; + _pinfo.index = 0; + } else { + // Wait for more data + // async_ws_log_w("WS[%" PRIu32 "]: waiting for more mask data: read=%zu/4", _clientId, mask_offset); + _pinfo.index = mask_offset; // Save progress _pstate = 1; return; } - _pinfo.mask[_pinfo.masked - 1] = data[0]; - data += 1; - plen -= 1; - _pinfo.masked++; + } else { + // All mask bytes received + // async_ws_log_w("WS[%" PRIu32 "]: mask complete", _clientId); + _pinfo.index = 0; // Reset index for payload processing } } const size_t datalen = std::min((size_t)(_pinfo.len - _pinfo.index), plen); const auto datalast = data[datalen]; + // async_ws_log_w("WS[%" PRIu32 "]: _processing data: datalen=%" PRIu32 ", plen=%" PRIu32, _clientId, datalen, plen); + if (_pinfo.masked) { for (size_t i = 0; i < datalen; i++) { data[i] ^= _pinfo.mask[(_pinfo.index + i) % 4]; @@ -614,7 +641,7 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { } } } else { - // os_printf("frame error: len: %u, index: %llu, total: %llu\n", datalen, _pinfo.index, _pinfo.len); + // async_ws_log_w("frame error: len: %u, index: %llu, total: %llu\n", datalen, _pinfo.index, _pinfo.len); // what should we do? break; }