astrid-runtime/astrid · error
capsule path escaped source root
Error message
capsule path escaped source root
What it means
This error is thrown by `collect_entries` in the capsule archive builder when a directory entry path cannot be stripped of the source root prefix. The library walks the capsule source tree recursively and requires every entry to live strictly under the root; if `Path::strip_prefix` fails, the path (typically via a symlink) has escaped the source root, which would let archive entries point outside the capsule. It is a deliberate path-traversal safety guard, not a incidental I/O failure.
Source
Thrown at crates/astrid-capsule-install/src/storage.rs:779
}
let encoder = builder
.into_inner()
.context("finish canonical capsule tar stream")?;
encoder
.finish()
.context("finish canonical capsule gzip stream")
}
fn collect_entries(
root: &Path,
current: &Path,
entries: &mut Vec<(PathBuf, Metadata)>,
) -> anyhow::Result<()> {
let mut children = read_dir_sorted(current)?;
for (name, metadata) in children.drain(..) {
let relative = name
.strip_prefix(root)
.map_err(|_| anyhow::anyhow!("capsule path escaped source root"))?
.to_path_buf();
let file_type = metadata.file_type();
if file_type.is_symlink() {
let resolved = fs::canonicalize(&name).with_context(|| {
format!("canonicalize capsule source symlink {}", relative.display())
})?;
let canonical_root = fs::canonicalize(root)
.with_context(|| format!("canonicalize capsule source root {}", root.display()))?;
if !resolved.starts_with(&canonical_root) {
bail!(
"capsule source symlink {} resolves outside source root",
relative.display()
);
}
let resolved_metadata = fs::metadata(&resolved).with_context(|| {
format!("stat capsule source symlink target {}", relative.display())
})?;
if !resolved_metadata.is_file() {View on GitHub (pinned to affd8760f4)
Solutions
- Remove or replace any symlink inside the capsule source that resolves outside the source root (use real copies, or a symlink confined to the tree)
- Canonicalize the root before calling the API (e.g. `fs::canonicalize(root)`) so `strip_prefix` matches the paths produced by `read_dir`
- Re-run `collect_entries`/`canonical_capsule_archive` on a clean, self-contained source tree
Example fix
// before
let root = PathBuf::from("build/../my-capsule");
canonical_capsule_archive(home, &root, ...)?; // escapes root via mismatched prefixes
// after
let root = std::fs::canonicalize("build/../my-capsule")?;
// ensure my-capsule contains no symlinks pointing outside the tree
canonical_capsule_archive(home, &root, ...)?; Defensive patterns
Strategy: validation
Validate before calling
fn assert_capsule_source_is_contained(root: &Path) -> anyhow::Result<()> {
let root = std::fs::canonicalize(root)?;
for entry in walkdir::WalkDir::new(&root).follow_links(true) {
let entry = entry?;
if entry.path_is_symlink() {
let resolved = std::fs::canonicalize(entry.path())?;
anyhow::ensure!(resolved.starts_with(&root),
"symlink {} escapes capsule root {}", entry.path().display(), root.display());
}
}
Ok(())
}
assert_capsule_source_is_contained(&capsule_root)?; Type guard
fn is_under_root(root: &Path, p: &Path) -> bool {
p.strip_prefix(root).is_ok()
} Try / catch
match collect_entries(&root, &mut entries) {
Err(e) if e.to_string().contains("capsule path escaped source root") => {
// sanitize symlinks / canonicalize root, then retry once
}
Err(e) => return Err(e),
Ok(()) => {}
} Prevention
- Always canonicalize the capsule root with fs::canonicalize before archiving
- Audit capsule sources for symlinks pointing outside the tree (find . -type l ! -exec readlink -m {} ";" xargs -I{} sh -c 'case {} in "$PWD"/*) ;; *) echo outside: {};; esac')
- Never construct source paths with `..` segments; resolve them first
- Vendor symlinked dependencies as real copies in capsule sources
When it happens
Trigger: Calling `canonical_capsule_archive` (directly or via capsule install/migration flows) on a source tree where `read_dir` yields an entry whose full path does not begin with `root` — most commonly a symlink inside the capsule pointing to an absolute path or a parent directory outside the source root (`..`), or a root/child path mismatch such as a non-canonical root with `..` components.
Common situations: Capsule source directories containing convenience symlinks (e.g. symlinked vendored deps or `current -> ../releases/x`); checkouts with a symlinked top-level directory; building an archive from a path constructed with `..` segments instead of a canonicalized root.
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
- symlink {} resolves outside the capsule source root ({}); re
- directory symlink {} not allowed in capsule source tree (ref
- capsule path is not valid UTF-8
- capsule projection escaped its root: {}
- inspect projected file {}: {error}
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/8e574a7b771ede21.
Report an issue: GitHub.