{"record":{"id":"adaa1373b7a5b833","repo":"astrid-runtime/astrid","slug":"materialization-parent-is-a-symlink","errorCode":null,"errorMessage":"materialization parent is a symlink: {}","messagePattern":"materialization parent is a symlink: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/astrid-capsule-install/src/storage.rs","lineNumber":617,"sourceCode":"        fs::write(&output, bytes)\n            .with_context(|| format!(\"write materialized file {}\", output.display()))?;\n    }\n    fs::write(destination.join(\"meta.json\"), &package.metadata)\n        .context(\"write materialized capsule metadata\")?;\n    fs::write(destination.join(\"authority.json\"), &package.authority)\n        .context(\"write materialized capsule authority\")?;\n    Ok(())\n}\n\nfn reject_symlink_ancestors(root: &Path, path: &Path) -> anyhow::Result<()> {\n    let relative = path\n        .strip_prefix(root)\n        .map_err(|_| anyhow::anyhow!(\"materialization path escaped destination\"))?;\n    let mut current = root.to_path_buf();\n    for component in relative.components() {\n        current.push(component.as_os_str());\n        if fs::symlink_metadata(&current).is_ok_and(|metadata| metadata.file_type().is_symlink()) {\n            bail!(\"materialization parent is a symlink: {}\", current.display());\n        }\n    }\n    Ok(())\n}\n\nmod leftover;\nmod migration;\n\npub use leftover::retire_unmatched_legacy_authority_receipts;\npub use migration::{\n    LegacyCapsuleAuthorityReceipt, LegacyCapsuleMigrationReport, LegacyEnvSecretImportStatus,\n    legacy_capsule_authority_status, legacy_env_secret_import_status, migrate_all_native_capsules,\n    migrate_all_native_capsules_with_report, migrate_native_capsules,\n    migrate_native_capsules_with_report,\n};\n\nfn canonical_legacy_archive(\n    home: &astrid_core::dirs::AstridHome,","sourceCodeStart":599,"sourceCodeEnd":635,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/storage.rs#L599-L635","documentation":"reject_symlink_ancestors walks each path component from the destination root as files are created and bails if any existing component is a symlink. This prevents a symlinked directory inside the destination from redirecting extracted files outside the materialization root (symlink-escape attack).","triggerScenarios":"Extracting an archive whose entries create a directory path where an ancestor within the destination is a symlink — e.g. archive contains 'link -> /tmp/evil' plus 'link/file.wasm', or an attacker pre-created a symlink at an intermediate path.","commonSituations":"Materializing into a destination directory that already contains attacker-controlled symlinks; archives crafted to bypass plain path checks via link entries; shared temp directories with leftover symlinks from prior runs.","solutions":["Use a fresh, empty destination directory for materialization.","Since the extractor already rejects link entries (error 145), treat this error as evidence of pre-existing or injected symlinks and inspect/remove them: find dest -type l.","Re-obtain and re-verify the archive; combined with this error it likely indicates tampering."],"exampleFix":"// before\nlet dest = Path::new(\"/var/tmp/shared-capsule\"); // may contain symlinks\n// after\nlet dest = tempdir()?.path().to_path_buf(); // fresh empty dir\nassert!(fs::read_dir(&dest)?.next().is_none());","handlingStrategy":"validation","validationCode":"fn has_no_symlinks_under(dest: &Path) -> std::io::Result<bool> {\n    for entry in fs::read_dir(dest)? {\n        let e = entry?;\n        if fs::symlink_metadata(e.path())?.file_type().is_symlink() {\n            return Ok(false);\n        }\n    }\n    Ok(true)\n}\n// call before materializing into a reused directory","typeGuard":null,"tryCatchPattern":"match materialize_capsule_package(&pkg, &dest) {\n    Err(e) if e.to_string().contains(\"symlink\") => {\n        error!(\"symlink escape detected at {} — aborting, do not retry in place\", dest.display());\n        use_fresh_tempdir_and_retry()?;\n    }\n    other => other,\n}","preventionTips":["Always materialize into a fresh, empty, private temp directory.","Audit shared destination directories for symlinks (find dest -type l) before reuse.","Treat this error as a security event: investigate the archive and environment, don't just retry."],"tags":["security","symlink","path-escape","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"}