{"record":{"id":"50cef086880caff2","repo":"jdx/mise","slug":"brew-cask-refusing-operation-through-a-changed-ge-50cef0","errorCode":null,"errorMessage":"brew-cask: refusing operation through a changed generic artifact parent: {}","messagePattern":"brew-cask: refusing operation through a changed generic artifact parent: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask/mod.rs","lineNumber":2320,"sourceCode":"            .file_name()\n            .ok_or_else(|| eyre!(\"brew-cask: generic artifact target has no filename\"))?;\n        remove_all_at(&parent.fd, name)\n    }\n    #[cfg(not(unix))]\n    {\n        let _ = expected_parent;\n        file::remove_all(target)\n    }\n}\n\n#[cfg(unix)]\nfn validate_trusted_operation_parent(\n    parent: &TrustedOperationParent,\n    expected_parent: &Path,\n) -> Result<()> {\n    let actual_parent = std::fs::canonicalize(parent.path()?)?;\n    if actual_parent != expected_parent {\n        bail!(\n            \"brew-cask: refusing operation through a changed generic artifact parent: {}\",\n            expected_parent.display()\n        );\n    }\n    Ok(())\n}\n\nfn rename_trusted_generic_target(from: &Path, to: &Path, expected_parent: &Path) -> Result<()> {\n    #[cfg(unix)]\n    {\n        if from.parent() != to.parent() {\n            bail!(\"brew-cask: generic artifact backup changed directories\");\n        }\n        let parent = open_trusted_operation_parent(from, true, false)?;\n        validate_trusted_operation_parent(&parent, expected_parent)?;\n        let from_name = from\n            .file_name()\n            .ok_or_else(|| eyre!(\"brew-cask: generic artifact source has no filename\"))?;","sourceCodeStart":2302,"sourceCodeEnd":2338,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/packages/brew/cask/mod.rs#L2302-L2338","documentation":"Trusted generic-artifact operations open the parent directory by file descriptor and later re-verify (canonicalize) that the opened directory still matches the expected parent path. If the directory was swapped, renamed, or replaced by a symlink between open and use, the operation is aborted — a TOCTOU defense against an attacker replacing the directory mid-operation.","triggerScenarios":"During backup/restore (rename) or removal of a generic artifact, the directory that was opened via open_trusted_operation_parent no longer canonicalizes to expected_parent (e.g. it was deleted and recreated, or moved by another process).","commonSituations":"Another mise/Homebrew process concurrently modifying the same cask directory; antivirus or sync tools (Dropbox/iCloud) moving directories during install; a race during parallel upgrades.","solutions":["Re-run the install/upgrade command — the race usually does not recur","Exclude the mise prefix directory from file-sync/AV real-time scanning","Avoid running concurrent mise operations on the same toolset"],"exampleFix":"// before\nmise install <cask> &  mise upgrade <cask> &   # concurrent\n// after\nmise install <cask> && mise upgrade <cask>     # sequential","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"match result {\n    Err(e) if e.to_string().contains(\"changed generic artifact parent\") => {\n        // transient race: retry once after excluding concurrent writers\n        eprintln!(\"directory changed mid-operation; retry without concurrency\");\n    }\n    Err(e) => return Err(e),\n    Ok(v) => v,\n}","preventionTips":["Don't run concurrent mise operations on the same toolset","Exclude the prefix from file-sync (Dropbox/iCloud) and AV scanners","Retry the install — the race is usually transient"],"tags":["security","race-condition","filesystem","brew-cask"],"backgroundTag":"internal-invariant-violation","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}