fix(proxy): bound SSE parser via complete-line cap at 1 MiB#97191
fix(proxy): bound SSE parser via complete-line cap at 1 MiB#97191wangmiao0668000666 wants to merge 2 commits into
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 27, 2026, 3:34 AM ET / 07:34 UTC. Summary PR surface: Source +16, Tests +165. Total +181 across 2 files. Reproducibility: yes. Current main has a source-visible path that appends proxy SSE data into an unbounded buffer, and the PR body supplies after-fix live Review metrics: 1 noteworthy metric.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land this byte-accurate proxy SSE line/tail/EOF guard if maintainers accept the 1 MiB event boundary, while keeping the separate total-body guard coordinated but distinct. Do we have a high-confidence way to reproduce the issue? Yes. Current main has a source-visible path that appends proxy SSE data into an unbounded buffer, and the PR body supplies after-fix live Is this the best way to solve the issue? Yes, if maintainers accept the 1 MiB boundary. Capping complete lines, the retained tail, and the EOF final frame is the narrow parser fix; the total response-body cap remains separate in #96768. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against fbfadbd806e1. Label changesLabel justifications:
Evidence reviewedPR surface: Source +16, Tests +165. Total +181 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
Extend the byte-accurate 1 MiB policy to the unterminated tail (data after last \n) and the EOF final frame, closing the bypass paths identified by ClawSweeper review. The 1 MiB tail cap is deterministic unlike the prior 64 KiB attempt (openclaw#96993) because any hostile payload exceeding 1 MiB total bytes triggers the cap regardless of TCP chunking. - Add tail buffer cap after draining complete lines (proxy.ts:237) - Add EOF final frame cap before processSseLine (proxy.ts:246) - 2 new tests: unterminated tail across chunks, no-newline EOF frame Co-Authored-By: Claude <[email protected]>
|
@clawsweeper re-review P1 fix applied: extended byte-accurate 1 MiB policy to all three buffering paths:
2 new tests: (1) unterminated tail across chunks without \n triggers tail cap, Total: 9 tests pass (4 existing + 5 new: single-chunk, split-chunk, |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@NianJiuZst Thanks for the review! This was addressed in commit caa27dc — the same byte-accurate 1 MiB policy now covers all three buffering paths:
Plus 2 new tests: oversized unterminated tail across chunks, and oversized EOF final frame without trailing newline. |
|
Closing this as superseded by #97235. I reviewed the current branch against main. The OOM-hardening goal is now covered by the merged proxy path in #97235: bounded error-body reads, a 16 MiB total SSE byte guard, pending-buffer cap, and idle timeout in src/agents/runtime/proxy.ts. This branch adds a separate 1 MiB per-line/tail/EOF protocol cap against the older reader shape. That is a new compatibility policy, not required for the OOM fix anymore, and I do not want to merge it stale. If we want that stricter per-frame policy, it should come back as a fresh PR against current streamProxy with current-main tests. |
What Problem This Solves
The proxy SSE parser (
streamProxyinsrc/agents/runtime/proxy.ts) appends decoded chunks to abufferand splits on\n. Without any line-size guard, a hostile or misconfigured proxy endpoint can force unbounded buffering through three paths: (a) a singledata:line exceeding several MiB, (b) an unterminated tail that never receives\n, and (c) a final SSL frame without trailing newline at EOF — all OOM vectors.The previous attempt (#96993, closed by maintainer) used a 64 KiB string-
lengthtail cap, which failed non-deterministically: a 70 KiB line could cross or not cross the cap depending on TCP chunk boundaries, because 64 KiB is too close to legitimate data sizes.This fix applies a byte-accurate 1 MiB policy to all three paths: complete lines (deterministic via
split("\n")boundaries), the unterminated tail (now deterministic at 1 MiB because total accumulated bytes exceed the cap regardless of chunking), and the EOF final frame.Changes
src/agents/runtime/proxy.ts:228,237,246— three byte-accurateBuffer.byteLengthchecks at 1 MiB: (1) complete lines aftersplit("\n"), (2) unterminated tail after draining complete lines, (3) final frame at EOF before processing.src/agents/runtime/proxy.test.ts— 5 new tests: (1) oversized line in one chunk, (2) same line split across 3 chunks (deterministic), (3) 2000 small coalesced lines in one chunk (no false positive), (4) unterminated tail across 3 chunks without\n, (5) oversized final frame at EOF without trailing newline.Design Rationale
Why complete-line cap + tail cap + EOF cap? The proxy parser has three distinct buffering paths. The complete-line cap covers split-delimited lines; the tail cap covers the accumulated unterminated data; the EOF cap covers the final frame. All three use the same 1 MiB byte-accurate limit for a single consistent policy.
Why is a 1 MiB tail cap deterministic when the old 64 KiB tail cap wasn't? Non-determinism arises when the cap is smaller than or similar to legitimate data sizes, so chunk boundaries decide whether accumulated bytes cross the threshold. At 1 MiB — over an order of magnitude larger than any legitimate proxy SSE line (text deltas and tool call payloads are well under 100 KiB) — the total accumulated bytes from a hostile payload will exceed the cap regardless of TCP chunking: a 1.1 MiB line reaches 1.1 MiB total bytes in 1 chunk or 3 chunks or any number of chunks. The threshold is high enough that chunk-boundary artifacts vanish.
Why 1 MiB? Aligned with sibling SSE handling in the codebase (e.g.,
readResponseWithLimitsibling bounds).ProxyAssistantMessageEventhas no event-size contract, so 1 MiB is a generous safe guard that no legitimate text-delta or tool-call payload should approach.Why byte-accurate?
Buffer.byteLength(line, "utf-8")correctly measures UTF-8 bytes, unlikeString.lengthwhich counts UTF-16 code units and undercounts multibyte payloads. The previous tail-cap used.lengthwhile calling it "bytes", which was another finding in the maintainer's closure note.Real behavior proof
Real
node:httpserver + productionstreamProxypath (not mocked), driving 5 scenarios through real TCP sockets.node:httpserver on 127.0.0.1 (5 separate scenarios over real TCP sockets)Out of scope
Total proxy response body cap (separate concern, tracked in #96768). Other
.json()bounded-read PRs in the campaign. The provider SSE layer (provider-transport-fetch.ts) has its own tail-cap mechanism operating at\n\nevent boundaries and is not affected by this change.Risk checklist