jdx/mise · error

brew-cask: invalid generic artifact parent

Error message

brew-cask: invalid generic artifact parent

What it means

While opening the generic artifact's parent path component by component, only Component::Normal names are accepted. Any other component (ParentDir '..', CurDir '.', RootDir '/', or a disk prefix) makes the path invalid for a trusted parent, because such paths cannot be safely walked with openat relative to the trusted prefix root.

Source

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

            || current_groups.contains(&stat.st_gid);
        let writable_by_untrusted = stat.st_mode & 0o002 != 0
            || (stat.st_mode & 0o020 != 0 && (!allow_current_user || !trusted_group));
        if !SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFDIR)
            || !trusted_owner
            || writable_by_untrusted
        {
            bail!(
                "brew-cask: refusing operation through untrusted directory {}",
                directory.display()
            );
        }
        Ok(())
    };
    let mut directory = resolved_root.to_path_buf();
    verify(&fd, &directory)?;
    for component in relative.components() {
        let Component::Normal(name) = component else {
            bail!("brew-cask: invalid generic artifact parent");
        };
        directory.push(name);
        fd = match openat(&fd, name, flags, Mode::empty()) {
            Ok(fd) => fd,
            Err(nix::errno::Errno::ENOENT) if create_missing => {
                match nix::sys::stat::mkdirat(
                    &fd,
                    name,
                    Mode::S_IRWXU | Mode::S_IRGRP | Mode::S_IXGRP | Mode::S_IROTH | Mode::S_IXOTH,
                ) {
                    Ok(()) | Err(nix::errno::Errno::EEXIST) => {}
                    Err(err) => {
                        return Err(err).wrap_err_with(|| {
                            format!(
                                "brew-cask: cannot create operation directory {}",
                                directory.display()
                            )
                        });

View on GitHub (pinned to 9dcfcaa0dc)

Solutions

  1. Normalize the parent path before use: resolve '.'/'..' with a path-cleaning step or canonicalize the existing prefix portion
  2. Fix the cask/config so the parent is a plain sequence of names relative to the Homebrew prefix
  3. Validate user-supplied parent strings with a components() check and reject non-normal components early

Example fix

// before
let parent = format!("{}/../lib", prefix);

// after -- build with Path and assert no traversal
let parent = prefix.join("lib");
assert!(!parent.components().any(|c| matches!(c, std::path::Component::ParentDir)));
Defensive patterns

Strategy: validation

Validate before calling

use std::path::{Path, Component};

fn has_only_normal_components(path: &Path) -> bool {
    !path.is_absolute()
        && path.components().next().is_some()
        && path.components().all(|c| matches!(c, Component::Normal(_)))
}

Type guard

fn is_normal_relative_path(p: &std::path::Path) -> bool {
    p.components().all(|c| matches!(c, std::path::Component::Normal(_)))
}

Prevention

When it happens

Trigger: Passing a parent path containing '..', '.', or a leading '/' into the generic artifact parent resolution; parent strings built by string concatenation with unvalidated user input instead of Path::join.

Common situations: Cask stanzas or config with relative parent paths like "../shared/lib"; code that builds paths with format!() and never normalizes; templates leaving placeholder dot components behind.

Related errors


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