astrid-runtime/astrid · error
symlink {} resolves outside the capsule source root ({}); re
Error message
symlink {} resolves outside the capsule source root ({}); refusing to copy (sandbox-escape vector) What it means
While copying a capsule source tree, handle_symlink canonicalizes symlink targets and refuses any that resolve outside the canonicalized source root. This blocks a sandbox-escape vector: a malicious or misconfigured capsule source could otherwise plant a symlink that makes the copy routine write files outside the destination.
Source
Thrown at crates/astrid-capsule-install/src/copy.rs:138
fn handle_symlink(src_path: &Path, dst_path: &Path, canonical_root: &Path) -> anyhow::Result<()> {
// Canonicalize first — resolves the symlink chain to a real path
// we can reason about. A dangling symlink errors here; we treat
// that as a hard install failure because a capsule tree with
// broken symlinks isn't trustworthy.
let resolved: PathBuf = std::fs::canonicalize(src_path).with_context(|| {
format!(
"symlink {} could not be canonicalized (dangling or denied)",
src_path.display()
)
})?;
// Hard-refuse anything that resolves outside the source tree.
// `Path::starts_with` on canonical paths is sound: canonicalize
// has already collapsed every `..` and resolved every symlink in
// both the resolved target and the root, so there's no path-
// traversal escape hatch left.
if !resolved.starts_with(canonical_root) {
bail!(
"symlink {} resolves outside the capsule source root ({}); \
refusing to copy (sandbox-escape vector)",
src_path.display(),
resolved.display()
);
}
let resolved_meta = std::fs::metadata(&resolved)
.with_context(|| format!("stat resolved symlink target {}", resolved.display()))?;
if resolved_meta.is_dir() {
// Directory symlinks open the door to (a) infinite recursion
// when the link points to an ancestor and (b) ballooning
// copies of legitimately-shared trees. `npm install` only
// produces FILE symlinks for `.bin/` entries; we don't need
// directory symlinks for any current capsule layout. Refuse
// them outright.
bail!(View on GitHub (pinned to affd8760f4)
Solutions
- Remove or rewrite the offending symlink so it resolves inside the capsule source tree (relative link within the root).
- Replace the symlink with a real copy of the target file inside the source tree.
- If the target is genuinely external, vendor its content into the capsule rather than linking.
Example fix
// before (inside capsule source) bin/tool -> /usr/local/bin/tool // after cp /usr/local/bin/tool bin/tool # real file, or relative link: bin/tool -> ../shared/tool
Defensive patterns
Strategy: validation
Validate before calling
// pre-screen a source tree before installing:
fn has_escaping_symlinks(root: &Path) -> anyhow::Result<bool> {
let canon_root = root.canonicalize()?;
for entry in walkdir::WalkDir::new(root).follow_links(false) {
let p = entry?.path().to_path_buf();
if fs::symlink_metadata(&p)?.file_type().is_symlink() {
let target = fs::read_link(&p)?;
let resolved = p.parent().unwrap().join(target).canonicalize()?;
if !resolved.starts_with(&canon_root) { return Ok(true); }
}
}
Ok(false)
} Try / catch
match copy_capsule_dir_inner(...) {
Err(e) if e.to_string().contains("sandbox-escape") => {
eprintln!("source contains an escaping symlink; vendor the target file instead");
}
other => other?,
} Prevention
- Use relative symlinks that stay within the source tree.
- Replace external links with real copies before packaging.
- Never vendor trees containing absolute symlinks from other machines.
When it happens
Trigger: Calling copy_capsule_dir_inner (via the capsule install/copy flow) on a source tree containing a file symlink whose canonicalized target lies outside the source root — absolute links to /etc or $HOME, or relative links climbing past the root with ../ segments.
Common situations: A dev capsule source with convenience links into node_modules of a parent directory or to global tool paths; npm/pnpm layouts with absolute .bin links produced by unusual install setups; a vendored tree copied from another machine with dangling absolute links.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- directory symlink {} not allowed in capsule source tree (ref
- cache contains a redirect or special entry: {}
- capsule path escaped source root
- capsule projection escaped its root: {}
- legacy state source is redirected: {}
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/8889688730e4f80a.
Report an issue: GitHub.