mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 10:43:30 +00:00
295 lines
10 KiB
TypeScript
295 lines
10 KiB
TypeScript
import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
import { join } from "node:path";
|
|
import { expect, it, onTestFinished, vi } from "vitest";
|
|
import { OwnedCommandFailure } from "../../../src/process/OwnedCommand.js";
|
|
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisTimeoutError,
|
|
} from "../../../src/domain/analysisErrorCore.js";
|
|
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
|
|
import {
|
|
createTestWorkspace,
|
|
removeTestWorkspace,
|
|
} from "../../../tests/support/workspace/workspaceFixture.js";
|
|
import { inspectPwndbgCore } from "../../../src/native/pwndbg/PwndbgCoreContext.js";
|
|
|
|
const diagnostics = { stdout: "", stderr: "", truncated: false };
|
|
|
|
const accessFault = vi.hoisted(() => ({
|
|
handler: undefined as (() => Promise<void>) | undefined,
|
|
}));
|
|
|
|
vi.mock("node:fs/promises", async (importOriginal) => {
|
|
const original = await importOriginal<typeof import("node:fs/promises")>();
|
|
return {
|
|
...original,
|
|
access: async (...args: Parameters<typeof original.access>) => {
|
|
if (accessFault.handler !== undefined) return accessFault.handler();
|
|
return original.access(...args);
|
|
},
|
|
};
|
|
});
|
|
|
|
const prepareDebuggerPaths = async (root: string) => {
|
|
const gdb = join(root, "gdb");
|
|
const init = join(root, "gdbinit.py");
|
|
const venv = join(root, "venv");
|
|
await writeFile(gdb, "");
|
|
await chmod(gdb, 0o700);
|
|
await writeFile(init, "");
|
|
await mkdir(venv);
|
|
return {
|
|
REA_PWNDBG_GDB: gdb,
|
|
REA_PWNDBG_GDBINIT: init,
|
|
REA_PWNDBG_VENV_PATH: venv,
|
|
};
|
|
};
|
|
|
|
it.each(["pre-aborted", "aborted-during-probe"] as const)(
|
|
"observes cancellation at the debugger admission checkpoint: %s",
|
|
async (scenario) => {
|
|
const workspace = await createTestWorkspace("rea-pwndbg-cancel-");
|
|
onTestFinished(() => removeTestWorkspace(workspace.root));
|
|
const controller = new AbortController();
|
|
const reason = { cancelled: scenario };
|
|
const gdb = join(workspace.root, "missing-gdb");
|
|
let probes = 0;
|
|
accessFault.handler = async () => {
|
|
probes++;
|
|
if (scenario === "aborted-during-probe") {
|
|
await new Promise<void>((resolve) => {
|
|
setImmediate(() => {
|
|
controller.abort(reason);
|
|
resolve();
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
throw new Error("path is unavailable");
|
|
};
|
|
onTestFinished(() => {
|
|
accessFault.handler = undefined;
|
|
});
|
|
if (scenario === "pre-aborted") controller.abort(reason);
|
|
const inspection = inspectPwndbgCore({
|
|
environment: {
|
|
REA_PWNDBG_GDB: gdb,
|
|
REA_PWNDBG_GDBINIT: join(workspace.root, "missing-init"),
|
|
REA_PWNDBG_VENV_PATH: join(workspace.root, "missing-venv"),
|
|
},
|
|
architecture: "x86_64-little-endian",
|
|
rootPath: workspace.root,
|
|
resources: new ArtifactResourceScope(),
|
|
diagnostics,
|
|
options: { signal: controller.signal },
|
|
timeoutMs: 30_000,
|
|
runCommand: () => {
|
|
throw new Error("No process should exist before cancellation");
|
|
},
|
|
});
|
|
const failure = await inspection.catch((cause: unknown) => cause);
|
|
if (!(failure instanceof AnalysisCancelledError))
|
|
throw new Error(
|
|
"Expected cancellation at the debugger admission boundary",
|
|
);
|
|
expect(failure.cause).toBe(reason);
|
|
expect(probes).toBe(scenario === "pre-aborted" ? 0 : 1);
|
|
accessFault.handler = undefined;
|
|
await expect(
|
|
access(join(workspace.root, "debugger")),
|
|
).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
},
|
|
);
|
|
|
|
const execute = promisify(execFile);
|
|
|
|
it.each([
|
|
["REA_PWNDBG_GDBINIT", "directory", "file"],
|
|
["REA_PWNDBG_VENV_PATH", "file", "directory"],
|
|
] as const)(
|
|
"rejects a %s path with the wrong kind",
|
|
async (setting, given, expected) => {
|
|
const workspace = await createTestWorkspace("rea-pwndbg-path-kind-");
|
|
onTestFinished(() => removeTestWorkspace(workspace.root));
|
|
const environment = await prepareDebuggerPaths(workspace.root);
|
|
const wrong = join(workspace.root, "wrong-kind");
|
|
if (given === "directory") await mkdir(wrong);
|
|
else await writeFile(wrong, "");
|
|
const failure = await inspectPwndbgCore({
|
|
environment: { ...environment, [setting]: wrong },
|
|
architecture: "x86_64-little-endian",
|
|
rootPath: workspace.root,
|
|
resources: new ArtifactResourceScope(),
|
|
diagnostics,
|
|
options: undefined,
|
|
timeoutMs: 30_000,
|
|
runCommand: () => {
|
|
throw new Error("No command may start after path refusal");
|
|
},
|
|
}).catch((cause: unknown) => cause);
|
|
expect(failure).toMatchObject({
|
|
_tag: "AnalysisCapabilityUnavailableError",
|
|
message: expect.stringContaining(
|
|
`${setting} is unavailable at ${wrong}: Expected a ${expected}.`,
|
|
),
|
|
});
|
|
await expect(
|
|
access(join(workspace.root, "debugger")),
|
|
).rejects.toMatchObject({ code: "ENOENT" });
|
|
},
|
|
);
|
|
|
|
it("establishes core-only debugger state before querying recorded mappings", async () => {
|
|
await execute(
|
|
"python3",
|
|
["-m", "unittest", "bridge/pwndbg/test_core_context.py", "-v"],
|
|
{
|
|
timeout: 10_000,
|
|
maxBuffer: 1024 * 1024,
|
|
},
|
|
);
|
|
});
|
|
|
|
it.each(["x86_64-little-endian", "aarch64-little-endian"] as const)(
|
|
"passes the validated %s profile to the debugger without reinterpreting core bytes",
|
|
async (architecture) => {
|
|
const workspace = await createTestWorkspace("rea-pwndbg-profile-");
|
|
onTestFinished(() => removeTestWorkspace(workspace.root));
|
|
const environment = await prepareDebuggerPaths(workspace.root);
|
|
let requested = false;
|
|
await expect(
|
|
inspectPwndbgCore({
|
|
architecture,
|
|
environment,
|
|
rootPath: workspace.root,
|
|
resources: new ArtifactResourceScope(),
|
|
diagnostics,
|
|
options: undefined,
|
|
timeoutMs: 30_000,
|
|
runCommand: async () => {
|
|
const request: unknown = JSON.parse(
|
|
await readFile(
|
|
join(workspace.root, "debugger", "request.json"),
|
|
"utf8",
|
|
),
|
|
);
|
|
expect(request).toMatchObject({
|
|
architecture,
|
|
snapshot_path: join(workspace.root, "object.snapshot"),
|
|
});
|
|
requested = true;
|
|
throw new Error("Stop after observing the debugger request");
|
|
},
|
|
}),
|
|
).rejects.toBeDefined();
|
|
expect(requested).toBe(true);
|
|
},
|
|
);
|
|
|
|
it.each(["cancelled", "physical-name", "physical-identity"])(
|
|
"uses the debugger command owner's outcome despite a late signal: %s",
|
|
async (outcome) => {
|
|
const workspace = await createTestWorkspace("rea-pwndbg-command-outcome-");
|
|
onTestFinished(() => removeTestWorkspace(workspace.root));
|
|
const environment = await prepareDebuggerPaths(workspace.root);
|
|
const controller = new AbortController();
|
|
const reason = { stage: "command handoff" };
|
|
const failure = new OwnedCommandFailure(
|
|
outcome === "cancelled" ? "cancelled" : "process",
|
|
"owned command outcome",
|
|
null,
|
|
null,
|
|
{ cancellation: outcome === "cancelled" ? { reason } : undefined },
|
|
);
|
|
if (outcome === "physical-name") failure.name = "AbortError";
|
|
const inspection = inspectPwndbgCore({
|
|
environment,
|
|
architecture: "x86_64-little-endian",
|
|
rootPath: workspace.root,
|
|
resources: new ArtifactResourceScope(),
|
|
diagnostics: { stdout: "basic evidence", stderr: "", truncated: false },
|
|
options: { signal: controller.signal },
|
|
timeoutMs: 30_000,
|
|
runCommand: async () => {
|
|
controller.abort(outcome === "physical-identity" ? failure : reason);
|
|
throw failure;
|
|
},
|
|
});
|
|
const result = await inspection.catch((cause: unknown) => cause);
|
|
if (!(result instanceof Error))
|
|
throw new Error("Expected a typed command failure");
|
|
expect(result).toMatchObject({
|
|
_tag:
|
|
outcome === "cancelled"
|
|
? "AnalysisCancelledError"
|
|
: "ProviderAdapterError",
|
|
});
|
|
expect(result.cause).toBe(outcome === "cancelled" ? reason : failure);
|
|
},
|
|
);
|
|
|
|
it("reports a non-default debugger deadline instead of 30000 ms", async () => {
|
|
const workspace = await createTestWorkspace("rea-pwndbg-deadline-");
|
|
onTestFinished(() => removeTestWorkspace(workspace.root));
|
|
const environment = await prepareDebuggerPaths(workspace.root);
|
|
const failure = await inspectPwndbgCore({
|
|
environment,
|
|
architecture: "x86_64-little-endian",
|
|
rootPath: workspace.root,
|
|
resources: new ArtifactResourceScope(),
|
|
diagnostics,
|
|
options: undefined,
|
|
timeoutMs: 60_000,
|
|
runCommand: () => {
|
|
throw new OwnedCommandFailure("timeout", "deadline expired");
|
|
},
|
|
}).catch((cause: unknown) => cause);
|
|
if (!(failure instanceof AnalysisTimeoutError))
|
|
throw new Error("Expected a typed debugger timeout");
|
|
const projected = projectAnalysisError(failure);
|
|
expect(projected.code).toBe("provider_timeout");
|
|
expect(projected.message).toContain("of 60000 ms");
|
|
expect(projected.remediation.action).toContain("above 60000");
|
|
expect(projected.remediation.action).not.toContain("above 30000");
|
|
});
|
|
|
|
it("retains a physical debugger path failure when cancellation arrives in the probe", async () => {
|
|
const workspace = await createTestWorkspace("rea-pwndbg-probe-failure-");
|
|
onTestFinished(() => removeTestWorkspace(workspace.root));
|
|
const controller = new AbortController();
|
|
const physical = Object.assign(new Error("path denied"), { code: "EACCES" });
|
|
accessFault.handler = async () => {
|
|
controller.abort({ late: true });
|
|
throw physical;
|
|
};
|
|
onTestFinished(() => {
|
|
accessFault.handler = undefined;
|
|
});
|
|
const inspection = inspectPwndbgCore({
|
|
environment: {
|
|
REA_PWNDBG_GDB: workspace.path("gdb"),
|
|
REA_PWNDBG_GDBINIT: workspace.path("gdbinit.py"),
|
|
REA_PWNDBG_VENV_PATH: workspace.path("venv"),
|
|
},
|
|
architecture: "x86_64-little-endian",
|
|
rootPath: workspace.root,
|
|
resources: new ArtifactResourceScope(),
|
|
diagnostics,
|
|
options: { signal: controller.signal },
|
|
timeoutMs: 30_000,
|
|
runCommand: () => {
|
|
throw new Error("No command may start after probe failure");
|
|
},
|
|
});
|
|
const failure = await inspection.catch((cause: unknown) => cause);
|
|
expect(failure).toMatchObject({
|
|
_tag: "AnalysisCapabilityUnavailableError",
|
|
cause: physical,
|
|
});
|
|
});
|