astrid-runtime/astrid · error · io::Error
layout migration receipt is redirected or not a regular file
Error message
layout migration receipt is redirected or not a regular file: {} What it means
retire_verified_legacy_source checks the completion receipt at var/migrations/layout-v1-to-v2.complete with symlink_metadata and requires it to be a regular file, not a symlink and not a directory or other special entry. This is a no-follow security check: a redirected receipt path could make the library read attacker-controlled migration records or delete the wrong tree. Returned as InvalidData with the offending path in the message.
Source
Thrown at crates/astrid-core/src/dirs_layout.rs:345
pub(super) fn retire_verified_legacy_source(&self) -> io::Result<()> {
let receipt_path = self.migrations_dir().join(LAYOUT_MIGRATION_RECEIPT);
let intent_path = self.migrations_dir().join(LAYOUT_MIGRATION_INTENT);
match std::fs::symlink_metadata(&receipt_path) {
Err(error) if error.kind() == io::ErrorKind::NotFound => {
if path_entry_present(&self.state_db_path())?
|| path_entry_present(&self.cow_dir())?
|| path_entry_present(&intent_path)?
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"layout-two home contains legacy state without a completion receipt",
));
}
return Ok(());
},
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"layout migration receipt is redirected or not a regular file: {}",
receipt_path.display()
),
));
},
Ok(_) => {},
Err(error) => return Err(error),
}
let receipt: LayoutMigrationReceiptV1 = read_canonical_record(&receipt_path)?;
let intent: LayoutMigrationRecordV1 = read_canonical_record(&intent_path)?;
let retirement_path = self.migrations_dir().join(LAYOUT_MIGRATION_RETIREMENT);
let retirement: Option<LayoutRetirementV1> = match read_canonical_record(&retirement_path) {
Ok(retirement) => Some(retirement),
Err(error) if error.kind() == io::ErrorKind::NotFound => None,
Err(error) => return Err(error),View on GitHub (pinned to affd8760f4)
Solutions
- Inspect var/migrations/layout-v1-to-v2.complete with ls -la: if it is a symlink or directory, remove it and restore the original regular receipt file from backup
- Re-run the v1→v2 migration from a clean legacy sentinel state so a fresh, genuine receipt is written
- Audit the var/migrations/ directory for tampering; ensure it is a private (0700) directory owned by the Astrid user
- Never hand-create the receipt as a symlink or placeholder — it must be a canonical regular file written by the migration itself
Example fix
// before: receipt is a symlink ls -la ~/.astrid/var/migrations/layout-v1-to-v2.complete # -> symlink home.complete_layout_v2(&target)?; // InvalidData: redirected receipt // after rm ~/.astrid/var/migrations/layout-v1-to-v2.complete # remove redirect # restore genuine receipt from backup or re-run migration from legacy sentinel
Defensive patterns
Strategy: validation
Validate before calling
use std::os::unix::fs::FileTypeExt;
let md = std::fs::symlink_metadata(&receipt_path)?;
if md.file_type().is_symlink() || !md.is_file() {
return Err(anyhow!("receipt is redirected or not a regular file"));
} Type guard
fn is_regular_no_follow(p: &Path) -> bool {
std::fs::symlink_metadata(p).map(|m| m.is_file() && !m.file_type().is_symlink()).unwrap_or(false)
} Try / catch
match home.complete_layout_v2(&target) {
Err(e) if e.to_string().contains("redirected or not a regular file") => {
// remove the symlink/odd entry and restore the genuine receipt
},
r => r?,
} Prevention
- Never place symlinks anywhere inside the Astrid home
- Keep var/migrations/ private (0700) and owned by the service account
- After restores, audit for symlinks: find ~/.astrid -type l
- Re-run migration from legacy sentinel state instead of recreating receipts by hand
When it happens
Trigger: Calling complete_layout_v2 when the receipt path is a symlink (e.g. someone linked it to /tmp or another volume), a directory, a FIFO/device node, or was replaced by any non-regular file.
Common situations: Malicious or careless symlinking inside var/migrations/; a restore tool that recreated the receipt as a symlink or directory; overlay/bind mounts presenting the receipt as something other than a regular file.
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
- layout path is redirected or not a directory: {}
- legacy state source is redirected: {}
- legacy state source contains a redirect: {}
- private path is redirected: {}
- legacy principal home root is not a regular directory: {}
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/f13df790793b2ddd.
Report an issue: GitHub.