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

  1. 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).
  2. Re-run the read after any in-flight materialization/repair completes; treat the error as a transient race and retry once.
  3. Check permissions/ownership on the projection path and the containing cache directory.
  4. 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

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


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/e79dc7c59cafabf1. Report an issue: GitHub.