mirror of
https://github.com/morluto/rea.git
synced 2026-10-18 20:10:13 -05:00
Keep opaque reasons at admission, queued work, command/file projection and successful final cleanup. Normalize Node's ABORT_ERR envelope only at the snapshot write API that produced it, using the verified original cause. Remove downstream signal/name/identity guesses in decoder and debugger catches, including reply-close and path-probe handling. The initial regressions failed 15 cases with seven controls passing. The complete final series passes 68 focused tests, static checks, docs and discovery. Real owned Node/file fixtures use the actual macOS inspector; only the Linux x64 availability gate is simulated. No native pwntools or GDB qualification or performance improvement is claimed. Refs #4945
440 lines
14 KiB
TypeScript
440 lines
14 KiB
TypeScript
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 {
|
|
waitForProviderProcessReady,
|
|
stopProviderProcessFixture,
|
|
} from "../fixtures/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<unknown> => {
|
|
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();
|
|
const reason = { stage: "final cleanup" };
|
|
let removals = 0;
|
|
const provider = new PwntoolsLayoutProvider(
|
|
{ REA_PWNTOOLS_PYTHON: process.execPath },
|
|
launch,
|
|
async () => ({
|
|
path: root.path,
|
|
close: async () => {
|
|
if (++removals === 1) {
|
|
controller.abort(reason);
|
|
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(result.error.cause).toBe(reason);
|
|
expect(result.error.partialObservation).toBeDefined();
|
|
expect(projectAnalysisError(result.error)).toMatchObject({
|
|
code: "cancelled",
|
|
details: {
|
|
captured_output: { stdout: "decoder output", truncated: false },
|
|
},
|
|
});
|
|
await expect(access(root.path)).rejects.toMatchObject({ code: "ENOENT" });
|
|
},
|
|
);
|
|
|
|
it.runIf(supportedHost).each(["inspect", "inspectAndClose"] as const)(
|
|
"preserves pre-admission reasons without acquiring resources: %s",
|
|
async (method) => {
|
|
const reason = { stage: "pre-admission" };
|
|
const controller = new AbortController();
|
|
controller.abort(reason);
|
|
const provider = new PwntoolsLayoutProvider({});
|
|
onTestFinished(() => provider.close());
|
|
const result = await provider[method](
|
|
{ path: "/not-acquired" },
|
|
{ signal: controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected pre-admission cancellation");
|
|
expect(result.error._tag).toBe("AnalysisCancelledError");
|
|
expect(result.error.cause).toBe(reason);
|
|
const retry = await provider.inspect({ path: "/not-acquired" });
|
|
if (retry.ok) throw new Error("Unconfigured provider must fail");
|
|
expect(retry.error.message).not.toContain("Provider is closing");
|
|
},
|
|
);
|
|
|
|
it.runIf(supportedHost)(
|
|
"normalizes cancellation from the actual snapshot write boundary",
|
|
async () => {
|
|
const { path, root, launch } = await fixture();
|
|
const reason = { stage: "snapshot write" };
|
|
const controller = new AbortController();
|
|
const provider = new PwntoolsLayoutProvider(
|
|
{ REA_PWNTOOLS_PYTHON: process.execPath },
|
|
launch,
|
|
async () => {
|
|
controller.abort(reason);
|
|
return root;
|
|
},
|
|
);
|
|
onTestFinished(() => provider.close());
|
|
const result = await provider.inspect(
|
|
{ path },
|
|
{ signal: controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected snapshot write cancellation");
|
|
expect(result.error._tag).toBe("AnalysisCancelledError");
|
|
expect(result.error.cause).toBe(reason);
|
|
await expect(access(root.path)).rejects.toMatchObject({ code: "ENOENT" });
|
|
},
|
|
);
|
|
|
|
it.runIf(supportedHost).each(["AbortError-name", "same-as-reason"])(
|
|
"preserves a physical runtime acquisition failure despite late cancellation: %s",
|
|
async (scenario) => {
|
|
const { path, root, launch } = await fixture();
|
|
onTestFinished(() => root.close());
|
|
const physical = Object.assign(
|
|
new Error("owned runtime acquisition failed"),
|
|
{ name: scenario === "AbortError-name" ? "AbortError" : "Error" },
|
|
);
|
|
const controller = new AbortController();
|
|
const provider = new PwntoolsLayoutProvider(
|
|
{ REA_PWNTOOLS_PYTHON: process.execPath },
|
|
launch,
|
|
async () => {
|
|
controller.abort(
|
|
scenario === "same-as-reason" ? physical : { late: true },
|
|
);
|
|
throw physical;
|
|
},
|
|
);
|
|
onTestFinished(() => provider.close());
|
|
const result = await provider.inspect(
|
|
{ path },
|
|
{ signal: controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected runtime acquisition failure");
|
|
expect(result.error._tag).toBe("ProviderAdapterError");
|
|
expect(result.error.cause).toBe(physical);
|
|
},
|
|
);
|
|
|
|
it.runIf(supportedHost)(
|
|
"preserves cancellation across a late owned process handshake",
|
|
async () => {
|
|
const { path, root } = await fixture();
|
|
const controller = new AbortController();
|
|
const reason = { stage: "late owned handshake" };
|
|
let child:
|
|
| Awaited<ReturnType<typeof spawnOwnedProviderProcess>>["process"]
|
|
| undefined;
|
|
const provider = new PwntoolsLayoutProvider(
|
|
{ REA_PWNTOOLS_PYTHON: process.execPath },
|
|
async (input) => {
|
|
const launched = await spawnOwnedProviderProcess({
|
|
...input,
|
|
arguments: [
|
|
"-e",
|
|
"process.stdout.write('ready\\n'); setInterval(() => {}, 1000)",
|
|
],
|
|
});
|
|
child = launched.process;
|
|
await waitForProviderProcessReady(launched.process);
|
|
controller.abort(reason);
|
|
return launched;
|
|
},
|
|
async () => root,
|
|
);
|
|
onTestFinished(async () => {
|
|
try {
|
|
await provider.close();
|
|
} finally {
|
|
if (child !== undefined) await stopProviderProcessFixture(child);
|
|
}
|
|
});
|
|
const result = await provider.inspect(
|
|
{ path },
|
|
{ signal: controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected handshake cancellation");
|
|
expect(result.error._tag).toBe("AnalysisCancelledError");
|
|
expect(result.error.cause).toBe(reason);
|
|
expect(child).toBeDefined();
|
|
expect(child?.exitCode !== null || child?.signalCode !== null).toBe(true);
|
|
await expect(access(root.path)).rejects.toMatchObject({ code: "ENOENT" });
|
|
},
|
|
);
|