mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
b2257653ca
Fixes #3405. Isolate each negative source-admission case with a cold Ghidra session while retaining valid snapshot readback coverage. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
195 lines
6.3 KiB
JavaScript
195 lines
6.3 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import {
|
|
chmod,
|
|
mkdir,
|
|
mkdtemp,
|
|
readFile,
|
|
realpath,
|
|
readdir,
|
|
rm,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
/** Verify changed-source admission and retained private database identity through MCP. */
|
|
export async function verifyGhidraTargetAdmission({
|
|
call,
|
|
reject,
|
|
target,
|
|
entry,
|
|
runtimeRoot,
|
|
}) {
|
|
const workspace = await mkdtemp(
|
|
join(tmpdir(), "rea-ghidra-source-admission-"),
|
|
);
|
|
const source = join(workspace, "caller-selected.%PATH%");
|
|
try {
|
|
const original = await readFile(target.path);
|
|
await writeFile(source, original);
|
|
await call("close_binary");
|
|
const opened = await call("open_binary", {
|
|
path: source,
|
|
provider_id: "ghidra",
|
|
});
|
|
assert.equal(opened.sha256, target.sha256);
|
|
const selectedPath = await realpath(source);
|
|
assert.equal(opened.path, selectedPath);
|
|
const before = await call("binary_session");
|
|
const changed = Buffer.concat([
|
|
original,
|
|
Buffer.from("REA_SOURCE_CHANGED_AFTER_OPEN"),
|
|
]);
|
|
const changedSha256 = createHash("sha256").update(changed).digest("hex");
|
|
await writeFile(source, changed);
|
|
const failure = await reject("analyze_function", { procedure: entry });
|
|
assert.equal(failure.code, "artifact_changed");
|
|
assert.equal(failure.category, "integrity_mismatch");
|
|
assert.equal(failure.details.operation, "analyze_function");
|
|
assert.equal(failure.details.path, selectedPath);
|
|
assert.ok(failure.details.reason.includes(target.sha256));
|
|
assert.ok(failure.details.reason.includes(changedSha256));
|
|
assert.match(failure.remediation.action, /open_binary/u);
|
|
assert.doesNotMatch(failure.remediation.action, /doctor/u);
|
|
const after = await call("binary_session");
|
|
assert.equal(after.analysis_run.run_id, before.analysis_run.run_id);
|
|
assert.equal(after.sha256, target.sha256);
|
|
assert.equal(
|
|
after.capabilities.find(
|
|
({ operation }) => operation === "analyze_function",
|
|
)?.available,
|
|
true,
|
|
"An artifact identity mismatch must not mark the installed provider unavailable",
|
|
);
|
|
assert.deepEqual(
|
|
(await readdir(runtimeRoot)).filter((name) =>
|
|
name.startsWith("rea-ghidra-"),
|
|
),
|
|
[],
|
|
"Failed admission must remove the private snapshot without launching Ghidra",
|
|
);
|
|
assert.deepEqual(await readFile(source), changed);
|
|
const permissionDenied = await verifySourceReadFailures({
|
|
call,
|
|
reject,
|
|
source,
|
|
bytes: changed,
|
|
entry,
|
|
runtimeRoot,
|
|
});
|
|
|
|
const reopened = await call("open_binary", {
|
|
path: source,
|
|
provider_id: "ghidra",
|
|
});
|
|
assert.equal(reopened.sha256, changedSha256);
|
|
assert.notEqual(
|
|
(await call("binary_session")).analysis_run.run_id,
|
|
before.analysis_run.run_id,
|
|
);
|
|
const dossier = await call("analyze_function", { procedure: entry });
|
|
const instruction = await call("inspect_native_instruction", {
|
|
address: entry,
|
|
});
|
|
assert.equal(dossier.procedure.address, entry);
|
|
assert.equal(instruction.status, "decoded");
|
|
for (const address of [
|
|
entry.toUpperCase(),
|
|
entry.slice(2),
|
|
`0x0000${entry.slice(2)}`,
|
|
`ram:${entry}`,
|
|
`%72%61%6D:${entry}`,
|
|
]) {
|
|
assert.deepEqual(
|
|
await call("inspect_native_instruction", { address }),
|
|
instruction,
|
|
);
|
|
assert.equal(
|
|
(await call("resolve_containing_procedure", { address })).procedure
|
|
.address,
|
|
entry,
|
|
);
|
|
}
|
|
// Imported facts belong to the captured bytes even if the caller's path disappears.
|
|
await rm(source);
|
|
assert.deepEqual(
|
|
await call("analyze_function", { procedure: entry }),
|
|
dossier,
|
|
);
|
|
assert.deepEqual(
|
|
await call("inspect_native_instruction", { address: entry }),
|
|
instruction,
|
|
);
|
|
assert.equal((await call("binary_session")).sha256, changedSha256);
|
|
await call("close_binary");
|
|
await call("open_binary", { path: target.path, provider_id: "ghidra" });
|
|
return { permissionDenied };
|
|
} finally {
|
|
await rm(workspace, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
async function verifySourceReadFailures({
|
|
call,
|
|
reject,
|
|
source,
|
|
bytes,
|
|
entry,
|
|
runtimeRoot,
|
|
}) {
|
|
const permissionDenied =
|
|
typeof process.getuid === "function" && process.getuid() !== 0;
|
|
const scenarios = [
|
|
"missing",
|
|
"directory",
|
|
...(permissionDenied ? ["permission"] : []),
|
|
];
|
|
for (const scenario of scenarios) {
|
|
// Same-target reopen may admit restored bytes through retained-client health.
|
|
// Each rejection needs a cold client, without an existing private snapshot.
|
|
await call("close_binary");
|
|
await call("open_binary", { path: source, provider_id: "ghidra" });
|
|
const before = await call("binary_session");
|
|
if (scenario === "permission") await chmod(source, 0);
|
|
else {
|
|
await rm(source);
|
|
if (scenario === "directory") await mkdir(source);
|
|
}
|
|
try {
|
|
const failure = await reject("analyze_function", { procedure: entry });
|
|
assert.equal(
|
|
failure.code,
|
|
scenario === "permission" ? "access_denied" : "artifact_changed",
|
|
);
|
|
assert.equal(failure.details.path, before.path);
|
|
assert.equal(failure.details.operation, "analyze_function");
|
|
if (scenario === "permission") {
|
|
assert.ok(["EACCES", "EPERM"].includes(failure.details.system_code));
|
|
assert.match(failure.remediation.action, /read access/u);
|
|
} else assert.match(failure.remediation.action, /open_binary/u);
|
|
assert.doesNotMatch(failure.remediation.action, /doctor/u);
|
|
const after = await call("binary_session");
|
|
assert.equal(after.analysis_run.run_id, before.analysis_run.run_id);
|
|
assert.equal(after.sha256, before.sha256);
|
|
assert.equal(
|
|
after.capabilities.find(
|
|
({ operation }) => operation === "analyze_function",
|
|
)?.available,
|
|
true,
|
|
);
|
|
assert.deepEqual(
|
|
(await readdir(runtimeRoot)).filter((name) =>
|
|
name.startsWith("rea-ghidra-"),
|
|
),
|
|
[],
|
|
);
|
|
} finally {
|
|
if (scenario === "permission") await chmod(source, 0o600);
|
|
await rm(source, { recursive: true, force: true });
|
|
await writeFile(source, bytes);
|
|
}
|
|
}
|
|
return permissionDenied;
|
|
}
|