jdx/mise · error
brew-cask: temporary artifact directory was replaced
Error message
brew-cask: temporary artifact directory was replaced
What it means
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.
Source
Thrown at src/system/packages/brew/cask/mod.rs:1895
nix::unistd::unlinkat(parent, name, nix::unistd::UnlinkatFlags::NoRemoveDir)?;
}
Ok(())
}
#[cfg(unix)]
fn remove_private_staging_dir(
parent: &TrustedOperationParent,
staging: &TrustedOperationParent,
staging_name: &std::ffi::OsStr,
) -> Result<()> {
let bound = nix::sys::stat::fstat(&staging.fd)?;
let linked = nix::sys::stat::fstatat(
&parent.fd,
staging_name,
nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW,
)?;
if bound.st_dev != linked.st_dev || bound.st_ino != linked.st_ino {
bail!("brew-cask: temporary artifact directory was replaced");
}
nix::unistd::unlinkat(
&parent.fd,
staging_name,
nix::unistd::UnlinkatFlags::RemoveDir,
)?;
Ok(())
}
fn validate_generic_copy_target(target: &Path) -> Result<()> {
let prefix = prefix::prefix();
if !target.starts_with(&prefix)
|| target.strip_prefix(&prefix)?.components().next().is_none()
|| !path_starts_with_resolved_root(target, &prefix)
{
bail!(
"brew-cask: refusing generic artifact copy outside Homebrew prefix: {}",
target.display()View on GitHub (pinned to afd2eddd3a)
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.
Defensive patterns
Strategy: retry
Validate before calling
use nix::sys::stat::fstatat;
fn staging_dir_stable(parent: &std::fs::File, name: &std::ffi::CStr, bound: &nix::sys::stat::FileStat) -> bool {
fstatat(parent, name, nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW)
.map(|s| s.st_dev == bound.st_dev && s.st_ino == bound.st_ino)
.unwrap_or(false)
}
Try / catch
match result {
Err(e) if e.to_string().contains("temporary artifact directory was replaced") => {
// retry once with no concurrent processes; abort if it recurs
}
Err(e) => return Err(e),
Ok(v) => v,
}
Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- brew-cask: refusing operation through a changed generic arti
- brew-cask: refusing elevated operation because target appear
- brew-cask: temporary artifact directory was replaced
- expected pre-planted symlink destination to be refused
- created path component {} was replaced before it could be op
AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09).
Data as JSON: /api/errors/ff6a93abecc5e257.
Report an issue: GitHub.