{"record":{"id":"e79dc7c59cafabf1","repo":"astrid-runtime/astrid","slug":"open-projected-file-error","errorCode":null,"errorMessage":"open projected file {}: {error}","messagePattern":"open projected file (.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/lib.rs","lineNumber":162,"sourceCode":"    std::iter::from_fn(move || {\n        let directory = next.take()?;\n        next = directory.parent().map(ToOwned::to_owned);\n        if directory.as_os_str().is_empty() {\n            return None;\n        }\n        Some(directory.to_string_lossy().into_owned())\n    })\n}\n\n#[cfg(all(unix, not(all(target_arch = \"wasm32\", target_os = \"unknown\"))))]\nfn open_projection_file_nofollow(path: &Path) -> anyhow::Result<std::fs::File> {\n    use std::os::unix::fs::OpenOptionsExt as _;\n\n    std::fs::OpenOptions::new()\n        .read(true)\n        .custom_flags(nix::libc::O_NOFOLLOW | nix::libc::O_CLOEXEC)\n        .open(path)\n        .map_err(|error| anyhow::anyhow!(\"open projected file {}: {error}\", path.display()))\n}\n\n#[cfg(all(not(unix), not(all(target_arch = \"wasm32\", target_os = \"unknown\"))))]\nfn open_projection_file_nofollow(path: &Path) -> anyhow::Result<std::fs::File> {\n    std::fs::File::open(path)\n        .map_err(|error| anyhow::anyhow!(\"open projected file {}: {error}\", path.display()))\n}\n\nimpl Drop for CapsuleViewLease {\n    fn drop(&mut self) {\n        self.locks.remove_if(&self.key, |_, stored| {\n            stored.ptr_eq(&self.lock) && stored.strong_count() == 0\n        });\n    }\n}\n\nstruct CapsuleViewGuard {\n    held: Option<tokio::sync::OwnedMutexGuard<()>>,","sourceCodeStart":144,"sourceCodeEnd":180,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/lib.rs#L144-L180","documentation":"This error comes from `open_projection_file_nofollow` on Unix: opening a projected capsule file with `O_NOFOLLOW` failed. The kernel intentionally refuses to follow symlinks when reading durable projections, so this error fires for ordinary open failures (file missing, permissions, race) AND for the case where the projected path is actually a symlink (open returns ELOOP, since O_NOFOLLOW rejects links to protect against symlink attacks in the cache/projection tree). The path is included in the message.","triggerScenarios":"Calling `read_projection_file_nofollow` (directly or via capsule view reads) when: the projected file does not exist; the process lacks read permission; the path is a symlink (ELOOP under O_NOFOLLOW); or the file disappears mid-read due to concurrent cache repair (remove_dir_all from errors 1025–1027).","commonSituations":"Another process replaced a projection file with a symlink (tampering or a sync tool); reading concurrently while the capsule cache is being repaired/re-materialized; running the tool under a user without permissions on the cache directory; TOCTOU where a file seen in an inventory listing is deleted before it is opened.","solutions":["If the message indicates ELOOP ('too many levels of symbolic links'), inspect the path — replace the symlink with a real file by re-materializing the capsule (delete the cache dir and re-run).","Re-run the read after any in-flight materialization/repair completes; treat the error as a transient race and retry once.","Check permissions/ownership on the projection path and the containing cache directory.","Verify the file exists with `ls -la <path>` (looking for the `l` file-type flag) before deeper debugging."],"exampleFix":"// before: sharing projection files via symlinks breaks O_NOFOLLOW reads\nln -s /shared/config.toml ~/.cache/astrid/projections/app/config.toml\n// after: ensure real files exist\nrm ~/.cache/astrid/projections/app/config.toml && re-materialize the capsule","handlingStrategy":"try-catch","validationCode":"use std::path::Path;\n\nfn readable_regular_file(path: &Path) -> Result<(), String> {\n    let md = std::fs::symlink_metadata(path)\n        .map_err(|e| format!(\"missing or unreadable: {e}\"))?;\n    if md.file_type().is_symlink() {\n        return Err(format!(\"{} is a symlink; O_NOFOLLOW reads will fail\", path.display()));\n    }\n    if !md.is_file() {\n        return Err(format!(\"{} is not a regular file\", path.display()));\n    }\n    std::fs::File::open(path)\n        .map(|_| ())\n        .map_err(|e| format!(\"{} not openable: {e}\", path.display()))\n}","typeGuard":"fn is_openable_regular_file(path: &Path) -> bool {\n    std::fs::symlink_metadata(path)\n        .map(|md| md.is_file() && !md.file_type().is_symlink())\n        .unwrap_or(false)\n}","tryCatchPattern":"match read_projection_file_nofollow(&path) {\n    Ok(bytes) => Ok(bytes),\n    Err(e) if e.to_string().contains(\"symbolic link\") || e.to_string().contains(\"ELOOP\") => {\n        // projection tampered/replaced with a symlink: repair and retry once\n        repair_capsule_projection(&capsule_id)?;\n        read_projection_file_nofollow(&path)\n    }\n    Err(e) if e.kind() == std::io::ErrorKind::NotFound => {\n        // race with cache repair: retry after repair completes\n        retry_after_repair(&path)\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Never replace files inside capsule projections with symlinks; O_NOFOLLOW reads deliberately reject them.","Do not read projections concurrently with cache repair/re-materialization; subscribe to repair completion or retry.","Run reads as a user with permissions on the entire cache directory tree.","Avoid tools (sync clients, link managers) that rewrite cache contents in place."],"tags":["filesystem","symlink","security","open-failed"],"backgroundTag":"file-open-failed","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"}