From 47ed29a98ca466d36c9c50ce10a5d0fe5819688b Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 3 Nov 2026 23:57:31 +0800 Subject: [PATCH] fix(ghidra): explain literal search candidate evidence --- docs/native-investigation.md | 3 +++ src/contracts/officialToolContracts.ts | 4 ++-- src/ghidra/GhidraLiteralSearch.ts | 4 ++++ src/ghidra/GhidraProvider.behavior.test.ts | 10 ++++++++++ src/ghidra/GhidraProviderClient.ts | 18 +++++++++++++++--- 5 files changed, 34 insertions(+), 5 deletions(-) diff --git a/docs/native-investigation.md b/docs/native-investigation.md index 83b414f5bd..0c5927bfd1 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -72,6 +72,9 @@ its address when selecting the containing function. `search_procedures` matches the same names, in leaf or qualified spelling: the function appears once with its primary name as `value`, and `matched_aliases` lists the qualified names of the secondary symbols that matched. +For Ghidra case-insensitive literal searches of procedures or strings, +`raw_result` contains bridge candidates before REA applies Unicode-aware +matching. Use `normalized_result` for matches; a raw candidate need not match. Overloads can share a fully qualified name; ambiguity errors return every matching entry address so the caller can select the intended function directly. diff --git a/src/contracts/officialToolContracts.ts b/src/contracts/officialToolContracts.ts index 16ba5d0b66..54588bbe73 100644 --- a/src/contracts/officialToolContracts.ts +++ b/src/contracts/officialToolContracts.ts @@ -218,13 +218,13 @@ export const OFFICIAL_TOOL_CONTRACTS = [ ), official( "search_procedures", - `Search every analyzed procedure name, including secondary names exact lookup accepts, using literal matching by default or regex when requested. Each procedure appears once under its primary name; matched_aliases lists matching secondary names. Results are deterministic and complete. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`, + `Search every analyzed procedure name, including secondary names exact lookup accepts, using literal matching by default or regex when requested. Each procedure appears once under its primary name; matched_aliases lists matching secondary names. Results are deterministic and complete. For Ghidra case-insensitive literal searches, raw_result contains unfiltered candidates; normalized_result contains matches. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`, z.object({ ...nativeSearchInput, detail: analysisDetailSchema }), { evidenceParameters: "omit-detail" }, ), official( "search_strings", - `Search every provider-reported string object or candidate using literal matching by default or regex when requested. Results are deterministic and complete over that provider inventory, not every text sequence in artifact bytes. Kuna scans mapped sequences of at least five characters, including unterminated fragments, without computing references. Ghidra may omit packed or undefined literals, including Rust strings; an empty result does not prove text is absent. If an address is known, xrefs can look for references; read_bytes can inspect bytes where supported (not available with Kuna). Hopper retains typed byte decoding, native display, and explicit decoding unknowns. Adjacent long-literal fragments match independently. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`, + `Search every provider-reported string object or candidate using literal matching by default or regex when requested. Results are deterministic and complete over that provider inventory, not every text sequence in artifact bytes. Kuna scans mapped sequences of at least five characters, including unterminated fragments, without computing references. Ghidra may omit packed or undefined literals, including Rust strings; an empty result does not prove text is absent. If an address is known, xrefs can look for references; read_bytes can inspect bytes where supported (not available with Kuna). Hopper retains typed byte decoding, native display, and explicit decoding unknowns. Adjacent long-literal fragments match independently. For Ghidra case-insensitive literal searches, raw_result contains unfiltered candidates; normalized_result contains matches. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`, z.object({ ...nativeSearchInput, detail: analysisDetailSchema }), { evidenceParameters: "omit-detail" }, ), diff --git a/src/ghidra/GhidraLiteralSearch.ts b/src/ghidra/GhidraLiteralSearch.ts index 46bac8a09a..f38ab07eef 100644 --- a/src/ghidra/GhidraLiteralSearch.ts +++ b/src/ghidra/GhidraLiteralSearch.ts @@ -4,6 +4,10 @@ import { matchProcedureNames, } from "../domain/analysisSearch.js"; +/** Explain the retained bridge observation when Node owns final matching. */ +export const GHIDRA_LITERAL_CANDIDATES_LIMITATION = + "For this case-insensitive literal search, raw_result contains Ghidra's candidate rows before REA's Unicode-aware filtering; normalized_result contains the matches."; + /** ASCII-only candidates are exact; every non-ASCII source value stays eligible. * Node owns Unicode folding, so the Java runtime's Unicode version cannot omit * a match. Null means no ASCII-only value can match this query. diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts index e5728fe7f9..c991b7d900 100644 --- a/src/ghidra/GhidraProvider.behavior.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { GHIDRA_LITERAL_CANDIDATES_LIMITATION } from "./GhidraLiteralSearch.js"; import { EXISTING_PROJECT_WINDOWS_REMEDIATION, EXISTING_PROJECT_WINDOWS_UNAVAILABLE, @@ -5620,18 +5621,27 @@ it.each(["search_procedures", "search_strings"] as const)( if (!result.ok) throw result.error; expect(result.value.result).toEqual(rows.slice(0, 2)); expect(result.value.rawResult).toEqual(rows); + expect(result.value.limitations).toContain( + GHIDRA_LITERAL_CANDIDATES_LIMITATION, + ); const regex = await client.execute(operation, { pattern: "source", mode: "regex", }); if (!regex.ok) throw regex.error; expect(regex.value.result).toEqual(rows); + expect(regex.value.limitations).not.toContain( + GHIDRA_LITERAL_CANDIDATES_LIMITATION, + ); const sensitive = await client.execute(operation, { pattern: "source", case_sensitive: true, }); if (!sensitive.ok) throw sensitive.error; expect(sensitive.value.result).toEqual(rows); + expect(sensitive.value.limitations).not.toContain( + GHIDRA_LITERAL_CANDIDATES_LIMITATION, + ); } finally { await client.close(); } diff --git a/src/ghidra/GhidraProviderClient.ts b/src/ghidra/GhidraProviderClient.ts index 3040df85fc..23d7428113 100644 --- a/src/ghidra/GhidraProviderClient.ts +++ b/src/ghidra/GhidraProviderClient.ts @@ -1,4 +1,7 @@ -import { filterGhidraLiteralCandidates } from "./GhidraLiteralSearch.js"; +import { + filterGhidraLiteralCandidates, + GHIDRA_LITERAL_CANDIDATES_LIMITATION, +} from "./GhidraLiteralSearch.js"; import { ghidraProjectHistoryEvidence } from "./GhidraProjectHistory.js"; import type { Evidence, EvidenceLocation } from "../domain/evidence.js"; import { ghidraProjectCommitmentSchema } from "./GhidraProjectSnapshot.js"; @@ -845,7 +848,9 @@ export const createGhidraProviderClient = (input: { ? parseGhidraFunctionResult(operation, called.value) : parseGhidraInventoryResult(operation, called.value); if (!result.ok) return result; - let { value: normalized, ...observation } = result.value; + const { value: parsedValue, ...parsedObservation } = result.value; + let normalized = parsedValue; + let observation: GhidraExecutionMetadata = parsedObservation; if ( (operation === "search_procedures" || operation === "search_strings") && input.value.mode === "literal" && @@ -856,7 +861,14 @@ export const createGhidraProviderClient = (input: { normalized, input.value.pattern, ); - observation = { ...observation, rawResult: called.value }; + observation = { + ...observation, + rawResult: called.value, + limitations: [ + ...(observation.limitations ?? []), + GHIDRA_LITERAL_CANDIDATES_LIMITATION, + ], + }; } if ( operation === "inspect_native_data_type" &&