mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-26 14:01:14 -07:00
151 lines
4.3 KiB
TypeScript
151 lines
4.3 KiB
TypeScript
/**
|
|
* @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<SandboxedCommand> {
|
|
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,
|
|
};
|
|
}
|
|
}
|