diff --git a/bridge/ghidra/ReaGhidraBridge.java b/bridge/ghidra/ReaGhidraBridge.java index 70132eeae..c180c5866 100644 --- a/bridge/ghidra/ReaGhidraBridge.java +++ b/bridge/ghidra/ReaGhidraBridge.java @@ -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 { diff --git a/docs/ghidra-provider.md b/docs/ghidra-provider.md index 32edbd320..5b83ee152 100644 --- a/docs/ghidra-provider.md +++ b/docs/ghidra-provider.md @@ -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 diff --git a/src/contracts/officialToolContracts.ts b/src/contracts/officialToolContracts.ts index 82f1dc83d..4750528ad 100644 --- a/src/contracts/officialToolContracts.ts +++ b/src/contracts/officialToolContracts.ts @@ -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( diff --git a/src/ghidra/GhidraAnalysisProfile.ts b/src/ghidra/GhidraAnalysisProfile.ts index 8cc5f2807..81defabd3 100644 --- a/src/ghidra/GhidraAnalysisProfile.ts +++ b/src/ghidra/GhidraAnalysisProfile.ts @@ -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" diff --git a/src/ghidra/GhidraFunctionValues.test.ts b/src/ghidra/GhidraFunctionValues.test.ts index 333d2c913..e54692840 100644 --- a/src/ghidra/GhidraFunctionValues.test.ts +++ b/src/ghidra/GhidraFunctionValues.test.ts @@ -23,6 +23,8 @@ import { const observationOperations: ReadonlySet = 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) => { diff --git a/src/ghidra/GhidraFunctionValues.ts b/src/ghidra/GhidraFunctionValues.ts index 4e4c5054a..a6e62c93c 100644 --- a/src/ghidra/GhidraFunctionValues.ts +++ b/src/ghidra/GhidraFunctionValues.ts @@ -409,6 +409,8 @@ const functionBoundaryObservationOperations: ReadonlySet { 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)), })); diff --git a/tests/boundary/mcp/ghidraEvidenceMcp.test.ts b/tests/boundary/mcp/ghidraEvidenceMcp.test.ts index c28fcca39..ed82400aa 100644 --- a/tests/boundary/mcp/ghidraEvidenceMcp.test.ts +++ b/tests/boundary/mcp/ghidraEvidenceMcp.test.ts @@ -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; diff --git a/tests/boundary/mcp/ghidraMcpHarness.ts b/tests/boundary/mcp/ghidraMcpHarness.ts index ad3565992..a9b2248dc 100644 --- a/tests/boundary/mcp/ghidraMcpHarness.ts +++ b/tests/boundary/mcp/ghidraMcpHarness.ts @@ -263,6 +263,8 @@ const resultFor = ( const functionObservationOperations: ReadonlySet = new Set([ "analyze_function", "procedure_assembly", + "procedure_callees", + "procedure_callers", "procedure_info", "procedure_pseudo_code", "read_function_instructions",