fix(ghidra): warn when callee/caller reads stop at a truncated function body (#2360)
procedure_callees and procedure_callers returned a bare address list, so an
inferred no-return call that truncates Ghidra's function body silently dropped
real edges (fixes #2326). Wrap both views in the function-observation envelope
with noReturnCallLimitations, matching procedure_info, procedure_assembly,
procedure_pseudo_code, read_function_instructions, and analyze_function
(precedent: 69a8b3a95). Version the boundary-observation contract to
ghidra-terminal-call-limitations-v2 so older snapshots cannot replay
unqualified results.
Validation: red->green unit test for the envelope requirement, MCP boundary
evidence test extended for both views (limitations retained through Evidence
and snapshot replay), 8 harness test files + ghidra behavior tests green,
check:fast, docs:check, verify:test-discovery green. Real Ghidra host lane
unavailable here (no Ghidra installed); Java change mirrors the existing
procedureInfo call pattern.
This commit is contained in:
parent
dea770d5d9
commit
6637161e02
|
|
@ -812,7 +812,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|||
return functionObservation(result, boundaryLimitations);
|
||||
}
|
||||
|
||||
private JsonArray procedureCalls(JsonObject params, boolean callers) throws Exception {
|
||||
private JsonObject procedureCalls(JsonObject params, boolean callers) throws Exception {
|
||||
requireKeys(params, Set.of("document", "procedure"));
|
||||
requireDocument(params);
|
||||
Function function = resolveProcedure(requireString(params, "procedure"));
|
||||
|
|
@ -825,7 +825,12 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|||
for (Function item : ordered) {
|
||||
result.add(canonicalAddress(item.getEntryPoint()));
|
||||
}
|
||||
return result;
|
||||
return functionObservation(
|
||||
result,
|
||||
noReturnCallLimitations(
|
||||
currentProgram.getListing().getInstructions(function.getBody(), true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private JsonObject procedureInfo(JsonObject params) throws Exception {
|
||||
|
|
|
|||
|
|
@ -131,9 +131,10 @@ overview merely because its source artifact is unchanged.
|
|||
|
||||
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, and dossiers report
|
||||
observed terminal call sites in Evidence limitations, including resolved callee
|
||||
names, addresses, and FunctionManager flags when available. Instruction windows
|
||||
Pseudocode, assembly, procedure info, instruction windows, callee and caller
|
||||
reads, and dossiers report observed terminal call sites in Evidence limitations,
|
||||
including resolved callee names, addresses, and FunctionManager flags when
|
||||
available. Instruction windows
|
||||
and dossiers also retain these warnings in their result limitations. An
|
||||
unresolved callee or unestablished terminal-flow basis remains explicit. These
|
||||
observations qualify the analysis; they do not prove that a no-return flag is
|
||||
|
|
|
|||
|
|
@ -149,12 +149,12 @@ export const OFFICIAL_TOOL_CONTRACTS = [
|
|||
),
|
||||
official(
|
||||
"procedure_callees",
|
||||
"Return the provider's resolved direct callees for one procedure identified by symbol or address. Unresolved indirect calls may be absent; use analyze_function and typed references to preserve available edge uncertainty.",
|
||||
"Return the provider's resolved direct callees for one procedure identified by symbol or address. Unresolved indirect calls may be absent; use analyze_function and typed references to preserve available edge uncertainty. Ghidra terminal-call assumptions that may cut the analyzed body are reported in Evidence limitations.",
|
||||
z.object({ procedure, document }),
|
||||
),
|
||||
official(
|
||||
"procedure_callers",
|
||||
"Return the provider's resolved direct callers for one procedure identified by symbol or address. Results reflect completed static analysis and may omit unresolved indirect references.",
|
||||
"Return the provider's resolved direct callers for one procedure identified by symbol or address. Results reflect completed static analysis and may omit unresolved indirect references. Ghidra terminal-call assumptions that may cut the analyzed body are reported in Evidence limitations.",
|
||||
z.object({ procedure, document }),
|
||||
),
|
||||
official(
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ export const resolveGhidraAnalysisProfile = async (
|
|||
"address-first-unique-target-source-mapping-v2",
|
||||
instruction_flow_evidence: "decoded-return-pcode-v1",
|
||||
instruction_references: "exact-address-incoming-v1",
|
||||
function_boundary_observations: "ghidra-terminal-call-limitations-v1",
|
||||
function_boundary_observations: "ghidra-terminal-call-limitations-v2",
|
||||
no_return_repair:
|
||||
installation.platform === "win32"
|
||||
? "disabled-windows-p0"
|
||||
|
|
@ -218,7 +218,7 @@ export const resolveGhidraAnalysisProfile = async (
|
|||
"address-first-unique-target-source-mapping-v2",
|
||||
instruction_flow_evidence: "decoded-return-pcode-v1",
|
||||
instruction_references: "exact-address-incoming-v1",
|
||||
function_boundary_observations: "ghidra-terminal-call-limitations-v1",
|
||||
function_boundary_observations: "ghidra-terminal-call-limitations-v2",
|
||||
no_return_repair:
|
||||
installation.platform === "win32"
|
||||
? "disabled-windows-p0"
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import {
|
|||
const observationOperations: ReadonlySet<GhidraFunctionOperation> = new Set([
|
||||
"analyze_function",
|
||||
"procedure_assembly",
|
||||
"procedure_callees",
|
||||
"procedure_callers",
|
||||
"procedure_info",
|
||||
"procedure_pseudo_code",
|
||||
"read_function_instructions",
|
||||
|
|
@ -93,6 +95,29 @@ describe("Ghidra function-analysis result values", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it.each(["procedure_callees", "procedure_callers"] as const)(
|
||||
"requires the function-observation envelope to carry terminal-call limitations for %s",
|
||||
(operation) => {
|
||||
// A bare address list is the pre-fix bridge shape; without the envelope
|
||||
// a truncated function body reports no warning, so it must be rejected.
|
||||
expect(parseGhidraFunctionResult(operation, ["0x2000"])).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisOutputError" },
|
||||
});
|
||||
const limitation =
|
||||
"Ghidra Listing reports a terminal call at 0x401000 to 0x402000 abort; fallthrough is excluded by Ghidra's flow model.";
|
||||
expect(
|
||||
parseGhidraFunctionResult(operation, {
|
||||
value: ["0x2000"],
|
||||
limitations: [limitation],
|
||||
}),
|
||||
).toEqual({
|
||||
ok: true,
|
||||
value: { value: ["0x2000"], limitations: [limitation] },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["reference_kinds_available", "unresolved_calls"])(
|
||||
"rejects omitted %s instead of inventing complete reference observations",
|
||||
(field) => {
|
||||
|
|
|
|||
|
|
@ -409,6 +409,8 @@ const functionBoundaryObservationOperations: ReadonlySet<GhidraFunctionOperation
|
|||
new Set([
|
||||
"analyze_function",
|
||||
"procedure_assembly",
|
||||
"procedure_callees",
|
||||
"procedure_callers",
|
||||
"procedure_info",
|
||||
"procedure_pseudo_code",
|
||||
"read_function_instructions",
|
||||
|
|
|
|||
|
|
@ -828,7 +828,11 @@ describe("Ghidra relationship coverage", () => {
|
|||
start: () => Promise.resolve(ok(sessionInfo())),
|
||||
callTool: (operation) =>
|
||||
Promise.resolve(
|
||||
ok(operation === "procedure_callees" ? ["0x2000"] : []),
|
||||
ok(
|
||||
operation === "procedure_callees"
|
||||
? { value: ["0x2000"], limitations: [] }
|
||||
: { value: [], limitations: [] },
|
||||
),
|
||||
),
|
||||
close: () => Promise.resolve(ok(null)),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ it("retains terminal-call qualifications across function views and snapshot repl
|
|||
limitations: [limitation],
|
||||
},
|
||||
],
|
||||
["procedure_callees", ["0x402000"]],
|
||||
["procedure_callers", [identity.address]],
|
||||
["analyze_function", jsonValueSchema.parse(dossier)],
|
||||
]);
|
||||
const harness = await connectGhidraMcp(
|
||||
|
|
@ -81,7 +83,7 @@ it("retains terminal-call qualifications across function views and snapshot repl
|
|||
});
|
||||
expect(evidence.limitations, name).toContain(limitation);
|
||||
expect(evidence.analysis_profile?.parameters).toMatchObject({
|
||||
function_boundary_observations: "ghidra-terminal-call-limitations-v1",
|
||||
function_boundary_observations: "ghidra-terminal-call-limitations-v2",
|
||||
});
|
||||
const replay = await harness.session.execute(name, evidence.parameters);
|
||||
if (!replay.ok) throw replay.error;
|
||||
|
|
|
|||
|
|
@ -263,6 +263,8 @@ const resultFor = (
|
|||
const functionObservationOperations: ReadonlySet<GhidraOperation> = new Set([
|
||||
"analyze_function",
|
||||
"procedure_assembly",
|
||||
"procedure_callees",
|
||||
"procedure_callers",
|
||||
"procedure_info",
|
||||
"procedure_pseudo_code",
|
||||
"read_function_instructions",
|
||||
|
|
|
|||
Loading…
Reference in New Issue