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

  1. Ensure only one updater/installer instance runs at a time — add a process-level or directory lock around the transaction.
  2. Re-run the staging transaction after the concurrent replace has finished; the retry will open the new file coherently.
  3. Exclude the install directory from cloud-sync/backup tools that replace files atomically.
  4. 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

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


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