{"record":{"id":"ff6a93abecc5e257","repo":"jdx/mise","slug":"brew-cask-temporary-artifact-directory-was-replac-ff6a93","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/mod.rs","lineNumber":1895,"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":1877,"sourceCodeEnd":1913,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/packages/brew/cask/mod.rs#L1877-L1913","documentation":"This is an inode-identity integrity check during atomic artifact linking. The code first `fstatat`s the staging directory when it binds it (`bound`), and just before removing it re-`fstatat`s the same name with `AT_SYMLINK_NOFOLLOW`; if `st_dev`/`st_ino` no longer match, the original staging directory was unlinked and replaced by something else at the same path, so the operation bails instead of operating on the imposter. This prevents an attacker from swapping the staging directory between bind and unlink.","triggerScenarios":"A brew-cask operation using the staging/unlink sequence at mod.rs:1895 where the `staging_name` entry under `parent` was removed and recreated (different inode) between the initial bind and the final `unlinkat` — e.g. by a concurrent process, a cleanup job, or an attacker exploiting a writable parent directory.","commonSituations":"Another brew process or tmp-watcher deleting/recreating the staging directory mid-install; running two conflicting cask operations in parallel on the same prefix; a security-hardened environment where some other tool sanitizes the temporary area during the operation.","solutions":["Re-run the operation once, ensuring no other brew/cask process touches the Homebrew prefix concurrently.","Disable or check any background cleanup (temp sweeper, antivirus, periodic scripts) acting on the staging area.","If it reproduces, verify ownership/permissions of the staging parent directory — a replaced directory implies someone else can write there; fix permissions before retrying.","Treat repeated occurrences as a security signal: audit the prefix for untrusted write access."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"use nix::sys::stat::fstatat;\nfn staging_dir_stable(parent: &std::fs::File, name: &std::ffi::CStr, bound: &nix::sys::stat::FileStat) -> bool {\n    fstatat(parent, name, nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW)\n        .map(|s| s.st_dev == bound.st_dev && s.st_ino == bound.st_ino)\n        .unwrap_or(false)\n}\n","typeGuard":null,"tryCatchPattern":"match result {\n    Err(e) if e.to_string().contains(\"temporary artifact directory was replaced\") => {\n        // retry once with no concurrent processes; abort if it recurs\n    }\n    Err(e) => return Err(e),\n    Ok(v) => v,\n}\n","preventionTips":["Do not run parallel brew operations or temp-cleaning jobs during a cask install.","Keep the staging parent directory root-owned and un-writable by others.","Treat repeated occurrences as a security signal and audit prefix permissions."],"tags":["security","race-condition","toctou","integrity-check"],"backgroundTag":"internal-invariant-violation","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}