Merge remote-tracking branch 'upstream/main' into fix/release-exposure-audit

This commit is contained in:
Matt Van Horn committed 2026-11-07 16:22:07 -08:00
commit b2219ee16d
1374 files changed
+75859 -8498

No files matched your search

+1
View File
@@ -229,6 +229,7 @@ if (tier === "reused") {
file,
) ||
file.startsWith("docs/") ||
file === "scripts/verify-documentation-routes.mjs" ||
file.startsWith(".github/ISSUE_TEMPLATE/")
) {
selected.docs = true;
+1
View File
@@ -102,6 +102,7 @@ const PATH_SCOPES = {
"tests/boundary/providers/RizinTimeoutEvidence.test.ts",
"tests/boundary/providers/RizinDebugSession*.test.ts",
"tests/boundary/providers/RizinInferiorTty.test.ts",
"tests/boundary/providers/RizinDecoderInputs.test.ts",
"tests/boundary/providers/RizinTestInferiorTty.ts",
"tests/boundary/process/ownedDebuggerSession.test.ts",
"tests/boundary/cli/debuggerOneShotExit.test.ts",
+146 -60
View File
@@ -1,15 +1,26 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { createVerifierCommands } from "./verify-commands.mjs";
import { createOwnedMcpStdioTransport } from "./owned-mcp-stdio.mjs";
import { withVerifierCleanup } from "./verifier-cleanup.mjs";
import { INLINE_EVIDENCE_MCP_ENVIRONMENT } from "./mcp-verification-environment.mjs";
// REA registers process-static tool contracts. Bind their parsed definitions to
// the SDK's negotiated connection identity, including when a Client reconnects.
// Bind parsed definitions to this connection. Hot/cold catalogs may add tools;
// refresh a missing name once without throwing away already validated bindings.
const toolBindings = new WeakMap();
const readDefinitions = (client) =>
client.listTools().then(({ tools }) => {
const definitions = new Map();
for (const tool of tools) {
assert.ok(
!definitions.has(tool.name),
`Duplicate MCP tool: ${tool.name}`,
);
definitions.set(tool.name, tool);
}
return definitions;
});
const advertisedTool = async (client, name) => {
const connection = client.getServerVersion();
@@ -18,23 +29,19 @@ const advertisedTool = async (client, name) => {
if (binding?.connection !== connection) {
binding = {
connection,
definitions: client.listTools().then(({ tools }) => {
const definitions = new Map();
for (const tool of tools) {
assert.ok(
!definitions.has(tool.name),
`Duplicate MCP tool: ${tool.name}`,
);
definitions.set(tool.name, tool);
}
return definitions;
}),
definitions: readDefinitions(client),
};
toolBindings.set(client, binding);
}
let definitions;
const snapshot = binding.definitions;
try {
definitions = await binding.definitions;
definitions = await snapshot;
if (!definitions.has(name)) {
if (binding.definitions === snapshot)
binding.definitions = readDefinitions(client);
definitions = await binding.definitions;
}
assert.equal(
client.getServerVersion(),
connection,
@@ -67,6 +74,23 @@ export async function artifactMcpCall(client, parameters, options = {}) {
return client.callTool(parameters, { ...options, toolDefinition });
}
/** Retain the exact command owner when a verifier cannot finish cleanup. */
class ArtifactVerifierCleanupError extends Error {
constructor(cause, cleanupOwner) {
super("Artifact verifier cleanup is incomplete", { cause });
this.name = "ArtifactVerifierCleanupError";
this.cleanupOwner = cleanupOwner;
}
}
const closeOwnedCommands = async (owner) => {
try {
await owner.close();
} catch (cause) {
throw new ArtifactVerifierCleanupError(cause, owner);
}
};
const environment = () => {
const compileCache =
process.env.NODE_COMPILE_CACHE ??
@@ -101,68 +125,130 @@ export async function artifactCliEvidence(
command,
target,
arguments_ = [],
{ timeoutMs = 60000 } = {},
{ timeoutMs = 60000, signal, commands } = {},
) {
const { stdout } = await promisify(execFile)(
process.execPath,
[
fileURLToPath(new URL("../rea.mjs", import.meta.url)),
command,
target,
...arguments_,
"--json",
],
{ env: environment(), timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 },
);
signal?.throwIfAborted();
const owner = commands ?? (await createVerifierCommands());
const execute = () =>
owner.exec(
process.execPath,
[
fileURLToPath(new URL("../rea.mjs", import.meta.url)),
command,
target,
...arguments_,
"--json",
],
{
env: environment(),
timeout: timeoutMs,
maxBuffer: 16 * 1024 * 1024,
signal,
},
);
const { stdout } =
commands === undefined
? await withVerifierCleanup(execute, [() => closeOwnedCommands(owner)])
: await execute();
signal?.throwIfAborted();
const evidence = JSON.parse(stdout);
assert.equal(evidence.error, undefined, stdout);
assert.ok(evidence.normalized_result, "CLI omitted normalized result");
return evidence;
}
const withOpenedArtifactMcp = async (target, openTool, verify) => {
const transport = new StdioClientTransport({
const withOpenedArtifactMcp = async (
target,
openTool,
verify,
options = {},
) => {
options.signal?.throwIfAborted();
const owner = options.commands ?? (await createVerifierCommands());
const transport = await createOwnedMcpStdioTransport({
command: process.execPath,
args: [fileURLToPath(new URL("../rea.mjs", import.meta.url)), "mcp"],
env: { ...environment(), ...INLINE_EVIDENCE_MCP_ENVIRONMENT },
stderr: "pipe",
arguments: [fileURLToPath(new URL("../rea.mjs", import.meta.url)), "mcp"],
hostEnvironment: { ...environment(), ...INLINE_EVIDENCE_MCP_ENVIRONMENT },
cwd: process.cwd(),
expectedCommand: null,
sweepTokenOwnedProcesses: true,
signal: options.signal,
});
owner.adopt(transport, "artifact MCP process");
const client = new Client({ name: "artifact-real-e2e", version: "1" });
return withVerifierCleanup(async () => {
await client.connect(transport);
const opened = await artifactMcpCall(client, {
name: openTool,
arguments: { path: target },
});
assert.notEqual(opened.isError, true, JSON.stringify(opened));
if (openTool === "open_bundle_resources")
assert.equal(opened.structuredContent?.result?.caller_path, target);
await verify(client);
}, [
() => {
toolBindings.delete(client);
},
() => client.close(),
() => transport.close(),
]);
let clientClose;
const closeClient = () =>
(clientClose ??= Promise.resolve().then(() => client.close()));
const abortTransport = () => {
// Keep the same promise for finalization; concurrent SDK closes can return
// before the first close has observed the child exit.
void closeClient().catch(() => undefined);
};
options.signal?.addEventListener("abort", abortTransport, { once: true });
try {
const result = await withVerifierCleanup(async () => {
options.signal?.throwIfAborted();
await client.connect(transport);
options.signal?.throwIfAborted();
const opened = await artifactMcpCall(
client,
{
name: openTool,
arguments: { path: target },
},
{ signal: options.signal },
);
assert.notEqual(opened.isError, true, JSON.stringify(opened));
if (openTool === "open_bundle_resources")
assert.equal(opened.structuredContent?.result?.caller_path, target);
await verify(client);
}, [
() => {
toolBindings.delete(client);
},
() => closeClient(),
() => transport.close(),
...(options.commands === undefined
? [() => closeOwnedCommands(owner)]
: []),
]);
options.signal?.throwIfAborted();
return result;
} finally {
options.signal?.removeEventListener("abort", abortTransport);
}
};
/** Run a real stdio MCP subprocess with an explicitly opened binary target. */
export async function withArtifactMcp(target, verify) {
return withOpenedArtifactMcp(target, "open_binary", verify);
export async function withArtifactMcp(target, verify, options = {}) {
return withOpenedArtifactMcp(target, "open_binary", verify, options);
}
/** Run a real stdio MCP subprocess with an explicitly opened bundle resource root. */
export async function withBundleResourcesMcp(target, verify) {
return withOpenedArtifactMcp(target, "open_bundle_resources", verify);
export async function withBundleResourcesMcp(target, verify, options = {}) {
return withOpenedArtifactMcp(
target,
"open_bundle_resources",
verify,
options,
);
}
/** Require a successful structured MCP tool result rather than its text rendering. */
export async function artifactMcpResult(client, name, arguments_ = {}) {
const result = await artifactMcpCall(client, {
name,
arguments: arguments_,
});
export async function artifactMcpResult(
client,
name,
arguments_ = {},
options = {},
) {
const result = await artifactMcpCall(
client,
{
name,
arguments: arguments_,
},
options,
);
assert.notEqual(result.isError, true, JSON.stringify(result));
assert.ok(
result.structuredContent?.normalized_result,
@@ -0,0 +1,5 @@
/** Allocate under a caller-selected parent and return the host's canonical spelling. */
export declare const createCanonicalTemporaryDirectory: (
prefix: string,
parent?: string,
) => Promise<{ readonly path: string; readonly allocatedPath: string }>;
@@ -0,0 +1,17 @@
import { mkdtemp, realpath, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
/** Allocate under a caller-selected parent and return the host's canonical spelling. */
export const createCanonicalTemporaryDirectory = async (
prefix,
parent = tmpdir(),
) => {
const allocatedPath = await mkdtemp(join(parent, prefix));
try {
return { path: await realpath(allocatedPath), allocatedPath };
} catch (cause) {
await rm(allocatedPath, { recursive: true, force: true });
throw cause;
}
};
+9 -4
View File
@@ -121,14 +121,19 @@ export const documentationFactIssues = async (root, catalog) => {
issues,
path,
content,
"(docs/installation.md#supported-agents)",
"(docs/getting-started/installation.md#supported-agents)",
);
requireText(issues, path, content, "(docs/installation.md#mcp-registry)");
requireText(
issues,
path,
content,
"(docs/mcp-contracts.md#generated-catalog)",
"(docs/getting-started/installation.md#mcp-registry)",
);
requireText(
issues,
path,
content,
"(docs/reference/mcp-contracts.md#generated-catalog)",
);
}
}
@@ -156,7 +161,7 @@ export const documentationFactIssues = async (root, catalog) => {
)
issues.push(`${path}: documentation links differ from README.md`);
const installationPath = "docs/installation.md";
const installationPath = "docs/getting-started/installation.md";
const installation = await readFile(join(root, installationPath), "utf8");
for (const client of catalog.setup_clients)
requireText(issues, installationPath, installation, client.display_name);
+8
View File
@@ -18,6 +18,7 @@ import {
} from "./mcp-verifier-results.mjs";
import { verifyGhidraBoundaries } from "./real-ghidra-boundaries.mjs";
import { verifyGhidraProjectIdentity } from "./real-ghidra-project-identity.mjs";
import { verifyGhidraSocketBoundary } from "./real-ghidra-socket-boundary.mjs";
const defaultEntrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
const environment = () => ({
@@ -355,6 +356,12 @@ async function verifyNativeValueAdapters(
entrypoint,
env,
});
const socketBoundary = await verifyGhidraSocketBoundary({
target,
procedure,
entrypoint,
env,
});
return {
mocked: false,
cli: true,
@@ -363,6 +370,7 @@ async function verifyNativeValueAdapters(
direct_function_comparison: true,
boundaries,
project_identity: projectIdentity,
socket_boundary: socketBoundary,
nodes: cliGraph.total_nodes,
edges: cliGraph.total_edges,
decompilations: cliGraph.decompilations,
+24
View File
@@ -0,0 +1,24 @@
import { importVerifierSource } from "./verifier-source-loader.mjs";
let implementation;
/** Create an SDK-compatible transport which retains the exact process owner. */
export async function createOwnedMcpStdioTransport(options) {
implementation ??= importVerifierSource(
new URL("../../src/process/OwnedMcpStdioTransport.ts", import.meta.url)
.href,
import.meta.url,
);
const { OwnedMcpStdioTransport } = await implementation;
return new OwnedMcpStdioTransport(options, {
provider: "artifact-verifier",
label: "Artifact verifier",
// Match the SDK's default maximum buffered stdio message size.
maxFrameBytes: 10 * 1024 * 1024,
// Unlike the JADX adapter, artifact verifiers do not impose a cumulative
// per-connection output limit; the SDK bounds each buffered message.
maxOperationBytes: Infinity,
gracefulInputCloseMs: 2000,
maxDiagnosticBytes: 64 * 1024,
});
}
@@ -1,8 +1,12 @@
import type { Client } from "@modelcontextprotocol/client";
/** Verify segmented aliases and restore the entry's observed public name. */
/**
* Verify segmented aliases and restore the entry's observed public name.
* With `mutable: false` (Windows P0), annotation and naming edits must be refused.
*/
export function verifyGhidraAddressBoundaries(
client: Pick<Client, "callTool">,
entry: string,
cli: (command: string, address: string, flags: string[]) => Promise<unknown>,
options?: { readonly mutable?: boolean },
): Promise<void>;
+75 -24
View File
@@ -1,8 +1,16 @@
import { requireMcpToolError } from "./mcp-verifier-results.mjs";
import assert from "node:assert/strict";
/** Exercise segmented-address truncation through real CLI/MCP reads and edits. */
export async function verifyGhidraAddressBoundaries(client, entry, cli) {
/**
* Exercise segmented-address truncation through real CLI/MCP reads and edits.
* A read-only provider (Windows P0) must refuse edits before parsing them.
*/
export async function verifyGhidraAddressBoundaries(
client,
entry,
cli,
{ mutable = true } = {},
) {
const query = async (name, args) => {
const reply = await client.callTool(
{ name, arguments: args },
@@ -45,6 +53,14 @@ export async function verifyGhidraAddressBoundaries(client, entry, cli) {
);
assert.equal(reply.isError, true, `${name} accepted ${address}`);
const error = requireMcpToolError(reply);
if (!mutable && name === "annotate_native_function") {
assert.equal(
error?.code,
"capability_unavailable",
JSON.stringify(reply),
);
continue;
}
assert.equal(error?.code, "invalid_request", JSON.stringify(reply));
assert.equal(error.details.operation, name);
const issues = JSON.stringify(error.details.issues);
@@ -65,6 +81,63 @@ export async function verifyGhidraAddressBoundaries(client, entry, cli) {
await query("read_bytes", { address: padded, length: 1 }),
await query("read_bytes", { address: entry, length: 1 }),
);
if (mutable) await verifyAliasNaming(query, entry, padded);
else await verifyNamingRefused(client, query, entry, padded);
for (const [command, flags, operation] of [
["read-bytes", ["--length", "1"], "read_bytes"],
[
"annotate-native-function",
["--name", "MUST_NOT_ALIAS"],
"annotate_native_function",
],
]) {
let failure;
try {
await cli(command, representations[0], flags);
} catch (error) {
assert.equal(error.code, 1);
failure = JSON.parse(error.stdout);
}
if (!mutable && operation === "annotate_native_function") {
assert.equal(failure?.code, "capability_unavailable");
continue;
}
assert.equal(failure?.code, "invalid_request");
assert.equal(failure.details.operation, operation);
assert.match(
JSON.stringify(failure.details.issues),
/cannot be represented without truncation/u,
);
}
}
/** A read-only provider refuses both naming operations and keeps the entry name. */
async function verifyNamingRefused(client, query, entry, padded) {
const originalName = await query("address_name", { address: entry });
for (const [name, args] of [
["set_address_name", { address: entry, name: "MUST_NOT_RENAME" }],
[
"set_addresses_names",
{ names: { [entry]: "MUST_NOT_RENAME", [padded]: "MUST_NOT_RENAME" } },
],
]) {
const reply = await client.callTool(
{ name, arguments: args },
{ timeout: 240000 },
);
assert.equal(reply.isError, true, `${name} was not refused`);
const error = requireMcpToolError(reply);
assert.equal(error?.code, "capability_unavailable", JSON.stringify(reply));
}
assert.equal(
await query("address_name", { address: entry }),
originalName,
"A refused naming edit changed the entry name",
);
}
/** Equivalent spellings of one address share or reject one naming edit together. */
async function verifyAliasNaming(query, entry, padded) {
const originalName = await query("address_name", { address: entry });
assert.equal(
typeof originalName,
@@ -127,26 +200,4 @@ export async function verifyGhidraAddressBoundaries(client, entry, cli) {
throw cleanupCause;
}
}
for (const [command, flags, operation] of [
["read-bytes", ["--length", "1"], "read_bytes"],
[
"annotate-native-function",
["--name", "MUST_NOT_ALIAS"],
"annotate_native_function",
],
]) {
let failure;
try {
await cli(command, representations[0], flags);
} catch (error) {
assert.equal(error.code, 1);
failure = JSON.parse(error.stdout);
}
assert.equal(failure?.code, "invalid_request");
assert.equal(failure.details.operation, operation);
assert.match(
JSON.stringify(failure.details.issues),
/cannot be represented without truncation/u,
);
}
}
+16 -3
View File
@@ -13,6 +13,7 @@ import { promisify } from "node:util";
import { GHIDRA_ANNOTATIONS_RETAINED } from "../../dist/ghidra/GhidraProviderClient.js";
import Ajv from "ajv/dist/2020.js";
import addFormats from "ajv-formats";
import { OFFICIAL_TOOL_CONTRACTS } from "../../dist/contracts/officialToolContracts.js";
import { verifyLegacyGhidraReferenceSnapshot } from "./ghidra-reference-snapshot-e2e.mjs";
import { verifyGhidraSnapshotLifecycle } from "./real-ghidra-snapshot-lifecycle.mjs";
@@ -22,6 +23,7 @@ import { verifyGhidraEntryAliases } from "./real-ghidra-entry-aliases.mjs";
import { verifyGhidraNamespaceAnnotations } from "./real-ghidra-namespace-annotations.mjs";
import { verifyGhidraBatchIdentity } from "./real-ghidra-batch-identity.mjs";
import { verifyGhidraInstructionFlow } from "./real-ghidra-instruction-flow.mjs";
import { verifyGhidraSearchFacts } from "./real-ghidra-search-facts.mjs";
/** Probe real Ghidra location, annotation and error contracts through public adapters. */
export async function verifyGhidraBoundaries(
@@ -30,6 +32,14 @@ export async function verifyGhidraBoundaries(
) {
const options = { timeout: 180000 };
const catalog = await client.listTools();
const annotation = catalog.tools.find(
(tool) => tool.name === "annotate_native_function",
);
const annotationContract = OFFICIAL_TOOL_CONTRACTS.find(
(tool) => tool.name === "annotate_native_function",
);
assert.ok(annotationContract, "Annotation contract is missing");
assert.equal(annotation?.description, annotationContract.description);
const ajv = new Ajv({ strict: false });
addFormats(ajv);
const validators = new Map();
@@ -401,6 +411,11 @@ export async function verifyGhidraBoundaries(
(item) => item.value === "REA_GHIDRA_INVENTORY_ENTRY",
);
assert.ok(marker);
await verifyGhidraSearchFacts(call, {
strings,
procedures,
entryAddress: address,
});
assert.ok(
(
await call("search_strings", { pattern: marker.value.toLowerCase() })
@@ -589,9 +604,7 @@ export async function verifyGhidraBoundaries(
const longString = strings.find((item) => item.value === longLiteral);
assert.ok(longString, "The provider must retain the full regression literal");
const literalMatches = await call("search_strings", { pattern: longLiteral });
assert.deepEqual(literalMatches, [
{ address: longString.address, value: longLiteral },
]);
assert.deepEqual(literalMatches, [longString]);
assert.deepEqual(await cli("search", longLiteral), literalMatches);
await assert.rejects(
cli("search", regexPattern, ["--mode", "regex"]),
@@ -156,6 +156,15 @@ export async function verifyGhidraEntryAliases({
alias.address,
"Explicit interior addresses keep their existing containment semantics",
);
const interiorEdit = await call("annotate_native_function", {
procedure: interior.address,
comment: "Selected by interior address",
});
assert.equal(interiorEdit.annotations.address, alias.address);
assert.equal(
interiorEdit.annotations.comment,
"Selected by interior address",
);
const edited = await call("annotate_native_function", {
procedure: alias.value,
comment: "Selected by imported entry alias",
+58
View File
@@ -0,0 +1,58 @@
import assert from "node:assert/strict";
/** Compare public searches and composed traces with their provider inventory rows. */
export async function verifyGhidraSearchFacts(
call,
{ strings, procedures, entryAddress },
) {
const marker = strings.find(
(row) => row.value === "REA_GHIDRA_INVENTORY_ENTRY",
);
const entry = procedures.find((row) => row.address === entryAddress);
const external = procedures.find((row) => row.procedure.external);
const thunk = procedures.find((row) => row.procedure.thunk);
assert.ok(marker, "Missing source string fixture");
assert.ok(entry, "Missing selected entry procedure");
assert.ok(external, "Missing external procedure fixture");
const cases = [
{ type: "string", operation: "search_strings", row: marker },
...[entry, external, ...(thunk === undefined ? [] : [thunk])].map(
(row) => ({
type: "procedure",
operation: "search_procedures",
row,
}),
),
];
for (const { type, operation, row } of cases) {
const search = await call(operation, {
pattern: row.value,
mode: "literal",
case_sensitive: true,
});
const match = search.find(
(item) => item.address === row.address && item.value === row.value,
);
assert.deepEqual(
match,
row,
`${operation} must preserve inventory facts for ${row.value}`,
);
const trace = await call("trace_feature", {
query: row.value,
case_sensitive: true,
});
const traced = trace.matches.find(
(item) =>
item.type === type &&
item.address === row.address &&
item.value === row.value,
);
assert.deepEqual(
traced,
{ ...row, type },
`trace_feature must preserve ${operation} facts for ${row.value}`,
);
}
return { compared: cases.length, thunk_observed: thunk !== undefined };
}
+200
View File
@@ -0,0 +1,200 @@
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 });
}
}
+2 -1
View File
@@ -476,7 +476,8 @@ export async function verifyInventoryHistory(run, raw, source) {
"package-lock.json",
"server.json",
"src/generatedPackageMetadata.ts",
"docs/installation.md",
"docs/installation.md", // Historical releases before documentation organization.
"docs/getting-started/installation.md",
`docs/releases/${inventory.version}.contributions.json`,
]);
const paths = (
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env node
// Pack this checkout as the REA a build computer runs, the archive REA_BUILD_COMPUTER_REA_ARCHIVE names:
// `npm pack`, its package folder as rea-prod with the lockfile, production dependencies installed with
// `npm ci --omit=dev --omit=optional`, then rea-prod as a .tar.xz.
// Usage: node scripts/pack-build-computer-runtime.mjs <archive.tar.xz>
import { spawnSync } from "node:child_process";
import { copyFile, mkdtemp, readdir, rename, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
const run = (command, args, cwd) => {
const result = spawnSync(command, args, { cwd, stdio: "inherit" });
if (result.error !== undefined) throw result.error;
if (result.status !== 0)
throw new Error(
`${command} ${args.join(" ")} failed with status ${String(result.status)}`,
);
};
const [destination] = process.argv.slice(2);
if (destination === undefined || !destination.endsWith(".tar.xz")) {
process.stderr.write(
"Usage: node scripts/pack-build-computer-runtime.mjs <archive.tar.xz>\n",
);
process.exit(2);
}
const archive = resolve(destination);
const work = await mkdtemp(join(tmpdir(), "rea-build-runtime-"));
try {
run(npm, ["pack", "--pack-destination", work], root);
const [packed] = (await readdir(work)).filter((name) =>
name.endsWith(".tgz"),
);
if (packed === undefined) throw new Error("npm pack wrote no .tgz");
run("tar", ["-xzf", packed], work);
const runtime = join(work, "rea-prod");
await rename(join(work, "package"), runtime);
await copyFile(
join(root, "package-lock.json"),
join(runtime, "package-lock.json"),
);
run(npm, ["ci", "--omit=dev", "--omit=optional"], runtime);
run("tar", ["-cJf", archive, "-C", work, "rea-prod"], work);
const { size } = await stat(archive);
process.stdout.write(
`${archive} (${(size / 1024 / 1024).toFixed(1)} MiB): set REA_BUILD_COMPUTER_REA_ARCHIVE to it.\n`,
);
} finally {
await rm(work, { recursive: true, force: true });
}
+1 -1
View File
@@ -27,7 +27,7 @@ const MIN_RETRY_MS = 5 * 60_000;
for (const key of ["REA_AGENT_ENV_COMMAND", "REA_DOCKER_COMMAND"])
if (!isAbsolute(process.env[key] ?? ""))
throw new Error(
`verify:agentenv:environment requires absolute ${key}; see docs/testing.md. No dependencies are installed by this verifier.`,
`verify:agentenv:environment requires absolute ${key}; see docs/development/testing.md. No dependencies are installed by this verifier.`,
);
const agentEnv = process.env.REA_AGENT_ENV_COMMAND;
const docker = process.env.REA_DOCKER_COMMAND;
+130
View File
@@ -0,0 +1,130 @@
import assert from "node:assert/strict";
import { readFile, readdir, stat } from "node:fs/promises";
import { basename, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
// Public route baseline before categorization, including upstream signal recordings.
// This fixture is independent of the navigation/rewrite implementation.
const routes = `
adb-device-analysis android-analysis apktool-resource-analysis apple-application-analysis
binary-diagnostics browser-observation browser-scenario-contract cli client-configuration
client-verification electron-observation evm-bytecode firmware-analysis flutter-analysis
frida-instrumentation ghidra-dos ghidra-mips ghidra-nativeaot ghidra-provider ghidra-psp
ghidra-riscv go-binaries harmony-analysis http3-capture-lab ida-provider installation
javascript-application-graph javascript-application-workflows javascript-artifact-reconstruction
javascript-recovery javascript-runtime-observation javascript-runtime-reconciliation
jeb-analysis managed-code-analysis mcp-contracts mcp-prompts migration-6.2
native-investigation packet-captures pcap-analysis process-capture raw-binary reconstruction-obligation-ledgers
reconstruction-readiness recorded-crashes releasing roadmap signal-recordings sqlite-databases
testing tool-design wasm-artifacts web-network-captures web-runtime web-source-location
website-module-trace website-script-export windows-ghidra-p0
binary-hardening desktop-application-capture desktop-application-export desktop-application-state
desktop-application-tasks desktop-build-computer desktop-build-witness ines-containers
design/limits-and-recovery
`
.trim()
.split(/\s+/u);
assert.equal(routes.length, 67);
assert.equal(new Set(routes).size, routes.length);
const documentation = resolve(
fileURLToPath(new URL("../docs/", import.meta.url)),
);
const output = resolve(documentation, ".vitepress/dist");
const origin = "https://documentation.invalid";
const checked = new Set();
let internalTargets = 0;
let sourceLinks = 0;
// Legacy public routes remain an independent baseline; all generated pages also
// participate so newly authored directories/specifications cannot escape checks.
for (const route of routes) {
const html = await readFile(resolve(output, route + ".html"), "utf8");
const edit = physicalEditSource(html, route);
assert.equal(basename(edit), basename(route) + ".md");
}
const htmlPages = [];
const directories = [output];
while (directories.length > 0) {
const directory = directories.pop();
for (const entry of await readdir(directory, { withFileTypes: true })) {
const path = resolve(directory, entry.name);
if (entry.isDirectory()) directories.push(path);
else if (entry.isFile() && entry.name.endsWith(".html"))
htmlPages.push(path);
}
}
for (const page of htmlPages.sort()) {
const route = page
.slice(output.length + 1)
.split(sep)
.join("/");
const html = await readFile(page, "utf8");
// The configured home layout and generated 404 render no edit action.
// Their outgoing links still participate in the same all-page scan.
if (route !== "404.html" && route !== "index.html") {
const source = physicalEditSource(html, route);
assert.ok(
(await stat(source)).isFile(),
route + ": edit link source is missing",
);
sourceLinks++;
}
for (const match of html.matchAll(
/<(?:a|img)\b[^>]*\b(?:href|src)="([^"]+)"/gu,
)) {
const destination = match[1].replaceAll("&amp;", "&");
const url = new URL(destination, origin + "/rea/" + route);
if (url.origin !== origin) continue;
assert.ok(
url.pathname.startsWith("/rea/"),
route + ": local link escaped site base " + destination,
);
const key = url.pathname + url.hash;
if (checked.has(key)) continue;
checked.add(key);
let target = decodeURIComponent(url.pathname.slice("/rea/".length));
if (!target || target.endsWith("/")) target += "index.html";
else if (!basename(target).includes(".")) target += ".html";
const physical = resolve(output, target);
assert.ok(
physical.startsWith(output + sep),
destination + ": published path escaped site output",
);
assert.ok(
(await stat(physical)).isFile(),
route + ": broken rendered target " + destination,
);
if (url.hash && target.endsWith(".html")) {
const fragment = decodeURIComponent(url.hash.slice(1));
const page = await readFile(physical, "utf8");
assert.ok(
page.includes('id="' + fragment + '"'),
route + ": missing rendered anchor " + destination,
);
}
internalTargets++;
}
}
function physicalEditSource(html, route) {
const edit = html.match(
/href="https:\/\/github\.com\/morluto\/rea\/edit\/main\/docs\/([^"]+)"/u,
);
assert.ok(edit, route + ": missing physical-source edit link");
const source = resolve(documentation, decodeURIComponent(edit[1]));
assert.ok(
source.startsWith(documentation + sep),
route + ": source escaped documentation root",
);
return source;
}
console.log(
JSON.stringify({
ok: true,
published_routes: routes.length,
html_pages: htmlPages.length,
physical_edit_links: sourceLinks,
distinct_internal_targets_and_anchors: internalTargets,
scope:
"actual generated HTML, local guide routes/edit sources/images and anchors; external network links are not fetched",
}),
);
+18
View File
@@ -61,6 +61,10 @@ const stable = (result) =>
file_address: event.file_address,
selector: event.selector,
receiver_class: event.receiver_class,
source_location: event.source_location,
caller_source_locations: event.backtrace.map(
(frame) => frame.source_location,
),
counter:
event.symbol === GREET
? event.registers[3].value
@@ -96,6 +100,18 @@ const checkScenario = async (binary) => {
});
const greet = result.events.filter(({ symbol }) => symbol === GREET);
const add = result.events.filter(({ symbol }) => symbol === "rea_call_add");
for (const location of [
...result.events,
...result.events.flatMap((event) => event.backtrace),
...result.breakpoints.flatMap((breakpoint) => breakpoint.locations),
]) {
assert.equal(location.source_location?.file_path, SOURCE);
assert.ok(location.source_location.line > 0);
assert.ok(
location.source_location.column === null ||
location.source_location.column > 0,
);
}
assert.equal(greet.length, 3, JSON.stringify(result));
assert.equal(add.length, 3, JSON.stringify(result));
for (const [index, event] of greet.entries()) {
@@ -269,6 +285,8 @@ try {
const binary = join(directory, "ReaCalls");
await exec("/usr/bin/xcrun", [
"clang",
"-g",
"-O0",
"-fobjc-arc",
"-framework",
"Foundation",
+186
View File
@@ -0,0 +1,186 @@
import assert from "node:assert/strict";
import { readFile, rm } from "node:fs/promises";
import { isAbsolute, join } from "node:path";
import { fileURLToPath } from "node:url";
import { createPackageVerifierCommands } from "./lib/verify-package-commands.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
import {
artifactCli,
artifactMcpResult,
withArtifactMcp,
} from "./lib/artifact-e2e.mjs";
import { createCanonicalTemporaryDirectory } from "./lib/canonical-temporary-directory.mjs";
const cancellation = new AbortController();
let shutdownSignal;
const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
const forwardCancellation = (signal) => {
if (shutdownSignal !== undefined) return;
shutdownSignal = signal;
cancellation.abort(new Error(`Verification cancelled by ${signal}`));
};
const signalHandlers = Object.fromEntries(
Object.keys(signalExitCodes).map((signal) => [
signal,
() => forwardCancellation(signal),
]),
);
for (const [signal, handler] of Object.entries(signalHandlers))
process.on(signal, handler);
const compiler = process.env.REA_VERIFY_FPC_COMMAND;
if (process.platform !== "darwin" || !isAbsolute(compiler ?? ""))
throw new Error(
"Requires macOS and an absolute caller-selected REA_VERIFY_FPC_COMMAND; build the runtime first.",
);
const source = fileURLToPath(
new URL("../tests/conformance/native/calls.pas", import.meta.url),
);
const fixtureLines = (await readFile(source, "utf8")).split(/\r?\n/u);
const callerLines = new Set(
["REA_VERIFY_CALLER_LOOP", "REA_VERIFY_CALLER_CALL"].map((marker) => {
const matches = fixtureLines.flatMap((line, index) =>
line.includes(marker) ? [index + 1] : [],
);
assert.equal(matches.length, 1, `fixture has one ${marker} marker`);
return matches[0];
}),
);
const allocatedDirectory =
await createCanonicalTemporaryDirectory("rea-pascal-calls-");
let directory = allocatedDirectory.path;
const commands = await createPackageVerifierCommands({
workspace: {
path: allocatedDirectory.allocatedPath,
close: () =>
rm(allocatedDirectory.allocatedPath, { recursive: true, force: true }),
},
});
const execute = (command, arguments_, { timeout }) =>
commands.exec(command, arguments_, {
timeout,
diagnosticBytes: 1024 * 1024,
signal: cancellation.signal,
});
const input = {
breakpoints: [{ kind: "function", name: "rea_pascal_call" }],
argument_registers: 1,
backtrace_frames: 1,
duration_ms: 30_000,
};
const inspect = (result) => {
assert.equal(result.process.outcome, "exited");
assert.equal(result.process.exit_status, 0);
assert.equal(result.process.terminated, true);
assert.equal(result.process.stdout.text, "pascal 0\npascal 1\npascal 2\n");
assert.equal(result.events.length, 3);
assert.deepEqual(
result.events.map((event) => event.registers[0].value),
["0x0", "0x1", "0x2"],
);
assert.equal(result.breakpoints[0].location_count, 1);
for (const event of result.events) {
assert.equal(event.backtrace.length, 1);
assert.equal(event.backtrace[0].source_location?.file_path, source);
assert.equal(event.backtrace[0].module_path, join(directory, "calls"));
assert.ok(
callerLines.has(event.backtrace[0].source_location.line),
"caller is the Pascal loop/call site",
);
}
for (const location of [
...result.events,
...result.breakpoints[0].locations,
]) {
assert.equal(location.source_location?.file_path, source);
assert.ok(location.source_location.line > 0);
}
};
const stable = (result) =>
result.events.map((event) => ({
file_address: event.file_address,
source_location: event.source_location,
registers: event.registers,
callers: event.backtrace.map((frame) => ({
file_address: frame.file_address,
symbol: frame.symbol,
source_location: frame.source_location,
})),
}));
try {
const result = await withVerifierCleanup(async () => {
const version = (
await execute(compiler, ["-iV"], { timeout: 30_000 })
).stdout.trim();
const architecture = (
await execute(compiler, ["-iTP"], {
timeout: 30_000,
})
).stdout.trim();
await execute(
compiler,
[
"-dDEBUG",
"-gw3",
"-O-",
"-Xs-",
`-FE${directory}`,
`-FU${directory}`,
source,
],
{ timeout: 120_000 },
);
const binary = join(directory, "calls");
const cli = await artifactCli(
"observe-native-calls",
binary,
[JSON.stringify(input)],
{ timeoutMs: 180_000, signal: cancellation.signal, commands },
);
cancellation.signal.throwIfAborted();
inspect(cli);
await withArtifactMcp(
binary,
async (client) => {
const mcp = await artifactMcpResult(
client,
"observe_native_calls",
input,
{ signal: cancellation.signal },
);
cancellation.signal.throwIfAborted();
inspect(mcp);
assert.deepEqual(stable(mcp), stable(cli));
},
{ signal: cancellation.signal, commands },
);
cancellation.signal.throwIfAborted();
return {
ok: true,
mocked: false,
compiler,
version,
architecture,
debugger: cli.debugger,
cli: true,
stdio_mcp: true,
scope:
"bounded native call entries and compiler-provided source coordinates; no typed Pascal locals or interactive stepping",
};
}, [() => commands.close()]);
cancellation.signal.throwIfAborted();
console.log(JSON.stringify(result));
} catch (cause) {
const cancelled =
cause === cancellation.signal.reason ||
(cause?.reason === "cancelled" &&
cause.cancellation?.reason === cancellation.signal.reason &&
cause.cleanupFailure === null);
if (shutdownSignal === undefined || !cancelled) throw cause;
console.error(`Verification cancelled by ${shutdownSignal}.`);
process.exitCode = signalExitCodes[shutdownSignal];
} finally {
for (const signal of Object.keys(signalExitCodes))
process.off(signal, signalHandlers[signal]);
}
+489
View File
@@ -0,0 +1,489 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import {
copyFile,
mkdtemp,
mkdir,
readFile,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import {
requireMcpOperationResult,
requireMcpToolError,
} from "./lib/mcp-verifier-results.mjs";
import {
createVerifierRun,
completeVerifierRun,
assertVerifierCleanupComplete,
} from "./lib/verifier-run.mjs";
if (process.platform === "win32")
throw new Error(
"Owned Ghidra checkpoints currently support Linux/macOS only.",
);
const run = createVerifierRun(),
exec = promisify(execFile);
const root = await mkdtemp(join(tmpdir(), "rea-ghidra-checkpoints-"));
const runtime = join(root, "runtime"),
checkpointDirectory = join(root, "checkpoints");
const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url));
const sourcePath = join(root, "fixture.c"),
target = join(root, "checkpoint-fixture");
const env = {
...process.env,
HOME: root,
XDG_CONFIG_HOME: join(root, "config"),
XDG_CACHE_HOME: join(root, "cache"),
XDG_DATA_HOME: join(root, "data"),
TMPDIR: runtime,
JDK_JAVA_OPTIONS: `-Duser.home=${root}`,
REA_ANALYSIS_PROVIDER: "ghidra",
REA_GHIDRA_CHECKPOINT_DIR: checkpointDirectory,
};
for (const name of [
"REA_GHIDRA_PROJECT_PATH",
"REA_GHIDRA_PROGRAM_NAME",
"REA_GHIDRA_SEED_FILE",
"REA_GHIDRA_NATIVEAOT_JAR",
"REA_GHIDRA_LANGUAGE_ID",
"REA_GHIDRA_COMPILER_SPEC_ID",
"REA_GHIDRA_DISABLED_ANALYZERS",
])
delete env[name];
const report = {
verifier: "ghidra-owned-checkpoints",
phases: [],
target: null,
};
const sha = async (path) =>
createHash("sha256")
.update(await readFile(path))
.digest("hex");
async function phase(name, operation) {
const start = performance.now();
try {
const facts = await operation();
report.phases.push({
name,
status: "verified",
elapsed_ms: Math.round(performance.now() - start),
...facts,
});
} catch (cause) {
report.phases.push({
name,
status: "failed",
elapsed_ms: Math.round(performance.now() - start),
});
throw cause;
}
}
async function cli(procedure, flags = [], path = target) {
const { stdout } = await exec(
process.execPath,
[
entrypoint,
"decompile",
path,
procedure,
"--provider",
"ghidra",
"--json",
...flags,
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
return JSON.parse(stdout);
}
async function refuseCli(procedure, flags, expected) {
let refusal;
try {
await cli(procedure, flags);
} catch (cause) {
assert.equal(cause.code, 1, cause.stderr);
refusal = JSON.parse(cause.stdout);
}
assert.ok(refusal, "CLI unexpectedly accepted unavailable checkpoint state");
assert.equal(refusal.code, expected, JSON.stringify(refusal));
return refusal;
}
async function connect(environment = {}) {
const client = new Client({
name: "rea-ghidra-checkpoint-verifier",
version: "1",
});
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env: { ...env, ...environment },
stderr: "pipe",
});
transport.stderr?.on("data", () => {});
try {
await client.connect(transport);
} catch (cause) {
await client.close();
await transport.close();
throw cause;
}
const raw = (name, args = {}) =>
client.callTool({ name, arguments: args }, { timeout: 240000 });
return {
raw,
call: async (name, args = {}) =>
requireMcpOperationResult(await raw(name, args), name),
close: async () => {
await client.close();
await transport.close();
},
};
}
let manifestPath;
const manifest = async () => JSON.parse(await readFile(manifestPath, "utf8"));
try {
await mkdir(runtime);
await writeFile(
sourcePath,
`typedef struct CheckpointRecord { int value; int flag; } CheckpointRecord;
__attribute__((noinline)) int rea_checkpoint_leaf(CheckpointRecord *record) { return record->value * 7 + record->flag; }
int main(void) { CheckpointRecord record = { 3, 11 }; return rea_checkpoint_leaf(&record); }
`,
);
await exec("cc", [
"-O0",
"-g",
"-gdwarf-4",
"-fno-inline",
sourcePath,
"-o",
target,
]);
const targetDigest = await sha(target);
report.target = {
sha256: targetDigest,
source: "self-built C struct/function fixture; never executed",
host: process.platform,
architecture: process.arch,
};
let original, initialManifest;
const snapshotPath = join(root, "analysis-snapshot.json");
await phase(
"a cold MCP session keeps its live Program across initial save and repeated open",
async () => {
const cold = await connect({
REA_GHIDRA_CHECKPOINT_DIR: join(root, "cold-session-checkpoints"),
});
try {
await cold.call("open_binary", { path: target, provider_id: "ghidra" });
await cold.call("procedure_pseudo_code", {
procedure: "rea_checkpoint_leaf",
});
const id = (await cold.call("binary_session")).analysis_run.run_id;
await cold.call("annotate_native_function", {
procedure: "rea_checkpoint_leaf",
comment: "cold-session unsaved comment",
});
await cold.call("open_binary", { path: target, provider_id: "ghidra" });
assert.equal(
(await cold.call("binary_session")).analysis_run.run_id,
id,
);
const current = await cold.call("procedure_pseudo_code", {
procedure: "rea_checkpoint_leaf",
});
assert.match(JSON.stringify(current), /cold-session unsaved comment/);
await cold.call("close_binary");
return { same_live_run: true, unsaved_metadata_retained: true };
} finally {
await cold.close();
}
},
);
await phase("first analysis creates a checkpoint", async () => {
original = await cli("rea_checkpoint_leaf", ["--snapshot", snapshotPath]);
assert.match(JSON.stringify(original.normalized_result), /value/);
const keys = await readdir(checkpointDirectory);
assert.equal(keys.length, 1);
manifestPath = join(checkpointDirectory, keys[0], "current.json");
initialManifest = await manifest();
assert.equal(initialManifest.target_sha256, targetDigest);
assert.deepEqual(
original.analysis_profile.parameters.owned_checkpoint.state,
{ kind: "initial-analysis" },
);
return {
provider_version: original.provider.version,
response_bytes: Buffer.byteLength(JSON.stringify(original)),
};
});
await phase(
"the first checkpoint publication preserves initial immutable snapshot identity",
async () => {
const cached = await cli("rea_checkpoint_leaf", [
"--snapshot",
snapshotPath,
]);
assert.deepEqual(cached, original);
assert.deepEqual(await manifest(), initialManifest);
return {};
},
);
await phase(
"fresh CLI restores the Program and does not rewrite it",
async () => {
const restored = await cli("rea_checkpoint_leaf");
assert.deepEqual(restored.normalized_result, original.normalized_result);
assert.equal(
restored.analysis_profile.digest,
original.analysis_profile.digest,
);
assert.ok(
restored.limitations.some((value) =>
value.includes(initialManifest.source.sha256),
),
);
assert.deepEqual(await manifest(), initialManifest);
return {
checkpoint_sha256: initialManifest.source.sha256,
response_bytes: Buffer.byteLength(JSON.stringify(restored)),
};
},
);
await phase(
"another path with identical target bytes reuses the same Program",
async () => {
const alias = join(root, "renamed-target");
await copyFile(target, alias);
const restored = await cli("rea_checkpoint_leaf", [], alias);
assert.deepEqual(restored.normalized_result, original.normalized_result);
assert.equal(restored.caller_input_path, alias);
assert.deepEqual(await manifest(), initialManifest);
return {};
},
);
await phase(
"MCP preserves a live run, refuses a concurrent writer, and saves edits",
async () => {
const active = await connect();
try {
await active.call("open_binary", {
path: target,
provider_id: "ghidra",
});
await active.call("procedure_pseudo_code", {
procedure: "rea_checkpoint_leaf",
});
const id = (await active.call("binary_session")).analysis_run.run_id;
const other = await connect();
try {
await other.call("open_binary", {
path: target,
provider_id: "ghidra",
});
const error = requireMcpToolError(await other.raw("list_documents"));
assert.equal(error.code, "configuration_invalid");
assert.match(error.message, /another REA session/);
await other.call("close_binary");
} finally {
await other.close();
}
await active.call("open_binary", {
path: target,
provider_id: "ghidra",
});
assert.equal(
(await active.call("binary_session")).analysis_run.run_id,
id,
);
await active.call("annotate_native_function", {
procedure: "rea_checkpoint_leaf",
name: "rea_checkpoint_retained",
comment: "REA checkpoint first comment",
});
await active.call("close_binary");
assert.notDeepEqual(await manifest(), initialManifest);
return { same_live_run: true, concurrent_owner_refused: true };
} finally {
await active.close();
}
},
);
await phase("new MCP restores edits and remains editable", async () => {
const resumed = await connect();
try {
await resumed.call("open_binary", {
path: target,
provider_id: "ghidra",
});
const analysis = await resumed.call("analyze_function", {
procedure: "rea_checkpoint_retained",
detail: "complete",
});
assert.equal(analysis.procedure.name, "rea_checkpoint_retained");
assert.match(JSON.stringify(analysis), /REA checkpoint first comment/);
assert.match(analysis.pseudocode, /value/);
await resumed.call("annotate_native_function", {
procedure: "rea_checkpoint_retained",
comment: "REA checkpoint second comment",
});
await resumed.call("close_binary");
return { structure_field_preserved: true };
} finally {
await resumed.close();
}
});
await phase("CLI sees the second MCP edit", async () => {
const result = await cli("rea_checkpoint_retained");
assert.match(
JSON.stringify(result.normalized_result),
/REA checkpoint second comment/,
);
return {};
});
await phase(
"an older immutable REA snapshot refuses the changed checkpoint",
async () => {
const refusal = await refuseCli(
"rea_checkpoint_leaf",
["--snapshot", snapshotPath],
"evidence_integrity_mismatch",
);
assert.ok(refusal.details.binding_mismatch.profile_digest);
return {};
},
);
await phase(
"an edited live Program switches to identical bytes at another path",
async () => {
const active = await connect();
const alias = join(root, "live-alias");
await copyFile(target, alias);
try {
await active.call("open_binary", {
path: target,
provider_id: "ghidra",
});
await active.call("annotate_native_function", {
procedure: "rea_checkpoint_retained",
comment: "REA checkpoint second comment; saved during alias switch",
});
await active.call("open_binary", {
path: alias,
provider_id: "ghidra",
});
const result = await active.call("procedure_pseudo_code", {
procedure: "rea_checkpoint_retained",
});
assert.match(JSON.stringify(result), /saved during alias switch/);
await active.call("close_binary");
return { alias_switch_preserved_edits: true };
} finally {
await active.close();
}
},
);
const current = await manifest(),
bytes = await readFile(manifestPath);
await phase(
"target/profile mismatch is refused without replacing the saved state",
async () => {
try {
await writeFile(
manifestPath,
JSON.stringify({ ...current, target_sha256: "0".repeat(64) }),
);
await refuseCli("rea_checkpoint_retained", [], "configuration_invalid");
} finally {
await writeFile(manifestPath, bytes);
}
return {};
},
);
await phase(
"malformed manifest is refused without a fresh import",
async () => {
try {
await writeFile(manifestPath, "{");
await refuseCli("rea_checkpoint_retained", [], "configuration_invalid");
} finally {
await writeFile(manifestPath, bytes);
}
return {};
},
);
await phase(
"corrupt packed data is refused and explicit restoration recovers",
async () => {
const packed = join(
checkpointDirectory,
(await readdir(checkpointDirectory))[0],
current.source.file,
),
backup = join(root, "good.gzf");
await copyFile(packed, backup);
try {
await writeFile(packed, "corrupted packed data");
await refuseCli("rea_checkpoint_retained", [], "configuration_invalid");
} finally {
await copyFile(backup, packed);
}
const result = await cli("rea_checkpoint_retained");
assert.match(
JSON.stringify(result.normalized_result),
/REA checkpoint second comment/,
);
return {};
},
);
assert.equal(await sha(target), targetDigest);
assert.deepEqual(await readdir(runtime), []);
const checkpointFiles = await readdir(
join(checkpointDirectory, (await readdir(checkpointDirectory))[0]),
);
assert.equal(
checkpointFiles.length,
2,
"Only the current manifest and packed Program should remain",
);
report.status = "verified";
} catch (cause) {
report.status = "failed";
report.error = cause instanceof Error ? cause.stack : String(cause);
if (typeof cause?.stdout === "string")
report.failure_stdout = cause.stdout.slice(-12000);
if (typeof cause?.stderr === "string")
report.failure_stderr = cause.stderr.slice(-4000);
process.exitCode = 1;
} finally {
report.run = await completeVerifierRun(run);
let cleanupVerified = false;
try {
assertVerifierCleanupComplete(report.run.process_lineage);
cleanupVerified = true;
} catch (cause) {
report.cleanup_error = String(cause);
report.retained_workspace = root;
process.exitCode = 1;
}
if (cleanupVerified) {
try {
await rm(root, { recursive: true, force: true });
} catch (cause) {
report.cleanup_error = String(cause);
report.retained_workspace = root;
process.exitCode = 1;
}
}
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
}
+24 -13
View File
@@ -35,10 +35,10 @@ import { verifyGhidraAddressBoundaries } from "./lib/real-ghidra-address-boundar
// This lane intentionally has no game paths, binary seeds or compiler dependency.
if (process.argv.length !== 2)
throw new Error("Usage: node scripts/verify-real-ghidra-dos.mjs");
if (!["linux", "darwin"].includes(process.platform))
throw new Error(
"verify:ghidra:dos supports Linux/macOS hosts only; Windows P0 is PE x86-64 only.",
);
if (!["linux", "darwin", "win32"].includes(process.platform))
throw new Error("verify:ghidra:dos supports Linux, macOS and Windows hosts.");
// Windows P0 is read-only: annotation and naming edits must be refused.
const windows = process.platform === "win32";
if (process.env.GHIDRA_INSTALL_DIR === undefined)
throw new Error(
"verify:ghidra:dos prerequisite missing: GHIDRA_INSTALL_DIR (bring your own Ghidra).",
@@ -72,6 +72,8 @@ await writeFile(targetPath, fixture.bytes, { mode: 0o600 });
const sha256 = digest(fixture.bytes);
const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url));
const env = {
// Windows children need the system environment (SystemRoot, profile paths).
...(windows ? { ...process.env, TMP: runtime, TEMP: runtime } : {}),
PATH: process.env.PATH ?? "/usr/bin:/bin",
TMPDIR: runtime,
REA_LOG_LEVEL: "silent",
@@ -396,21 +398,25 @@ try {
client,
identities.entry.address,
cliEvidence,
{ mutable: !windows },
);
const entrySymbol = entryWindow.procedure.symbol;
assert.ok(entrySymbol != null, "The DOS entry needs observed symbol facts");
// Restoring the name is itself a user edit, so Ghidra records user provenance.
// Windows P0 refuses edits and never renames, so its entry stays unchanged.
assert.deepEqual(
await call("read_function_instructions", {
procedure: identities.entry.address,
}),
{
...entryWindow,
procedure: {
...entryWindow.procedure,
symbol: { ...entrySymbol, source: "user_defined" },
},
},
windows
? entryWindow
: {
...entryWindow,
procedure: {
...entryWindow.procedure,
symbol: { ...entrySymbol, source: "user_defined" },
},
},
"Restoring the entry name changed more than its symbol provenance",
);
assert.equal(
@@ -474,7 +480,11 @@ try {
"The complete body is Ghidra's observed AddressSet, not independently proven historical compiler ownership.",
"Language/compiler identity is bound by the production authenticated bridge handshake; profile loader is the declared import policy.",
"Native high-pcode flow is excluded from cross-runtime parity: LOAD/STORE address-space selector constants are process-specific in current Ghidra output. Both dossiers are still strictly parsed.",
"Windows P0 remains unsupported for DOS MZ.",
...(windows
? [
"Windows P0 is read-only: annotation and naming edits were checked as refused, not exercised.",
]
: []),
],
};
} catch (error) {
@@ -510,7 +520,8 @@ try {
}
assert.ok(report, "No DOS proof report was produced");
const verifierRun = await completeVerifierRun(run);
assertVerifierCleanupComplete(verifierRun.process_lineage);
// Windows provider jobs are verified by their native owners, as in the 6502 lane.
if (!windows) assertVerifierCleanupComplete(verifierRun.process_lineage);
process.stdout.write(
`${JSON.stringify({ verifier_run: verifierRun, ...report, cleanup: "complete" })}\n`,
);
@@ -0,0 +1,187 @@
#!/usr/bin/env node
// Exercise the public one-shot CLI on a caller-owned saved project. The report
// is intentionally synthetic; it is not a claim about Ghidra's analysis history.
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import {
mkdtemp,
mkdir,
readFile,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { createVerifierCommands } from "./lib/verify-commands.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
const installation = process.env.GHIDRA_INSTALL_DIR;
if (!installation || !process.env.JAVA_HOME)
throw new Error(
"Set GHIDRA_INSTALL_DIR and JAVA_HOME to caller-installed Ghidra and JDK 21.",
);
const commands = await createVerifierCommands();
let root;
let commandsClosed = false;
const targetHash = await withVerifierCleanup(async () => {
root = await mkdtemp(join(tmpdir(), "rea-function-bundle-"));
const target = join(root, "fixture");
const projectDir = join(root, "project");
const scriptDir = join(root, "scripts");
const source = join(root, "fixture.c");
const project = join(projectDir, "SourceControl.gpr");
const repo = resolve(process.cwd());
const hash = (data) => createHash("sha256").update(data).digest("hex");
const tree = async (directory, prefix = "") => {
const result = [];
for (const entry of (
await readdir(directory, { withFileTypes: true })
).sort((a, b) => a.name.localeCompare(b.name))) {
const path = join(directory, entry.name);
const relative = join(prefix, entry.name);
if (entry.isDirectory()) result.push(...(await tree(path, relative)));
else if (entry.isFile())
result.push([relative, hash(await readFile(path))]);
else throw new Error(`Unexpected project entry: ${relative}`);
}
return result;
};
const run = async (command, args, options = {}) =>
commands.exec(command, args, {
timeout: 120_000,
maxBuffer: 16 * 1024 * 1024,
...options,
});
await mkdir(projectDir);
await mkdir(scriptDir);
await writeFile(
source,
"__attribute__((noinline)) int leaf(int x) { return x + 1; }\nint main(void) { return leaf(2); }\n",
);
await writeFile(
join(scriptDir, "SetHistory.java"),
`import ghidra.app.script.GhidraScript;
import ghidra.program.model.listing.Program;
public class SetHistory extends GhidraScript {
public void run() throws Exception {
currentProgram.getOptions(Program.PROGRAM_INFO).setString("REA disabled analyzers", "{\\"format\\":\\"rea-ghidra-disabled-analyzers-v1\\",\\"analyzers\\":[{\\"name\\":\\"Synthetic historical control\\",\\"status\\":\\"disabled\\"}]}");
}
}
`,
);
await run(process.env.REA_CC ?? "cc", ["-O0", "-g", source, "-o", target]);
await run(join(installation, "support", "analyzeHeadless"), [
projectDir,
"SourceControl",
"-import",
target,
"-scriptPath",
scriptDir,
"-postScript",
"SetHistory.java",
]);
const before = await tree(projectDir);
const targetHash = hash(await readFile(target));
const env = {
...process.env,
REA_GHIDRA_PROJECT_PATH: project,
REA_GHIDRA_PROGRAM_NAME: "fixture",
};
const cli = join(repo, "scripts", "rea.mjs");
for (const detail of ["complete", "summary"]) {
const output = join(root, `${detail}.json`);
const { stdout } = await run(
process.execPath,
[
cli,
"function",
target,
"main",
"--provider",
"ghidra",
"--detail",
detail,
"--evidence_output",
output,
"--format",
"json",
"--full-output",
],
{ env },
);
assert.equal(JSON.parse(stdout).ok, true);
const bundle = JSON.parse(await readFile(output, "utf8"));
const parent = bundle.records.find(
(record) => record.operation === "analyze_function",
);
assert.ok(parent, "function observation missing from bundle");
assert.ok(
parent.evidence_links.length,
"fixture did not produce a linked historical source",
);
const records = new Map(
bundle.records.map((record) => [record.evidence_id, record]),
);
for (const record of bundle.records)
for (const link of record.evidence_links)
assert.ok(records.has(link), `bundle omits linked Evidence ${link}`);
assert.ok(
parent.evidence_links.some(
(id) => records.get(id)?.operation === "health",
),
"historical health source missing",
);
}
const existing = join(root, "existing.json");
await writeFile(existing, "caller-owned\n");
let failure;
try {
await run(
process.execPath,
[
cli,
"function",
target,
"main",
"--provider",
"ghidra",
"--detail",
"summary",
"--evidence_output",
existing,
"--format",
"json",
"--full-output",
],
{ env },
);
assert.fail("existing output unexpectedly overwritten");
} catch (error) {
if (error.code !== 1) throw error;
failure = JSON.parse(error.snapshot.stdout.text);
}
assert.equal(await readFile(existing, "utf8"), "caller-owned\n");
assert.equal(failure.ok, false);
assert.equal(failure.error.details.source_closure.complete, true);
assert.ok(
failure.error.details.source_closure.records.some(
(record) => record.operation === "health",
),
);
assert.equal(hash(await readFile(target)), targetHash);
assert.deepEqual(await tree(projectDir), before);
return targetHash;
}, [
async () => {
await commands.close();
commandsClosed = true;
},
async () => {
if (commandsClosed && root !== undefined)
await rm(root, { recursive: true, force: true });
},
]);
process.stdout.write(
`PASS Ghidra function bundles: complete, summary, no-overwrite; target and project unchanged (${targetHash})\n`,
);
+47
View File
@@ -38,6 +38,8 @@ import {
buildPspFixture,
assertPspLoadedImage,
assertPspProbe,
PSP_VFPU_DECODE_CASES,
assertPspVfpuProbe,
} from "../tests/conformance/ghidra/psp-fixture.mjs";
import {
requireMcpToolError,
@@ -541,6 +543,47 @@ try {
});
if (psp) assertPspProbe(move, branch);
else assertMipsProbe(move, branch, byteOrder, probe);
let vfpuCoverage;
if (psp) {
const vfpuAddress = BigInt(independent.symbols.rea_psp_vfpu_probe);
const vfpuInstructions = [];
for (const { index } of PSP_VFPU_DECODE_CASES) {
vfpuInstructions.push(
await call("inspect_native_instruction", {
address: `0x${(vfpuAddress + 4n * BigInt(index)).toString(16)}`,
}),
);
}
assertPspVfpuProbe(vfpuInstructions);
const vfpuDossier = functionDossierSchema.parse(
await call("analyze_function", {
procedure: independent.symbols.rea_psp_vfpu_probe,
detail: "complete",
}),
);
assert.equal(BigInt(vfpuDossier.procedure.address), vfpuAddress);
const flow = vfpuDossier.native_value_flow;
const hasHighPcode = flow?.available === true;
vfpuCoverage = {
decoded_instructions: vfpuInstructions.length,
prefix_sites: 4,
vadd_sites: 3,
high_pcode: hasHighPcode ? "observed" : "unavailable",
opaque_high_pcode_operations: hasHighPcode
? flow.operations.filter(({ opcode }) => opcode === "CALLOTHER")
.length
: null,
truncated: hasHighPcode ? flow.truncated : null,
unavailable_reason:
flow?.available === false
? flow.reason
: hasHighPcode
? null
: "Ghidra did not report a HighFunction value flow",
pseudocode_nonempty: vfpuDossier.pseudocode.trim().length > 0,
numeric_vector_semantics: "not_verified",
};
}
const global = await call("read_bytes", {
address: independent.symbols[`${prefix}_global`],
length: 4,
@@ -807,6 +850,9 @@ try {
"independent ELF/ABI and symbols",
"loaded Ghidra identity",
"fixed instruction bytes and immediate",
...(psp
? ["VFPU prefix/vadd opcode decoding (not arithmetic equivalence)"]
: []),
psp
? "Allegrex BITREV decoding and A-B-A target identity"
: "conditional branch destination",
@@ -816,6 +862,7 @@ try {
? ["soft-float o32 parameter/return storage and caller constants"]
: []),
],
...(vfpuCoverage === undefined ? {} : { vfpu_decode: vfpuCoverage }),
...(softFloat === undefined ? {} : { soft_float: softFloat }),
procedures: procedures.length,
carved: carved ?? {
+2 -2
View File
@@ -31,7 +31,7 @@ const { values } = parseArgs({
});
if (values.target === undefined || process.env.REA_IDA_MCP_CONFIG === undefined)
throw new Error(
"IDA verification requires --target and REA_IDA_MCP_CONFIG; see docs/ida-provider.md. No toolchain is installed by this lane.",
"IDA verification requires --target and REA_IDA_MCP_CONFIG; see docs/providers/ida-provider.md. No toolchain is installed by this lane.",
);
if (
(values["second-procedure"] === undefined) !==
@@ -79,7 +79,7 @@ if (
values["second-marker"] === undefined)
)
throw new Error(
"Native IDA verification requires --second-procedure and --second-marker to verify distinct-function switching; see docs/ida-provider.md.",
"Native IDA verification requires --second-procedure and --second-marker to verify distinct-function switching; see docs/providers/ida-provider.md.",
);
const environment = Object.fromEntries(
Object.entries({
+130 -19
View File
@@ -26,8 +26,17 @@ import { parseArgs, promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { inspectKunaInstallation } from "../dist/kuna/KunaInstallation.js";
import {
KUNA_RELEASE_VERIFICATION_EXPLANATION,
KUNA_UNVERIFIED_VERSION_REMEDIATION,
KUNA_VERIFIED_VERSION,
} from "../dist/kuna/KunaRelease.js";
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
import { AnalysisCleanupLease } from "../dist/artifacts/AnalysisCleanupLease.js";
import {
KUNA_XREF_MISSING_NAME_REMEDIATION,
missingKunaXrefNameMessage,
} from "../dist/kuna/KunaSelectorGuidance.js";
import {
requireMcpOperationResult,
requireMcpToolError,
@@ -233,13 +242,13 @@ const openServer = async (provider, kunaSettings = {}) => {
structured: result.structuredContent,
};
};
const callError = async (name, arguments_ = {}) => {
const result = await client.callTool(
{ name, arguments: arguments_ },
{ timeout: 3_600_000 },
const callError = async (name, arguments_ = {}) =>
requireMcpToolError(
await client.callTool(
{ name, arguments: arguments_ },
{ timeout: 3_600_000 },
),
);
return requireMcpToolError(result);
};
const close = async (priorFailure) => {
clearInterval(timer);
await sample();
@@ -277,6 +286,34 @@ const openServer = async (provider, kunaSettings = {}) => {
path: provider.path,
provider_id: provider.id,
});
if (provider.id === "kuna") {
const status = (await call("binary_session")).value;
const binding = status.analysis_provider_binding;
const verification = binding.release_verification;
assert.equal(verification.basis, "reported_version");
assert.equal(verification.verified_version, KUNA_VERIFIED_VERSION);
assert.equal(
verification.reported_version,
binding.analysis_profile.parameters.kuna_version,
);
assert.equal(
verification.status,
binding.analysis_profile.parameters.kuna_version ===
KUNA_VERIFIED_VERSION
? "verified"
: "unverified",
);
assert.equal(
verification.explanation,
KUNA_RELEASE_VERIFICATION_EXPLANATION,
);
if (verification.status === "unverified")
assert.equal(
verification.remediation,
KUNA_UNVERIFIED_VERSION_REMEDIATION,
);
else assert.equal(verification.remediation, null);
}
} catch (cause) {
// A rejected connection/target still owns its transport and workspace.
await close(cause);
@@ -409,6 +446,44 @@ const verifyKuna = async (
}),
});
const listed = await cold.call("list_procedures");
if (verifyNamedTrace) {
const missing = await cold.callError("xrefs", {
address: "rea_missing_xref_inventory_name",
});
assert.equal(missing.code, "invalid_request");
assert.equal(
missing.message,
missingKunaXrefNameMessage("rea_missing_xref_inventory_name"),
);
assert.equal(
missing.remediation.action,
KUNA_XREF_MISSING_NAME_REMEDIATION,
);
assert.ok(missing.message.includes("rea_missing_xref_inventory_name"));
assert.equal(missing.recovery, undefined);
const procedure = await cold.callError("procedure_address", {
procedure: "rea_missing_procedure_name",
});
assert.equal(procedure.code, "invalid_request");
assert.equal(procedure.recovery, undefined);
assert.match(procedure.remediation.action, /search_procedures/u);
assert.ok(
procedure.details.issues.some(
(issue) => issue.observed?.procedure === "rea_missing_procedure_name",
),
);
const missingAddress = await cold.callError("procedure_address", {
procedure: "0x00000001",
});
assert.equal(missingAddress.code, "invalid_request");
assert.equal(missingAddress.recovery, undefined);
assert.match(missingAddress.remediation.action, /list_procedures/u);
assert.ok(
missingAddress.details.issues.some(
(issue) => issue.observed?.procedure === "0x00000001",
),
);
}
for (const [name, address] of functions) {
assert.ok(
listed.value.some((entry) => entry.address === address),
@@ -595,15 +670,37 @@ const measureGhidra = async (path, functions) => {
return answers;
};
/** Symbol-bearing caller binaries with the requested functions, beside their stripped copies. */
const targetPairs = async (root, names) => {
const describeFailure = (cause) => ({
message: cause instanceof Error ? cause.message : String(cause),
...(cause instanceof AggregateError
? { causes: cause.errors.map(describeFailure) }
: {}),
});
/** Inspect symbol-bearing caller binaries, retaining both selected and unmatched inputs. */
const targetPairs = async (root, names, selection) => {
const pairs = [];
for (const flavor of (await readdir(root)).sort()) {
const directory = join(root, flavor);
if (!(await stat(directory)).isDirectory()) continue;
for (const file of (await readdir(directory)).sort()) {
if (file.endsWith(".stripped")) continue;
const symbols = await symbolAddresses(join(directory, file), names);
let symbols;
try {
symbols = await symbolAddresses(join(directory, file), names);
} catch (cause) {
selection.examined.push({
binary: `${flavor}/${file}`,
status: "inspection_failed",
error: describeFailure(cause),
});
throw cause;
}
selection.examined.push({
binary: `${flavor}/${file}`,
status: symbols.size > 0 ? "selected" : "no_matching_text_symbols",
matched_functions: [...symbols.keys()],
});
if (symbols.size > 0) pairs.push({ flavor, directory, file, symbols });
}
}
@@ -624,6 +721,18 @@ const verifyVariant = async ({ flavor, directory, symbols }, variant) => {
const results = [];
let timeoutRecovery;
const failures = [];
const callerTargetSelection =
values.targets === undefined
? undefined
: {
directory: values.targets,
functions_argument: values.functions ?? null,
requested_functions: (values.functions ?? "")
.split(",")
.filter(Boolean),
symbol_scan_complete: false,
examined: [],
};
let inspectionResourcesClean = true;
try {
const installation = await inspectKunaInstallation(
@@ -636,13 +745,18 @@ try {
);
kunaVersion = `kuna ${installation.value.version}`;
let callerPairs = [];
if (values.targets !== undefined) {
const names = (values.functions ?? "").split(",").filter(Boolean);
if (callerTargetSelection !== undefined) {
const names = callerTargetSelection.requested_functions;
assert.ok(names.length > 0, "--targets requires --functions");
callerPairs = await targetPairs(values.targets, names);
callerPairs = await targetPairs(
callerTargetSelection.directory,
names,
callerTargetSelection,
);
callerTargetSelection.symbol_scan_complete = true;
assert.ok(
callerPairs.length > 0,
`No caller-built targets contain the requested functions in ${values.targets}`,
`No caller-built targets contain the requested functions in ${callerTargetSelection.directory}`,
);
const found = new Set(
callerPairs.flatMap(({ symbols }) => [...symbols.keys()]),
@@ -730,18 +844,15 @@ if (cleanupComplete)
cleanupComplete = false;
failures.push(cause);
}
const describeFailure = (cause) => ({
message: cause instanceof Error ? cause.message : String(cause),
...(cause instanceof AggregateError
? { causes: cause.errors.map(describeFailure) }
: {}),
});
const report = {
lane: "kuna",
status: failures.length === 0 ? "passed" : "failed",
kuna_version: kunaVersion,
platform: `${process.platform}/${process.arch}`,
verifier_run: lifecycle,
...(callerTargetSelection === undefined
? {}
: { caller_target_selection: callerTargetSelection }),
results,
...(timeoutRecovery === undefined
? {}
+1 -1
View File
@@ -28,7 +28,7 @@ for (const key of [
])
if (!isAbsolute(environment[key] ?? ""))
throw new Error(
`verify:recorded:crash requires absolute ${key}; see docs/recorded-crashes.md. No dependencies are installed by this verifier.`,
`verify:recorded:crash requires absolute ${key}; see docs/guides/native/recorded-crashes.md. No dependencies are installed by this verifier.`,
);
if (process.platform !== "linux" || process.arch !== "x64")
throw new Error("verify:recorded:crash requires Linux x64.");
+1 -1
View File
@@ -24,7 +24,7 @@ const apk = process.env.REA_ANDROID_TEST_APK;
const jar = process.env.REA_JADX_MCP_JAR;
if (apk === undefined || jar === undefined)
throw new Error(
"verify:android requires REA_ANDROID_TEST_APK and REA_JADX_MCP_JAR. Run npm run fixtures:android and select the downloaded files; see docs/android-analysis.md.",
"verify:android requires REA_ANDROID_TEST_APK and REA_JADX_MCP_JAR. Run npm run fixtures:android and select the downloaded files; see docs/guides/android/android-analysis.md.",
);
await access(apk);
await access(jar);
+2 -2
View File
@@ -49,7 +49,7 @@ async function checkPrerequisites() {
const value = process.env[name];
if (value === undefined) {
throw new Error(
`verify:firmware requires ${name}; see docs/firmware-analysis.md`,
`verify:firmware requires ${name}; see docs/guides/firmware/firmware-analysis.md`,
);
}
await fs.access(value);
@@ -341,7 +341,7 @@ async function stageExt4() {
async function stageGhidra(mcpFiles) {
if (process.env.GHIDRA_INSTALL_DIR === undefined) {
throw new Error(
"Firmware Ghidra handoff requires GHIDRA_INSTALL_DIR and Java; see docs/ghidra.md",
"Firmware Ghidra handoff requires GHIDRA_INSTALL_DIR and Java; see docs/providers/ghidra-provider.md",
);
}
+1 -1
View File
@@ -20,7 +20,7 @@ const repository = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
const hap = process.env.REA_HARMONY_TEST_HAP;
if (hap === undefined)
throw new Error(
"verify:harmony requires REA_HARMONY_TEST_HAP. Run npm run fixtures:harmony and set it to the downloaded file; see docs/harmony-analysis.md.",
"verify:harmony requires REA_HARMONY_TEST_HAP. Run npm run fixtures:harmony and set it to the downloaded file; see docs/guides/harmony/harmony-analysis.md.",
);
await access(hap);
const hashFile = async (path) => {