Skip to content

Commit 7a8e2f3

Browse files
committed
backup: PR810 r5 — coderabbit Major (Close error on WriteChecksums)
Coderabbit r5 Major (checksums.go:82): WriteChecksums returned out.Sync() but discarded the deferred out.Close() error. Same shape as the r4 gemini medium on emitManifest, applied here too. On NFS / some FUSE backends Close is where buffered-writeback failures surface, so a swallowed Close means the CLI can declare success on a truncated or unreadable CHECKSUMS file. Fix: explicit Sync() then Close() with both errors propagated, with the pre-existing defer'd Close swallowed only on the error-return paths (the primary failure has already been surfaced). Extracted writeAllChecksumLines so the parent stays a thin open / write / sync / close skeleton with the durability contract spelled out. Caller audit (CLAUDE.md "semantic-change → grep all callers"): - WriteChecksums has one caller (cmd/elastickv-snapshot-decode/main.go:253), which already surfaces the returned error verbatim via errors.Wrap. The semantic tightening (Close errors now surface) is honored by the existing wrapping; no caller change needed. Also addresses coderabbit r5 nitpick: typed-sentinel assertion in TestVerifyChecksums_DetectsTampering (was checking err != nil generically; now pins ErrChecksumMismatch so a future change that turns the tamper signal into a different error class surfaces as a test failure). Self-review: 1. Data loss — fix prevents silent CHECKSUMS-truncation on NFS / FUSE. 2. Concurrency — none. 3. Performance — no new I/O; just surfaces an existing error. 4. Data consistency — durability contract now honest. 5. Test coverage — typed-sentinel assertion added; existing round-trip tests still pass.
1 parent ec358ab commit 7a8e2f3

2 files changed

Lines changed: 39 additions & 5 deletions

File tree

internal/backup/checksums.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,40 @@ func WriteChecksums(root string) error {
7272
if err != nil {
7373
return errors.WithStack(err)
7474
}
75-
defer func() { _ = out.Close() }()
75+
if err := writeAllChecksumLines(out, entries); err != nil {
76+
_ = out.Close()
77+
return err
78+
}
79+
if err := out.Sync(); err != nil {
80+
_ = out.Close()
81+
return errors.WithStack(err)
82+
}
83+
// Surface Close errors instead of swallowing them. On NFS /
84+
// some FUSE backends Close is the authoritative durability
85+
// signal — writeback failures buffered after the Sync()
86+
// fsync surface here, not at Write time. A swallowed Close
87+
// would let the CLI declare success with a truncated or
88+
// unreadable CHECKSUMS file. coderabbit Major on PR #810
89+
// round 4 (same shape as the gemini r1 medium on
90+
// emitManifest, applied here too).
91+
if err := out.Close(); err != nil {
92+
return errors.WithStack(err)
93+
}
94+
return nil
95+
}
96+
97+
// writeAllChecksumLines emits the formatted CHECKSUMS rows. Split
98+
// out from WriteChecksums so the parent stays a thin
99+
// open-write-sync-close skeleton — its job is the durability
100+
// contract; the per-line escaping is delegated.
101+
func writeAllChecksumLines(w io.Writer, entries []checksumEntry) error {
76102
for _, e := range entries {
77103
line, prefix := formatChecksumLine(e.hex, e.relPath)
78-
if _, err := fmt.Fprintf(out, "%s%s\n", prefix, line); err != nil {
104+
if _, err := fmt.Fprintf(w, "%s%s\n", prefix, line); err != nil {
79105
return errors.WithStack(err)
80106
}
81107
}
82-
return errors.WithStack(out.Sync())
108+
return nil
83109
}
84110

85111
// formatChecksumLine builds one CHECKSUMS row using GNU coreutils

internal/backup/checksums_test.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,11 @@ func TestVerifyChecksums_HappyPath(t *testing.T) {
103103

104104
// TestVerifyChecksums_DetectsTampering pins the failure mode: a
105105
// post-checksum write to one of the listed files must surface as
106-
// a verification error.
106+
// ErrChecksumMismatch (not just "some error"). Pinning the
107+
// typed sentinel keeps the contract honest — a future change
108+
// that turns this into ErrChecksumsMalformedLine or
109+
// ErrChecksumsPathTraversal would otherwise pass this test
110+
// silently. CodeRabbit r5 nitpick on PR #810.
107111
func TestVerifyChecksums_DetectsTampering(t *testing.T) {
108112
t.Parallel()
109113
root := t.TempDir()
@@ -112,9 +116,13 @@ func TestVerifyChecksums_DetectsTampering(t *testing.T) {
112116
t.Fatalf("WriteChecksums: %v", err)
113117
}
114118
mustWrite(t, filepath.Join(root, "a.txt"), []byte("tampered"))
115-
if err := VerifyChecksums(root); err == nil {
119+
err := VerifyChecksums(root)
120+
if err == nil {
116121
t.Fatalf("expected VerifyChecksums to detect tampering, got nil")
117122
}
123+
if !errors.Is(err, ErrChecksumMismatch) {
124+
t.Fatalf("err = %v, want ErrChecksumMismatch", err)
125+
}
118126
}
119127

120128
// TestVerifyChecksums_RejectsTraversalPath is the regression for

0 commit comments

Comments
 (0)