{"record":{"id":"e9045e26af095b63","repo":"astrid-runtime/astrid","slug":"layout-migration-source-changed-type","errorCode":null,"errorMessage":"layout migration source changed type: {}","messagePattern":"layout migration source changed type: (.+?)","errorType":"exception","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/astrid-core/src/dirs_layout_records.rs","lineNumber":414,"sourceCode":"            hasher.update(&metadata.len().to_le_bytes());\n            let mut options = OpenOptions::new();\n            options.read(true);\n            #[cfg(unix)]\n            {\n                use std::os::unix::fs::OpenOptionsExt as _;\n\n                options.custom_flags(nix::libc::O_NOFOLLOW | nix::libc::O_NONBLOCK);\n            }\n            #[cfg(windows)]\n            {\n                use std::os::windows::fs::OpenOptionsExt as _;\n                use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;\n\n                options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);\n            }\n            let mut file = options.open(&child_path)?;\n            if !file.metadata()?.is_file() {\n                return Err(io::Error::new(\n                    io::ErrorKind::InvalidData,\n                    format!(\n                        \"layout migration source changed type: {}\",\n                        child_path.display()\n                    ),\n                ));\n            }\n            let mut buffer = vec![0_u8; 64 * 1024].into_boxed_slice();\n            let mut file_bytes = 0_u64;\n            loop {\n                let read = file.read(&mut buffer)?;\n                if read == 0 {\n                    break;\n                }\n                *bytes = bytes\n                    .checked_add(read as u64)\n                    .ok_or_else(|| io::Error::other(\"layout inventory byte count overflow\"))?;\n                file_bytes = file_bytes","sourceCodeStart":396,"sourceCodeEnd":432,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-core/src/dirs_layout_records.rs#L396-L432","documentation":"While inventorying the migration source, the library opens each regular file with no-follow flags (O_NOFOLLOW / FILE_FLAG_OPEN_REPARSE_POINT) and re-checks the opened file's metadata. If the opened handle is no longer a regular file (e.g. the entry was swapped for a symlink or fifo between the initial readdir stat and the open), it aborts instead of hashing unexpected content. This is a TOCTOU guard protecting the integrity of the migration inventory digest.","triggerScenarios":"inventory_directory opens a child with O_NOFOLLOW/REPARSE_POINT flags and file.metadata()?.is_file() is false — the entry changed from a regular file to something else (symlink, fifo, socket) between the directory scan and the open.","commonSituations":"Another process (backup, editor, sync daemon like Dropbox/Nextcloud) rewrites or replaces files mid-migration; a user manually deletes/recreates an entry as a symlink while a migration is running.","solutions":["Stop the process that is modifying the state directory and re-run the migration when the tree is quiescent.","Check the reported path and restore it to a regular file with the expected content.","Exclude the state directory from live-sync/backup tools, or pause them during migration.","Retry the migration; the error is transient if it was caused by a concurrent swap."],"exampleFix":"// before: rsync/backup daemon rewriting state files during migration\n* * * * * rsync -a --delete ~/live/ ~/.local/state/astrid/\n// after: pause syncs while migrating\nsystemctl --user stop backup.timer && astrid migrate && systemctl --user start backup.timer","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"match result {\n    Err(e) if e.to_string().contains(\"layout migration source changed type\") => {\n        // pause writers/sync tools, then retry the migration\n        retry_with_backoff(|| run_migration(), 3);\n    }\n    Err(e) => return Err(e),\n    Ok(v) => v,\n}","preventionTips":["Fully stop the application and any sync/backup daemons touching the state directory before migrating.","Run migrations during maintenance windows when nothing writes to the tree.","Exclude the state directory from live file-sync services."],"tags":["toctou","filesystem","migration","race-condition"],"backgroundTag":"incompatible-source-type","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}