astrid-runtime/astrid · error

legacy source crosses a mount or device boundary: {}

Error message

legacy source crosses a mount or device boundary: {}

What it means

During legacy tree retirement, retire_tree walks each child of the legacy source directory and refuses to descend into any child that is itself an active mountpoint or whose st_dev differs from the parent directory's device. The migration path intentionally only sweeps regular files and directories that live wholly on one filesystem, because a bind mount or cross-device subtree cannot be safely renamed/removed as part of the same atomic retirement. The path of the offending child is interpolated into the message.

Source

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

                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)?;
            retire_tree(&child, &child_snapshot, protected)?;
        } else {
            astrid_core::platform_fs::verify_no_redirects(&child)?;
            let leaf_snapshot = snapshot_path(&child)?;
            if leaf_snapshot.entries != 1 || leaf_snapshot.bytes != child_meta.len() {
                return Err(io::Error::new(
                    io::ErrorKind::AlreadyExists,
                    format!(
                        "legacy source changed before retirement: {}",

View on GitHub (pinned to affd8760f4)

Solutions

  1. Unmount (umount) the mountpoint located at the reported child path before re-running the migration.
  2. Move the mounted data off the legacy tree (e.g. mount elsewhere and use a symlink outside the migrated root), then re-run migration.
  3. If the mount is stale, remove it from /etc/fstab or the container mount configuration so it no longer appears in the tree.
  4. If the child is merely on another device but not mounted, consolidate it onto the same filesystem as its parent.

Example fix

// before: bind mount inside legacy source
/home/u/.local/share/cache on /home/u/.local/audit type tmpfs
// after
$ umount /home/u/.local/audit
$ astrid migrate-legacy-audit   # proceeds past the boundary check
Defensive patterns

Strategy: validation

Validate before calling

use std::os::unix::fs::MetadataExt;
fn precheck_child_mounts(root: &std::path::Path) -> std::io::Result<()> {
    let root_dev = std::fs::symlink_metadata(root)?.dev();
    for e in std::fs::read_dir(root)? {
        let p = e?.path();
        let m = std::fs::symlink_metadata(&p)?;
        if m.is_dir() && m.dev() != root_dev {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                format!("cross-device child: {}", p.display()),
            ));
        }
    }
    Ok(())
}

Type guard

fn same_device(parent_dev: u64, child: &std::path::Path) -> std::io::Result<bool> {
    Ok(std::os::unix::fs::MetadataExt::dev(&std::fs::symlink_metadata(child)?) == parent_dev)
}

Try / catch

match result {
    Err(e) if e.kind() == std::io::ErrorKind::InvalidData && e.to_string().contains("mount or device boundary") => {
        // prompt user to unmount the reported path, then retry
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling migrate_legacy_audit (which reaches retire_tree via retire_post_barrier_sources) while a direct child of the legacy principal-home tree is a mountpoint, a bind mount, an NFS/overlay mount, or a directory on a different device (st_dev differs from the parent). Also triggered by tests retirement_rejects_source_mutation_and_preserves_data-style harnesses that mount something inside the source tree.

Common situations: Users who mount a network share, tmpfs, or extra disk under ~/.local or the principal home directory; container images with overlayfs bind mounts inside the home; developers testing with mounted fixtures inside the legacy source.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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