Files
codex-action/test/dropSudo.test.mjs
T
viyatb-oaiandCodex 86365089eb
Verify TypeScript Build / verify (push) Failing after 1m13s
fix: improve runner setup and configuration handling
Co-authored-by: Codex <[email protected]>
2026-08-20 16:38:51 -07:00

621 lines
20 KiB
JavaScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import {
chmodSync,
copyFileSync,
existsSync,
mkdtempSync,
readFileSync,
statSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { test } from "node:test";
import { fileURLToPath } from "node:url";
const mainPath = fileURLToPath(new URL("../dist/main.js", import.meta.url));
const dockerSocket = "/var/run/docker.sock";
const canTestLinuxIsolation =
process.platform === "linux" &&
process.getuid?.() !== 0 &&
existsSync(dockerSocket) &&
spawnSync("sudo", ["-n", "true"]).status === 0;
const canTestSocketAcls =
canTestLinuxIsolation &&
spawnSync("/usr/bin/setfacl", ["--version"], { stdio: "ignore" }).status === 0;
function sudo(args) {
const result = spawnSync("sudo", ["-n", "--", ...args], {
encoding: "utf8",
timeout: 30_000,
});
assert.equal(result.status, 0, result.stderr);
return result.stdout.trim();
}
function snapshotRootSockets() {
const output = sudo([
"find",
"/run",
"-type",
"s",
"-uid",
"0",
"-printf",
"%p\t%U\t%G\t%m\n",
]);
return output.length === 0
? []
: output.split("\n").map((line) => {
const [socketPath, uid, gid, mode] = line.split("\t");
return { socketPath, uid, gid, mode };
});
}
function restoreRootSockets(sockets) {
for (const { socketPath, uid, gid, mode } of sockets) {
const exists = spawnSync("sudo", ["-n", "--", "test", "-S", socketPath]);
if (exists.status === 0) {
sudo(["chown", `${uid}:${gid}`, socketPath]);
sudo(["chmod", mode, socketPath]);
}
}
}
function createRootSocket(socketPath, group, mode) {
sudo([
process.execPath,
"-e",
'require("node:net").createServer().listen(process.argv[1], () => process.exit(0))',
socketPath,
]);
sudo(["chown", `0:${group}`, socketPath]);
sudo(["chmod", mode, socketPath]);
}
test(
"drop-sudo closes inherited Linux credential and service-socket bypasses",
{ skip: !canTestLinuxIsolation, timeout: 300_000 },
async (t) => {
if (!canTestSocketAcls) {
t.diagnostic("setfacl unavailable; named socket ACL coverage skipped");
}
for (const [index, scenario] of [
{
name: "workspace sandbox preserves compatible workflow arguments",
extraArgs: [
"--enable",
"use_legacy_landlock",
"--search",
"--ephemeral",
"--model",
"gpt-5.4",
"-c",
'model_reasoning_effort="high"',
"--config",
'service_tier="fast"',
],
verifyForwardedArgs: true,
},
{ name: "stale live groups cannot retain socket access", staleGroups: true },
{
name: "permission profiles cannot gain privileges",
profile: ":workspace",
},
{
name: "built-in read-only profiles cannot gain privileges",
profile: ":read-only",
},
{
name: "named permission profiles cannot gain privileges",
profile: "public-review",
},
{
name: "unconfined execution cannot gain privileges",
sandbox: "danger-full-access",
},
{
name: "the unconfined permission profile cannot gain privileges",
profile: ":danger-full-access",
},
{
name: "the explicit sandbox bypass cannot gain privileges",
extraArgs: ["--dangerously-bypass-approvals-and-sandbox"],
},
{
name: "the sandbox bypass alias cannot gain privileges",
extraArgs: ["--yolo"],
},
{
name: "hook trust bypasses fail before irreversible cleanup",
extraArgs: ["--dangerously-bypass-hook-trust"],
rejectedBeforeCleanup: true,
},
{
name: "read-only full-auto conflicts fail before irreversible cleanup",
sandbox: "read-only",
extraArgs: ["--full-auto"],
rejectedBeforeCleanup: true,
},
{
name: "MCP command overrides fail before irreversible cleanup",
extraArgs: ["--config", 'mcp_servers.evil.command="/bin/sh"'],
rejectedBeforeCleanup: true,
},
{
name: "the replacement primary group cannot inherit sudo policy",
replacementGroupSudoGrant: true,
},
{
name: "a command-specific User_Alias cannot gain privileges",
userAliasSudoGrant: true,
},
{ name: "remaining sudo grants fail closed", remainingSudoGrant: true },
].entries()) {
await t.test(scenario.name, { timeout: 45_000 }, (subtest) => {
const suffix = `${process.pid}${Date.now().toString(36)}${index}`;
const user = `codexdrop${suffix}`;
const privilegedGroup = `cdxp${suffix}`;
const worldGroup = `cdxw${suffix}`;
const remainingGroup = `cdxr${suffix}`;
const tempDir = mkdtempSync(
path.join(tmpdir(), "codex-action-drop-sudo-")
);
const capturePath = path.join(tempDir, "capture.json");
const outputPath = path.join(tempDir, "output.md");
const codexPath = path.join(tempDir, "codex");
const bundledActionPath = path.join(tempDir, "main.js");
const sudoersPath = `/etc/sudoers.d/${user}`;
const serviceSocket = `/run/codex-action-${suffix}-service.sock`;
const worldSocket = `/run/codex-action-${suffix}-world.sock`;
const fallbackSocket = `/run/codex-action-${suffix}-fallback.sock`;
const namedUserAclSocket = `/run/codex-action-${suffix}-acl-user.sock`;
const fallbackAclSocket = `/run/codex-action-${suffix}-acl-fallback.sock`;
const staleAclSocket = `/run/codex-action-${suffix}-acl-stale.sock`;
const liveGroupsPath = path.join(tempDir, "live-groups.txt");
const originalDockerSocket = statSync(dockerSocket);
const safeGroup = Number(sudo(["id", "-g", "nobody"]));
const safeGroupName = sudo(["id", "-gn", "nobody"]);
const sudoAlias = `CODEX_DROP_${suffix}`.toUpperCase();
const groupsCreated = [];
let userCreated = false;
let originalSockets = [];
subtest.after(() => {
sudo(["rm", "-f", sudoersPath]);
restoreRootSockets(originalSockets);
sudo([
"rm",
"-f",
serviceSocket,
worldSocket,
fallbackSocket,
namedUserAclSocket,
fallbackAclSocket,
staleAclSocket,
]);
if (userCreated) {
sudo(["userdel", user]);
}
for (const group of groupsCreated) {
sudo(["groupdel", group]);
}
sudo(["rm", "-rf", tempDir]);
});
for (const group of [
privilegedGroup,
worldGroup,
...(scenario.remainingSudoGrant ? [remainingGroup] : []),
]) {
sudo(["groupadd", group]);
groupsCreated.push(group);
}
const supplementaryGroups = [
"sudo",
privilegedGroup,
...(scenario.remainingSudoGrant ? [remainingGroup] : []),
];
sudo([
"useradd",
"--no-create-home",
"--gid",
String(originalDockerSocket.gid),
"--groups",
supplementaryGroups.join(","),
user,
]);
userCreated = true;
const userId = Number(sudo(["id", "-u", user]));
const sudoersSource = path.join(tempDir, "sudoers");
const sudoRules = [`${user} ALL=(ALL) NOPASSWD:ALL`];
if (scenario.remainingSudoGrant) {
sudoRules.push(`%${remainingGroup} ALL=(ALL) NOPASSWD:ALL`);
}
if (scenario.replacementGroupSudoGrant) {
sudoRules.push(
`%${safeGroupName} ALL=(root) NOPASSWD: /usr/bin/true`
);
}
if (scenario.userAliasSudoGrant) {
sudoRules.push(
`User_Alias ${sudoAlias} = ${user}`,
`${sudoAlias} ALL=(root) NOPASSWD: /usr/bin/id -u`
);
}
writeFileSync(
sudoersSource,
`${sudoRules.join("\n")}\n`
);
sudo(["install", "--mode=0440", sudoersSource, sudoersPath]);
createRootSocket(serviceSocket, privilegedGroup, "0660");
createRootSocket(worldSocket, worldGroup, "0666");
createRootSocket(fallbackSocket, String(safeGroup), "0660");
if (canTestSocketAcls) {
createRootSocket(namedUserAclSocket, worldGroup, "0660");
createRootSocket(fallbackAclSocket, worldGroup, "0660");
sudo([
"/usr/bin/setfacl",
"-m",
`u:${user}:rw`,
namedUserAclSocket,
]);
sudo([
"/usr/bin/setfacl",
"-m",
`g:${safeGroup}:rw`,
fallbackAclSocket,
]);
if (scenario.staleGroups) {
createRootSocket(staleAclSocket, worldGroup, "0660");
sudo(["/usr/bin/setfacl", "-m", `g:${privilegedGroup}:rw`, staleAclSocket]);
}
}
originalSockets = snapshotRootSockets();
for (const socket of [
dockerSocket,
serviceSocket,
worldSocket,
...(canTestSocketAcls ? [namedUserAclSocket] : []),
...(canTestSocketAcls && scenario.staleGroups ? [staleAclSocket] : []),
]) {
sudo(["/usr/bin/sudo", "-n", "-u", user, "--", "test", "-w", socket]);
}
assert.notEqual(
spawnSync("sudo", [
"-n",
"-u",
user,
"--",
"test",
"-w",
fallbackSocket,
]).status,
0
);
if (canTestSocketAcls) {
assert.notEqual(
spawnSync("sudo", [
"-n",
"-u",
user,
"--",
"test",
"-w",
fallbackAclSocket,
]).status,
0
);
sudo([
"/usr/bin/setpriv",
`--reuid=${userId}`,
`--regid=${safeGroup}`,
"--clear-groups",
"--",
"/usr/bin/test",
"-w",
fallbackAclSocket,
]);
}
copyFileSync(mainPath, bundledActionPath);
writeFileSync(
codexPath,
`#!${process.execPath}
const { accessSync, constants, readFileSync, writeFileSync } = require("node:fs");
const { spawnSync } = require("node:child_process");
const args = process.argv.slice(2);
const output = args[args.indexOf("--output-last-message") + 1];
function canWrite(socket) {
try {
accessSync(socket, constants.W_OK);
return true;
} catch {
return false;
}
}
const status = Object.fromEntries(
readFileSync("/proc/self/status", "utf8")
.split("\\n")
.map((line) => line.split(/:\\s*/, 2))
);
const child = spawnSync(process.execPath, ["-p", "process.env.NODE_OPTIONS"], {
encoding: "utf8",
});
const sudoTrue = spawnSync("/usr/bin/sudo", ["-n", "/usr/bin/true"], {
encoding: "utf8",
});
const sudoId = spawnSync("/usr/bin/sudo", ["-n", "/usr/bin/id", "-u"], {
encoding: "utf8",
});
writeFileSync(process.env.CODEX_CAPTURE_PATH, JSON.stringify({
args,
uid: process.getuid(),
gid: process.getgid(),
groups: process.getgroups(),
supplementaryGroups: status.Groups.trim(),
dockerAccessible: canWrite("/var/run/docker.sock"),
serviceAccessible: canWrite(process.env.CODEX_SERVICE_SOCKET),
worldAccessible: canWrite(process.env.CODEX_WORLD_SOCKET),
fallbackAccessible: canWrite(process.env.CODEX_FALLBACK_SOCKET),
namedUserAclAccessible: process.env.CODEX_NAMED_USER_ACL_SOCKET
? canWrite(process.env.CODEX_NAMED_USER_ACL_SOCKET)
: null,
fallbackAclAccessible: process.env.CODEX_FALLBACK_ACL_SOCKET
? canWrite(process.env.CODEX_FALLBACK_ACL_SOCKET)
: null,
sudoStatus: sudoTrue.status,
sudoIdStatus: sudoId.status,
sudoIdStdout: sudoId.stdout.trim(),
noNewPrivs: status.NoNewPrivs,
capBounding: status.CapBnd,
capEffective: status.CapEff,
capInheritable: status.CapInh,
capPermitted: status.CapPrm,
capAmbient: status.CapAmb,
nodeOptions: process.env.NODE_OPTIONS,
childNodeOptions: child.stdout.trim(),
home: process.env.HOME,
marker: process.env.CODEX_TEST_MARKER,
prompt: readFileSync(0, "utf8"),
}));
writeFileSync(output, "fake final message\\n");
`
);
chmodSync(codexPath, 0o755);
chmodSync(tempDir, 0o711);
sudo(["chown", "-R", user, tempDir]);
const command = [
"-n",
"-u",
user,
"--",
"/usr/bin/env",
`HOME=${tempDir}`,
`PATH=${tempDir}:${process.env.PATH ?? ""}`,
"NODE_OPTIONS=--disable-sigusr1",
`CODEX_CAPTURE_PATH=${capturePath}`,
`CODEX_SERVICE_SOCKET=${serviceSocket}`,
`CODEX_WORLD_SOCKET=${worldSocket}`,
`CODEX_FALLBACK_SOCKET=${fallbackSocket}`,
...(canTestSocketAcls
? [
`CODEX_NAMED_USER_ACL_SOCKET=${namedUserAclSocket}`,
`CODEX_FALLBACK_ACL_SOCKET=${fallbackAclSocket}`,
]
: []),
"CODEX_TEST_MARKER=preserved",
process.execPath,
bundledActionPath,
"run-codex-exec",
"--prompt",
"test prompt\nsecond line",
"--prompt-file",
"",
"--codex-home",
"",
"--cd",
tempDir,
"--extra-args",
scenario.extraArgs ? JSON.stringify(scenario.extraArgs) : "",
"--output-file",
outputPath,
"--output-schema-file",
"",
"--output-schema",
"",
"--sandbox",
scenario.sandbox ?? "",
"--permission-profile",
scenario.profile ?? "",
"--model",
"",
"--effort",
"",
"--safety-strategy",
"drop-sudo",
"--codex-user",
"",
];
const launchCommand = scenario.staleGroups
? [
...command.slice(0, 4),
"/bin/sh", "-ec",
'/usr/bin/sudo -n /usr/bin/gpasswd -d "$1" "$2"; /usr/bin/id -G > "$3"; shift 3; exec "$@"',
"codex-stale-groups", user, privilegedGroup, liveGroupsPath,
...command.slice(4),
]
: command;
const result = spawnSync("sudo", launchCommand, {
encoding: "utf8",
timeout: 35_000,
});
if (scenario.rejectedBeforeCleanup) {
assert.notEqual(result.status, 0);
assert.match(result.stderr, /codex-args/);
assert.equal(existsSync(capturePath), false);
const stillPrivileged = spawnSync(
"/usr/bin/sudo",
["-n", "-u", user, "--", "/usr/bin/sudo", "-n", "/usr/bin/true"],
{ encoding: "utf8" }
);
assert.equal(stillPrivileged.status, 0, stillPrivileged.stderr);
const accountGroups = sudo(["id", "-nG", user]).split(/\s+/);
assert.equal(accountGroups.includes("sudo"), true);
assert.equal(accountGroups.includes(privilegedGroup), true);
for (const { socketPath, mode } of originalSockets) {
assert.equal(
Number.parseInt(sudo(["stat", "-c", "%a", socketPath]), 8),
Number.parseInt(mode, 8),
`${socketPath} changed before argument rejection`
);
}
return;
}
if (scenario.remainingSudoGrant) {
assert.notEqual(result.status, 0);
assert.match(result.stderr, /Expected sudo to be disabled/);
assert.equal(existsSync(capturePath), false);
return;
}
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.match(
result.stdout,
/Confirmed the standard sudo probe is disabled/
);
assert.match(
result.stdout,
/Launching Codex with no_new_privs and empty capability sets/
);
const capture = JSON.parse(readFileSync(capturePath, "utf8"));
assert.equal(capture.uid, userId);
assert.equal(capture.gid, safeGroup);
assert.notEqual(capture.gid, originalDockerSocket.gid);
assert.deepEqual(capture.groups, [safeGroup]);
assert.equal(capture.supplementaryGroups, "");
assert.equal(capture.dockerAccessible, false);
assert.equal(capture.serviceAccessible, false);
assert.equal(capture.worldAccessible, false);
assert.equal(capture.fallbackAccessible, false);
assert.equal(
capture.namedUserAclAccessible,
canTestSocketAcls ? false : null
);
assert.equal(
capture.fallbackAclAccessible,
canTestSocketAcls ? false : null
);
assert.ok(Number.isInteger(capture.sudoStatus));
assert.notEqual(capture.sudoStatus, 0);
assert.ok(Number.isInteger(capture.sudoIdStatus));
assert.notEqual(capture.sudoIdStatus, 0);
assert.notEqual(capture.sudoIdStdout, "0");
assert.equal(capture.noNewPrivs, "1");
assert.equal(capture.capBounding, "0000000000000000");
assert.equal(capture.capEffective, "0000000000000000");
assert.equal(capture.capInheritable, "0000000000000000");
assert.equal(capture.capPermitted, "0000000000000000");
assert.equal(capture.capAmbient, "0000000000000000");
assert.equal(capture.nodeOptions, "--disable-sigusr1");
assert.equal(capture.childNodeOptions, "--disable-sigusr1");
assert.equal(capture.home, tempDir);
assert.equal(capture.marker, "preserved");
assert.equal(capture.prompt, "test prompt\nsecond line");
if (scenario.verifyForwardedArgs) {
const forwardedStart = capture.args.indexOf(scenario.extraArgs[0]);
assert.notEqual(forwardedStart, -1);
assert.deepEqual(
capture.args.slice(
forwardedStart,
forwardedStart + scenario.extraArgs.length
),
scenario.extraArgs
);
}
assert.equal(readFileSync(outputPath, "utf8"), "fake final message\n");
if (
scenario.replacementGroupSudoGrant ||
scenario.userAliasSudoGrant
) {
const grantedCommand = scenario.replacementGroupSudoGrant
? ["/usr/bin/true"]
: ["/usr/bin/id", "-u"];
const unprotected = spawnSync(
"/usr/bin/sudo",
[
"-n",
"--",
"/usr/bin/setpriv",
`--reuid=${userId}`,
`--regid=${safeGroup}`,
"--clear-groups",
"--",
"/usr/bin/sudo",
"-n",
...grantedCommand,
],
{ encoding: "utf8" }
);
assert.equal(
unprotected.status,
0,
`expected the residual sudo rule to remain effective without no_new_privs: ${unprotected.stderr}`
);
if (scenario.userAliasSudoGrant) {
assert.equal(unprotected.stdout.trim(), "0");
}
}
const accountGroups = sudo(["id", "-nG", user]).split(/\s+/);
assert.equal(accountGroups.includes("sudo"), false);
assert.equal(accountGroups.includes(privilegedGroup), false);
if (scenario.staleGroups) {
const staleGid = sudo(["getent", "group", privilegedGroup]).split(":")[2];
assert.ok(readFileSync(liveGroupsPath, "utf8").trim().split(/\s+/).includes(staleGid));
}
for (const socket of [
dockerSocket,
serviceSocket,
worldSocket,
fallbackSocket,
...(canTestSocketAcls
? [namedUserAclSocket, fallbackAclSocket]
: []),
...(canTestSocketAcls && scenario.staleGroups ? [staleAclSocket] : []),
]) {
assert.equal(statSync(socket).mode & 0o077, 0);
const access = spawnSync("sudo", [
"-n",
"-u",
user,
"--",
"test",
"-w",
socket,
]);
assert.notEqual(access.status, 0);
}
const failed = spawnSync("sudo", command, {
encoding: "utf8",
timeout: 30_000,
});
assert.notEqual(failed.status, 0);
assert.match(failed.stderr, /requires passwordless sudo.*separate job/);
assert.deepEqual(JSON.parse(readFileSync(capturePath, "utf8")), capture);
});
}
}
);