fix(ghidra): preserve decoded return flow evidence
This commit is contained in:
parent
c5c75c38c3
commit
bcc8a773e2
|
|
@ -866,6 +866,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|||
JsonArray operands = new JsonArray();
|
||||
JsonArray references = new JsonArray();
|
||||
JsonArray destinations = new JsonArray();
|
||||
JsonArray flowEvidence = new JsonArray();
|
||||
JsonObject flow = new JsonObject();
|
||||
flow.addProperty("kind", "unavailable");
|
||||
flow.addProperty("conditional", false);
|
||||
|
|
@ -881,7 +882,20 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|||
result.addProperty("mnemonic", instruction.getMnemonicString());
|
||||
result.addProperty("raw_disassembly", instruction.toString());
|
||||
RefType type = instruction.getFlowType();
|
||||
flow.addProperty("kind", type.isCall() ? "call" : type.isJump() ? "jump" : type.isTerminal() ? "terminal" : "fallthrough");
|
||||
JsonObject listingFlow = new JsonObject();
|
||||
listingFlow.addProperty("source", "ghidra-listing-flow-type");
|
||||
listingFlow.addProperty("value", type.getName());
|
||||
flowEvidence.add(listingFlow);
|
||||
boolean decodedReturn = false;
|
||||
for (PcodeOp operation : instruction.getPcode()) {
|
||||
if (operation.getOpcode() != PcodeOp.RETURN) continue;
|
||||
decodedReturn = true;
|
||||
JsonObject returnEvidence = new JsonObject();
|
||||
returnEvidence.addProperty("source", "ghidra-instruction-pcode");
|
||||
returnEvidence.addProperty("value", operation.getMnemonic());
|
||||
flowEvidence.add(returnEvidence);
|
||||
}
|
||||
flow.addProperty("kind", type.isCall() ? "call" : type.isJump() ? "jump" : decodedReturn ? "return" : type.isTerminal() ? "terminal" : "fallthrough");
|
||||
flow.addProperty("conditional", type.isConditional());
|
||||
flow.addProperty("computed", type.isComputed());
|
||||
if (!type.isComputed()) for (Address target : instruction.getFlows()) destinations.add(canonicalAddress(target));
|
||||
|
|
@ -917,10 +931,13 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|||
}
|
||||
}
|
||||
flow.add("direct_destinations", destinations);
|
||||
flow.add("classification_evidence", flowEvidence);
|
||||
result.add("operands", operands);
|
||||
result.add("flow", flow);
|
||||
result.add("references", references);
|
||||
result.add("limitations", GSON.toJsonTree(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.", "Terminal flow is not classified as a return without provider evidence.")));
|
||||
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 (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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -185,6 +185,19 @@ outside-memory and undecodable addresses have separate outcomes. Effective
|
|||
memory base/index/displacement roles and per-instruction context mode remain
|
||||
unavailable; `mode` is the program language variant.
|
||||
|
||||
Ghidra distinguishes a return using decoded instruction p-code's `RETURN`
|
||||
operation. `flow.classification_evidence` preserves the Listing flow type and
|
||||
the observed return evidence; mnemonic spelling alone is not the classifier.
|
||||
Other terminal or trap flows retain the provider's classification, and this
|
||||
inspection does not change function extents or prove a calling convention.
|
||||
The analysis profile commits this decoder behavior. Snapshots from the earlier
|
||||
profile produce a profile mismatch rather than replaying the former generic
|
||||
terminal result. The old snapshot's `evidence_bundle` remains valid retained
|
||||
Evidence: copy that object to a separate JSON file and use `rea evidence-import`
|
||||
to validate it. Run analysis without the old snapshot and save a fresh one
|
||||
under the current profile. Other providers and older captures may omit the
|
||||
classification evidence field.
|
||||
|
||||
Call resolution reports direct, resolved indirect, ambiguous, unresolved and
|
||||
non-call outcomes from static call references. It does not establish runtime
|
||||
execution or classify Objective-C/Swift/vtable/closure mechanisms from names.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { verifyGhidraTargetAdmission } from "./real-ghidra-target-admission.mjs"
|
|||
import { verifyGhidraLargeResults } from "./real-ghidra-large-results.mjs";
|
||||
import { verifyGhidraEntryAliases } from "./real-ghidra-entry-aliases.mjs";
|
||||
import { verifyGhidraNamespaceAnnotations } from "./real-ghidra-namespace-annotations.mjs";
|
||||
import { verifyGhidraInstructionFlow } from "./real-ghidra-instruction-flow.mjs";
|
||||
|
||||
/** Probe real Ghidra location, annotation and error contracts through public adapters. */
|
||||
export async function verifyGhidraBoundaries(
|
||||
|
|
@ -157,6 +158,7 @@ export async function verifyGhidraBoundaries(
|
|||
/Annotations require a local function entry/u,
|
||||
);
|
||||
const names = await call("list_names");
|
||||
const instructionFlow = await verifyGhidraInstructionFlow(call, names, cli);
|
||||
const leaf = names.find((item) =>
|
||||
item.value.endsWith("rea_ghidra_inventory_leaf"),
|
||||
);
|
||||
|
|
@ -766,6 +768,7 @@ export async function verifyGhidraBoundaries(
|
|||
missing_and_nonregular_source_rejected: true,
|
||||
imported_source_identity_retained: true,
|
||||
equivalent_instruction_address_spellings: true,
|
||||
instruction_flow: instructionFlow,
|
||||
qualified_annotation_name_roundtrip: true,
|
||||
imported_entry_alias_selection: true,
|
||||
oversized_result_retention_and_complete_export: true,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
import assert from "node:assert/strict";
|
||||
|
||||
/** Verify decoded return evidence and distinct non-return flows on owned fixtures. */
|
||||
export async function verifyGhidraInstructionFlow(call, names, cli) {
|
||||
const leaf = names.find((item) =>
|
||||
item.value.endsWith("rea_ghidra_inventory_leaf"),
|
||||
);
|
||||
assert.ok(leaf, "Source fixture lacks its returning leaf");
|
||||
const leafInstructions = await inspectFunction(call, leaf.address);
|
||||
const returned = leafInstructions.find(
|
||||
(instruction) => instruction.flow.kind === "return",
|
||||
);
|
||||
assert.ok(returned, "Returning source leaf lacks decoded RETURN evidence");
|
||||
assertReturnEvidence(returned);
|
||||
assert.equal(
|
||||
(await call("resolve_native_call_targets", { address: returned.address }))
|
||||
.status,
|
||||
"not-call",
|
||||
);
|
||||
assert.deepEqual(
|
||||
await cli("inspect-native-instruction", returned.address),
|
||||
returned,
|
||||
);
|
||||
|
||||
const controls = [
|
||||
["rea_ghidra_flow_return", "RET", "c3"],
|
||||
["rea_ghidra_flow_cleanup_return", "RET", "c21000"],
|
||||
["rea_ghidra_flow_shared_tail", "JMP", null],
|
||||
["rea_ghidra_flow_trap", "UD2", "0f0b"],
|
||||
];
|
||||
let checked = 0;
|
||||
for (const [symbol, mnemonic, bytes] of controls) {
|
||||
const procedure = names.find((item) => item.value === symbol);
|
||||
if (procedure === undefined) continue;
|
||||
const before = await call("resolve_containing_procedure", {
|
||||
address: procedure.address,
|
||||
});
|
||||
const instructions = await inspectFunction(call, procedure.address);
|
||||
const instruction = instructions.find((item) => item.mnemonic === mnemonic);
|
||||
assert.ok(instruction, `Source fixture lacks ${symbol}'s ${mnemonic}`);
|
||||
if (bytes !== null) assert.equal(instruction.bytes, bytes);
|
||||
if (mnemonic === "RET") {
|
||||
assertReturnEvidence(instruction);
|
||||
assert.equal(instruction.flow.kind, "return");
|
||||
if (bytes === "c21000")
|
||||
assert.equal(instruction.operands[0]?.raw, "0x10");
|
||||
} else {
|
||||
assert.notEqual(instruction.flow.kind, "return");
|
||||
assert.ok(
|
||||
instruction.flow.classification_evidence.some(
|
||||
(item) => item.source === "ghidra-listing-flow-type",
|
||||
),
|
||||
);
|
||||
}
|
||||
assert.deepEqual(
|
||||
await call("resolve_containing_procedure", {
|
||||
address: procedure.address,
|
||||
}),
|
||||
before,
|
||||
);
|
||||
checked++;
|
||||
}
|
||||
const unavailable = await call("inspect_native_instruction", {
|
||||
address: "0x0",
|
||||
});
|
||||
assert.equal(unavailable.status, "outside-memory");
|
||||
assert.equal(unavailable.flow.kind, "unavailable");
|
||||
assert.deepEqual(unavailable.flow.classification_evidence, []);
|
||||
return {
|
||||
return_instruction: returned.address,
|
||||
decoded_pcode_return: true,
|
||||
return_cli_mcp_parity: true,
|
||||
explicit_x86_elf_controls: checked,
|
||||
explicit_control_scope:
|
||||
checked === 0
|
||||
? "Not present on this host-native fixture format/architecture"
|
||||
: "x86-64 ELF",
|
||||
generic_unknown_preserved: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function inspectFunction(call, procedure) {
|
||||
const listing = await call("read_function_instructions", { procedure });
|
||||
const instructions = [];
|
||||
for (const line of listing.instructions) {
|
||||
const delimiter = line.indexOf(": ");
|
||||
assert.ok(delimiter > 0, `Malformed provider instruction listing: ${line}`);
|
||||
instructions.push(
|
||||
await call("inspect_native_instruction", {
|
||||
address: line.slice(0, delimiter),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return instructions;
|
||||
}
|
||||
|
||||
function assertReturnEvidence(instruction) {
|
||||
assert.equal(instruction.status, "decoded");
|
||||
assert.ok(
|
||||
instruction.flow.classification_evidence.some(
|
||||
(item) =>
|
||||
item.source === "ghidra-instruction-pcode" && item.value === "RETURN",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
!instruction.limitations.some((text) =>
|
||||
text.includes("not classified as a return"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -92,8 +92,7 @@ export async function verifyDebugFunctionOperations(
|
|||
if (
|
||||
instruction.status !== "decoded" ||
|
||||
instruction.length < 1 ||
|
||||
instruction.bytes.length !== instruction.length * 2 ||
|
||||
instruction.operands.length === 0
|
||||
instruction.bytes.length !== instruction.length * 2
|
||||
)
|
||||
throw new Error("Structured native instruction facts drifted");
|
||||
const directSite = outgoing.references.find(
|
||||
|
|
|
|||
|
|
@ -59,6 +59,17 @@ export const nativeInstructionSchema = z.strictObject({
|
|||
conditional: z.boolean(),
|
||||
computed: z.boolean(),
|
||||
direct_destinations: z.array(z.string()),
|
||||
classification_evidence: z
|
||||
.array(
|
||||
z.strictObject({
|
||||
source: z.string().min(1),
|
||||
value: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.describe(
|
||||
"Observed provider facts used to classify flow; older captures or other providers may omit them.",
|
||||
),
|
||||
}),
|
||||
references: z.array(reference),
|
||||
limitations: z.array(z.string()),
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export const resolveGhidraAnalysisProfile = (
|
|||
function_body_evidence: "complete-inclusive-ranges-v1",
|
||||
function_references: "complete-body-and-entry-reference-manager-v2",
|
||||
location_resolution: "explicit-address-exact-entry-symbol-first-v3",
|
||||
instruction_flow_evidence: "decoded-return-pcode-v1",
|
||||
process_launch:
|
||||
installation.platform === "win32"
|
||||
? "official-headless-script-v1"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,22 @@
|
|||
|
||||
volatile int rea_ghidra_inventory_global = 7;
|
||||
|
||||
#if defined(__x86_64__) && defined(__ELF__)
|
||||
// Static decoder controls: these functions are retained but never executed.
|
||||
__attribute__((naked, noinline, used)) void rea_ghidra_flow_return(void) {
|
||||
__asm__("ret");
|
||||
}
|
||||
__attribute__((naked, noinline, used)) void rea_ghidra_flow_cleanup_return(void) {
|
||||
__asm__("ret $16");
|
||||
}
|
||||
__attribute__((naked, noinline, used)) void rea_ghidra_flow_shared_tail(void) {
|
||||
__asm__("jmp rea_ghidra_flow_return");
|
||||
}
|
||||
__attribute__((naked, noinline, used)) void rea_ghidra_flow_trap(void) {
|
||||
__asm__("ud2");
|
||||
}
|
||||
#endif
|
||||
|
||||
// This literal reproduces stack exhaustion in Java's recursive regex matcher.
|
||||
#define REA_REPEAT_4(value) value value value value
|
||||
#define REA_REPEAT_16(value) REA_REPEAT_4(REA_REPEAT_4(value))
|
||||
|
|
|
|||
Loading…
Reference in New Issue