mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
chmod/chown: symlink cycles detection (#11805)
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
// spell-checker:ignore (words) dirfd subdirs openat FDCWD
|
||||
// spell-checker:ignore (words) dirfd subdirs openat FDCWD rwxr
|
||||
|
||||
use std::fs::{OpenOptions, Permissions, metadata, set_permissions};
|
||||
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
||||
@@ -1440,3 +1440,63 @@ fn test_chmod_colored_output() {
|
||||
.stderr_contains("\x1b[31merreur\x1b[0m") // Red "erreur" in French
|
||||
.stderr_contains("\x1b[33m--invalid-option\x1b[0m"); // Yellow invalid option
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chmod_symlink_cycles() {
|
||||
let scene = TestScenario::new(util_name!());
|
||||
let at = &scene.fixtures;
|
||||
|
||||
at.mkdir_all("a/b/c");
|
||||
at.symlink_dir("a", "a/b/c/d");
|
||||
|
||||
// Pin the directory modes so the assertions below don't depend on the ambient
|
||||
// umask (a fresh mkdir under umask 077/027 would not be 0755).
|
||||
at.set_mode("a", 0o755);
|
||||
at.set_mode("a/b", 0o755);
|
||||
at.set_mode("a/b/c", 0o755);
|
||||
|
||||
scene
|
||||
.ucmd()
|
||||
.arg("-vRL")
|
||||
.arg("+r")
|
||||
.arg("a")
|
||||
.run()
|
||||
// cSpell:disable
|
||||
.stdout_contains_line("mode of 'a' retained as 0755 (rwxr-xr-x)")
|
||||
.stdout_contains_line("mode of 'a/b' retained as 0755 (rwxr-xr-x)")
|
||||
.stdout_contains_line("mode of 'a/b/c' retained as 0755 (rwxr-xr-x)")
|
||||
.stdout_contains_line("mode of 'a/b/c/d' retained as 0755 (rwxr-xr-x)")
|
||||
.stdout_does_not_contain("mode of 'a/b/c/d/b' retained as 0755 (rwxr-xr-x)")
|
||||
.stdout_does_not_contain("mode of 'a/b/c/d/b/c' retained as 0755 (rwxr-xr-x)")
|
||||
.stdout_does_not_contain("mode of 'a/b/c/d/b/c/d' retained as 0755 (rwxr-xr-x)");
|
||||
// cSpell:enable
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chmod_symlink_two_links_same_dir() {
|
||||
// Two symlinks pointing at the same directory is NOT a cycle: neither link is
|
||||
// an ancestor of the other, so the target's contents must be visited through
|
||||
// *both* links (and through the real directory). This guards the backtracking
|
||||
// in the cycle-detection logic against false positives.
|
||||
let scene = TestScenario::new(util_name!());
|
||||
let at = &scene.fixtures;
|
||||
|
||||
// cSpell:disable
|
||||
at.mkdir_all("base/realdir");
|
||||
at.touch("base/realdir/file");
|
||||
at.symlink_dir("base/realdir", "base/link1");
|
||||
at.symlink_dir("base/realdir", "base/link2");
|
||||
|
||||
// Assert on the path only (not the mode bits), so the test is robust to the
|
||||
// file's umask-dependent permissions across platforms.
|
||||
scene
|
||||
.ucmd()
|
||||
.arg("-vRL")
|
||||
.arg("+r")
|
||||
.arg("base")
|
||||
.run()
|
||||
.stdout_contains("mode of 'base/realdir/file'")
|
||||
.stdout_contains("mode of 'base/link1/file'")
|
||||
.stdout_contains("mode of 'base/link2/file'");
|
||||
// cSpell:enable
|
||||
}
|
||||
|
||||
@@ -943,3 +943,89 @@ fn test_chown_no_dereference_symlink_to_dir() {
|
||||
"dir's ctime should not have changed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chown_symlink_cycles() {
|
||||
let scene = TestScenario::new(util_name!());
|
||||
let at = &scene.fixtures;
|
||||
|
||||
let result = scene.cmd("whoami").run();
|
||||
if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") {
|
||||
return;
|
||||
}
|
||||
let user_name = String::from(result.stdout_str().trim());
|
||||
assert!(!user_name.is_empty());
|
||||
|
||||
at.mkdir_all("a/b/c");
|
||||
at.symlink_dir("a", "a/b/c/d");
|
||||
|
||||
let result = scene.ucmd().arg("-vRL").arg(&user_name).arg("a").run();
|
||||
|
||||
if cfg!(target_os = "macos") || cfg!(target_os = "openbsd") || cfg!(target_os = "android") {
|
||||
result
|
||||
.stderr_contains(format!("ownership of 'a' retained as {user_name}"))
|
||||
.stderr_contains(format!("ownership of 'a/b' retained as {user_name}"))
|
||||
.stderr_contains(format!("ownership of 'a/b/c' retained as {user_name}"))
|
||||
.stderr_does_not_contain(format!("ownership of 'a/b/c/d' retained as {user_name}"))
|
||||
.stderr_does_not_contain(format!("ownership of 'a/b/c/d/b' retained as {user_name}"))
|
||||
.stderr_does_not_contain(format!(
|
||||
"ownership of 'a/b/c/d/b/c' retained as {user_name}"
|
||||
));
|
||||
} else {
|
||||
result
|
||||
.success()
|
||||
.stderr_contains(format!("ownership of 'a' retained as {user_name}"))
|
||||
.stderr_contains(format!("ownership of 'a/b' retained as {user_name}"))
|
||||
.stderr_contains(format!("ownership of 'a/b/c' retained as {user_name}"))
|
||||
.stderr_contains(format!("ownership of 'a/b/c/d' retained as {user_name}"))
|
||||
.stderr_does_not_contain(format!("ownership of 'a/b/c/d/b' retained as {user_name}"))
|
||||
.stderr_does_not_contain(format!(
|
||||
"ownership of 'a/b/c/d/b/c' retained as {user_name}"
|
||||
))
|
||||
.stderr_does_not_contain(format!(
|
||||
"ownership of 'a/b/c/d/b/c/d' retained as {user_name}"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chown_symlink_two_links_same_dir() {
|
||||
// Two symlinks pointing at the same directory is NOT a cycle: neither link is
|
||||
// an ancestor of the other, so the target's contents must be visited through
|
||||
// *both* links. This guards the backtracking in the linux safe-traversal
|
||||
// cycle detection against false positives.
|
||||
let scene = TestScenario::new(util_name!());
|
||||
let at = &scene.fixtures;
|
||||
|
||||
let result = scene.cmd("whoami").run();
|
||||
if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") {
|
||||
return;
|
||||
}
|
||||
let user_name = String::from(result.stdout_str().trim());
|
||||
assert!(!user_name.is_empty());
|
||||
|
||||
// cSpell:disable
|
||||
at.mkdir_all("base/realdir");
|
||||
at.touch("base/realdir/file");
|
||||
at.symlink_dir("base/realdir", "base/link1");
|
||||
at.symlink_dir("base/realdir", "base/link2");
|
||||
|
||||
let result = scene.ucmd().arg("-vRL").arg(&user_name).arg("base").run();
|
||||
|
||||
// Only linux uses the safe-traversal cycle detection that this test exercises;
|
||||
// other platforms fall back to walkdir with its own loop handling.
|
||||
if cfg!(target_os = "linux") {
|
||||
result
|
||||
.success()
|
||||
.stderr_contains(format!(
|
||||
"ownership of 'base/realdir/file' retained as {user_name}"
|
||||
))
|
||||
.stderr_contains(format!(
|
||||
"ownership of 'base/link1/file' retained as {user_name}"
|
||||
))
|
||||
.stderr_contains(format!(
|
||||
"ownership of 'base/link2/file' retained as {user_name}"
|
||||
));
|
||||
}
|
||||
// cSpell:enable
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user