fix: return digest in attestation state save response to prevent OCC conflicts#2910
Closed
sonarly[bot] wants to merge 1 commit into
Closed
fix: return digest in attestation state save response to prevent OCC conflicts#2910sonarly[bot] wants to merge 1 commit into
sonarly[bot] wants to merge 1 commit into
Conversation
…conflicts https://sonarly.com/issue/17198?type=bug The `AttestationStateServiceSaveResponse` proto message is empty and does not return the digest of the newly saved state. After a successful save, the client's `UpdateCheckSum` remains stale, causing subsequent writes to send an outdated `base_digest` that triggers an OCC conflict error. Fix: The fix addresses the OCC conflict bug across all layers of the stack: **1. Proto definition** (`attestation_state.proto`): Added `string digest = 1` field to `AttestationStateServiceSaveResponse`, which was previously empty. This is symmetric with the existing `digest` field in `AttestationStateServiceReadResponse`. **2. Data layer** (`data/attestationstate.go`): Changed `Save` return type from `error` to `(string, error)`. The digest of the newly saved state is computed before the transaction and returned on success. **3. Business layer** (`biz/attestationstate.go`): Updated `AttestationStateRepo` interface and `AttestationStateUseCase.Save` to propagate the digest return value. **4. Service layer** (`service/attestationstate.go`): Captures the returned digest and sets it in the `AttestationStateServiceSaveResponse`. **5. Remote state manager** (`remote/remote.go`): The critical client-side fix — after a successful Save, updates `state.UpdateCheckSum` with the digest from the response. This ensures subsequent writes send the correct `base_digest` for OCC validation. **6. Integration tests**: Updated all `Save` call sites to handle the new return value. Added a new test case `"save returns digest that can be used for consecutive writes"` that validates three consecutive saves using returned digests succeed, and the final digest matches what `Read` returns. **NOTE**: After merging these changes, `make api` must be run to regenerate the protobuf files (`.pb.go`, `.ts`, etc.) before the code will compile. This is the standard team workflow per CLAUDE.md.
There was a problem hiding this comment.
1 issue found across 6 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="pkg/attestation/crafter/statemanager/remote/remote.go">
<violation number="1" location="pkg/attestation/crafter/statemanager/remote/remote.go:77">
P1: Avoid overwriting `UpdateCheckSum` with an empty digest; it can disable OCC on the next write by sending an empty `base_digest`.
(Based on your team's feedback about cross-component and version compatibility.) [FEEDBACK_USED]</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
|
|
||
| // Update the checksum with the digest of the newly saved state | ||
| // so subsequent writes use the correct base digest for OCC | ||
| state.UpdateCheckSum = resp.GetDigest() |
There was a problem hiding this comment.
P1: Avoid overwriting UpdateCheckSum with an empty digest; it can disable OCC on the next write by sending an empty base_digest.
(Based on your team's feedback about cross-component and version compatibility.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/attestation/crafter/statemanager/remote/remote.go, line 77:
<comment>Avoid overwriting `UpdateCheckSum` with an empty digest; it can disable OCC on the next write by sending an empty `base_digest`.
(Based on your team's feedback about cross-component and version compatibility.) </comment>
<file context>
@@ -67,10 +67,15 @@ func (r *Remote) Write(ctx context.Context, key string, state *crafter.Versioned
+ // Update the checksum with the digest of the newly saved state
+ // so subsequent writes use the correct base digest for OCC
+ state.UpdateCheckSum = resp.GetDigest()
+
return nil
</file context>
Suggested change
| state.UpdateCheckSum = resp.GetDigest() | |
| digest := resp.GetDigest() | |
| if digest == "" { | |
| return fmt.Errorf("failed to save state: empty digest in response") | |
| } | |
| state.UpdateCheckSum = digest |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Automated fix for bug 17198
Severity:
highSummary
The
AttestationStateServiceSaveResponseproto message is empty and does not return the digest of the newly saved state. After a successful save, the client'sUpdateCheckSumremains stale, causing subsequent writes to send an outdatedbase_digestthat triggers an OCC conflict error.User Impact
Any CI/CD pipeline using remote attestation state with multiple collectors (PR metadata, AI agent config) enabled will fail during attestation initialization. The second collector's state save will always fail with a 409 conflict, preventing attestation completion.
Root Cause
The
AttestationStateServiceSaveResponseproto message atapp/controlplane/api/controlplane/v1/attestation_state.proto:56is defined as empty:When the remote state manager calls
Save, it discards the response and never updates the client-sideUpdateCheckSum:Meanwhile, the server-side data layer at
app/controlplane/pkg/data/attestationstate.go:56-88computes a SHA-256 digest of the stored state on each save and validates it against thebase_digeston subsequent saves. The digest changes every time the state is written because the encrypted payload includes random salt/IV.Failure sequence during
RunCollectors(crafter.go:144-155):LoadCraftingState→Read()returns state + digest D1 →UpdateCheckSum = D1AddMaterial*→Write()sendsbase_digest=D1→ server accepts, new state gets digest D2 → response is empty,UpdateCheckSumstays D1AddMaterial*→Write()sendsbase_digest=D1→ server sees current digest is D2 →base_digest != storedDigest→ returnsErrAttestationStateConflictThis bug was introduced in commit
dc1eb2efwhich added the OCC mechanism. The OCC design correctly added adigestfield to the Read response but omitted it from the Save response, creating an asymmetry where the client cannot track state versions across consecutive writes without performing an explicit Read between each Write.Triggering cause: This bug manifests whenever multiple collectors are registered (e.g., PR metadata collector + AI agent config collector) that each add materials during initialization. The collectors feature has been expanded over time (PR metadata in earlier releases, AI agent config more recently in commits like
9c664501), making the multi-write scenario increasingly common and the bug more visible.The fix requires three changes:
string digest = 1field toAttestationStateServiceSaveResponseremote.Write()to capture the response and setstate.UpdateCheckSumto the returned digestIntroduced by: migmartri on 2024-07-25 in commit
dc1eb2eSuggested Fix
The fix addresses the OCC conflict bug across all layers of the stack:
1. Proto definition (
attestation_state.proto): Addedstring digest = 1field toAttestationStateServiceSaveResponse, which was previously empty. This is symmetric with the existingdigestfield inAttestationStateServiceReadResponse.2. Data layer (
data/attestationstate.go): ChangedSavereturn type fromerrorto(string, error). The digest of the newly saved state is computed before the transaction and returned on success.3. Business layer (
biz/attestationstate.go): UpdatedAttestationStateRepointerface andAttestationStateUseCase.Saveto propagate the digest return value.4. Service layer (
service/attestationstate.go): Captures the returned digest and sets it in theAttestationStateServiceSaveResponse.5. Remote state manager (
remote/remote.go): The critical client-side fix — after a successful Save, updatesstate.UpdateCheckSumwith the digest from the response. This ensures subsequent writes send the correctbase_digestfor OCC validation.6. Integration tests: Updated all
Savecall sites to handle the new return value. Added a new test case"save returns digest that can be used for consecutive writes"that validates three consecutive saves using returned digests succeed, and the final digest matches whatReadreturns.NOTE: After merging these changes,
make apimust be run to regenerate the protobuf files (.pb.go,.ts, etc.) before the code will compile. This is the standard team workflow per CLAUDE.md.Generated by Sonarly