Skip to content

fix: harden dynamic tool handlers against deadlock, hangs, and runaway output#41

Open
electronicBlacksmith wants to merge 1 commit intoghostwright:mainfrom
electronicBlacksmith:upstream/fix/dynamic-handler-hardening
Open

fix: harden dynamic tool handlers against deadlock, hangs, and runaway output#41
electronicBlacksmith wants to merge 1 commit intoghostwright:mainfrom
electronicBlacksmith:upstream/fix/dynamic-handler-hardening

Conversation

@electronicBlacksmith
Copy link
Copy Markdown

What Changed

  • Added drainProcessWithLimits() that drains stdout/stderr concurrently with a hard timeout (SIGTERM, then SIGKILL after 2s grace period)
  • Added readStreamWithCap() that caps output at 1MB but keeps reading past the cap so the child process never blocks on a full pipe
  • Configurable via PHANTOM_DYNAMIC_HANDLER_TIMEOUT_MS (default 60s) and PHANTOM_DYNAMIC_HANDLER_MAX_OUTPUT_BYTES (default 1MB)
  • Timeout returns error message with partial output snippet

Why

Dynamic MCP tool handlers (shell/script) can hang indefinitely or produce unbounded output. A runaway find / or infinite loop blocks the agent forever. If both stdout and stderr fill their OS pipe buffers (~64KB each), the child deadlocks waiting for a reader while the parent blocks reading one pipe.

How I Tested

  • bun test src/mcp/__tests__/dynamic-handlers.test.ts - 25 pass (timeout, output cap, concurrent drain, graceful shutdown)
  • bun test src/mcp/__tests__/dynamic-tools.test.ts - 8 pass
  • bun run lint - clean
  • bun run typecheck - clean
  • Full suite: 976 pass

Checklist

  • Tests pass (bun test)
  • Lint passes (bun run lint)
  • Typecheck passes (bun run typecheck)
  • No secrets or .env files included
  • Files stay under 300 lines
  • No Cardinal Rule violations
  • No default exports or barrel files added

…y output

Adds timeout protection, streaming drain to prevent pipe deadlock,
and output byte capping for dynamic MCP tool handlers.

- drainProcessWithLimits() drains stdout/stderr concurrently with
  a hard timeout (SIGTERM then SIGKILL after 2s grace period)
- readStreamWithCap() caps output at 1MB but keeps reading past
  the cap so the child process never blocks on a full pipe
- Configurable via PHANTOM_DYNAMIC_HANDLER_TIMEOUT_MS (default 60s)
  and PHANTOM_DYNAMIC_HANDLER_MAX_OUTPUT_BYTES (default 1MB)
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.

1 participant