fix(ghidra): preserve decoded return flow evidence

This commit is contained in:
N0zoM1z0
2026-10-09 18:53:28 +08:00
parent c5c75c38c3
commit bcc8a773e2
8 changed files with 174 additions and 4 deletions
+3
View File
@@ -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"),
),
);
}
+1 -2
View File
@@ -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(