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
2 changes: 1 addition & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
runner: ubuntu-24.04-arm
os: linux
- arch: x86-64
runner: macos-13
runner: macos-15-large
os: macos
- arch: arm64
runner: macos-14
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@anthropic-ai/sandbox-runtime",
"version": "0.0.19",
"version": "0.0.20",
"description": "Anthropic Sandbox Runtime (ASRT) - A general-purpose tool for wrapping security boundaries around arbitrary processes",
"type": "module",
"main": "./dist/index.js",
Expand Down
4 changes: 2 additions & 2 deletions src/sandbox/linux-sandbox-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ async function linuxGetMandatoryDenyPaths(
}
// Git hooks always blocked in nested repos
iglobArgs.push('--iglob', '**/.git/hooks/**')

// Git config conditionally blocked in nested repos
if (!allowGitConfig) {
iglobArgs.push('--iglob', '**/.git/config')
Expand Down Expand Up @@ -660,7 +660,7 @@ export async function wrapCommandWithSandboxLinux(
return command
}

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

try {
Expand Down
102 changes: 99 additions & 3 deletions test/sandbox/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,102 @@ describe('Sandbox Integration Tests', () => {
expect(endTime - startTime).toBeLessThan(4000)
})

it('should kill child processes when sandbox is terminated via SIGTERM (--die-with-parent)', async () => {
if (skipIfNotLinux()) {
return
}

// This test verifies the --die-with-parent flag is working.
// Without it, child processes would continue running after timeout kills bwrap.
const markerFile = join(TEST_DIR, 'sigterm-test-marker.txt')

if (existsSync(markerFile)) {
unlinkSync(markerFile)
}

// Start a long-running process that we'll kill with timeout
// The process writes to a file every 0.2s - if it survives the kill,
// we'll see many more writes than expected
const command = await SandboxManager.wrapWithSandbox(
`for i in $(seq 1 50); do echo "tick $i" >> ${markerFile}; sleep 0.2; done`,
)

// Use timeout to kill the sandbox after 1 second
// The inner command would take 10 seconds to complete
const result = spawnSync('timeout', ['1', 'bash', '-c', command], {
encoding: 'utf8',
cwd: TEST_DIR,
timeout: 5000,
})

// timeout returns 124 when it kills the process
expect(result.status).toBe(124)

// Wait a bit to let any orphaned processes continue (if they weren't killed)
await new Promise(resolve => setTimeout(resolve, 1500))

// Check how many ticks were written
if (existsSync(markerFile)) {
const content = readFileSync(markerFile, 'utf8')
const lines = content.trim().split('\n').length

// With 1 second timeout and 0.2s per tick, we expect ~5 ticks
// If child survived, we'd see 5 + 7 (1.5s more) = 12+ ticks
// Allow some margin for timing variance
expect(lines).toBeLessThan(10)

unlinkSync(markerFile)
}
})

it('should not leave orphan processes after timeout kills sandbox', async () => {
if (skipIfNotLinux()) {
return
}

// Create a unique marker that only our test process would have
const uniqueMarker = `sandbox-orphan-test-${Date.now()}`
const markerFile = join(TEST_DIR, 'orphan-test.txt')

if (existsSync(markerFile)) {
unlinkSync(markerFile)
}

// Start a process that will be killed by timeout
// Use a distinctive command that we can grep for
const command = await SandboxManager.wrapWithSandbox(
`export ORPHAN_MARKER="${uniqueMarker}"; while true; do echo "$ORPHAN_MARKER" >> ${markerFile}; sleep 0.5; done`,
)

// Kill after 0.5 seconds
spawnSync('timeout', ['0.5', 'bash', '-c', command], {
encoding: 'utf8',
cwd: TEST_DIR,
timeout: 3000,
})

// Wait to see if any orphan continues
await new Promise(resolve => setTimeout(resolve, 1500))

// Check for orphan processes with our marker
const psResult = spawnSync(
'bash',
['-c', `ps aux | grep "${uniqueMarker}" | grep -v grep || true`],
{
encoding: 'utf8',
timeout: 2000,
},
)

// Should not find any orphan processes
expect(psResult.stdout.trim()).toBe('')

// Cleanup
if (existsSync(markerFile)) {
unlinkSync(markerFile)
}
})

it('should prevent privilege escalation attempts', async () => {
if (skipIfNotLinux()) {
return
Expand Down Expand Up @@ -1029,7 +1125,7 @@ describe('Empty allowedDomains Network Blocking Integration', () => {
// Network should fail - either connection error, timeout, or "network_failed" echo
const networkBlocked =
output.includes('network_failed') ||
output.includes('couldn\'t connect') ||
output.includes("couldn't connect") ||
output.includes('connection refused') ||
output.includes('network is unreachable') ||
output.includes('name or service not known') ||
Expand Down Expand Up @@ -1064,7 +1160,7 @@ describe('Empty allowedDomains Network Blocking Integration', () => {
// Network should fail
const networkBlocked =
output.includes('network_failed') ||
output.includes('couldn\'t connect') ||
output.includes("couldn't connect") ||
output.includes('connection refused') ||
output.includes('network is unreachable') ||
output.includes('name or service not known') ||
Expand Down Expand Up @@ -1258,7 +1354,7 @@ describe('Empty allowedDomains Network Blocking Integration', () => {
const output = (result.stdout + result.stderr).toLowerCase()
const isBlocked =
output.includes('blocked') ||
output.includes('couldn\'t connect') ||
output.includes("couldn't connect") ||
output.includes('network is unreachable') ||
result.status !== 0

Expand Down