astrid-runtime/astrid · error
open projected file {}: {error}
Error message
open projected file {}: {error} What it means
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.
Source
Thrown at crates/astrid-kernel/src/lib.rs:162
std::iter::from_fn(move || {
let directory = next.take()?;
next = directory.parent().map(ToOwned::to_owned);
if directory.as_os_str().is_empty() {
return None;
}
Some(directory.to_string_lossy().into_owned())
})
}
#[cfg(all(unix, not(all(target_arch = "wasm32", target_os = "unknown"))))]
fn open_projection_file_nofollow(path: &Path) -> anyhow::Result<std::fs::File> {
use std::os::unix::fs::OpenOptionsExt as _;
std::fs::OpenOptions::new()
.read(true)
.custom_flags(nix::libc::O_NOFOLLOW | nix::libc::O_CLOEXEC)
.open(path)
.map_err(|error| anyhow::anyhow!("open projected file {}: {error}", path.display()))
}
#[cfg(all(not(unix), not(all(target_arch = "wasm32", target_os = "unknown"))))]
fn open_projection_file_nofollow(path: &Path) -> anyhow::Result<std::fs::File> {
std::fs::File::open(path)
.map_err(|error| anyhow::anyhow!("open projected file {}: {error}", path.display()))
}
impl Drop for CapsuleViewLease {
fn drop(&mut self) {
self.locks.remove_if(&self.key, |_, stored| {
stored.ptr_eq(&self.lock) && stored.strong_count() == 0
});
}
}
struct CapsuleViewGuard {
held: Option<tokio::sync::OwnedMutexGuard<()>>,View on GitHub (pinned to affd8760f4)
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.
Example fix
// before: sharing projection files via symlinks breaks O_NOFOLLOW reads ln -s /shared/config.toml ~/.cache/astrid/projections/app/config.toml // after: ensure real files exist rm ~/.cache/astrid/projections/app/config.toml && re-materialize the capsule
Defensive patterns
Strategy: try-catch
Validate before calling
use std::path::Path;
fn readable_regular_file(path: &Path) -> Result<(), String> {
let md = std::fs::symlink_metadata(path)
.map_err(|e| format!("missing or unreadable: {e}"))?;
if md.file_type().is_symlink() {
return Err(format!("{} is a symlink; O_NOFOLLOW reads will fail", path.display()));
}
if !md.is_file() {
return Err(format!("{} is not a regular file", path.display()));
}
std::fs::File::open(path)
.map(|_| ())
.map_err(|e| format!("{} not openable: {e}", path.display()))
} Type guard
fn is_openable_regular_file(path: &Path) -> bool {
std::fs::symlink_metadata(path)
.map(|md| md.is_file() && !md.file_type().is_symlink())
.unwrap_or(false)
} Try / catch
match read_projection_file_nofollow(&path) {
Ok(bytes) => Ok(bytes),
Err(e) if e.to_string().contains("symbolic link") || e.to_string().contains("ELOOP") => {
// projection tampered/replaced with a symlink: repair and retry once
repair_capsule_projection(&capsule_id)?;
read_projection_file_nofollow(&path)
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
// race with cache repair: retry after repair completes
retry_after_repair(&path)
}
Err(e) => Err(e),
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
Background: "open() failed", "failed to open file", "cannot create file" — what a file open error means and how to fix it — this error's family across 42 libraries.
Related errors
- leftover capsule authority receipt is not a regular file: {}
- legacy capsule authority root is not a regular directory: {}
- legacy capsule authority root contains a non-regular entry:
- legacy capsule authority root is not a regular directory: {}
- directory symlink {} not allowed in capsule source tree (ref
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/e79dc7c59cafabf1.
Report an issue: GitHub.