Skip to content

bug(snapshot): TOCTOU race on --force save path #995

@carlos-alm

Description

@carlos-alm

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 existsSyncunlinkSyncVACUUM 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions