mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42bfd8bf79 | ||
|
|
9926cbb740 | ||
|
|
4db1433264 | ||
|
|
1d00dbd6f2 | ||
|
|
91d9e6ebe3 | ||
|
|
45233309aa | ||
|
|
c02f7d1330 | ||
|
|
f8e56e2ab6 | ||
|
|
a240e19c2e | ||
|
|
db4b13cc53 | ||
|
|
98120f3158 |
No files matched your search
@@ -1414,8 +1414,29 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
flow.addProperty("conditional", false);
|
||||
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" : "not-in-listing");
|
||||
Instruction containing = currentProgram.getListing().getInstructionContaining(address);
|
||||
boolean inMemory = currentProgram.getMemory().contains(address);
|
||||
Data containingData = inMemory && containing == null
|
||||
? currentProgram.getListing().getDefinedDataContaining(address) : null;
|
||||
result.addProperty("status", !inMemory ? "outside-memory" : containing != null
|
||||
? "not-instruction-boundary" : containingData != null ? "data" : "not-in-listing");
|
||||
if (inMemory && containing == null && containingData != null) {
|
||||
JsonObject parentData = new JsonObject();
|
||||
parentData.addProperty("address", canonicalAddress(containingData.getAddress()));
|
||||
parentData.addProperty("length", containingData.getLength());
|
||||
parentData.addProperty("type_id", containingData.getDataType().getPathName());
|
||||
parentData.addProperty("provenance", "ghidra-listing");
|
||||
result.add("containing_data", parentData);
|
||||
}
|
||||
if (containing != null && currentProgram.getMemory().contains(address)) {
|
||||
JsonObject containingInstruction = new JsonObject();
|
||||
containingInstruction.addProperty("address", canonicalAddress(containing.getAddress()));
|
||||
containingInstruction.addProperty("length", containing.getLength());
|
||||
containingInstruction.addProperty("mnemonic", containing.getMnemonicString());
|
||||
containingInstruction.addProperty("raw_disassembly", containing.toString());
|
||||
containingInstruction.addProperty("provenance", "ghidra-listing");
|
||||
result.add("containing_instruction", containingInstruction);
|
||||
}
|
||||
for (String key : List.of("bytes", "length", "mnemonic", "raw_disassembly")) result.add(key, JsonNull.INSTANCE);
|
||||
} else {
|
||||
result.addProperty("status", "decoded");
|
||||
|
||||
@@ -267,6 +267,18 @@ same function can be retried after raising the limit. See the
|
||||
[settings design](design/limits-and-recovery.md) for snapshots, precedence,
|
||||
and provider integration.
|
||||
|
||||
When `inspect_native_instruction` selects a byte inside a listed instruction,
|
||||
`containing_instruction` reports its separate start, length and Ghidra Listing
|
||||
text, retaining any non-default address space. The requested address remains
|
||||
unchanged, and a containing function may be absent. For defined data, the same
|
||||
tool reports its containing object's start, length and type path; use the start
|
||||
with `inspect_native_data_type` to inspect its layout. This does not select a
|
||||
field or union member. These are listing observations, not proof of execution or
|
||||
an alternate decode. Earlier Evidence can
|
||||
omit the field. The analysis-profile revision prevents older snapshots from
|
||||
replaying results without it; open the target without the old snapshot and save
|
||||
a fresh snapshot when needed.
|
||||
|
||||
Function extents and instruction completeness refer to Ghidra's analyzed body.
|
||||
An inferred no-return call can terminate that body before subsequent bytes.
|
||||
Pseudocode, assembly, procedure info, instruction windows, incoming and outgoing
|
||||
|
||||
@@ -312,7 +312,7 @@ export const registerCoreNativeCommands = (
|
||||
cli.command(command, {
|
||||
description:
|
||||
operation === "inspect_native_instruction"
|
||||
? "Inspect one native instruction with decoded operand facts"
|
||||
? "Inspect a code or data address, its containing Ghidra listing object, and typed incoming references where supported"
|
||||
: "Resolve one static native call site",
|
||||
args: z.object({
|
||||
path: localPathStringSchema.describe(
|
||||
|
||||
@@ -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. Page retained references with inspect_analysis_view kind native-instruction-references; null coverage means the selected collection was unavailable or not recorded. 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.",
|
||||
"Inspect one exact code or data address: decoded bytes, mnemonic, ordered operand tokens, flow, outgoing instruction references and typed incoming references where available. For a byte inside a listed instruction or defined data, Ghidra reports the containing instruction or data object separately, preserving the query and address space. Inspect the starting address of a data object with inspect_native_data_type for its layout; a containing object does not select a field or union member. Incoming references preserve source, access kind, operand index and analysis provenance even at data addresses; they do not prove runtime access. Page retained references with inspect_analysis_view kind native-instruction-references; null coverage means the selected collection was unavailable or not recorded. 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(
|
||||
|
||||
@@ -57,6 +57,34 @@ export const nativeInstructionSchema = z.strictObject({
|
||||
"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(),
|
||||
containing_instruction: z
|
||||
.strictObject({
|
||||
address: z.string().min(1),
|
||||
length: z.int().positive(),
|
||||
mnemonic: z.string().min(1),
|
||||
raw_disassembly: z.string().min(1),
|
||||
provenance: z.string().min(1),
|
||||
})
|
||||
.optional()
|
||||
.describe(
|
||||
"Provider-observed listed instruction containing the exact requested interior address. Its address and length describe the containing instruction; the query address remains unchanged. Omission means not recorded or not applicable, including older Evidence. This does not imply a function, alternate decode, reachability, or runtime execution.",
|
||||
),
|
||||
containing_data: z
|
||||
.strictObject({
|
||||
address: z.string().min(1),
|
||||
length: z
|
||||
.int()
|
||||
.positive()
|
||||
.describe(
|
||||
"Observed object length in bytes; a dynamic type may not have a fixed size.",
|
||||
),
|
||||
type_id: z.string().min(1),
|
||||
provenance: z.string().min(1),
|
||||
})
|
||||
.optional()
|
||||
.describe(
|
||||
"Provider-observed containing defined data object, including its start and type identity; the selected address remains unchanged. Inspect its start with inspect_native_data_type for its layout. This does not choose a field or union member or prove runtime use. Omission means not recorded or not applicable, including older Evidence.",
|
||||
),
|
||||
architecture: z.string(),
|
||||
mode: z
|
||||
.string()
|
||||
|
||||
@@ -68,7 +68,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_status: "unlisted-and-containing-code-data-v3",
|
||||
instruction_references: "exact-address-incoming-without-synthetic-entry-v3",
|
||||
function_boundary_observations: "ghidra-function-boundary-limitations-v4",
|
||||
} as const;
|
||||
|
||||
@@ -522,9 +522,15 @@ describe("Ghidra compact reference provenance", () => {
|
||||
it("distinguishes incoming references, examined emptiness and unavailable coverage at data addresses", () => {
|
||||
const instruction = {
|
||||
...compactInstruction(),
|
||||
address: "ram:00402000",
|
||||
address: "ram:0x402000",
|
||||
status: "data",
|
||||
procedure: null,
|
||||
containing_data: {
|
||||
address: "ram:0x402000",
|
||||
length: 4,
|
||||
type_id: "/dword",
|
||||
provenance: "ghidra-listing",
|
||||
},
|
||||
};
|
||||
const reference = {
|
||||
...compactReference(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionOperations.js";
|
||||
import { nativeAddressObservationSchema } from "../domain/native/nativeAddress.js";
|
||||
import { addressDistance } from "../domain/native/addressCoordinate.js";
|
||||
import { procedureInfoSchema } from "../domain/native/procedureInfo.js";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
@@ -288,6 +289,51 @@ const ghidraFunctionInstructionWindow = functionInstructionWindowSchema
|
||||
.extend({ procedure: procedureIdentity })
|
||||
.strict();
|
||||
|
||||
// Current Ghidra profiles commit to emitting containment exactly for an interior
|
||||
// listing address. The shared Evidence schema remains optional for older records.
|
||||
const ghidraInstruction = nativeInstructionSchema.superRefine(
|
||||
(value, context) => {
|
||||
if (
|
||||
(value.status === "not-instruction-boundary") !==
|
||||
(value.containing_instruction !== undefined)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["containing_instruction"],
|
||||
message:
|
||||
"Ghidra instruction containment must match the reported status",
|
||||
});
|
||||
if ((value.status === "data") !== (value.containing_data !== undefined))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["containing_data"],
|
||||
message: "Ghidra data containment must match the reported status",
|
||||
});
|
||||
const data = value.containing_data;
|
||||
if (data !== undefined) {
|
||||
const distance = addressDistance(data.address, value.address);
|
||||
if (distance === null || distance >= data.length)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["containing_data", "address"],
|
||||
message:
|
||||
"Ghidra containing data must enclose the queried byte in the same address space",
|
||||
});
|
||||
}
|
||||
const containing = value.containing_instruction;
|
||||
if (containing !== undefined) {
|
||||
const distance = addressDistance(containing.address, value.address);
|
||||
if (distance === null || distance === 0 || distance >= containing.length)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["containing_instruction", "address"],
|
||||
message:
|
||||
"Ghidra containing instruction must enclose the queried byte in the same address space",
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const resultSchemas = {
|
||||
annotate_native_function: nativeFunctionAnnotationsSchema.safeExtend({
|
||||
dossier: ghidraFunctionDossier,
|
||||
@@ -299,7 +345,7 @@ const resultSchemas = {
|
||||
procedure_info: procedureInfo,
|
||||
procedure_pseudo_code: z.string().nullable(),
|
||||
read_function_instructions: ghidraFunctionInstructionWindow,
|
||||
inspect_native_instruction: nativeInstructionSchema,
|
||||
inspect_native_instruction: ghidraInstruction,
|
||||
inspect_native_data_type: nativeDataTypeSchema,
|
||||
resolve_native_call_targets: nativeCallTargetsSchema,
|
||||
procedure_references: procedureReferences,
|
||||
|
||||
@@ -11,9 +11,9 @@ import { GhidraProvider } from "./GhidraProvider.js";
|
||||
// bit-field base-type revisions that prevent reuse of snapshots with earlier
|
||||
// results.
|
||||
const RAW_DIGESTS = {
|
||||
"6502": "6f904539c00521b04f76a3bfd1ca88275e080b5751f9e2305de66c20d0bd2fa0",
|
||||
"6502": "6105f8d9c1739f7261a85e4a8463ed381c92ab85d6195974df73b40e45058300",
|
||||
"x86-disabled-analyzer":
|
||||
"5a534d959baf34abdafaea64ae9fcb9126553802d42ffa5e4bc0426de5f0af8c",
|
||||
"4f059962604e029c688bf6f181a8a60d6a642bd569c5e8e8ec540f888aee7de0",
|
||||
} as const;
|
||||
|
||||
const configFor = (environment: NodeJS.ProcessEnv) => {
|
||||
@@ -48,9 +48,11 @@ describe("raw-binary analysis profile digest", () => {
|
||||
load_image_evidence: "independent-raw-mapping-context-v4",
|
||||
load_image_observations:
|
||||
"source-mappings-entry-context-relocation-symbol-v3",
|
||||
instruction_status: "unlisted-address-without-decode-v1",
|
||||
instruction_status: "unlisted-and-containing-code-data-v3",
|
||||
byte_search_boundary: "contiguous-eligible-initialized-blocks-v1",
|
||||
data_type_observations: "bit-field-base-and-lookup-v1",
|
||||
instruction_references:
|
||||
"exact-address-incoming-without-synthetic-entry-v3",
|
||||
analyzer_preset: "ghidra-default",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -319,7 +319,12 @@ describe("Ghidra snapshot profile admission", () => {
|
||||
},
|
||||
{
|
||||
parameter: "instruction_status",
|
||||
current: "unlisted-address-without-decode-v1",
|
||||
current: "unlisted-and-containing-code-data-v3",
|
||||
legacy: "unlisted-address-without-decode-v1",
|
||||
},
|
||||
{
|
||||
parameter: "instruction_status",
|
||||
current: "unlisted-and-containing-code-data-v3",
|
||||
legacy: undefined,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -68,6 +68,12 @@ describe("compact Ghidra reference MCP evidence", () => {
|
||||
inspect_native_instruction: {
|
||||
...compactInstruction(),
|
||||
status: "data",
|
||||
containing_data: {
|
||||
address: "0x401000",
|
||||
length: 4,
|
||||
type_id: "/dword",
|
||||
provenance: "ghidra-listing",
|
||||
},
|
||||
incoming_references: [],
|
||||
},
|
||||
resolve_native_call_targets: compactCallTargets(),
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { ok } from "../../../src/domain/result.js";
|
||||
import { compactInstruction } from "../../../src/domain/native/nativeInstruction.fixture.js";
|
||||
import { connectGhidraMcp, sessionEvidence } from "./ghidraMcpHarness.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
|
||||
it.each(["compact", "full"] as const)(
|
||||
"keeps the selected interior address and containing instruction separate (%s)",
|
||||
async (profile) => {
|
||||
const observed = {
|
||||
...compactInstruction(),
|
||||
address: "overlay:0x1001",
|
||||
status: "not-instruction-boundary",
|
||||
procedure: null,
|
||||
bytes: null,
|
||||
length: null,
|
||||
mnemonic: null,
|
||||
raw_disassembly: null,
|
||||
operands: [],
|
||||
containing_instruction: {
|
||||
address: "overlay:0x1000",
|
||||
length: 4,
|
||||
mnemonic: "sub",
|
||||
raw_disassembly: "sub sp,sp,#0x10",
|
||||
provenance: "ghidra-listing",
|
||||
},
|
||||
};
|
||||
const harness = await connectGhidraMcp(
|
||||
"ghidra-containing-instruction",
|
||||
() => Promise.resolve(ok(observed)),
|
||||
{ environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: profile } },
|
||||
);
|
||||
try {
|
||||
const result = await harness.mcp.callTool({
|
||||
name: "inspect_native_instruction",
|
||||
arguments: { address: observed.address },
|
||||
});
|
||||
expect(result.isError).not.toBe(true);
|
||||
expect(
|
||||
sessionEvidence(harness.session, result.structuredContent)
|
||||
.normalized_result,
|
||||
).toMatchObject({
|
||||
address: observed.address,
|
||||
status: observed.status,
|
||||
procedure: null,
|
||||
bytes: null,
|
||||
containing_instruction: observed.containing_instruction,
|
||||
});
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["missing", "unexpected"] as const)(
|
||||
"rejects a %s containing instruction at the Ghidra boundary",
|
||||
async (condition) => {
|
||||
const observed = {
|
||||
...compactInstruction(),
|
||||
address: "0x1001",
|
||||
status: condition === "missing" ? "not-instruction-boundary" : "decoded",
|
||||
...(condition === "unexpected"
|
||||
? {
|
||||
containing_instruction: {
|
||||
address: "0x1000",
|
||||
length: 4,
|
||||
mnemonic: "sub",
|
||||
raw_disassembly: "sub sp,sp,#0x10",
|
||||
provenance: "ghidra-listing",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
const harness = await connectGhidraMcp("ghidra-invalid-containment", () =>
|
||||
Promise.resolve(ok(observed)),
|
||||
);
|
||||
try {
|
||||
const result = await harness.mcp.callTool({
|
||||
name: "inspect_native_instruction",
|
||||
arguments: { address: "0x1001" },
|
||||
});
|
||||
expect(parseMcpToolError(result).error).toMatchObject({
|
||||
code: "unreadable_output",
|
||||
details: { operation: "inspect_native_instruction" },
|
||||
});
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["different space", "other:0x1000"],
|
||||
["instruction start", "overlay:0x1001"],
|
||||
["past instruction", "overlay:0xff0"],
|
||||
["invalid coordinate", "not-an-address"],
|
||||
] as const)(
|
||||
"rejects a containing instruction with %s",
|
||||
async (_description, address) => {
|
||||
const observed = {
|
||||
...compactInstruction(),
|
||||
address: "overlay:0x1001",
|
||||
status: "not-instruction-boundary",
|
||||
containing_instruction: {
|
||||
address,
|
||||
length: 4,
|
||||
mnemonic: "sub",
|
||||
raw_disassembly: "sub sp,sp,#0x10",
|
||||
provenance: "ghidra-listing",
|
||||
},
|
||||
};
|
||||
const harness = await connectGhidraMcp("ghidra-invalid-coordinate", () =>
|
||||
Promise.resolve(ok(observed)),
|
||||
);
|
||||
try {
|
||||
const result = await harness.mcp.callTool({
|
||||
name: "inspect_native_instruction",
|
||||
arguments: { address: observed.address },
|
||||
});
|
||||
expect(parseMcpToolError(result).error).toMatchObject({
|
||||
code: "unreadable_output",
|
||||
details: { operation: "inspect_native_instruction" },
|
||||
});
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["compact", "full"] as const)(
|
||||
"retains a containing data identity without selecting a field (%s)",
|
||||
async (profile) => {
|
||||
const observed = {
|
||||
...compactInstruction(),
|
||||
address: "overlay:0x1002",
|
||||
status: "data",
|
||||
procedure: null,
|
||||
bytes: null,
|
||||
length: null,
|
||||
mnemonic: null,
|
||||
raw_disassembly: null,
|
||||
containing_data: {
|
||||
address: "overlay:0x1000",
|
||||
length: 5,
|
||||
type_id: "/Record",
|
||||
provenance: "ghidra-listing",
|
||||
},
|
||||
};
|
||||
const harness = await connectGhidraMcp(
|
||||
"ghidra-containing-data",
|
||||
() => Promise.resolve(ok(observed)),
|
||||
{ environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: profile } },
|
||||
);
|
||||
try {
|
||||
const result = await harness.mcp.callTool({
|
||||
name: "inspect_native_instruction",
|
||||
arguments: { address: observed.address },
|
||||
});
|
||||
expect(result.isError).not.toBe(true);
|
||||
expect(
|
||||
sessionEvidence(harness.session, result.structuredContent)
|
||||
.normalized_result,
|
||||
).toMatchObject({
|
||||
address: observed.address,
|
||||
status: "data",
|
||||
containing_data: observed.containing_data,
|
||||
});
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["missing", undefined],
|
||||
[
|
||||
"wrong space",
|
||||
{
|
||||
address: "other:0x1000",
|
||||
length: 5,
|
||||
type_id: "/Record",
|
||||
provenance: "ghidra-listing",
|
||||
},
|
||||
],
|
||||
[
|
||||
"outside extent",
|
||||
{
|
||||
address: "overlay:0x1000",
|
||||
length: 2,
|
||||
type_id: "/Record",
|
||||
provenance: "ghidra-listing",
|
||||
},
|
||||
],
|
||||
] as const)("rejects %s data containment", async (_name, containing_data) => {
|
||||
const observed = {
|
||||
...compactInstruction(),
|
||||
address: "overlay:0x1002",
|
||||
status: "data",
|
||||
...(containing_data === undefined ? {} : { containing_data }),
|
||||
};
|
||||
const harness = await connectGhidraMcp("ghidra-invalid-data", () =>
|
||||
Promise.resolve(ok(observed)),
|
||||
);
|
||||
try {
|
||||
const result = await harness.mcp.callTool({
|
||||
name: "inspect_native_instruction",
|
||||
arguments: { address: observed.address },
|
||||
});
|
||||
expect(parseMcpToolError(result).error).toMatchObject({
|
||||
code: "unreadable_output",
|
||||
details: { operation: "inspect_native_instruction" },
|
||||
});
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects data containment on a decoded instruction", async () => {
|
||||
const observed = {
|
||||
...compactInstruction(),
|
||||
address: "0x1001",
|
||||
containing_data: {
|
||||
address: "0x1000",
|
||||
length: 4,
|
||||
type_id: "/Record",
|
||||
provenance: "ghidra-listing",
|
||||
},
|
||||
};
|
||||
const harness = await connectGhidraMcp("ghidra-unexpected-data", () =>
|
||||
Promise.resolve(ok(observed)),
|
||||
);
|
||||
try {
|
||||
const result = await harness.mcp.callTool({
|
||||
name: "inspect_native_instruction",
|
||||
arguments: { address: observed.address },
|
||||
});
|
||||
expect(parseMcpToolError(result).error).toMatchObject({
|
||||
code: "unreadable_output",
|
||||
details: { operation: "inspect_native_instruction" },
|
||||
});
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user