jdx/mise · critical
brew-cask: temporary artifact directory is not private
Error message
brew-cask: temporary artifact directory is not private
What it means
Privileged artifact copies stage through a temporary directory opened with O_NOFOLLOW and then fstat-verified (src/system/packages/brew/cask.rs:1905): the staging directory must be owned by the effective user and have zero group/other permission bits (mode & 0o077 == 0). If a different uid owns it or it is group/world-accessible, mise aborts — defeating symlink-swap and snooping races on a directory that elevated (sudo) operations write through.
Solutions
- Remove the stale staging directory (under the target's parent, from the failed run) and retry the install
- Check the parent directory chain's ownership and permissions — elevated copies require root-owned, non-group/world-writable parents
- If it recurs, find the process creating those dirs with wrong ownership (concurrent brew/mise runs, broken sudo wrappers) and serialize or fix it
Defensive patterns
Strategy: validation
Validate before calling
use std::os::unix::fs::MetadataExt;
let md = std::fs::symlink_metadata(&staging_dir)?;
if md.uid() != nix::unistd::geteuid().as_raw() || md.mode() & 0o077 != 0 {
return Err(anyhow!("staging directory is not private - refusing privileged copy"));
} Prevention
- Clean stale staging directories after crashed privileged installs
- Keep parents of privileged targets root-owned and not group-writable
- Serialize installs so staging directories are never created by a different uid
When it happens
Trigger: A pre-existing or raced staging directory under the target's parent that is owned by another uid or carries permissive bits — e.g. a root-created 0755 staging dir when mise runs as the user, or a group-writable 0770 dir an attacker could populate.
Common situations: Leftover staging directories from crashed privileged installs; parents with loose permissions adjacent to /Applications; multi-admin machines where another admin's run left differently-owned dirs; deliberate hardening probes in security-sensitive environments.
Related errors
- brew-cask: refusing elevated operation through mutable…
- brew-cask: refusing elevated operation through mutable…
- brew-cask: refusing operation through untrusted directory
- brew-cask: refusing operation through untrusted directory
- brew-cask: staging directory is not owned by the current…
AI-assisted analysis of jdx/mise@6f52dcdf99 (2026-08-22).
Data as JSON: /api/errors/3fcbbcf41f9ba03a.
Report an issue: GitHub.
Appendix: source
Thrown at src/system/packages/brew/cask.rs:1905
.ok_or_else(|| eyre!("brew-cask: generic artifact target has no filename"))?;
let staging_name = format!(".mise-copy-{}", crate::rand::random_string(16));
nix::sys::stat::mkdirat(
&parent.fd,
staging_name.as_str(),
nix::sys::stat::Mode::S_IRWXU,
)?;
let flags = nix::fcntl::OFlag::O_RDONLY
| nix::fcntl::OFlag::O_DIRECTORY
| nix::fcntl::OFlag::O_NOFOLLOW;
let staging_fd = nix::fcntl::openat(
&parent.fd,
staging_name.as_str(),
flags,
nix::sys::stat::Mode::empty(),
)?;
let staging_stat = nix::sys::stat::fstat(&staging_fd)?;
if staging_stat.st_uid != nix::unistd::geteuid().as_raw() || staging_stat.st_mode & 0o077 != 0 {
bail!("brew-cask: temporary artifact directory is not private");
}
let staging = TrustedOperationParent { fd: staging_fd };
let temporary_name = std::ffi::OsStr::new("payload");
match copy_cask_artifact_at(from, &staging.fd, temporary_name) {
Ok(()) => {
match nix::fcntl::renameat(&staging.fd, temporary_name, &parent.fd, name)
.wrap_err_with(|| format!("failed to install {}", to.display()))
{
Ok(()) => {
remove_private_staging_dir(&parent, &staging, staging_name.as_ref())?;
Ok(())
}
Err(err) => {
remove_all_at(&staging.fd, temporary_name).wrap_err_with(|| {
format!(
"failed to clean up temporary generic artifact after rename failed: {err:#}"
)
})?;View on GitHub (pinned to 6f52dcdf99)