Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions dist/main.js

Large diffs are not rendered by default.

21 changes: 13 additions & 8 deletions src/dropSudo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface ExecOptions {
}

interface ExecResult {
code: number;
code: number | null;
stdout: string;
stderr: string;
}
Expand Down Expand Up @@ -717,25 +717,30 @@ async function execCommand(
reject(error);
});

child.on("close", (code) => {
const exitCode = code ?? 0;
if (exitCode !== 0 && !options.ignoreFailure) {
child.on("close", (code, signal) => {
if (code !== 0 && !options.ignoreFailure) {
const outcome =
code === null
? `signal ${signal ?? "unknown"}`
: `exit code ${code}`;
const error = new Error(
`Command failed: ${command} ${args.join(" ")} (exit code ${exitCode})`
`Command failed: ${command} ${args.join(" ")} (${outcome})`
);
(error as ExecError).code = exitCode;
(error as ExecError).code = code;
(error as ExecError).signal = signal;
(error as ExecError).stdout = stdout;
(error as ExecError).stderr = stderr;
reject(error);
return;
}
resolve({ code: exitCode, stdout, stderr });
resolve({ code, stdout, stderr });
});
});
}

interface ExecError extends Error {
code: number;
code: number | null;
signal: NodeJS.Signals | null;
stdout: string;
stderr: string;
}
30 changes: 28 additions & 2 deletions test/linuxCredentials.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const { parseLinuxRunnerCredentials, includeAccountGroups } = await import(
const original = { userId: 1001, primaryGroupId: 999, supplementaryGroupIds: [27, 998] };
const require = createRequire(import.meta.url);

function loadLauncher(file, spawn) {
function loadLauncher(file, spawn, platform = "linux") {
const { outputFiles } = buildSync({
entryPoints: [fileURLToPath(new URL(`../src/${file}.ts`, import.meta.url))],
bundle: true, format: "cjs", platform: "node", write: false,
Expand All @@ -35,7 +35,7 @@ function loadLauncher(file, spawn) {
if (name === "os") return { userInfo: () => ({ username: "runner", homedir: "/synthetic" }) };
return require(name);
},
process: { platform: "linux", getuid: () => original.userId,
process: { platform, getuid: () => original.userId,
getgid: () => original.primaryGroupId, getgroups: () => original.supplementaryGroupIds,
execPath: "/synthetic/node", argv: ["node", "/synthetic/main.js"], execArgv: [], env: {} },
console: { log() {} },
Expand Down Expand Up @@ -77,6 +77,32 @@ test("both Linux launch paths pass the original process credentials", async () =
assert.deepEqual(JSON.parse(captured.args[captured.args.indexOf("--runner-credentials") + 1]), original);
});

test("drop-sudo rejects a signal-terminated privileged helper", async () => {
const drop = loadLauncher(
"dropSudo",
(_program, args) => {
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.stdout.setEncoding = child.stderr.setEncoding = () => {};
process.nextTick(() =>
child.emit(
"close",
args.includes("--root-phase") ? null : 0,
args.includes("--root-phase") ? "SIGTERM" : null
)
);
return child;
},
"darwin"
);

await assert.rejects(
drop.dropSudo({ user: "runner", group: "admin", rootPhase: false }),
/SIGTERM/
);
});

test("captures the live credentials without serializing them", () => {
const { captureLinuxRunnerCredentials } = loadLauncher("linuxCredentials", () => {
assert.fail("capturing credentials must not launch a process");
Expand Down
Loading