Skip to content

Snapshot resume should reject mismatched Content-Range starts before appending to .part files #319

Description

@Kewe63

Summary

The snapshot downloader supports resumable downloads by keeping a .part file and retrying with a Range request starting at the existing partial file size.

However, when the server returns HTTP 206 Partial Content, the downloader does not verify that the response Content-Range start offset matches the local .part file size before appending the response body.

This means a server, CDN, cache, or proxy that returns a mismatched partial response can cause the downloader to append bytes from the wrong offset to the existing .part file. The resulting snapshot file can be corrupted while still being treated as a successful download.


Affected File

crates/snapshots/src/download.rs


Observed Behavior

When a partial file already exists, the downloader sends a request like:

Range: bytes=7-

If the server responds with:

HTTP/1.1 206 Partial Content
Content-Range: bytes 0-3/11

the response starts at byte 0, not byte 7.

The current implementation still appends the returned body to the existing .part file instead of rejecting the response or restarting the download.


Expected Behavior

For resumable downloads, when a local .part file exists and the downloader requests:

Range: bytes=<existing_size>-

then a 206 response should only be accepted if the Content-Range start offset is exactly <existing_size>.

If the Content-Range start does not match the requested/local offset, the downloader should either:

  1. return an error, or
  2. discard the stale .part file and restart the download from byte 0.

It should not append mismatched bytes to the existing partial file.


Reproduction

I verified this with a focused regression test against the current code.

The test creates a partial file containing:

prefix-

The partial file size is 7 bytes, so the downloader sends:

Range: bytes=7-

The mock server intentionally responds with a mismatched Content-Range:

Content-Range: bytes 0-3/11

and body:

pref

The expected behavior is for the downloader to reject this response. Instead, the current code accepts it and appends the bytes, so the regression test fails.

Test command used:

cargo +1.94.0 test -p arc-snapshots resumable_download_rejects_mismatched_content_range_start -- --nocapture

Result on current code:

test download::tests::resumable_download_rejects_mismatched_content_range_start ... FAILED

Failure message:

mismatched Content-Range should not append to the existing .part file


Why This Matters

A resumed snapshot download relies on the server returning exactly the missing byte range. If the response starts at a different offset, appending it to the local .part file produces a corrupted snapshot.

This can happen due to:

  • a buggy origin server,
  • a misbehaving proxy/cache/CDN,
  • stale cached partial responses,
  • or any server-side issue that returns an incorrect 206 response.

The downloader already tracks the local partial size and sends a precise Range request, so validating the returned Content-Range start would prevent accepting corrupted resumed downloads.


Suggested Fix

When resuming from an existing .part file:

  1. Parse the Content-Range header on HTTP 206 responses.
  2. Extract the start offset.
  3. Compare it with the local partial file size / requested Range start.
  4. If they differ, reject the response or restart from scratch.

For example:

  • existing .part size: 7
  • request: Range: bytes=7-
  • valid response: Content-Range: bytes 7-10/11
  • invalid response: Content-Range: bytes 0-3/11

The invalid response should not be appended to the existing .part file.


Potential Regression Test

A regression test can be added near the existing resumable download tests in crates/snapshots/src/download.rs.

Test idea:

  • seed a .part file with b"prefix-"
  • configure the mock server to expect Range: bytes=7-
  • return HTTP 206 with Content-Range: bytes 0-3/11
  • assert that run_resumable_download(...) returns an error
  • assert that mismatched data is not accepted as a completed snapshot

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions