JSON-RPC: bound the per-connection read buffer so unterminated input cannot exhaust memory - #3861
JSON-RPC: bound the per-connection read buffer so unterminated input cannot exhaust memory#3861mcfnord wants to merge 1 commit into
Conversation
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 <noreply@anthropic.com>
|
@mcfnord, pass your LLM this PR and get it to work out why it failed in the build: and work out what to put in the appropriate guidance notes to stop it happening again. |
NO need to prompt the LLM. Very likely just an issue on GitHubs side |
|
MY LLM WROTE: The blocking check was the "Verify C-like coding style" job, conclusion cancelled. The job record shows zero steps executed, no stored log, and inverted timestamps (started_at 2026-08-07 06:38 UTC, completed_at 2026-08-06 16:24 UTC) — it never ran. Re-running it directly needs repo write access, so the PR was closed and reopened to fire a fresh |
|
TIL: Closing and reopening a pull request can retrigger CI. Good to know. There was a GitHub Actions outage. Details: https://www.githubstatus.com/incidents/qcvjkzcs7j74 Seems like the LLM agent hasn't yet been trained to look at the CI log, and then deduce that they should reach for |
| return object; | ||
| } | ||
|
|
||
| // Maximum size of a single JSON-RPC request line. An unauthenticated client that |
There was a problem hiding this comment.
Shorter comment, please. This declaration only needs a comment saying what it is, not where it's used.
The reasoning is repeated later anyway.
There was a problem hiding this comment.
If you'd like to save tokens, use the Add a suggestion button, which might shortcut to Ctrl+g. The AI will still learn your preference.
There was a problem hiding this comment.
The size should definitely allow about a welcome message/chat message + some slack. That's how I would define the maximum.
| // 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" ) ) ); |
There was a problem hiding this comment.
Maximum size of which length -> return it.
| // (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; |
There was a problem hiding this comment.
This number needs explaining - where does the 64K bound actually come from?
Is there a bound in the Server GUI Welcome Message text box? This shouldn't exceed that, for example.
I don't know if there's a bound on the command line welcome message file size. The command line bounds as plain text would be bounded by the shell -- I've probably cut-n-paste over 64K into bash before now.
So those other sources should be checked, too.
|
🤖 AI: The 64K was not derived from anything, so here is the number to derive it from. Two inputs compete for "largest legitimate request", and both land on Welcome message: every path goes through Chat: Measured against a server built from this branch, request line as sent on the wire:
So the largest legitimate request is 9,708 bytes and 64 KiB is 6.8x that. Worth flagging while looking: of the string-taking methods, only Proposed for the declaration, saying what it is plus where the number comes from: // Largest JSON-RPC request accepted on one line: 6.8x the largest legitimate
// request, which is a 1600-character welcome or chat message (MAX_LEN_CHAT_TEXT),
// 9708 bytes once JSON-escaped.
static constexpr int MAX_JSON_RPC_REQUEST_BYTES = 64 * 1024;@ann0see, on stating the limit in the error: the reply becomes If 6.8x reads as too much slack, 16 KiB still leaves 1.6x and is a one-line change. |
|
Yes. Then bound it to a lower number. |
MY LLM WROTE:
The JSON-RPC server consumes input only on a complete line —
while ( pSocket->canReadLine() )inCRpcServer::OnNewConnection(src/rpcserver.cpp). Until a newline arrives the received bytes stay in theQTcpSocketread buffer, which is unbounded becausesetReadBufferSize()is never called. A client that connects and sends bytes with no\ngrows that buffer 1:1 with bytes sent, before authentication, until the process is killed by the allocator (uncaughtstd::bad_alloc→ SIGABRT, dropping every connected client).Measured on a non-ASan release build of
main, one connection sending 200 MiB with no newline:mainWell-formed traffic is unaffected on the patched build:
jamulus/apiAuthreturns"result":"ok"andjamulus/getVersionreturns the version, connection stays open.Scope: reachable only when the RPC server is enabled (
--jsonrpcport) and bound off-loopback (--jsonrpcbindip); the default bind is127.0.0.1. This is the denial-of-service classSECURITY.mddocuments as a non-guarantee, so the change hardens a documented limitation rather than closing a promised guarantee — the buffer bounds in a few lines.The change, all in
OnNewConnection:setReadBufferSize ( MAX_JSON_RPC_REQUEST_BYTES )on each new connection, so buffering stops at the bound.MAX_JSON_RPC_REQUEST_BYTES= 64 KiB — far above any real request (secret, method name, or a large batch), and adjustable.A regression test fits the fork's JSON-RPC test surface directly: send an unterminated payload and assert the server's RSS stays flat and the connection is dropped, versus a terminated payload parsed normally.
@dtinth — flagging you as the JSON-RPC author and since this sits in the surface the fork is bringing under test.