Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 57 additions & 31 deletions src/sandbox/linux-sandbox-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
FsWriteRestrictionConfig,
} from './sandbox-schemas.js'
import { getApplySeccompBinaryPath } from './generate-seccomp-filter.js'
import type { SeccompConfig } from './sandbox-config.js'

export interface LinuxNetworkBridgeContext {
httpSocketPath: string
Expand Down Expand Up @@ -49,7 +50,7 @@ export interface LinuxSandboxParams {
/** Allow writes to .git/config files (default: false) */
allowGitConfig?: boolean
/** Custom seccomp binary paths */
seccompConfig?: { applyPath?: string }
seccompConfig?: SeccompConfig
/** Abort signal to cancel the ripgrep scan */
abortSignal?: AbortSignal
}
Expand Down Expand Up @@ -390,31 +391,37 @@ export type SandboxDependencyCheck = {
/**
* Get detailed status of Linux sandbox dependencies
*/
export function getLinuxDependencyStatus(seccompConfig?: {
applyPath?: string
}): LinuxDependencyStatus {
export function getLinuxDependencyStatus(
seccompConfig?: SeccompConfig,
): LinuxDependencyStatus {
// argv0 mode: apply-seccomp is compiled into the caller's binary — skip
// the on-disk lookup and trust that applyPath resolves inside bwrap.
return {
hasBwrap: whichSync('bwrap') !== null,
hasSocat: whichSync('socat') !== null,
hasSeccompApply:
getApplySeccompBinaryPath(seccompConfig?.applyPath) !== null,
hasSeccompApply: seccompConfig?.argv0
? true
: getApplySeccompBinaryPath(seccompConfig?.applyPath) !== null,
}
}

/**
* Check sandbox dependencies and return structured result
*/
export function checkLinuxDependencies(seccompConfig?: {
applyPath?: string
}): SandboxDependencyCheck {
export function checkLinuxDependencies(
seccompConfig?: SeccompConfig,
): SandboxDependencyCheck {
const errors: string[] = []
const warnings: string[] = []

if (whichSync('bwrap') === null)
errors.push('bubblewrap (bwrap) not installed')
if (whichSync('socat') === null) errors.push('socat not installed')

if (getApplySeccompBinaryPath(seccompConfig?.applyPath) === null) {
if (
!seccompConfig?.argv0 &&
getApplySeccompBinaryPath(seccompConfig?.applyPath) === null
) {
warnings.push('seccomp not available - unix socket access not restricted')
}

Expand Down Expand Up @@ -575,6 +582,31 @@ export async function initializeLinuxNetworkBridge(
}
}

/**
* Resolve how to invoke apply-seccomp: either a standalone binary path, or a
* multicall-binary prefix that dispatches on the ARGV0 env var.
*
* Returns a shell-ready string ending in a trailing space — callers append
* shellquote.quote([shell, '-c', cmd]). Returns undefined when seccomp is
* unavailable (no argv0, no binary found).
*
* When argv0 is set, applyPath is used verbatim (no existence check); the
* caller is responsible for ensuring it resolves inside the bwrap namespace.
*/
function resolveApplySeccompPrefix(
applyPath: string | undefined,
argv0: string | undefined,
): string | undefined {
if (argv0) {
if (!applyPath) {
throw new Error('seccompConfig.argv0 requires seccompConfig.applyPath')
}
return `ARGV0=${shellquote.quote([argv0])} ${shellquote.quote([applyPath])} `
}
const binary = getApplySeccompBinaryPath(applyPath)
return binary ? `${shellquote.quote([binary])} ` : undefined
}

/**
* Build the command that runs inside the sandbox.
* Sets up HTTP proxy on port 3128 and SOCKS proxy on port 1080
Expand All @@ -583,7 +615,7 @@ function buildSandboxCommand(
httpSocketPath: string,
socksSocketPath: string,
userCommand: string,
applySeccompBinary: string | undefined,
applySeccompPrefix: string | undefined,
shell?: string,
): string {
// Default to bash for backward compatibility
Expand All @@ -595,13 +627,9 @@ function buildSandboxCommand(
]

// apply-seccomp runs after socat so socat can still create Unix sockets.
if (applySeccompBinary) {
const applySeccompCmd = shellquote.quote([
applySeccompBinary,
shellPath,
'-c',
userCommand,
])
if (applySeccompPrefix) {
const applySeccompCmd =
applySeccompPrefix + shellquote.quote([shellPath, '-c', userCommand])
const innerScript = [...socatCommands, applySeccompCmd].join('\n')
return `${shellPath} -c ${shellquote.quote([innerScript])}`
} else {
Expand Down Expand Up @@ -1036,17 +1064,19 @@ export async function wrapCommandWithSandboxLinux(
activeSandboxCount++

const bwrapArgs: string[] = ['--new-session', '--die-with-parent']
let applySeccompBinary: string | undefined
let applySeccompPrefix: string | undefined

try {
// ========== SECCOMP FILTER (Unix Socket Blocking) ==========
// apply-seccomp wraps the workload and applies the baked-in BPF filter
// that blocks socket(AF_UNIX, ...). Skipped when allowAllUnixSockets is true.
if (!allowAllUnixSockets) {
applySeccompBinary =
getApplySeccompBinaryPath(seccompConfig?.applyPath) ?? undefined
applySeccompPrefix = resolveApplySeccompPrefix(
seccompConfig?.applyPath,
seccompConfig?.argv0,
)

if (!applySeccompBinary) {
if (!applySeccompPrefix) {
logForDebugging(
'[Sandbox Linux] apply-seccomp binary not available - unix socket blocking disabled. ' +
'Install @anthropic-ai/sandbox-runtime globally for full protection.',
Expand Down Expand Up @@ -1188,17 +1218,13 @@ export async function wrapCommandWithSandboxLinux(
httpSocketPath,
socksSocketPath,
command,
applySeccompBinary,
applySeccompPrefix,
shell,
)
bwrapArgs.push(sandboxCommand)
} else if (applySeccompBinary) {
const applySeccompCmd = shellquote.quote([
applySeccompBinary,
shell,
'-c',
command,
])
} else if (applySeccompPrefix) {
const applySeccompCmd =
applySeccompPrefix + shellquote.quote([shell, '-c', command])
bwrapArgs.push(applySeccompCmd)
} else {
bwrapArgs.push(command)
Expand All @@ -1210,7 +1236,7 @@ export async function wrapCommandWithSandboxLinux(
if (needsNetworkRestriction) restrictions.push('network')
if (hasReadRestrictions || hasWriteRestrictions)
restrictions.push('filesystem')
if (applySeccompBinary) restrictions.push('seccomp(unix-block)')
if (applySeccompPrefix) restrictions.push('seccomp(unix-block)')

logForDebugging(
`[Sandbox Linux] Wrapped command with bwrap (${restrictions.join(', ')} restrictions)`,
Expand Down
11 changes: 11 additions & 0 deletions src/sandbox/sandbox-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,17 @@ export const RipgrepConfigSchema = z.object({
*/
export const SeccompConfigSchema = z.object({
applyPath: z.string().optional().describe('Path to the apply-seccomp binary'),
argv0: z
.string()
.optional()
.describe(
'Invoke apply-seccomp as a multicall binary that dispatches on the ' +
'ARGV0 environment variable. When set, applyPath is used verbatim ' +
'(no existence check) and the invocation inside bwrap is prefixed ' +
'with ARGV0=<this value>. The caller is responsible for ensuring ' +
'applyPath resolves inside the bwrap namespace and that the target ' +
'binary implements the apply-seccomp interface when ARGV0 matches.',
),
})

/**
Expand Down
4 changes: 2 additions & 2 deletions src/sandbox/sandbox-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { whichSync } from '../utils/which.js'
import { cloneDeep } from 'lodash-es'
import { getPlatform, getWslVersion } from '../utils/platform.js'
import * as fs from 'fs'
import type { SandboxRuntimeConfig } from './sandbox-config.js'
import type { SandboxRuntimeConfig, SeccompConfig } from './sandbox-config.js'
import type {
SandboxAskCallback,
FsReadRestrictionConfig,
Expand Down Expand Up @@ -528,7 +528,7 @@ function getAllowGitConfig(): boolean {
return config?.filesystem?.allowGitConfig ?? false
}

function getSeccompConfig(): { applyPath?: string } | undefined {
function getSeccompConfig(): SeccompConfig | undefined {
return config?.seccomp
}

Expand Down
24 changes: 24 additions & 0 deletions test/sandbox/linux-dependency-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ describe('checkLinuxDependencies', () => {

expect(applySpy).toHaveBeenCalledWith('/custom/apply')
})

test('argv0 mode: no seccomp warning even when binary lookup would fail', () => {
applySpy.mockReturnValue(null)

const result = checkLinuxDependencies({
argv0: 'apply-seccomp',
applyPath: '/proc/self/fd/3',
})

expect(result.warnings).toEqual([])
expect(applySpy).not.toHaveBeenCalled()
})
})

describe('getLinuxDependencyStatus', () => {
Expand Down Expand Up @@ -123,4 +135,16 @@ describe('getLinuxDependencyStatus', () => {
expect(status.hasBwrap).toBe(true)
expect(status.hasSocat).toBe(true)
})

test('argv0 mode: hasSeccompApply is true without touching disk', () => {
applySpy.mockReturnValue(null)

const status = getLinuxDependencyStatus({
argv0: 'apply-seccomp',
applyPath: '/does/not/exist',
})

expect(status.hasSeccompApply).toBe(true)
expect(applySpy).not.toHaveBeenCalled()
})
})
40 changes: 40 additions & 0 deletions test/sandbox/seccomp-filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,44 @@ describe.if(isLinux)('Sandbox Integration', () => {

expect(wrappedCommand).toContain(real)
})

it('argv0 mode: builds ARGV0 prefix and uses applyPath verbatim', async () => {
if (checkLinuxDependencies().errors.length > 0) return

const wrappedCommand = await wrapCommandWithSandboxLinux({
command: 'echo test',
needsNetworkRestriction: false,
writeConfig: { allowOnly: ['/tmp'], denyWithinAllow: [] },
seccompConfig: { argv0: 'apply-seccomp', applyPath: '/proc/self/fd/3' },
})

expect(wrappedCommand).toContain('ARGV0=apply-seccomp /proc/self/fd/3 ')
expect(wrappedCommand).not.toContain('vendor/seccomp')
})

it('argv0 mode: shell-quotes argv0 and applyPath', async () => {
if (checkLinuxDependencies().errors.length > 0) return

const wrappedCommand = await wrapCommandWithSandboxLinux({
command: 'echo test',
needsNetworkRestriction: false,
writeConfig: { allowOnly: ['/tmp'], denyWithinAllow: [] },
seccompConfig: { argv0: 'x; rm -rf /', applyPath: '/path with space' },
})

expect(wrappedCommand).toContain("ARGV0='x; rm -rf /' '/path with space' ")
})

it('argv0 mode: rejects argv0 without applyPath', () => {
if (checkLinuxDependencies().errors.length > 0) return

expect(
wrapCommandWithSandboxLinux({
command: 'echo test',
needsNetworkRestriction: false,
writeConfig: { allowOnly: ['/tmp'], denyWithinAllow: [] },
seccompConfig: { argv0: 'apply-seccomp' },
}),
).rejects.toThrow('seccompConfig.argv0 requires seccompConfig.applyPath')
})
})
Loading