Files
rea/tests/process-global/pwntoolsCleanup.test.ts
T
gensokyo-dev aa53c06538 feat(firmware): make analysis deadlines recoverable per call
Expose per-call firmware analysis budgets with exact recovery guidance, shared
CLI/MCP validation and retained actual settings. Keep publication and fixed
resource policy independent; preserve typed interruption and one captured-output
owner through command and one-shot cleanup.

Fixes #5851. Executed verification, real-provider limits, known baseline issues
and authorized readiness override are recorded in #5863.

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-21 19:25:22 +08:00

309 lines
10 KiB
TypeScript

import { access, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import {
afterEach,
beforeAll,
beforeEach,
expect,
it,
onTestFinished,
} from "vitest";
import { PwntoolsLayoutProvider } from "../../src/native/pwntools/PwntoolsLayoutProvider.js";
import { PrivateRuntimeRoot } from "../../src/process/PrivateRuntimeRoot.js";
import { spawnOwnedProviderProcess } from "../../src/process/ProviderProcess.js";
import { cleanupOwnedProcessGroup } from "../../src/process/ProcessOwnership.js";
import { prepareProcessOwnershipInspection } from "../../src/process/ProcessOwnershipObservation.js";
import { stopProviderProcessFixture } from "../fixtures/providerProcess.js";
import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js";
import {
createTestWorkspace,
removeTestWorkspace,
} from "../support/workspace/workspaceFixture.js";
const platform = Object.getOwnPropertyDescriptor(process, "platform");
const architecture = Object.getOwnPropertyDescriptor(process, "arch");
const supportedHost = process.platform !== "win32";
beforeAll(() =>
supportedHost ? prepareProcessOwnershipInspection() : undefined,
);
// Only the decoder's availability gate is simulated. The module-owned process
// inspector was created and prepared for the real host before these overrides;
// every fixture launch, output stream, cleanup signal and filesystem is real.
beforeEach(() => {
Object.defineProperty(process, "platform", { value: "linux" });
Object.defineProperty(process, "arch", { value: "x64" });
});
afterEach(() => {
if (platform !== undefined)
Object.defineProperty(process, "platform", platform);
if (architecture !== undefined)
Object.defineProperty(process, "arch", architecture);
});
const fixture = async () => {
const workspace = await createTestWorkspace("rea-pwntools-cleanup-");
onTestFinished(() => removeTestWorkspace(workspace.root));
const path = join(workspace.root, "selected.elf");
await writeFile(path, "source-owned-seam-bytes");
return { workspace, path };
};
it.runIf(supportedHost)(
"retains live worker files, refuses another launch, and retries cleanup on close",
async () => {
const { workspace, path } = await fixture();
let ownedPath = "";
let pid: number | undefined;
let launches = 0;
let cleanupAllowed = false;
let rootRemovalAllowed = false;
let releaseFixture: (() => Promise<void>) | undefined;
const provider = new PwntoolsLayoutProvider(
{ REA_PWNTOOLS_PYTHON: process.execPath },
async (input) => {
launches++;
ownedPath = input.cwd ?? "";
const launched = await spawnOwnedProviderProcess({
...input,
arguments: [
"-e",
'process.stdout.write("x".repeat(1048577)); setInterval(() => undefined, 1000)',
],
});
pid = launched.process.pid;
releaseFixture = () => stopProviderProcessFixture(launched.process);
return {
...launched,
cleanup: () =>
cleanupAllowed
? cleanupOwnedProcessGroup(launched.ownership)
: Promise.resolve({
cleaned: false as const,
reason: "fixture process inspection denied",
}),
};
},
async () => {
const root = await PrivateRuntimeRoot.create({
parent: workspace.root,
prefix: "owned-",
});
return {
path: root.path,
close: async () => {
if (!rootRemovalAllowed)
throw new Error("root removal refused after process retirement");
await root.close();
},
};
},
);
onTestFinished(async () => {
cleanupAllowed = true;
rootRemovalAllowed = true;
await provider.close();
await releaseFixture?.();
});
const result = await provider.inspect({ path });
if (result.ok)
throw new Error("Expected output overflow and failed cleanup");
expect(result.error).toMatchObject({
cleanupIncomplete: true,
capturedOutput: { truncated: true },
diagnostics: {
primary_error: {
code: "unreadable_output",
},
cleanup_error: { code: "cleanup_incomplete" },
},
});
const projected = projectAnalysisError(result.error);
expect(projected.details?.captured_output).toEqual(
result.error.capturedOutput,
);
expect(projected).not.toHaveProperty(
"details.diagnostics.primary_error.details.captured_output",
);
const output = result.error.capturedOutput?.stdout;
if (!output) throw new Error("Expected retained truncated worker output");
// The cleanup owner also retains a bounded worker snapshot for its own
// failed-close receipt. Generic primary history must not add a third copy.
expect(projected).toHaveProperty(
"details.diagnostics.cleanup_error.details.diagnostics.captured_output.stdout",
output,
);
expect(JSON.stringify(projected).split(output)).toHaveLength(3);
if (pid === undefined)
throw new Error("The fixture did not acquire a process");
const workerPid = pid;
expect(() => process.kill(workerPid, 0)).not.toThrow();
expect(await readFile(join(ownedPath, "object.snapshot"), "utf8")).toBe(
"source-owned-seam-bytes",
);
const refused = await provider.inspect({ path });
if (refused.ok)
throw new Error("Unreleased ownership must block another launch");
expect(refused.error.cleanupResources).toContain(ownedPath);
expect(launches).toBe(1);
await expect(provider.close()).rejects.toMatchObject({
cleanupIncomplete: true,
});
await expect(access(ownedPath)).resolves.toBeUndefined();
cleanupAllowed = true;
await expect(provider.close()).rejects.toMatchObject({
cleanupIncomplete: true,
});
expect(() => process.kill(workerPid, 0)).toThrow();
expect(result.error.cleanupResources).toEqual([ownedPath]);
expect(result.error.cleanupIncomplete).toBe(true);
rootRemovalAllowed = true;
await provider.close();
expect(result.error.cleanupIncomplete).toBe(false);
expect(refused.error.cleanupIncomplete).toBe(false);
await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it.runIf(supportedHost)(
"retains a failed root removal and retries it before acquiring another decoder",
async () => {
const { workspace, path } = await fixture();
let releases = 0;
let launches = 0;
let removalAllowed = false;
const root = await PrivateRuntimeRoot.create({
parent: workspace.root,
prefix: "owned-",
});
const provider = new PwntoolsLayoutProvider(
{ REA_PWNTOOLS_PYTHON: process.execPath },
async (input) => {
launches++;
return spawnOwnedProviderProcess({
...input,
arguments: [
"-e",
'process.stdout.write("retained diagnostic"); process.exitCode = 2',
],
});
},
async () => ({
path: root.path,
close: async () => {
releases++;
if (!removalAllowed) throw new Error("fixture root removal denied");
await root.close();
},
}),
);
onTestFinished(async () => {
removalAllowed = true;
await provider.close();
await root.close();
});
const result = await provider.inspect({ path });
if (result.ok)
throw new Error("Expected command and root-removal failures");
expect(result.error).toMatchObject({
cleanupIncomplete: true,
diagnostics: {
primary_error: {
code: "execution_failure",
details: { diagnostics: { stdout: "retained diagnostic" } },
},
cleanup_error: { code: "cleanup_incomplete" },
},
});
const refused = await provider.inspect({ path });
if (refused.ok)
throw new Error("Retained root cleanup must precede a new launch");
expect(refused.error.cleanupResources).toEqual([root.path]);
expect(launches).toBe(1);
expect(releases).toBe(2);
removalAllowed = true;
await provider.close();
expect(result.error.cleanupIncomplete).toBe(false);
await expect(access(root.path)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it.runIf(supportedHost)(
"cancels queued inspections promptly and drains the running inspection when closing",
async () => {
const { workspace, path } = await fixture();
let release: (() => void) | undefined;
let entered: (() => void) | undefined;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
const started = new Promise<void>((resolve) => {
entered = resolve;
});
let launches = 0;
const provider = new PwntoolsLayoutProvider(
{ REA_PWNTOOLS_PYTHON: process.execPath },
async (input) => {
launches++;
entered?.();
await gate;
return spawnOwnedProviderProcess({
...input,
arguments: ["-e", "process.exit(2)"],
});
},
() =>
PrivateRuntimeRoot.create({ parent: workspace.root, prefix: "owned-" }),
);
onTestFinished(async () => {
release?.();
await provider.close();
});
const first = provider.inspect({ path });
await started;
const controller = new AbortController();
const cancelled = provider.inspect({ path }, { signal: controller.signal });
const reason = { stage: "queued" };
controller.abort(reason);
await expect(cancelled).resolves.toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
const cancelledResult = await cancelled;
if (cancelledResult.ok) throw new Error("Expected queued cancellation");
expect(cancelledResult.error.cause).toBe(reason);
const queued = provider.inspect({ path });
const closing = provider.close();
const late = provider.inspect({ path });
release?.();
const [firstResult, queuedResult, lateResult] = await Promise.all([
first,
queued,
late,
]);
if (firstResult.ok) throw new Error("The fixture exits with failure");
expect(firstResult.error).not.toMatchObject({ cleanupIncomplete: true });
expect(queuedResult).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCapabilityUnavailableError",
cleanupIncomplete: false,
},
});
expect(lateResult).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCapabilityUnavailableError",
cleanupIncomplete: false,
},
});
await closing;
expect(launches).toBe(1);
},
);