jdx/mise · critical

brew-cask: staged symlink path escaped extraction root: {}

Error message

brew-cask: staged symlink path escaped extraction root: {}

What it means

When mise promotes the staged extraction into its owned stage directory, every staged path must map back inside the extraction root. If a staged file's parent no longer sits under the resolved stage root — the signature of symlinks in the archive resolving through absolute targets or ../-escapes — the copy is refused: the archive is trying to write outside its sandbox.

Source

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

}

fn copy_staged_artifact_closure(stage: &Path, owned_stage: &Path, source: &Path) -> Result<()> {
    let stage = lexically_normalized_path(stage);
    let mut pending = vec![lexically_normalized_path(source)];
    let mut visited = BTreeSet::new();
    while let Some(source) = pending.pop() {
        let relative = staged_relative_path(&stage, &source).ok_or_else(|| {
            eyre!(
                "brew-cask: staged symlink target escaped extraction root: {}",
                source.display()
            )
        })?;
        if relative.components().next().is_some()
            && !source
                .parent()
                .is_some_and(|parent| path_starts_with_resolved_root(parent, &stage))
        {
            bail!(
                "brew-cask: staged symlink path escaped extraction root: {}",
                source.display()
            );
        }
        if !visited.insert(relative.to_path_buf()) {
            continue;
        }
        let destination = owned_stage.join(&relative);
        let metadata = source.symlink_metadata()?;
        if destination.symlink_metadata().is_err() {
            if let Some(parent) = destination.parent() {
                file::create_dir_all(parent)?;
            }
            if metadata.file_type().is_symlink() {
                file::make_symlink(&std::fs::read_link(&source)?, &destination)?;
            } else {
                copy_cask_artifact(&source, &destination)?;
            }

View on GitHub (pinned to 9dcfcaa0dc)

Solutions

  1. Do not install the cask; download its URL manually, list the archive contents, and report it — this guard blocks archive (zip-slip) escapes
  2. If you package the artifact, rebuild it so every symlink is relative and stays inside the extraction root
  3. Prefer casks served from the official Homebrew API where artifacts are widely exercised
Defensive patterns

Strategy: try-catch

Validate before calling

// If you build/audit cask archives before publishing, verify symlinks stay inside:
fn symlinks_stay_in_root(root: &Path) -> Result<bool> {
    for entry in walkdir::WalkDir::new(root).follow_links(false) {
        let e = entry?;
        if e.path_is_symlink() {
            let t = std::fs::read_link(e.path())?;
            if t.is_absolute() || t.components().any(|c| c == std::path::Component::ParentDir) {
                return Ok(false);
            }
        }
    }
    Ok(true)
}

Try / catch

Catch the 'escaped extraction root' bails as a hard stop: quarantine the downloaded archive, log the cask token and URL, and report the cask — never retry or attempt manual extraction of the same payload.

Prevention

When it happens

Trigger: An extracted archive contains symlinks whose chain resolves outside the stage directory (e.g. link -> ../../../../usr/local/lib), so resolving the staged tree escapes the extraction root during promotion to the owned stage.

Common situations: Supply-chain probing of mise's cask pipeline; tarballs packed with absolute symlink targets; pathological packaging from niche taps.

Related errors


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