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
- Do not install the cask; download its URL manually, list the archive contents, and report it — this guard blocks archive (zip-slip) escapes
- If you package the artifact, rebuild it so every symlink is relative and stays inside the extraction root
- 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
- Treat any symlink-escape hit as a supply-chain red flag, not an inconvenience
- Package archives with relative, in-root symlinks only
- Use official Homebrew API casks where artifacts are broadly validated
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
- brew-cask: refusing generic artifact source outside the extr
- brew-cask: refusing to stage generic artifact through a path
- brew-cask: invalid {kind} '{value}'
- brew-cask: temporary artifact directory is not private
- brew-cask: refusing elevated operation through mutable direc
AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17).
Data as JSON: /api/errors/76c67b62f58702ae.
Report an issue: GitHub.