From 0190b03e5fe7cc0f2d83c1211ba2e9f4b97ff6ee Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Wed, 9 Sep 2026 16:42:42 +0100 Subject: [PATCH] Refine macOS service policy - Define the system services available to macOS subprocesses so command-line installation tasks have a consistent environment. - Keep directory, power, network and certificate services available and document the macOS-specific behaviour for cask steps. - Extend native coverage for application registration and verify the Core Foundation conversions used by the test queries. - Skip live service checks in nested sandboxes, where availability depends on the outer profile. - Validate with `brew lgtm --online` and all 21 native sandbox tests. --- Library/Homebrew/extend/os/mac/sandbox.rb | 13 ++++++ Library/Homebrew/test/sandbox_spec.rb | 54 ++++++++++++++++++++++- docs/Cask-Cookbook.md | 3 +- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/Library/Homebrew/extend/os/mac/sandbox.rb b/Library/Homebrew/extend/os/mac/sandbox.rb index e6e56bd60f263..56341902c29f3 100644 --- a/Library/Homebrew/extend/os/mac/sandbox.rb +++ b/Library/Homebrew/extend/os/mac/sandbox.rb @@ -34,6 +34,19 @@ module Sandbox (deny file-write*) ; deny non-allowlist file write operations (deny file-write-setugid) ; deny non-allowlist file write SUID/SGID operations (deny file-write-mode) ; deny non-allowlist file write mode operations + (deny mach-lookup) + (allow mach-lookup + (global-name "com.apple.bsd.dirhelper") + (global-name "com.apple.system.opendirectoryd.libinfo") + (global-name "com.apple.system.opendirectoryd.membership") + (global-name "com.apple.PowerManagement.control") + (global-name "com.apple.SecurityServer") + (global-name "com.apple.networkd") + (global-name "com.apple.ocspd") + (global-name "com.apple.trustd.agent") + (global-name "com.apple.SystemConfiguration.DNSConfiguration") + (global-name "com.apple.SystemConfiguration.configd") + ) (deny lsopen) (deny appleevent-send) (allow process-exec diff --git a/Library/Homebrew/test/sandbox_spec.rb b/Library/Homebrew/test/sandbox_spec.rb index 57e2513ecbdba..5315926e20760 100644 --- a/Library/Homebrew/test/sandbox_spec.rb +++ b/Library/Homebrew/test/sandbox_spec.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "sandbox" +require "securerandom" RSpec.describe Sandbox, :needs_macos do subject(:sandbox) { described_class.new } @@ -26,8 +27,8 @@ end.new end - it "denies LaunchServices and Apple Events even when network access is allowed" do - expect(sandbox.seatbelt_profile).to include("(deny lsopen)", "(deny appleevent-send)") + it "restricts Mach services, LaunchServices and Apple Events even when network access is allowed" do + expect(sandbox.seatbelt_profile).to include("(deny mach-lookup)", "(deny lsopen)", "(deny appleevent-send)") end end @@ -82,6 +83,25 @@ end describe "#run" do + let(:handlers_for_scheme) do + lambda do |scheme| + SystemCommand.run!("/usr/bin/osascript", args: ["-l", "JavaScript", "-e", <<~JS, scheme]).stdout + ObjC.import('CoreServices'); + function run(argv) { + return JSON.stringify(ObjC.deepUnwrap(ObjC.castRefToObject($.LSCopyAllHandlersForURLScheme($(argv[0])))) || []); + } + JS + end + end + + it "reports an empty array for an unregistered URL scheme" do + expect(handlers_for_scheme.call("org.homebrew.sandbox-#{SecureRandom.uuid}")).to eq("[]\n") + end + + it "reports registered HTTP URL handlers" do + expect(JSON.parse(handlers_for_scheme.call("http"))).not_to be_empty + end + it "prevents LaunchServices from launching an application outside the sandbox" do app = dir/"SandboxTest.app" SystemCommand.run!("/usr/bin/osacompile", args: ["-o", app, "-e", "return"]) @@ -91,6 +111,36 @@ expect { sandbox.run "/usr/bin/open", "-W", "-n", app }.to raise_error(ErrorDuringExecution) end + it "prevents LaunchServices from registering a URL handler" do + lsregister = "/System/Library/Frameworks/CoreServices.framework/Frameworks/" \ + "LaunchServices.framework/Support/lsregister" + identifier = "org.homebrew.sandbox-#{SecureRandom.uuid}" + + # LaunchServices does not register applications under /private/tmp. + Dir.mktmpdir("homebrew-sandbox", "#{Dir.home(ENV.fetch("USER"))}/Library/Caches") do |cache| + app = Pathname(cache)/"SandboxTest.app" + SystemCommand.run!("/usr/bin/osacompile", args: ["-o", app, "-e", "return"]) + SystemCommand.run!("/usr/bin/plutil", args: [ + "-replace", "CFBundleIdentifier", "-string", identifier, app/"Contents/Info.plist" + ]) + SystemCommand.run!("/usr/bin/plutil", args: [ + "-insert", "CFBundleURLTypes", "-json", [{ CFBundleURLSchemes: [identifier] }].to_json, + app/"Contents/Info.plist" + ]) + sandbox.allow_write_temp_and_cache + sandbox.allow_write_path(cache) + + # lsregister's exit status does not indicate whether registration succeeded. + sandbox.run "/bin/sh", "-c", '"$@"; exit 0', "--", lsregister, "-f", app + expect(handlers_for_scheme.call(identifier)).to eq("[]\n") + + SystemCommand.run!(lsregister, args: ["-f", app]) + expect(handlers_for_scheme.call(identifier)).to include(identifier) + ensure + SystemCommand.run(lsregister, args: ["-u", app]) if app + end + end + it "fails when writing to file not specified with ##allow_write" do expect do sandbox.run "touch", file diff --git a/docs/Cask-Cookbook.md b/docs/Cask-Cookbook.md index d2bf3e3679f63..a1defbf3c568e 100644 --- a/docs/Cask-Cookbook.md +++ b/docs/Cask-Cookbook.md @@ -188,7 +188,8 @@ Official macOS casks must also meet the [Gatekeeper requirement](Acceptable-Cask Generated completion artifacts are different: `generate_completions_from_executable` runs an installed executable only to produce shell completion text. The complete generation operation, including writing the completion, runs in an isolated Ruby subprocess where Homebrew has an available sandbox. The sandbox allows reading the staged cask, writing the completion and temporary/cache files and blocks network access. This limits side effects from commands that should only print completion data. -On macOS, sandboxed operations cannot launch applications through LaunchServices (including `open`) or send Apple Events to other applications. +On macOS, sandboxed operations can only look up explicitly allowed Mach services for directory information, power management, networking and certificates. +They cannot register or launch applications through LaunchServices (including `lsregister` and `open`) or send Apple Events to other applications. These restrictions also apply to steps with `network_access: true`. `installer script:` is not sandboxed. Many installer scripts are vendor installers that require broad filesystem writes, macOS services or `sudo`; macOS sandboxing does not work for root processes, and narrowing the write allowlist to the Caskroom plus uninstall or zap paths would break installers that legitimately write elsewhere. It would also change documented `SystemCommand` behaviours such as `sudo:`, `must_succeed:` and output handling.