/** * @license * Copyright 2026 Google LLC * 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, type SandboxedCommand, } from '../../services/sandboxManager.js'; import { sanitizeEnvironment, getSecureSanitizationConfig, 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. */ export interface LinuxSandboxOptions { /** The primary workspace path to bind into the sandbox. */ workspace: string; /** Additional paths to bind into the sandbox. */ allowedPaths?: string[]; /** Optional base sanitization config. */ sanitizationConfig?: EnvironmentSanitizationConfig; } /** * A SandboxManager implementation for Linux that uses Bubblewrap (bwrap). */ export class LinuxSandboxManager implements SandboxManager { constructor(private readonly options: LinuxSandboxOptions) {} async prepareCommand(req: SandboxRequest): Promise { const sanitizationConfig = getSecureSanitizationConfig( req.config?.sanitizationConfig, this.options.sanitizationConfig, ); const sanitizedEnv = sanitizeEnvironment(req.env, sanitizationConfig); const bwrapArgs: string[] = [ '--unshare-all', '--new-session', // Isolate session '--die-with-parent', // Prevent orphaned runaway processes '--ro-bind', '/', '/', '--dev', // Creates a safe, minimal /dev (replaces --dev-bind) '/dev', '--proc', // Creates a fresh procfs for the unshared PID namespace '/proc', '--tmpfs', // Provides an isolated, writable /tmp directory '/tmp', // Note: --dev /dev sets up /dev/pts automatically '--bind', this.options.workspace, this.options.workspace, ]; const allowedPaths = this.options.allowedPaths ?? []; for (const path of allowedPaths) { if (path !== this.options.workspace) { bwrapArgs.push('--bind', path, path); } } 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: 'sh', args: shArgs, env: sanitizedEnv, }; } }