Audit & rewrite: security, reliability, performance hardening#24
Open
Audit & rewrite: security, reliability, performance hardening#24
Conversation
Fix a broad set of issues uncovered in audit (#2-#16): Security: - Skip symlinks during traversal (no longer rewrite/rename through links) - Use os.CreateTemp (O_EXCL) for tempfiles; remove math/rand-based naming - Defer tempfile cleanup so failures don't leak content on disk - Replace stat+rename with hardlink+remove for race-free no-overwrite rename Reliability: - Replace log.Fatal in worker paths with error accumulation; walk continues on error and main exits non-zero if any error was reported - Reject empty FIND and identical FIND/REPLACE arguments - Stream rewrites through a bounded buffer (no whole-file in memory) - Skip files with NUL bytes or invalid UTF-8 in the sniff prefix; drop the golang.org/x/tools dependency Performance: - Single-threaded depth-first walk (matches v1.1.2's documented design and bounds FD/memory; the previous unbounded-goroutine fan-out caused EMFILE on large trees) - Use fs.DirEntry from ReadDir directly (no extra os.Stat per child) - Strip the Contains pre-check (Replace already short-circuits on no match) - Skip filepath.Abs for already-absolute child paths Tests: - Add security_test.go: symlink traversal, tempfile attack, rename overwrite - Add reliability_test.go: error continuation, exit code, large-file streaming, CLI arg validation - Add file_handling_test.go: streaming rewriter (incl. boundary-spanning matches), binary detection, mode preservation, no leaked tempfiles - Run the suite under -race in build.sh - Replace the network-dependent BenchmarkNova with a synthetic-tree benchmark
Address issues found in second-pass review: #17 Preserve owner/group across rewrite (chown to original uid/gid before rename). Best-effort under non-root. #18 Refuse to rewrite files with setuid/setgid bits — those bits cannot be safely preserved through a tempfile-and-rename when uid changes. #19 Skip `.git` whether it is a directory (normal repo) or a file (worktree/submodule linkage). #20 Update README to document the security/reliability model: symlink skip, O_EXCL tempfile, hardlink-based no-overwrite rename, owner/mtime preservation, exit-code semantics. #21 Skip `.find-replace-*` files in the walker so orphans from a crashed prior run aren't picked up as targets. #22 Pre-compute []byte forms of find/replace once per Walk instead of per file. #23 Preserve mtime on rewritten files (os.Chtimes after the streaming write). Tests: - Add TestRewritePreservesMtime, TestRewritePreservesOwner, TestRefusesSetuidFile, TestGitAsFileNotRewritten, TestStaleTempFilesSkipped. - Helper: chownToOriginal split into Unix and Windows files.
os.CopyFS landed in Go 1.23 but the module declares go 1.19 and CI uses 1.19 too. Replace the call with a small filepath.Walk-based copyTree that has no version requirement beyond the rest of the codebase.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two-pass audit of the codebase: opened 22 issues across security, reliability, performance, and documentation; addressed all of them. Rewrote the walker and the file-rewrite pipeline for safety, bounded resource use, and metadata preservation.
What was wrong with
mainos.Statfollows them), letting a malicious or accidental symlink redirect rewrites to anywhere on disk the running user could write — including/etcif root.math/rand+ plainO_CREATE) enabled a co-resident attacker to pre-create the path as a symlink and have the rewrite clobber the link target.os.Stat+os.Rename) silently overwrote concurrently-created destinations.EMFILEon large trees and thrashed the disk.Readto string,Replaceallocates another copy. Multi-GB files OOM the host.log.Fatalfrom worker paths — single error abandoned the run withos.Exit, skipping deferred cleanup; tempfiles leaked.-racein tests, no validation of emptyfind, redundantos.Statper child, network-bound benchmark that didn't even work.Plus several second-pass issues (preserved on a maintainer re-review): owner not preserved, mtime jumped forward, setuid bits silently stripped,
.gitworktree files clobbered, orphaned tempfiles picked up on re-run.What this PR does
Security
fs.DirEntry.Type(), neveros.Stat).os.CreateTempfor tempfiles (O_EXCL+ crypto-random name) withdefer-ed cleanup.Reliability
mainexits non-zero when anything failed.findand identicalfind/replacearguments..gitwhether it's a directory or a worktree-linkage file..find-replace-*tempfiles from a crashed prior run.-race.Performance
fs.DirEntrydirectly — no extraos.Statper child.strings.Containspre-check (Replace short-circuits internally).filepath.Absfor already-absolute child paths.[]byteform of find/replace once per Walk.Metadata preservation
Dependencies
golang.org/x/tools(replaced its 6-lineIsTextheuristic with an in-tree NUL/UTF-8 check).Tests
-race. New coverage:security_test.go: symlink traversal, tempfile symlink attack, rename overwrite refusal, mtime/owner preservation, setuid refusal.reliability_test.go: error continuation, exit code, large-file streaming, CLI arg rejection,.git-as-file, stale tempfiles.file_handling_test.go: streaming rewriter (incl. cross-buffer matches), binary detection, mode preservation, no leaked tempfiles,renameNoReplacesemantics.BenchmarkNova(cloned over SSH inside a removed temp dir) with a reproducible synthetic-tree benchmark.Issues closed
P0 (critical security): #2 #3
P1 (high): #4 #6 #7 #8
P2 (medium): #5 #9 #10 #11 #12 #13 #17 #20
P3 (low): #14 #15 #16 #18 #19 #21 #22 #23
Test plan
go test -race -cover ./...— all 34 tests pass (1 skipped under root: permission-bit test)go vet ./...go test -bench BenchmarkSyntheticTreeruns without network and reports stable numbersTestSymlinkNotFollowed)TestBigFileStreamsThroughBoundedMemoryat 5 MB scale)https://claude.ai/code/session_013qhiGeDHdyL2hSEaZJPSdf
Generated by Claude Code