Hmbown/CodeWhale · error

provider catalog lock

Error message

provider catalog lock {} must not be a reparse point

What it means

open_cache_lock verifies that the provider catalog lock file on Windows is not a reparse point (symlink, junction, mount point). Reparse points could redirect the lock to an attacker-controlled or unexpected location, so the code refuses to open such a lock file to keep the cache directory trustworthy.

Solutions

  1. Remove the symlink/junction at the lock path and replace it with a real file (delete and let the app recreate the lock)
  2. Point the cache/config directory at a real, non-reparse location instead of a symlinked folder
  3. Copy the cache directory contents to a physical directory and update any redirection
  4. If the path is on a subst/mounted volume, relocate the catalog cache to a normal local path

Example fix

// before (broken: lock path is a junction)
C:\Users\me\.codewhale\cache\catalog.lock -> D:\cache\catalog.lock
// after
mklink /j removed; real file C:\Users\me\.codewhale\cache\catalog.lock recreated by the app
Defensive patterns

Strategy: validation

Validate before calling

#[cfg(windows)]
fn is_reparse_point(path: &Path) -> std::io::Result<bool> {
    use std::os::windows::fs::MetadataExt;
    const REPARSE: u32 = 0x0000_0400;
    Ok(std::fs::symlink_metadata(path)?.file_attributes() & REPARSE != 0)
}
// call before opening: if is_reparse_point(&lock_path)? { relocate / warn }

Try / catch

match open_cache_lock(&path) {
    Err(e) if e.to_string().contains("reparse point") => {
        // remove the symlink/junction and recreate a real lock file
    }
    Ok(f) => { /* proceed */ }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: On Windows, opening the provider catalog cache lock when the file at the cache path has FILE_ATTRIBUTE_REPARSE_POINT set — e.g. the lock file (or an ancestor) was replaced by a symlink or junction, or the cache dir lives on a mounted/substituted path.

Common situations: Users syncing their config/cache directory via symlinks (dotfile managers, OneDrive/Dropbox folder redirection), moving the cache dir with junctions, or CI setups that substitute drives.

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 Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/a0b60a2dd2b8c054. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/provider_catalog_live.rs:555

    anyhow::ensure!(
        metadata.is_file(),
        "provider catalog lock {} must be a regular file",
        path.display()
    );
    #[cfg(unix)]
    {
        use std::os::unix::fs::MetadataExt as _;
        anyhow::ensure!(
            metadata.nlink() == 1,
            "provider catalog lock {} must not be hard linked",
            path.display()
        );
    }
    #[cfg(windows)]
    {
        use std::os::windows::fs::MetadataExt as _;
        const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
        anyhow::ensure!(
            metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT == 0,
            "provider catalog lock {} must not be a reparse point",
            path.display()
        );
    }
    Ok(file)
}

fn load_from_disk_unlocked_with_limit(path: &Path, max_bytes: u64) -> Option<ProviderCatalogCache> {
    let mut options = OpenOptions::new();
    options.read(true);
    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt as _;
        options.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK);
    }
    #[cfg(windows)]
    {

View on GitHub (pinned to 73e0f67d83)