{"record":{"id":"7282582df00926e0","repo":"astrid-runtime/astrid","slug":"capsule-materialization-target-is-redirected-or-no","errorCode":null,"errorMessage":"capsule materialization target is redirected or not a directory","messagePattern":"capsule materialization target is redirected or not a directory","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/capsule_materialization.rs","lineNumber":186,"sourceCode":"\n    /// Replace a canonical stale projection without trusting the old manifest.\n    #[cfg(not(all(target_arch = \"wasm32\", target_os = \"unknown\")))]\n    pub(crate) fn repair_published_materialization(\n        &self,\n        target: &Path,\n        principal: &astrid_core::principal::PrincipalId,\n        discovery_manifest: &astrid_capsule_types::manifest::CapsuleManifest,\n        snapshot: &astrid_storage::CapsulePackageSnapshot,\n    ) -> anyhow::Result<astrid_capsule_types::manifest::CapsuleManifest> {\n        self.validate_published_cache_path(target, principal, discovery_manifest, snapshot)?;\n        let target_metadata = match std::fs::symlink_metadata(target) {\n            Ok(metadata) => Some(metadata),\n            Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,\n            Err(error) => return Err(anyhow::anyhow!(\"inspect capsule materialization: {error}\")),\n        };\n        if let Some(metadata) = target_metadata {\n            if metadata.file_type().is_symlink() || !metadata.is_dir() {\n                anyhow::bail!(\"capsule materialization target is redirected or not a directory\");\n            }\n            if let Ok(bound_manifest) =\n                astrid_capsule::discovery::load_manifest(&target.join(\"Capsule.toml\"))\n                && self\n                    .verify_published_materialization(target, principal, &bound_manifest, snapshot)\n                    .is_ok()\n            {\n                return Ok(bound_manifest);\n            }\n            astrid_core::platform_fs::verify_no_redirects(target).map_err(|error| {\n                anyhow::anyhow!(\"capsule materialization target is redirected: {error}\")\n            })?;\n            std::fs::remove_dir_all(target).map_err(|error| {\n                anyhow::anyhow!(\"remove stale capsule materialization: {error}\")\n            })?;\n        }\n        astrid_capsule_install::materialize_capsule_package(snapshot.package(), target)\n            .map_err(|error| anyhow::anyhow!(\"materialize durable capsule package: {error:#}\"))?;","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/capsule_materialization.rs#L168-L204","documentation":"Before reusing or replacing an existing capsule projection, repair_published_materialization inspects the target path with symlink_metadata (following nothing). If the path exists but is a symlink or any non-directory entry, the kernel bails rather than deleting or writing through it — a redirected target could point the removal/re-materialization at arbitrary filesystem locations. Note symlink_metadata does not follow the final component, so a symlinked target is detected directly.","triggerScenarios":"repair_published_materialization (via capture_bound_materialization or ensure_published_materialization) is called with a target that exists and std::fs::symlink_metadata reports file_type().is_symlink() == true or is_dir() == false — e.g. the cache path is a symlink to another location, or a regular file/socket was left at the expected directory path.","commonSituations":"A user symlinked the cache directory to a bigger disk or to dotfile-managed storage; a previous failed run left a stray file where the directory should be; container image layers replaced the directory with a symlink; the state_dir was relocated and a compat symlink was created manually.","solutions":["Remove the symlink or non-directory entry at the target path yourself (after confirming it is safe), then retry so the kernel can materialize a real directory there.","Reconfigure the cache/state location to its real directory path instead of using a symlink (the kernel intentionally refuses to traverse redirected targets).","If the path should be a directory, inspect what created the file/symlink (dotfile managers, previous tools) and fix that setup before re-running.","Use an alternative target path under the workspace state dir that is a plain directory."],"exampleFix":"// before: cache path is a symlink, kernel refuses\n// ~/.astrid/capsules/my-capsule -> /mnt/bigdisk/my-capsule (symlink)\n\n// after: remove the redirect and let the kernel own the directory\nstd::fs::remove_file(&runtime_dir)?; // removes the symlink itself\nstd::fs::create_dir_all(&runtime_dir)?;\nlet manifest = kernel.ensure_published_materialization(\n    &runtime_dir, &principal, &discovery_manifest, &snapshot,\n)?;","handlingStrategy":"validation","validationCode":"// Refuse to proceed if the target is not a plain directory\nlet meta = std::fs::symlink_metadata(&target)?;\nif meta.file_type().is_symlink() || !meta.is_dir() {\n    std::fs::remove_file(&target)?; // drop redirect; kernel will re-materialize\n}","typeGuard":"fn is_plain_directory(path: &Path) -> bool {\n    std::fs::symlink_metadata(path)\n        .map(|m| m.is_dir() && !m.file_type().is_symlink())\n        .unwrap_or(false)\n}","tryCatchPattern":"match kernel.ensure_published_materialization(&target, &principal, &manifest, &snapshot) {\n    Err(e) if e.to_string().contains(\"redirected or not a directory\") => {\n    remove_redirect_at(&target)?;\n    kernel.ensure_published_materialization(&target, &principal, &manifest, &snapshot)\n    }\n    other => other,\n}","preventionTips":["Do not symlink capsule cache directories to other volumes; configure the cache root instead.","Check dotfile/sync managers for rules that replace state directories with symlinks.","Verify target paths with symlink_metadata before invoking repair/ensure APIs.","Clean up stray files left by crashed runs at expected cache paths before reloading."],"tags":["filesystem","security","symlink","path-validation"],"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"}