Skip to content

MCP stdio child process not killed on drop — leaks subprocess on hot reload #3800

Description

@houko

Severity

P1 — repeated reload cycles accumulate orphan node/python processes holding fds, sockets, workspace locks.

Files

  • crates/librefang-runtime-mcp/src/lib.rs:1009-1036 (connect_stdio)

Finding

connect_stdio builds tokio::process::Command and hands it to
TokioChildProcess::new(...) without ever calling .kill_on_drop(true).
Tokio defaults kill_on_drop to false, so when an McpConnection is
dropped (server config removed, hot reload, daemon panic, OAuth retry
replacing the connection) the subprocess is only signaled via stdin
EOF.

A misbehaving MCP server that ignores EOF (or is mid-write) becomes a
zombie running indefinitely, holding file descriptors, network sockets, and
potentially a lock on workspace files. There is also no Drop impl on
McpConnection or stored Child handle that calls .kill() explicitly,
and no test asserting the child dies.

Evidence

let transport = TokioChildProcess::new(
    tokio::process::Command::new(&resolved_command).configure(|cmd| {
        cmd.args(&args_owned);
        cmd.env_clear();
        // ... no .kill_on_drop(true), no .stderr(...) call

Suggested Fix

Inside the configure closure add cmd.kill_on_drop(true);. Optionally
implement Drop for McpConnection to abort lingering tasks and explicitly
kill() the rmcp child handle if TokioChildProcess exposes one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/runtimeAgent loop, LLM drivers, WASM sandboxbugSomething isn't workinghas-prA pull request has been linked to this issueseverity/highSignificant functional, security, or performance impact

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions