From 2f90b46537ceb1c63b7896e9390b15fc5cfa4399 Mon Sep 17 00:00:00 2001 From: David Pierce Date: Tue, 17 Mar 2026 20:29:13 +0000 Subject: [PATCH] Linux sandbox seccomp (#22815) Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com> --- .../sandbox/linux/LinuxSandboxManager.test.ts | 28 ++++++- .../src/sandbox/linux/LinuxSandboxManager.ts | 76 ++++++++++++++++++- .../services/FolderTrustDiscoveryService.ts | 6 +- 3 files changed, 99 insertions(+), 11 deletions(-) diff --git a/packages/core/src/sandbox/linux/LinuxSandboxManager.test.ts b/packages/core/src/sandbox/linux/LinuxSandboxManager.test.ts index 05e19f66b1..4b1237b167 100644 --- a/packages/core/src/sandbox/linux/LinuxSandboxManager.test.ts +++ b/packages/core/src/sandbox/linux/LinuxSandboxManager.test.ts @@ -22,8 +22,16 @@ describe('LinuxSandboxManager', () => { const result = await manager.prepareCommand(req); - expect(result.program).toBe('bwrap'); - expect(result.args).toEqual([ + expect(result.program).toBe('sh'); + expect(result.args[0]).toBe('-c'); + expect(result.args[1]).toBe( + 'bpf_path="$1"; shift; exec bwrap "$@" 9< "$bpf_path"', + ); + expect(result.args[2]).toBe('_'); + expect(result.args[3]).toMatch(/gemini-cli-seccomp-.*\.bpf$/); + + const bwrapArgs = result.args.slice(4); + expect(bwrapArgs).toEqual([ '--unshare-all', '--new-session', '--die-with-parent', @@ -39,6 +47,8 @@ describe('LinuxSandboxManager', () => { '--bind', workspace, workspace, + '--seccomp', + '9', '--', 'ls', '-la', @@ -59,8 +69,16 @@ describe('LinuxSandboxManager', () => { const result = await manager.prepareCommand(req); - expect(result.program).toBe('bwrap'); - expect(result.args).toEqual([ + expect(result.program).toBe('sh'); + expect(result.args[0]).toBe('-c'); + expect(result.args[1]).toBe( + 'bpf_path="$1"; shift; exec bwrap "$@" 9< "$bpf_path"', + ); + expect(result.args[2]).toBe('_'); + expect(result.args[3]).toMatch(/gemini-cli-seccomp-.*\.bpf$/); + + const bwrapArgs = result.args.slice(4); + expect(bwrapArgs).toEqual([ '--unshare-all', '--new-session', '--die-with-parent', @@ -82,6 +100,8 @@ describe('LinuxSandboxManager', () => { '--bind', '/opt/tools', '/opt/tools', + '--seccomp', + '9', '--', 'node', 'script.js', diff --git a/packages/core/src/sandbox/linux/LinuxSandboxManager.ts b/packages/core/src/sandbox/linux/LinuxSandboxManager.ts index 0a6287b259..db75eb2dfa 100644 --- a/packages/core/src/sandbox/linux/LinuxSandboxManager.ts +++ b/packages/core/src/sandbox/linux/LinuxSandboxManager.ts @@ -4,6 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { join } from 'node:path'; +import { writeFileSync } from 'node:fs'; +import os from 'node:os'; import { type SandboxManager, type SandboxRequest, @@ -15,6 +18,64 @@ import { type EnvironmentSanitizationConfig, } from '../../services/environmentSanitization.js'; +let cachedBpfPath: string | undefined; + +function getSeccompBpfPath(): string { + if (cachedBpfPath) return cachedBpfPath; + + const arch = os.arch(); + let AUDIT_ARCH: number; + let SYS_ptrace: number; + + if (arch === 'x64') { + AUDIT_ARCH = 0xc000003e; // AUDIT_ARCH_X86_64 + SYS_ptrace = 101; + } else if (arch === 'arm64') { + AUDIT_ARCH = 0xc00000b7; // AUDIT_ARCH_AARCH64 + SYS_ptrace = 117; + } else if (arch === 'arm') { + AUDIT_ARCH = 0x40000028; // AUDIT_ARCH_ARM + SYS_ptrace = 26; + } else if (arch === 'ia32') { + AUDIT_ARCH = 0x40000003; // AUDIT_ARCH_I386 + SYS_ptrace = 26; + } else { + throw new Error(`Unsupported architecture for seccomp filter: ${arch}`); + } + + const EPERM = 1; + const SECCOMP_RET_KILL_PROCESS = 0x80000000; + const SECCOMP_RET_ERRNO = 0x00050000; + const SECCOMP_RET_ALLOW = 0x7fff0000; + + const instructions = [ + { code: 0x20, jt: 0, jf: 0, k: 4 }, // Load arch + { code: 0x15, jt: 1, jf: 0, k: AUDIT_ARCH }, // Jump to kill if arch != native arch + { code: 0x06, jt: 0, jf: 0, k: SECCOMP_RET_KILL_PROCESS }, // Kill + + { code: 0x20, jt: 0, jf: 0, k: 0 }, // Load nr + { code: 0x15, jt: 0, jf: 1, k: SYS_ptrace }, // If ptrace, jump to ERRNO + { code: 0x06, jt: 0, jf: 0, k: SECCOMP_RET_ERRNO | EPERM }, // ERRNO + + { code: 0x06, jt: 0, jf: 0, k: SECCOMP_RET_ALLOW }, // Allow + ]; + + const buf = Buffer.alloc(8 * instructions.length); + for (let i = 0; i < instructions.length; i++) { + const inst = instructions[i]; + const offset = i * 8; + buf.writeUInt16LE(inst.code, offset); + buf.writeUInt8(inst.jt, offset + 2); + buf.writeUInt8(inst.jf, offset + 3); + buf.writeUInt32LE(inst.k, offset + 4); + } + + const bpfPath = join(os.tmpdir(), `gemini-cli-seccomp-${process.pid}.bpf`); + writeFileSync(bpfPath, buf); + cachedBpfPath = bpfPath; + return bpfPath; +} + /** * Options for configuring the LinuxSandboxManager. */ @@ -67,11 +128,22 @@ export class LinuxSandboxManager implements SandboxManager { } } + const bpfPath = getSeccompBpfPath(); + + bwrapArgs.push('--seccomp', '9'); bwrapArgs.push('--', req.command, ...req.args); + const shArgs = [ + '-c', + 'bpf_path="$1"; shift; exec bwrap "$@" 9< "$bpf_path"', + '_', + bpfPath, + ...bwrapArgs, + ]; + return { - program: 'bwrap', - args: bwrapArgs, + program: 'sh', + args: shArgs, env: sanitizedEnv, }; } diff --git a/packages/core/src/services/FolderTrustDiscoveryService.ts b/packages/core/src/services/FolderTrustDiscoveryService.ts index 09e32210a8..499077d33f 100644 --- a/packages/core/src/services/FolderTrustDiscoveryService.ts +++ b/packages/core/src/services/FolderTrustDiscoveryService.ts @@ -163,11 +163,7 @@ export class FolderTrustDiscoveryService { for (const event of Object.values(hooksConfig)) { if (!Array.isArray(event)) continue; for (const hook of event) { - if ( - this.isRecord(hook) && - // eslint-disable-next-line no-restricted-syntax - typeof hook['command'] === 'string' - ) { + if (this.isRecord(hook) && typeof hook['command'] === 'string') { hooks.add(hook['command']); } }