diff --git a/src/sandbox/macos-sandbox-utils.ts b/src/sandbox/macos-sandbox-utils.ts index 33d7b24..39fde01 100644 --- a/src/sandbox/macos-sandbox-utils.ts +++ b/src/sandbox/macos-sandbox-utils.ts @@ -28,6 +28,7 @@ export interface MacOSSandboxParams { allowUnixSockets?: string[] allowAllUnixSockets?: boolean allowLocalBinding?: boolean + allowMachLookup?: string[] readConfig: FsReadRestrictionConfig | undefined writeConfig: FsWriteRestrictionConfig | undefined ignoreViolations?: IgnoreViolationsConfig | undefined @@ -401,6 +402,7 @@ function generateSandboxProfile({ allowUnixSockets, allowAllUnixSockets, allowLocalBinding, + allowMachLookup, allowPty, allowGitConfig = false, enableWeakerNetworkIsolation = false, @@ -414,6 +416,7 @@ function generateSandboxProfile({ allowUnixSockets?: string[] allowAllUnixSockets?: boolean allowLocalBinding?: boolean + allowMachLookup?: string[] allowPty?: boolean allowGitConfig?: boolean enableWeakerNetworkIsolation?: boolean @@ -460,6 +463,16 @@ function generateSandboxProfile({ '(allow mach-lookup (global-name "com.apple.trustd.agent"))', ] : []), + ...(allowMachLookup && allowMachLookup.length > 0 + ? [ + '; User-specified XPC/Mach services', + ...allowMachLookup.map(name => + name.endsWith('*') + ? `(allow mach-lookup (global-name-prefix ${escapePath(name.slice(0, -1))}))` + : `(allow mach-lookup (global-name ${escapePath(name)}))`, + ), + ] + : []), '', '; POSIX IPC - shared memory', '(allow ipc-posix-shm)', @@ -699,6 +712,7 @@ export function wrapCommandWithSandboxMacOS( allowUnixSockets, allowAllUnixSockets, allowLocalBinding, + allowMachLookup, readConfig, writeConfig, allowPty, @@ -733,6 +747,7 @@ export function wrapCommandWithSandboxMacOS( allowUnixSockets, allowAllUnixSockets, allowLocalBinding, + allowMachLookup, allowPty, allowGitConfig, enableWeakerNetworkIsolation, diff --git a/src/sandbox/sandbox-config.ts b/src/sandbox/sandbox-config.ts index 8d5a086..eb048d9 100644 --- a/src/sandbox/sandbox-config.ts +++ b/src/sandbox/sandbox-config.ts @@ -122,6 +122,23 @@ export const NetworkConfigSchema = z.object({ .boolean() .optional() .describe('Whether to allow binding to local ports (default: false)'), + allowMachLookup: z + .array( + z.string().refine( + val => { + const prefix = val.endsWith('*') ? val.slice(0, -1) : val + return !prefix.includes('*') + }, + { + message: + 'Wildcards are only allowed as a single trailing "*" (e.g., "com.example.*" or "*" for all services).', + }, + ), + ) + .optional() + .describe( + 'macOS only: Additional XPC/Mach service names to allow looking up. Supports trailing-wildcard prefix matching (e.g., "2BUA8C4S2C.com.1password.*"). Needed for tools like 1Password CLI, Playwright, or the iOS Simulator that communicate via XPC.', + ), httpProxyPort: z .number() .int() diff --git a/src/sandbox/sandbox-manager.ts b/src/sandbox/sandbox-manager.ts index 3acc533..6fa0cc6 100644 --- a/src/sandbox/sandbox-manager.ts +++ b/src/sandbox/sandbox-manager.ts @@ -504,6 +504,10 @@ function getAllowLocalBinding(): boolean | undefined { return config?.network?.allowLocalBinding } +function getAllowMachLookup(): string[] | undefined { + return config?.network?.allowMachLookup +} + function getIgnoreViolations(): Record | undefined { return config?.ignoreViolations } @@ -671,6 +675,7 @@ async function wrapWithSandbox( allowUnixSockets: getAllowUnixSockets(), allowAllUnixSockets: getAllowAllUnixSockets(), allowLocalBinding: getAllowLocalBinding(), + allowMachLookup: getAllowMachLookup(), ignoreViolations: getIgnoreViolations(), allowPty, allowGitConfig: getAllowGitConfig(), @@ -1006,6 +1011,7 @@ export interface ISandboxManager { getNetworkRestrictionConfig(): NetworkRestrictionConfig getAllowUnixSockets(): string[] | undefined getAllowLocalBinding(): boolean | undefined + getAllowMachLookup(): string[] | undefined getIgnoreViolations(): Record | undefined getEnableWeakerNestedSandbox(): boolean | undefined getProxyPort(): number | undefined @@ -1046,6 +1052,7 @@ export const SandboxManager: ISandboxManager = { getNetworkRestrictionConfig, getAllowUnixSockets, getAllowLocalBinding, + getAllowMachLookup, getIgnoreViolations, getEnableWeakerNestedSandbox, getProxyPort, diff --git a/test/config-validation.test.ts b/test/config-validation.test.ts index 9a6d3d9..432f72b 100644 --- a/test/config-validation.test.ts +++ b/test/config-validation.test.ts @@ -224,6 +224,41 @@ describe('Config Validation', () => { } }) + test('should accept valid allowMachLookup entries', () => { + const config = { + network: { + allowedDomains: [], + deniedDomains: [], + allowMachLookup: [ + '2BUA8C4S2C.com.1password.*', + 'com.apple.CoreSimulator.CoreSimulatorService', + '*', + ], + }, + filesystem: { denyRead: [], allowWrite: [], denyWrite: [] }, + } + + const result = SandboxRuntimeConfigSchema.safeParse(config) + expect(result.success).toBe(true) + }) + + test.each(['com.*.foo', 'com.example.**'])( + 'should reject allowMachLookup entry with non-trailing wildcard: %s', + entry => { + const config = { + network: { + allowedDomains: [], + deniedDomains: [], + allowMachLookup: [entry], + }, + filesystem: { denyRead: [], allowWrite: [], denyWrite: [] }, + } + + const result = SandboxRuntimeConfigSchema.safeParse(config) + expect(result.success).toBe(false) + }, + ) + test('should use default ripgrep command when not specified', () => { const config = { network: { diff --git a/test/sandbox/macos-seatbelt.test.ts b/test/sandbox/macos-seatbelt.test.ts index 73cb0ce..51b13bc 100644 --- a/test/sandbox/macos-seatbelt.test.ts +++ b/test/sandbox/macos-seatbelt.test.ts @@ -866,3 +866,43 @@ describe.if(isMacOS)('macOS Seatbelt Process Enumeration', () => { } }) }) + +describe.if(isMacOS)('macOS Seatbelt allowMachLookup', () => { + it('should emit global-name and global-name-prefix rules for configured services', () => { + const wrappedCommand = wrapCommandWithSandboxMacOS({ + command: 'true', + needsNetworkRestriction: true, + allowMachLookup: [ + 'com.apple.CoreSimulator.CoreSimulatorService', + '2BUA8C4S2C.com.1password.*', + ], + readConfig: undefined, + writeConfig: undefined, + }) + + expect(wrappedCommand).toContain( + '(allow mach-lookup (global-name \\"com.apple.CoreSimulator.CoreSimulatorService\\"))', + ) + expect(wrappedCommand).toContain( + '(allow mach-lookup (global-name-prefix \\"2BUA8C4S2C.com.1password.\\"))', + ) + }) + + it('should emit a syntactically valid profile with allowMachLookup set', () => { + const wrappedCommand = wrapCommandWithSandboxMacOS({ + command: 'true', + needsNetworkRestriction: true, + allowMachLookup: ['com.example.service', 'com.example.prefix.*', '*'], + readConfig: undefined, + writeConfig: undefined, + }) + + const result = spawnSync(wrappedCommand, { + shell: true, + encoding: 'utf8', + timeout: 5000, + }) + + expect(result.status).toBe(0) + }) +})