Add chunked transfer-encoding decoding#10
Conversation
2056a37 to
ece9f86
Compare
|
Rebased onto Verified locally after the rebase:
Still a draft: the open design question — expose decoding as an explicit |
There was a problem hiding this comment.
Build & Tests
This is the first substantive code review (the rebase comment aside, there are no prior reviews). CI is green, but note CI runs the branch on its own stale base — it does not reflect a merge into current master, and it does not feed dechunk any adversarial input.
The branch does not build in isolation locally: it still pins time@0.2.0 (http.carp:2), whose tm_zone const-discard trips -Werror here. master already moved to time@0.4.0 (#11) to fix exactly this. The dechunk code itself compiles fine — I verified it against a local merge into current master (which auto-merges http.carp to time@0.4.0): all 15 dechunk/chunked? tests pass, including a case I added where the chunk data contains CRLF (size-driven slicing handles it correctly). The happy path is solid, and chunked? (case-insensitive name+value, coding-list aware) is nicely done.
Then I tried to break it, and it broke.
Findings
1. [critical — memory safety] A crafted chunk size segfaults the decoder. parse-hex accumulates into a signed 32-bit Int with no overflow check, and dechunk never rejects a negative size. A hex size of 80000000 overflows to INT_MIN; being negative it slips past the (> size (- avail 2)) truncation guard, so data-end = data-start + size becomes a large-negative index and crlf-at? → String.char-at reads far out of bounds. Reproduced:
(TransferEncoding.dechunk "80000000\r\nWiki\r\n0\r\n\r\n") ; => process exits with signal 11 (SIGSEGV)
Int is 32-bit on CI's x86-64 too, so this is not an armhf artifact — CI is green only because nothing feeds an oversized size. Since dechunk decodes untrusted server responses, any malicious or buggy server can crash a client that decodes its chunked body (DoS / memory unsafety). Fix: reject size < 0 in dechunk (turns the crash into a clean error), and detect overflow in parse-hex.
2. [medium] Silent mis-decode on sizes that wrap to zero. Same root cause. 100000000 (2^32) wraps to 0 in 32-bit Int, so dechunk treats it as the terminating zero chunk and returns (Success "") instead of an error:
(TransferEncoding.dechunk "100000000\r\nWiki\r\n0\r\n\r\n") ; => Success "" (should be an Error)
A real overflow guard in parse-hex fixes both #1 and #2. (For contrast: 7fffffff correctly errors via the truncation guard, and ffffffff → -1 happens to land in-bounds and errors — only the specific overflow-to-large-negative values crash, which is exactly why a targeted test missed it.)
3. [blocker for merge] Does not merge cleanly into current master; missing #12/#13. The branch's merge-base is c77d972, but master is now ea5f0ef; the branch contains none of #11 (time), #12 (multipart/MediaType), or #13 (Accept) — despite the rebase comment, it never actually incorporated #12. git merge-tree origin/master HEAD reports content conflicts in README.md and test/http.carp (both additive: #12's helper/assert blocks vs. the dechunk blocks); http.carp auto-merges. It needs a genuine rebase onto current master, re-resolving those additive conflicts, to be mergeable — and that also picks up time@0.4.0 explicitly.
Verdict: revise
The decode logic is correct for well-formed input and the API is well-shaped, but dechunk crashes on a crafted chunk size from untrusted input — that must be fixed before this can merge. It also needs a real rebase onto current master (currently conflicts, and is missing #12/#13). The open design question (explicit dechunk vs. integrating into body parsing) is yours to decide, but the overflow fix is needed either way.
Add TransferEncoding.dechunk to decode a chunked-encoded body per RFC 7230 section 4.1, plus Request.chunked?/Response.chunked? predicates that detect a chunked Transfer-Encoding (case-insensitive, comma-list aware). parse behavior is unchanged: it still stores the raw body verbatim, so callers opt into decoding only when chunked? is true.
parse-hex accumulated the chunk size in a signed 32-bit Int with no overflow check, so a crafted hex size like 80000000 wrapped to a negative value that slipped past dechunk's truncation guard and became an out-of-bounds index (SIGSEGV / DoS on untrusted server data), and 100000000 wrapped to 0 and silently mis-decoded the body as empty. Reject sizes that would overflow Int in parse-hex, guard against a negative size in dechunk as defense in depth, and add regression tests for both inputs.
ece9f86 to
4a4fe24
Compare
|
Addressed the review's blocking findings; the branch is rebased onto current Findings #1 & #2 (overflow → SIGSEGV / silent mis-decode). Root fix is in I confirmed the tests bite by reverting just the Finding #3 (rebase / conflicts). Rebased onto Left as a draft: the explicit- |
There was a problem hiding this comment.
Build & Tests
Checked out claude/chunked-transfer-decoding (4a4fe24) and built/ran it against current master.
- Build: now builds in isolation —
http.carp:2moved totime@0.4.0, so the oldtime@0.2.0tm_zone-Werrorbreak that blocked the last round is gone. - Tests:
carp -x test/http.carp— 120 pass, 1 fail. The single failure isform-data on empty body returns empty map, which also fails onmaster(the pre-existing armhfsplit-by ""artifact, green on CI). This branch adds 17 net-new passing tests. CI green on ubuntu + macos.
Prior feedback
All three findings from the last review are genuinely addressed — I re-verified each rather than taking the comment at its word.
- #1 (critical — SIGSEGV on crafted chunk size): FIXED.
parse-hex(http.carp:537) now rejects any digit that would overflow with(> acc (/ (- Int.MAX d) 16)). That threshold isfloor(Int.MAX/16) = 134217727, which is exactly the boundary:acc ≤ 134217727 ⇒ acc*16+d ≤ Int.MAX, and the multiply only runs after the guard passes, so there's no intermediate overflow either. I re-ran the exact repro(dechunk "80000000\r\nWiki\r\n0\r\n\r\n")→ cleanError, no crash, process exit 0. - #2 (silent mis-decode on wrap-to-zero): FIXED by the same guard.
(dechunk "100000000\r\nWiki\r\n0\r\n\r\n")→Error, no longerSuccess "". The(< size 0)guard indechunk(http.carp:587) is kept as defense-in-depth. Both regression tests usedechunk-err?, which matches onResult.Error, so they actually bite. - #3 (didn't merge; missing #12/#13): FIXED. Merge-base is now current
master(ea5f0ef),git merge-treereports no conflicts, and the branch carries #11/#12/#13.
Findings
I pushed harder on the overflow path: ffffffff, fffffffff (9 nibbles), 80000001, uppercase FFFFFFFF, and 7fffffff (parses to Int.MAX, then correctly hits the truncation guard) — every one returns a clean Error with no crash, and legit bodies (Wiki+pedia, plus chunk-extension and trailer forms) still decode. Bounds are safe: the (< avail 2) / (> size (- avail 2)) guards ensure data-end+1 < len before crlf-at? reads. No new issues.
Verdict: merge
The security blocker is closed and verified, and the branch is mergeable against current master. From a code standpoint there's nothing left to revise. It remains a draft on purpose: the one open question — expose decoding as explicit TransferEncoding.dechunk (current) vs. integrating it into body parsing — is a design call for you, and un-drafting is yours to make. The overflow fix was needed either way and is done.
Chunked transfer-encoding decoding (RFC 7230 §4.1)
Response.parse/Request.parsestore the raw body bytes verbatim. For amessage with
Transfer-Encoding: chunked, that raw body is the chunk framing(hex sizes + CRLFs), not the actual content — so a complete chunked response
body was unusable at this layer. This adds a decoder so callers can recover the
content.
What's new
Request.chunked?/Response.chunked?— predicate that reports whether aparsed message's
Transfer-Encodingheader listschunked. Case-insensitivein both the header name and value, and aware that the value may be a coding
list such as
gzip, chunked.TransferEncoding.dechunk— takes a chunked-encoded bodyStringandreturns
(Result String String). It parses each chunk (hex size line, withany
;-delimited chunk extension ignored; then that many body bytes; then aCRLF) until a zero-size chunk, ignores optional trailer headers, and returns
an
Erroron malformed framing (bad hex, truncated chunk, missing terminator).Work is byte-accurate (reuses
String.find-crlf,String.byte-slice,String.char-at), not codepoint slicing.Additive by design
parseis unchanged — it still stores the raw body. Decoding is opt-in:Tests
Adds 15 cases to
test/http.carp: multi-chunk decode, multi-digit hex size,chunk-extension ignored, zero-only body, trailing headers ignored, three
malformed-framing errors (non-hex size, chunk shorter than declared, missing
terminating chunk), a full parse +
chunked?+dechunkintegration, and thechunked?predicate cases (case-insensitive, comma list, absent, non-chunked).Ran the suite locally (
carp -x test/http.carp): all 15 new tests pass.carp-fmt -cand
anglerare clean on the changed files.gendocs.carpnow documents the newTransferEncodingmodule and the README has a usage section.Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.