From 9da97e5e88c73b1e2ce83a995f53cefd8a470739 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 29 Mar 2026 19:36:32 -0700 Subject: [PATCH] fix: partial send on non-blocking socket silently dropped data A single send() that returned fewer bytes than requested was logged but not retried, leaving the server with a truncated packet. This causes an irreversible TCP framing desync (next header lands mid-payload) that manifests as a disconnect under network pressure. Added a retry loop that handles EWOULDBLOCK with a brief yield. Also rejects payloads > 64KB instead of silently truncating the 16-bit CMSG size field, which would have written a wrong header while still appending all bytes. --- src/network/world_socket.cpp | 37 +++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/network/world_socket.cpp b/src/network/world_socket.cpp index 08d69b61..ba15003d 100644 --- a/src/network/world_socket.cpp +++ b/src/network/world_socket.cpp @@ -314,6 +314,14 @@ void WorldSocket::send(const Packet& packet) { const auto& data = packet.getData(); uint16_t opcode = packet.getOpcode(); + // CMSG header uses a 16-bit size field, so payloads > 64KB are unsupported. + // Guard here rather than silently truncating via cast, which would write a + // wrong size to the header while still appending all bytes. + if (data.size() > 0xFFFF) { + LOG_ERROR("Packet payload too large for CMSG header: ", data.size(), + " bytes (opcode=0x", std::hex, opcode, std::dec, "). Dropping."); + return; + } uint16_t payloadLen = static_cast(data.size()); // Debug: parse and log character-create payload fields (helps diagnose appearance issues). @@ -426,14 +434,29 @@ void WorldSocket::send(const Packet& packet) { " payload=", payloadLen, " enc=", encryptionEnabled ? "yes" : "no"); } - // Send complete packet - ssize_t sent = net::portableSend(sockfd, sendData.data(), sendData.size()); - if (sent < 0) { - LOG_ERROR("Send failed: ", net::errorString(net::lastError())); - } else { - if (static_cast(sent) != sendData.size()) { - LOG_WARNING("Partial send: ", sent, " of ", sendData.size(), " bytes"); + // Send complete packet, retrying on partial sends. Non-blocking sockets + // can return fewer bytes than requested when the kernel buffer is full. + // Without a retry loop, the server receives a truncated packet and the + // TCP stream permanently desyncs (next header lands mid-payload). + size_t totalSent = 0; + while (totalSent < sendData.size()) { + ssize_t sent = net::portableSend(sockfd, sendData.data() + totalSent, + sendData.size() - totalSent); + if (sent < 0) { + int err = net::lastError(); + if (net::isWouldBlock(err)) { + // Kernel buffer full — yield briefly and retry. + std::this_thread::sleep_for(std::chrono::microseconds(100)); + continue; + } + LOG_ERROR("Send failed: ", net::errorString(err)); + break; } + if (sent == 0) break; // connection closed + totalSent += static_cast(sent); + } + if (totalSent != sendData.size()) { + LOG_WARNING("Incomplete send: ", totalSent, " of ", sendData.size(), " bytes"); } }