mirror of
https://github.com/openai/codex.git
synced 2026-09-16 12:13:30 +00:00
feat: use Landlock for sandboxing on Linux
This commit is contained in:
@@ -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<ExecResult> {
|
||||
// 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(
|
||||
|
||||
@@ -303,6 +303,11 @@ async function getSandbox(runInSandbox: boolean): Promise<SandboxType> {
|
||||
"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.
|
||||
|
||||
114
codex-cli/src/utils/agent/sandbox/landlock.ts
Normal file
114
codex-cli/src/utils/agent/sandbox/landlock.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import type { ExecResult } from "./interface.js";
|
||||
import type { SpawnOptions } from "child_process";
|
||||
|
||||
import { exec } from "./raw-exec.js";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
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<string>,
|
||||
opts: SpawnOptions,
|
||||
userProvidedWritableRoots: ReadonlyArray<string>,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<ExecResult> {
|
||||
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<string> | null = null;
|
||||
|
||||
async function detectSandboxExecutable(): Promise<string> {
|
||||
// Map Node-reported architectures to the corresponding binary name.
|
||||
const exeBaseName: 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";
|
||||
}
|
||||
})();
|
||||
|
||||
// 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 candidate = path.join(dir, "bin", exeBaseName);
|
||||
try {
|
||||
await fs.promises.access(candidate, fs.constants.X_OK);
|
||||
return candidate;
|
||||
} catch {
|
||||
throw new Error(`${candidate} not found or not executable`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the architecture-specific Landlock helper
|
||||
* binary. (Could be a rejected promise if not found.)
|
||||
*/
|
||||
function getSandboxExecutable(): Promise<string> {
|
||||
if (!sandboxExecutablePromise) {
|
||||
sandboxExecutablePromise = detectSandboxExecutable();
|
||||
}
|
||||
|
||||
return sandboxExecutablePromise;
|
||||
}
|
||||
Reference in New Issue
Block a user