{"record":{"id":"286cc92e9201eccf","repo":"astrid-runtime/astrid","slug":"permissiondenied-286cc9","errorCode":"PermissionDenied","errorMessage":"source executable identity changed while staging","messagePattern":"source executable identity changed while staging","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-core/src/platform_fs/windows/io.rs","lineNumber":271,"sourceCode":"    drop(output);\n    if let Err(error) = result {\n        let _ = remove_guarded_file(destination_guard, &destination);\n        return Err(error);\n    }\n    if let Err(error) = validate_file_contract(\n        source_file.as_raw_handle().cast(),\n        source_path,\n        source_file_contract,\n    ) {\n        let _ = remove_guarded_file(destination_guard, &destination);\n        return Err(error);\n    }\n    source_guard.verify_contract(source_boundary_contract)?;\n    match file_identity(&source_file) {\n        Ok(identity) if identity == source_identity => {},\n        Ok(_) => {\n            let _ = remove_guarded_file(destination_guard, &destination);\n            return Err(io::Error::new(\n                io::ErrorKind::PermissionDenied,\n                \"source executable identity changed while staging\",\n            ));\n        },\n        Err(error) => {\n            let _ = remove_guarded_file(destination_guard, &destination);\n            return Err(error);\n        },\n    }\n    destination_guard.verify()?;\n    cleanup.disarm();\n    Ok((destination, source_hash))\n}\n\npub(super) fn stage_unique_bytes(\n    guard: &TrustedPathGuard,\n    parent: &Path,\n    bytes: &[u8],","sourceCodeStart":253,"sourceCodeEnd":289,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-core/src/platform_fs/windows/io.rs#L253-L289","documentation":"Raised after a staged copy completes: the library snapshots the source file's identity (file ID/volume serial) when it opens the locked source handle and re-checks it once staging finishes. If the identity no longer matches, the source executable was replaced (delete/rename/recreate) during staging, so the staged bytes may not correspond to a coherent trusted file. The staged copy is deleted and PermissionDenied is returned to abort the transaction.","triggerScenarios":"In stage_transaction_copy_authenticated, after `source_guard.verify_contract(...)` succeeds, `file_identity(&source_file)` returns an identity different from the one captured at open time. Caused by the source path's file being renamed away and a new file created in its place, an atomic replace (POSIX-semantics rename over the source), or deletion+recreation while the copy ran.","commonSituations":"Two update processes running concurrently against the same install directory; package managers or deploy tools atomically swapping binaries while this transaction stages; cloud-sync (OneDrive/Dropbox) re-downloading and replacing the source file; a CI/CD agent redeploying binaries mid-update.","solutions":["Ensure only one updater/installer instance runs at a time — add a process-level or directory lock around the transaction.","Re-run the staging transaction after the concurrent replace has finished; the retry will open the new file coherently.","Exclude the install directory from cloud-sync/backup tools that replace files atomically.","If this recurs, check for other tooling (deploy scripts, package managers) touching the same source path and serialize with them."],"exampleFix":"// before: two processes racing on the same install dir\nupdate_transaction(source, install_dir); // source replaced concurrently -> identity check fails\n// after: serialize with a cross-process lock\nlet _lock = fslock::LockFile::open(&install_dir.join(\".update.lock\"))?;\n_lock.lock()?;\nupdate_transaction(source, install_dir); // no concurrent replace possible","handlingStrategy":"try-catch","validationCode":"// Rust: detect concurrent writers on the install dir before staging\nlet lock = fslock::LockFile::open(&install_dir.join(\".update.lock\"))?;\nif lock.try_lock().is_err() {\n    return Err(io::Error::new(io::ErrorKind::AlreadyExists, \"another updater is running\"));\n}","typeGuard":null,"tryCatchPattern":"// Rust\nmatch stage_transaction_copy(/* args */) {\n    Ok(staged) => use_staged(staged),\n    Err(e) if e.kind() == io::ErrorKind::PermissionDenied\n        && e.to_string().contains(\"source executable identity changed\") => {\n        // source was replaced mid-copy; wait for the other writer then retry\n        std::thread::sleep(Duration::from_millis(500));\n        retry_transaction();\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Serialize updates with a cross-process file lock.","Exclude install directories from cloud-sync, backup, and deploy agents that replace files atomically.","Treat PermissionDenied during staging as 'retry later', not as a fatal configuration error.","Log the source path when this fires to identify the competing process."],"tags":["windows","filesystem","race-condition","tamper-detection","staging"],"backgroundTag":"permission-denied","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"}