Skip to content

fix(cache): verify cache checksum on restore - #4169

Open
ss1909 wants to merge 1 commit into
mainfrom
sneha/A-1494-verify-cache-checksum-restore
Open

fix(cache): verify cache checksum on restore#4169
ss1909 wants to merge 1 commit into
mainfrom
sneha/A-1494-verify-cache-checksum-restore

Conversation

@ss1909

@ss1909 ss1909 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Description

When we download a cache, its object name in storage is the SHA-256 of its contents (content-addressed storage). Until now we trusted that the bytes we downloaded matched that name but never checked — so a corrupt or half-written upload would sail through, we'd wipe the target folders, and the failure would surface later as a confusing "not a valid zip file" error, after we'd already destroyed any good existing state.

This adds a verification step on restore: re-hash the downloaded archive and confirm it matches the name it was fetched under, before we touch any target folder.

  • On match: carry on and unpack as normal (unchanged behaviour).
  • On mismatch: treat it as a clean cache miss — never unpack, leave existing files untouched, let the build continue normally, and emit a log line + span attribute so we can see how often it happens.

The entry is deliberately not invalidated on a mismatch: the stored bytes are wrong, not the entry, and this is a read-side safety check only.

Context

https://linear.app/buildkite/issue/A-1494/verify-cache-checksum-on-restore

Changes

  • Add verifyBlobDigest in internal/cache/restore.go, reusing the existing streaming archive.ChecksumSHA256 calculator that save already uses. Unknown digest algorithms are skipped rather than failed, so a future algorithm can't turn every verifiable cache into a false mismatch.
  • Call it in downloadCache after the download completes and before returning to the caller (i.e. before cleanPath); on failure it cleans up the temp dir, records cache.digest_mismatch on the span, and returns a new ErrDigestMismatch sentinel.
  • Handle ErrDigestMismatch in Restore as a clean miss: CacheHit=false, CacheRestored=false, no error returned, slog.Warn + span attributes for observability, no entry invalidation.
  • Add TestCacheIntegration_RestoreDigestMismatchIsMiss: corrupts a stored blob's bytes (keeping its content-addressed name), then asserts the restore is a clean miss, a pre-existing sentinel file in the target folder survives, and no expire/invalidate call fires.

Testing

  • Tests have run locally (with go test ./...). Buildkite employees may check this if the pipeline has run automatically.
  • Code is formatted (with go tool gofumpt -extra -w .)

Affiliation (optional, external contributors)

Disclosures / Credits

@ss1909
ss1909 requested review from a team as code owners August 4, 2026 01:06
@ss1909
ss1909 force-pushed the sneha/A-1494-verify-cache-checksum-restore branch from 71fb0a0 to 0709d21 Compare August 4, 2026 01:07
@ss1909 ss1909 added the bug label Aug 4, 2026

@buildsworth-bk-app buildsworth-bk-app Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues in the verifier need addressing before merge; details inline.

Want to dig deeper?

Paste this into your agent to explore the findings from this review's Buildkite build:

Download the buildsworth logs from build 11438, then answer my questions about the findings.

Install the reading-buildsworth-logs skill to run this.

About buildsworth

Model: gpt-5.6-sol with xhigh thinking.

How to request a review: Comment @buildsworth-bk review on the PR, or request buildsworth-bk as a reviewer.

Risk labels (how buildsworth classifies risk) — buildsworth classifies risk itself from the diff. To let it approve, grant L2 approval by mentioning @buildsworth-bk (see L2 approval grant):

  • L1 — Low risk (dep bumps, docs/copy, lockfiles, small presentational fixes). buildsworth may approve by default.
  • L2 — Standard risk (new UI, additive API fields, refactors). Approved only with an L2 grant; otherwise comment-only.
  • L3 — High risk (auth, migrations, payments, secrets, perf-critical paths). Human review always required.

Comment thread internal/cache/restore.go
Comment thread internal/cache/restore.go Outdated

@zhming0 zhming0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note @ss1909 per the Linear ticket, this is blocked by A-1584. If you wish to fast track this, it might be good to target one of the branch in A-1584 (PR stack).
~~
wrong, sorry!

@ss1909
ss1909 force-pushed the sneha/A-1494-verify-cache-checksum-restore branch from 0709d21 to 598b75d Compare August 4, 2026 03:22
@ss1909
ss1909 force-pushed the sneha/A-1494-verify-cache-checksum-restore branch from 598b75d to 27f4eab Compare August 4, 2026 03:24

@buildsworth-bk-app buildsworth-bk-app Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The algorithm guard and cancellation handling are addressed. The remaining latency question is re-raised in its existing thread; I found no new issues in this pass.

Want to dig deeper?

Paste this into your agent to explore the findings from this review's Buildkite build:

Download the buildsworth logs from build 11499, then answer my questions about the findings.

Install the reading-buildsworth-logs skill to run this.

Comment thread internal/cache/restore.go
Comment on lines +412 to +425
func verifyBlobDigest(ctx context.Context, archiveFile string, digest api.CacheDigest) error {
if digest.Algorithm != "sha256" {
return nil
}
f, err := os.Open(archiveFile)
if err != nil {
return fmt.Errorf("failed to open archive for digest check: %w", err)
}
defer func() { _ = f.Close() }()

// Hash in chunks, checking ctx between reads, so a cancelled restore aborts
// promptly instead of reading the whole archive.
sum := archive.NewChecksumSHA256(io.Discard)
buf := make([]byte, 1024*1024)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linear ticket has a note but it wasn't clear enough, my bad. But as I commented in the other PR, it'd be best if we avoid reading the same archive file twice from the disk.

@zhming0 zhming0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left one blocking comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants