[agent] Filed by the scheduled architecture audit routine (CLI and core). Register: discussion #560 register.
Kind: bug. Source: new finding (not in the October review); register C37.
Problem
ApiClient::fetch_binary, which backs fetch_blob and fetch_diff, reads a 200 response with resp.bytes().await and applies no bound (api/client.rs#L1089-L1099). The bytes are hash-checked only after they are fully in memory.
The crate already has a shared capped body reader for this. utils::http::read_capped / read_capped_typed (utils/http.rs#L22-L40) rejects an over-large Content-Length and an over-long stream. Its doc says it was hoisted "so the self-update downloader shares the exact cap semantics". It is used by:
The patch blob and diff path, which is the one every apply/get hits, bypasses it. The diff archive's decompressed size is capped later, at 64 MiB in patch/package.rs, but the download itself is not.
Reproduced twice on main @ 1169ae6 with an integration test against ApiClient (not committed). A local server answers the authenticated blob URL with a 600 MiB application/octet-stream body. fetch_blob returned Ok(Some(len = 629145600)), buffering 600 MiB, more than twice the cap the vendor path enforces on the same client.
Symptoms
None filed.
Impact
A misbehaving or hostile endpoint can exhaust memory with one response: a mis-set --api-url/SOCKET_API_URL, a proxy, or the public patch proxy serving a wrong file. The blob is rejected afterwards anyway. This is low-to-medium risk, a consistency defect with a small fix.
Proposed change
- In
fetch_binary, replace resp.bytes() with read_capped_typed(resp, MAX_PATCH_BLOB_BYTES, kind) and map CapExceeded / Truncated onto ApiError the way the vendor path does.
- Choose one named cap. 64 MiB matches the per-file cap the patch engine applies elsewhere (
MAX_FILE_BYTES, patch/package.rs).
- No new reader. This deletes the last uncapped
bytes() body read in api/.
Size and scope
api/client.rs, ~15–30 production lines plus one test. Out of scope: timeouts (#570) and retry for blob/diff (C15).
Acceptance criteria
Dependencies
None. It is best landed with or next to #570 because both touch fetch_binary.
[agent] Filed by the scheduled architecture audit routine (CLI and core). Register: discussion #560 register.
Kind: bug. Source: new finding (not in the October review); register C37.
Problem
ApiClient::fetch_binary, which backsfetch_blobandfetch_diff, reads a 200 response withresp.bytes().awaitand applies no bound (api/client.rs#L1089-L1099). The bytes are hash-checked only after they are fully in memory.The crate already has a shared capped body reader for this.
utils::http::read_capped/read_capped_typed(utils/http.rs#L22-L40) rejects an over-largeContent-Lengthand an over-long stream. Its doc says it was hoisted "so the self-update downloader shares the exact cap semantics". It is used by:MAX_VENDOR_PACKAGE_BYTES= 256 MiB, described as a "defensive bound against a runaway / hostile serve response" (#L1689-L1692,#L1783-L1785);#L2796-L2798);update/download.rs#L100,update/release.rs#L408).The patch blob and diff path, which is the one every
apply/gethits, bypasses it. The diff archive's decompressed size is capped later, at 64 MiB inpatch/package.rs, but the download itself is not.Reproduced twice on main @
1169ae6with an integration test againstApiClient(not committed). A local server answers the authenticated blob URL with a 600 MiBapplication/octet-streambody.fetch_blobreturnedOk(Some(len = 629145600)), buffering 600 MiB, more than twice the cap the vendor path enforces on the same client.Symptoms
None filed.
Impact
A misbehaving or hostile endpoint can exhaust memory with one response: a mis-set
--api-url/SOCKET_API_URL, a proxy, or the public patch proxy serving a wrong file. The blob is rejected afterwards anyway. This is low-to-medium risk, a consistency defect with a small fix.Proposed change
fetch_binary, replaceresp.bytes()withread_capped_typed(resp, MAX_PATCH_BLOB_BYTES, kind)and mapCapExceeded/TruncatedontoApiErrorthe way the vendor path does.MAX_FILE_BYTES,patch/package.rs).bytes()body read inapi/.Size and scope
api/client.rs, ~15–30 production lines plus one test. Out of scope: timeouts (#570) and retry for blob/diff (C15).Acceptance criteria
Content-Lengthor by stream length, returns an error without buffering past the cap. A diff response gets the same test.binary_fetch_error_classification_e2e,blob_fetcher_edges_e2eandcovgap_api_blob_fetcherstay green.Dependencies
None. It is best landed with or next to #570 because both touch
fetch_binary.