{"record":{"id":"2cbf2b8234415edb","repo":"astrid-runtime/astrid","slug":"capsule-archive-contains-a-link-or-special-file-n","errorCode":null,"errorMessage":"capsule archive contains a link or special file {name}","messagePattern":"capsule archive contains a link or special file (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule-install/src/source_digest.rs","lineNumber":58,"sourceCode":"        let mut entry = entry.context(\"read capsule archive entry\")?;\n        let path = entry.path().context(\"read capsule archive path\")?;\n        if path.is_absolute()\n            || path\n                .components()\n                .any(|component| matches!(component, std::path::Component::ParentDir))\n        {\n            bail!(\"capsule archive contains an unsafe path {}\", path.display());\n        }\n        let name = path\n            .to_str()\n            .ok_or_else(|| anyhow::anyhow!(\"capsule archive path is not UTF-8\"))?\n            .replace('\\\\', \"/\");\n        if !names.insert(name.clone()) {\n            bail!(\"capsule archive contains duplicate path {name}\");\n        }\n        let entry_type = entry.header().entry_type();\n        if !entry_type.is_dir() && !entry_type.is_file() {\n            bail!(\"capsule archive contains a link or special file {name}\");\n        }\n        let destination = staging.path().join(&path);\n        if entry_type.is_dir() {\n            fs::create_dir_all(&destination)\n                .with_context(|| format!(\"create capsule archive directory {name}\"))?;\n            continue;\n        }\n        if let Some(parent) = destination.parent() {\n            fs::create_dir_all(parent)\n                .with_context(|| format!(\"create capsule archive parent for {name}\"))?;\n        }\n        entry\n            .unpack(&destination)\n            .with_context(|| format!(\"unpack capsule archive file {name}\"))?;\n        // Drain the entry explicitly so malformed/truncated streams fail\n        // before the canonical builder reads the staged tree.\n        let mut sink = Vec::new();\n        entry","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/source_digest.rs#L40-L76","documentation":"When digesting a capsule archive, each entry's tar entry type is checked: only directories and regular files are allowed. Symlinks, hardlinks, and special files (devices, fifos) are rejected because the canonical archive format does not support them and links could redirect reads outside the intended tree during unpacking.","triggerScenarios":"Calling archive_digest_for_source on a .tar.gz containing a symlink, hardlink, or special-file entry — e.g. archives built with `tar -h` disabled over trees containing symlinks, or archives capturing dev/fifo nodes.","commonSituations":"Project trees containing symlinks (node_modules links, vendored deps linked in) archived without dereferencing; archives made from a build directory containing sockets/fifos; hand-crafted archives with hardlinks for space savings.","solutions":["Dereference symlinks when packing: `tar -czhf capsule.tgz -C <root> .` (or remove the symlinks and copy real files).","Find offending entries with `tar -tvzf capsule.tgz | grep -E '^l|^[^d-]'` and remove or replace them.","Exclude non-file artifacts from the archive (build sockets, fifos) via --exclude.","Hard-code a packaging step that copies the tree (cp -rL) before archiving."],"exampleFix":"// before: symlink left in tree, archive keeps the link entry\nln -s ../vendor/wit wit; tar -czf capsule.tgz .\n// after: dereference links into real files\ntar -czhf capsule.tgz -C capsule-root .","handlingStrategy":"validation","validationCode":"let out = std::process::Command::new(\"tar\")\n    .args([\"-tvzf\", archive_path])\n    .output()?;\nlet has_links = String::from_utf8_lossy(&out.stdout)\n    .lines()\n    .any(|l| l.starts_with('l') || l.starts_with('h'));","typeGuard":"fn only_dirs_and_files(types: &[tar::EntryType]) -> bool {\n    types.iter().all(|t| t.is_dir() || t.is_file())\n}","tryCatchPattern":"match archive_digest_for_source(archive) {\n    Err(e) if e.to_string().contains(\"link or special file\") => {\n        anyhow::bail!(\"repack with dereferenced symlinks: tar -czhf capsule.tgz -C <root> .\");\n    }\n    other => other,\n}","preventionTips":["Use `tar -h` (dereference) or `cp -rL` before archiving trees containing symlinks","Exclude build artifacts (sockets, fifos) from capsule archives","Avoid hardlinks in capsule packaging; ship regular files","Lint the source tree for symlinks before publishing"],"tags":["archive","tar","symlink","security"],"backgroundTag":"incompatible-source-type","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}