astrid-runtime/astrid · error

legacy source contains redirect or special entry: {}

Error message

legacy source contains redirect or special entry: {}

What it means

A child of the legacy tree being retired is a symlink or a special file (FIFO, socket, device). retire_tree only deletes plain files and directories; anything that could redirect the walk or have side effects on open is rejected with InvalidData, naming the offending child.

Source

Thrown at crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs:286

            io::ErrorKind::InvalidData,
            format!("legacy source is an active mount: {}", path.display()),
        ));
    }
    let device = device_id(&metadata);
    for entry in fs::read_dir(path).map_err(io::Error::other)? {
        let child = entry.map_err(io::Error::other)?.path();
        if protected.iter().any(|candidate| candidate == &child) {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!(
                    "legacy component source reappeared during ordinary retirement: {}",
                    child.display()
                ),
            ));
        }
        let child_meta = fs::symlink_metadata(&child).map_err(io::Error::other)?;
        if child_meta.file_type().is_symlink() || (!child_meta.is_file() && !child_meta.is_dir()) {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!(
                    "legacy source contains redirect or special entry: {}",
                    child.display()
                ),
            ));
        }
        if active_mountpoint(&child)? || device_id(&child_meta) != device {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!(
                    "legacy source crosses a mount or device boundary: {}",
                    child.display()
                ),
            ));
        }
        if child_meta.is_dir() {
            let child_snapshot = snapshot_path(&child)?;

View on GitHub (pinned to affd8760f4)

Solutions

  1. Remove or replace the reported symlink/special entry: `rm` the symlink/FIFO/socket, then `cp -a` the real file if one is needed.
  2. Stop any legacy process that may own the socket/FIFO before removing it.
  3. Verify the tree contains only regular files and directories (`find <path> ! -type f ! -type d`) before rerunning migration.
  4. Rerun the migration; the check is per-entry, so clean all offenders the message reports.

Example fix

# before
ln -s /mnt/big/archive.dat ~/.astrid/legacy/db/archive.dat
// after
rm ~/.astrid/legacy/db/archive.dat
cp -a /mnt/big/archive.dat ~/.astrid/legacy/db/archive.dat
Defensive patterns

Strategy: validation

Validate before calling

use std::fs;
fn tree_only_regular_entries(path: &std::path::Path) -> std::io::Result<Vec<std::path::PathBuf>> {
    let mut bad = Vec::new();
    for entry in walkdir_like(path) {
        let m = fs::symlink_metadata(&entry)?;
        if m.file_type().is_symlink() || (!m.is_file() && !m.is_dir()) {
            bad.push(entry);
        }
    }
    Ok(bad) // must be empty before migration
}

Type guard

fn is_plain_entry(m: &std::fs::Metadata) -> bool {
    !m.file_type().is_symlink() && (m.is_file() || m.is_dir())
}

Try / catch

match retire_tree(path, &expected, &protected) {
    Err(e) if e.to_string().contains("redirect or special entry") => {
        let child = extract_path(&e.to_string());
        eprintln!("remove or replace {child} with a regular file");
    }
    other => other?,
}

Prevention

When it happens

Trigger: retire_tree (and retire_leaf's re-check) calls symlink_metadata on each child; `child_meta.file_type().is_symlink() || (!is_file() && !is_dir())` matches — a symlink swapped in for a regular file, a leftover socket/FIFO from a legacy daemon, or a device node. A second check in retire_leaf catches entries swapped between the directory scan and unlink.

Common situations: Legacy daemons leaving Unix sockets inside their state dirs; users symlinking large files into the legacy tree to save space; backup/sync tools creating hardlink-like shortcuts; compromised or tampered trees.

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/b1cd6ae62ad103c2. Report an issue: GitHub.