From ed15a26a6ae0fa61db0178d33ef075052fb122c8 Mon Sep 17 00:00:00 2001 From: Lorenzo Rossi <65499789+rossilor95@users.noreply.github.com> Date: Sat, 30 May 2026 14:29:58 +0200 Subject: [PATCH] chown: -h on symlink to directory operates on link, not target (#12522) closes: #11887 --- src/uucore/src/lib/features/perms.rs | 3 +- tests/by-util/test_chown.rs | 46 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index 79a51b0cc..865377872 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -309,7 +309,8 @@ impl ChownExecutor { let ret = if self.matched(meta.uid(), meta.gid()) { // Use safe syscalls for root directory to prevent TOCTOU attacks on Linux #[cfg(target_os = "linux")] - let chown_result = if path.is_dir() { + // We cannot check path.is_dir() here, as this would resolve symlinks + let chown_result = if meta.is_dir() { // For directories on Linux, use safe traversal from the start match DirFd::open(path, SymlinkBehavior::Follow) { Ok(dir_fd) => self diff --git a/tests/by-util/test_chown.rs b/tests/by-util/test_chown.rs index 0ebf744ab..b5db811ac 100644 --- a/tests/by-util/test_chown.rs +++ b/tests/by-util/test_chown.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. // spell-checker:ignore (words) agroupthatdoesntexist auserthatdoesntexist cuuser groupname notexisting passgrp +use std::os::unix::fs::MetadataExt; use uutests::util::{CmdResult, TestScenario, is_ci, run_ucmd_as_root}; use uutests::util_name; use uutests::{at_and_ucmd, new_ucmd}; @@ -897,3 +898,48 @@ fn test_chown_reference_file() { .stderr_contains("ownership of 'b' retained as") .no_stdout(); } + +#[test] +#[cfg(unix)] +fn test_chown_no_dereference_symlink_to_dir() { + 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("dir"); + at.symlink_dir("dir", "link_to_dir"); + + let link_meta_before = std::fs::symlink_metadata(at.plus("link_to_dir")).unwrap(); + let link_ctime_before = (link_meta_before.ctime(), link_meta_before.ctime_nsec()); + + let dir_meta_before = std::fs::metadata(at.plus("dir")).unwrap(); + let dir_ctime_before = (dir_meta_before.ctime(), dir_meta_before.ctime_nsec()); + + scene + .ucmd() + .arg("--no-dereference") + .arg(&user_name) + .arg("link_to_dir") + .succeeds(); + + let link_meta_after = std::fs::symlink_metadata(at.plus("link_to_dir")).unwrap(); + let link_ctime_after = (link_meta_after.ctime(), link_meta_after.ctime_nsec()); + + let dir_meta_after = std::fs::metadata(at.plus("dir")).unwrap(); + let dir_ctime_after = (dir_meta_after.ctime(), dir_meta_after.ctime_nsec()); + + assert_ne!( + link_ctime_before, link_ctime_after, + "link's ctime should have advanced" + ); + assert_eq!( + dir_ctime_before, dir_ctime_after, + "dir's ctime should not have changed" + ); +}