{"record":{"id":"7ff2454c30c621a4","repo":"astrid-runtime/astrid","slug":"capsule-materialization-cache-is-redirected-or-not","errorCode":null,"errorMessage":"capsule materialization cache is redirected or not a directory: {}","messagePattern":"capsule materialization cache is redirected or not a directory: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule-install/src/paths.rs","lineNumber":183,"sourceCode":"    Ok(())\n}\n\n/// Remove every disposable user capsule materialization from the runtime\n/// cache after validating the complete tree without following redirects.\n///\n/// Durable packages are never touched. A fresh materialization is created\n/// from a verified storage snapshot when needed, so deleting stale or\n/// interrupted cache generations at boot is safe and avoids reusing an\n/// unverified crash residue.\npub fn clear_capsule_materialization_cache(home: &AstridHome) -> anyhow::Result<()> {\n    let root = home.run_dir().join(\"capsules\");\n    let metadata = match std::fs::symlink_metadata(&root) {\n        Ok(metadata) => metadata,\n        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),\n        Err(error) => return Err(error).context(\"inspect capsule materialization cache\"),\n    };\n    if metadata.file_type().is_symlink() || !metadata.is_dir() {\n        anyhow::bail!(\n            \"capsule materialization cache is redirected or not a directory: {}\",\n            root.display()\n        );\n    }\n    validate_cache_tree(&root)?;\n    for entry in std::fs::read_dir(&root).context(\"read capsule materialization cache\")? {\n        let path = entry\n            .context(\"read capsule materialization cache entry\")?\n            .path();\n        remove_cache_tree(&path)?;\n    }\n    Ok(())\n}\n\nfn validate_cache_tree(path: &Path) -> anyhow::Result<()> {\n    astrid_core::platform_fs::verify_no_redirects(path)\n        .with_context(|| format!(\"verify cache path {}\", path.display()))?;\n    for entry in std::fs::read_dir(path).with_context(|| format!(\"read {}\", path.display()))? {","sourceCodeStart":165,"sourceCodeEnd":201,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/paths.rs#L165-L201","documentation":"clear_capsule_materialization_cache refuses to wipe the materialization cache root if symlink_metadata shows it is a symlink or not a directory. This prevents a redirected cache root (an attacker- or misconfiguration-planted symlink) from causing deletion of an arbitrary directory. NotFound is tolerated (nothing to clean); any other inspection failure is wrapped in a context error.","triggerScenarios":"Calling clear_capsule_materialization_cache when the cache root path is a symlink (e.g. a user linked it to another location), or a regular file was created at the cache root path, or a previous misconfiguration pointed the cache at a non-directory path.","commonSituations":"Users symlinking the cache to a bigger disk or shared location; leftover file occupying the cache path after a failed install; container images where the cache path is a mounted file; tests (like cache_cleanup_removes_crash_residue) that pre-create the path incorrectly.","solutions":["Remove the symlink or file at the cache root path, then re-run the cleanup so the library can recreate a real directory","Fix whatever creates the cache root so it is always a real directory owned by the runtime","Point the cache root configuration at the intended directory rather than a link","If redirecting the cache is a legitimate need, relocate the configured root itself instead of symlinking"],"exampleFix":"// before\n$ ln -s /mnt/bigdisk/capsule-cache ~/.cache/astrid/capsules\n// after\n$ rm ~/.cache/astrid/capsules\n$ mkdir -p ~/.cache/astrid/capsules   # or reconfigure the cache root to /mnt/bigdisk/capsule-cache","handlingStrategy":"validation","validationCode":"fn cache_root_safe(root: &Path) -> anyhow::Result<bool> {\n    match std::fs::symlink_metadata(root) {\n        Ok(m) => Ok(!m.file_type().is_symlink() && m.is_dir()),\n        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(true),\n        Err(e) => Err(e.into()),\n    }\n}","typeGuard":"fn is_real_directory(root: &Path) -> bool {\n    std::fs::symlink_metadata(root).map(|m| m.is_dir() && !m.file_type().is_symlink()).unwrap_or(false)\n}","tryCatchPattern":"if let Err(e) = clear_capsule_materialization_cache(&root) {\n    if e.to_string().contains(\"redirected or not a directory\") {\n        // remove the symlink/file at root, recreate a real dir, retry once\n    } else { return Err(e); }\n}","preventionTips":["Never symlink the configured cache root; change the configuration instead","Have the runtime create the cache root itself with create_dir_all","Alert on any file or link appearing at the cache root path","In tests/CI, assert the cache root is a real directory before cleanup"],"tags":["cache","symlink","security"],"backgroundTag":"invalid-argument-value","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"}