diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 1f8889acfac..392aed0972e 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1248,6 +1248,20 @@ pub fn list_with_output( Ok(()) } +/// Build the path for the ".." entry of `parent`. +/// +/// On WASI the sandbox may block access to ".." at the preopened root, +/// so fall back to the parent path itself when its metadata can't be +/// read. On other targets this is just `parent/..`. +fn dotdot_path(parent: &Path) -> PathBuf { + let dotdot = parent.join(".."); + #[cfg(target_os = "wasi")] + if dotdot.metadata().is_err() { + return parent.to_path_buf(); + } + dotdot +} + fn collect_directory_entries( entries: &mut Vec, path_data: &PathData, @@ -1266,7 +1280,7 @@ fn collect_directory_entries( false, )); entries.push(PathData::new( - path_data.path().join("..").into(), + dotdot_path(path_data.path()).into(), None, Some(OsStr::new("..").into()), config, diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 307038ee099..8eb10f5a01e 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -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 +// spell-checker:ignore (words) fakeroot setcap drwxr bcdlps mdangling mentry awith acolons NOFILE NOTCAPABLE #![allow( clippy::similar_names, clippy::too_many_lines, @@ -7244,3 +7244,20 @@ fn test_ls_a_dotdot_no_error_on_wasi() { .stdout_contains("..") .no_stderr(); } + +#[test] +#[cfg(target_os = "wasi")] +fn test_ls_al_no_capabilities_insufficient_on_wasi() { + // `ls -al` reads metadata for every entry including "..". Without the + // WASI fallback, stat on ".." at the preopened root returns + // ERRNO_NOTCAPABLE, which surfaces to the user as "Capabilities + // insufficient". Guard against that regression here. + let scene = TestScenario::new(util_name!()); + let out = scene.ucmd().arg("-al").succeeds(); + out.no_stderr(); + assert!( + !out.stdout_str().contains("Capabilities insufficient"), + "ls -al stdout leaked a WASI capability error: {}", + out.stdout_str() + ); +}