102 lines
3.6 KiB
TypeScript
102 lines
3.6 KiB
TypeScript
import { EventEmitter } from "node:events";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { PassThrough } from "node:stream";
|
|
import { expect, it, onTestFinished, vi } from "vitest";
|
|
import { PwntoolsLayoutProvider } from "../../../src/native/pwntools/PwntoolsLayoutProvider.js";
|
|
import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js";
|
|
import {
|
|
createTestWorkspace,
|
|
removeTestWorkspace,
|
|
} from "../../support/workspace/workspaceFixture.js";
|
|
|
|
// Exercise the decoder's launch/cleanup contract independently of the host's
|
|
// process table. The interpreter has exec'd over its caller-selected wrapper.
|
|
it
|
|
.runIf(process.platform === "linux" && process.arch === "x64")
|
|
.each(["owned", "wrong-token", "wrong-parent"] as const)(
|
|
"cancels an exec wrapper only with current ownership: %s",
|
|
async (identity) => {
|
|
const { root } = await createTestWorkspace("rea-pwntools-wrapper-");
|
|
onTestFinished(() => removeTestWorkspace(root));
|
|
const path = join(root, "input");
|
|
await writeFile(path, "source-owned-seam-bytes");
|
|
const controller = new AbortController();
|
|
class DecoderProcess extends EventEmitter {
|
|
readonly pid = 41234;
|
|
readonly stdout = new PassThrough();
|
|
readonly stderr = new PassThrough();
|
|
exitCode: number | null = null;
|
|
signalCode: NodeJS.Signals | null = null;
|
|
kill(): boolean {
|
|
throw new Error("Cleanup must validate ownership before signaling");
|
|
}
|
|
}
|
|
const child = new DecoderProcess();
|
|
let live = true;
|
|
const signalGroup = vi.fn(() => {
|
|
live = false;
|
|
child.signalCode = "SIGTERM";
|
|
child.stdout.end();
|
|
child.stderr.end();
|
|
child.emit("exit", null, "SIGTERM");
|
|
child.emit("close", null, "SIGTERM");
|
|
});
|
|
const provider = new PwntoolsLayoutProvider(
|
|
{ REA_PWNTOOLS_PYTHON: process.execPath },
|
|
async (spawn) => {
|
|
const ownership = {
|
|
runId: spawn.runId,
|
|
leaderPid: child.pid,
|
|
processGroupId: child.pid,
|
|
expectedParentPid: process.pid,
|
|
...(spawn.expectedCommand === null
|
|
? {}
|
|
: { expectedCommand: spawn.expectedCommand ?? spawn.command }),
|
|
};
|
|
setImmediate(() => controller.abort());
|
|
return {
|
|
process: child,
|
|
ownership,
|
|
cleanup: () =>
|
|
cleanupOwnedProcessGroup(ownership, {
|
|
listProcesses: async () =>
|
|
live
|
|
? [
|
|
{
|
|
pid: child.pid,
|
|
parentPid:
|
|
identity === "wrong-parent"
|
|
? process.pid + 1
|
|
: process.pid,
|
|
processGroupId: child.pid,
|
|
state: "S",
|
|
command:
|
|
"/selected/python -I bridge/pwntools/layout.py",
|
|
},
|
|
]
|
|
: [],
|
|
environment: async () => ({
|
|
REA_PROCESS_RUN_ID:
|
|
identity === "wrong-token" ? "another-run" : spawn.runId,
|
|
}),
|
|
signalGroup,
|
|
}),
|
|
};
|
|
},
|
|
);
|
|
const result = await provider.inspect(
|
|
{ path },
|
|
{ signal: controller.signal },
|
|
);
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error:
|
|
identity === "owned"
|
|
? { _tag: "AnalysisCancelledError" }
|
|
: { _tag: "ProviderAdapterError", cleanupIncomplete: true },
|
|
});
|
|
expect(signalGroup).toHaveBeenCalledTimes(identity === "owned" ? 1 : 0);
|
|
},
|
|
);
|