You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
cp -r mutates symlink targets' permissions on macOS (chmod follows symlink to source)
Summary
When cp -r SRC DEST recursively copies a directory tree containing symlinks, uutils correctly creates the symlinks at DEST (without dereferencing) — but then calls set_permissions(dest_symlink_path, ...). On platforms where chmod() follows symlinks (macOS / BSD by default; Linux too unless lchmod/fchmodat(AT_SYMLINK_NOFOLLOW) is used), this set_permissions call traverses the new symlink and mutates the original target file's mode — silently, with no error.
This affected files outside the cp source tree entirely. In our case, a routine cp -r client_data client_data_backup invisibly chmoded ~9 files in a different repository (the symlink targets), causing a "phantom" mode-only diff on next git status.
The bug is present regardless of --preserve value:
The destination is created correctly (a symlink to the same target). The unintended side-effect is on the source target file, which lives outside src/ and outside dst/.
Why this happens (hypothesis)
uutils cp -r on a symlink:
Creates the destination symlink via symlink(src_target, dst_path) ✓
Calls something like set_permissions(dst_path, mode) to "preserve" attributes
Rust's std::fs::set_permissions ultimately invokes chmod() (POSIX), which follows symlinks by default on macOS/BSD
The chmod hits the file the new symlink points to — i.e., the original source target
The mode applied depends on what uutils computed:
--preserve mode (default): uses the symlink's lstat mode, which on macOS is always 0755 regardless of the actual target's permissions
--preserve []: uses an apparent fallback default of umask & 0666 = 0644
Either way, the original target's mode is replaced — irrespective of what it was before.
The fix would be to either (a) call fchmodat(AT_SYMLINK_NOFOLLOW) / lchmod for symlink destinations, or (b) skip set_permissions entirely for symlinks (since POSIX symlinks don't carry meaningful mode bits anyway).
Severity
This is silent data corruption of files outside the copy operation's stated scope. It's particularly nasty because:
No error or warning is printed
The destination tree looks correct
The damage is in the source-of-symlink target, which the user typically didn't even open
It's discovered later via git status showing inexplicable mode changes — or worse, isn't discovered at all
Real-world trigger: any "backup-by-copy" workflow over a workspace containing symlinks into another repo (e.g. monorepo-style external storage layouts, dotfile farms, dev workspace links).
Use external BSD /bin/cp -R (via ^cp in nushell) when copying trees that contain symlinks. The bundled cp is unsafe for this case until uutils chmods through lchmod/fchmodat.
Related
cp wrong permission of source directory instead of umask #10862 — cp -r doesn't intersect copied permissions with the process umask. Different symptom (wrong perms on destination, sometimes EPERM mid-copy) and different reproducer (no symlinks needed, requires custom umask), but likely shares infrastructure: both look like consequences of cp calling std::fs::set_permissions without the flags it needs. cp wrong permission of source directory instead of umask #10862 is "wrong mode value chosen"; this one is "right mode value applied to wrong file (followed symlink to source)". A fix that audits every set_permissions call site in uutils cp would likely address both.
cp -rmutates symlink targets' permissions on macOS (chmod follows symlink to source)Summary
When
cp -r SRC DESTrecursively copies a directory tree containing symlinks, uutils correctly creates the symlinks atDEST(without dereferencing) — but then callsset_permissions(dest_symlink_path, ...). On platforms wherechmod()follows symlinks (macOS / BSD by default; Linux too unlesslchmod/fchmodat(AT_SYMLINK_NOFOLLOW)is used), thisset_permissionscall traverses the new symlink and mutates the original target file's mode — silently, with no error.This affected files outside the
cpsource tree entirely. In our case, a routinecp -r client_data client_data_backupinvisibly chmoded ~9 files in a different repository (the symlink targets), causing a "phantom" mode-only diff on nextgit status.The bug is present regardless of
--preservevalue:0600)nu -c 'cp -r src dst'(default)0755(preserves symlink lstat mode)nu -c 'cp --preserve [] -r src dst'0644(umask-derived default)/bin/cp -R src dst(BSD/macOS)0600(correct)/bin/cp -RP src dst0600(correct)/bin/cp -Rp src dst0600(correct)nu -c '^cp -R src dst'(external)0600(correct)Reproducer
The destination is created correctly (a symlink to the same target). The unintended side-effect is on the source target file, which lives outside
src/and outsidedst/.Why this happens (hypothesis)
uutils
cp -ron a symlink:symlink(src_target, dst_path)✓set_permissions(dst_path, mode)to "preserve" attributesstd::fs::set_permissionsultimately invokeschmod()(POSIX), which follows symlinks by default on macOS/BSDThe mode applied depends on what uutils computed:
--preserve mode(default): uses the symlink's lstat mode, which on macOS is always0755regardless of the actual target's permissions--preserve []: uses an apparent fallback default ofumask & 0666=0644Either way, the original target's mode is replaced — irrespective of what it was before.
The fix would be to either (a) call
fchmodat(AT_SYMLINK_NOFOLLOW)/lchmodfor symlink destinations, or (b) skipset_permissionsentirely for symlinks (since POSIX symlinks don't carry meaningful mode bits anyway).Severity
This is silent data corruption of files outside the copy operation's stated scope. It's particularly nasty because:
git statusshowing inexplicable mode changes — or worse, isn't discovered at allReal-world trigger: any "backup-by-copy" workflow over a workspace containing symlinks into another repo (e.g. monorepo-style external storage layouts, dotfile farms, dev workspace links).
Environment
0.112.2, buildmacos-aarch64, rustc1.94.126.4.1, Darwin Kernel25.4.0, ARM64Workaround
Use external BSD
/bin/cp -R(via^cpin nushell) when copying trees that contain symlinks. The bundledcpis unsafe for this case until uutils chmods throughlchmod/fchmodat.Related
cp -rdoesn't intersect copied permissions with the process umask. Different symptom (wrong perms on destination, sometimes EPERM mid-copy) and different reproducer (no symlinks needed, requires customumask), but likely shares infrastructure: both look like consequences ofcpcallingstd::fs::set_permissionswithout the flags it needs. cp wrong permission of source directory instead of umask #10862 is "wrong mode value chosen"; this one is "right mode value applied to wrong file (followed symlink to source)". A fix that audits everyset_permissionscall site in uutilscpwould likely address both.