Linux sandbox seccomp (#22815)

Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
This commit is contained in:
David Pierce
2026-03-17 20:29:13 +00:00
committed by GitHub
parent 82d8680dcc
commit 2f90b46537
3 changed files with 99 additions and 11 deletions

View File

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

View File

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

View File

@@ -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']);
}
}