mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(native): preserve procedure selector coordinate intent
This commit is contained in:
+19
-5
@@ -337,11 +337,14 @@ def _bookmark_name(document, address):
|
||||
return _bookmark_api(document, "getBookmarkName")(address)
|
||||
|
||||
|
||||
def _address(document, value=None):
|
||||
"""Keep explicit hexadecimal coordinates distinct from symbol names."""
|
||||
def _address_selection(document, value=None):
|
||||
"""Own the coordinate and whether it was selected by symbol or address."""
|
||||
if value is None:
|
||||
return document.getCurrentAddress()
|
||||
return document.getCurrentAddress(), "cursor"
|
||||
_api_text(value, "Address")
|
||||
if value.startswith("file-offset:"):
|
||||
raise InvalidRequestError("Hopper does not support file-offset: selectors; select Ghidra for source-file coordinates or use a Hopper analysis address")
|
||||
source = "address"
|
||||
if value.lower().startswith("0x"):
|
||||
try:
|
||||
address = int(value, 16)
|
||||
@@ -352,6 +355,8 @@ def _address(document, value=None):
|
||||
# Prefer an observed symbol, retaining bare-hex compatibility only
|
||||
# when Hopper has no symbol with the supplied name.
|
||||
address = document.getAddressForName(value)
|
||||
if address not in BAD_ADDRESSES:
|
||||
source = "symbol"
|
||||
if address in BAD_ADDRESSES:
|
||||
try:
|
||||
address = int(value, 16)
|
||||
@@ -359,7 +364,11 @@ def _address(document, value=None):
|
||||
raise InvalidRequestError("Unknown Hopper address or name: %s" % value) from error
|
||||
if not 0 <= address < 0xFFFFFFFFFFFFFFFF:
|
||||
raise InvalidRequestError("Address is outside Hopper's usable unsigned 64-bit range: %s" % value)
|
||||
return address
|
||||
return address, source
|
||||
|
||||
|
||||
def _address(document, value=None):
|
||||
return _address_selection(document, value)[0]
|
||||
|
||||
|
||||
def _segment(document, address):
|
||||
@@ -370,10 +379,15 @@ def _segment(document, address):
|
||||
|
||||
|
||||
def _procedure(document, value=None):
|
||||
address = document.getCurrentAddress() if value is None else _address(document, value)
|
||||
address, source = _address_selection(document, value)
|
||||
result = _segment(document, address).getProcedureAtAddress(address)
|
||||
if result is None:
|
||||
raise InvalidRequestError("No procedure exists at the requested address")
|
||||
if source == "symbol" and address != result.getEntryPoint():
|
||||
raise InvalidRequestError(
|
||||
"Symbol %s at %s is inside procedure %s at %s; select its entry name/address or use resolve_containing_procedure for explicit containment"
|
||||
% (value, _hex(address), _procedure_name(result), _hex(result.getEntryPoint()))
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -982,3 +982,10 @@ 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.
|
||||
|
||||
Hopper procedure names must identify a procedure entry. A non-entry symbol returns
|
||||
an input diagnostic with its coordinate and containing procedure; an explicit
|
||||
numeric address still selects its containing procedure. Use
|
||||
`resolve_containing_procedure` to request containment directly. IDA and Hopper
|
||||
reject reserved `file-offset:` selectors before name lookup; source-file offset
|
||||
selection belongs to the Ghidra import mapping.
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { rm } from "node:fs/promises";
|
||||
import { expect, it } from "vitest";
|
||||
import {
|
||||
createIdaTarget,
|
||||
RecordingIdaMcp,
|
||||
} from "../../tests/fixtures/idaMcp.js";
|
||||
import { IdaOperationRunner } from "./IdaOperationRunner.js";
|
||||
|
||||
it.each(["legacy", "modern", "native"] as const)(
|
||||
"%s procedure resolution rejects file coordinates before any symbol lookup",
|
||||
async (protocol) => {
|
||||
const { root, target } = await createIdaTarget();
|
||||
const producer = new RecordingIdaMcp(
|
||||
target,
|
||||
protocol === "legacy" ? "attached" : "headless",
|
||||
);
|
||||
const runner = new IdaOperationRunner(producer, protocol);
|
||||
try {
|
||||
await expect(
|
||||
runner.resolve("file-offset:0x10", "procedure_pseudo_code"),
|
||||
).rejects.toMatchObject({
|
||||
_tag: "AnalysisInputError",
|
||||
operation: "procedure_pseudo_code",
|
||||
issues: [
|
||||
{
|
||||
path: ["procedure"],
|
||||
message: expect.stringContaining("source-file coordinates"),
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(producer.calls).toEqual([]);
|
||||
expect((await runner.resolve("main")).name).toBe("main");
|
||||
} finally {
|
||||
await producer.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -166,6 +166,19 @@ export class IdaOperationRunner {
|
||||
* addresses were reported by IDA itself.
|
||||
*/
|
||||
async resolve(query: string, requestedBy?: string): Promise<IdaFunction> {
|
||||
if (query.startsWith("file-offset:"))
|
||||
throw new AnalysisInputError(
|
||||
requestedBy ?? "procedure_address",
|
||||
undefined,
|
||||
[
|
||||
{
|
||||
path: ["procedure"],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"IDA does not support file-offset: procedure selectors. Select Ghidra for source-file coordinates or supply an IDA analysis address or exact procedure name.",
|
||||
},
|
||||
],
|
||||
);
|
||||
if (this.protocol !== "legacy") {
|
||||
const item = modernLookupSchema.parse(
|
||||
await this.call("lookup_funcs", { queries: [query] }),
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
it("Hopper preserves named-entry selection and explicit address containment", async () => {
|
||||
const fixture = fileURLToPath(
|
||||
new URL(
|
||||
"../../../fixtures/hopperProcedureSelectionProbe.py",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const bridge = fileURLToPath(
|
||||
new URL("../../../../bridge/hopper_bridge.py", import.meta.url),
|
||||
);
|
||||
const { stdout } = await promisify(execFile)(
|
||||
"python3",
|
||||
["-I", "-S", fixture, bridge],
|
||||
{ timeout: 3_000 },
|
||||
);
|
||||
const observed = JSON.parse(stdout);
|
||||
for (const selector of ["entry", "alias_entry", "0x401004", "401004"])
|
||||
expect(observed.results[selector]).toEqual({ entry: "0x401000" });
|
||||
expect(observed.results.inner).toMatchObject({
|
||||
diagnostic_type: "invalid_request",
|
||||
message: expect.stringContaining(
|
||||
"inner at 0x401004 is inside procedure entry at 0x401000",
|
||||
),
|
||||
});
|
||||
expect(observed.results["file-offset:0x4"]).toMatchObject({
|
||||
diagnostic_type: "invalid_request",
|
||||
message: expect.stringContaining("source-file coordinates"),
|
||||
});
|
||||
expect(observed.name_lookups).toEqual([
|
||||
"entry",
|
||||
"alias_entry",
|
||||
"inner",
|
||||
"401004",
|
||||
]);
|
||||
});
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
"""Exercise named-entry and explicit-coordinate selection at the Hopper boundary."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
bridge_path = sys.argv[1]
|
||||
bridge = {"__file__": bridge_path, "__name__": "rea_hopper_bridge"}
|
||||
exec(compile(Path(bridge_path).read_text(encoding="utf-8"), bridge_path, "exec"), bridge)
|
||||
|
||||
class Procedure:
|
||||
def getEntryPoint(self):
|
||||
return 0x401000
|
||||
def getSegment(self):
|
||||
return segment
|
||||
class Segment:
|
||||
def getNameAtAddress(self, address):
|
||||
return "entry" if address == 0x401000 else "inner"
|
||||
def getProcedureAtAddress(self, address):
|
||||
return procedure
|
||||
class Document:
|
||||
def __init__(self):
|
||||
self.names = []
|
||||
def getCurrentAddress(self):
|
||||
return 0x401004
|
||||
def getAddressForName(self, name):
|
||||
self.names.append(name)
|
||||
return {"entry": 0x401000, "alias_entry": 0x401000, "inner": 0x401004}.get(name)
|
||||
def getSegmentAtAddress(self, address):
|
||||
return segment
|
||||
segment = Segment()
|
||||
procedure = Procedure()
|
||||
document = Document()
|
||||
results = {}
|
||||
for selector in ["entry", "alias_entry", "inner", "0x401004", "401004", "file-offset:0x4"]:
|
||||
try:
|
||||
selected = bridge["_procedure"](document, selector)
|
||||
results[selector] = {"entry": bridge["_hex"](selected.getEntryPoint())}
|
||||
except Exception as error:
|
||||
results[selector] = {"diagnostic_type": bridge["_diagnostic_type"](error), "message": str(error)}
|
||||
print(json.dumps({"results": results, "name_lookups": document.names}))
|
||||
Reference in New Issue
Block a user