{"record":{"id":"a0117444eb7ace2f","repo":"jdx/mise","slug":"brew-cask-generic-artifact-target-must-stay","errorCode":null,"errorMessage":"brew-cask: generic artifact target '{}' must stay below {}","messagePattern":"brew-cask: generic artifact target '(.+?)' must stay below (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":2501,"sourceCode":"        run_installer_artifact(stage, installer, targets.copied_files())?;\n        completed(index)?;\n    }\n    durabilize_staged_symlink_targets(stage, temporary_caskroom, targets)\n}\n\nfn generic_artifact_target_path(target: &str) -> Result<PathBuf> {\n    let prefix = prefix::prefix();\n    let expanded = target.replace(\"$HOMEBREW_PREFIX\", &prefix.to_string_lossy());\n    let target = PathBuf::from(expanded);\n    if !target.is_absolute()\n        || !target.starts_with(&prefix)\n        || target.strip_prefix(&prefix)?.components().next().is_none()\n        || target\n            .components()\n            .any(|component| matches!(component, Component::ParentDir))\n        || !path_starts_with_resolved_root(&target, &prefix)\n    {\n        bail!(\n            \"brew-cask: generic artifact target '{}' must stay below {}\",\n            target.display(),\n            prefix.display()\n        );\n    }\n    Ok(target)\n}\n\nfn generic_artifact_targets(artifacts: &CaskArtifacts) -> Result<Vec<PathBuf>> {\n    artifacts\n        .generic\n        .iter()\n        .map(|artifact| generic_artifact_target_path(&artifact.target))\n        .collect()\n}\n\nfn previous_generic_targets(cask: &Cask) -> Result<Vec<CaskTargetRecord>> {\n    let Some(version) = installed_version(&cask.token) else {","sourceCodeStart":2483,"sourceCodeEnd":2519,"githubUrl":"https://github.com/jdx/mise/blob/6f52dcdf99e282ef7a7db68c81301fa4618d0f79/src/system/packages/brew/cask.rs#L2483-L2519","documentation":"generic_artifact_target_path parses a cask artifact's target string after substituting $HOMEBREW_PREFIX with the active prefix. The result must be absolute, start with the prefix, contain at least one component below it, contain no ParentDir ('..') component, and pass path_starts_with_resolved_root so symlinked segments cannot route the real destination outside the prefix. Violations abort before anything is staged or copied.","triggerScenarios":"artifact target: \"/etc/foo\" or \"~/Library/x\" (not under the prefix); \"$HOMEBREW_PREFIX\" alone; \"$HOMEBREW_PREFIX/opt/../etc/x\"; a target under a symlink that resolves outside the prefix; unexpanded ${HOMEBREW_PREFIX} spelling (only the bare $HOMEBREW_PREFIX form is replaced).","commonSituations":"Casks ported from Homebrew ruby that install to system paths; hardcoded /usr/local targets on arm64 machines whose prefix is /opt/homebrew; templating that emits relative paths or unexpanded variables.","solutions":["Use an absolute target strictly below the active prefix, e.g. $HOMEBREW_PREFIX/etc/foo.conf","Use the literal $HOMEBREW_PREFIX spelling for substitution — ${HOMEBREW_PREFIX} is not expanded","Strip any '..' segments and ensure at least one component follows the prefix","Verify the prefix mise resolves (HOMEBREW_PREFIX, mise settings) matches the cask's assumption"],"exampleFix":"# before\nartifact \"lib/libfoo.dylib\", target: \"/usr/local/lib/libfoo.dylib\"\n\n# after\nartifact \"lib/libfoo.dylib\", target: \"$HOMEBREW_PREFIX/lib/libfoo.dylib\"","handlingStrategy":"validation","validationCode":"// Validate cask artifact target strings before running the install.\nfn valid_generic_target(target: &str, prefix: &Path) -> bool {\n    let expanded = target.replace(\"$HOMEBREW_PREFIX\", &prefix.to_string_lossy());\n    let p = Path::new(&expanded);\n    p.is_absolute()\n        && p.starts_with(prefix)\n        && p.strip_prefix(prefix).map(|r| r.components().next().is_some()).unwrap_or(false)\n        && !p.components().any(|c| matches!(c, std::path::Component::ParentDir))\n}","typeGuard":"fn is_expanded_absolute_under_prefix(target: &str, prefix: &Path) -> bool {\n    valid_generic_target(target, prefix)\n}","tryCatchPattern":null,"preventionTips":["Use the exact $HOMEBREW_PREFIX token (not ${HOMEBREW_PREFIX}) when relying on substitution","Never hardcode /usr/local in casks that must also run on arm64 (/opt/homebrew)","Keep targets at least one component below the prefix and free of '..'"],"tags":["brew-cask","generic-artifact","path-validation","homebrew-prefix"],"backgroundTag":"path-outside-allowed-root","analyzedSha":"6f52dcdf99e282ef7a7db68c81301fa4618d0f79","analyzedAt":"2026-08-22T10:14:23.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}