Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/uucore/src/lib/features/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,33 @@ fn ensure_is_directory(path: &Path) -> IOResult<()> {
}
}

/// Validates path components preceding each `..` component
///
/// Ensures that the accumulated path is a directory before traversing
/// to its parent. This prevents paths containing a regular file followed
/// by `..` from being incorrectly accepted after logical normalization..
fn validate_parent_component(path: &Path) -> IOResult<()> {
let mut current = PathBuf::new();

for component in path.components() {
match component {
Component::Prefix(_) | Component::RootDir | Component::Normal(_) => {
current.push(component.as_os_str());
}

Component::CurDir => {}

Component::ParentDir => {
if !current.as_os_str().is_empty() {
ensure_is_directory(&current)?;
}
current.pop();
}
}
}
Ok(())
}

/// Return the canonical, absolute form of a path.
///
/// This function is a generalization of [`std::fs::canonicalize`] that
Expand Down Expand Up @@ -362,6 +389,9 @@ pub fn canonicalize<P: AsRef<Path>>(
dunce::canonicalize(current_dir)?.join(original)
};
let path = if res_mode == ResolveMode::Logical {
if miss_mode != MissingHandling::Missing {
validate_parent_component(&original)?;
}
normalize_path(&original)
} else {
original
Expand All @@ -380,6 +410,9 @@ pub fn canonicalize<P: AsRef<Path>>(
}
OwningComponent::CurDir => {}
OwningComponent::ParentDir => {
if !result.as_os_str().is_empty() && miss_mode != MissingHandling::Missing {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this also runs with -s (ResolveMode::None), so realpath -s file/.. and -s missing/.. change too
could you please check what GNU does and add a -s test?

ensure_is_directory(&result)?;
}
result.pop();
}
}
Expand Down
17 changes: 17 additions & 0 deletions tests/by-util/test_readlink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -563,3 +563,20 @@ fn test_verbose_or_silent() {
)
.no_stdout();
}
#[test]
fn test_readlink_canonicalize_regular_file_followed_by_parent_dir() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;

at.touch("regular_file");

scene
.ucmd()
.arg("-v")
.arg("-f")
.arg("regular_file/..")
.fails()
.code_is(1)
.stderr_contains(NOT_A_DIRECTORY)
.no_stdout();
}
155 changes: 155 additions & 0 deletions tests/by-util/test_realpath.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ use std::path::{MAIN_SEPARATOR, Path};

static GIBBERISH: &str = "supercalifragilisticexpialidocious";

#[cfg(not(windows))]
static NOT_A_DIRECTORY: &str = "Not a directory";
#[cfg(windows)]
static NOT_A_DIRECTORY: &str = "The directory name is invalid.";

#[test]
#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")]
fn test_realpath_current_directory() {
Expand Down Expand Up @@ -670,3 +675,153 @@ fn test_realpath_canonicalize_vs_existing() {
}
}
}

#[test]
fn test_realpath_followed_by_parent_dir() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;

at.touch("regular_file");

scene
.ucmd()
.arg("regular_file/..")
.fails()
.code_is(1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the issue also mentions -L, -e and readlink -f, please add tests for them
-L still goes through normalize_path, so it isn't fixed yet

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes i totaly forgot to add Logical path support .

.stderr_contains(NOT_A_DIRECTORY)
.no_stdout();
}

#[test]
#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")]
fn test_realpath_directory_followed_by_parent_dir() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;

at.mkdir("directory");

scene
.ucmd()
.arg("directory/..")
.succeeds()
.stdout_is(format!("{}\n", at.root_dir_resolved()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the other tests using root_dir_resolved() have #[cfg_attr(wasi_runner, ignore = ...)], these ones probably need it too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

#[test]
#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")]
fn test_realpath_directory_followed_by_parent_dir_missing_mode() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please also test -m regular_file/.., it should succeed too


scene
.ucmd()
.arg("-m")
.arg("missing/..")
.succeeds()
.stdout_is(format!("{}\n", at.root_dir_resolved()));
}

#[test]
#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")]
fn test_realpath_strip_followed_by_parent_dir() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;

at.mkdir("directory");
at.touch("regular_file");

// With `-s` (no symlink resolution), GNU still validates that the
// component preceding `..` is an existing directory.
scene
.ucmd()
.arg("-s")
.arg("directory/..")
.succeeds()
.stdout_is(format!("{}\n", at.root_dir_resolved()));

scene
.ucmd()
.arg("-s")
.arg("regular_file/..")
.fails()
.code_is(1)
.stderr_contains(NOT_A_DIRECTORY)
.no_stdout();

scene
.ucmd()
.arg("-s")
.arg("missing/..")
.fails()
.code_is(1)
.stderr_contains("No such file or directory")
.no_stdout();

// `-m` relaxes the directory/existence check, so these succeed even
// combined with `-s`.
scene
.ucmd()
.args(&["-s", "-m"])
.arg("regular_file/..")
.succeeds()
.stdout_is(format!("{}\n", at.root_dir_resolved()));

scene
.ucmd()
.args(&["-s", "-m"])
.arg("missing/..")
.succeeds()
.stdout_is(format!("{}\n", at.root_dir_resolved()));
}

#[test]
#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")]
fn test_realpath_missing_mode_regular_file_followed_by_parent_dir() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;

at.touch("regular_file");

// `-m` permits missing and non-directory components, so a regular file
// followed by `..` must succeed (matching GNU).
scene
.ucmd()
.arg("-m")
.arg("regular_file/..")
.succeeds()
.stdout_is(format!("{}\n", at.root_dir_resolved()));
}

#[test]
fn test_realpath_logical_followed_by_parent_dir() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;

at.touch("regular_file");

scene
.ucmd()
.arg("-L")
.arg("regular_file/..")
.fails()
.code_is(1)
.stderr_contains(NOT_A_DIRECTORY)
.no_stdout();
}

#[test]
fn test_realpath_existing_followed_by_parent_dir() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;

at.touch("regular_file");

scene
.ucmd()
.arg("-e")
.arg("regular_file/..")
.fails()
.code_is(1)
.stderr_contains(NOT_A_DIRECTORY)
.no_stdout();
}