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

  1. Remove the stale staging directory (under the target's parent, from the failed run) and retry the install
  2. Check the parent directory chain's ownership and permissions — elevated copies require root-owned, non-group/world-writable parents
  3. 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

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


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)