{"record":{"id":"39d606a41f913f42","repo":"astrid-runtime/astrid","slug":"legacy-source-changed-while-opening","errorCode":null,"errorMessage":"legacy source changed while opening: {}","messagePattern":"legacy source changed while opening: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs","lineNumber":954,"sourceCode":"    astrid_core::platform_fs::validate_private_file(path)?;\n    if metadata.len() > max {\n        return Err(io::Error::other(format!(\n            \"legacy source exceeds migration cap: {}\",\n            path.display()\n        )));\n    }\n    astrid_core::platform_fs::verify_no_redirects(path)?;\n    let mut options = OpenOptions::new();\n    options.read(true);\n    #[cfg(unix)]\n    {\n        use std::os::unix::fs::OpenOptionsExt as _;\n        options.custom_flags(nix::libc::O_NOFOLLOW | nix::libc::O_CLOEXEC);\n    }\n    let mut file = options.open(path)?;\n    let opened = file.metadata()?;\n    if !opened.is_file() || opened.len() != metadata.len() {\n        return Err(io::Error::new(\n            io::ErrorKind::AlreadyExists,\n            format!(\"legacy source changed while opening: {}\", path.display()),\n        ));\n    }\n    let capacity = usize::try_from(opened.len())\n        .map_err(|_| io::Error::other(\"legacy source is too large for this platform\"))?;\n    let mut bytes = Vec::with_capacity(capacity);\n    file.read_to_end(&mut bytes)?;\n    Ok(Some(bytes))\n}\n\npub(super) fn sync_parent(path: &Path) -> io::Result<()> {\n    if let Some(parent) = path.parent() {\n        sync_directory(parent)?;\n    }\n    Ok(())\n}\n","sourceCodeStart":936,"sourceCodeEnd":972,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs#L936-L972","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Quiesce all processes that write to the legacy directory before migrating (stop services, pause sync tools).","Re-run the migration once the legacy source is stable; the metadata comparison should then match.","If it persists, verify no one is tampering: compare inode/size with `stat` across runs."],"exampleFix":"// before: migration runs while a sync daemon rewrites the source\n$ astrid migrate  # fails: legacy source changed while opening\n// after: pause the writer, then migrate\n$ systemctl stop dropbox-sync && astrid migrate","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"let mut attempts = 0;\nloop {\n    match read_bounded_file(path, MAX) {\n        Err(e) if e.to_string().contains(\"changed while opening\") && attempts < 3 => {\n            attempts += 1;\n            std::thread::sleep(Duration::from_millis(200));\n        }\n        r => break r,\n    }\n}","preventionTips":["Stop all writers (services, sync clients, editors) touching the legacy directory before migrating.","Run migration while the system is quiescent (single-user/rescue mode for system-level migration).","If it recurs, check for concurrent processes with `fuser -v <path>` or lsof."],"tags":["io","filesystem","race-condition","security"],"backgroundTag":"invalid-state-transition","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}