{"record":{"id":"8e574a7b771ede21","repo":"astrid-runtime/astrid","slug":"capsule-path-escaped-source-root","errorCode":null,"errorMessage":"capsule path escaped source root","messagePattern":"capsule path escaped source root","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule-install/src/storage.rs","lineNumber":779,"sourceCode":"    }\n    let encoder = builder\n        .into_inner()\n        .context(\"finish canonical capsule tar stream\")?;\n    encoder\n        .finish()\n        .context(\"finish canonical capsule gzip stream\")\n}\n\nfn collect_entries(\n    root: &Path,\n    current: &Path,\n    entries: &mut Vec<(PathBuf, Metadata)>,\n) -> anyhow::Result<()> {\n    let mut children = read_dir_sorted(current)?;\n    for (name, metadata) in children.drain(..) {\n        let relative = name\n            .strip_prefix(root)\n            .map_err(|_| anyhow::anyhow!(\"capsule path escaped source root\"))?\n            .to_path_buf();\n        let file_type = metadata.file_type();\n        if file_type.is_symlink() {\n            let resolved = fs::canonicalize(&name).with_context(|| {\n                format!(\"canonicalize capsule source symlink {}\", relative.display())\n            })?;\n            let canonical_root = fs::canonicalize(root)\n                .with_context(|| format!(\"canonicalize capsule source root {}\", root.display()))?;\n            if !resolved.starts_with(&canonical_root) {\n                bail!(\n                    \"capsule source symlink {} resolves outside source root\",\n                    relative.display()\n                );\n            }\n            let resolved_metadata = fs::metadata(&resolved).with_context(|| {\n                format!(\"stat capsule source symlink target {}\", relative.display())\n            })?;\n            if !resolved_metadata.is_file() {","sourceCodeStart":761,"sourceCodeEnd":797,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/storage.rs#L761-L797","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nlet root = PathBuf::from(\"build/../my-capsule\");\ncanonical_capsule_archive(home, &root, ...)?; // escapes root via mismatched prefixes\n\n// after\nlet root = std::fs::canonicalize(\"build/../my-capsule\")?;\n// ensure my-capsule contains no symlinks pointing outside the tree\ncanonical_capsule_archive(home, &root, ...)?;","handlingStrategy":"validation","validationCode":"fn assert_capsule_source_is_contained(root: &Path) -> anyhow::Result<()> {\n    let root = std::fs::canonicalize(root)?;\n    for entry in walkdir::WalkDir::new(&root).follow_links(true) {\n        let entry = entry?;\n        if entry.path_is_symlink() {\n            let resolved = std::fs::canonicalize(entry.path())?;\n            anyhow::ensure!(resolved.starts_with(&root),\n                \"symlink {} escapes capsule root {}\", entry.path().display(), root.display());\n        }\n    }\n    Ok(())\n}\nassert_capsule_source_is_contained(&capsule_root)?;","typeGuard":"fn is_under_root(root: &Path, p: &Path) -> bool {\n    p.strip_prefix(root).is_ok()\n}","tryCatchPattern":"match collect_entries(&root, &mut entries) {\n    Err(e) if e.to_string().contains(\"capsule path escaped source root\") => {\n        // sanitize symlinks / canonicalize root, then retry once\n    }\n    Err(e) => return Err(e),\n    Ok(()) => {}\n}","preventionTips":["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"],"tags":["filesystem","path-traversal","symlink","rust","archive"],"backgroundTag":"path-traversal-blocked","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}