fix(snapshots): reject mismatched resume Content-Range starts - #325
fix(snapshots): reject mismatched resume Content-Range starts#325Kewe63 wants to merge 1 commit into
Conversation
|
Reviewed at What's right
Blocking: rejecting without clearing the
|
| merge | result |
|---|---|
#325 alone onto current main (97f8da0) |
clean |
#139 alone onto current main |
conflict |
#139 on top of main + #325 |
conflict |
#139 already conflicts with main on its own — its merge base is a85368c0, well behind — so it needs a rebase regardless and nothing here is #325's fault. Worth stating plainly so this PR doesn't get blamed for it.
That said, the two do collide textually. The import change auto-resolves because both PRs make a byte-identical edit, but both then rewrite the body of parse_total_size in different ways, and that conflicts:
<<<<<<< HEAD (#325: inlined, using CONTENT_RANGE)
=======
parse_content_range_total(response.headers()) (#139: extracted helper)
>>>>>>> pr139
If both land you end up with parse_content_range_total(&HeaderMap) and parse_content_range_start(&Response) side by side — two near-duplicate parsers of the same header with different signatures. A single parse_content_range() -> Option<ContentRange { start, end, total }> would serve both and remove the conflict entirely. Worth a word with #139's author about who absorbs it.
Also worth noting the two are genuinely complementary: #139's 416 handling covers the case where the .part is already complete, which is a neighbouring branch of the same resume logic.
Carried over from #319 (not blocking, scope discipline here is good)
Listing only because they live in the lines this PR touches, so they're cheap to fold in if a maintainer wants them:
Content-Range: bytes 7-10/*is legal (RFC 9110 §14.4, complete-length may be*). The new start check passes, thenparse_total_sizedoes"*".parse::<u64>()→None→"Server did not provide Content-Length or Content-Range header", which is false — it did.strip_prefix("bytes ")is exact on case and spacing; range units are case-insensitive tokens per RFC 9110.- An inverted range like
bytes 7-3/11still validates, since only the start is compared. - The final
.partlength is still never compared againsttotalbeforerename.
Good, tightly-scoped fix — the remove_file line is the one thing I'd want before it merges.
Summary
Fixes #319
This fixes resumable snapshot downloads so a HTTP 206 Partial Content response is only appended to an existing
.partfile when the responseContent-Rangestart offset matches the local partial file size.Previously, the downloader sent:
Range: bytes=<existing_size>-
when a
.partfile existed, but it only usedContent-Rangeto read the total size. It did not validate that the returned byte range actually started at<existing_size>.That allowed a mismatched response such as:
Content-Range: bytes 0-3/11
to be appended to a 7-byte
.partfile requested with:Range: bytes=7-
which could corrupt the resumed snapshot.
Changes
CONTENT_RANGEheader constant instead of a string literal.Content-Rangestart parsing for 206 responses.Content-Rangestart does not match the local.partfile size.Content-Rangestart..partfile.Tests
Regression test failed before the fix:
cargo +1.94.0 test -p arc-snapshots resumable_download_rejects_mismatched_content_range_start -- --nocaptureFailure before fix:
mismatched Content-Range should not append to the existing .part file
After the fix:
cargo +1.94.0 test -p arc-snapshots resumable_download_rejects_mismatched_content_range_start -- --nocaptureResult:
1 passed; 0 failed
Related resumable download tests:
cargo +1.94.0 test -p arc-snapshots resumable_download -- --nocaptureResult:
11 passed; 0 failed
Full arc-snapshots package tests:
cargo +1.94.0 test -p arc-snapshotsResult:
108 passed; 0 failed
Formatting and lint:
Result:
passed
Notes
There is an existing open PR #139 touching
crates/snapshots/src/download.rs, but it handles a different resume edge case: treating a fully downloaded.partfile as complete when the server returns 416 with a matching total size. This PR addresses #319 specifically: rejecting mismatched 206Content-Rangestart offsets before appending to.partfiles.Checklist
cargo fmt/cargo clippycleanRisk & Impact
Low. The validation only rejects the specific mismatched-offset case — a correctly-aligned 206 response (
Content-Rangestart matching the local.partsize) is unaffected. Verified the regression test fails against the old behavior and passes with the fix, confirming it exercises the actual corruption path.Type: 🐛 Bug fix
Fixes: #319