Skip to content

Registry.pullBlob accepts resumed 206 responses for the wrong byte range #1255

Description

@coygeek

Registry.pullBlob accepts resumed 206 responses for the wrong byte range

Target: openai/tart at 6ada2b955d5af1724751056753f22a69f9fe00d1

Lane: Tart registry/OCI/fetcher/push/pull/credential/network error handling

Summary

When Registry.pullBlob resumes a blob request with rangeStart != 0, it sends Range: bytes=<rangeStart>- and requires HTTP 206, but it never verifies the response Content-Range header. A server can return 206 Partial Content for a different range, such as bytes 0-2/6, and Tart streams those bytes to the caller as if they start at the requested offset.

For disk-layer resume, this can feed duplicate or wrong compressed bytes into the LZ4 decompressor and turn a registry protocol error into a later decompression/retry failure. The registry layer should reject the mismatched range before handing any bytes to downstream consumers.

Root Cause

Sources/tart/OCI/Registry.swift changes the expected status to 206 and sends a Range header when rangeStart != 0, but after status validation it immediately streams the body:

if rangeStart != 0 {
  expectedStatusCode = HTTPCode.PartialContent
  headers["Range"] = "bytes=\(rangeStart)-"
}
...
for try await part in channel {
  try await handler(part)
}

There is no check that Content-Range starts at the requested offset.

Red Evidence

Proof worktree: /tmp/tart-source-bug-range-content

Added a focused local-registry test:

func testPullBlobRejectsPartialContentWithWrongStartOffset() async throws

The test calls pullBlob(digest, rangeStart: 3) while the fake registry returns:

HTTP/1.1 206 OK
Content-Range: bytes 0-2/6

Red command:

swift test --filter RegistryRangeVerificationTests/testPullBlobRejectsPartialContentWithWrongStartOffset

Red result:

failed - pullBlob should reject a 206 response whose Content-Range starts at the wrong offset
Executed 1 test, with 1 failure

The failure proves Tart accepted a resumed response for the wrong byte range.

Minimal Proof Fix

After the status-code check, require a Content-Range header whose start offset matches rangeStart before streaming the body:

+ case UnexpectedContentRange(expectedStart: Int64, actual: String)
+ if rangeStart != 0 {
+   guard let contentRange = response.value(forHTTPHeaderField: "Content-Range"),
+         contentRange.starts(with: "bytes \(rangeStart)-") else {
+     throw RegistryError.UnexpectedContentRange(...)
+   }
+ }

Green Evidence

Focused green:

swift test --filter RegistryRangeVerificationTests/testPullBlobRejectsPartialContentWithWrongStartOffset

Result:

Executed 1 test, with 0 failures

Broader non-live suite:

swift test --skip LayerizerTests --skip RegistryTests

Result:

Executed 42 tests, with 0 failures

Classification

accepted_source_bug

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions