Skip to content

[Security] Bound MCP stdio framing buffers#258

Open
fettpl wants to merge 5 commits into
sybil-solutions:mainfrom
fettpl:p1/issue-238-mcp-stdio-framing
Open

[Security] Bound MCP stdio framing buffers#258
fettpl wants to merge 5 commits into
sybil-solutions:mainfrom
fettpl:p1/issue-238-mcp-stdio-framing

Conversation

@fettpl

@fettpl fettpl commented Jul 19, 2026

Copy link
Copy Markdown

Summary

  • cap newline-delimited MCP stdio framing at 4 MiB with raw-byte accounting and fatal UTF-8 decoding
  • preserve the official MCP SDK client and HTTP transport while hardening only the owned stdio transport boundary
  • keep the 64 KiB bootstrap cap and route same-chunk post-bootstrap bytes losslessly into normal framing
  • converge framing, stream, process, and explicit-close failures on one terminal error and one cleanup path
  • retain final-response delivery with a bounded exit drain, then terminate the complete owned process tree

Root cause

The stdio connector accumulated decoded text without a size bound until a newline arrived. A faulty or hostile child could therefore retain arbitrary stdout, force repeated buffer copies, and defer request failure until unrelated request timeouts. Decoding before a complete frame also made split multi-byte UTF-8 and exact byte limits difficult to enforce reliably.

Transport termination was spread across stream and process callbacks. That made it possible for pending and future operations to observe different errors, for listeners or timers to outlive the connection, and for descendants holding stdout open to delay cleanup indefinitely.

Solution design

Bounded byte framing

StdioJsonLineFramer retains raw bytes, grows capacity only as needed, emits every complete newline-delimited frame, and rejects a frame above 4 MiB before unbounded retention can occur. It uses fatal UTF-8 decoding after frame completion, supports split code points and multiple frames per chunk, and releases retained data on terminal failure.

Bootstrap remains independently capped at 64 KiB. The framer changes to the 4 MiB runtime limit only while empty, and bytes following the initialize response in the same stdout chunk continue through the runtime parser without loss.

SDK-compatible protocol handling

The existing official SDK Client and StreamableHTTPClientTransport remain authoritative. Stdio messages are validated with the SDK's canonical JSONRPCMessageSchema and forwarded through the transport's onmessage callback, preserving SDK handling for responses, notifications, server requests, request timeouts, and abort signals.

Deterministic terminal lifecycle

A single terminal-failure state rejects pending and future operations with the same error object, notifies the SDK promptly, and closes the owned transport once. Stable stdout, stdin, stderr, child-process, exit, and close listeners are removed exactly once.

Parent exit starts a 100 ms stdout drain so an already-piped final response can settle. Clean or partial EOF from a live child becomes terminal, partial frames produce a typed unexpected-EOF error, and descendants that retain stdout are bounded by the drain before POSIX process-group or Windows job cleanup completes. Pre-spawn failures never signal a nonexistent process group, and explicit close remains idempotent.

Acceptance criteria

  • bootstrap data is capped at 64 KiB and runtime frame data is capped at 4 MiB
  • exact-limit frames succeed and limit-plus-one, malformed JSON, invalid UTF-8, invalid JSON-RPC, blank frames, and partial EOF fail promptly
  • split UTF-8, multiple frames per chunk, notifications, server requests, and same-chunk bootstrap/runtime traffic remain SDK-compatible
  • pending and future calls observe the same terminal error while SDK timeout and abort semantics remain intact
  • stdout, stdin, stderr, and process listeners are removed exactly once
  • final responses survive bounded parent-exit draining and descendant-held stdout cannot prevent teardown
  • POSIX process groups and Windows jobs are cleaned up without signaling before spawn
  • focused, stress, complete runtime, repository, pre-push, packaging, signature, reinstall, and desktop-health checks pass

Validation

Validated at exact head 070dd1bb35905c4cb7badb407951541d676277dc, whose direct parent is the reviewed #257 head 01ac7ff18f1097da281f4603e77d751568e130be.

  • focused MCP client and framer suites: 49 passed, 490 assertions
  • independent five-repeat focused stress run: 245 passed
  • complete agent-runtime suite: 138 passed, 707 assertions, including TypeScript and Windows helper verification
  • full repository gate: npm run check
  • pre-push frontend quality gate, including lint, typechecks, dependency checks, duplicate checks, and production build
  • two independent exact-head P0/P1 reviews: no findings
  • exact-head workflow records: CI run 29735798436 and Security run 29735798320, both action_required with zero jobs pending upstream approval for fork-authored workflows
  • production desktop:dist with embedded runtime, package-signature, and stdio MCP verification
  • deep strict code-signature verification of the built and canonical installed app
  • clean canonical /Applications/Local Studio.app reinstall and GET /api/desktop-health returning HTTP 200 with { "ok": true }

Dependency

Depends on #257. This branch is based directly on its exact reviewed head because its official GitHub MCP artifact and plugin bootstrap boundaries are part of the compatibility surface exercised here.

Closes #238

@fettpl
fettpl requested a review from 0xSero as a code owner July 19, 2026 15:03
@fettpl

fettpl commented Jul 19, 2026

Copy link
Copy Markdown
Author

@0xSero Ready for maintainer review.

Exact head: 070dd1bb35905c4cb7badb407951541d676277dc
Direct parent: reviewed #257 head 01ac7ff18f1097da281f4603e77d751568e130be

This revision preserves the official MCP SDK client and HTTP transport while bounding owned stdio framing, retaining the separate bootstrap limit, and making terminal settlement and process-tree cleanup deterministic. Two independent exact-head P0/P1 reviews found no issues.

Validation is green: focused suites 49/49 with 490 assertions, independent five-repeat stress 245/245, complete agent runtime 138/138 with 707 assertions plus TypeScript and Windows-helper verification, full npm run check, the hook-enforced frontend quality gate, production desktop packaging, deep signature verification, clean canonical reinstall, and desktop health HTTP 200.

Exact-head workflow records: CI run 29735798436 and Security run 29735798320. Both are action_required with zero jobs because GitHub requires upstream approval before fork-authored workflows can start; neither record represents a test failure.

@fettpl
fettpl force-pushed the p1/issue-238-mcp-stdio-framing branch from d577ca9 to 070dd1b Compare July 20, 2026 10:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] Bound MCP stdio framing buffers

1 participant