Bound exec-server JSON-RPC heap expansion#31807
Open
jif-oai wants to merge 2 commits into
Open
Conversation
anp-oai
approved these changes
Jul 10, 2026
| // A maximum-size fs/walk response has at most 50,000 entries and needs roughly | ||
| // 150,000 JSON values. Keep ample headroom for legitimate protocol messages | ||
| // while preventing compact arrays from expanding into millions of heap values. | ||
| const MAX_JSONRPC_VALUE_NODES: usize = 256 * 1024; |
Contributor
There was a problem hiding this comment.
Codex thinks process/read can return an unbounded number of (single byte even?) chunks inside the 1MB limit which would blow this up. Should be easy to rule out?
| .map_err(map_fs_error)? | ||
| .map_err(map_fs_error)?; | ||
| let entry_count = entries.len(); | ||
| if entry_count > MAX_READ_DIRECTORY_ENTRIES { |
Contributor
There was a problem hiding this comment.
If the math in the comment above is correct, when can we actually hit this path after a successful json parse? Maybe add a test if it's actually a useful guardrail?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The exec-server transports cap serialized messages, but the shared JSON-RPC decoder could turn a small, valid message into a much larger heap allocation before method routing.
In a focused macOS parse using the production protocol type, the concrete reproduction was a notification whose
paramswas a flat array of zeroes:Each
0costs about two bytes on the wire, but becomes a fullserde_json::Valuein memory. The untagged JSON-RPC envelope also needs an intermediate representation while it selects request, notification, response, or error. Method lookup happens later, so an unknown method does not protect the process. At the 64 MiB serialized ceiling, the same shape can require several GiB and terminate the orchestrator.What changed
JSONRPCMessagethrough a shared, budgeted JSON-value visitor.fs/readDirectoryproducers to 50,000 entries, keeping their responses below the shared decoder budget.Why 262,144 values
The largest configured
fs/walkresponse can contain 50,000 entries, which is roughly 150,000 JSON values before the small fixed envelope. A maximum same-versionfs/readDirectoryresponse contains about 200,004 values. A 262,144-value ceiling leaves headroom for both while putting the reproduction about 8x over budget.This is a structural limit, not a byte limit. A large scalar string counts as one value, so base64 file payloads remain governed by the transport's serialized-message ceiling rather than this guard. The tests explicitly cover a scalar larger than 262,144 bytes.
Behavior and scope
Messages over the value budget or with duplicate object keys follow the existing malformed-message path. A same-version server returns a JSON-RPC error instead of producing an
fs/readDirectoryresponse with more than 50,000 entries. Serialization and the JSON-RPC wire schema do not change; this intentionally assumes the client and exec-server move together and adds no version negotiation.This complements #31782: that PR bounds raw stdio framing at 64 MiB; this PR bounds the heap expansion that can happen inside an accepted frame. Existing Noise and WebSocket byte ceilings are unchanged.
This PR intentionally does not add method-specific limits beyond the producer guard above or a byte-weighted budget spanning every queued and in-flight RPC owner. That would require carrying ownership permits through notifications, pending responses, and typed decoding. It is a separate hardening layer; this change closes the demonstrated single-message structural amplification without introducing that machinery.