Address #112 re-review: fail rewind on undecodable checkpoint payload

[major] restoreToSequenceLocked silently continued past a checkpoint whose payload failed to decode, which let a NEWER checkpoint win for that path and restore a later state than the caller requested. Treat corruption as a hard rewind error. Regression test TestRestoreFailsOnCorruptCheckpointPayload (fails-open before, errors now).

Re-review note: the earlier capture-target-validation (critical) and test bounds (minor) were already addressed in 600337b (resolveWithinWorkspace guard + len checks); those re-anchored threads are stale. The restore-side symlink-swap TOCTOU remains a documented heavy-lift residual (needs openat2/descriptor traversal), tracked for the rewind-wiring work.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
KRATOSandClaude Opus 4.8 committed 2026-06-07 12:18:40 +05:30
1 parent 600337bf70
commit 24e9ba2299
2 files changed
+15 -1

No files matched your search

+11
View File
@@ -256,6 +256,17 @@ func TestRestoreRejectsPathTraversal(t *testing.T) {
}
}
func TestRestoreFailsOnCorruptCheckpointPayload(t *testing.T) {
store, ws := newCkStore(t)
target := mustAppend(t, store, AppendEventInput{Type: EventMessage, Payload: map[string]any{}})
// A checkpoint event whose payload does not decode into CheckpointPayload must
// abort the rewind, not be silently skipped (which could restore a later state).
mustAppend(t, store, AppendEventInput{Type: EventSessionCheckpoint, Payload: "corrupt-not-a-checkpoint"})
if _, err := store.RestoreToSequence("s", ws, target.Sequence); err == nil {
t.Fatal("expected rewind to fail on undecodable checkpoint payload")
}
}
func TestTruncateToZeroProducesEmptyFile(t *testing.T) {
store, _ := newCkStore(t)
mustAppend(t, store, AppendEventInput{Type: EventMessage, Payload: map[string]any{}})
+4 -1
View File
@@ -56,7 +56,10 @@ func (store *Store) restoreToSequenceLocked(sessionID, workspaceRoot string, tar
ev := checkpoints[i]
var payload CheckpointPayload
if err := json.Unmarshal(ev.Payload, &payload); err != nil {
continue
// Fail the rewind rather than skipping: silently continuing would let a
// NEWER checkpoint win for that path and restore a later state than the
// caller asked for. Corruption is a hard error.
return report, fmt.Errorf("decode checkpoint payload seq %d: %w", ev.Sequence, err)
}
for _, f := range payload.Files {
// Process only the CLOSEST-to-target entry per path. We iterate