🐛 fix symlinks handling across various resources and connections#8874
🐛 fix symlinks handling across various resources and connections#8874slntopp wants to merge 3 commits into
Conversation
| sb.WriteString("SL=0; test -L ") | ||
| sb.WriteString(path) | ||
| sb.WriteString(" && SL=1; test -e ") | ||
| sb.WriteString(path) | ||
| sb.WriteString(" -c '%s.%f.%u.%g.%X.%Y.%C'") | ||
| sb.WriteString(` -o $SL -eq 1 || exit 1; stat -L `) | ||
| sb.WriteString(path) | ||
| sb.WriteString(` -c "$SL.%s.%f.%u.%g.%X.%Y.%C" 2>/dev/null || stat `) | ||
| sb.WriteString(path) | ||
| sb.WriteString(` -c "$SL.%s.%f.%u.%g.%X.%Y.%C"`) |
There was a problem hiding this comment.
🔴 critical — The compound command breaks under sudo. BuildSudoCommand prepends sudo to the whole string, producing sudo SL=0; test -L path && SL=1; stat .... The semicolons cause the shell to run sudo SL=0 (a no-op variable assignment under root), then test -L, stat, etc. without elevation. The original code ran two separate commands, each individually prefixed with sudo.
Fix: wrap the compound body so sudo applies to all of it:
sb.WriteString("sh -c 'SL=0; test -L ")
// ... rest of compound command ...
sb.WriteString("'")This way BuildSudoCommand produces sudo sh -c '...', executing everything elevated. Alternatively, pass sudo into the builder and prefix each sub-command.
| sb.WriteString("SL=0; test -L ") | ||
| sb.WriteString(path) | ||
| sb.WriteString(` && SL=1; stat -L -f "$SL:%z:%p:%u:%g:%a:%m" `) | ||
| sb.WriteString(path) |
There was a problem hiding this comment.
🟡 warning — The unix (BSD/macOS) path has no fallback for dangling symlinks. stat -L will fail when the target doesn't exist, but unlike the linux path (line 95-97) there is no || stat path ... fallback without -L. A dangling symlink on BSD will return os.ErrNotExist instead of reporting the symlink's own metadata.
Add the same fallback pattern used in linux():
sb.WriteString(` && SL=1; stat -L -f "$SL:%z:%p:%u:%g:%a:%m" `)
sb.WriteString(path)
sb.WriteString(` 2>/dev/null || stat -f "$SL:%z:%p:%u:%g:%a:%m" `)
sb.WriteString(path)| //11 blksize preferred block size for file system I/O | ||
| //12 blocks actual number of blocks allocated | ||
| script := `perl -e '@a = stat(shift) or exit 2; $u = getpwuid($a[4]); $g = getgrgid($a[5]); printf("0%o:%s:%d:%s:%d:%d:%d", $a[2], $u, $a[4], $g, $a[5], $a[7], $a[9])'` | ||
| script := `perl -e '$p = shift; $l = -l $p ? 1 : 0; @a = stat($p) or exit 2; $u = getpwuid($a[4]); $g = getgrgid($a[5]); printf("%d:0%o:%s:%d:%s:%d:%d:%d", $l, $a[2], $u, $a[4], $g, $a[5], $a[7], $a[9])'` |
There was a problem hiding this comment.
🟡 warning — AIX: Perl's stat() follows symlinks (like stat -L), so for a dangling symlink @a = stat($p) will fail and the script exits with code 2, losing the symlink metadata. The linux/unix paths fall back to non-dereferencing stat for dangling links; AIX should use lstat($p) as a fallback:
$l = -l $p ? 1 : 0; @a = stat($p); @a = lstat($p) if !@a && $l; ...| // -H follows only command-line symlinks (resolving the start path) | ||
| // while keeping -type l functional for discovered symlinks. | ||
| if isLinkSearch { | ||
| call.WriteString("find -H ") | ||
| } else { | ||
| call.WriteString("find -L ") | ||
| } |
There was a problem hiding this comment.
🟡 warning — Switching from -L to -H for link searches fixes -type l, but it also changes traversal semantics: -H does not follow symlinks to directories encountered during the walk, only the starting path. If a user has /home/user/link-to-dir -> /data/ and runs files.find(from: "/home/user", type: "link"), the tool will still descend into link-to-dir only if it's the starting path — but intermediate symlink-dirs won't be followed. This is a behavioral change vs. the old -L mode (which followed all symlinks). Consider documenting this trade-off or using -L with a post-filter on lstat to preserve traversal behavior while still detecting symlinks.
Test Results7 725 tests - 3 503 7 719 ✅ - 3 502 4m 23s ⏱️ +51s For more details on these failures, see this check. Results for commit 975bd8a. ± Comparison against base commit ae96214. This pull request removes 3503 tests. |
Summary
file.permissions.isSymlinkalways returningfalsefor symlinks across all connection types (local, SSH/SFTP, SSH+sudo/SCP, Docker)files.find(type: "link")returning empty results on macOS (BSDfindlacks-xtype)permissions.stringrenderingdinstead oflfor symlinks pointing to directoriesRoot Cause
isSymlink: Every stat path followed symlinks before checking the file mode.os.Stat,sftp.Stat, andstat -Lall resolve symlinks to their target, soModeSymlinkwas never present in the returned mode bits. The file resource correctly checkedstat.Mode & ModeSymlink(file.go:111), but the bit was never set.files.find: PR #8729 fixedfiles.find(type: "link")by using-xtype lunderfind -L. This works on GNU find (Linux) but macOS ships BSD find, which does not support-xtypeand fails with exit code 1.permissions.string: Theid()method checkedisDirectorybeforeisSymlink, so a symlink to a directory rendered asdrwxr-xr-xinstead oflrwxr-xr-x.Fix
isSymlink-- lstat-primary approachAll stat code paths now call lstat first, then stat only when a symlink is detected:
local.go)afero.Stat(follows symlinks)LstatIfPossiblefirst; if symlink,Statfor target metadata, then OR inModeSymlinkssh.go)sftp.Stat(SSH_FXP_STAT, follows)sftp.Lstat(SSH_FXP_LSTAT) first; same two-step for symlinksstatutil/stat.go)test -e+stat -L(two round-trips, no symlink info)Compound command examples:
SL=0; test -L <path> && SL=1; test -e <path> -o $SL -eq 1 || exit 1; stat -L <path> -c "$SL.%s.%f.%u.%g.%X.%Y.%C"(falls back tostatwithout-Lfor dangling symlinks)SL=0; test -L <path> && SL=1; stat -L -f "$SL:%z:%p:%u:%g:%a:%m" <path>-loperator prepended to existing perl stat scriptFor non-symlinks, lstat and stat return identical results (single syscall, no extra cost). For symlinks, the returned mode carries
ModeSymlinkwhile size/permissions/ownership reflect the target -- except for dangling symlinks, which return lstat metadata.files.find(type: "link")-- portablefind -HReplaced
find -L ... -xtype lwithfind -H ... -type l:-Hfollows only command-line symlinks (resolving paths like/tmp->/private/tmp) but not symlinks found during traversal-type lthen correctly matches discovered symlinksfile,directory, etc.) continue usingfind -Lunchangedpermissions.string-- symlink takes prioritySwapped the check order in
file.go:id()soisSymlinkis tested beforeisDirectory. A symlink to a directory now correctly renders aslrwxr-xr-x.Test plan
statutil,filesfind,file_internal_test)file.permissions.isSymlinkreturnstruefor symlinksfiles.find(from: "/tmp", type: "link")finds symlinksisSymlinkreturnsfalsefiles.findwithtype: "file"/"directory"unaffectedisSymlink: truepermissions.stringshowslprefixisSymlink: false,isFile: truefiles.find(type: "link")finds all 3 symlinksfiles.findwith other types unaffectedisSymlink: trueisSymlink: true, no errorisSymlink: false