modelcontextprotocol / modelcontextprotocol/rust-sdk
Unbounded line buffer in `AsyncRwTransport::receive` (stdio transport) — memory-exhaustion denial-of-service
@alexhancock is already working on this.
Since Jul 28, 2026.
- 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_servera way to configure a maximum line length, and checkself.line_buf.len()against it inside theread_untilloop inreceive, erroring out (and clearing the buffer) instead of appending past the limit; or - route reads back through
JsonRpcMessageCodec's existing, correctmax_lengthhandling inDecoder::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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.