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

layout staging path is redirected: {}

Error message

layout staging path is redirected: {}

What it means

Before writing, atomic_write stages to parent/.{name}.next. If that staging path exists but is NOT a regular file (symlink, directory, etc.), the library refuses to touch it and raises InvalidData "layout staging path is redirected: {path}". This prevents an attacker (or a misconfiguration) from redirecting the atomic write through a symlink to an arbitrary location.

Source

Thrown at crates/astrid-core/src/dirs_layout.rs:534

        let parent = path.parent().ok_or_else(|| {
            io::Error::new(io::ErrorKind::InvalidInput, "layout record has no parent")
        })?;
        std::fs::create_dir_all(parent)?;
        let name = path
            .file_name()
            .and_then(|name| name.to_str())
            .ok_or_else(|| {
                io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "layout record has no file name",
                )
            })?;
        let staged = parent.join(format!(".{name}.next"));
        match std::fs::symlink_metadata(&staged) {
            Ok(metadata) if metadata.file_type().is_file() => std::fs::remove_file(&staged)?,
            Ok(_) => {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidData,
                    format!("layout staging path is redirected: {}", staged.display()),
                ));
            },
            Err(error) if error.kind() == io::ErrorKind::NotFound => {},
            Err(error) => return Err(error),
        }
        let mut file = OpenOptions::new()
            .write(true)
            .create_new(true)
            .mode(0o600)
            .open(&staged)?;
        file.write_all(bytes)?;
        file.sync_all()?;
        crate::platform_fs::rename_with_write_through(&staged, path)?;
        File::open(parent)?.sync_all()
    }

View on GitHub (pinned to affd8760f4)

Solutions

  1. Inspect the reported staged path; remove or replace the non-file entry (e.g. rm the symlink) so it is either absent or a regular file.
  2. Investigate how the symlink appeared — treat it as a possible tampering attempt and audit directory permissions (avoid world-writable layout dirs).
  3. Re-run the layout write after cleaning the staging path; the library will then remove a plain stale file itself.
Defensive patterns

Strategy: try-catch

Validate before calling

let staged = parent.join(format!(".{}.next", name));
if let Ok(md) = std::fs::symlink_metadata(&staged) {
    assert!(md.file_type().is_file(), "staging path {} is not a plain file", staged.display());
}

Type guard

fn staging_path_is_clean(parent: &Path, name: &str) -> bool {
    let staged = parent.join(format!(".{name}.next"));
    match std::fs::symlink_metadata(&staged) {
        Ok(md) => md.file_type().is_file(),
        Err(_) => true, // NotFound is fine
    }
}

Try / catch

match write_layout_version(path, record) {
    Err(e) if e.kind() == io::ErrorKind::InvalidData && e.to_string().contains("staging path is redirected") => {
        eprintln!("possible symlink tampering: audit directory permissions and remove the staged entry");
    },
    other => other?,
}

Prevention

When it happens

Trigger: A file or symlink named .{name}.next exists next to the target layout record and symlink_metadata shows it is not a plain file, so the Ok(_) arm returns the formatted InvalidData error.

Common situations: A leftover symlink .layout-v2.json.next planted by an attacker in a world-writable directory; a previous crashed run left a directory at the staging name; security tooling replaced the temp file with a symlink.

Understand the failure class

Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.

Related errors


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