Skip to content

Commit 2ccf3a9

Browse files
committed
fix: reject dangling symlinks escaping sandbox in FsSandbox::resolve
Signed-off-by: danbugs <danilochiarlone@gmail.com>
1 parent f5eb506 commit 2ccf3a9

1 file changed

Lines changed: 97 additions & 0 deletions

File tree

host/src/lib.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,35 @@ impl FsSandbox {
767767
let mut out = resolved_ancestor;
768768
for name in tail.into_iter().rev() {
769769
out.push(name);
770+
// Catch symlinks in the non-existing tail — including dangling ones.
771+
if let Ok(meta) = std::fs::symlink_metadata(&out) {
772+
if meta.file_type().is_symlink() {
773+
let target = std::fs::read_link(&out)?;
774+
let abs = if target.is_absolute() {
775+
target
776+
} else {
777+
out.parent().unwrap_or(&self.root).join(&target)
778+
};
779+
// Normalise the absolute target (resolve .. etc) without
780+
// touching the filesystem, then check the prefix.
781+
let mut norm = std::path::PathBuf::new();
782+
for c in abs.components() {
783+
match c {
784+
std::path::Component::ParentDir => {
785+
norm.pop();
786+
}
787+
std::path::Component::CurDir => {}
788+
c => norm.push(c),
789+
}
790+
}
791+
if !norm.starts_with(&self.root) {
792+
return Err(anyhow!(
793+
"symlink target escapes mount root: {:?}",
794+
guest_path
795+
));
796+
}
797+
}
798+
}
770799
}
771800
Ok(out)
772801
}
@@ -2488,6 +2517,74 @@ mod tests {
24882517
assert!(err.contains("escapes mount root"), "{err}");
24892518
}
24902519

2520+
#[cfg(unix)]
2521+
#[test]
2522+
fn test_resolve_dangling_symlink_escape() {
2523+
use std::os::unix::fs::symlink;
2524+
let root = tmpdir("dangling-escape");
2525+
// Create a dangling symlink whose target is outside root.
2526+
symlink("/tmp/nonexistent-outside-root", root.join("bad_link")).unwrap();
2527+
let fs_sb = FsSandbox::new(&root).unwrap();
2528+
let err = fs_sb.resolve("bad_link").unwrap_err().to_string();
2529+
assert!(
2530+
err.contains("escapes mount root"),
2531+
"expected escape error, got: {err}"
2532+
);
2533+
}
2534+
2535+
#[cfg(unix)]
2536+
#[test]
2537+
fn test_resolve_valid_internal_symlink() {
2538+
use std::os::unix::fs::symlink;
2539+
let root = tmpdir("valid-internal");
2540+
fs::write(root.join("real_file.txt"), "hello").unwrap();
2541+
symlink(root.join("real_file.txt"), root.join("good_link")).unwrap();
2542+
let fs_sb = FsSandbox::new(&root).unwrap();
2543+
let resolved = fs_sb.resolve("good_link").unwrap();
2544+
// The symlink target is inside root, so resolve should succeed.
2545+
assert!(
2546+
resolved.starts_with(&root),
2547+
"expected path under root, got: {resolved:?}"
2548+
);
2549+
}
2550+
2551+
#[cfg(unix)]
2552+
#[test]
2553+
fn test_resolve_dangling_symlink_inside_root() {
2554+
use std::os::unix::fs::symlink;
2555+
let root = tmpdir("dangling-inside");
2556+
// Dangling symlink whose target is inside root (just doesn't exist yet).
2557+
symlink(root.join("future_file.txt"), root.join("ok_link")).unwrap();
2558+
let fs_sb = FsSandbox::new(&root).unwrap();
2559+
// This should be allowed — the target is within root even though it
2560+
// doesn't exist yet.
2561+
let resolved = fs_sb.resolve("ok_link").unwrap();
2562+
assert!(
2563+
resolved.starts_with(&root),
2564+
"expected path under root, got: {resolved:?}"
2565+
);
2566+
}
2567+
2568+
#[cfg(unix)]
2569+
#[test]
2570+
fn test_resolve_symlink_chain_escape() {
2571+
use std::os::unix::fs::symlink;
2572+
let root = tmpdir("chain-escape");
2573+
let outside = tmpdir("chain-outside");
2574+
// symlink B -> outside (dangling or not, target is outside root)
2575+
symlink(&outside, root.join("link_b")).unwrap();
2576+
// symlink A -> link_b
2577+
symlink(root.join("link_b"), root.join("link_a")).unwrap();
2578+
let fs_sb = FsSandbox::new(&root).unwrap();
2579+
// Resolving link_a should fail because it chains through link_b
2580+
// which points outside root.
2581+
let err = fs_sb.resolve("link_a").unwrap_err().to_string();
2582+
assert!(
2583+
err.contains("escapes mount root"),
2584+
"expected escape error, got: {err}"
2585+
);
2586+
}
2587+
24912588
#[test]
24922589
fn resolve_allows_paths_under_the_root() {
24932590
let root = tmpdir("allow");

0 commit comments

Comments
 (0)