mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 03:45:39 +00:00
fix(ghidra)!: distinguish unlisted instruction addresses (#7823)
* fix(ghidra)!: distinguish unlisted instruction addresses BREAKING CHANGE: Ghidra now returns not-in-listing for in-memory addresses without listed instructions or defined data. Consumers matching the former undecodable value must handle the new status. * fix(ghidra): invalidate snapshots with old instruction status * test(ghidra): update raw profile commitment goldens
This commit is contained in:
1 parent
b0880d660f
commit
bd687bbc40
8 files changed
+83
-16
No files matched your search
@@ -1313,7 +1313,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
flow.addProperty("computed", false);
|
||||
if (instruction == null) {
|
||||
result.addProperty("status", !currentProgram.getMemory().contains(address) ? "outside-memory" : currentProgram.getListing().getInstructionContaining(address) != null
|
||||
? "not-instruction-boundary" : currentProgram.getListing().getDefinedDataContaining(address) != null ? "data" : "undecodable");
|
||||
? "not-instruction-boundary" : currentProgram.getListing().getDefinedDataContaining(address) != null ? "data" : "not-in-listing");
|
||||
for (String key : List.of("bytes", "length", "mnemonic", "raw_disassembly")) result.add(key, JsonNull.INSTANCE);
|
||||
} else {
|
||||
result.addProperty("status", "decoded");
|
||||
@@ -1385,6 +1385,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
result.add("incoming_references", incomingReferences);
|
||||
}
|
||||
List<String> limitations = new ArrayList<>(List.of("Operand components are Ghidra Listing decoder tokens; effective memory base/index/displacement roles and per-instruction context mode are unavailable. The mode field is the program language variant."));
|
||||
if (result.get("status").getAsString().equals("not-in-listing")) limitations.add("No listed instruction or defined data covers this in-memory address. REA did not attempt a decode; the memory may be uninitialized or contain valid instructions.");
|
||||
if (flow.get("kind").getAsString().equals("terminal")) limitations.add("No RETURN operation was observed in the provider's instruction p-code; terminal classification remains generic.");
|
||||
result.add("limitations", GSON.toJsonTree(limitations));
|
||||
return result;
|
||||
|
||||
@@ -127,7 +127,11 @@ rea inspect-native-data-type <binary> --address <typed-data-address> --provider
|
||||
|
||||
Instruction facts include bytes, length, decoder text, ordered register/scalar/
|
||||
address tokens, typed references and flow destinations. Mid-instruction, data,
|
||||
outside-memory and undecodable addresses have separate outcomes. Effective
|
||||
outside-memory and unlisted addresses have separate outcomes. For Ghidra,
|
||||
`not-in-listing` means the address is in memory without a listed instruction or
|
||||
defined data. REA does not attempt a decode there: the memory may be uninitialized
|
||||
or contain valid instructions. Older Evidence may report `undecodable` for this
|
||||
same ambiguous case; it does not establish that a decode failed. Effective
|
||||
memory base/index/displacement roles remain unavailable. Ghidra reports its program
|
||||
language variant in `mode`; other providers retain their own mode label. Ghidra
|
||||
MIPS results additionally expose `decoder_context`
|
||||
@@ -138,7 +142,7 @@ that do not expose this facet. The context is an analysis observation, not proof
|
||||
of a runtime processor state.
|
||||
|
||||
`inspect_native_instruction` also returns `incoming_references` to the exact
|
||||
queried address, including data, interior-instruction and undecodable addresses.
|
||||
queried address, including data, interior-instruction and unlisted addresses.
|
||||
Ghidra enumerates its ReferenceManager, excludes synthetic entry-point
|
||||
references, and returns deterministic source-address ordering while retaining
|
||||
distinct operand references from the same source. `[]` means that enumeration
|
||||
|
||||
@@ -13,8 +13,10 @@ import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { functionDossierSchema } from "../dist/domain/native/functionDossier.js";
|
||||
import { nativeInstructionSchema } from "../dist/domain/native/nativeInstruction.js";
|
||||
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
|
||||
import { buildDosComFixture } from "../tests/conformance/ghidra/dos-com-fixture.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
@@ -158,6 +160,54 @@ try {
|
||||
instruction.bytes.replaceAll(" ", "").toLowerCase(),
|
||||
fixture.entry_hex,
|
||||
);
|
||||
// This NOP follows the entry's terminating INT 20h and is not reached by
|
||||
// either function. It remains valid instruction bytes outside the listing.
|
||||
const unlistedAddress = "0x1013f";
|
||||
const unlistedBytes = await call("read_bytes", {
|
||||
address: unlistedAddress,
|
||||
length: 1,
|
||||
});
|
||||
assert.equal(unlistedBytes.bytes_hex, "90");
|
||||
const unlistedResponse = await client.callTool({
|
||||
name: "inspect_native_instruction",
|
||||
arguments: { address: unlistedAddress },
|
||||
});
|
||||
const unlistedEvidence = parseEvidence(
|
||||
requireMcpOutput(unlistedResponse, "inspect_native_instruction"),
|
||||
);
|
||||
const unlisted = unlistedEvidence.normalized_result;
|
||||
assert.equal(unlisted.status, "not-in-listing");
|
||||
assert.equal(unlisted.bytes, null);
|
||||
assert.ok(
|
||||
unlisted.limitations.some((item) =>
|
||||
item.includes("did not attempt a decode"),
|
||||
),
|
||||
);
|
||||
const advertised = (await client.listTools()).tools.find(
|
||||
(tool) => tool.name === "inspect_native_instruction",
|
||||
);
|
||||
assert.ok(advertised?.outputSchema);
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
assert.equal(ajv.validateSchema(advertised.outputSchema), true);
|
||||
assert.equal(
|
||||
ajv.validate(advertised.outputSchema, unlistedResponse.structuredContent),
|
||||
true,
|
||||
JSON.stringify(ajv.errors),
|
||||
);
|
||||
assert.ok(
|
||||
nativeInstructionSchema.safeParse({ ...unlisted, status: "undecodable" })
|
||||
.success,
|
||||
);
|
||||
assert.equal(
|
||||
(await call("resolve_native_call_targets", { address: unlistedAddress }))
|
||||
.status,
|
||||
"unavailable",
|
||||
);
|
||||
assert.deepEqual(
|
||||
(await cli("inspect-native-instruction", unlistedAddress))
|
||||
.normalized_result,
|
||||
unlisted,
|
||||
);
|
||||
const cliImage = await cli("inspect-native-load-image");
|
||||
const stable = (x) => ({
|
||||
...x,
|
||||
|
||||
@@ -66,7 +66,7 @@ export const OFFICIAL_TOOL_CONTRACTS = [
|
||||
),
|
||||
official(
|
||||
"inspect_native_instruction",
|
||||
"Inspect one exact code or data address: decoded bytes, mnemonic, ordered operand tokens, flow, outgoing instruction references and typed incoming references where available. Incoming references preserve source, access kind, operand index and analysis provenance even at data addresses; they do not prove runtime access. Memory addressing decomposition is unavailable when the provider supplies only tokens. Data, interior instruction addresses and undecodable bytes are distinct outcomes.",
|
||||
"Inspect one exact code or data address: decoded bytes, mnemonic, ordered operand tokens, flow, outgoing instruction references and typed incoming references where available. Incoming references preserve source, access kind, operand index and analysis provenance even at data addresses; they do not prove runtime access. Memory addressing decomposition is unavailable when the provider supplies only tokens. Ghidra reports in-memory addresses without a listed instruction or defined data as not-in-listing, without attempting a decode.",
|
||||
nativeInstructionInputSchema,
|
||||
),
|
||||
official(
|
||||
|
||||
@@ -44,13 +44,18 @@ const reference = z.strictObject({
|
||||
/** Provider-decoded facts for one instruction; operand tokens preserve their native ordering. */
|
||||
export const nativeInstructionSchema = z.strictObject({
|
||||
address: z.string(),
|
||||
status: z.enum([
|
||||
"decoded",
|
||||
"not-instruction-boundary",
|
||||
"data",
|
||||
"undecodable",
|
||||
"outside-memory",
|
||||
]),
|
||||
status: z
|
||||
.enum([
|
||||
"decoded",
|
||||
"not-instruction-boundary",
|
||||
"data",
|
||||
"not-in-listing",
|
||||
"undecodable",
|
||||
"outside-memory",
|
||||
])
|
||||
.describe(
|
||||
"decoded is an instruction at the address; not-instruction-boundary is inside a listed instruction; data is defined data; outside-memory is outside the program's memory. Ghidra's not-in-listing means the address is in memory with no listed instruction or defined data; no decode was attempted, and memory may be uninitialized. The legacy undecodable value in older Evidence has the same ambiguous meaning and does not prove decoding failed.",
|
||||
),
|
||||
procedure: z.string().nullable(),
|
||||
architecture: z.string(),
|
||||
mode: z
|
||||
|
||||
@@ -65,6 +65,7 @@ const sharedAnalysisSemantics = {
|
||||
procedure_file_offset_resolution:
|
||||
"address-first-unique-target-source-mapping-v2",
|
||||
instruction_flow_evidence: "decoded-return-pcode-v1",
|
||||
instruction_status: "unlisted-address-without-decode-v1",
|
||||
instruction_references: "exact-address-incoming-without-synthetic-entry-v2",
|
||||
function_boundary_observations: "ghidra-function-boundary-limitations-v4",
|
||||
} as const;
|
||||
|
||||
@@ -6,12 +6,12 @@ import { silentLogger } from "../logger.js";
|
||||
import { ghidraProfileInstallationHost as host } from "./GhidraInstallation.fixture.js";
|
||||
import { GhidraProvider } from "./GhidraProvider.js";
|
||||
|
||||
// Digests were computed by the unmodified upstream resolver (3ae7daa10,
|
||||
// raw branch inline) and must not change when the binary-loader policy is shared.
|
||||
// Digests commit to the current shared policy, including the instruction status
|
||||
// revision that prevents reuse of snapshots with the previous ambiguous result.
|
||||
const RAW_DIGESTS = {
|
||||
"6502": "adbec31711f31471f9cdbdbb2fc743a0ce6b46c9be5568bf90a3f51ef46e376d",
|
||||
"6502": "8e33776d1cc5c5d77d32923d5978c05ec2d989d00920dd458cc2dafb4c9c0cad",
|
||||
"x86-disabled-analyzer":
|
||||
"493a02888eca575be286b5a96041af8007444586854edb974d6a0c12fd2768c4",
|
||||
"d17bddc8ac110c891d30fb989fc7906df421bd9bea5df4aac05a950f50c7bdaf",
|
||||
} as const;
|
||||
|
||||
const configFor = (environment: NodeJS.ProcessEnv) => {
|
||||
@@ -33,7 +33,7 @@ const raw = (architecture: "6502" | "x86", entryPoint: string): BinaryTarget =>
|
||||
}) as unknown as BinaryTarget;
|
||||
|
||||
describe("raw-binary analysis profile digest", () => {
|
||||
it("keeps the 6502 raw profile digest from before the shared policy refactor", async () => {
|
||||
it("commits the 6502 raw profile to the current shared policy", async () => {
|
||||
const ghidra = new GhidraProvider(configFor({}), silentLogger, {}, host);
|
||||
const resolved = await ghidra.resolveAnalysisProfile(raw("6502", "0x8001"));
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
@@ -44,6 +44,7 @@ describe("raw-binary analysis profile digest", () => {
|
||||
language_id: "6502:LE:16:default",
|
||||
language_selection: "caller-supplied-v1",
|
||||
load_image_evidence: "independent-raw-mapping-context-v4",
|
||||
instruction_status: "unlisted-address-without-decode-v1",
|
||||
analyzer_preset: "ghidra-default",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -307,6 +307,11 @@ describe("Ghidra relationship coverage replay", () => {
|
||||
|
||||
describe("Ghidra snapshot profile admission", () => {
|
||||
it.each([
|
||||
{
|
||||
parameter: "instruction_status",
|
||||
current: "unlisted-address-without-decode-v1",
|
||||
legacy: undefined,
|
||||
},
|
||||
{
|
||||
parameter: "function_boundary_observations",
|
||||
current: "ghidra-function-boundary-limitations-v4",
|
||||
|
||||
Reference in new issue
Block a user