Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,22 @@ Renaming ./alphabet to betabet
* Files and directories are renamed.
* Searches are performed recursively from the current working directory.
* Searches are case sensitive.
* `.git/` directories are skipped.
* Binary files are ignored.
* `.git` is always skipped (whether it's a directory or a worktree-linkage file).
* Binary files (those with a NUL byte or invalid UTF-8 in the first 1 KiB) are ignored.
* Symbolic links are not followed and not rewritten.
* Files with the setuid or setgid bit set are not rewritten.
* Original mode, owner/group, and modification time are preserved across rewrites.
* Exits non-zero if any file failed to be rewritten or renamed (the rest of the tree is still processed on a best-effort basis).

## Security model

`find-replace` is designed to be safe to run inside an untrusted directory:

* It will never traverse out of the working tree through a symbolic link.
* Temp files used during rewrites are created with `O_EXCL` under a `crypto/rand`-generated name, so a co-resident attacker cannot pre-create the temp-file path to redirect the write.
* Renames refuse to overwrite an existing destination (implemented as a hardlink + remove pair, so the existence check and the directory-entry creation are a single step).

When run as root, `find-replace` preserves the original uid/gid of every rewritten file. Files with `setuid`/`setgid` bits are skipped to avoid producing a setuid binary owned by the wrong user.

## Goal

Expand Down
6 changes: 3 additions & 3 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set -e

# Vet
go vet
go vet ./...

# Build
GIT_COMMIT="$(git rev-parse --short $(git rev-list -1 HEAD))"
Expand All @@ -27,5 +27,5 @@ go build \
-X 'main.BuildTainted=$BUILD_TAINTED'" \
./...

# Test
go test -cover -v ./...
# Test (with race detector to catch concurrency bugs).
go test -race -cover -v ./...
32 changes: 32 additions & 0 deletions chown_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//go:build unix

package main

import (
"errors"
"io/fs"
"os"
"syscall"
)

// chownToOriginal applies the original uid/gid from info to path. Best-effort:
// permission errors (typical for non-root users) are silently ignored. Other
// errors are returned.
func chownToOriginal(path string, info os.FileInfo) error {
st, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return nil
}
err := os.Chown(path, int(st.Uid), int(st.Gid))
if err == nil {
return nil
}
// Non-root processes lack CAP_CHOWN and will get EPERM here. That's
// expected: the temp file already has the correct uid (the running
// user), and the running user is the owner of the original file too
// (otherwise they couldn't have rewritten it). Don't fail.
if errors.Is(err, fs.ErrPermission) {
return nil
}
return err
}
10 changes: 10 additions & 0 deletions chown_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//go:build windows

package main

import "os"

func chownToOriginal(path string, info os.FileInfo) error {
// Windows does not have a meaningful equivalent.
return nil
}
Loading
Loading