fix(native): share exact-name xref ambiguity resolution

This commit is contained in:
morluto
2026-10-24 02:58:35 +08:00
parent 590bab9ada
commit c313f33674
4 changed files with 103 additions and 14 deletions
+4
View File
@@ -978,3 +978,7 @@ skipped uninitialized blocks. A missing match does not establish absence from
uninitialized memory or across block boundaries. See
[Ghidra byte search](ghidra-provider.md#masked-byte-search) for mask and coverage
semantics. This query never executes or modifies target bytes.
Exact-name xref lookup reports distinct candidate addresses as an input ambiguity;
inventory order cannot select one silently. Duplicate rows at one address remain
a singular match, and a missing name remains an unresolved observation.
+11 -6
View File
@@ -59,6 +59,7 @@ import {
} from "./EnhancedToolTypes.js";
import { resolveAnalysisDocument } from "./AnalysisDocument.js";
import { traceCallPath } from "./CallPathTracing.js";
import { resolveXrefsName } from "./XrefsAddressResolution.js";
import { resolveProcedureAddress } from "./ProcedureAddressResolution.js";
import { traceLiteralFeature } from "./EnhancedLiteralTracing.js";
import { projectNativeApiInspection } from "./native/NativeApiInspection.js";
@@ -686,15 +687,19 @@ export class EnhancedTools {
}
async #findXrefs(name: string, signal?: AbortSignal): EnhancedResult {
const names = await this.#allAddressed("list_names", signal);
if (!names.ok) return names;
const resolved = names.value.find((entry) => entry.name === name);
if (resolved === undefined)
const resolved = await resolveXrefsName(
this.analysis,
name,
signal,
"find_xrefs_to_name",
);
if (!resolved.ok) return resolved;
if (resolved.value === null)
return ok({ status: "unresolved", name, reason: "name_not_found" });
const xrefs = await this.#call(
"xrefs",
{ address: resolved.address },
{ address: resolved.value },
signal,
);
if (!xrefs.ok) return xrefs;
@@ -711,7 +716,7 @@ export class EnhancedTools {
return ok({
status: "resolved",
name,
address: resolved.address,
address: resolved.value,
xrefs: xrefs.value,
});
}
+37 -8
View File
@@ -2,6 +2,7 @@ import { addressedValueSchema } from "../domain/native/nativeInventory.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisCapabilityUnavailableError,
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
@@ -23,9 +24,38 @@ export const resolveXrefsAddress = async (
)
return ok(selector);
const inventory = await analysis.execute("list_names", {}, { signal });
const named = await resolveXrefsName(analysis, selector, signal);
if (!named.ok) return named;
return named.value !== null
? ok(named.value)
: err(
new AnalysisInputError("xrefs", undefined, [
{
path: ["address"],
reason: "invalid_value",
message: `Unknown analyzed symbol name ${JSON.stringify(selector)}. Use an exact analyzed name or a hexadecimal address.`,
},
]),
);
};
/** Exact name resolution has one ambiguity owner for enhanced tools and CLI fallback. */
export const resolveXrefsName = async (
analysis: AnalysisOperationPort,
selector: string,
signal?: AbortSignal,
operation: "xrefs" | "find_xrefs_to_name" = "xrefs",
): Promise<Result<string | null, AnalysisError>> => {
if (signal?.aborted)
return err(new AnalysisCancelledError(operation, { cause: signal.reason }));
const inventory = await analysis.execute(
"list_names",
{},
signal === undefined ? {} : { signal },
);
if (!inventory.ok) {
if (
operation === "xrefs" &&
inventory.error instanceof AnalysisCapabilityUnavailableError &&
inventory.error.operation === "list_names" &&
!inventory.error.cleanupIncomplete
@@ -44,6 +74,8 @@ export const resolveXrefsAddress = async (
);
return inventory;
}
if (signal?.aborted)
return err(new AnalysisCancelledError(operation, { cause: signal.reason }));
if (!Array.isArray(inventory.value.result))
return err(
new AnalysisOutputError(
@@ -65,16 +97,13 @@ export const resolveXrefsAddress = async (
if (parsed.data.value === selector) addresses.add(parsed.data.address);
}
const [address] = addresses;
if (addresses.size === 1 && address !== undefined) return ok(address);
if (addresses.size <= 1) return ok(address ?? null);
return err(
new AnalysisInputError("xrefs", undefined, [
new AnalysisInputError(operation, undefined, [
{
path: ["address"],
path: [operation === "xrefs" ? "address" : "name"],
reason: "invalid_value",
message:
addresses.size === 0
? `Unknown analyzed symbol name ${JSON.stringify(selector)}. Use an exact analyzed name or a hexadecimal address.`
: `Analyzed symbol name ${JSON.stringify(selector)} is ambiguous at ${[...addresses].join(", ")}. Select an exact address.`,
message: `Analyzed symbol name ${JSON.stringify(selector)} is ambiguous at ${[...addresses].sort().join(", ")}. Select an exact address.`,
},
]),
);
@@ -9,6 +9,7 @@ import {
inventory,
jsonResult,
} from "./enhancedToolsHarness.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { observed as ok } from "../../fixtures/analysisExecution.js";
afterEach(closeEnhancedToolResources);
@@ -75,3 +76,53 @@ describe("enhanced MCP tools", () => {
expect(result.error._tag).toBe("AnalysisCancelledError");
});
});
it("exact-name xrefs preserves ambiguity independently of inventory order and accepts repeated rows", async () => {
let rows = [
{ address: "0x20", value: "entry" },
{ address: "0x10", value: "entry" },
];
const queried: unknown[] = [];
const client = await connect({
execute: async (operation, parameters) => {
if (operation === "list_names") return ok(rows);
if (operation === "xrefs") {
queried.push(parameters.address);
return ok(["0x90"]);
}
throw new Error(`Unexpected operation ${operation}`);
},
});
for (const reversed of [false, true]) {
if (reversed) rows.reverse();
const rejected = await client.callTool({
name: "find_xrefs_to_name",
arguments: { name: "entry" },
});
expect(parseMcpToolError(rejected).error).toMatchObject({
code: "invalid_request",
details: {
issues: [
expect.objectContaining({
path: ["name"],
message: expect.stringContaining("ambiguous at 0x10, 0x20"),
}),
],
},
});
}
expect(queried).toEqual([]);
rows = [
{ address: "0x10", value: "entry" },
{ address: "0x10", value: "entry" },
];
expect(
jsonResult(
await client.callTool({
name: "find_xrefs_to_name",
arguments: { name: "entry" },
}),
),
).toMatchObject({ status: "resolved", address: "0x10", xrefs: ["0x90"] });
expect(queried).toEqual(["0x10"]);
});