Skip to content

store(dedup): reject torn content-chunk reads instead of serving them as success - #51

Open
b7r6 wants to merge 2 commits into
mainfrom
pr/worker-cleanup-guard
Open

b7r6 wants to merge 2 commits into
mainfrom
pr/worker-cleanup-guard

Conversation

@b7r6

@b7r6 b7r6 commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

What and why

Lane A stability fix from the findings catalog (see nativelink-findings-2026-10-05.csv
on the meta/findings-catalog-2026-10-05 branch), rebased onto OSS main
@bf4b3aa855. This fork PR exists to validate the change on CI that is
representative of upstream PR lanes (public read-only cache, same endpoints)
before any upstream submission.

How was this verified?

The branch carries its own regression tests (fails-without where the fix
class allows it — see the branch's test commits). This PR's purpose is the
full representative CI run; green here rehearses the exact checks an
upstream PR would face.

Risk

Fork-internal validation PR; nothing lands anywhere by merging this. The
underlying fix's risk assessment happens at upstream-submission time.

AI assistance

Agent-assisted pipeline (Claude) for rebase and PR mechanics under my
direction; the fix itself carries verification in its tests.

@b7r6
b7r6 force-pushed the pr/worker-cleanup-guard branch 3 times, most recently from acc1d8b to efe34ec Compare October 8, 2026 16:32
@b7r6
b7r6 force-pushed the pr/worker-cleanup-guard branch 2 times, most recently from 25468d6 to d3d7900 Compare October 15, 2026 11:19
… as success

DedupStore::get_part trusted the per-chunk sizes recorded in the index and
streamed each content chunk without checking it actually held as many bytes as
its index entry declared. A short content chunk (a torn/partial read or an
index/content size disagreement) would either panic the worker on the
bytes_to_skip assert, or silently under-deliver and send EOF, serving a
truncated read to the caller as success.

Add a per-chunk size-consistency check: a fetched content chunk whose length
does not equal the size its index entry declares now yields a NotFound error.
The check is per-chunk (not a post-loop total), so a legitimate bounded request
that overshoots the end of the blob still returns the available bytes.

Regression tests: get_part_short_chunk_is_error_test (fails without the fix:
"got Ok with 10 bytes") and get_part_over_length_request_still_ok_test (guards
against over-correction).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@b7r6
b7r6 force-pushed the pr/worker-cleanup-guard branch 2 times, most recently from f302292 to 84976fe Compare October 15, 2026 12:02
Rejecting a wrong-size chunk with NotFound stopped the truncated read, but
left the bad chunk in place. `has()` sums the sizes the content store reports,
so the blob stayed "present", and `update()` skips any chunk the content store
already has, so the client's re-upload kept the bad copy. The blob could never
be read again.

On a size mismatch, `get_part` now makes a best-effort `remove` of the chunk
from the content store, as `VerifyStore` does for a copy that fails
verification. The blob then reads as missing and the next upload writes the
chunk again. The error stays NotFound, not DataLoss, so a `FastSlowStore`
above can still fall back to its slow tier.

Tests now cover the error code, the removal, a read that starts inside a short
chunk (the old assert panic), a chunk longer than declared, and an end-to-end
truncate / fail / re-upload / read cycle. The removal, has() and repair
assertions fail without this change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.

1 participant