Tighten macOS Seatbelt sandbox permissions

Richard Feldman created

- Scope mach-lookup to a specific allowlist of Mach services needed
  for shell operation (DNS, directory services, security, logging,
  launch services, etc.) instead of granting unrestricted IPC access.
  Add troubleshooting comment for diagnosing missing services.

- Remove blanket iokit-open permission. Terminal shells don't need
  IOKit user client access (no GPU, audio, USB, or HID). Random
  numbers use /dev/urandom or getentropy(), timing uses syscalls.

- Remove blanket process-exec and rely on the existing subpath rules
  that grant process-exec only to designated executable directories.
  Add process-exec to the project directory rule so users can run
  scripts like ./build.sh or ./gradlew.

Change summary

crates/terminal/src/sandbox_macos.rs | 85 ++++++++++++++++++++++++++++-
1 file changed, 80 insertions(+), 5 deletions(-)

Detailed changes

crates/terminal/src/sandbox_macos.rs 🔗

@@ -51,14 +51,85 @@ fn generate_sbpl_profile(config: &SandboxConfig) -> String {
     let mut p = String::from("(version 1)\n(deny default)\n");
 
     // Process lifecycle
-    p.push_str("(allow process-exec)\n");
     p.push_str("(allow process-fork)\n");
     p.push_str("(allow signal)\n");
 
-    // System services needed for basic operation
-    p.push_str("(allow mach-lookup)\n");
+    // Mach service allowlist.
+    //
+    // TROUBLESHOOTING: If users report broken terminal behavior (e.g. DNS failures,
+    // keychain errors, or commands hanging), a missing Mach service here is a likely
+    // cause. To diagnose:
+    //   1. Open Console.app and filter for "sandbox" or "deny mach-lookup" to find
+    //      the denied service name.
+    //   2. Or test interactively:
+    //      sandbox-exec -p '(version 1)(deny default)(allow mach-lookup ...)' /bin/sh
+    //   3. Add the missing service to the appropriate group below.
+
+    // Logging: unified logging (os_log) and legacy syslog.
+    p.push_str("(allow mach-lookup (global-name \"com.apple.logd\"))\n");
+    p.push_str("(allow mach-lookup (global-name \"com.apple.logd.events\"))\n");
+    p.push_str("(allow mach-lookup (global-name \"com.apple.system.logger\"))\n");
+
+    // User/group directory lookups (getpwuid, getgrnam, id, etc.).
+    p.push_str("(allow mach-lookup (global-name \"com.apple.system.opendirectoryd.libinfo\"))\n");
+    p.push_str(
+        "(allow mach-lookup (global-name \"com.apple.system.opendirectoryd.membership\"))\n",
+    );
+
+    // Darwin notification center, used internally by many system frameworks.
+    p.push_str("(allow mach-lookup (global-name \"com.apple.system.notification_center\"))\n");
+
+    // CFPreferences: reading user and system preferences.
+    p.push_str("(allow mach-lookup (global-name \"com.apple.cfprefsd.agent\"))\n");
+    p.push_str("(allow mach-lookup (global-name \"com.apple.cfprefsd.daemon\"))\n");
+
+    // Temp directory management (_CS_DARWIN_USER_CACHE_DIR, etc.).
+    p.push_str("(allow mach-lookup (global-name \"com.apple.bsd.dirhelper\"))\n");
+
+    // DNS and network configuration.
+    p.push_str("(allow mach-lookup (global-name \"com.apple.dnssd.service\"))\n");
+    p.push_str(
+        "(allow mach-lookup (global-name \"com.apple.SystemConfiguration.DNSConfiguration\"))\n",
+    );
+    p.push_str("(allow mach-lookup (global-name \"com.apple.SystemConfiguration.configd\"))\n");
+    p.push_str(
+        "(allow mach-lookup (global-name \"com.apple.SystemConfiguration.NetworkInformation\"))\n",
+    );
+    p.push_str("(allow mach-lookup (global-name \"com.apple.SystemConfiguration.SCNetworkReachability\"))\n");
+    p.push_str("(allow mach-lookup (global-name \"com.apple.networkd\"))\n");
+    p.push_str("(allow mach-lookup (global-name \"com.apple.nehelper\"))\n");
+
+    // Security, keychain, and TLS certificate verification.
+    p.push_str("(allow mach-lookup (global-name \"com.apple.SecurityServer\"))\n");
+    p.push_str("(allow mach-lookup (global-name \"com.apple.trustd.agent\"))\n");
+    p.push_str("(allow mach-lookup (global-name \"com.apple.ocspd\"))\n");
+    p.push_str("(allow mach-lookup (global-name \"com.apple.security.authtrampoline\"))\n");
+
+    // Launch Services: needed for the `open` command, file-type associations,
+    // and anything that uses NSWorkspace or LaunchServices.
+    p.push_str("(allow mach-lookup (global-name \"com.apple.coreservices.launchservicesd\"))\n");
+    p.push_str("(allow mach-lookup (global-name \"com.apple.CoreServices.coreservicesd\"))\n");
+    p.push_str("(allow mach-lookup (global-name-regex #\"^com\\.apple\\.lsd\\.\" ))\n");
+
+    // Kerberos: needed in enterprise environments for authentication.
+    p.push_str("(allow mach-lookup (global-name \"com.apple.GSSCred\"))\n");
+    p.push_str("(allow mach-lookup (global-name \"org.h5l.kcm\"))\n");
+
+    // Distributed notifications: some command-line tools using Foundation may need this.
+    p.push_str(
+        "(allow mach-lookup (global-name-regex #\"^com\\.apple\\.distributed_notifications\"))\n",
+    );
+
     p.push_str("(allow sysctl-read)\n");
-    p.push_str("(allow iokit-open)\n");
+
+    // No iokit-open rules: a terminal shell does not need to open IOKit user
+    // clients (kernel driver interfaces). IOKit access is needed for GPU/
+    // graphics (IOAccelerator, AGPMClient), audio (IOAudioEngine), USB,
+    // Bluetooth, and similar hardware — none of which a shell requires. Random
+    // numbers come from /dev/urandom or getentropy(), and timing uses syscalls,
+    // so no IOKit involvement is needed for basic process operation. Chromium's
+    // network process and Firefox's content process both operate without any
+    // iokit-open rules.
 
     // System executable paths (read + execute)
     for path in &config.system_paths.executable {
@@ -76,7 +147,11 @@ fn generate_sbpl_profile(config: &SandboxConfig) -> String {
     }
 
     // Project directory: full access
-    write_subpath_rule(&mut p, &config.project_dir, "file-read* file-write*");
+    write_subpath_rule(
+        &mut p,
+        &config.project_dir,
+        "file-read* file-write* process-exec",
+    );
 
     // User-configured additional paths
     for path in &config.additional_executable_paths {