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
21 changes: 20 additions & 1 deletion src/uu/ls/src/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,25 @@ fn dotdot_path(parent: &Path) -> PathBuf {
dotdot
}

fn is_still_linked(path_data: &PathData) -> bool {
use std::os::unix::fs::MetadataExt;

let Some(self_metadata) = path_data.metadata() else {
return false;
};

let parent = dotdot_path(path_data.path());
let Ok(parent_entries) = fs::read_dir(&parent) else {
return false;
};

parent_entries.flatten().any(|entry| {
entry
.metadata()
.is_ok_and(|md| md.ino() == self_metadata.ino() && md.dev() == self_metadata.dev())
})
}

fn collect_directory_entries<O: LsOutput>(
entries: &mut Vec<PathData>,
path_data: &PathData,
Expand All @@ -1283,7 +1302,7 @@ fn collect_directory_entries<O: LsOutput>(
) -> UResult<()> {
entries.clear();

if config.files == Files::All {
if config.files == Files::All && is_still_linked(path_data) {
entries.push(PathData::new(
path_data.path().to_path_buf().into(),
None,
Expand Down
48 changes: 47 additions & 1 deletion tests/by-util/test_ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff dired subdired tmpfs mdir COLORTERM mexe bcdef mfoo timefile
// spell-checker:ignore (words) fakeroot setcap drwxr bcdlps mdangling mentry awith acolons NOFILE NOTCAPABLE
// spell-checker:ignore (words) fakeroot setcap drwxr bcdlps mdangling mentry awith acolons NOFILE NOTCAPABLE newfstatat fchdir
#![allow(
clippy::similar_names,
clippy::too_many_lines,
Expand Down Expand Up @@ -7626,6 +7626,52 @@ fn test_ls_recursive_no_fd_leak() {
.stderr_is("");
}

#[test]
#[cfg(unix)]
fn test_ls_dot_orphaned_directory() {
use std::os::fd::AsRawFd;
use std::os::unix::process::CommandExt;
use std::process::Command;
use uucore::libc;

let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;

at.mkdir("z");

// Hold fd on `z` while its a directory
let dir_fd = at.open("z");
let raw_fd = dir_fd.as_raw_fd();

// Delete and replace it with a file of the same name
at.rmdir("z");
at.touch("z");

// Run `ls` with cwd set on the fd to operate on the old orphaned dir in fs tree
let mut cmd = Command::new(&scene.bin_path);
cmd.arg(&scene.util_name).arg("-la");
unsafe {
cmd.pre_exec(move || {
if libc::fchdir(raw_fd) != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
let output = cmd.output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);

let has_dot_entries = stdout.lines().any(|line| {
let trimmed = line.trim_end();
trimmed.ends_with(" .") || trimmed.ends_with(" ..")
});

assert!(
!has_dot_entries,
"expected no '.'/'..' entries. got:\n{stdout}"
);
}

#[test]
#[cfg(all(unix, not(target_os = "macos")))]
fn test_ls_non_utf8_hidden() {
Expand Down
Loading