{"record":{"id":"d19801dff1899b4c","repo":"jdx/mise","slug":"brew-cask-temporary-artifact-directory-was-replac","errorCode":null,"errorMessage":"brew-cask: temporary artifact directory was replaced","messagePattern":"brew-cask: temporary artifact directory was replaced","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":2047,"sourceCode":"        nix::unistd::unlinkat(parent, name, nix::unistd::UnlinkatFlags::NoRemoveDir)?;\n    }\n    Ok(())\n}\n\n#[cfg(unix)]\nfn remove_private_staging_dir(\n    parent: &TrustedOperationParent,\n    staging: &TrustedOperationParent,\n    staging_name: &std::ffi::OsStr,\n) -> Result<()> {\n    let bound = nix::sys::stat::fstat(&staging.fd)?;\n    let linked = nix::sys::stat::fstatat(\n        &parent.fd,\n        staging_name,\n        nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW,\n    )?;\n    if bound.st_dev != linked.st_dev || bound.st_ino != linked.st_ino {\n        bail!(\"brew-cask: temporary artifact directory was replaced\");\n    }\n    nix::unistd::unlinkat(\n        &parent.fd,\n        staging_name,\n        nix::unistd::UnlinkatFlags::RemoveDir,\n    )?;\n    Ok(())\n}\n\nfn validate_generic_copy_target(target: &Path) -> Result<()> {\n    let prefix = prefix::prefix();\n    if !target.starts_with(&prefix)\n        || target.strip_prefix(&prefix)?.components().next().is_none()\n        || !path_starts_with_resolved_root(target, &prefix)\n    {\n        bail!(\n            \"brew-cask: refusing generic artifact copy outside Homebrew prefix: {}\",\n            target.display()","sourceCodeStart":2029,"sourceCodeEnd":2065,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/system/packages/brew/cask.rs#L2029-L2065","documentation":"Thrown while deleting the private staging directory mise creates inside the Caskroom to stage cask artifacts. The code holds an open directory descriptor and fstats it, then fstats the directory currently reachable at the staging name (AT_SYMLINK_NOFOLLOW). If the (st_dev, st_ino) pairs differ, the directory at that path was replaced after it was opened, so the unlinkat is refused instead of deleting whatever now lives at that name.","triggerScenarios":"A cask install/uninstall run in which another process removes and recreates the staging directory (or swaps a symlink in its place) between staging-dir creation and cleanup: a second mise/brew cask process on the same Caskroom, a temp-file cleaner, or a deliberate symlink-swap race this check exists to catch.","commonSituations":"Two concurrent cask operations in one Caskroom; cleanup/'optimizer' tools purging staging trees mid-install; a previously crashed run leaving a different directory at the staging name; Caskroom on a network filesystem where inode identity is unstable.","solutions":["Make sure no other mise/brew cask process is running against the same Caskroom, then retry the command","Inspect the staging parent under the Caskroom, delete the leftover/replaced staging directory manually, and re-run","Disable third-party cleanup or antivirus tools that touch the Caskroom during installs","If it reproduces with nothing else running, capture the staging path and report it: it can indicate a local security issue or an exotic filesystem"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"use std::os::unix::io::AsRawFd;\nuse nix::fcntl::{flock, FlockArg};\n\n// Serialize cask operations per Caskroom so no second process can swap staging dirs\nfn lock_caskroom(caskroom: &std::path::Path) -> std::fs::File {\n    let lock = std::fs::OpenOptions::new()\n        .create(true).append(true)\n        .open(caskroom.join(\".mise-cask.lock\")).expect(\"open lock\");\n    flock(lock.as_raw_fd(), FlockArg::LockExclusive).expect(\"lock caskroom\");\n    lock // keep alive for the whole install/uninstall\n}","typeGuard":null,"tryCatchPattern":"match run_cask_op(&cask) {\n    Err(e) if e.to_string().contains(\"temporary artifact directory was replaced\") => {\n        // transient race: wait for the concurrent operation, then retry once\n        std::thread::sleep(std::time::Duration::from_secs(2));\n        run_cask_op(&cask)?\n    }\n    other => other?,\n}","preventionTips":["Never run two mise/brew cask operations against the same Caskroom at once","Keep the Caskroom on a local filesystem (inode identity is unreliable on NFS/SMB)","Exclude the Caskroom from temp-cleaning and antivirus tools"],"tags":["brew-cask","toctou","staging","race-condition","security"],"backgroundTag":"toctou-race-condition","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}