From c1e617519fbe1a3cda9bbc08f178eade8f410653 Mon Sep 17 00:00:00 2001 From: jrd Date: Thu, 6 Aug 2026 16:09:22 +0000 Subject: [PATCH 1/3] JSON-RPC: bound the per-connection read buffer An unauthenticated client that sends no newline grows the QTcpSocket read buffer without bound (setReadBufferSize is never called), before auth, until std::bad_alloc aborts the process and drops all clients. Bound the buffer to 64 KiB per connection and drop a connection whose buffer fills with no complete line. Co-Authored-By: Claude Opus 4.8 --- src/rpcserver.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/rpcserver.cpp b/src/rpcserver.cpp index 2c05c2f12d..0356953b59 100644 --- a/src/rpcserver.cpp +++ b/src/rpcserver.cpp @@ -110,6 +110,13 @@ QJsonObject CRpcServer::CreateJsonRpcErrorReply ( int code, QString message ) return object; } +// Maximum size of a single JSON-RPC request line. An unauthenticated client that +// sends data without a terminating newline is only ever consumed on a complete line +// (canReadLine()), so without a bound the received bytes accumulate in the socket read +// buffer without limit until the process is killed by the allocator. Requests larger +// than this, or unterminated data that fills the buffer, are rejected instead of held. +static constexpr int MAX_JSON_RPC_REQUEST_BYTES = 64 * 1024; + void CRpcServer::OnNewConnection() { QTcpSocket* pSocket = pTransportServer->nextPendingConnection(); @@ -122,6 +129,9 @@ void CRpcServer::OnNewConnection() vecClients.append ( pSocket ); isAuthenticated[pSocket] = false; + // Bound the per-connection read buffer so unterminated input cannot exhaust memory. + pSocket->setReadBufferSize ( MAX_JSON_RPC_REQUEST_BYTES ); + connect ( pSocket, &QTcpSocket::disconnected, [this, pSocket]() { qDebug() << "- JSON-RPC: connection from:" << pSocket->peerAddress().toString() << "closed"; vecClients.removeAll ( pSocket ); @@ -197,6 +207,14 @@ void CRpcServer::OnNewConnection() pSocket->disconnectFromHost(); return; } + + // A full buffer with no complete line is an oversized or unterminated request: + // reject and close rather than hold the bytes indefinitely. + if ( !pSocket->canReadLine() && pSocket->bytesAvailable() >= MAX_JSON_RPC_REQUEST_BYTES ) + { + Send ( pSocket, QJsonDocument ( CreateJsonRpcErrorReply ( iErrParseError, "Parse error: Request exceeds maximum size" ) ) ); + pSocket->disconnectFromHost(); + } } ); } From a98ab3f5b40fd44ec640348ba7bcad775418a12a Mon Sep 17 00:00:00 2001 From: jrd Date: Tue, 11 Aug 2026 04:39:09 +0000 Subject: [PATCH 2/3] JSON-RPC: lower the request bound to 16 KiB and state it in the error --- src/rpcserver.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/rpcserver.cpp b/src/rpcserver.cpp index 0356953b59..b5724a3984 100644 --- a/src/rpcserver.cpp +++ b/src/rpcserver.cpp @@ -115,7 +115,11 @@ QJsonObject CRpcServer::CreateJsonRpcErrorReply ( int code, QString message ) // (canReadLine()), so without a bound the received bytes accumulate in the socket read // buffer without limit until the process is killed by the allocator. Requests larger // than this, or unterminated data that fills the buffer, are rejected instead of held. -static constexpr int MAX_JSON_RPC_REQUEST_BYTES = 64 * 1024; +// The largest single request is a welcome or chat message, which CServer truncates to +// MAX_LEN_CHAT_TEXT (1600) characters and which is 9698 bytes as a compact JSON line +// when every character is escaped as \uXXXX; 16 KiB leaves 1.6x that, or room for a +// batch of about 220 ordinary calls. +static constexpr int MAX_JSON_RPC_REQUEST_BYTES = 16 * 1024; void CRpcServer::OnNewConnection() { @@ -212,7 +216,10 @@ void CRpcServer::OnNewConnection() // reject and close rather than hold the bytes indefinitely. if ( !pSocket->canReadLine() && pSocket->bytesAvailable() >= MAX_JSON_RPC_REQUEST_BYTES ) { - Send ( pSocket, QJsonDocument ( CreateJsonRpcErrorReply ( iErrParseError, "Parse error: Request exceeds maximum size" ) ) ); + Send ( pSocket, + QJsonDocument ( CreateJsonRpcErrorReply ( + iErrParseError, + QString ( "Parse error: Request exceeds maximum size of %1 bytes" ).arg ( MAX_JSON_RPC_REQUEST_BYTES ) ) ) ); pSocket->disconnectFromHost(); } } ); From dd92b3b57f721f343fdf2834bfdd1a1fbba39f5d Mon Sep 17 00:00:00 2001 From: jrd Date: Tue, 11 Aug 2026 04:49:39 +0000 Subject: [PATCH 3/3] JSON-RPC: discard an oversized request instead of closing the connection --- src/rpcserver.cpp | 28 +++++++++++++++++++++++----- src/rpcserver.h | 1 + 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/rpcserver.cpp b/src/rpcserver.cpp index b5724a3984..248c971b47 100644 --- a/src/rpcserver.cpp +++ b/src/rpcserver.cpp @@ -117,8 +117,8 @@ QJsonObject CRpcServer::CreateJsonRpcErrorReply ( int code, QString message ) // than this, or unterminated data that fills the buffer, are rejected instead of held. // The largest single request is a welcome or chat message, which CServer truncates to // MAX_LEN_CHAT_TEXT (1600) characters and which is 9698 bytes as a compact JSON line -// when every character is escaped as \uXXXX; 16 KiB leaves 1.6x that, or room for a -// batch of about 220 ordinary calls. +// when every character is escaped as \uXXXX; 16 KiB leaves 1.7x that, or room for a +// batch of 221 ordinary calls. static constexpr int MAX_JSON_RPC_REQUEST_BYTES = 16 * 1024; void CRpcServer::OnNewConnection() @@ -131,7 +131,8 @@ void CRpcServer::OnNewConnection() qDebug() << "- JSON-RPC: received connection from:" << pSocket->peerAddress().toString(); vecClients.append ( pSocket ); - isAuthenticated[pSocket] = false; + isAuthenticated[pSocket] = false; + isDiscardingLine[pSocket] = false; // Bound the per-connection read buffer so unterminated input cannot exhaust memory. pSocket->setReadBufferSize ( MAX_JSON_RPC_REQUEST_BYTES ); @@ -140,10 +141,26 @@ void CRpcServer::OnNewConnection() qDebug() << "- JSON-RPC: connection from:" << pSocket->peerAddress().toString() << "closed"; vecClients.removeAll ( pSocket ); isAuthenticated.remove ( pSocket ); + isDiscardingLine.remove ( pSocket ); pSocket->deleteLater(); } ); connect ( pSocket, &QTcpSocket::readyRead, [this, pSocket]() { + // An oversized request was already answered with an error; the rest of its line + // is discarded here so that the connection, and the authentication bound to it, + // survive and the next request is read normally. + if ( isDiscardingLine[pSocket] ) + { + const QByteArray sPending = pSocket->peek ( pSocket->bytesAvailable() ); + const int iEndOfLine = sPending.indexOf ( '\n' ); + pSocket->read ( iEndOfLine < 0 ? sPending.size() : iEndOfLine + 1 ); + if ( iEndOfLine < 0 ) + { + return; + } + isDiscardingLine[pSocket] = false; + } + while ( pSocket->canReadLine() ) { QByteArray line = pSocket->readLine(); @@ -213,14 +230,15 @@ void CRpcServer::OnNewConnection() } // A full buffer with no complete line is an oversized or unterminated request: - // reject and close rather than hold the bytes indefinitely. + // answer it, then drop the bytes instead of holding them. if ( !pSocket->canReadLine() && pSocket->bytesAvailable() >= MAX_JSON_RPC_REQUEST_BYTES ) { Send ( pSocket, QJsonDocument ( CreateJsonRpcErrorReply ( iErrParseError, QString ( "Parse error: Request exceeds maximum size of %1 bytes" ).arg ( MAX_JSON_RPC_REQUEST_BYTES ) ) ) ); - pSocket->disconnectFromHost(); + isDiscardingLine[pSocket] = true; + pSocket->read ( pSocket->bytesAvailable() ); } } ); } diff --git a/src/rpcserver.h b/src/rpcserver.h index cf9da2706f..0803099a56 100644 --- a/src/rpcserver.h +++ b/src/rpcserver.h @@ -94,6 +94,7 @@ class CRpcServer : public QObject // A map from method name to handler functions QMap mapMethodHandlers; QMap isAuthenticated; + QMap isDiscardingLine; QVector vecClients; void HandleApiAuth ( QTcpSocket* pSocket, const QJsonObject& params, QJsonObject& response );