From e946e4fa3f70ca51ff79cdc76f185a50572afb76 Mon Sep 17 00:00:00 2001 From: Chaganti-Reddy Date: Wed, 8 Jul 2026 16:48:06 +0530 Subject: [PATCH] realpath: don't require read permission on a directory just to confirm a trailing slash. Fixes #13151 --- src/uucore/src/lib/features/fs.rs | 20 ++++++++++++++++++-- tests/by-util/test_realpath.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index afa87d27729..0d267ce756f 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -350,6 +350,22 @@ impl<'a> From> for OwningComponent { } } +/// Confirm that `path` (already known to exist) is a directory, without +/// requiring permission to list its contents. +/// +/// `read_dir` alone would raise the right error for a non-directory, but +/// also requires listing permission on the target, which a plain "is this a +/// directory" check should not need (e.g. `realpath /root/` succeeds for +/// non-root users even though they can't list `/root`). +fn ensure_is_directory(path: &Path) -> IOResult<()> { + if fs::metadata(path)?.is_dir() { + Ok(()) + } else { + read_dir(path)?; + Ok(()) + } +} + /// Return the canonical, absolute form of a path. /// /// This function is a generalization of [`std::fs::canonicalize`] that @@ -457,13 +473,13 @@ pub fn canonicalize>( match miss_mode { MissingHandling::Existing => { if has_to_be_directory { - read_dir(&result)?; + ensure_is_directory(&result)?; } } MissingHandling::Normal => { if result.exists() { if has_to_be_directory { - read_dir(&result)?; + ensure_is_directory(&result)?; } } else if let Some(parent) = result.parent() { read_dir(parent)?; diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index 136f7fa6d8d..16d43e0c65a 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -472,6 +472,32 @@ fn test_realpath_trailing_slash() { .stderr_contains("No such file or directory\n"); } +#[test] +#[cfg(unix)] +fn test_realpath_trailing_slash_unreadable_directory() { + // A trailing slash only asserts that the operand is a directory, which is + // a stat-level check needing search permission on the parent, not read + // permission on the directory itself. + // https://github.com/uutils/coreutils/issues/13151 + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir("dir"); + at.set_mode("dir", 0o100); + + scene + .ucmd() + .arg("dir/") + .succeeds() + .stdout_contains(format!("{MAIN_SEPARATOR}dir\n")); + scene + .ucmd() + .args(&["-e", "dir/"]) + .succeeds() + .stdout_contains(format!("{MAIN_SEPARATOR}dir\n")); + + at.set_mode("dir", 0o700); +} + #[test] fn test_realpath_empty() { new_ucmd!().fails_with_code(1);