mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(native): share exact-name xref ambiguity resolution
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user