astrid-runtime/astrid · error

legacy source changed while opening: {}

Error message

legacy source changed while opening: {}

What it means

After opening the legacy source with O_NOFOLLOW, `read_bounded_file` compares the opened file's metadata (type and length) against the symlink metadata captured earlier and throws this error if they disagree. The library throws it to detect a race where the file changed (grew, shrank, or was replaced by a different non-file) between the initial stat and the open.

Source

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

    astrid_core::platform_fs::validate_private_file(path)?;
    if metadata.len() > max {
        return Err(io::Error::other(format!(
            "legacy source exceeds migration cap: {}",
            path.display()
        )));
    }
    astrid_core::platform_fs::verify_no_redirects(path)?;
    let mut options = OpenOptions::new();
    options.read(true);
    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt as _;
        options.custom_flags(nix::libc::O_NOFOLLOW | nix::libc::O_CLOEXEC);
    }
    let mut file = options.open(path)?;
    let opened = file.metadata()?;
    if !opened.is_file() || opened.len() != metadata.len() {
        return Err(io::Error::new(
            io::ErrorKind::AlreadyExists,
            format!("legacy source changed while opening: {}", path.display()),
        ));
    }
    let capacity = usize::try_from(opened.len())
        .map_err(|_| io::Error::other("legacy source is too large for this platform"))?;
    let mut bytes = Vec::with_capacity(capacity);
    file.read_to_end(&mut bytes)?;
    Ok(Some(bytes))
}

pub(super) fn sync_parent(path: &Path) -> io::Result<()> {
    if let Some(parent) = path.parent() {
        sync_directory(parent)?;
    }
    Ok(())
}

View on GitHub (pinned to affd8760f4)

Solutions

  1. Quiesce all processes that write to the legacy directory before migrating (stop services, pause sync tools).
  2. Re-run the migration once the legacy source is stable; the metadata comparison should then match.
  3. If it persists, verify no one is tampering: compare inode/size with `stat` across runs.

Example fix

// before: migration runs while a sync daemon rewrites the source
$ astrid migrate  # fails: legacy source changed while opening
// after: pause the writer, then migrate
$ systemctl stop dropbox-sync && astrid migrate
Defensive patterns

Strategy: retry

Try / catch

let mut attempts = 0;
loop {
    match read_bounded_file(path, MAX) {
        Err(e) if e.to_string().contains("changed while opening") && attempts < 3 => {
            attempts += 1;
            std::thread::sleep(Duration::from_millis(200));
        }
        r => break r,
    }
}

Prevention

When it happens

Trigger: A concurrent writer or attacker modifies the legacy file's length or swaps the file between the initial `symlink_metadata` call and the guarded `options.open` inside read_bounded_file.

Common situations: Another process (sync client, editor, package manager) writes to the legacy directory while migration runs; deliberate tampering during a privileged migration; retrying migration while a background job rewrites the source.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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