mirror of
https://github.com/morluto/rea.git
synced 2026-10-11 21:39:55 +00:00
fix(providers): validate ELF versions and preserve probe cancellation
This commit is contained in:
@@ -43,6 +43,10 @@ def inspect_core(path, cache):
|
||||
raise CoreFailure("unsupported-target", "Initial recorded-core profile requires ELF64 little-endian.")
|
||||
if len(content) < 64:
|
||||
raise CoreFailure("format", "Selected ELF64 core header is truncated.")
|
||||
if content[6] != 1:
|
||||
raise CoreFailure("format", "ELF identification version is not EV_CURRENT.")
|
||||
if int.from_bytes(content[20:24], "little") != 1:
|
||||
raise CoreFailure("format", "ELF header version is not EV_CURRENT.")
|
||||
# Linux commonly emits ELFOSABI_NONE (0), also accepted as GNU/Linux (3).
|
||||
# Other declared OSABIs must never select Linux note ctypes.
|
||||
if content[7] not in (0, 3) or content[8] != 0:
|
||||
|
||||
@@ -127,7 +127,6 @@ const environment = {
|
||||
process.env.ComSpec ??
|
||||
join(process.env.SystemRoot ?? "C:\\Windows", "System32", "cmd.exe")
|
||||
).replaceAll("\\", "/"),
|
||||
REA_ANALYSIS_PROVIDER: "ghidra",
|
||||
REA_LOG_LEVEL: "error",
|
||||
GHIDRA_HEADLESS_MAXMEM: "512m",
|
||||
NODE_OPTIONS: "--max-old-space-size=512",
|
||||
@@ -291,7 +290,17 @@ try {
|
||||
expected.every((name) => report.mcpOperations.includes(name)),
|
||||
"Packaged verification omitted an admitted operation.",
|
||||
);
|
||||
await call("binary_session", {});
|
||||
const session = await call("binary_session", {});
|
||||
assert.equal(
|
||||
session.analysis_provider_binding?.selection_source,
|
||||
"auto-single-candidate",
|
||||
"Packaged MCP verification must exercise automatic provider selection.",
|
||||
);
|
||||
assert.equal(
|
||||
session.analysis_provider_binding?.provider?.id,
|
||||
"ghidra",
|
||||
"The sole available Windows analysis provider must be Ghidra.",
|
||||
);
|
||||
await call("close_binary", {});
|
||||
await assertCleanup();
|
||||
} finally {
|
||||
|
||||
@@ -49,12 +49,18 @@ export const inspectPwndbgCore = async ({
|
||||
diagnostics,
|
||||
options,
|
||||
launcher,
|
||||
probePath,
|
||||
}: {
|
||||
readonly environment: Readonly<NodeJS.ProcessEnv>;
|
||||
readonly rootPath: string;
|
||||
readonly diagnostics: AnalysisCapturedOutput;
|
||||
readonly options: ExecutionOptions | undefined;
|
||||
readonly launcher?: PwntoolsLauncher;
|
||||
readonly probePath?: (
|
||||
path: string,
|
||||
kind: "file" | "directory",
|
||||
mode: number,
|
||||
) => Promise<void>;
|
||||
}): Promise<RecordedCrash["debugger"]> => {
|
||||
const gdb = environment.REA_PWNDBG_GDB ?? "/usr/bin/gdb";
|
||||
const entry = environment.REA_PWNDBG_GDBINIT ?? "";
|
||||
@@ -65,28 +71,45 @@ export const inspectPwndbgCore = async ({
|
||||
userMessage: message,
|
||||
capturedOutput: diagnostics,
|
||||
});
|
||||
const checkCancelled = () => {
|
||||
if (options?.signal?.aborted)
|
||||
throw new AnalysisCancelledError(OPERATION, {
|
||||
capturedOutput: diagnostics,
|
||||
});
|
||||
};
|
||||
checkCancelled();
|
||||
for (const [key, path, kind] of [
|
||||
["REA_PWNDBG_GDB", gdb, "file"],
|
||||
["REA_PWNDBG_GDBINIT", entry, "file"],
|
||||
["REA_PWNDBG_VENV_PATH", venv, "directory"],
|
||||
] as const) {
|
||||
try {
|
||||
checkCancelled();
|
||||
if (!isAbsolute(path))
|
||||
throw new Error("Expected an absolute caller-supplied path.");
|
||||
await access(
|
||||
path,
|
||||
key === "REA_PWNDBG_GDB" ? constants.X_OK : constants.R_OK,
|
||||
);
|
||||
const information = await stat(path);
|
||||
if (kind === "file" ? !information.isFile() : !information.isDirectory())
|
||||
throw new Error(`Expected a ${kind}.`);
|
||||
const mode = key === "REA_PWNDBG_GDB" ? constants.X_OK : constants.R_OK;
|
||||
if (probePath !== undefined) {
|
||||
await probePath(path, kind, mode);
|
||||
} else {
|
||||
await access(path, mode);
|
||||
checkCancelled();
|
||||
const information = await stat(path);
|
||||
checkCancelled();
|
||||
if (
|
||||
kind === "file" ? !information.isFile() : !information.isDirectory()
|
||||
)
|
||||
throw new Error(`Expected a ${kind}.`);
|
||||
}
|
||||
checkCancelled();
|
||||
} catch (cause: unknown) {
|
||||
if (options?.signal?.aborted) checkCancelled();
|
||||
throw unavailable(
|
||||
`${key} is unavailable at ${path}: ${cause instanceof Error ? cause.message : String(cause)}. Configure the requested core-only context, or omit include_debugger_context for basic recorded evidence.`,
|
||||
cause,
|
||||
);
|
||||
}
|
||||
}
|
||||
checkCancelled();
|
||||
const stage = join(rootPath, "debugger");
|
||||
await mkdir(stage, { mode: 0o700 });
|
||||
const requestPath = join(stage, "request.json");
|
||||
|
||||
@@ -85,6 +85,8 @@ export function validateRecordedCrashStructure(
|
||||
!header
|
||||
.subarray(0, 6)
|
||||
.equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46, 2, 1])) ||
|
||||
header[6] !== 1 ||
|
||||
header.readUInt32LE(20) !== 1 ||
|
||||
![0, 3].includes(header[7] ?? -1) ||
|
||||
header[8] !== 0 ||
|
||||
header.readUInt16LE(16) !== 4 ||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { expect, it } from "vitest";
|
||||
import {
|
||||
recordedCrashFixture,
|
||||
recordedCrashFixtureBytes,
|
||||
} from "../../fixtures/binaryDiagnostics/recordedCrash.js";
|
||||
import { validateRecordedCrashStructure } from "../../../src/native/pwntools/RecordedCrashStructureBindings.js";
|
||||
|
||||
it.each([
|
||||
[6, 0],
|
||||
[6, 2],
|
||||
[20, 0],
|
||||
[20, 2],
|
||||
] as const)(
|
||||
"rejects non-current ELF version at byte %i: %i",
|
||||
(offset, version) => {
|
||||
const report = recordedCrashFixture();
|
||||
const snapshot = recordedCrashFixtureBytes(report);
|
||||
if (offset === 6) snapshot[offset] = version;
|
||||
else snapshot.writeUInt32LE(version, offset);
|
||||
expect(() =>
|
||||
validateRecordedCrashStructure(
|
||||
report,
|
||||
snapshot,
|
||||
report.decoder_diagnostics,
|
||||
),
|
||||
).toThrow(/Reported core structure differs/);
|
||||
},
|
||||
);
|
||||
@@ -43,6 +43,10 @@ it
|
||||
"omitted-physical-note",
|
||||
"hidden-segment",
|
||||
"wrong-header-range",
|
||||
"wrong-ei-version-zero",
|
||||
"wrong-ei-version-two",
|
||||
"wrong-e-version-zero",
|
||||
"wrong-e-version-two",
|
||||
"wrong-note-kind",
|
||||
"wrong-owner-display",
|
||||
"wrong-pid",
|
||||
@@ -60,6 +64,11 @@ it
|
||||
true,
|
||||
scenario === "wrong-owner-display",
|
||||
);
|
||||
if (scenario.startsWith("wrong-ei-version-"))
|
||||
bytes[6] = Number(scenario.at(-1));
|
||||
if (scenario.startsWith("wrong-e-version-"))
|
||||
bytes.writeUInt32LE(Number(scenario.at(-1)), 20);
|
||||
if (scenario.startsWith("wrong-e")) await writeFile(path, bytes);
|
||||
let owned = "";
|
||||
const provider = new PwntoolsRecordedCrashProvider(
|
||||
{ REA_PWNTOOLS_PYTHON: process.execPath },
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { access } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import {
|
||||
createTestWorkspace,
|
||||
removeTestWorkspace,
|
||||
} from "../../../tests/support/workspace/workspaceFixture.js";
|
||||
import { inspectPwndbgCore } from "../../../src/native/pwndbg/PwndbgCoreContext.js";
|
||||
|
||||
const diagnostics = { stdout: "", stderr: "", truncated: false };
|
||||
|
||||
it.each(["pre-aborted", "aborted-during-probe"] as const)(
|
||||
"cancellation wins over debugger path failures: %s",
|
||||
async (scenario) => {
|
||||
const workspace = await createTestWorkspace("rea-pwndbg-cancel-");
|
||||
onTestFinished(() => removeTestWorkspace(workspace.root));
|
||||
const controller = new AbortController();
|
||||
const gdb = join(workspace.root, "missing-gdb");
|
||||
let probes = 0;
|
||||
const probePath = async () => {
|
||||
probes++;
|
||||
if (scenario === "aborted-during-probe") {
|
||||
await new Promise<void>((resolve) => {
|
||||
setImmediate(() => {
|
||||
controller.abort();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
throw new Error("path became unavailable");
|
||||
}
|
||||
throw new Error("path is unavailable");
|
||||
};
|
||||
if (scenario === "pre-aborted") controller.abort();
|
||||
await expect(
|
||||
inspectPwndbgCore({
|
||||
environment: {
|
||||
REA_PWNDBG_GDB: gdb,
|
||||
REA_PWNDBG_GDBINIT: join(workspace.root, "missing-init"),
|
||||
REA_PWNDBG_VENV_PATH: join(workspace.root, "missing-venv"),
|
||||
},
|
||||
rootPath: workspace.root,
|
||||
diagnostics,
|
||||
options: { signal: controller.signal },
|
||||
probePath,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(probes).toBe(scenario === "pre-aborted" ? 0 : 1);
|
||||
await expect(
|
||||
access(join(workspace.root, "debugger")),
|
||||
).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -139,6 +139,7 @@ export const recordedCrashSignalFixture = (path?: string): RecordedCrash => {
|
||||
export const recordedCrashFixtureBytes = (value: RecordedCrash): Buffer => {
|
||||
const bytes = Buffer.alloc(value.artifact.bytes);
|
||||
bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1]);
|
||||
bytes.writeUInt32LE(1, 20);
|
||||
bytes.writeUInt16LE(4, 16);
|
||||
bytes.writeUInt16LE(62, 18);
|
||||
bytes.writeBigUInt64LE(64n, 32);
|
||||
|
||||
Reference in New Issue
Block a user