Skip to content

[Security] Bound MCP stdio framing buffers #238

Description

@fettpl

Audit metadata

  • Priority: P1
  • Estimated effort: S
  • Implementation risk: LOW
  • Category: security / perf
  • Evidence baseline: 07be5be7ce69bea0c3118744ab90d148b010fce0 (origin/main on 2026-07-17)

Dependencies

  • P1 proposal 039 — Reapprove changed plugin artifacts and minimize MCP child environments. Included in the current P1 issue wave.

Description and impact

A faulty or malicious stdio MCP server can write indefinitely without a newline. The frontend concatenates every chunk into an unbounded string, allowing memory exhaustion and potentially quadratic copying. Bound frames and terminate protocol-invalid connections while rejecting every pending call promptly.

Existing issue overlap: #200 covers Windows .cmd spawning and child cleanup, not protocol framing or buffer limits.

Current state and reproduction

services/agent-runtime/src/mcp-client.ts:62-82 caps stderr at 2,000 characters but leaves buffer unbounded. Lines 96-115 append until newline and swallow parse/schema errors:

this.buffer += chunk.toString("utf8");
let newline = this.buffer.indexOf("\n");

Configure a stdio connector whose process continuously writes bytes without \n; initialization never completes and memory grows with stdout.

Solution design

Use a byte-based newline framer with a documented 4 MiB maximum frame and leftover buffer. Preserve split UTF-8 and multiple frames per chunk without repeated whole-string concatenation. On overflow or malformed JSON/schema, raise a typed MCP protocol error, clear the buffer, reject and clear all pending requests/timers, and terminate the child once. Normal JSON-RPC notifications remain accepted. Add no source comments.

Verification commands

Purpose Command Expected on success
MCP tests cd frontend && bun test ../tests/frontend/agent-runtime/mcp-client.test.ts exit 0
Runtime build npm --prefix services/agent-runtime run build exit 0
Frontend quality npm --prefix frontend run check:quality exit 0
Full integration npm run test:integration exit 0

Scope

In scope: mcp-client.ts; an extracted stdio-json-line-framer.ts if it keeps framing pure; a focused test; and a synthetic child fixture only if end-to-end close behavior needs it.

Out of scope: connector approval/permissions, Windows command resolution (#200), HTTP MCP transport, JSON-RPC feature additions, stderr retention, and changing the request timeout.

Implementation plan

Step 1: Characterize framing and overflow

Test one frame split across chunks/UTF-8 boundaries, multiple frames in one chunk, empty lines, a frame exactly at the limit, limit plus one byte, endless no-newline output, malformed JSON, schema-invalid JSON, and child close with pending requests.

Verify: the overflow case grows/remains pending on baseline and fails the new expectation.

Step 2: Implement a bounded byte framer

Accumulate bytes only up to the first newline/limit, emit complete frame bytes, retain a bounded remainder, and decode UTF-8 only for a complete frame. Avoid exported test-only APIs; keep the framer a real production abstraction used by the connection.

Verify: pure framing tests pass with buffer size never above 4 MiB.

Step 3: Close the connection on protocol failure

Centralize terminal failure so overflow/malformed frames, spawn close, and explicit close settle once, reject all pending promises, clear timers and buffers, and terminate the child. Do not wait for the normal 60-second timeout.

Verify: end-to-end fixture test observes child termination and immediate typed rejection.

Step 4: Verify build and desktop bundle

Run all gates, build desktop:dist, perform the canonical AGENTS.md reinstall, relaunch, and verify desktop-health returns HTTP 200.

Verify: runtime/frontend/integration gates and packaged health all succeed.

Test plan

Prefer a pure framer test plus one synthetic child-process integration. Assert byte—not character—limits, split UTF-8 correctness, all pending timers cleared, no double rejection, and normal initialize/list/call traffic still works.

Acceptance criteria

  • Stdio frame/remainder memory is capped at 4 MiB.
  • Oversized/no-newline/malformed frames close the connection immediately with a typed error.
  • All pending requests reject and timers are cleared.
  • Normal split/multiple frames parse correctly.
  • Runtime build, npm run check, and npm run test:integration exit 0.
  • Canonical desktop is reinstalled and healthy.

Risks and stop conditions

  • A supported MCP server demonstrably emits valid frames larger than 4 MiB; report measured sizes and choose a documented cap.
  • Framing is not newline-delimited for a supported stdio transport.
  • The change requires connector permission or spawn portability work.
  • Source drift or two verification failures occur.

Maintenance considerations

Keep stdout and decoded JSON limits explicit if binary/content payloads grow. Reviewers should inspect timer rejection and child-close idempotence, not only buffer arithmetic.


Generated from Local Studio 2.0 main-branch audit proposal 045.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions