From f0a2f5ef145e9ea30596d52c7b7a814f6babf8fb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 18 Mar 2026 20:38:52 +0900 Subject: [PATCH] Skip symlinks targeting ancestors of walker root to prevent resource exhaustion When --walker=follow is used, symlinks like Wine's z: -> / cause fzf to traverse the entire root filesystem. fastwalk's built-in loop detection only catches this on the second pass, but a single pass through / already causes severe CPU and memory exhaustion. This fix resolves each symlink-to-directory target to its absolute real path and skips it if it is an ancestor of (or equal to) the walker root. Close #4710 --- CHANGELOG.md | 1 + src/reader.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed7df1fc..37546913 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ CHANGELOG - With the reduced per-entry cost, the cache now has broader coverage. - fish: Improved command history (CTRL-R) (#4703) (@bitraid) - Bug fixes + - `--walker=follow` no longer follows symlinks whose target is an ancestor of the walker root, avoiding severe resource exhaustion when a symlink points outside the tree (e.g. Wine's `z:` → `/`) (#4710) - Fixed AWK tokenizer not treating a new line character as whitespace - Fixed `--{accept,with}-nth` removing trailing whitespaces with a non-default `--delimiter` - Fixed OSC8 hyperlinks being mangled when the URL contains unicode characters (#4707) diff --git a/src/reader.go b/src/reader.go index 76df4a5a..f05d311c 100644 --- a/src/reader.go +++ b/src/reader.go @@ -274,6 +274,24 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo ToSlash: fastwalk.DefaultToSlash(), Sort: fastwalk.SortFilesFirst, } + + // When following symlinks, precompute the absolute real paths of walker + // roots so we can skip symlinks that point to an ancestor. fastwalk's + // built-in loop detection (shouldTraverse) catches loops on the second + // pass, but a single pass through a symlink like z: -> / already + // traverses the entire root filesystem, causing severe resource + // exhaustion. Skipping ancestor symlinks prevents this entirely. + var absRoots []string + if opts.follow { + for _, root := range roots { + if real, err := filepath.EvalSymlinks(root); err == nil { + if abs, err := filepath.Abs(real); err == nil { + absRoots = append(absRoots, filepath.Clean(abs)) + } + } + } + } + ignoresBase := []string{} ignoresFull := []string{} ignoresSuffix := []string{} @@ -307,6 +325,24 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo if isDirSymlink && !opts.follow { return filepath.SkipDir } + // Skip symlinks whose target is an ancestor of (or equal to) + // any walker root. Following such symlinks would traverse a + // superset of the tree we're already walking. + if isDirSymlink && len(absRoots) > 0 { + if target, err := filepath.EvalSymlinks(path); err == nil { + if abs, err := filepath.Abs(target); err == nil { + abs = filepath.Clean(abs) + if abs == string(os.PathSeparator) { + return filepath.SkipDir + } + for _, absRoot := range absRoots { + if absRoot == abs || strings.HasPrefix(absRoot, abs+string(os.PathSeparator)) { + return filepath.SkipDir + } + } + } + } + } isDir := de.IsDir() || isDirSymlink if isDir { base := filepath.Base(path)