Files
rea/scripts/verify-packaged-ghidra-windows.mjs
Jonah Paul Simon 162f9bf7ff fix(ghidra): clear wrong no-return flags before serving a session (#1419)
* fix(ghidra): clear wrong no-return flags before serving a session

Auto-analysis can flag ordinary functions as no-return (stream readers,
__cxa_guard_acquire, ...). Their callers then end at the call and the
decompiler truncates them. Run a postScript before the bridge that clears the
flag on functions containing a real RET and re-disassembles the call sites that
were terminators.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(ghidra): refit in place and scope no-return call-site repair

Keep function types by refitting the owning functions with fixupFunctionBody
instead of removing and recreating them. Only redo call sites whose callee was
actually cleared (including thunks to it), skip sites followed by a function
start or x86 padding so real end calls do not run on, and always log the result.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(ghidra): constrain no-return repair to admitted return evidence

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-10 23:01:50 +08:00

463 lines
16 KiB
JavaScript

#!/usr/bin/env node
import assert from "node:assert/strict";
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
import { verifyGhidraCompactReferences } from "./lib/real-ghidra-compact-references.mjs";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import {
mkdtemp,
mkdir,
readFile,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { promisify } from "node:util";
import { fileURLToPath, pathToFileURL } from "node:url";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
if (process.platform !== "win32" || process.arch !== "x64")
throw new Error(
"Packaged Windows Ghidra acceptance requires a Windows x64 host.",
);
const exec = promisify(execFile);
const arguments_ = process.argv.slice(2);
const internalWorkspace = arguments_[0] === "--internal-workspace";
const fixtureArguments = internalWorkspace ? arguments_.slice(2) : arguments_;
const x86Fixture = fixtureArguments.includes("--x86");
const dllFixture = fixtureArguments.includes("--dll");
const [packageRootArgument, targetArgument, reportArgument] =
fixtureArguments.filter(
(argument) => argument !== "--x86" && argument !== "--dll",
);
// Windows locks the loaded package-owned addon until the verifier exits.
// Keep installation cleanup in a parent that never loads that addon.
if (!internalWorkspace) {
const ownedWorkspace = await mkdtemp(join(tmpdir(), "rea-packaged-ghidra-"));
let result;
try {
result = await exec(
process.execPath,
[
fileURLToPath(import.meta.url),
"--internal-workspace",
ownedWorkspace,
...arguments_,
],
{ timeout: 900_000, maxBuffer: 8 * 1024 * 1024 },
);
} catch (cause) {
if (typeof cause.stdout === "string") process.stdout.write(cause.stdout);
if (typeof cause.stderr === "string") process.stderr.write(cause.stderr);
throw cause;
} finally {
await rm(ownedWorkspace, { recursive: true, force: true });
}
const report = JSON.parse(result.stdout);
report.workspaceCleanup = true;
report.ok = true;
if (reportArgument !== undefined)
await writeFile(reportArgument, `${JSON.stringify(report)}\n`);
process.stdout.write(`${JSON.stringify(report)}\n`);
process.stderr.write(result.stderr);
process.exit(0);
}
assert.ok(arguments_[1] !== undefined, "Missing verifier workspace.");
const workspace = resolve(arguments_[1]);
let packageRoot = resolve(packageRootArgument ?? ".");
// The default lane verifies the npm artifact in an isolated prefix. Explicit
// package roots support an already-installed artifact without a second install.
if (packageRootArgument === undefined) {
try {
const npmEntry =
process.env.npm_execpath ??
join(dirname(process.execPath), "node_modules/npm/bin/npm-cli.js");
const packed = JSON.parse(
(
await exec(
process.execPath,
[
npmEntry,
"pack",
"--json",
"--ignore-scripts",
"--pack-destination",
workspace,
],
{ timeout: 120_000, maxBuffer: 8 * 1024 * 1024 },
)
).stdout,
);
assert.equal(
packed.length,
1,
"The package lane must produce one artifact.",
);
const prefix = join(workspace, "installed");
await exec(
process.execPath,
[
npmEntry,
"install",
"--prefix",
prefix,
"--ignore-scripts",
"--omit=dev",
"--no-audit",
"--no-fund",
join(workspace, packed[0].filename),
],
{ timeout: 120_000, maxBuffer: 8 * 1024 * 1024 },
);
packageRoot = join(prefix, "node_modules", "rea-agents");
} catch (cause) {
throw new Error("Packaged Windows verifier installation failed.", {
cause,
});
}
}
const target = resolve(
targetArgument ??
`build/fixtures/rea-ghidra-windows${x86Fixture ? "-x86" : ""}.${dllFixture ? "dll" : "exe"}`,
);
let TOOL_CONTRACTS, native, token, sha256;
let parsedTarget;
try {
({ TOOL_CONTRACTS } = await import(
pathToFileURL(join(packageRoot, "dist/contracts/toolContracts.js"))
));
const { requireWindowsNativeAuthority } = await import(
pathToFileURL(join(packageRoot, "dist/windows/WindowsNativeLoader.js"))
);
native = requireWindowsNativeAuthority();
token = native.call("process_caller_token", []);
assert.equal(
token.administrator,
false,
"The real-provider lane must run as an ordinary user.",
);
assert.equal(
token.elevated,
false,
"The real-provider lane must use a non-elevated token.",
);
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
sha256 = digest(await readFile(target));
const { parseBinaryTarget } = await import(
pathToFileURL(join(packageRoot, "dist/application/BinaryTargetResolver.js"))
);
parsedTarget = await parseBinaryTarget(target);
assert.equal(parsedTarget.ok, true);
assert.equal(parsedTarget.value.format, "pe");
assert.equal(parsedTarget.value.managed, false);
if (targetArgument === undefined) {
assert.equal(
parsedTarget.value.architecture,
x86Fixture ? "x86" : "x86_64",
);
assert.equal(
parsedTarget.value.executableRole,
dllFixture ? "shared-library" : "application",
);
}
} catch (cause) {
throw new Error("Packaged Windows verifier prerequisites failed.", { cause });
}
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
const runtimeParent = join(workspace, "runtime with spaces");
await mkdir(runtimeParent);
const callerDirectory = join(workspace, "caller cwd with spaces");
const scriptCollisions = [
"ReaGhidraBridge.java",
"ReaGhidraPrepareCom.java",
"ReaGhidraNoReturnFix.java",
];
await mkdir(callerDirectory);
for (const name of scriptCollisions) await mkdir(join(callerDirectory, name));
const environment = {
...Object.fromEntries(
Object.entries(process.env).filter(
([name]) => name.toLowerCase() !== "comspec",
),
),
ComSpec: (
process.env.ComSpec ??
join(process.env.SystemRoot ?? "C:\\Windows", "System32", "cmd.exe")
).replaceAll("\\", "/"),
REA_LOG_LEVEL: "error",
GHIDRA_HEADLESS_MAXMEM: "512m",
NODE_OPTIONS: "--max-old-space-size=512",
TEMP: runtimeParent.replaceAll("\\", "/"),
TMP: runtimeParent.replaceAll("\\", "/"),
HOME: workspace,
USERPROFILE: workspace,
APPDATA: join(workspace, "appdata"),
LOCALAPPDATA: join(workspace, "localappdata"),
};
const entry = join(packageRoot, "scripts/rea.mjs");
const report = {
ok: false,
platform: "win32",
architecture: "x64",
nonAdmin: true,
targetSha256: sha256,
targetArchitecture: parsedTarget.value.architecture,
targetRole: parsedTarget.value.executableRole,
cli: {},
mcpOperations: [],
nativeControls: "available",
runtimeCleanup: false,
};
const assertCleanup = async () => {
const entries = await readdir(runtimeParent);
report.runtimeResidualCount = entries.length;
report.runtimeResidualKinds = entries.map((name) =>
name.startsWith("rea-")
? "rea-runtime"
: name.startsWith("hsperfdata_")
? "jvm-performance-data"
: "other",
);
assert.equal(entries.length, 0, "Provider runtime cleanup left files.");
};
try {
const providers = JSON.parse(
(
await exec(process.execPath, [entry, "providers", "--json"], {
env: environment,
cwd: callerDirectory,
timeout: 30_000,
})
).stdout,
);
assert.equal(providers.analysis_provider_binding, null);
assert.ok(Array.isArray(providers.analysis_provider_candidates));
const ghidra = providers.analysis_provider_candidates.find(
({ provider }) => provider.id === "ghidra",
);
assert.ok(ghidra !== undefined);
assert.equal(ghidra.selected, false);
assert.equal(ghidra.target_support.status, "unknown");
report.cli.providers = true;
const inspected = await exec(
process.execPath,
[entry, "inspect", target, "--provider", "ghidra", "--format", "json"],
{
env: environment,
cwd: callerDirectory,
timeout: 360_000,
maxBuffer: 8 * 1024 * 1024,
},
);
const inspection = JSON.parse(inspected.stdout);
assert.ok(inspection.error === undefined);
report.cli.inspect = true;
await assertCleanup();
const transport = new StdioClientTransport({
command: process.execPath,
args: [entry, "mcp"],
env: environment,
cwd: callerDirectory,
stderr: "pipe",
});
const client = new Client({
name: "rea-packaged-windows-ghidra",
version: "1.0.0",
});
const call = async (name, arguments_) => {
report.lastOperation = name;
if (reportArgument !== undefined)
await writeFile(reportArgument, `${JSON.stringify(report)}\n`);
const contract = TOOL_CONTRACTS.find((value) => value.name === name);
assert.ok(contract !== undefined, `Missing canonical contract: ${name}`);
const argumentsParsed = contract.inputSchema.parse(arguments_);
const result = await client.callTool(
{ name, arguments: argumentsParsed },
{ timeout: 360_000 },
);
assert.notEqual(result.isError, true, `Packaged operation failed: ${name}`);
const projected = requireMcpOperationResult(result, name, contract);
report.mcpOperations.push(name);
return projected;
};
try {
await client.connect(transport);
await call("open_binary", { path: target });
await call("binary_overview", {});
assert.equal((await call("list_documents", {})).length, 1);
await call("list_names", {});
const procedures = await call("list_procedures", {});
assert.ok(procedures.length > 0);
if (targetArgument === undefined) {
const calleeAddress = x86Fixture ? "0x401010" : "0x140001010";
const calleeCode = await call("procedure_pseudo_code", {
procedure: calleeAddress,
});
assert.match(calleeCode, /\b(?:42|0x2a)\b/iu);
if (dllFixture) {
const callerAddress = x86Fixture ? "0x401000" : "0x140001000";
assert.ok(
(
await call("procedure_callees", { procedure: callerAddress })
).includes(calleeAddress),
);
assert.match(
await call("procedure_pseudo_code", { procedure: callerAddress }),
/\+\s*1\b/u,
);
}
}
assert.ok((await call("list_segments", {})).length > 0);
await call("list_strings", {});
const procedure =
procedures.find((item) => item.procedure?.external === false) ??
procedures[0];
assert.ok(procedure !== undefined);
const loadImage = await call("inspect_native_load_image", {});
assert.equal(
loadImage.status,
"unsupported",
"PE load-image attestation must remain explicitly unsupported.",
);
assert.ok(loadImage.observations !== undefined);
const memory = await call("read_bytes", {
address: procedure.address,
length: 16,
});
const mapping = await call("address_to_file_offset", {
address: procedure.address,
});
assert.equal(memory.complete, true);
assert.equal(memory.returned_bytes, 16);
assert.equal(
memory.bytes_hex,
(await readFile(target))
.subarray(mapping.file_offset, mapping.file_offset + 16)
.toString("hex"),
);
await call("address_name", { address: procedure.address });
assert.equal(
await call("procedure_address", { procedure: procedure.value }),
procedure.address,
);
await call("resolve_containing_procedure", { address: procedure.address });
await call("search_procedures", { pattern: procedure.value });
await call("search_strings", { pattern: "rea" });
for (const name of [
"procedure_assembly",
"procedure_callees",
"procedure_callers",
"procedure_info",
"procedure_pseudo_code",
"read_function_instructions",
"procedure_references",
"analyze_function",
])
await call(name, { procedure: procedure.value });
await call("xrefs", { address: procedure.address });
await call("inspect_native_data_type", { type: "/undefined8" });
await call("inspect_native_instruction", { address: procedure.address });
await call("resolve_native_call_targets", { address: procedure.address });
// Explicit caller-selected targets need not contain our fixture instructions.
const controlledTargets = [
"rea-ghidra-windows.exe",
"rea-ghidra-windows-x86.exe",
].map((name) => resolve("build", "fixtures", name));
if (controlledTargets.includes(target))
report.compactReferences = await verifyGhidraCompactReferences(
call,
target === controlledTargets[1],
async (command, address) => {
const result = await exec(
process.execPath,
[entry, command, target, address, "--provider", "ghidra", "--json"],
{
env: environment,
cwd: callerDirectory,
timeout: 360_000,
maxBuffer: 8 * 1024 * 1024,
},
);
const evidence = JSON.parse(result.stdout);
assert.ok(evidence.error === undefined, `CLI ${command} failed`);
const contract = TOOL_CONTRACTS.find(
({ name }) => name === command.replaceAll("-", "_"),
);
assert.ok(contract, `Missing CLI ${command} contract`);
contract.outputSchema.parse(evidence);
return evidence.normalized_result;
},
);
const expected = (
await import(
pathToFileURL(join(packageRoot, "dist/ghidra/GhidraSessionValues.js"))
)
).GHIDRA_SESSION_CAPABILITIES.filter(
(name) =>
name !== "ping" &&
name !== "shutdown" &&
name !== "annotate_native_function",
);
assert.ok(
expected.every((name) => report.mcpOperations.includes(name)),
"Packaged verification omitted an admitted operation.",
);
const session = await call("binary_session", {});
assert.equal(
session.analysis_provider_binding?.analysis_profile?.parameters
?.executable_role,
parsedTarget.value.executableRole,
"MCP profile must preserve the selected PE role.",
);
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 {
await client.close();
}
assert.equal(
digest(await readFile(target)),
sha256,
"Analysis modified the selected target.",
);
assert.deepEqual(
native.call("process_caller_token", []),
token,
"Analysis changed caller token authority.",
);
assert.deepEqual((await readdir(callerDirectory)).sort(), scriptCollisions);
for (const name of scriptCollisions)
assert.deepEqual(await readdir(join(callerDirectory, name)), []);
report.callerScriptCollisionsPreserved = true;
report.runtimeCleanup = true;
// Only the parent can attest success after removing the installation.
if (reportArgument !== undefined)
await writeFile(reportArgument, `${JSON.stringify(report)}\n`);
process.stdout.write(`${JSON.stringify(report)}\n`);
} catch (cause) {
report.failure = {
name: cause.name,
message: cause.message
.replaceAll(packageRoot, "<package>")
.replaceAll(target, "<target>")
.replaceAll(workspace, "<workspace>"),
};
if (reportArgument !== undefined)
await writeFile(reportArgument, `${JSON.stringify(report)}\n`);
throw cause;
}