Summary
snapshotSave() with --force performs a three-step sequence that is not atomic:
// src/features/snapshot.ts:40-53
if (fs.existsSync(dest)) {
if (!options.force) { throw ... }
fs.unlinkSync(dest);
debug(`Deleted existing snapshot: ${dest}`);
}
fs.mkdirSync(dir, { recursive: true });
const db = new Database(dbPath, { readonly: true });
try {
db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
} finally {
db.close();
}
Between existsSync → unlinkSync → VACUUM INTO, another process can:
- Create a snapshot under the same name (lost to the unlink).
- Observe a missing snapshot file mid-operation.
- Start its own
VACUUM INTO to the same path, producing a corrupted destination.
Reproduction
Two shells running against the same .codegraph/:
# shell A
codegraph snapshot save checkpoint --force &
# shell B (race window)
codegraph snapshot save checkpoint --force &
Either output can end up truncated or interleaved, depending on scheduling.
Suggested fix
- Write to a temp file (
<name>.db.tmp-<pid>) and fs.renameSync over the destination. rename is atomic on same-filesystem POSIX and on Windows when the destination exists (with MOVEFILE_REPLACE_EXISTING semantics that fs.renameSync uses).
- Alternatively, acquire a real file lock (e.g.
proper-lockfile) around the whole save operation, not just the DB handle.
Scope
Also affects snapshotDelete and snapshotRestore (line 67-88) which run unlink then copyFileSync — same family of race. Tackle together.
File refs
src/features/snapshot.ts:27-61 — save
src/features/snapshot.ts:67-88 — restore
src/features/snapshot.ts:119-131 — delete
Summary
snapshotSave()with--forceperforms a three-step sequence that is not atomic:Between
existsSync→unlinkSync→VACUUM INTO, another process can:VACUUM INTOto the same path, producing a corrupted destination.Reproduction
Two shells running against the same
.codegraph/:Either output can end up truncated or interleaved, depending on scheduling.
Suggested fix
<name>.db.tmp-<pid>) andfs.renameSyncover the destination.renameis atomic on same-filesystem POSIX and on Windows when the destination exists (withMOVEFILE_REPLACE_EXISTINGsemantics thatfs.renameSyncuses).proper-lockfile) around the whole save operation, not just the DB handle.Scope
Also affects
snapshotDeleteandsnapshotRestore(line 67-88) which rununlinkthencopyFileSync— same family of race. Tackle together.File refs
src/features/snapshot.ts:27-61— savesrc/features/snapshot.ts:67-88— restoresrc/features/snapshot.ts:119-131— delete