diff --git a/codex-cli/src/utils/agent/exec.ts b/codex-cli/src/utils/agent/exec.ts index 3a0e653d..79fe6374 100644 --- a/codex-cli/src/utils/agent/exec.ts +++ b/codex-cli/src/utils/agent/exec.ts @@ -4,6 +4,7 @@ import type { ParseEntry } from "shell-quote"; import { process_patch } from "./apply-patch.js"; import { SandboxType } from "./sandbox/interface.js"; +import { execWithLandlock } from "./sandbox/landlock.js"; import { execWithSeatbelt } from "./sandbox/macos-seatbelt.js"; import { exec as rawExec } from "./sandbox/raw-exec.js"; import { formatCommandForDisplay } from "../../format-command.js"; @@ -42,26 +43,30 @@ export function exec( sandbox: SandboxType, abortSignal?: AbortSignal, ): Promise { - // This is a temporary measure to understand what are the common base commands - // until we start persisting and uploading rollouts - const opts: SpawnOptions = { timeout: timeoutInMillis || DEFAULT_TIMEOUT_MS, ...(requiresShell(cmd) ? { shell: true } : {}), ...(workdir ? { cwd: workdir } : {}), }; - // Merge default writable roots with any user-specified ones. - const writableRoots = [ - process.cwd(), - os.tmpdir(), - ...additionalWritableRoots, - ]; - if (sandbox === SandboxType.MACOS_SEATBELT) { - return execWithSeatbelt(cmd, opts, writableRoots, abortSignal); - } - // SandboxType.NONE (or any other) falls back to the raw exec implementation - return rawExec(cmd, opts, abortSignal); + switch (sandbox) { + case SandboxType.NONE: { + // SandboxType.NONE uses the raw exec implementation. + return rawExec(cmd, opts, abortSignal); + } + case SandboxType.MACOS_SEATBELT: { + // Merge default writable roots with any user-specified ones. + const writableRoots = [ + process.cwd(), + os.tmpdir(), + ...additionalWritableRoots, + ]; + return execWithSeatbelt(cmd, opts, writableRoots, abortSignal); + } + case SandboxType.LINUX_LANDLOCK: { + return execWithLandlock(cmd, opts, additionalWritableRoots, abortSignal); + } + } } export function execApplyPatch( diff --git a/codex-cli/src/utils/agent/handle-exec-command.ts b/codex-cli/src/utils/agent/handle-exec-command.ts index ec0ba617..44a5d48f 100644 --- a/codex-cli/src/utils/agent/handle-exec-command.ts +++ b/codex-cli/src/utils/agent/handle-exec-command.ts @@ -303,6 +303,11 @@ async function getSandbox(runInSandbox: boolean): Promise { "Sandbox was mandated, but 'sandbox-exec' was not found in PATH!", ); } + } else if (process.platform === "linux") { + // TODO: Need to verify that the Landlock sandbox is working. For example, + // using Landlock in a Linux Docker container from a macOS host may not + // work. + return SandboxType.LINUX_LANDLOCK; } else if (CODEX_UNSAFE_ALLOW_NO_SANDBOX) { // Allow running without a sandbox if the user has explicitly marked the // environment as already being sufficiently locked-down. diff --git a/codex-cli/src/utils/agent/sandbox/landlock.ts b/codex-cli/src/utils/agent/sandbox/landlock.ts new file mode 100644 index 00000000..465b27fd --- /dev/null +++ b/codex-cli/src/utils/agent/sandbox/landlock.ts @@ -0,0 +1,173 @@ +import type { ExecResult } from "./interface.js"; +import type { SpawnOptions } from "child_process"; + +import { exec } from "./raw-exec.js"; +import { execFile } from "child_process"; +import fs from "fs"; +import path from "path"; +import { log } from "src/utils/logger/log.js"; +import { fileURLToPath } from "url"; + +/** + * Runs Landlock with the following permissions: + * - can read any file on disk + * - can write to process.cwd() + * - can write to the platform user temp folder + * - can write to any user-provided writable root + */ +export async function execWithLandlock( + cmd: Array, + opts: SpawnOptions, + userProvidedWritableRoots: ReadonlyArray, + abortSignal?: AbortSignal, +): Promise { + const sandboxExecutable = await getSandboxExecutable(); + + const extraSandboxPermissions = userProvidedWritableRoots.flatMap( + (root: string) => ["--sandbox-permission", `disk-write-folder=${root}`], + ); + + const fullCommand = [ + sandboxExecutable, + "--sandbox-permission", + "disk-full-read-access", + + "--sandbox-permission", + "disk-write-cwd", + + "--sandbox-permission", + "disk-write-platform-user-temp-folder", + + ...extraSandboxPermissions, + + "--", + ...cmd, + ]; + + return exec(fullCommand, opts, abortSignal); +} + +/** + * Lazily initialized promise that resolves to the absolute path of the + * architecture-specific Landlock helper binary. + */ +let sandboxExecutablePromise: Promise | null = null; + +async function detectSandboxExecutable(): Promise { + // Find the executable relative to the package.json file. + const __filename = fileURLToPath(import.meta.url); + let dir: string = path.dirname(__filename); + + // Ascend until package.json is found or we reach the filesystem root. + // eslint-disable-next-line no-constant-condition + while (true) { + try { + // eslint-disable-next-line no-await-in-loop + await fs.promises.access( + path.join(dir, "package.json"), + fs.constants.F_OK, + ); + break; // Found the package.json ⇒ dir is our project root. + } catch { + // keep searching + } + + const parent = path.dirname(dir); + if (parent === dir) { + throw new Error("Unable to locate package.json"); + } + dir = parent; + } + + const sandboxExecutable = getLinuxSandboxExecutableForCurrentArchitecture(); + const candidate = path.join(dir, "bin", sandboxExecutable); + try { + await fs.promises.access(candidate, fs.constants.X_OK); + } catch { + throw new Error(`${candidate} not found or not executable`); + } + + // Will throw if the executable is not working in this environment. + await verifySandboxExecutable(candidate); + return candidate; +} + +const ERROR_WHEN_LANDLOCK_NOT_SUPPORTED = `\ +The combination of seccomp/landlock that Codex uses for sandboxing is not +supported in this environment. + +If you are running in a Docker container, you may want to try adding +restrictions to your Docker container such that it provides your desired +sandboxing guarantees and then run Codex with the +--dangerously-auto-approve-everything option inside the container. + +If you are running on an older Linux kernel that does not support newer +features of seccomp/landlock, you will have to update your kernel to a newer +version. +`; + +/** + * Now that we have the path to the executable, make sure that it works in + * this environment. For example, when running a Linux Docker container from + * macOS like so: + * + * docker run -it alpine:latest /bin/sh + * + * Running `codex-linux-sandbox-x64 -- true` in the container fails with: + * + * ``` + * Error: sandbox error: seccomp setup error + * + * Caused by: + * 0: seccomp setup error + * 1: Error calling `seccomp`: Invalid argument (os error 22) + * 2: Invalid argument (os error 22) + * ``` + */ +function verifySandboxExecutable(sandboxExecutable: string): Promise { + // Note we are running `true` rather than `bash -lc true` because we want to + // ensure we run an executable, not a shell built-in. Note that `true` should + // always be available in a POSIX environment. + return new Promise((resolve, reject) => { + const args = ["--", "true"]; + execFile(sandboxExecutable, args, (error, stdout, stderr) => { + if (error) { + log( + `Sandbox check failed for ${sandboxExecutable} ${args.join(" ")}: ${error}`, + ); + log(`stdout: ${stdout}`); + log(`stderr: ${stderr}`); + reject(new Error(ERROR_WHEN_LANDLOCK_NOT_SUPPORTED)); + } else { + resolve(); + } + }); + }); +} + +/** + * Returns the absolute path to the architecture-specific Landlock helper + * binary. (Could be a rejected promise if not found.) + */ +function getSandboxExecutable(): Promise { + if (!sandboxExecutablePromise) { + sandboxExecutablePromise = detectSandboxExecutable(); + } + + return sandboxExecutablePromise; +} + +/** @return name of the native executable to use for Linux sandboxing. */ +function getLinuxSandboxExecutableForCurrentArchitecture(): string { + switch (process.arch) { + case "arm64": + return "codex-linux-sandbox-arm64"; + case "x64": + return "codex-linux-sandbox-x64"; + // Fall back to the x86_64 build for anything else – it will obviously + // fail on incompatible systems but gives a sane error message rather + // than crashing earlier. + default: + return "codex-linux-sandbox-x64"; + } +}