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
- 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.
- Investigate how the symlink appeared — treat it as a possible tampering attempt and audit directory permissions (avoid world-writable layout dirs).
- 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
- Keep layout directories writable only by the service user (avoid world-writable dirs)
- Clean up .next staging files from crashed runs during startup
- Alert on symlinks appearing next to layout records — treat as a security signal
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
- leftover capsule authority receipt is not a regular file: {}
- legacy capsule authority root is not a regular directory: {}
- legacy capsule authority root contains a non-regular entry:
- legacy capsule authority root is not a regular directory: {}
- directory symlink {} not allowed in capsule source tree (ref
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/8baef2189f22291c.
Report an issue: GitHub.