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

  1. 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)
  2. Canonicalize the root before calling the API (e.g. `fs::canonicalize(root)`) so `strip_prefix` matches the paths produced by `read_dir`
  3. 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

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


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/8e574a7b771ede21. Report an issue: GitHub.