astrid-runtime/astrid · error
PermissionDenied
PermissionDenied
Error message
source executable identity changed while staging
What it means
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.
Source
Thrown at crates/astrid-core/src/platform_fs/windows/io.rs:271
drop(output);
if let Err(error) = result {
let _ = remove_guarded_file(destination_guard, &destination);
return Err(error);
}
if let Err(error) = validate_file_contract(
source_file.as_raw_handle().cast(),
source_path,
source_file_contract,
) {
let _ = remove_guarded_file(destination_guard, &destination);
return Err(error);
}
source_guard.verify_contract(source_boundary_contract)?;
match file_identity(&source_file) {
Ok(identity) if identity == source_identity => {},
Ok(_) => {
let _ = remove_guarded_file(destination_guard, &destination);
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"source executable identity changed while staging",
));
},
Err(error) => {
let _ = remove_guarded_file(destination_guard, &destination);
return Err(error);
},
}
destination_guard.verify()?;
cleanup.disarm();
Ok((destination, source_hash))
}
pub(super) fn stage_unique_bytes(
guard: &TrustedPathGuard,
parent: &Path,
bytes: &[u8],View on GitHub (pinned to affd8760f4)
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.
Example fix
// before: two processes racing on the same install dir
update_transaction(source, install_dir); // source replaced concurrently -> identity check fails
// after: serialize with a cross-process lock
let _lock = fslock::LockFile::open(&install_dir.join(".update.lock"))?;
_lock.lock()?;
update_transaction(source, install_dir); // no concurrent replace possible Defensive patterns
Strategy: try-catch
Validate before calling
// Rust: detect concurrent writers on the install dir before staging
let lock = fslock::LockFile::open(&install_dir.join(".update.lock"))?;
if lock.try_lock().is_err() {
return Err(io::Error::new(io::ErrorKind::AlreadyExists, "another updater is running"));
} Try / catch
// Rust
match stage_transaction_copy(/* args */) {
Ok(staged) => use_staged(staged),
Err(e) if e.kind() == io::ErrorKind::PermissionDenied
&& e.to_string().contains("source executable identity changed") => {
// source was replaced mid-copy; wait for the other writer then retry
std::thread::sleep(Duration::from_millis(500));
retry_transaction();
}
Err(e) => return Err(e),
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- InvalidData
- AlreadyExists
- PermissionDenied
- capsule source disappeared while preparing replacement: {}
- mountpoint must be absolute
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/286cc92e9201eccf.
Report an issue: GitHub.