-
Notifications
You must be signed in to change notification settings - Fork 0
fix: prevent sync from overwriting existing files (#027) #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -97,6 +97,23 @@ func (g *GitVolume) sync(vol Volume, srcInfo os.FileInfo, opts SyncOptions) erro | |
| return nil | ||
| } | ||
|
|
||
| // Check if target exists and is safe to overwrite | ||
| if _, err := os.Lstat(vol.TargetPath); err == nil { | ||
| // Target exists, check if it's a symlink (safe to remove) | ||
| // We allow overwriting symlinks because they are likely created by us (or user wants to replace them) | ||
| // But we DO NOT overwrite regular files or directories to prevent data loss. | ||
| targetInfo, err := os.Lstat(vol.TargetPath) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to check target %s: %w", vol.Target, err) | ||
| } | ||
|
|
||
| if targetInfo.Mode()&os.ModeSymlink == 0 { | ||
| // It is NOT a symlink (regular file or directory) | ||
| // For safety, we skip this volume and report an error. | ||
| return fmt.Errorf("target %s already exists and is not a symlink (skipping to prevent data loss)", vol.Target) | ||
| } | ||
| } | ||
|
Comment on lines
+101
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This section introduces a critical Time-of-Check to Time-of-Use (TOCTOU) race condition. The |
||
|
|
||
| // Delete-and-Recreate strategy (Idempotency): | ||
| // ensuring the target state exactly matches the source state. | ||
| // We always remove the target path before syncing to guarantees a clean slate. | ||
|
|
@@ -163,7 +180,7 @@ func (g *GitVolume) syncCopy(src, dst string, srcInfo os.FileInfo) error { | |
|
|
||
| // syncLink handles link mode synchronization | ||
| func (g *GitVolume) syncLink(src, dst string, relativeLink bool) error { | ||
| // Ensure parent directory exists | ||
| // Ensure parent directory exists | ||
| if err := os.MkdirAll(filepath.Dir(dst), DefaultDirPerm); err != nil { | ||
| return fmt.Errorf("failed to create parent directory: %w", err) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regression: Broken Idempotency for Directory Sync
The check
targetInfo.Mode()&os.ModeSymlink == 0prevents overwriting any existing directory that is not a symlink. However, whengit-volumeis used inModeCopyfor a directory, it creates a real directory at the target path.On subsequent runs of the
synccommand, this check will identify the previously created directory as "not a symlink" and fail with an error. This breaks the idempotency of thesyncoperation, which is a core design principle of the tool (as noted in the comments on line 117). This can lead to partial sync states where some volumes are updated while others are skipped due to this error, potentially leaving the workspace in an inconsistent or insecure configuration.Severity: Medium
Vulnerability Type: Logic Error