jdx/mise · error

brew-cask: refusing generic artifact copy outside Homebrew p

Error message

brew-cask: refusing generic artifact copy outside Homebrew prefix: {}

What it means

`validate_generic_copy_target` enforces that every generic artifact copy destination lives inside the Homebrew prefix. It rejects targets that are not under `prefix::prefix()`, that resolve to the prefix itself (empty first component after strip_prefix), or whose resolved real path escapes the prefix via symlinks (`path_starts_with_resolved_root`). This is a path-escape guard so a malicious cask cannot write artifacts to arbitrary system locations via symlinked paths.

Source

Thrown at src/system/packages/brew/cask/mod.rs:1911

    )?;
    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()
        );
    }
    Ok(())
}

#[cfg(unix)]
struct TrustedOperationParent {
    fd: std::os::fd::OwnedFd,
}

#[cfg(unix)]
impl TrustedOperationParent {
    fn path(&self) -> Result<PathBuf> {
        #[cfg(target_os = "linux")]
        return Ok(
            Path::new("/proc/self/fd").join(std::os::fd::AsRawFd::as_raw_fd(&self.fd).to_string())

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. Check the printed target path against your actual prefix (`brew --prefix`) and correct the artifact destination to live inside it.
  2. Verify the Homebrew prefix configuration (HOMEBREW_PREFIX / install location) matches where the artifacts are expected; a mismatched prefix causes legitimate targets to be rejected.
  3. Remove intermediate symlinks between the target and the prefix, or reference the target via its real path under the prefix so the resolved-root check passes.
  4. If the cask genuinely must install outside the prefix, use the appropriate cask stanza/mechanism for that (not the generic copier), which has its own validation.

Example fix

// before: absolute destination outside the prefix
let target = Path::new("/usr/local/bin/mytool");
// after: derive destination from the prefix
let target = prefix::prefix().join("bin").join("mytool");
Defensive patterns

Strategy: validation

Validate before calling

use std::path::{Path, PathBuf};
fn target_inside_prefix(target: &Path, prefix: &Path) -> bool {
    target.is_absolute()
        && target.starts_with(prefix)
        && target.strip_prefix(prefix).map(|r| r.components().next().is_some()).unwrap_or(false)
        && target.canonicalize().ok().filter(|r| r.starts_with(prefix)).is_some()
}

Type guard

fn is_safe_prefix_target(t: &Path, prefix: &Path) -> bool {
    t.starts_with(prefix) && t.canonicalize().map(|c| c.starts_with(prefix)).unwrap_or(false)
}

Prevention

When it happens

Trigger: A generic artifact copy whose `target` path (a) lies outside the Homebrew prefix, (b) equals the prefix itself with no remaining components, or (c) is a symlinked path that resolves outside the prefix — all reachable through any cask install/artifact-staging call that goes through `validate_generic_copy_target`.

Common situations: A cask artifact stanza with an absolute destination outside the brew prefix (e.g. `/usr/local/bin` on a non-Homebrew-managed layout); `HOMEBREW_PREFIX` pointing somewhere unexpected so valid destinations fall outside it; a symlinked prefix component (e.g. `/opt/homebrew` aliased through another path) making the resolved root differ from the lexical prefix.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/0a3d92b84579eade. Report an issue: GitHub.