mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 00:07:18 +00:00
201 lines
6.3 KiB
JavaScript
201 lines
6.3 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import {
|
|
access,
|
|
mkdir,
|
|
mkdtemp,
|
|
readFile,
|
|
readdir,
|
|
realpath,
|
|
rm,
|
|
stat,
|
|
} from "node:fs/promises";
|
|
import { basename, dirname, join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
import { withVerifierCleanup } from "./verifier-cleanup.mjs";
|
|
|
|
const exec = promisify(execFile);
|
|
|
|
/** Exercise the OpenJDK Unix-socket boundary through real Ghidra and REA. */
|
|
export async function verifyGhidraSocketBoundary({
|
|
target,
|
|
procedure,
|
|
entrypoint,
|
|
env,
|
|
}) {
|
|
if (process.platform !== "darwin" && process.platform !== "linux")
|
|
return {
|
|
status: "not-applicable",
|
|
reason: "The Ghidra Unix-socket lane is not used on this host.",
|
|
};
|
|
const limit = process.platform === "darwin" ? 102 : 106;
|
|
const before = createHash("sha256")
|
|
.update(await readFile(target.path))
|
|
.digest("hex");
|
|
const root = await realpath(await mkdtemp("/tmp/rea-gsock-"));
|
|
const suffixBytes = Buffer.byteLength("/rea-ghidra-XXXXXX/bridge.sock");
|
|
let cleanupIncomplete = false;
|
|
const samples = [];
|
|
try {
|
|
for (const { proposedBytes, unicode } of [
|
|
{ proposedBytes: limit, unicode: false },
|
|
{ proposedBytes: limit + 1, unicode: false },
|
|
{ proposedBytes: limit + 2, unicode: false },
|
|
{ proposedBytes: limit + 1, unicode: true },
|
|
]) {
|
|
const character = unicode ? "雪" : "";
|
|
const padding =
|
|
proposedBytes -
|
|
suffixBytes -
|
|
Buffer.byteLength(root) -
|
|
1 -
|
|
Buffer.byteLength(character);
|
|
assert.ok(
|
|
padding > 0,
|
|
"Verifier workspace leaves no room for the boundary fixture",
|
|
);
|
|
const parent = join(root, "x".repeat(padding) + character);
|
|
await mkdir(parent);
|
|
assert.equal(Buffer.byteLength(parent) + suffixBytes, proposedBytes);
|
|
if (unicode) assert.ok(Buffer.byteLength(parent) > parent.length);
|
|
const sessionEnv = { ...env, TMPDIR: parent };
|
|
const client = new Client({
|
|
name: "rea-ghidra-socket-boundary",
|
|
version: "1",
|
|
});
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [entrypoint, "mcp"],
|
|
env: sessionEnv,
|
|
stderr: "pipe",
|
|
});
|
|
let connected = false;
|
|
let opened = false;
|
|
let runtimeRoot;
|
|
let socketRoot;
|
|
let endpoint;
|
|
const cleanup = async (task) => {
|
|
try {
|
|
await task();
|
|
} catch (cause) {
|
|
cleanupIncomplete = true;
|
|
throw cause;
|
|
}
|
|
};
|
|
await withVerifierCleanup(async () => {
|
|
await client.connect(transport);
|
|
connected = true;
|
|
const tools = new Map(
|
|
(await client.listTools()).tools.map((tool) => [tool.name, tool]),
|
|
);
|
|
const call = async (name, arguments_ = {}) => {
|
|
const reply = await client.callTool(
|
|
{ name, arguments: arguments_ },
|
|
{ timeout: 180000, toolDefinition: tools.get(name) },
|
|
);
|
|
assert.notEqual(
|
|
reply.isError,
|
|
true,
|
|
`${name}: ${JSON.stringify(reply)}`,
|
|
);
|
|
return reply.structuredContent ?? JSON.parse(reply.content[0].text);
|
|
};
|
|
await call("open_binary", { path: target.path, provider_id: "ghidra" });
|
|
opened = true;
|
|
await call("list_documents");
|
|
const roots = (await readdir(parent)).filter((name) =>
|
|
name.startsWith("rea-ghidra-"),
|
|
);
|
|
assert.equal(roots.length, 1);
|
|
runtimeRoot = join(parent, roots[0]);
|
|
endpoint = JSON.parse(
|
|
await readFile(join(runtimeRoot, "ownership.json"), "utf8"),
|
|
).endpoint_path;
|
|
assert.equal(
|
|
Buffer.byteLength(join(runtimeRoot, "bridge.sock")),
|
|
proposedBytes,
|
|
);
|
|
const fallback = dirname(endpoint) !== runtimeRoot;
|
|
assert.equal(fallback, proposedBytes > limit);
|
|
if (fallback) {
|
|
socketRoot = dirname(endpoint);
|
|
assert.match(basename(socketRoot), /^rea-ghidra-socket-/u);
|
|
assert.equal((await stat(socketRoot)).mode & 0o777, 0o700);
|
|
}
|
|
samples.push({
|
|
proposed_bytes: proposedBytes,
|
|
unicode,
|
|
endpoint_bytes: Buffer.byteLength(endpoint),
|
|
fallback,
|
|
});
|
|
}, [
|
|
() =>
|
|
cleanup(async () => {
|
|
if (!connected || !opened) return;
|
|
const reply = await client.callTool(
|
|
{ name: "close_binary", arguments: {} },
|
|
{ timeout: 180000 },
|
|
);
|
|
assert.notEqual(
|
|
reply.isError,
|
|
true,
|
|
`close_binary: ${JSON.stringify(reply)}`,
|
|
);
|
|
}),
|
|
() =>
|
|
cleanup(async () => {
|
|
await client.close();
|
|
}),
|
|
() =>
|
|
cleanup(async () => {
|
|
await transport.close();
|
|
}),
|
|
]);
|
|
await assert.rejects(access(runtimeRoot), { code: "ENOENT" });
|
|
if (socketRoot !== undefined)
|
|
await assert.rejects(access(socketRoot), { code: "ENOENT" });
|
|
if (proposedBytes === limit + 1 && !unicode) {
|
|
const { stdout } = await exec(
|
|
process.execPath,
|
|
[
|
|
entrypoint,
|
|
"function",
|
|
target.path,
|
|
procedure,
|
|
"--provider",
|
|
"ghidra",
|
|
"--json",
|
|
],
|
|
{ env: sessionEnv, timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
|
|
);
|
|
assert.ok(JSON.parse(stdout).normalized_result.procedure);
|
|
assert.deepEqual(
|
|
(await readdir(parent)).filter((name) =>
|
|
name.startsWith("rea-ghidra-"),
|
|
),
|
|
[],
|
|
"CLI left a Ghidra runtime directory",
|
|
);
|
|
}
|
|
}
|
|
assert.equal(
|
|
createHash("sha256")
|
|
.update(await readFile(target.path))
|
|
.digest("hex"),
|
|
before,
|
|
);
|
|
return {
|
|
status: "verified",
|
|
samples,
|
|
cli_boundary: true,
|
|
target_unchanged: true,
|
|
};
|
|
} finally {
|
|
// A failed cleanup retains its exact workspace for investigation and retry.
|
|
if (!cleanupIncomplete) await rm(root, { recursive: true, force: true });
|
|
}
|
|
}
|