Skip to content

fix(): Fix Cross-Device Bug — Use Instead of Raw - #4

Merged
edaywalid merged 1 commit into
edaywalid:mainfrom
Arielpetit:fix/cross_device_fix
Jul 29, 2026
Merged

fix(): Fix Cross-Device Bug — Use Instead of Raw#4
edaywalid merged 1 commit into
edaywalid:mainfrom
Arielpetit:fix/cross_device_fix

Conversation

@Arielpetit

Copy link
Copy Markdown
Contributor

PR: Fix OpExchange Cross-Device Bug — Use swapAny Instead of Raw os.Rename

Issue

The OpExchange handler used three raw os.Rename calls to swap files back during undo operations.

os.Rename ultimately calls rename(2), which fails with EXDEV when the session store and working files are located on different filesystems. As a result, undo operations could silently fail, leaving files in an inconsistent state.

Fix

Replaced the three manual os.Rename calls with:

swapAny(field(0), field(1))

This is the same helper already used by OpMod.

swapAny:

  • Attempts os.Rename first for same-filesystem moves.
  • Falls back to a copy-and-delete implementation when EXDEV occurs.
  • Provides atomic behavior using a temporary file with rollback on failure.

Before

The undo path performed three sequential renames:

A ──os.Rename──▶ tmp      ❌ EXDEV if A and tmp are on different devices
B ──os.Rename──▶ A        never reached
tmp ──os.Rename──▶ B      never reached

Problems:

  • Three system calls
  • Three potential failure points
  • No rollback if the second rename fails
  • Could silently leave files corrupted on cross-device setups

After

The undo path now performs a single swap operation:

A ──swapAny──▶ B          ✅ rename first, copy+delete fallback

Benefits:

  • Single, well-tested helper used elsewhere in the codebase
  • Handles cross-device moves transparently
  • Atomic via temporary file
  • Rolls back cleanly on failure

Code Diff

Before

tmp := field(0) + ".undo-xchg"
if err = os.Rename(field(0), tmp); err == nil {
    if err = os.Rename(field(1), field(0)); err == nil {
        err = os.Rename(tmp, field(1))
    }
}

After

err = swapAny(field(0), field(1))

Files Changed

File Changes
internal/restore/restore.go Replaced three os.Rename calls with swapAny(field(0), field(1)).
internal/restore/restore_test.go Added TestExchange to verify cross-device-safe exchange behavior.

Verification

  • ✅ 4/4 unit test packages passing
  • ✅ 20/20 end-to-end tests passing
  • ✅ Project builds successfully
  • ✅ Cross-device behavior verified with:
UNDO_DATA_DIR=/dev/shm/undo-test go test ./internal/restore/ -run TestExchange

@Arielpetit

Copy link
Copy Markdown
Contributor Author

@edaywalid can you please review this?

@edaywalid
edaywalid merged commit ec4de57 into edaywalid:main Jul 29, 2026
5 checks passed
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.

2 participants