Skip to content

fix(engine): refuse restore_to_epoch onto an existing path - #363

Open
teipsum wants to merge 1 commit into
GrafeoDB:mainfrom
teipsum:u3a-restore-fresh-path-guard
Open

fix(engine): refuse restore_to_epoch onto an existing path#363
teipsum wants to merge 1 commit into
GrafeoDB:mainfrom
teipsum:u3a-restore-fresh-path-guard

Conversation

@teipsum

@teipsum teipsum commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Problem

do_restore_to_epoch copied the full backup to output_path with an unconditional std::fs::copy, and further down deleted any existing <output_path>.wal sidecar directory. Pointing a restore at a live database therefore clobbered the .grafeo file and silently destroyed its sidecar WAL — the very data an operator may be trying to recover.

Fix

Add a refuse-if-exists guard at the top of do_restore_to_epoch that returns Error::InvalidValue when output_path or its sidecar already exists, before any filesystem mutation. Restores must target a fresh path.

Tests

Two new unit tests assert the refusal and that the pre-existing artifacts are left untouched. Full grafeo-engine backup suite is green:

cargo fmt --all -- --check                     -> clean
cargo clippy -p grafeo-engine --all-features   -> clean (no new warnings)
cargo test  -p grafeo-engine --all-features -- backup
  -> 12 passed; 0 failed  (10 pre-existing + 2 new)

Single commit, based on main @ 4ebae02.


Summary by cubic

Prevents accidental data loss by refusing to restore onto an existing database path or its .wal sidecar in grafeo-engine. Restores must now target a fresh path.

  • Bug Fixes

    • Add early guard in do_restore_to_epoch that returns Error::InvalidValue if output_path or <output_path>.wal exists.
    • Add two unit tests to confirm refusal and that existing files/dirs are untouched.
  • Migration

    • Restore to a new, empty path.
    • To replace a database, stop the service and move or remove the existing .grafeo file and <path>.wal/ directory, then run restore.

Written for commit 69ef608. Summary will update on new commits.

Review in cubic

do_restore_to_epoch copied the full backup to output_path with an
unconditional std::fs::copy and further down deleted any existing
<output_path>.wal sidecar directory. Pointing a restore at a live
database therefore clobbered the .grafeo file and silently destroyed
its sidecar WAL -- the very data an operator may be trying to recover.

Add a refuse-if-exists guard at the top of do_restore_to_epoch that
returns Error::InvalidValue when output_path or its sidecar already
exists, before any filesystem mutation. Restores must target a fresh
path. Covered by two unit tests asserting the refusal and that the
pre-existing artifacts are left untouched.
@teipsum
teipsum requested a review from StevenBtw as a code owner June 11, 2026 00:35

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 1 file

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="crates/grafeo-engine/src/database/backup.rs">

<violation number="1" location="crates/grafeo-engine/src/database/backup.rs:517">
P2: Sidecar path construction uses `display()` string formatting instead of path-native `OsString`/`PathBuf` operations, which can miss existing sidecar directories for non-UTF8 paths and bypass the data-loss guard.</violation>

<violation number="2" location="crates/grafeo-engine/src/database/backup.rs:518">
P2: Refuse-if-exists guard is non-atomic (TOCTOU): a concurrent process can create the output file or sidecar after the `exists()` checks but before `std::fs::copy` or `remove_dir_all`+`rename`, allowing silent clobbering.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

// be trying to recover. Restores must target a fresh path; callers that
// intend to replace a database should move or remove it first.
let sidecar_dir = format!("{}.wal", output_path.display());
if output_path.exists() || Path::new(&sidecar_dir).exists() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Refuse-if-exists guard is non-atomic (TOCTOU): a concurrent process can create the output file or sidecar after the exists() checks but before std::fs::copy or remove_dir_all+rename, allowing silent clobbering.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/grafeo-engine/src/database/backup.rs, line 518:

<comment>Refuse-if-exists guard is non-atomic (TOCTOU): a concurrent process can create the output file or sidecar after the `exists()` checks but before `std::fs::copy` or `remove_dir_all`+`rename`, allowing silent clobbering.</comment>

<file context>
@@ -507,6 +507,22 @@ pub(super) fn do_restore_to_epoch(
+    // be trying to recover. Restores must target a fresh path; callers that
+    // intend to replace a database should move or remove it first.
+    let sidecar_dir = format!("{}.wal", output_path.display());
+    if output_path.exists() || Path::new(&sidecar_dir).exists() {
+        return Err(Error::InvalidValue(format!(
+            "restore output path already exists: {} (restore must target a fresh path; \
</file context>

// and silently destroy its sidecar WAL -- exactly the data an operator may
// be trying to recover. Restores must target a fresh path; callers that
// intend to replace a database should move or remove it first.
let sidecar_dir = format!("{}.wal", output_path.display());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Sidecar path construction uses display() string formatting instead of path-native OsString/PathBuf operations, which can miss existing sidecar directories for non-UTF8 paths and bypass the data-loss guard.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/grafeo-engine/src/database/backup.rs, line 517:

<comment>Sidecar path construction uses `display()` string formatting instead of path-native `OsString`/`PathBuf` operations, which can miss existing sidecar directories for non-UTF8 paths and bypass the data-loss guard.</comment>

<file context>
@@ -507,6 +507,22 @@ pub(super) fn do_restore_to_epoch(
+    // and silently destroy its sidecar WAL -- exactly the data an operator may
+    // be trying to recover. Restores must target a fresh path; callers that
+    // intend to replace a database should move or remove it first.
+    let sidecar_dir = format!("{}.wal", output_path.display());
+    if output_path.exists() || Path::new(&sidecar_dir).exists() {
+        return Err(Error::InvalidValue(format!(
</file context>

@codspeed-hq

codspeed-hq Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 70 untouched benchmarks


Comparing teipsum:u3a-restore-fresh-path-guard (69ef608) with main (4ebae02)

Open in CodSpeed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant