Skip to content

Commit b209eac

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

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
@@ -762,6 +762,35 @@ impl FsSandbox {
762762
let mut out = resolved_ancestor;
763763
for name in tail.into_iter().rev() {
764764
out.push(name);
765+
// Catch symlinks in the non-existing tail — including dangling ones.
766+
if let Ok(meta) = std::fs::symlink_metadata(&out) {
767+
if meta.file_type().is_symlink() {
768+
let target = std::fs::read_link(&out)?;
769+
let abs = if target.is_absolute() {
770+
target
771+
} else {
772+
out.parent().unwrap_or(&self.root).join(&target)
773+
};
774+
// Normalise the absolute target (resolve .. etc) without
775+
// touching the filesystem, then check the prefix.
776+
let mut norm = std::path::PathBuf::new();
777+
for c in abs.components() {
778+
match c {
779+
std::path::Component::ParentDir => {
780+
norm.pop();
781+
}
782+
std::path::Component::CurDir => {}
783+
c => norm.push(c),
784+
}
785+
}
786+
if !norm.starts_with(&self.root) {
787+
return Err(anyhow!(
788+
"symlink target escapes mount root: {:?}",
789+
guest_path
790+
));
791+
}
792+
}
793+
}
765794
}
766795
Ok(out)
767796
}
@@ -2483,6 +2512,74 @@ mod tests {
24832512
assert!(err.contains("escapes mount root"), "{err}");
24842513
}
24852514

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

0 commit comments

Comments
 (0)