Skip to content

fix(serve): bound request-line size in handle_connection to prevent local DoS - #209

Merged
Shengyu Fu (shengyfu) merged 2 commits into
mainfrom
fix/handle-connection-unbounded-read-dos
Oct 17, 2026
Merged

Shengyu Fu (shengyfu) merged 2 commits into
mainfrom
fix/handle-connection-unbounded-read-dos

Conversation

@msftsiwei

Copy link
Copy Markdown
Member

Summary

Fixes a local unauthenticated DoS in tgrep serve's handle_connection, tracked as MSRC case #162882 / ICM 31000000761603.

handle_connection (tgrep-cli/src/serve.rs) read request lines with plain BufReader::read_line(&mut String), which has no upper bound on buffered data before a newline is seen. A local client connecting to the server's TCP port (port is discoverable via the world-readable serve.json in the index dir) can stream bytes without ever sending \n and grow that String without limit, exhausting process memory. Locally reproduced: 11MB of input in -> 2.7GB resident in ~2s.

MSRC assessment: Severity: None / Security Impact: Defense in Depth / Resolution: Informational (CWE-770), with a Plan of Action due 2026-10-25.

Root cause

This connection path predates the managed shared-daemon protocol (tgrep-cli/src/serve/shared/protocol.rs), which already solved this exact problem with its own bounded read_line(stream, limit) helper built on tgrep_core::managed::MAX_REQUEST_BYTES (1MB). The primary tgrep serve socket was simply never updated to use the same pattern — this looks like an oversight in older code rather than an intentional design choice, since the bounded pattern is used consistently everywhere else in the codebase (protocol_v2.rs, catalog.rs, private_control.rs, storage.rs, ...).

Fix

Added read_bounded_line, a drop-in replacement for the unbounded read_line call in handle_connection, capped at the same MAX_REQUEST_BYTES limit (reusing the existing constant, not duplicating it). Once a peer exceeds the limit without a newline — or disconnects mid-line — the connection is dropped with an error instead of being read indefinitely.

Tests

Added serve::read_bounded_line_tests (unit tests against the new function directly, via io::Cursor, no real sockets needed):

  • normal single-line round trip
  • clean EOF handling
  • multiple lines read sequentially off one stream
  • regression test for the reported DoS: an oversized line with no newline is rejected rather than buffered indefinitely
  • boundary test: a line exactly at the limit (inclusive) is still accepted — not off-by-one
  • a line missing its newline but under the byte limit is still rejected (no silent "treat partial read as complete request")

All 345 existing tgrep-cli tests continue to pass. cargo fmt --check is clean.

Checklist

  • Builds clean (cargo build -p tgrep-cli)
  • New + existing tests pass (cargo test -p tgrep-cli --bin tgrep)
  • cargo fmt --check clean
  • ICM 31000000761603 to be updated with a link to this PR once opened

…ocal DoS

`handle_connection` in `tgrep-cli/src/serve.rs` used
`BufReader::read_line(&mut String)` with no upper bound on how much data
it would buffer before seeing a newline. A local, unauthenticated client
that connects to the server's TCP port (discoverable via the
world-readable `serve.json`) and streams bytes without ever sending `\n`
can grow that `String` without limit, exhausting process memory
(observed: 11MB -> 2.7GB in ~2s in local testing).

This is tracked as MSRC case #162882 / ICM 31000000761603
(CWE-770: Allocation of Resources Without Limits or Throttling,
Severity: None / Security Impact: Defense in Depth / Resolution:
Informational, Plan of Action due 2026-10-25).

Notably, the managed shared-daemon protocol in
`tgrep-cli/src/serve/shared/protocol.rs` already enforces exactly this
kind of bound via its own `read_line(stream, limit)` helper, reusing
`tgrep_core::managed::MAX_REQUEST_BYTES` (1MB). The primary
`tgrep serve` connection path predates that protocol and was never
updated to match -- this fix brings it in line.

Fix: introduce `read_bounded_line`, a drop-in replacement for the
unbounded `BufReader::read_line` call, capped at the same
`MAX_REQUEST_BYTES` limit used elsewhere in the codebase. Once a peer
exceeds the limit without sending a newline (or disconnects mid-line),
the connection is dropped with an error instead of being read
indefinitely.

Tests added (`serve::read_bounded_line_tests`):
- normal line round-trip
- clean EOF handling
- multiple lines on one stream
- regression test: oversized line with no newline is rejected (the
  exact DoS pattern from the report)
- boundary test: a line exactly at the limit is still accepted
  (bound is inclusive, not off-by-one)
- a line missing its newline but under the limit is still rejected
  (no silent truncation/partial-read as a "complete" request)

All 345 existing tgrep-cli tests continue to pass; `cargo fmt --check`
is clean.

Co-authored-by: Copilot <[email protected]>
Copilot AI balanced review requested due to automatic review settings October 17, 2026 09:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Lossy UTF-8 decoding accepts altered requests that the previous reader rejected.

1 open issue
What changed in this PR

Bounds request-line reads on the primary server socket to prevent unbounded memory growth.

Changes:

  • Reuses the managed protocol’s request-size limit.
  • Rejects oversized and incomplete request lines.
  • Adds tests for framing, EOF, and size boundaries.
File Description
tgrep-cli/​src/​serve.rs Adds and integrates a bounded reader with regression tests.

Review effort: Balanced

Comment thread tgrep-cli/src/serve.rs Resolved
…decoding

Per PR review from Copilot: String::from_utf8_lossy() silently replaces
invalid UTF-8 bytes with U+FFFD, which would let JSON parsing succeed
against a line the attacker (or a bug) corrupted -- e.g. a malformed
params.pattern silently becomes a different pattern instead of being
rejected. Switch to String::from_utf8(bytes)?, which fails the request
the same way the original read_line(&mut String) implicitly guaranteed
(std strings are always valid UTF-8).

Added a regression test with an invalid UTF-8 byte embedded in an
otherwise well-formed, newline-terminated JSON-RPC request line.

346 tgrep-cli tests pass (was 345); cargo fmt --check clean.

Co-authored-by: Copilot <[email protected]>
Copilot AI balanced review requested due to automatic review settings October 17, 2026 09:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused change bounds input buffering while preserving valid request handling, with no unresolved findings.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@shengyfu
Shengyu Fu (shengyfu) merged commit e67570a into main Oct 17, 2026
16 checks passed
@shengyfu
Shengyu Fu (shengyfu) deleted the fix/handle-connection-unbounded-read-dos branch October 17, 2026 17:39
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.

3 participants