Skip to content

cask/staged: check App Management permissions before chown#22967

Merged
MikeMcQuaid merged 1 commit into
Homebrew:mainfrom
MiMoHo:cask-set-ownership-app-management
Jul 5, 2026
Merged

cask/staged: check App Management permissions before chown#22967
MikeMcQuaid merged 1 commit into
Homebrew:mainfrom
MiMoHo:cask-set-ownership-app-management

Conversation

@MiMoHo

@MiMoHo MiMoHo commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

  • Have you followed our Contributing guidelines?
  • Have you checked for other open Pull Requests for the same change?
  • Have you explained what your changes do? Performance claims (e.g. "this is faster") must include Hyperfine benchmarks.
  • Have you explained why you'd like these changes included, not just what they do?
  • For bug fixes, have you given step-by-step brew commands to reproduce the bug?
  • Have you written new tests (excluding integration tests)? Here's an example.
  • Have you successfully run brew lgtm (style, typechecking and tests) locally?

  • AI was used to generate or assist with generating this PR.

This PR was prepared with the assistance of Claude Code, driven and reviewed by the account owner. The bug was diagnosed and the fix verified on a real affected system (macOS 15, Apple Silicon, Tunnelblick 8.0.2 installed via the tunnelblick cask); every command and test run quoted below was actually executed on that machine.


What does this PR do?

Cask::Staged#set_ownership (used by casks via uninstall_preflight { set_ownership ... }) now checks Quarantine.app_management_permissions_granted? for each path before running sudo chown -R. If the App Management permission is missing, it raises a CaskError with actionable guidance instead of aborting with one raw chown: ...: Operation not permitted line per file inside the app bundle. The check itself also triggers the macOS App Management permission prompt (a plain chown never does), so in the common case the user can simply approve the prompt and re-run the command.

Why is this needed? (root cause)

On macOS 13 or later, modifying the contents of an app bundle requires the App Management TCC permission (kTCCServiceSystemPolicyAppBundles) — even when running as root via sudo. Homebrew already knows this: Cask::Quarantine.app_management_permissions_granted? exists for exactly this reason and is wired into the upgrade path (Cask::Artifact::Moved). But set_ownership is not covered, so brew uninstall of a cask whose app is root-owned fails hard when the terminal lacks the permission.

The tunnelblick cask is the prime example: Tunnelblick "secures" itself on first launch (its privileged helper chowns the whole bundle to root:wheel), which is why the cask uses uninstall_preflight { set_ownership "#{appdir}/Tunnelblick.app" }. On an affected machine, brew uninstall tunnelblick prints ~620 lines of

chown: /Applications/Tunnelblick.app/Contents/...: Operation not permitted

and aborts with Error: Failure while executing; /usr/bin/sudo -E -- chown -R -- user:staff /Applications/Tunnelblick.app exited with 1, giving the user no hint that a privacy setting (not file permissions) is the cause.

Root-cause evidence gathered on the affected machine:

  • The failure is EPERM ("Operation not permitted"), not EACCES ("Permission denied"), and happens even as root — so it is not a DAC/ownership problem.
  • The bundle carries no uchg/schg flags (ls -lO is clean), ruling out immutable flags.
  • A write probe inside the bundle (touch /Applications/Tunnelblick.app/Contents/Resources/x) fails with Operation not permitted, while creating a file directly in /Applications succeeds for the same user — the TCC App Management signature.
  • This is exactly the situation Quarantine.app_management_permissions_granted? was introduced for (see its comments in Library/Homebrew/cask/quarantine.rb): App Management has been required for modifying other apps' bundles since macOS 13 Ventura, and only an actual write attempt — not chown — makes macOS show the permission prompt.

Steps to reproduce

On macOS ≥ 13, in a terminal that does not have App Management permission:

  1. brew install --cask tunnelblick
  2. Launch Tunnelblick once and let it secure itself (it becomes owned by root:wheel).
  3. brew uninstall tunnelblick

Before this change: hundreds of chown: ...: Operation not permitted lines, then a generic Error: Failure while executing. No macOS permission prompt is shown, because chown does not trigger one.

After this change:

==> Uninstalling Cask tunnelblick
Error: Cannot change the ownership of '/Applications/Tunnelblick.app' because your terminal does not have App Management permissions.
macOS prevents modifying apps without these permissions, even when using `sudo`.
To fix this, approve the permissions prompt (if one was just shown) or go to
System Settings → Privacy & Security → App Management and add or enable your terminal.
Then run this command again.

(and macOS shows the App Management prompt for the terminal, so approving it and re-running succeeds.)

The error wording follows the existing App Management message in Library/Homebrew/cask/quarantine.rb.

Testing

  • New RSpec examples in test/cask/dsl/shared_examples/staged.rb (exercised via the preflight, postflight and uninstall_preflight specs): permission granted → chown runs as before; permission missing → CaskError mentioning App Management and no chown attempt. All three spec files pass.
  • srb tc: no errors. brew style on the changed files: no offenses. brew tests --changed selects no specs for these files (shared examples are not mapped to their consumers), so the three consumer spec files were run explicitly instead — a superset of what brew lgtm runs.
  • Functional check on the affected machine, using the real tunnelblick cask and the real root-owned /Applications/Tunnelblick.app, with SystemCommand stubbed to fail exactly like a TCC-denied touch: the new CaskError is raised. A stubbed plain sudo failure (sudo: a terminal is required to read the password) still propagates unchanged, so genuine sudo problems are not misattributed to App Management.

Existing set_ownership behaviour is unchanged whenever the permission check passes (including non-directory paths, for which app_management_permissions_granted? returns true immediately).

🤖 Generated with Claude Code

`uninstall_preflight { set_ownership ... }` runs `sudo chown -R` on the
app bundle. On macOS 13 or later, modifying the contents of an app
bundle requires the App Management TCC permission even when running as
root, and `chown` does not trigger the system permission prompt:
without the permission, the uninstall aborts with one
`Operation not permitted` line per file in the bundle and no hint at
the actual cause.

Reuse Quarantine.app_management_permissions_granted? (already used for
upgrades in Cask::Artifact::Moved) before chowning, so that:

- the system permission prompt is triggered, letting the user grant
  access on the spot, and
- a clear, actionable CaskError is raised if the permission is missing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@MikeMcQuaid MikeMcQuaid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, thanks!

@MikeMcQuaid MikeMcQuaid added this pull request to the merge queue Jul 5, 2026
Merged via the queue into Homebrew:main with commit 37a7153 Jul 5, 2026
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants