diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 62c2ee822c0..6f57dcdbf77 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -1127,10 +1127,12 @@ pub fn replace_link(target: &Path, dest: &Path, symbolic: bool) -> IOResult<()> let basename = dest .file_name() .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "invalid link path"))?; + // No NOFOLLOW: the parent may be a symlink to a directory, which the + // create attempt above already followed. let dir = openat( CWD, parent, - OFlags::DIRECTORY | OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW, + OFlags::DIRECTORY | OFlags::RDONLY | OFlags::CLOEXEC, Mode::empty(), )?; let mut urandom = fs::File::open("/dev/urandom")?; diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index ddd5e70463b..f53028cdb4c 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -193,6 +193,48 @@ fn test_force_replace_same_inode_leaves_no_temp_file() { ); } +/// The destination can sit inside a directory reached through a symlink, as +/// when the target argument is a symlink to a directory. The replace must +/// follow it the same way the first create attempt does. +#[test] +#[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: creating a link inside a symlinked directory is denied" +)] +fn test_force_replace_in_symlinked_directory() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("real"); + at.symlink_dir("real", "dirlink"); + at.symlink_file("old", "real/link"); + + ucmd.args(&["-s", "-f", "new", "dirlink/link"]).succeeds(); + + assert_eq!(at.resolve_link("real/link"), "new"); +} + +/// Same as above for a hard link, which goes through the same replace path. +#[test] +// Android's app-private filesystem refuses hard links. +#[cfg(all(unix, not(any(target_os = "redox", target_os = "android"))))] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: creating a link inside a symlinked directory is denied" +)] +fn test_force_replace_hard_link_in_symlinked_directory() { + use std::os::unix::fs::MetadataExt; + + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("new"); + at.mkdir("real"); + at.symlink_dir("real", "dirlink"); + at.touch("real/link"); + + ucmd.args(&["-f", "new", "dirlink/link"]).succeeds(); + + assert_eq!(at.metadata("real/link").ino(), at.metadata("new").ino()); +} + #[test] fn test_symlink_overwrite_force_overrides_interactive() { let (at, mut ucmd) = at_and_ucmd!();