Repository navigation
fix: remote attestation state writes fail on second save during initialization #2908
Description
Activity
- added a commit that references this issue
on Mar 21, 2026 Additional research
Remote State Digest Mismatch Regression
Summary
A race condition in remote state attestation causes digest mismatch errors when collectors write materials and
EvaluateAttestationPoliciessubsequently tries to save state. The root cause:Remote.Write()never updated the in-memoryUpdateCheckSumafter a successful save, so any second write in the same flow sent a stale digest.Bug Mechanism
AutoCollectPRMetadata(orRunCollectors) callsLoadCraftingState→ gets digest D1addMaterial→stateManager.Write()sends D1 asBaseDigest, server saves and computes new digest D2EvaluateAttestationPolicies→stateManager.Write()still sends D1 (stale)- Server compares D1 against stored D2 → conflict
The fundamental flaw:
Remote.Write()discarded theSaveRPC response and never updatedstate.UpdateCheckSum. TheAttestationStateServiceSaveResponseproto was empty — nodigestfield existed to return.Regression Timeline
PR Date Effect #2617 2025-12-15 Added AutoCollectPRMetadata — single write at end of init, no issue #2765 2026-02-20 Regression introduced — added EvaluateAttestationPolicies after AutoCollectPRMetadata, creating two sequential writes without a Read in between #2843 2026-03-14 Worsened — collector framework added more writes before EvaluateAttestationPolicies Key InsightBefore PR #2765, PR info worked with remote state because there was only one
Writeat the end of init. The secondWritefromEvaluateAttestationPoliciesis what broke it.Fix
Return the new digest from the
SaveRPC soRemote.Write()can updateUpdateCheckSumimmediately — same pattern as HTTP ETags after a successfulPUTwithIf-Match.Changes
- Proto: Added
string digest = 1toAttestationStateServiceSaveResponse - Data layer:
Save()now returns(string, error), computing digest of newly stored state - Biz layer: Updated
AttestationStateRepointerface andAttestationStateUseCase.Save - Service layer: Returns digest in the response
- Client:
Remote.Write()updatesstate.UpdateCheckSumfromresp.GetDigest()
Key Files
app/controlplane/api/controlplane/v1/attestation_state.protoapp/controlplane/pkg/data/attestationstate.goapp/controlplane/pkg/biz/attestationstate.goapp/controlplane/internal/service/attestationstate.gopkg/attestation/crafter/statemanager/remote/remote.gopkg/attestation/crafter/crafter.go
Reproduction
go run app/cli/main.go --insecure att init \ --workflow foo --project bar \ --collectors aiconfig --replace -y --debug --remote-stateError:
the provided base digest doesn't match the stored state, it might have changed, got="...", want="..."
Problem
When the attestation crafter performs multiple state saves during initialization (e.g., running collectors that each update state), the second save fails with an OCC (Optimistic Concurrency Control) conflict.
The root cause is that
AttestationStateServiceSaveResponsedid not return the digest of the newly saved state. After a successful save, the client'sUpdateCheckSumremained stale, so the next write sent an outdatedbase_digestthat no longer matched the server-side state.