cp -r creates directories with the source's mode, ignoring the umask
cp -r gives every directory it creates the source's mode bits unmasked, where GNU applies the umask. Regular files in the same copy are masked correctly, so files and directories disagree and the directory branch wins.
$ mkdir -p src/dir && echo x > src/dir/f && chmod 777 src/dir && chmod 666 src/dir/f
$ (umask 077; cp -r src d)
$ stat -c '%a %n' d/dir d/dir/fd/dir |
d/dir/f |
|
|---|---|---|
| uutils 0.11.0 / main | 777 | 600 |
| GNU 9.11 | 700 | 600 |
Under umask 022 uutils gives 777 where GNU gives 755. --no-preserve=mode gives 700, matching GNU, and -a gives 777 on both, which is correct because -a preserves.
Cause
copy_dir creates each directory through build_dir, which does apply the umask, and records it in dirs_needing_permissions (copydir.rs:520) for a final pass (copydir.rs:574). That pass calls copy_attributes, which promotes any freshly created directory to Preserve::Yes even when the attribute set is Attributes::NONE:
// cp.rs:1868
let mode = if !mode_explicitly_disabled && dest_is_freshly_created_dir {and the preserving path then writes the source's unmasked mode at cp.rs:1943, overwriting what build_dir chose. Regular files take the other path and apply the umask explicitly.
Suggested fix
In that branch, when attributes.mode is Preserve::No, apply source_mode & !umask - the computation the file path already uses - instead of promoting to Preserve::Yes. The promotion presumably exists so a created directory ends up with the source's mode rather than build_dir's temporary one; masking keeps that and drops the widening.
Affected: 0.11.0 and main. Reported privately by Hongkai Chen (SEFCOM Lab, Arizona State University); handled as a normal bug rather than an advisory.
Source: uutils/coreutils