mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(ghidra): bind existing MIPS projects to target metadata (#8605)
Co-authored-by: Cursor Agent <[email protected]>
This commit is contained in:
1 parent
4afb6b79ae
commit
982e750a70
6 files changed
+159
-4
No files matched your search
@@ -1473,6 +1473,9 @@ reopen the target to resolve a new profile after intentionally changing it.
|
||||
The profile records the caller path, resolved project/name, program and content
|
||||
fingerprint. Original analysis settings and completeness remain **unknown**;
|
||||
prior annotations or edits are caller-project state, not fresh REA discoveries.
|
||||
For supported header-selected MIPS ELF targets, the profile also binds the
|
||||
selected target's ELF metadata; it does not claim a fresh-import MIPS lane was
|
||||
used for the saved project.
|
||||
A matching recorded import digest does not claim that the database is unedited.
|
||||
Saved REA seed/analyzer reports remain historical facts in retained source Evidence;
|
||||
they are not interpreted as overrides applied by this session.
|
||||
|
||||
@@ -6,12 +6,13 @@ import {
|
||||
mkdtemp,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
realpath,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { basename, join, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
@@ -41,8 +42,10 @@ import {
|
||||
import {
|
||||
requireMcpToolError,
|
||||
requireMcpOperationResult,
|
||||
requireMcpOutput,
|
||||
} from "./lib/mcp-verifier-results.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { runOwnedCommand } from "../dist/process/OwnedCommand.js";
|
||||
|
||||
// Optional cross-target lane. No tool acquisition and no target execution.
|
||||
const mode = process.argv[2];
|
||||
@@ -90,6 +93,114 @@ for (const setting of ["GHIDRA_HEADLESS_MAXMEM", "GHIDRA_MAXMEM"]) {
|
||||
if (process.env[setting] !== undefined) env[setting] = process.env[setting];
|
||||
}
|
||||
|
||||
/** Observe an existing project, and verify REA never alters the caller's copy. */
|
||||
async function verifyExistingMipsProject(fixture, byteOrder) {
|
||||
const projectDir = join(workspace, `${byteOrder}-existing-project`);
|
||||
await mkdir(projectDir);
|
||||
assert.ok(installation.analyzeHeadlessPath);
|
||||
await runOwnedCommand(
|
||||
{
|
||||
command: installation.analyzeHeadlessPath,
|
||||
arguments: [
|
||||
projectDir,
|
||||
"ExistingMips",
|
||||
"-import",
|
||||
fixture.path,
|
||||
"-noanalysis",
|
||||
],
|
||||
env,
|
||||
runId: run.run_id,
|
||||
expectedCommand: null,
|
||||
},
|
||||
{ timeoutMs: 180000, diagnosticBytes: 1024 * 1024 },
|
||||
);
|
||||
const projectPath = join(projectDir, "ExistingMips.gpr");
|
||||
const digestTree = async () => {
|
||||
const paths = [projectPath];
|
||||
const walk = async (dir) => {
|
||||
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) await walk(path);
|
||||
else if (entry.isFile()) paths.push(path);
|
||||
}
|
||||
};
|
||||
await walk(join(projectDir, "ExistingMips.rep"));
|
||||
return Promise.all(
|
||||
paths.sort().map(async (path) => [
|
||||
relative(projectDir, path),
|
||||
createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex"),
|
||||
]),
|
||||
);
|
||||
};
|
||||
const before = await digestTree();
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env: {
|
||||
...env,
|
||||
REA_GHIDRA_PROJECT_PATH: projectPath,
|
||||
REA_GHIDRA_PROGRAM_NAME: basename(fixture.path),
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "rea-existing-mips-proof", version: "1" });
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const opened = await client.callTool(
|
||||
{
|
||||
name: "open_binary",
|
||||
arguments: { path: fixture.path, provider_id: "ghidra" },
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
requireMcpOperationResult(opened, "open_binary");
|
||||
const result = await client.callTool(
|
||||
{ name: "list_procedures", arguments: {} },
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
const observation = parseEvidence(
|
||||
requireMcpOutput(result, "list_procedures"),
|
||||
);
|
||||
assert.ok(Array.isArray(observation.normalized_result));
|
||||
const parameters = observation.analysis_profile?.parameters;
|
||||
assert.equal(parameters?.import_mode, "existing-project-private-copy-v1");
|
||||
assert.equal(parameters?.mips_elf?.byte_order, byteOrder);
|
||||
assert.equal(parameters?.mips_support_lane, undefined);
|
||||
assert.deepEqual(parameters?.original_analysis, {
|
||||
completeness: "unknown",
|
||||
settings: "unknown",
|
||||
});
|
||||
requireMcpOperationResult(
|
||||
await client.callTool({ name: "close_binary", arguments: {} }),
|
||||
"close_binary",
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
assert.deepEqual(
|
||||
await digestTree(),
|
||||
before,
|
||||
"REA changed the source project",
|
||||
);
|
||||
assert.equal(
|
||||
createHash("sha256")
|
||||
.update(await readFile(fixture.path))
|
||||
.digest("hex"),
|
||||
fixture.sha256,
|
||||
);
|
||||
return {
|
||||
byte_order: byteOrder,
|
||||
project_unchanged: true,
|
||||
target_unchanged: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed float signatures must use soft-float o32 storage, and callers must see
|
||||
* the source constants through it rather than FPU inputs.
|
||||
@@ -320,6 +431,10 @@ try {
|
||||
);
|
||||
}
|
||||
assert.equal(resolved.value.sha256, fixture.sha256);
|
||||
const existingProject =
|
||||
!psp && floatAbi === "hard"
|
||||
? await verifyExistingMipsProject(fixture, byteOrder)
|
||||
: undefined;
|
||||
const snapshotPath = join(
|
||||
workspace,
|
||||
`${byteOrder}-${floatAbi}.snapshot.json`,
|
||||
@@ -682,6 +797,9 @@ try {
|
||||
}
|
||||
reports.push({
|
||||
...fixture,
|
||||
...(existingProject === undefined
|
||||
? {}
|
||||
: { existing_project: existingProject }),
|
||||
independent_reader: independent,
|
||||
effective_language: image.observations.language_id,
|
||||
effective_compiler_spec: image.observations.compiler_spec_id,
|
||||
|
||||
@@ -475,11 +475,13 @@ export const existingGhidraAnalysisProfile = (
|
||||
target: BinaryTarget,
|
||||
provider: Parameters<typeof createAnalysisProfile>[0],
|
||||
project: JsonValue,
|
||||
) =>
|
||||
createAnalysisProfile(provider, {
|
||||
) => {
|
||||
const mipsElf = ghidraMipsProfileParameters(target).mips_elf;
|
||||
return createAnalysisProfile(provider, {
|
||||
target_kind: target.kind,
|
||||
target_format: target.format,
|
||||
architecture: target.architecture ?? null,
|
||||
...(mipsElf === undefined ? {} : { mips_elf: mipsElf }),
|
||||
import_mode: "existing-project-private-copy-v1",
|
||||
...sharedAnalysisSemantics,
|
||||
annotation_policy: "read-only-existing-project-v1",
|
||||
@@ -488,3 +490,4 @@ export const existingGhidraAnalysisProfile = (
|
||||
no_return_repair: "disabled-existing-project",
|
||||
process_launch: "inspected-jvm-launch-support-v1",
|
||||
});
|
||||
};
|
||||
@@ -13,11 +13,15 @@ import { HopperProvider } from "../hopper/HopperProvider.js";
|
||||
import { hopperLoaderArgsForTarget } from "../hopper/HopperAnalysisProfile.js";
|
||||
import { IdaProvider } from "../ida/IdaProvider.js";
|
||||
import { GhidraProvider } from "./GhidraProvider.js";
|
||||
import { ghidraMipsProfileParameters } from "./GhidraMipsProfile.js";
|
||||
import {
|
||||
ghidraMipsProfileMismatchReason,
|
||||
ghidraMipsProfileParameters,
|
||||
} from "./GhidraMipsProfile.js";
|
||||
import { ghidraProfileInstallationHost as host } from "./GhidraInstallation.fixture.js";
|
||||
import { GHIDRA_SESSION_CAPABILITIES } from "./GhidraSessionValues.js";
|
||||
import type { GhidraMipsSoftFloatReport } from "./GhidraMipsProfile.js";
|
||||
import {
|
||||
existingGhidraAnalysisProfile,
|
||||
ghidraPinnedLanguage,
|
||||
ghidraProfileLanguageOverride,
|
||||
ghidraProfilePinnedLanguage,
|
||||
@@ -96,6 +100,28 @@ const abi = (
|
||||
const softFloat = abi({ cpr1Size: 0, fpAbi: 3 });
|
||||
|
||||
describe("bounded Ghidra MIPS admission", () => {
|
||||
it("binds a saved project's MIPS metadata without claiming a fresh-import lane", () => {
|
||||
const little = target();
|
||||
const big = target({ byteOrder: "big" });
|
||||
const profile = existingGhidraAnalysisProfile(
|
||||
little,
|
||||
{ id: "ghidra", name: "Ghidra", version: "12.1.4" },
|
||||
{ project_name: "fixture" },
|
||||
);
|
||||
expect(profile.parameters).not.toHaveProperty("mips_support_lane");
|
||||
expect(
|
||||
ghidraMipsProfileMismatchReason(little, profile.parameters, true),
|
||||
).toBeNull();
|
||||
expect(
|
||||
ghidraMipsProfileMismatchReason(big, profile.parameters, true),
|
||||
).toMatch(/metadata does not match/u);
|
||||
expect(
|
||||
ghidraMipsProfileMismatchReason(little, profile.parameters, false),
|
||||
).toMatch(/different target/u);
|
||||
expect(ghidraMipsProfileMismatchReason(little, {}, true)).toMatch(
|
||||
/metadata does not match/u,
|
||||
);
|
||||
});
|
||||
it("binds endian and flags without widening Windows P0", async () => {
|
||||
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
|
||||
const little = target();
|
||||
|
||||
@@ -435,6 +435,7 @@ export const ghidraMipsProfileParameters = (
|
||||
export const ghidraMipsProfileMismatchReason = (
|
||||
target: BinaryTarget,
|
||||
parameters: Readonly<Record<string, unknown>>,
|
||||
existingProject: boolean,
|
||||
): string | null => {
|
||||
if (target.architecture !== "mips" || target.mips === undefined) return null;
|
||||
const expected = ghidraMipsProfileParameters(target);
|
||||
@@ -447,6 +448,7 @@ export const ghidraMipsProfileMismatchReason = (
|
||||
const expectedBasis =
|
||||
committedLane === pspLane ? undefined : expected.mips_abi_basis;
|
||||
if (
|
||||
!existingProject &&
|
||||
expectedLane !== undefined &&
|
||||
(committedLane !== expectedLane ||
|
||||
parameters.mips_abi_basis !== expectedBasis)
|
||||
|
||||
@@ -439,6 +439,9 @@ export const createGhidraProviderClient = (input: {
|
||||
const mipsProfileMismatch = ghidraMipsProfileMismatchReason(
|
||||
target,
|
||||
committedProfile.parameters,
|
||||
project !== undefined &&
|
||||
committedProfile.parameters.import_mode ===
|
||||
"existing-project-private-copy-v1",
|
||||
);
|
||||
if (mipsProfileMismatch !== null)
|
||||
return err(
|
||||
|
||||
Reference in new issue
Block a user