{"record":{"id":"0e5c8d7c85abd946","repo":"astrid-runtime/astrid","slug":"capsule-path-is-not-canonical-path","errorCode":null,"errorMessage":"capsule path is not canonical: {path}","messagePattern":"capsule path is not canonical: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule-install/src/storage.rs","lineNumber":863,"sourceCode":"        let metadata = fs::symlink_metadata(&name)\n            .with_context(|| format!(\"inspect capsule source entry {}\", name.display()))?;\n        children.push((name, metadata));\n    }\n    children.sort_by(|left, right| left.0.cmp(&right.0));\n    Ok(children)\n}\n\nfn append_entry(\n    builder: &mut Builder<GzEncoder<Vec<u8>>>,\n    root: &Path,\n    relative: &Path,\n    metadata: &Metadata,\n) -> anyhow::Result<()> {\n    let path = relative\n        .to_str()\n        .ok_or_else(|| anyhow::anyhow!(\"capsule path is not valid UTF-8\"))?;\n    if path.starts_with('/') || path.split('/').any(|part| part == \"..\" || part.is_empty()) {\n        bail!(\"capsule path is not canonical: {path}\");\n    }\n    let mut header = Header::new_gnu();\n    header.set_uid(0);\n    header.set_gid(0);\n    header.set_mtime(0);\n    header.set_mode(if metadata.is_dir() { 0o755 } else { 0o644 });\n    if metadata.is_dir() {\n        header.set_entry_type(EntryType::Directory);\n        header.set_size(0);\n        header.set_cksum();\n        builder\n            .append_data(&mut header, path, io::empty())\n            .with_context(|| format!(\"append capsule directory {path}\"))?;\n    } else {\n        let mut file =\n            File::open(root.join(relative)).with_context(|| format!(\"open capsule file {path}\"))?;\n        let mut bytes = Vec::new();\n        file.read_to_end(&mut bytes)","sourceCodeStart":845,"sourceCodeEnd":881,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/storage.rs#L845-L881","documentation":"Raised by append_entry while writing an entry into the canonical tar archive: the relative path is absolute, contains a `..` segment, or has an empty segment. Canonical capsules must contain only normalized, relative, root-anchored paths, so the entry is rejected rather than written.","triggerScenarios":"canonical_capsule_archive encountering a collected relative path that is not canonical — typically from a symlink relative target with `..` components, or unusual file names with `//` produced by joining; entries reaching append_entry fail the starts_with('/') / `..` / empty-segment check.","commonSituations":"Symlink targets written as `../foo` inside the tree; files created with odd names containing double slashes via tooling; custom code paths that hand-build relative paths instead of deriving them from the root walk.","solutions":["Normalize the offending path: make it relative to the capsule root with no `..`, `.`, empty, or duplicate slash segments.","Fix any in-tree symlink whose stored relative target contains `..`; use paths anchored at the root instead.","If you call collect_entries/canonical_capsule_archive with custom paths, derive them via path joins from the root rather than string concatenation."],"exampleFix":"// before: relative path with a parent segment\nlet relative = \"assets/../lib/main.wit\";\n\n// after: normalized canonical path\nlet relative = \"lib/main.wit\";","handlingStrategy":"validation","validationCode":"fn path_is_canonical(p: &str) -> bool {\n    !p.starts_with('/') && p.split('/').all(|part| !part.is_empty() && part != \"..\")\n}","typeGuard":"fn is_canonical_relative(p: &Path) -> bool {\n    p.is_relative() && p.components().all(|c| matches!(c, std::path::Component::Normal(_)))\n}","tryCatchPattern":"if !path_is_canonical(&rel) {\n    eprintln!(\"normalize archive paths before publishing: {rel}\");\n    std::process::exit(1);\n}","preventionTips":["Build relative paths via PathBuf::join from the root, never string concat","Avoid symlink targets containing `..`","Normalize paths with path.components() before adding to archives"],"tags":["path-validation","tar","capsule"],"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"}