import { z } from "zod"; import { access, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { afterEach, beforeAll, beforeEach, expect, it, onTestFinished, vi, } from "vitest"; import { BinaryLayoutService } from "../../src/application/binaryDiagnostics/BinaryLayoutService.js"; import { createCli } from "../../src/cli.js"; import * as composition from "../../src/composition/binaryDiagnostics.js"; import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js"; import { PwntoolsLayoutProvider } from "../../src/native/pwntools/PwntoolsLayoutProvider.js"; import type { PwntoolsLauncher } from "../../src/native/pwntools/PwntoolsDecoder.js"; import { PWNTOOLS_PROVIDER_IDENTITY } from "../../src/native/pwntools/PwntoolsRelease.js"; import { PrivateRuntimeRoot } from "../../src/process/PrivateRuntimeRoot.js"; import { cleanupOwnedProcessGroup } from "../../src/process/ProcessOwnership.js"; import { prepareProcessOwnershipInspection } from "../../src/process/ProcessOwnershipObservation.js"; import { spawnOwnedProviderProcess } from "../../src/process/ProviderProcess.js"; import { binaryLayoutFixture } from "../fixtures/binaryDiagnostics/layout.js"; import { createTestWorkspace, removeTestWorkspace, } from "../support/workspace/workspaceFixture.js"; const supportedHost = process.platform !== "win32"; const platform = Object.getOwnPropertyDescriptor(process, "platform"); const architecture = Object.getOwnPropertyDescriptor(process, "arch"); const exitCode = process.exitCode; beforeAll(() => supportedHost ? prepareProcessOwnershipInspection() : undefined, ); beforeEach(() => { // Prepare the real host inspector first; only decoder availability is simulated. 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); process.exitCode = exitCode; vi.restoreAllMocks(); }); const fixture = async (outcome: "decoded" | "failed" = "decoded") => { const workspace = await createTestWorkspace("rea-pwntools-finalization-"); onTestFinished(() => removeTestWorkspace(workspace.root)); const path = workspace.path("selected.elf"); await writeFile(path, Buffer.alloc(128)); const root = await PrivateRuntimeRoot.create({ parent: workspace.root, prefix: "owned-", }); const { artifact: _artifact, diagnostics: _diagnostics, ...payload } = binaryLayoutFixture(path); const launch: PwntoolsLauncher = async (input) => { const request = z .object({ reply_path: z.string() }) .parse( JSON.parse(await readFile(join(root.path, "request.json"), "utf8")), ); return spawnOwnedProviderProcess({ ...input, arguments: [ "-e", outcome === "decoded" ? 'require("node:fs").writeFileSync(process.argv[1], process.argv[2]); process.stdout.write("decoder output")' : 'process.stdout.write("decoder output"); process.exitCode = 2', request.reply_path, JSON.stringify({ ok: true, profile: PWNTOOLS_PROVIDER_IDENTITY.version, value: payload, }), ], }); }; return { path, root, launch }; }; const runCli = async ( provider: PwntoolsLayoutProvider, path: string, ): Promise => { vi.spyOn(composition, "createBinaryLayoutService").mockReturnValue( new BinaryLayoutService(provider), ); let stdout = ""; await createCli({}).serve(["inspect-binary-layout", path, "--json"], { env: {}, stdout: (text) => { stdout += text; }, exit: () => undefined, }); return JSON.parse(stdout); }; it.runIf(supportedHost).each(["decoded", "failed"] as const)( "returns the original %s outcome after a successful runtime-removal retry through the CLI", async (outcome) => { const { path, root, launch } = await fixture(outcome); let removals = 0; const provider = new PwntoolsLayoutProvider( { REA_PWNTOOLS_PYTHON: process.execPath }, launch, async () => ({ path: root.path, close: async () => { if (++removals === 1) throw new Error("transient removal failure"); await root.close(); }, }), ); onTestFinished(() => provider.close()); const output = await runCli(provider, path); if (outcome === "decoded") { expect(output).toMatchObject({ normalized_result: { artifact: { path }, diagnostics: { stdout: "decoder output", truncated: false }, }, }); expect(process.exitCode ?? 0).toBe(0); } else { expect(output).toMatchObject({ code: "execution_failure", details: { diagnostics: { exit_code: 2, captured_output: { stdout: "decoder output" }, }, }, }); expect(output).not.toHaveProperty("details.cleanup"); expect(process.exitCode).toBe(1); } expect(removals).toBe(2); await expect(access(root.path)).rejects.toMatchObject({ code: "ENOENT" }); }, ); it.runIf(supportedHost)( "recovers a completed worker reply after terminal process cleanup succeeds", async () => { const { path, root, launch } = await fixture(); let stops = 0; const provider = new PwntoolsLayoutProvider( { REA_PWNTOOLS_PYTHON: process.execPath }, async (input) => { const launched = await launch(input); return { ...launched, cleanup: () => ++stops < 3 ? Promise.resolve({ cleaned: false as const, reason: "transient process inspection failure", }) : cleanupOwnedProcessGroup(launched.ownership), }; }, async () => root, ); onTestFinished(() => provider.close()); expect(await runCli(provider, path)).toMatchObject({ normalized_result: { artifact: { path } }, }); expect(process.exitCode ?? 0).toBe(0); expect(stops).toBe(3); await expect(access(root.path)).rejects.toMatchObject({ code: "ENOENT" }); }, ); it.runIf(supportedHost)( "reports only the runtime still owned after process retirement", async () => { const { path, root, launch } = await fixture("failed"); let stops = 0; let removalAllowed = false; const provider = new PwntoolsLayoutProvider( { REA_PWNTOOLS_PYTHON: process.execPath }, async (input) => { const launched = await launch(input); return { ...launched, cleanup: () => ++stops === 1 ? Promise.resolve({ cleaned: false as const, reason: "transient process inspection failure", }) : cleanupOwnedProcessGroup(launched.ownership), }; }, async () => ({ path: root.path, close: async () => { if (!removalAllowed) throw new Error("runtime removal denied"); await root.close(); }, }), ); onTestFinished(async () => { removalAllowed = true; await provider.close(); }); expect(await runCli(provider, path)).toMatchObject({ code: "cleanup_incomplete", details: { resources: [root.path], diagnostics: { primary_error: { code: "execution_failure" }, cleanup_error: { code: "cleanup_incomplete", details: { resources: [root.path] }, }, }, }, }); expect(stops).toBe(2); await expect(access(root.path)).resolves.toBeUndefined(); }, ); it.runIf(supportedHost)( "preserves the validated report when runtime removal remains denied", async () => { const { path, root, launch } = await fixture(); let removalAllowed = false; const provider = new PwntoolsLayoutProvider( { REA_PWNTOOLS_PYTHON: process.execPath }, launch, async () => ({ path: root.path, close: async () => { if (!removalAllowed) throw new Error("runtime removal denied"); await root.close(); }, }), ); onTestFinished(async () => { removalAllowed = true; await provider.close(); }); expect(await runCli(provider, path)).toMatchObject({ code: "cleanup_incomplete", details: { resources: [root.path], partial_observation: { artifact: { path }, diagnostics: { stdout: "decoder output", truncated: false }, sections: binaryLayoutFixture(path).sections, }, }, }); expect(process.exitCode).toBe(1); await expect(access(root.path)).resolves.toBeUndefined(); }, ); it.runIf(supportedHost)( "preserves cancellation during successful final cleanup", async () => { const { path, root, launch } = await fixture(); const controller = new AbortController(); let removals = 0; const provider = new PwntoolsLayoutProvider( { REA_PWNTOOLS_PYTHON: process.execPath }, launch, async () => ({ path: root.path, close: async () => { if (++removals === 1) { controller.abort(); throw new Error("transient removal failure"); } await root.close(); }, }), ); onTestFinished(() => provider.close()); const result = await provider.inspectAndClose( { path }, { signal: controller.signal }, ); if (result.ok) throw new Error("Cancelled inspection must not publish success"); expect(projectAnalysisError(result.error)).toMatchObject({ code: "cancelled", details: { captured_output: { stdout: "decoder output", truncated: false }, }, }); await expect(access(root.path)).rejects.toMatchObject({ code: "ENOENT" }); }, );