Skip to content

TOCTOU race in RenameFile silently overwrites concurrently-created destination #4

Description

@dolph

Summary

RenameFile checks whether the destination exists with os.Stat, and if not, calls os.Rename:

if _, err := os.Stat(newPath); errors.Is(err, os.ErrNotExist) {
    if err := os.Rename(f.Path, newPath); err != nil { ... }
}

This is a TOCTOU window: between the Stat and the Rename, another process (or another goroutine inside find-replace itself, since the walker is concurrent) can create newPath. os.Rename on Linux silently overwrites the destination — the safety check is bypassed and data is destroyed.

Impact (Security: Medium)

  • Concurrent traversal of two siblings whose names collapse to the same newPath after replacement (e.g., Foo and foo with find=Foo replace=foo) will race; one will overwrite the other.
  • External process writing in the same directory can race the check and have its file silently clobbered.
  • Loss of safety guarantee documented in the source comment ("renames a file if the destination file name does not already exist").

Suggested Fix

Use os.Link(src, dst) followed by os.Remove(src). os.Link fails atomically with EEXIST if dst exists, eliminating the TOCTOU. Fall back to Rename only on EXDEV/EPERM (e.g., filesystems without hardlink support), and document the weaker guarantee.

Alternatively, on Linux, use unix.Renameat2(..., RENAME_NOREPLACE) directly.

Files

  • find_replace.go:98-112 (RenameFile)

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions