You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
asyncfn receive(&mutself) -> Option<RxJsonRpcMessage<Role>>{loop{matchself.read.read_until(b'\n',&mutself.line_buf).await{Ok(0) => returnNone,Ok(_) => {}Err(e) => { tracing::error!("Error reading from stream: {}", e);returnNone;}}// 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
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.
Summary
AsyncRwTransport::receive— the read path used by the stdio server transport (rmcp::transport::io::stdio()) and by theTokioChildProcessclient transport — buffers an incoming line withBufReader::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 growline_buf: Vec<u8>without bound, exhausting memory.JsonRpcMessageCodecalready has amax_lengthfield with correct enforcement logic, but that logic lives inDecoder::decode, whichAsyncRwTransportnever 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
rmcp2.2.0 (current latest) and onmain(byte-identical as of this report). TheBufReader+line_buf: Vec<u8>+ uncappedread_untilpattern is present at least back to 1.8.0. Earlier releases (checked at 1.0.0) routereceive()throughFramedRead+Decoder::decode, butAsyncRwTransport::newalways constructsJsonRpcMessageCodec::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:line_bufis a plainVec<u8>with no capacity ceiling.JsonRpcMessageCodec::max_lengthis fully implemented and correct insideDecoder::decode, but is unreachable from this transport.Impact
Any stdio MCP server built with this SDK (
.serve(rmcp::transport::io::stdio())), and any client usingTokioChildProcess, 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
Suggested fix
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; orJsonRpcMessageCodec's existing, correctmax_lengthhandling inDecoder::decode, while preserving the cancellation-safety behavior added in fix(transport): make AsyncRwTransport::receive cancel-safe (#941) #947.Workaround
Until fixed, downstream consumers can wrap the reader passed to
stdio()/serve()in their own size-cappedAsyncReadthat 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.