modelcontextprotocol / modelcontextprotocol/rust-sdk

Unbounded line buffer in `AsyncRwTransport::receive` (stdio transport) — memory-exhaustion denial-of-service

Open
#1,030 1 comment 0 reactions 1 assignee View on GitHub

@alexhancock is already working on this.

Since Jul 28, 2026.

P2
Dominant language
Rust
Stars
3.9k
Forks
645
Avg merge
4d 13h
Merged PRs (30d)
36

Description

Summary

AsyncRwTransport::receive — the read path used by the stdio server transport (rmcp::transport::io::stdio()) and by the TokioChildProcess client transport — buffers an incoming line with BufReader::read_until(b'\n', &mut self.line_buf) and never checks a maximum length. A peer that sends a single line without a \n (or an extremely long line) makes the process grow line_buf: Vec<u8> without bound, exhausting memory. JsonRpcMessageCodec already has a max_length field with correct enforcement logic, but that logic lives in Decoder::decode, which AsyncRwTransport never calls — the transport's read side does not go through the codec's decoder at all.

Severity: Medium (denial-of-service, untrusted input, no data exposure).

Affected versions

Confirmed in rmcp 2.2.0 (current latest) and on main (byte-identical as of this report). The BufReader + line_buf: Vec<u8> + uncapped read_until pattern is present at least back to 1.8.0. Earlier releases (checked at 1.0.0) route receive() through FramedRead + Decoder::decode, but AsyncRwTransport::new always constructs JsonRpcMessageCodec::default() (max_length: usize::MAX) with no way for a caller to lower it — so those versions are equally unbounded via a different internal path.

Code reference

crates/rmcp/src/transport/async_rw.rs, AsyncRwTransport::receive:

async fn receive(&mut self) -> Option<RxJsonRpcMessage<Role>> {
    loop {
        match self.read.read_until(b'\n', &mut self.line_buf).await {
            Ok(0) => return None,
            Ok(_) => {}
            Err(e) => { tracing::error!("Error reading from stream: {}", e); return None; }
        }
        // line_buf is only cleared after a full '\n'-terminated line is parsed;
        // nothing bounds it before that.

line_buf is a plain Vec<u8> with no capacity ceiling. JsonRpcMessageCodec::max_length is fully implemented and correct inside Decoder::decode, but is unreachable from this transport.

Impact

Any stdio MCP server built with this SDK (.serve(rmcp::transport::io::stdio())), and any client using TokioChildProcess, accepts an unbounded amount of memory from a single unterminated or oversized input line from an untrusted peer — a straightforward denial-of-service.

Minimal reproduction
// Attacker / harness, writing to the server's stdin:
let chunk = vec![b'A'; 1 << 20]; // 1 MiB, contains no b'\n'
loop {
    stdin.write_all(&chunk).await?;
}
// The server process's RSS grows without bound and never shrinks, since
// line_buf is only cleared once a full '\n'-terminated line has been parsed.
Suggested fix
  • Give AsyncRwTransport::new/new_client/new_server a way to configure a maximum line length, and check self.line_buf.len() against it inside the read_until loop in receive, erroring out (and clearing the buffer) instead of appending past the limit; or
  • route reads back through JsonRpcMessageCodec's existing, correct max_length handling in Decoder::decode, while preserving the cancellation-safety behavior added in #947.
  • A conservative built-in default (e.g. a few MiB) would close the gap for existing callers with no API change; exposing it as configurable would help callers who legitimately need larger single-line payloads.
Workaround

Until fixed, downstream consumers can wrap the reader passed to stdio()/serve() in their own size-capped AsyncRead that errors once a bounded number of bytes has been read without a newline.


Found while integrating the SDK into a desktop application. Happy to provide more detail, a runnable repro, or test against a candidate patch.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.