Add allowMachLookup config for additional macOS XPC services (#204)

This commit is contained in:
Dylan Conway
2026-04-02 14:12:34 -07:00
committed by GitHub
parent 2dc232be92
commit d3d27dd3a6
5 changed files with 114 additions and 0 deletions

View File

@@ -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,

View File

@@ -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()

View File

@@ -504,6 +504,10 @@ function getAllowLocalBinding(): boolean | undefined {
return config?.network?.allowLocalBinding
}
function getAllowMachLookup(): string[] | undefined {
return config?.network?.allowMachLookup
}
function getIgnoreViolations(): Record<string, string[]> | 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<string, string[]> | undefined
getEnableWeakerNestedSandbox(): boolean | undefined
getProxyPort(): number | undefined
@@ -1046,6 +1052,7 @@ export const SandboxManager: ISandboxManager = {
getNetworkRestrictionConfig,
getAllowUnixSockets,
getAllowLocalBinding,
getAllowMachLookup,
getIgnoreViolations,
getEnableWeakerNestedSandbox,
getProxyPort,

View File

@@ -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: {

View File

@@ -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)
})
})