Merge main into #870

This commit is contained in:
bflatastic
2026-10-19 23:42:15 +07:00
31 changed files with 665 additions and 100 deletions
+17 -5
View File
@@ -62,7 +62,7 @@ impl Roots {
Ok(resolved) if roots.iter().any(|r| resolved.starts_with(r)) => return Ok(resolved),
Ok(_) => {}
// Why a path can't be resolved says what exists there: only told for paths inside a root.
Err(why) if lexically_inside(path, &roots) => return Err(format!("automation path rejected: {asked}: {why}")),
Err(why) if inside_as_far_as_it_exists(path, &roots) => return Err(format!("automation path rejected: {asked}: {why}")),
Err(_) => {}
}
if roots.is_empty() {
@@ -79,9 +79,10 @@ impl Roots {
}
}
/// Whether `path`, made absolute with its `.` and `..` taken by name (no file system), lies in one
/// of `roots` (as resolved, or as shown without Windows' `\\?\` prefix).
fn lexically_inside(path: &Path, roots: &[PathBuf]) -> bool {
/// Whether `path` lies in one of `roots` once made absolute with its `.` and `..` taken by name and
/// its longest leading part that exists resolved, so links on the way are followed as the roots'
/// were (`/tmp/work/x` is inside root `/private/tmp/work`, a link out of a root leads out of it).
fn inside_as_far_as_it_exists(path: &Path, roots: &[PathBuf]) -> bool {
let Ok(abs) = std::path::absolute(path) else { return false };
let mut lexical = PathBuf::new();
for c in abs.components() {
@@ -93,7 +94,18 @@ fn lexically_inside(path: &Path, roots: &[PathBuf]) -> bool {
c => lexical.push(c),
}
}
roots.iter().any(|r| lexical.starts_with(r) || lexical.starts_with(shown(r)))
let mut rest: Vec<&OsStr> = Vec::new();
let mut at = lexical.as_path();
let resolved = loop {
if let Ok(mut base) = std::fs::canonicalize(at) {
base.extend(rest.iter().rev());
break base;
}
let (Some(name), Some(parent)) = (at.file_name(), at.parent()) else { return false };
rest.push(name);
at = parent;
};
roots.iter().any(|r| resolved.starts_with(r))
}
/// Whether `path` lies in the app's own folder `folder`, both resolved (links and `..` followed);
+17
View File
@@ -348,6 +348,10 @@ fn outside_paths_are_refused_alike_whatever_exists_there() {
let mut outside = vec![f.p("outside/secret.txt"), f.p("outside/none.txt"), f.p("outside/missing/../x.txt"), f.p("nowhere/deeper/x.txt")];
if symlink_file(&f.0.join("outside/not-yet.txt"), &f.0.join("outside/dangling.txt")) {
outside.push(f.p("outside/dangling.txt"));
// Reached through a link in the root, it is outside all the same.
if symlink_dir(&f.0.join("outside"), &f.0.join("root/escape")) {
outside.push(f.p("root/escape/dangling.txt"));
}
}
for p in &outside {
let e = refusal(p);
@@ -359,6 +363,19 @@ fn outside_paths_are_refused_alike_whatever_exists_there() {
}
}
#[test]
fn a_root_given_through_a_link_tells_why_paths_in_it_are_refused() {
let f = Fixture::new("linked-root");
if !symlink_dir(&f.0.join("root"), &f.0.join("via")) || !symlink_file(&f.0.join("outside/not-yet.txt"), &f.0.join("root/dangling.txt")) {
eprintln!("skipped: this system doesn't let tests make symbolic links");
return;
}
// Like `/tmp/work` on macOS, where `/tmp` leads to `/private/tmp`.
let r = Roots::new(&[], &[f.p("via")]).unwrap();
let e = r.check(Path::new(&f.p("via/dangling.txt")), Access::Write).unwrap_err();
assert!(e.contains("leads nowhere"), "{e}");
}
#[test]
fn only_resolved_paths_count_as_the_apps_own_folder() {
let f = Fixture::new("own");