jdx/mise · critical

brew-cask: temporary artifact directory is not private

Error message

brew-cask: temporary artifact directory is not private

What it means

For atomic installs mise opens a private staging directory inside a trusted parent (openat with O_NOFOLLOW) and then fstats it: the directory must be owned by the current euid and have no group/other permission bits (mode & 0o077 == 0). If ownership or permissions are looser — a pre-planted or leftover directory — the install aborts, because anything readable or writable by others could be tampered with between staging and the rename.

Source

Thrown at src/system/packages/brew/cask.rs:1779

        .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 9dcfcaa0dc)

Solutions

  1. Remove mise's brew-cask staging/cache leftovers owned by the other user (under the mise data prefix and the system-brew cache dir), then retry
  2. Pick one identity (always root, or always your user) for mise cask installs on a given machine
  3. If the directory is one you did not create, treat it as suspicious and investigate before deleting
Defensive patterns

Strategy: validation

Validate before calling

use std::os::unix::fs::MetadataExt;
fn staging_dir_private(p: &Path) -> Result<bool> {
    let m = p.symlink_metadata()?;
    Ok(m.uid() == nix::unistd::geteuid().as_raw() && m.mode() & 0o077 == 0)
}

Try / catch

Catch 'temporary artifact directory is not private', identify and remove the foreign staging directory, then retry once; if it reappears without explanation, stop and investigate for tampering.

Prevention

When it happens

Trigger: A stale staging directory left by a previous mise run under a different user (ran with sudo once, then without, or vice versa), or another process creating the staging name with group/other access inside the trusted parent.

Common situations: Switching between root and non-root mise runs on the same machine; shared or kiosk Macs; CI runners reusing $HOME across jobs with different UIDs.

Related errors


AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17). Data as JSON: /api/errors/3fcbbcf41f9ba03a. Report an issue: GitHub.