Skip to content

fix(storage): never commit a partial blob write - #612

Open
dylanjeffers wants to merge 1 commit into
mainfrom
fix/abort-partial-blob-writes
Open

dylanjeffers wants to merge 1 commit into
mainfrom
fix/abort-partial-blob-writes

Conversation

@dylanjeffers

Copy link
Copy Markdown
Contributor

Summary

A reported track (hoodinternet "1986") plays for 11 seconds and stops. Its Qm blob is 447,449 bytes on all 12 nodes holding it. The file's own LAME Info header says the encode was 7,333,590 bytes, the stored copy ends mid-frame, and it does not hash to its CID. Nothing ever caught it, because mediorum can't validate Qm CIDs and pulls them unchecked. This PR closes the two ways that can still happen.

1. Failed copies were committed. gocloud's Writer.Close commits whatever has been written, on every driver; only cancelling the writer's context aborts (blob.go: "To abort a write, cancel ctx"). replicateToMyBucket and relocateBetweenBuckets called _ = w.Close() after a failed io.Copy, so a pull that died partway stored a truncated blob under the full CID. In relocateBetweenBuckets that partial copy can later be taken as the real one. The new copyToBucket cancels before closing on error. The image-variant cache uses it too.

2. Short legacy pulls were accepted. Repair, sniff and the serve fallback use pullFileFromHost, which never checks content. For a Qm CID the byte count is the only check available. chunkedBlobReader now fails with errPulledBlobSizeMismatch when the bytes received differ from the size the peer reported, from Content-Range or Content-Length. A legacy pull from a peer that streams with no length asks /internal/blobs/info for the size first, and is refused if that fails.

Not in scope: a peer that already holds a short copy reports the short size, so this can't repair existing truncations (76833's full file no longer exists anywhere). It stops new ones and stops a partial pull from spreading.

Tests

  • New partial_write_test.go: a failed copy leaves nothing under the key (memblob and fileblob); a truncated legacy pull errors and stores nothing; a complete legacy pull succeeds; a legacy pull of unknown size is refused; a 206 body longer than its Content-Range errors.
  • Mutation check: removing the cancel() fails the two write tests, and removing the size check fails the two size tests.
  • go test ./pkg/mediorum/... -count=1 against a local Postgres: all packages pass. go vet and gofmt are clean on the changed files.

🤖 Generated with Claude Code

gocloud's Writer.Close commits whatever was written; only cancelling the
writer's context aborts. replicateToMyBucket and relocateBetweenBuckets
closed the writer after a failed io.Copy, so a pull that died partway
stored a truncated blob under the full CID. Route every bucket copy through
copyToBucket, which cancels before closing on error.

Qm CIDs can't be hash-checked, and repair, sniff and the serve fallback pull
them unvalidated, so a short copy then spreads to every node. The chunked
reader now fails when the byte count differs from the size the peer
reported (Content-Range or Content-Length), and legacy pulls with no
reported size ask the peer's blob info first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants