astrid-runtime/astrid · error · io::Error

principal-store capability entry is redirected or not a dire

Error message

principal-store capability entry is redirected or not a directory

What it means

validate_directory_entry uses symlink_metadata to check that a principal-store directory entry is a real directory and not a symlink or other redirected entry. If the entry is a symlink, or otherwise not a directory, the library refuses to open it. This enforces that capability-relative paths only traverse genuine directories.

Source

Thrown at crates/astrid-storage/src/engine/durable/native_io.rs:96

                .map_err(|source| {
                    io_error("create principal-store capability directory", source)
                })?;
            sync_directory(parent)?;
            open()
                .map(Some)
                .map_err(|source| io_error("open principal-store capability directory", source))
        },
        Err(source) => Err(io_error(
            "open principal-store capability directory",
            source,
        )),
    }
}

fn validate_directory_entry(parent: &Dir, name: &Path) -> io::Result<()> {
    let metadata = parent.symlink_metadata(name)?;
    if !metadata.is_dir() || directory_entry_is_redirected(&metadata) {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "principal-store capability entry is redirected or not a directory",
        ));
    }
    Ok(())
}

#[cfg(windows)]
fn directory_entry_is_redirected(metadata: &cap_std::fs::Metadata) -> bool {
    use cap_std::fs::MetadataExt as _;
    use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;

    metadata.file_type().is_symlink()
        || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
}

#[cfg(not(windows))]
fn directory_entry_is_redirected(metadata: &cap_std::fs::Metadata) -> bool {

View on GitHub (pinned to affd8760f4)

Solutions

  1. Replace the symlink with a real directory (rm the link, mkdir the directory) and restore/migrate its contents
  2. Do not symlink any entries inside the principal-store root
  3. Check permissions on the store root so untrusted users cannot plant symlinks
  4. Run any store-integrity/repair tooling the library provides to rebuild the directory layout

Example fix

// before
ln -s /shared/data store/principals/tenant-a
// after
rm store/principals/tenant-a && mkdir store/principals/tenant-a
Defensive patterns

Strategy: validation

Validate before calling

fn assert_real_dir(p: &Path) -> io::Result<()> {
    let md = p.symlink_metadata()?;
    if md.file_type().is_symlink() || !md.is_dir() {
        return Err(io::Error::new(io::ErrorKind::InvalidData, "entry is symlink or not a dir"));
    }
    Ok(())
}

Type guard

fn is_plain_dir(p: &Path) -> bool {
    std::fs::symlink_metadata(p).map(|m| m.is_dir() && !m.file_type().is_symlink()).unwrap_or(false)
}

Try / catch

match open_directory_result {
    Err(e) if e.kind() == io::ErrorKind::InvalidData => {
        // replace symlink with real directory, then retry
    }
    r => r,
}

Prevention

When it happens

Trigger: Calling open_directory where the named entry is a symlink (possibly to outside the store), a regular file, or another non-directory object; also triggered when filesystem-level redirection (e.g. overlay/symlink tricks) makes the entry look redirected to directory_entry_is_redirected.

Common situations: A developer replaced a store subdirectory with a symlink to share data between environments; automated tooling linked directories to save space; an attacker planted a symlink inside a world-writable store root.

Related errors


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