Repository navigation
fix(serve): bound request-line size in handle_connection to prevent local DoS - #209
Merged
Shengyu Fu (shengyfu) merged 2 commits intoOct 17, 2026
Merged
Conversation
…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]>
Contributor
There was a problem hiding this comment.
🟡 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
…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]>
Shengyu Fu (shengyfu)
approved these changes
Oct 17, 2026
Shengyu Fu (shengyfu)
deleted the
fix/handle-connection-unbounded-read-dos
branch
October 17, 2026 17:39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Fixes a local unauthenticated DoS in
tgrep serve'shandle_connection, tracked as MSRC case #162882 / ICM 31000000761603.handle_connection(tgrep-cli/src/serve.rs) read request lines with plainBufReader::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-readableserve.jsonin the index dir) can stream bytes without ever sending\nand grow thatStringwithout 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 boundedread_line(stream, limit)helper built ontgrep_core::managed::MAX_REQUEST_BYTES(1MB). The primarytgrep servesocket 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 unboundedread_linecall inhandle_connection, capped at the sameMAX_REQUEST_BYTESlimit (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, viaio::Cursor, no real sockets needed):All 345 existing
tgrep-clitests continue to pass.cargo fmt --checkis clean.Checklist
cargo build -p tgrep-cli)cargo test -p tgrep-cli --bin tgrep)cargo fmt --checkclean