Skip to content

fleet(dispatch): dechunked_len does unchecked arithmetic on the peer-declared chunk size — overflow panics in debug, wraps in release #6512

Open

Description

@dezren39

Summary

fleet-dispatch's hand-rolled HTTP response reader does unchecked arithmetic on the peer-declared chunk size in dechunked_len — the function read_response uses to decide whether a chunked body is complete. A chunk-size line naming one of the two largest usize values panics in debug/test builds (attempt to add with overflow) and, in release builds where overflow checks are off, wraps so the completeness guard and the slice index are computed from wrapped values. The sibling decoder dechunk in the same file guards the same declared size before slicing and handles the identical input safely — the two halves of the chunked reader disagree about whether peer-declared sizes can be trusted.

This is the #5579 rule ("peer-declared sizes are never trusted") applied everywhere else in the fleet HTTP readers — fleet-proxy's http.rs caps chunk sizes against MAX_RESPONSE_BODY_BYTES with checked_add before allocating — but missed in the dispatch client.

Evidence (verified this run, laptop, origin/main 540d3bea3f)

pkgs/fleet/crates/fleet-dispatch/src/lib.rs:

  • :303 — let size = usize::from_str_radix(size_str.trim().split(';').next()?.trim(), 16).ok()?; — the chunk size is parsed straight from the response bytes, unbounded.
  • :308 — if rest.len() < size + 2 { return None; } — size + 2 is unchecked: for size = usize::MAX or usize::MAX - 1 this overflows.
  • :311 — total += size; — also unchecked.
  • :312 — rest = &rest[size + 2..]; — slices at the same unchecked expression.
  • read_response (:273, the complete closure at :265-272) calls dechunked_len(body) after every read while filling a chunked body, so the panic/wrap triggers mid-exchange on response bytes alone — no local input needed beyond running fleet dispatch status|launch|enqueue against an endpoint that returns such a chunk-size line.
  • Contrast :326 — dechunk checks size == 0 || rest.len() < size before slicing and never adds to the declared size; and pkgs/fleet/crates/fleet-proxy/src/http.rs read_chunked, which does out.len().checked_add(size) and refuses anything past MAX_RESPONSE_BODY_BYTES before allocating.

Probe (real crate, laptop worktree ~/git/fleet/probe-dispatch-dechunk-wt at 540d3bea3f)

A temporary #[test] appended to the crate's own test module, run with cargo test -p fleet-dispatch probe_dechunked_len_huge_chunk_size -- --nocapture:

  • Control: dechunked_len(b"4\r\nWiki\r\n5\r\npedia\r\n0\r\n\r\n") == Some(9) ✔
  • Control: dechunked_len(b"4\r\nWi") == None ✔
  • Control: dechunk(b"ffffffffffffffff\r\nXX") == [] — the decoder is safe on the same bytes ✔
  • dechunked_len(b"ffffffffffffffff\r\nXX") → PANIC at crates/fleet-dispatch/src/lib.rs:308:25: attempt to add with overflow
  • dechunked_len(b"fffffffffffffffe\r\nXX") → PANIC at the same line
  • Probe result: PROBE DONE panics=true true, test passed (panics caught via catch_unwind).

In a release build the same expressions wrap instead of panicking: size + 2 becomes 1 (for MAX) or 0 (for MAX - 1), the :308 guard passes against a 2-byte remainder, total wraps, and rest[size + 2..] advances by the wrapped index — the completeness verdict for the exchange is then computed from wrapped arithmetic while dechunk decodes something else, so the client can finish an exchange early on a body the decoder reads differently.

Why it matters

fleet dispatch is the thin client every seat uses to launch and enqueue sessions through pm-dispatch, over both the direct and the CONNECT-tunnel transports. Its response reader is the one hand-rolled HTTP reader in the fleet workspace that still does raw arithmetic on a peer-declared length. pm-dispatch (Bun.serve) will not emit such a chunk size itself, but the reader's contract — per #5579 and the module's own framing — is that response framing is validated before it is trusted: a buggy, compromised, or mis-proxied endpoint (the tunnel path traverses the egress proxy) turns a status/launch/enqueue call into a client panic in debug builds and a silently wrong completeness verdict in release builds, instead of the crate's normal DispatchError::Transport exit-3 path. Panics also bypass the crate's exit-code contract (0/2/3/4) entirely.

Acceptance criteria

  • dechunked_len uses checked arithmetic (e.g. size.checked_add(2), a checked running total, and/or a bound of the declared size against the remaining bytes / the existing 64 MiB body cap) so no peer-declared chunk size can overflow, wrap, or panic in any build profile.
  • A chunk-size line that cannot describe a body within the buffered remainder (including the usize::MAX / usize::MAX - 1 cases) is treated as "not complete" or as a transport error — never as a completed body computed from wrapped values.
  • Unit tests in fleet-dispatch pin: the two overflow sizes above, a large-but-plausible size with insufficient bytes (None/error, no panic), and the existing happy-path pins (chunked_bodies_dechunk) keep passing.
  • cargo test -p fleet-dispatch passes on the laptop or in CI.

Scope note: read_response's Content-Length handling and dechunk itself are out of scope except where the fix naturally shares a guard; the pooled-connection desync concerns of #5579 do not apply here (one exchange per connection, Connection: close).

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    on Nov 6, 2026
  2. developing-today commented on Nov 6, 2026

    @developing-today
    Contributor
  3. added
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/fleetpkgs/fleet multicall binary + platform crates
    on Nov 6, 2026
  4. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  5. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 6, 2026
  6. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  7. 10 remaining items

  8. added and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 6, 2026
  9. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  10. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 6, 2026
  11. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  12. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · design-architect pass 62 — adopted as Phase 3 of design #6582

  13. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  14. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/fleetpkgs/fleet multicall binary + platform crateskind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)queue/queuedHas a queued task in the Supabase agent queuesource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobstatus/parkedDeliberately paused with a recorded resume conditiontask/triageQueue packet kind: verify, reproduce, and route this itemwork/assignedQueue worker lifecycle state

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions