fetch: reject malformed chunk-size tokens instead of reading them as zero - #34918
Conversation
…t as zero phr_decode_chunked stopped at the first non-hex byte in the chunk-size token and fell through to CHUNKED_IN_CHUNK_EXT, which scans to LF without validating. A size line like "0x5\r\n" was therefore read as size 0 (last chunk) and fetch() resolved 200 with an empty body while the 5 payload bytes were still on the wire. node/llhttp reject this with HPE_INVALID_CHUNK_SIZE. After the hex digits, only ';' (chunk-ext), CR, or LF are valid per RFC 9112 7.1. Anything else now returns -1, surfaced as InvalidHTTPResponse, matching llhttp strict mode. Applied as a patch on the vendored picohttpparser.
|
Status: fix verified locally. Reproduced with the standalone script in the report: system bun resolves 6 malformed-token cases fail on main, pass with the patch. 5 well-formed-token cases pass on both as regression guards. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review. WalkthroughChangesChunked response validation
Merge Risk: ⚪ Minimal · up to Malformed chunk-size tokens are now rejected while valid chunk extensions remain supported. The regression tests and build pass, with no actionable merge-blocking risk identified. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 1:20 PM PT - Sep 10th, 2026
❌ @cirospaciari, your commit 990ab37 has 1 failures in 🧪 To try this PR locally: bunx bun-pr 34918That installs a local version of the PR into your bun-34918 --bun |
chunked-trailing.test.js binds its raw servers to "localhost" which races against IPv6-first resolution in some environments. The new chunk-size tests bind to 127.0.0.1 explicitly and live in their own file so the gate runs only the cases that exercise this fix.
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
No bugs found. Deferring to a human because this patches vendored HTTP wire-parsing (picohttpparser phr_decode_chunked) and tightens fetch() rejection behavior — small and well-tested, but a maintainer should sign off on going RFC-strict here.
What was reviewed:
- Patch placement against upstream
picohttpparser.cat the pinned commit — the new check runs only after ≥1 hex digit andsrc < bufsz, so no OOB read; incremental (split-packet) decoding still works becausesrc == bufszexits with-2before the new check. patches:field on theDependencytype and 10+ existing users — build integration matches the established pattern.- Only consumer of
phr_decode_chunkedis the HTTP client insrc/http/lib.rs; server-side chunked parsing (bun-uws/ChunkedEncoding.h) is separate and already strict. net.ServerhasSymbol.asyncDisposesoawait using _s = serverin the test cleans up correctly.
Extended reasoning...
Overview
Three files: a 6-line patch to vendored picohttpparser.c (phr_decode_chunked, CHUNKED_IN_CHUNK_SIZE state), a one-line patches: registration in scripts/build/deps/picohttpparser.ts, and a new parameterized test file with 6 reject + 5 accept cases. The patch adds a check that after the hex-digit run in a chunk-size token, the next byte is one of ; / CR / LF; anything else returns -1, which surfaces to JS as InvalidHTTPResponse.
Security risks
This is a strictness increase in an HTTP response parser. It closes a response-smuggling-shaped ambiguity (a token like 0x5 was read as size 0, silently truncating the body) and matches Node/llhttp strict mode. No new attack surface is introduced; the risk is compat — a server emitting non-RFC chunk-size tokens that Bun previously tolerated will now fail. Node already rejects every case in the test matrix, so any such server is already broken there. One nuance a maintainer may want to weigh: RFC 9112 §7.1.1 defines chunk-ext = *( BWS ";" ... ), and BWS is technically "MUST be accepted"; this patch (like llhttp) rejects 5 ;ext. That's a deliberate strict-mode choice, not a bug, but it's a policy call.
Level of scrutiny
Higher than the diff size suggests — it's a vendored-dep patch to the HTTP client's wire decoder, on the fetch() hot path, and it changes user-visible acceptance behavior. That said, the change is mechanically trivial (a 3-byte allowlist), RFC-grounded, Node-verified, and the test file exercises both directions.
Other factors
I verified the patch context lines against upstream source at commit 066d2b1 and confirmed: (1) buf[src] is already bounds-checked and dereferenced by decode_hex before the new check runs, (2) the incremental case (chunk-size split across reads) still returns -2 at src == bufsz before reaching the new check, so partial tokens aren't spuriously rejected, (3) phr_decode_chunked is only called from src/http/lib.rs (fetch client), so blast radius is contained. The patches mechanism is well-established (boringssl, libuv, libarchive, etc.). The test uses await using on a net.Server, which has Symbol.asyncDispose in Bun. Bug-hunting system found nothing.
There was a problem hiding this comment.
No issues found. This patches the vendored picohttpparser chunked decoder (HTTP parsing hot path used by every fetch()), so a maintainer should sign off on the vendor patch and the stricter-reject compat tradeoff.
What was reviewed:
- Verified the new check in
phr_decode_chunkedruns after thesrc == bufszbounds guard, sobuf[src]is always in-bounds; state is preserved correctly across split reads. - Confirmed only
;/CR/LF are accepted after the hex run per RFC 9112 §7.1, matching llhttp; chunk-ext (5;ext) still parses via the positive test cases. patches:field on theDependencyfollows the existinglsqpackpattern;net.ServerhasSymbol.asyncDisposesoawait using _s = servercleans up on assertion failure.
Extended reasoning...
Overview
Three files: a 6-line patch to vendored picohttpparser.c (phr_decode_chunked, CHUNKED_IN_CHUNK_SIZE state), registration of that patch in scripts/build/deps/picohttpparser.ts via the existing patches: field on the Dependency type, and a new test file with 11 it.each cases (6 malformed tokens rejected, 5 well-formed accepted). The fix: after consuming ≥1 hex digit, if the next byte is not ;, CR, or LF, return -1 instead of falling through to CHUNKED_IN_CHUNK_EXT and treating the garbage as an extension.
Security risks
This is HTTP response parsing — security-adjacent (chunked-encoding ambiguity is a known desync/smuggling vector). The change is strictly fail-closed: it rejects inputs that were previously mis-parsed with silent data loss. No new acceptance paths. I checked the read is in-bounds (the if (src == bufsz) goto Exit guard runs at the top of the same loop iteration), and traced the split-read case: if the hex run ends exactly at bufsz, the function exits with -2 and _hex_count is preserved, so the next call re-enters the loop, hits the non-hex byte, and the new check fires correctly.
Level of scrutiny
Higher than the line count suggests. This is the first patch applied to the picohttpparser vendor dep, it sits in the fetch() hot path, and it changes observable behavior (previously-resolving responses now reject). The change itself is mechanically simple and verified against the upstream source at the pinned commit, but vendored-dep patches and HTTP-parser strictness changes are the kind of thing a maintainer should explicitly approve rather than auto-merge.
Other factors
The patches: mechanism is already used by lsqpack (scripts/build/deps/lsqpack.ts:24), so the wiring is not novel. Tests use net.Server with await using for cleanup (Server.prototype[Symbol.asyncDispose] exists at src/js/node/net.ts:3318), port: 0, and cover both the new rejections and regression guards for ;-extensions and leading-zero/uppercase-hex sizes. The PR description tabulates Node vs before/after behavior. No outstanding reviewer comments.
|
One more symptom of the same hole, found while fixing a Server writes (then destroys the socket):
Checked the last row by compiling the vendored |
|
@robobun is this fix still relevant? aka the fix already landed? |
|
@cirospaciari Still relevant. The fix has not landed on main:
On matching Node 26.3 for the streaming case in the comment you linked: Node resolves |
|
@robobun check CI test/js/bun/http/serve-pending-promise-abort-leak.test.ts - code 1 on :debian: 13 x64-asan |
|
@cirospaciari Checked. That failure is not from this PR. It is a pre-existing red test on the
I have flagged the test separately so it gets looked at on its own. The other failures in builds 113925 and 113928 are the usual one-retry flakes on unrelated files ( Still working on the Node 26.3 streaming-parity check from my previous comment; will follow up with that next. |
|
Follow-up on Node 26.3 parity for the streaming case (
In all three rows
Closing that last gap means flushing the already-decoded bytes to the body stream before failing the request in those two arms. It touches the progress-then-fail ordering in |
|
@robobun open a new PR to add this parity |
|
On it. I will open the follow-up PR from main and link it here. |
|
Follow-up is open: #42261. It delivers the chunks ahead of a malformed chunk-size line to a streaming reader before the error, in both packetizations, and matches the Node v26.3.0 column of the table above. |
Repro
Before:
resolved: status=200 body=""(5 payload bytes silently dropped).Node:
rejected: TypeError(llhttpHPE_INVALID_CHUNK_SIZE).After:
rejected: InvalidHTTPResponse.Cause
phr_decode_chunkedin picohttpparser stops consuming the chunk-size at the first non-hex byte and falls through toCHUNKED_IN_CHUNK_EXT, which scans to LF without validating what came after the hex run. For0x5\r\nit reads0, seesx, breaks, skipsx5as "extension", andbytes_left_in_chunk == 0makes it the last chunk. The response resolves with an empty body.The already-rejected cases in the report (
+5, leading non-hex, overflow) fail because_hex_count == 0or the overflow guard fires; the hole is specifically "at least one hex digit, then garbage".Fix
After the hex run, only
;(chunk-ext), CR, or LF are valid (RFC 9112 7.1). Anything else returns-1and surfaces asInvalidHTTPResponse, matching llhttp strict mode. Verified against Node:55;foo0x55g55.0vendor/is fetched at build time, so the change is applied aspatches/picohttpparser/strict-chunk-size.patchand registered inscripts/build/deps/picohttpparser.ts. The server-side chunked parser inpackages/bun-uws/src/ChunkedEncoding.halready rejects these tokens.Verification
6 parameterized cases fail on main (resolve instead of reject), pass with the patch; the chunk-ext positive case passes both ways as a regression guard.
[policy-decision:dep] gate passed · iteration 2 · 3 files touched
passes on PR (with fix)
diff hotspot
gate history · 3 passed · 1 rejected · iteration 2
evidence per changed file