astrid-runtime/astrid · error

PermissionDenied

PermissionDenied

Error message

trusted Windows path contains a redirect or non-directory component: {}

What it means

This PermissionDenied error is raised by `TrustedPathGuard::capture` while it walks each component of the path with `symlink_metadata`: any component that is a reparse point (symlink, junction, mount point) or not a directory makes the trusted path un-verifiable. The guard pins directory identities by handle so later mutations cannot be redirected; a redirect component in the chain would defeat that guarantee, so capture refuses with the offending component's path in the message.

Source

Thrown at crates/astrid-core/src/platform_fs/windows/path.rs:205

        let mut components: Vec<LockedPathComponent> = Vec::new();
        let mut current = PathBuf::new();
        let mut rooted = false;
        for component in path.components() {
            current.push(component.as_os_str());
            if matches!(component, Component::RootDir) {
                rooted = true;
            }
            if !rooted {
                continue;
            }
            let metadata = match std::fs::symlink_metadata(&current) {
                Ok(metadata) => metadata,
                Err(error) if error.kind() == io::ErrorKind::NotFound => continue,
                Err(error) => return Err(error),
            };
            if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 || !metadata.is_dir()
            {
                return Err(io::Error::new(
                    io::ErrorKind::PermissionDenied,
                    format!(
                        "trusted Windows path contains a redirect or non-directory component: {}",
                        current.display()
                    ),
                ));
            }
            let (handle, identity) = if let Some(parent) = components.last() {
                open_directory_identity_relative(
                    parent.handle.0,
                    component.as_os_str(),
                    current == path,
                )?
            } else if current == path {
                open_locked_directory(&current)?
            } else {
                open_directory_identity(&current, true)?
            };

View on GitHub (pinned to affd8760f4)

Solutions

  1. Replace the symlink/junction component with a real directory, or capture the guard for the resolved real path.
  2. Resolve the path first (e.g. `std::fs::canonicalize` minus the final symlink) and pass the physical location to capture.
  3. Disable folder redirection (OneDrive Known Folder Move) for the directory used as the authority boundary, or choose a location outside redirected trees.
  4. Audit the printed component path to identify exactly which ancestor is the reparse point.

Example fix

// before
let install = home.join("MyApp"); // home is a OneDrive-junctioned path
let guard = TrustedPathGuard::capture(install.as_path())?; // PermissionDenied

// after
let install = std::env::var_os("LOCALAPPDATA")
    .map(PathBuf::into)
    .unwrap_or_else(|| home.join("AppData\\Local"))
    .join("MyApp"); // real, non-reparse directory
let guard = TrustedPathGuard::capture(install.as_path())?;
Defensive patterns

Strategy: validation

Validate before calling

fn check_no_reparse_ancestors(path: &Path) -> std::io::Result<()> {
    let mut current = PathBuf::new();
    for component in path.components() {
        current.push(component.as_os_str());
        if let Ok(meta) = std::fs::symlink_metadata(&current) {
            if meta.is_symlink() || !meta.is_dir() {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::PermissionDenied,
                    format!("{} is a reparse point or not a directory", current.display()),
                ));
            }
        }
    }
    Ok(())
}

Type guard

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

Try / catch

match TrustedPathGuard::capture(&path) {
    Err(e) if e.kind() == io::ErrorKind::PermissionDenied
        && e.to_string().contains("redirect or non-directory component") => {
        eprintln!("resolve {} to a real directory chain first", path.display());
    }
    other => other?,
}

Prevention

When it happens

Trigger: Capturing a guard for a path where an ancestor (or the boundary itself) is a symlink/junction — e.g. `C:\Users\me\link\app` where `link` is a junction, or a per-user directory redirected by OneDrive/Dropbox placeholders, or a profile path containing a mounted folder.

Common situations: Home directories redirected to OneDrive (placeholder reparse points); junctioned `C:\Users\<user>` profiles created by migrations; junction-based workspace setups (`mklink /J`) in development environments; running from a substituted drive (`subst`).

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


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