mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(native): require observed symbol origin for named identity
This commit is contained in:
1 parent
98ddd04433
commit
62f7b5e4cd
23 files changed
+395
-107
No files matched your search
@@ -1097,6 +1097,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
Function function = resolveProcedure(requireString(params, "procedure"));
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("name", procedureName(function));
|
||||
Symbol symbol = function.getSymbol();
|
||||
result.add("symbol", symbol == null ? JsonNull.INSTANCE : symbolMetadata(symbol));
|
||||
result.addProperty("entrypoint", canonicalAddress(function.getEntryPoint()));
|
||||
result.addProperty("basicblock_count", basicBlocks(function).size());
|
||||
JsonObject body = functionBody(function);
|
||||
@@ -2138,13 +2140,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (!address.isMemoryAddress() && !address.isExternalAddress()) {
|
||||
continue;
|
||||
}
|
||||
JsonObject facts = new JsonObject();
|
||||
facts.addProperty("primary", symbol.isPrimary());
|
||||
facts.addProperty("dynamic", symbol.isDynamic());
|
||||
facts.addProperty("external", symbol.isExternal());
|
||||
facts.addProperty("type", normalizedSymbolType(symbol));
|
||||
facts.addProperty("source", normalizedSource(symbol.getSource()));
|
||||
result.add(new InventoryItem(address, symbol.getName(true), facts));
|
||||
JsonObject facts = symbolMetadata(symbol);
|
||||
result.add(new InventoryItem(address, symbol.getName(true), facts, null));
|
||||
}
|
||||
result.sort(INVENTORY_ORDER);
|
||||
nameInventory = List.copyOf(result);
|
||||
@@ -2188,7 +2185,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
InventoryItem item = new InventoryItem(
|
||||
function.getEntryPoint(),
|
||||
symbol == null ? function.getName() : symbol.getName(true),
|
||||
facts
|
||||
facts,
|
||||
symbol == null ? null : symbolMetadata(symbol)
|
||||
);
|
||||
destination.add(new FunctionEntry(function, item));
|
||||
}
|
||||
@@ -2219,7 +2217,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
instance.isMissingNullTerminator() ? "missing" : "present_or_not_required"
|
||||
);
|
||||
facts.addProperty("byte_length", Math.max(0, data.getLength()));
|
||||
result.add(new InventoryItem(data.getAddress(), value, facts));
|
||||
result.add(new InventoryItem(data.getAddress(), value, facts, null));
|
||||
}
|
||||
result.sort(INVENTORY_ORDER);
|
||||
stringInventory = List.copyOf(result);
|
||||
@@ -4406,6 +4404,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (factsName != null) {
|
||||
result.add(factsName, item.facts.deepCopy());
|
||||
}
|
||||
if (item.symbol != null) {
|
||||
result.add("symbol", item.symbol.deepCopy());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -4433,6 +4434,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("address", canonicalAddress(function.getEntryPoint()));
|
||||
result.addProperty("name", procedureName(function));
|
||||
Symbol symbol = function.getSymbol();
|
||||
result.add("symbol", symbol == null ? JsonNull.INSTANCE : symbolMetadata(symbol));
|
||||
result.add("classification", functionClassification(function));
|
||||
result.add("body", functionBody(function));
|
||||
return result;
|
||||
@@ -4661,6 +4664,16 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return value.isEmpty() ? "unknown" : value;
|
||||
}
|
||||
|
||||
private static JsonObject symbolMetadata(Symbol symbol) {
|
||||
JsonObject facts = new JsonObject();
|
||||
facts.addProperty("primary", symbol.isPrimary());
|
||||
facts.addProperty("dynamic", symbol.isDynamic());
|
||||
facts.addProperty("external", symbol.isExternal());
|
||||
facts.addProperty("type", normalizedSymbolType(symbol));
|
||||
facts.addProperty("source", normalizedSource(symbol.getSource()));
|
||||
return facts;
|
||||
}
|
||||
|
||||
private static String normalizedSource(SourceType source) {
|
||||
return source.name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
@@ -4907,7 +4920,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
.thenComparingInt(Reference::getOperandIndex)
|
||||
.thenComparing(Reference::isPrimary);
|
||||
|
||||
private record InventoryItem(Address address, String value, JsonObject facts) {}
|
||||
private record InventoryItem(Address address, String value, JsonObject facts, JsonObject symbol) {}
|
||||
private record AddressNameRequest(String inputAddress, Address address, String name) {}
|
||||
private record FunctionEntry(Function function, InventoryItem item) {}
|
||||
private record FunctionAnalysisObservation(JsonObject dossier, List<String> limitations, JsonObject relationshipCoverage, boolean decompilerFailed) {}
|
||||
|
||||
@@ -400,6 +400,7 @@ def _procedure_identity(procedure):
|
||||
return {
|
||||
"address": _hex(procedure.getEntryPoint()),
|
||||
"name": _procedure_name(procedure),
|
||||
"symbol": None,
|
||||
"classification": None,
|
||||
"body": {
|
||||
"available": False,
|
||||
@@ -1070,6 +1071,7 @@ def _dispatch(method, params):
|
||||
length = sum(max(0, _basic_block_end(procedure, block) - block.getStartingAddress()) for block in blocks)
|
||||
return {
|
||||
"name": _procedure_name(procedure),
|
||||
"symbol": None,
|
||||
"entrypoint": _hex(procedure.getEntryPoint()),
|
||||
"basicblock_count": procedure.getBasicBlockCount(),
|
||||
"length": length,
|
||||
|
||||
@@ -1276,14 +1276,15 @@ profiles, Ghidra universal-image imports without an exact slice commitment, and
|
||||
older or imported Evidence without a commitment leave coordinate identity
|
||||
unknown. Unparseable coordinates also remain unknown.
|
||||
|
||||
Across different artifacts, endpoints use nonempty stable procedure names.
|
||||
Missing or address-generated names remain unknown. A name observed at multiple
|
||||
procedure entries across either dossier's call and reference facets retains its
|
||||
label constraint but cannot establish equal endpoints. Same-container records
|
||||
without a common coordinate context likewise cannot establish equality from
|
||||
matching labels. Procedure labels do not carry authoritative symbol-origin
|
||||
facts, so recognition of generated names remains conservative and cannot prove
|
||||
whether every provider label was generated or explicitly named.
|
||||
Across different artifacts, endpoints use nonempty procedure names whose observed
|
||||
symbol facts establish a named origin. Dynamic/default symbols and unknown origin
|
||||
remain unknown. Ghidra emits the same source/dynamic symbol facts for inventories
|
||||
and procedure identities; Hopper, IDA and older imported dossiers without verified
|
||||
origin facts cannot establish correspondence from display labels. No name-spelling
|
||||
rule classifies `EntryPoint` or a `FUN_` prefix as generated. A name observed at
|
||||
multiple procedure entries retains its label constraint but cannot establish equal
|
||||
endpoints. Same-container records without a common coordinate context likewise
|
||||
cannot establish equality from matching labels.
|
||||
|
||||
Pairing preserves duplicate counts and considers alternative compatible
|
||||
pairings; array order never identifies an edge. Compatible records with unknown
|
||||
|
||||
@@ -19,11 +19,14 @@ session references as `left` and `right`:
|
||||
}
|
||||
```
|
||||
|
||||
Unique exact reported names are paired first. Optional `list_names` Evidence
|
||||
identifies dynamic/default names to exclude. If origin is unavailable, the
|
||||
existing automatic-name exclusion rule applies and each function retains that
|
||||
limitation. Exact names establish name correspondence, not unchanged code or
|
||||
original-source identity.
|
||||
Unique exact names with observed symbol origin are paired first. Inventory rows
|
||||
or optional `list_names` Evidence identify dynamic/default names to exclude.
|
||||
Unknown or conflicting origin does not establish name correspondence; complete
|
||||
independent body bytes remain eligible. Exact names establish name correspondence,
|
||||
not unchanged code or original-source identity. Ghidra publishes its symbol facts;
|
||||
Hopper, IDA and older imported dossiers retain unknown origin when no such facts
|
||||
were observed. Display spelling alone, including `EntryPoint` or `FUN_` prefixes,
|
||||
does not establish whether a name was generated.
|
||||
|
||||
Authentic Evidence selected for the wrong side, role, target, provider, profile
|
||||
or document returns an input error naming the selected field or observation
|
||||
|
||||
@@ -1038,3 +1038,10 @@ 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.
|
||||
|
||||
Native function names carry observed symbol metadata where available. Ghidra uses
|
||||
its function symbol's source and dynamic flags; Hopper and IDA leave origin
|
||||
unknown because this adapter has no verified origin API. Comparison and matching
|
||||
cannot use unknown labels as cross-artifact identity, even when their spelling
|
||||
looks meaningful. Original labels remain in Evidence. This qualification does not
|
||||
change address equality in a known common native coordinate context.
|
||||
@@ -7,7 +7,8 @@ import { createAnalysisProfile } from "../../domain/analysisProfile.js";
|
||||
import { functionDossierSchema } from "../../domain/hopperValues.js";
|
||||
import { procedureInfoSchema } from "../../domain/native/procedureInfo.js";
|
||||
import {
|
||||
nativeMatchingInventory,
|
||||
nativeMatchingNamedInventory as nativeMatchingInventory,
|
||||
nativeMatchingInventory as unqualifiedInventory,
|
||||
nativeMatchingDetails,
|
||||
nativeMatchingTarget,
|
||||
nativeMatchingProvider,
|
||||
@@ -81,10 +82,10 @@ it("retains transitive provenance without treating dependencies as native observ
|
||||
});
|
||||
|
||||
it("keeps symbol origin unknown when supplied metadata describes a different same-address alias", async () => {
|
||||
const left = nativeMatchingInventory("1", [
|
||||
const left = unqualifiedInventory("1", [
|
||||
{ address: "0x10", value: "reported_name" },
|
||||
]);
|
||||
const right = nativeMatchingInventory("2", [
|
||||
const right = unqualifiedInventory("2", [
|
||||
{ address: "0x20", value: "reported_name" },
|
||||
]);
|
||||
const alias = createEvidence(
|
||||
@@ -109,13 +110,14 @@ it("keeps symbol origin unknown when supplied metadata describes a different sam
|
||||
},
|
||||
);
|
||||
const { report } = await compare({ left, right, left_observations: [alias] });
|
||||
expect(report.summary.matched).toBe(1);
|
||||
expect(report.matches[0]?.left[0]?.limitations).toContain(
|
||||
"Symbol origin unavailable; exact-name matching uses the reported name and the existing automatic-name exclusion rule.",
|
||||
);
|
||||
expect(report.matches[0]?.left[0]?.evidence_links).toContain(
|
||||
alias.evidence_id,
|
||||
expect(report.summary.matched).toBe(0);
|
||||
const member = report.matches
|
||||
.flatMap((match) => match.left)
|
||||
.find((value) => value.address === "0x10");
|
||||
expect(member?.limitations).toContain(
|
||||
"Symbol origin unavailable; exact-name matching is unavailable. Independent complete body bytes remain eligible.",
|
||||
);
|
||||
expect(member?.evidence_links).toContain(alias.evidence_id);
|
||||
});
|
||||
|
||||
it("preserves contradictory symbol origins from inventory/name facts or multiple name observations, independently of byte correspondence", async () => {
|
||||
@@ -224,6 +226,13 @@ it.each(["", "ram:"])(
|
||||
{
|
||||
address: "0x500",
|
||||
name: "puts",
|
||||
symbol: {
|
||||
primary: true,
|
||||
dynamic: false,
|
||||
external: true,
|
||||
type: "function",
|
||||
source: "imported",
|
||||
},
|
||||
classification: null,
|
||||
body: { available: false, reason: "Synthetic external target" },
|
||||
},
|
||||
@@ -269,6 +278,19 @@ it.each(["", "ram:"])(
|
||||
report.matches[0]?.right[0]?.structural_candidate_sha256,
|
||||
);
|
||||
expect(report.matches[0]?.compare_functions_input).toBeNull();
|
||||
const unknownOrigin = dossier("1", "0x10");
|
||||
if (unknownOrigin.callees[0] === undefined)
|
||||
throw new Error("Missing fixture callee");
|
||||
unknownOrigin.callees[0].symbol = null;
|
||||
const unqualified = await compare({
|
||||
left,
|
||||
right,
|
||||
left_observations: [observe("1", "analyze_function", unknownOrigin)],
|
||||
right_observations: [observe("2", "analyze_function", r)],
|
||||
});
|
||||
expect(unqualified.report.source_coverage.left_structural_candidates).toBe(
|
||||
0,
|
||||
);
|
||||
const incomplete = await compare({
|
||||
left,
|
||||
right,
|
||||
@@ -637,8 +659,8 @@ it("does not use generated symbols and preserves literal named FUN_ symbols when
|
||||
})),
|
||||
});
|
||||
const { report } = await compare({
|
||||
left: nativeMatchingInventory("1", entries),
|
||||
right: nativeMatchingInventory("2", entries),
|
||||
left: unqualifiedInventory("1", entries),
|
||||
right: unqualifiedInventory("2", entries),
|
||||
left_observations: [names("1")],
|
||||
right_observations: [names("2")],
|
||||
});
|
||||
@@ -1027,3 +1049,25 @@ it.each(["left_observations", "right_observations"] as const)(
|
||||
expect(restored.report.summary.matched).toBe(1);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not propose exact-symbol correspondence from unqualified EntryPoint labels", async () => {
|
||||
const { report } = await compare({
|
||||
left: unqualifiedInventory("1", [{ address: "0x10", value: "EntryPoint" }]),
|
||||
right: unqualifiedInventory("2", [
|
||||
{ address: "0x20", value: "EntryPoint" },
|
||||
]),
|
||||
});
|
||||
expect(report.summary.matched).toBe(0);
|
||||
expect(
|
||||
report.matches.flatMap((match) => [...match.left, ...match.right]),
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "EntryPoint",
|
||||
limitations: expect.arrayContaining([
|
||||
"Symbol origin unavailable; exact-name matching is unavailable. Independent complete body bytes remain eligible.",
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { matchNativeFunctions } from "./NativeFunctionMatchingService.js";
|
||||
import { inspectAnalysisView } from "../analysisView/AnalysisViewService.js";
|
||||
import { nativeMatchingInventory } from "../../../tests/fixtures/nativeFunctionMatching.js";
|
||||
import { nativeMatchingNamedInventory as nativeMatchingInventory } from "../../../tests/fixtures/nativeFunctionMatching.js";
|
||||
import { analysisViewResultSchema } from "../../domain/analysisView/analysisView.js";
|
||||
|
||||
it("pages individual function facts without transferring an entire ambiguous alias group, preserving mandatory identity and source coverage", async () => {
|
||||
|
||||
@@ -7,6 +7,13 @@ const dossier = (text: string) =>
|
||||
procedure: {
|
||||
address: "0x1000",
|
||||
name: "main",
|
||||
symbol: {
|
||||
primary: true,
|
||||
dynamic: false,
|
||||
external: false,
|
||||
type: "function",
|
||||
source: "imported" as const,
|
||||
},
|
||||
classification: null,
|
||||
body: {
|
||||
available: false,
|
||||
|
||||
@@ -18,6 +18,13 @@ import type { RelationshipCoverage } from "./relationshipCoverage.js";
|
||||
const procedure = (address: string, name: string) => ({
|
||||
address,
|
||||
name,
|
||||
symbol: {
|
||||
primary: true,
|
||||
dynamic: false,
|
||||
external: false,
|
||||
type: "function",
|
||||
source: "imported" as const,
|
||||
},
|
||||
classification: null,
|
||||
body: {
|
||||
available: false as const,
|
||||
@@ -168,6 +175,7 @@ const keepsGeneratedNamesAmbiguous = (): void => {
|
||||
...dossier("return 0;", base),
|
||||
procedure: {
|
||||
...procedure(base, name),
|
||||
symbol: null,
|
||||
signature: "int helper(void)",
|
||||
locals: [],
|
||||
},
|
||||
@@ -265,7 +273,13 @@ describe("function comparison normalized identity", () => {
|
||||
keepsGeneratedNamesAmbiguous,
|
||||
);
|
||||
|
||||
it.each(["sub_deallocate", "sub_deadbeef_handler", "fcn.dispatch"])(
|
||||
it.each([
|
||||
"sub_deallocate",
|
||||
"sub_deadbeef_handler",
|
||||
"fcn.dispatch",
|
||||
"EntryPoint",
|
||||
"FUN_deadBEEF",
|
||||
])(
|
||||
"recognizes the explicit symbol %s without a generated-name prefix false positive",
|
||||
(name) => {
|
||||
const named = (base: "0x1000" | "0x2000") =>
|
||||
@@ -292,14 +306,15 @@ describe("function comparison normalized identity", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["sub_deadbeef", "fcn.00401000", "FUN_deadBEEF"])(
|
||||
"keeps %s as an address-derived name",
|
||||
it.each(["sub_deadbeef", "fcn.00401000", "FUN_deadBEEF", "EntryPoint"])(
|
||||
"keeps unqualified label %s unknown regardless of spelling",
|
||||
(name) => {
|
||||
const generated = (base: "0x1000" | "0x2000") =>
|
||||
functionDossierSchema.parse({
|
||||
...dossier("return 0;", base),
|
||||
procedure: {
|
||||
...procedure(base, name),
|
||||
symbol: null,
|
||||
name,
|
||||
signature: "int helper(void)",
|
||||
locals: [],
|
||||
@@ -319,21 +334,42 @@ describe("function comparison normalized identity", () => {
|
||||
|
||||
describe("function comparison address-derived call names", () => {
|
||||
it.each([
|
||||
{ name: "sub_deallocate", status: "unchanged", diagnostic: undefined },
|
||||
{
|
||||
name: "sub_deallocate",
|
||||
source: "imported",
|
||||
status: "unchanged",
|
||||
diagnostic: undefined,
|
||||
},
|
||||
{
|
||||
name: "sub_deadbeef",
|
||||
source: "default",
|
||||
status: "unknown",
|
||||
diagnostic: "Provider retained an unresolved call target.",
|
||||
},
|
||||
{ name: "FUN_00401000", status: "unknown", diagnostic: undefined },
|
||||
{ name: "FUN_initialize", status: "unchanged", diagnostic: undefined },
|
||||
{
|
||||
name: "FUN_00401000",
|
||||
source: "default",
|
||||
status: "unknown",
|
||||
diagnostic: undefined,
|
||||
},
|
||||
{
|
||||
name: "FUN_initialize",
|
||||
source: "imported",
|
||||
status: "unchanged",
|
||||
diagnostic: undefined,
|
||||
},
|
||||
])(
|
||||
"preserves the calls dimension for callee $name",
|
||||
({ name, status, diagnostic }) => {
|
||||
({ name, source, status, diagnostic }) => {
|
||||
const calling = (base: "0x1000" | "0x2000") =>
|
||||
functionDossierSchema.parse({
|
||||
...dossier("return helper();", base),
|
||||
callees: [procedure(base === "0x1000" ? "0x1010" : "0x2010", name)],
|
||||
callees: [
|
||||
{
|
||||
...procedure(base === "0x1000" ? "0x1010" : "0x2010", name),
|
||||
symbol: { ...procedure(base, name).symbol, source },
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = compareFunctions(
|
||||
functionSnapshotFixture(observe("6", calling("0x1000"))),
|
||||
@@ -996,6 +1032,7 @@ describe("function comparison identity and malformed CFG", () => {
|
||||
...dossier("return 0;", base),
|
||||
procedure: {
|
||||
...procedure(base, "sub_1000"),
|
||||
symbol: null,
|
||||
name: "sub_1000",
|
||||
signature: "int helper(void)",
|
||||
locals: [],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { hasStableNativeSymbol } from "./native/nativeSymbol.js";
|
||||
import { canonicalJson } from "./comparisonSemantics.js";
|
||||
import {
|
||||
compareFunctionRelationships,
|
||||
@@ -9,7 +10,6 @@ import type { FunctionSnapshot } from "./functionDossierEvidence.js";
|
||||
import {
|
||||
commentProjection,
|
||||
identityProjection,
|
||||
hasStableFunctionName,
|
||||
nameAndSwiftProjection,
|
||||
normalizeCfg,
|
||||
referencedStringProjection,
|
||||
@@ -550,8 +550,14 @@ const compareIdentity = (
|
||||
context: ComparisonContext,
|
||||
): FunctionDimension => {
|
||||
if (
|
||||
!hasStableFunctionName(left.dossier.procedure.name) ||
|
||||
!hasStableFunctionName(right.dossier.procedure.name)
|
||||
!hasStableNativeSymbol(
|
||||
left.dossier.procedure.name,
|
||||
left.dossier.procedure.symbol,
|
||||
) ||
|
||||
!hasStableNativeSymbol(
|
||||
right.dossier.procedure.name,
|
||||
right.dossier.procedure.symbol,
|
||||
)
|
||||
)
|
||||
return unresolvedDimension({
|
||||
dimension: "identity",
|
||||
@@ -559,7 +565,7 @@ const compareIdentity = (
|
||||
leftCount: null,
|
||||
rightCount: null,
|
||||
limitations: [
|
||||
"Empty or address-derived function names are not stable cross-version identity.",
|
||||
"Empty names, generated symbols and unknown symbol origins do not establish cross-version identity.",
|
||||
],
|
||||
});
|
||||
return compareValues(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { hasStableNativeSymbol } from "./native/nativeSymbol.js";
|
||||
import { canonicalJson } from "./comparisonSemantics.js";
|
||||
import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js";
|
||||
import { nativeAddressCoordinateInputSchema } from "./native/nativeAddress.js";
|
||||
@@ -20,7 +21,8 @@ export const functionMatch = (
|
||||
const leftName = left.dossier.procedure.name;
|
||||
const rightName = right.dossier.procedure.name;
|
||||
const stableNames =
|
||||
hasStableFunctionName(leftName) && hasStableFunctionName(rightName);
|
||||
hasStableNativeSymbol(leftName, left.dossier.procedure.symbol) &&
|
||||
hasStableNativeSymbol(rightName, right.dossier.procedure.symbol);
|
||||
if (stableNames && leftName === rightName)
|
||||
return {
|
||||
status: "matched",
|
||||
@@ -224,14 +226,6 @@ export const sorted = <T>(values: readonly T[]): readonly T[] =>
|
||||
.sort((left, right) => compareUnicodeCodePoints(left.json, right.json))
|
||||
.map(({ value }) => value);
|
||||
|
||||
/** Recognize address-derived procedure names without excluding named FUN_ symbols. */
|
||||
export const isAutoName = (name: string): boolean =>
|
||||
/^(?:sub_[0-9a-f]+|fcn\.[0-9a-f]+|FUN_[0-9a-f]+)$/iu.test(name);
|
||||
|
||||
/** A display label must be nonempty before it can constrain named correspondence. */
|
||||
export const hasStableFunctionName = (name: string): boolean =>
|
||||
name.length > 0 && !isAutoName(name);
|
||||
|
||||
const parseAddress = (value: string) => {
|
||||
const parsed = nativeAddressCoordinateInputSchema.safeParse(value);
|
||||
return parsed.success ? parsed.data : null;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { functionCoordinateContextsEqual } from "./native/functionCoordinateContext.js";
|
||||
import { canonicalJson } from "./comparisonSemantics.js";
|
||||
import type { FunctionSnapshot } from "./functionDossierEvidence.js";
|
||||
import { sorted } from "./functionComparisonNormalization.js";
|
||||
import {
|
||||
hasStableFunctionName,
|
||||
isAutoName,
|
||||
sorted,
|
||||
} from "./functionComparisonNormalization.js";
|
||||
hasStableNativeSymbol,
|
||||
nativeSymbolOrigin,
|
||||
} from "./native/nativeSymbol.js";
|
||||
import {
|
||||
referenceFactsCompatible,
|
||||
type ReferenceCompatibilityRow,
|
||||
@@ -30,6 +30,7 @@ type Endpoint =
|
||||
| "unparsed_address"
|
||||
| "missing_name"
|
||||
| "generated_name"
|
||||
| "symbol_origin"
|
||||
| "coordinate_context";
|
||||
readonly reported_address: string;
|
||||
readonly reported_name: string | null;
|
||||
@@ -115,13 +116,18 @@ const endpoint = (
|
||||
reported_address: address,
|
||||
reported_name: procedure?.name ?? null,
|
||||
};
|
||||
if (procedure === null || !hasStableFunctionName(procedure.name))
|
||||
if (
|
||||
procedure === null ||
|
||||
!hasStableNativeSymbol(procedure.name, procedure.symbol)
|
||||
)
|
||||
return {
|
||||
kind: "unknown",
|
||||
reason:
|
||||
procedure !== null && isAutoName(procedure.name)
|
||||
? "generated_name"
|
||||
: "missing_name",
|
||||
procedure === null || procedure.name.length === 0
|
||||
? "missing_name"
|
||||
: nativeSymbolOrigin(procedure.symbol) === "generated"
|
||||
? "generated_name"
|
||||
: "symbol_origin",
|
||||
reported_address: address,
|
||||
reported_name: procedure?.name ?? null,
|
||||
};
|
||||
@@ -272,7 +278,7 @@ const compareRelationshipFacts = (
|
||||
limitations.push(
|
||||
coordinateContext === "common"
|
||||
? "Relationship endpoint coordinates could not be parsed, so their identity remains unknown."
|
||||
: "Unnamed relationship endpoints and generated procedure names have no stable identity outside a common executable coordinate context.",
|
||||
: "Unnamed relationship endpoints, generated symbols and unknown symbol origins have no stable identity outside a common executable coordinate context.",
|
||||
);
|
||||
if (coordinateContext === "unknown" && (unknownIdentity || ambiguousIdentity))
|
||||
limitations.push(
|
||||
|
||||
@@ -19,6 +19,13 @@ export const ghidraFunctionBody = () => ({
|
||||
export const ghidraFunctionIdentity = () => ({
|
||||
address: "0x401000",
|
||||
name: "fixture_main",
|
||||
symbol: {
|
||||
primary: true,
|
||||
dynamic: false,
|
||||
external: false,
|
||||
type: "function",
|
||||
source: "imported" as const,
|
||||
},
|
||||
classification: ghidraFunctionClassification(),
|
||||
body: ghidraFunctionBody(),
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { nativeSymbolMetadataSchema } from "./native/nativeSymbol.js";
|
||||
import { z } from "zod";
|
||||
import { fixedTupleSchema } from "./fixedTupleSchema.js";
|
||||
import { addressCoordinate } from "./native/addressCoordinate.js";
|
||||
@@ -204,6 +205,7 @@ export const procedureIdentitySchema = z
|
||||
.object({
|
||||
address: z.string(),
|
||||
name: z.string(),
|
||||
symbol: nativeSymbolMetadataSchema.nullable().exactOptional(),
|
||||
classification: procedureClassificationSchema.nullable(),
|
||||
body: functionBodySchema,
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
functionBodyEntryAgrees,
|
||||
functionDossierSchema,
|
||||
} from "../hopperValues.js";
|
||||
import { isAutoName } from "../functionComparisonNormalization.js";
|
||||
import { nativeSymbolOrigin } from "./nativeSymbol.js";
|
||||
import {
|
||||
addressedValueSchema,
|
||||
nativeInventoryRowsSchemas,
|
||||
@@ -43,7 +43,12 @@ const matchingProcedureInfoSchema = procedureInfoSchema
|
||||
entrypoint: nativeAddressInputSchema,
|
||||
body: matchingBodySchema,
|
||||
})
|
||||
.transform(({ entrypoint, body }) => ({ address: entrypoint, body }));
|
||||
.transform(({ entrypoint, body, name, symbol }) => ({
|
||||
address: entrypoint,
|
||||
body,
|
||||
name,
|
||||
symbol,
|
||||
}));
|
||||
const matchingFunctionDossierSchema = functionDossierSchema.safeExtend({
|
||||
procedure: functionDossierSchema.shape.procedure.safeExtend({
|
||||
address: nativeAddressInputSchema,
|
||||
@@ -84,7 +89,11 @@ interface Facts {
|
||||
readonly bytes: FunctionByteCoverage;
|
||||
}
|
||||
|
||||
const addBody = (observation: Evidence, details: Facts["details"]): void => {
|
||||
const addBody = (
|
||||
observation: Evidence,
|
||||
details: Facts["details"],
|
||||
symbols: Facts["symbols"],
|
||||
): void => {
|
||||
const dossier =
|
||||
observation.operation === "analyze_function"
|
||||
? sourcePayload(
|
||||
@@ -96,6 +105,12 @@ const addBody = (observation: Evidence, details: Facts["details"]): void => {
|
||||
dossier?.procedure ??
|
||||
sourcePayload(matchingProcedureInfoSchema, observation.normalized_result);
|
||||
const address = procedure.address;
|
||||
if (procedure.symbol != null)
|
||||
addSymbol(
|
||||
{ address, value: procedure.name, symbol: procedure.symbol },
|
||||
observation.evidence_id,
|
||||
symbols,
|
||||
);
|
||||
if (!functionBodyEntryAgrees(procedure.body, address))
|
||||
throw new EvidenceIntegrityError(
|
||||
"Procedure body entry membership contradicts its address",
|
||||
@@ -125,6 +140,18 @@ const addBody = (observation: Evidence, details: Facts["details"]): void => {
|
||||
}
|
||||
detail.links.push(observation.evidence_id);
|
||||
};
|
||||
const addSymbol = (
|
||||
symbol: AddressedValue,
|
||||
evidenceId: string,
|
||||
symbols: Facts["symbols"],
|
||||
): void => {
|
||||
const address = symbol.address;
|
||||
let group = symbols.get(address);
|
||||
if (group === undefined)
|
||||
symbols.set(address, (group = { values: [], links: new Set() }));
|
||||
group.values.push(symbol);
|
||||
group.links.add(evidenceId);
|
||||
};
|
||||
const addNames = async (
|
||||
observation: Evidence,
|
||||
symbols: Facts["symbols"],
|
||||
@@ -135,12 +162,7 @@ const addNames = async (
|
||||
observation.normalized_result,
|
||||
)) {
|
||||
await checkpoint(128 + symbol.value.length * 2);
|
||||
const address = symbol.address;
|
||||
let group = symbols.get(address);
|
||||
if (group === undefined)
|
||||
symbols.set(address, (group = { values: [], links: new Set() }));
|
||||
group.values.push(symbol);
|
||||
group.links.add(observation.evidence_id);
|
||||
addSymbol(symbol, observation.evidence_id, symbols);
|
||||
}
|
||||
};
|
||||
const addBytes = async (
|
||||
@@ -184,18 +206,13 @@ const reportedSymbol = (
|
||||
...(known?.values.filter((symbol) => symbol.value === row.value) ?? []),
|
||||
];
|
||||
const generated = names.some(
|
||||
(symbol) =>
|
||||
symbol.symbol?.dynamic === true || symbol.symbol?.source === "default",
|
||||
(symbol) => nativeSymbolOrigin(symbol.symbol) === "generated",
|
||||
);
|
||||
const named = names.some(
|
||||
(symbol) =>
|
||||
symbol.symbol?.dynamic === false && symbol.symbol.source !== "default",
|
||||
(symbol) => nativeSymbolOrigin(symbol.symbol) === "named",
|
||||
);
|
||||
return {
|
||||
value:
|
||||
row.value.length === 0 || generated || (!named && isAutoName(row.value))
|
||||
? null
|
||||
: row.value,
|
||||
value: row.value.length === 0 || generated || !named ? null : row.value,
|
||||
origin:
|
||||
generated && named
|
||||
? "conflicting"
|
||||
@@ -242,7 +259,7 @@ const functionFact = async (
|
||||
);
|
||||
if (symbol.origin === "unknown")
|
||||
limitations.push(
|
||||
"Symbol origin unavailable; exact-name matching uses the reported name and the existing automatic-name exclusion rule.",
|
||||
"Symbol origin unavailable; exact-name matching is unavailable. Independent complete body bytes remain eligible.",
|
||||
);
|
||||
if (symbol.origin === "conflicting")
|
||||
limitations.push(
|
||||
@@ -296,7 +313,7 @@ export const readFunctionInventory = async (
|
||||
break;
|
||||
case "procedure_info":
|
||||
case "analyze_function":
|
||||
addBody(observation, facts.details);
|
||||
addBody(observation, facts.details, facts.symbols);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import type { Evidence } from "../evidence.js";
|
||||
import type { FunctionDossier } from "../hopperValues.js";
|
||||
import { digestCanonicalValue } from "../canonicalDigest.js";
|
||||
import {
|
||||
hasStableFunctionName,
|
||||
normalizeCfg,
|
||||
} from "../functionComparisonNormalization.js";
|
||||
import { normalizeCfg } from "../functionComparisonNormalization.js";
|
||||
import { hasStableNativeSymbol } from "./nativeSymbol.js";
|
||||
import { assessFunctionCallCoverage } from "../functionCallCoverage.js";
|
||||
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
||||
|
||||
@@ -29,7 +27,9 @@ export const functionStructuralCandidate = (
|
||||
if (
|
||||
cfg === null ||
|
||||
cfg.length === 0 ||
|
||||
dossier.callees.some((callee) => !hasStableFunctionName(callee.name))
|
||||
dossier.callees.some(
|
||||
(callee) => !hasStableNativeSymbol(callee.name, callee.symbol),
|
||||
)
|
||||
)
|
||||
return null;
|
||||
const calls = dossier.callees
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { nativeSymbolMetadataSchema } from "./nativeSymbol.js";
|
||||
import { analysisStringSchema } from "../hopperValues.js";
|
||||
|
||||
/** Canonical named native observations; same-address alternatives stay separate. */
|
||||
export const addressedValueSchema = z.object({
|
||||
address: z.string(),
|
||||
value: z.string(),
|
||||
symbol: z
|
||||
.object({
|
||||
primary: z.boolean(),
|
||||
dynamic: z.boolean(),
|
||||
external: z.boolean(),
|
||||
type: z.string(),
|
||||
source: z.enum(["default", "analysis", "ai", "imported", "user_defined"]),
|
||||
})
|
||||
.exactOptional(),
|
||||
symbol: nativeSymbolMetadataSchema.exactOptional(),
|
||||
objc_class_label: z.literal("hopper_legacy_class_object").exactOptional(),
|
||||
procedure: z
|
||||
.object({
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/** Native producer facts about the exact reported symbol, independent of display spelling. */
|
||||
export const nativeSymbolMetadataSchema = z.object({
|
||||
primary: z.boolean(),
|
||||
dynamic: z.boolean(),
|
||||
external: z.boolean(),
|
||||
type: z.string(),
|
||||
source: z.enum(["default", "analysis", "ai", "imported", "user_defined"]),
|
||||
});
|
||||
export type NativeSymbolMetadata = z.infer<typeof nativeSymbolMetadataSchema>;
|
||||
|
||||
export const nativeSymbolOrigin = (
|
||||
symbol: NativeSymbolMetadata | null | undefined,
|
||||
): "generated" | "named" | "unknown" =>
|
||||
symbol == null
|
||||
? "unknown"
|
||||
: symbol.dynamic || symbol.source === "default"
|
||||
? "generated"
|
||||
: "named";
|
||||
|
||||
/** Unknown origin never makes an arbitrary display label a cross-artifact identity. */
|
||||
export const hasStableNativeSymbol = (
|
||||
name: string,
|
||||
symbol: NativeSymbolMetadata | null | undefined,
|
||||
): boolean => name.length > 0 && nativeSymbolOrigin(symbol) === "named";
|
||||
@@ -1,3 +1,4 @@
|
||||
import { nativeSymbolMetadataSchema } from "./nativeSymbol.js";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
functionBodySchema,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
/** Provider-neutral procedure metadata, including exact body ranges. */
|
||||
export const procedureInfoSchema = z.object({
|
||||
name: z.string(),
|
||||
symbol: nativeSymbolMetadataSchema.nullable().exactOptional(),
|
||||
entrypoint: z.string(),
|
||||
basicblock_count: z.number().int().min(0),
|
||||
length: z.number().min(0),
|
||||
|
||||
@@ -71,6 +71,7 @@ const unknownBody = {
|
||||
const identity = (fn: IdaFunction) => ({
|
||||
address: fn.address,
|
||||
name: fn.name,
|
||||
symbol: null,
|
||||
classification: null,
|
||||
body: unknownBody,
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import {
|
||||
nativeMatchingInventory,
|
||||
nativeMatchingNamedInventory as nativeMatchingInventory,
|
||||
nativeMatchingDetails,
|
||||
nativeMatchingTarget,
|
||||
} from "../../fixtures/nativeFunctionMatching.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { NativeSymbolMetadata } from "../../../src/domain/native/nativeSymbol.js";
|
||||
import type { Client } from "@modelcontextprotocol/client";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
|
||||
@@ -44,9 +45,20 @@ const createHarness = async () => {
|
||||
return harness;
|
||||
};
|
||||
|
||||
const procedure = (address: string, name: string) => ({
|
||||
const procedure = (
|
||||
address: string,
|
||||
name: string,
|
||||
symbol: NativeSymbolMetadata | null = {
|
||||
primary: true,
|
||||
dynamic: false,
|
||||
external: false,
|
||||
type: "function",
|
||||
source: "imported",
|
||||
},
|
||||
) => ({
|
||||
address,
|
||||
name,
|
||||
symbol,
|
||||
classification: null,
|
||||
body: dossier.procedure.body,
|
||||
});
|
||||
@@ -58,6 +70,7 @@ const reference = (
|
||||
options: {
|
||||
readonly sourceName?: string;
|
||||
readonly targetName?: string | null;
|
||||
readonly targetSymbol?: NativeSymbolMetadata | null;
|
||||
readonly sourceEntryAddress?: string;
|
||||
readonly targetEntryAddress?: string;
|
||||
} = {},
|
||||
@@ -77,6 +90,7 @@ const reference = (
|
||||
: procedure(
|
||||
options.targetEntryAddress ?? targetAddress,
|
||||
options.targetName ?? "helper",
|
||||
options.targetSymbol,
|
||||
),
|
||||
kind:
|
||||
classification === "unknown"
|
||||
@@ -109,11 +123,12 @@ const evidence = (
|
||||
readonly incoming?: readonly Reference[];
|
||||
readonly procedureAddress?: string;
|
||||
readonly procedureName?: string;
|
||||
readonly procedureSymbol?: NativeSymbolMetadata | null;
|
||||
readonly architecture?: "arm64" | "x86_64" | null;
|
||||
readonly format?: "mach-o" | "mach-o-universal";
|
||||
readonly profile?: AnalysisProfileCommitment | null;
|
||||
readonly callees?: readonly ReturnType<typeof procedure>[];
|
||||
readonly callers?: readonly ReturnType<typeof procedure>[];
|
||||
readonly callees?: readonly (typeof dossier.callees)[number][];
|
||||
readonly callers?: readonly (typeof dossier.callers)[number][];
|
||||
} = {},
|
||||
) =>
|
||||
createEvidence(
|
||||
@@ -138,6 +153,10 @@ const evidence = (
|
||||
...dossier.procedure,
|
||||
address: options.procedureAddress ?? dossier.procedure.address,
|
||||
name: options.procedureName ?? dossier.procedure.name,
|
||||
symbol:
|
||||
options.procedureSymbol === undefined
|
||||
? dossier.procedure.symbol
|
||||
: options.procedureSymbol,
|
||||
},
|
||||
callees: [...(options.callees ?? [])],
|
||||
callers: [...(options.callers ?? [])],
|
||||
@@ -311,16 +330,24 @@ it("preserves reference identity uncertainty and same-artifact coordinates", asy
|
||||
expectReferences(await compare(client, namedRight, namedLeft), "unchanged");
|
||||
});
|
||||
|
||||
it("keeps generated and ambiguous procedure names from proving cross-artifact identity", async () => {
|
||||
it("keeps unqualified and ambiguous procedure names from proving cross-artifact identity", async () => {
|
||||
const { client } = await createHarness();
|
||||
for (const [index, name] of ["sub_1010", "fcn.1010", "FUN_1010"].entries()) {
|
||||
const left = evidence(`auto-left-${index}`, "1", [
|
||||
reference("0x1000", "0x1010", "call", { targetName: name }),
|
||||
reference("0x1000", "0x1010", "call", {
|
||||
targetName: name,
|
||||
targetSymbol: null,
|
||||
}),
|
||||
]);
|
||||
const right = evidence(
|
||||
`auto-right-${index}`,
|
||||
"2",
|
||||
[reference("0x2000", "0x2020", "call", { targetName: name })],
|
||||
[
|
||||
reference("0x2000", "0x2020", "call", {
|
||||
targetName: name,
|
||||
targetSymbol: null,
|
||||
}),
|
||||
],
|
||||
{ procedureAddress: "0x2000" },
|
||||
);
|
||||
expectReferences(await compare(client, left, right), "unknown");
|
||||
@@ -332,7 +359,12 @@ it("keeps generated and ambiguous procedure names from proving cross-artifact id
|
||||
evidence(
|
||||
`auto-different-${index}`,
|
||||
"3",
|
||||
[reference("0x2000", "0x2020", "call", { targetName: otherName })],
|
||||
[
|
||||
reference("0x2000", "0x2020", "call", {
|
||||
targetName: otherName,
|
||||
targetSymbol: null,
|
||||
}),
|
||||
],
|
||||
{ procedureAddress: "0x2000" },
|
||||
),
|
||||
),
|
||||
@@ -370,10 +402,12 @@ it("keeps generated and ambiguous procedure names from proving cross-artifact id
|
||||
client,
|
||||
evidence("generated-function-left", "7", [], {
|
||||
procedureName: "sub_1000",
|
||||
procedureSymbol: null,
|
||||
}),
|
||||
evidence("generated-function-right", "8", [], {
|
||||
procedureAddress: "0x2000",
|
||||
procedureName: "FUN_2000",
|
||||
procedureSymbol: null,
|
||||
}),
|
||||
);
|
||||
expect.soft(generatedFunctions.result.function_match).toMatchObject({
|
||||
@@ -823,7 +857,7 @@ it("retains caller entries and independent contradictions beside unknown endpoin
|
||||
callees: [
|
||||
procedure("0x1010", name),
|
||||
procedure("0x1010", name),
|
||||
procedure("0x1020", "sub_1020"),
|
||||
procedure("0x1020", "sub_1020", null),
|
||||
],
|
||||
});
|
||||
const mixed = await compare(
|
||||
@@ -876,3 +910,67 @@ it("keeps equal coordinates in distinct same-architecture universal slices unkno
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it.each([null, "default", "imported"] as const)(
|
||||
"native comparison uses observed symbol origin %s for EntryPoint labels",
|
||||
async (source) => {
|
||||
const harness = await createHarness();
|
||||
const edge = (base: "0x1000" | "0x2000", name: string) => {
|
||||
const value = reference(
|
||||
base,
|
||||
base === "0x1000" ? "0x1010" : "0x2010",
|
||||
"call",
|
||||
{ targetName: name },
|
||||
);
|
||||
if (value.target_procedure === null)
|
||||
throw new Error("Missing fixture target");
|
||||
value.target_procedure.symbol =
|
||||
source === null
|
||||
? null
|
||||
: {
|
||||
primary: true,
|
||||
dynamic: false,
|
||||
external: false,
|
||||
type: "function",
|
||||
source,
|
||||
};
|
||||
return value;
|
||||
};
|
||||
const leftEdge = edge(
|
||||
"0x1000",
|
||||
source === "imported" ? "EntryPoint" : "_main",
|
||||
);
|
||||
const rightEdge = edge("0x2000", "EntryPoint");
|
||||
if (
|
||||
leftEdge.target_procedure === null ||
|
||||
rightEdge.target_procedure === null
|
||||
)
|
||||
throw new Error("Missing fixture target");
|
||||
const compared = await compare(
|
||||
harness.client,
|
||||
evidence("origin-left", "1", [leftEdge], {
|
||||
callees: [leftEdge.target_procedure],
|
||||
}),
|
||||
evidence("origin-right", "2", [rightEdge], {
|
||||
callees: [rightEdge.target_procedure],
|
||||
}),
|
||||
);
|
||||
const status = source === "imported" ? "unchanged" : "unknown";
|
||||
expectReferences(compared, status);
|
||||
expect(
|
||||
compared.result.dimensions.find((value) => value.dimension === "calls"),
|
||||
).toMatchObject({ status });
|
||||
await verifyDimension(
|
||||
harness.client,
|
||||
compared.evidence.evidence_id,
|
||||
source === "imported" ? "pass" : "unknown",
|
||||
"references",
|
||||
);
|
||||
await verifyDimension(
|
||||
harness.client,
|
||||
compared.evidence.evidence_id,
|
||||
source === "imported" ? "pass" : "unknown",
|
||||
"calls",
|
||||
);
|
||||
},
|
||||
);
|
||||
+22
@@ -1,6 +1,7 @@
|
||||
import { createEvidence, type Evidence } from "../../src/domain/evidence.js";
|
||||
import { jsonValueSchema } from "../../src/domain/jsonValue.js";
|
||||
import { FUNCTION_COMPARISON_EXAMPLE } from "../../src/contracts/functionComparisonExample.js";
|
||||
import type { NativeSymbolMetadata } from "../../src/domain/native/nativeSymbol.js";
|
||||
import { functionDossierSchema } from "../../src/domain/hopperValues.js";
|
||||
|
||||
/** Explicit synthetic producer facts; they do not qualify any real provider. */
|
||||
@@ -26,6 +27,27 @@ export const nativeMatchingInventory = (
|
||||
result: jsonValueSchema.parse(entries),
|
||||
evidenceLinks: [...evidenceLinks],
|
||||
});
|
||||
/** Explicit imported-origin fixture for tests that select exact-symbol correspondence. */
|
||||
export const nativeMatchingNamedInventory = (
|
||||
side: "1" | "2",
|
||||
entries: readonly { address: string; value: string }[],
|
||||
evidenceLinks: readonly string[] = [],
|
||||
) =>
|
||||
createEvidence(nativeMatchingTarget(side), nativeMatchingProvider, {
|
||||
operation: "list_procedures",
|
||||
parameters: {},
|
||||
result: entries.map((entry) => ({
|
||||
...entry,
|
||||
symbol: {
|
||||
primary: true,
|
||||
dynamic: false,
|
||||
external: false,
|
||||
type: "function",
|
||||
source: "imported",
|
||||
} satisfies NativeSymbolMetadata,
|
||||
})),
|
||||
evidenceLinks: [...evidenceLinks],
|
||||
});
|
||||
export const nativeMatchingBody = (address: string, bytes: number) => ({
|
||||
available: true as const,
|
||||
provenance: "explicit synthetic body",
|
||||
|
||||
Reference in new issue
Block a user