mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(macho)!: preserve ambiguous chained import names
Keep the exact producer expression and report unknown symbol identity when chained-import text cannot distinguish a binding addend from a literal name suffix. Fixes #6648 BREAKING CHANGE: inspect_macho import names can be null when identity is ambiguous. Use raw_name for the observed expression and name_status to distinguish it from an exact symbol name.
This commit is contained in:
@@ -6,6 +6,15 @@ Use a native provider to discover functions, inspect instructions and pseudocode
|
||||
follow references, and examine database types. Static resources and runtime
|
||||
observation complement that analysis when they answer a specific question.
|
||||
|
||||
`inspect_macho` reports build platforms from the selected slice. Legacy
|
||||
`LC_VERSION_MIN_IPHONEOS`, `LC_VERSION_MIN_TVOS` and
|
||||
`LC_VERSION_MIN_WATCHOS` commands are interpreted with the Mach-O CPU type, so
|
||||
Intel slices report simulator platforms while device CPU slices keep the
|
||||
device platform. A chained-import row ending in `+0x...` is ambiguous in
|
||||
dyld's text output: it may be an addend or part of a literal symbol name. REA
|
||||
keeps the full expression in `raw_name`, sets `name` and `addend` to null, and
|
||||
marks `name_status: "ambiguous"`; it does not synthesize a symbol identity.
|
||||
|
||||
## Start with a function
|
||||
|
||||
Choose an installed provider from [setup](installation.md#hopper). For Ghidra:
|
||||
|
||||
@@ -77,6 +77,19 @@ const symbolSchema = z.object({
|
||||
reexport: z.boolean().nullable(),
|
||||
source: z.string().nullable(),
|
||||
});
|
||||
const importSymbolSchema = z.object({
|
||||
/** Resolved symbol identity; null when the textual row has multiple parses. */
|
||||
name: z.string().nullable(),
|
||||
/** Exact import text retained when a chained suffix makes identity ambiguous. */
|
||||
raw_name: z.string().nullable().default(null),
|
||||
name_status: z.enum(["exact", "ambiguous"]).nullable().default(null),
|
||||
/** Addend is unknown when only the ambiguous text expression is available. */
|
||||
addend: hexAddress.nullable().default(null),
|
||||
address: hexAddress.nullable(),
|
||||
weak: z.boolean().nullable(),
|
||||
reexport: z.boolean().nullable(),
|
||||
source: z.string().nullable(),
|
||||
});
|
||||
const dependencySchema = z.object({
|
||||
path: z.string(),
|
||||
kind: z.string(),
|
||||
@@ -132,7 +145,7 @@ export const inspectMachoSchema = z.object({
|
||||
build_metadata: coverage(buildMetadataSchema),
|
||||
load_commands: coverage(loadCommandSchema),
|
||||
dependencies: coverage(dependencySchema),
|
||||
imports: coverage(symbolSchema),
|
||||
imports: coverage(importSymbolSchema),
|
||||
exports: coverage(symbolSchema),
|
||||
segments: coverage(segmentSchema),
|
||||
provenance: z.array(nativeCommandInvocationSchema),
|
||||
|
||||
@@ -246,9 +246,18 @@ const normalizeMacho = (
|
||||
const dyldExportsCapture = byTool("dyld_info", 1);
|
||||
const missingImportsUuid = isMissingUuidDyldFailure(dyldImportsCapture);
|
||||
const missingExportsUuid = isMissingUuidDyldFailure(dyldExportsCapture);
|
||||
const hasChainedFixups = load.commands.some(
|
||||
({ kind }) => kind === "LC_DYLD_CHAINED_FIXUPS",
|
||||
);
|
||||
const imports = missingImportsUuid
|
||||
? []
|
||||
: parseDyldSymbols(sliceOutput(dyldImportsCapture), "imports");
|
||||
: parseDyldSymbols(
|
||||
sliceOutput(dyldImportsCapture),
|
||||
"imports",
|
||||
null,
|
||||
new Set(),
|
||||
hasChainedFixups,
|
||||
);
|
||||
const imageBase =
|
||||
load.segments.find(
|
||||
(segment) => segment.file_offset === 0 && (segment.file_size ?? 0) > 0,
|
||||
|
||||
+101
-64
@@ -1,92 +1,129 @@
|
||||
interface ParsedDyldSymbol {
|
||||
readonly name: string;
|
||||
interface ParsedDyldSymbolFields {
|
||||
readonly address: string | null;
|
||||
readonly weak: boolean | null;
|
||||
readonly reexport: boolean | null;
|
||||
readonly source: string | null;
|
||||
}
|
||||
|
||||
interface ParsedDyldImport extends ParsedDyldSymbolFields {
|
||||
readonly name: string | null;
|
||||
/** Full producer text when the symbol/addend split cannot be established. */
|
||||
readonly raw_name: string | null;
|
||||
readonly name_status: "exact" | "ambiguous";
|
||||
readonly addend: string | null;
|
||||
}
|
||||
|
||||
interface ParsedDyldExport extends ParsedDyldSymbolFields {
|
||||
readonly name: string;
|
||||
/** The source library's name for a symbol it re-exports under another name. */
|
||||
readonly sourceSymbol?: string;
|
||||
}
|
||||
|
||||
/** Decode Apple dyld inventory rows, projecting export offsets to VM addresses. */
|
||||
export const parseDyldSymbols = (
|
||||
export function parseDyldSymbols(
|
||||
output: string,
|
||||
mode: "imports",
|
||||
imageBase?: string | null,
|
||||
knownExportNames?: ReadonlySet<string>,
|
||||
hasChainedFixups?: boolean,
|
||||
): ParsedDyldImport[];
|
||||
export function parseDyldSymbols(
|
||||
output: string,
|
||||
mode: "exports",
|
||||
imageBase?: string | null,
|
||||
knownExportNames?: ReadonlySet<string>,
|
||||
): ParsedDyldExport[];
|
||||
export function parseDyldSymbols(
|
||||
output: string,
|
||||
mode: "imports" | "exports",
|
||||
imageBase: string | null = null,
|
||||
knownExportNames: ReadonlySet<string> = new Set(),
|
||||
) => {
|
||||
hasChainedFixups = false,
|
||||
): (ParsedDyldImport | ParsedDyldExport)[] {
|
||||
const offsets = /^\s*offset\s+symbol\s*$/mu.test(output);
|
||||
// dyld_info ends rows with LF. A carriage return before it belongs to the
|
||||
// symbol name, so export rows keep their trailing text.
|
||||
return output.split("\n").flatMap<ParsedDyldSymbol>((rawLine) => {
|
||||
const line = mode === "exports" ? rawLine.trimStart() : rawLine.trim();
|
||||
if (mode === "imports") {
|
||||
const imported =
|
||||
/^(?:0x[\da-f]+\s+)?(.+?)(?:\s+\[([^\]]+)\])?\s+\(from (.+)\)$/iu.exec(
|
||||
line,
|
||||
);
|
||||
if (imported?.[1] !== undefined)
|
||||
return output
|
||||
.split("\n")
|
||||
.flatMap<ParsedDyldImport | ParsedDyldExport>((rawLine) => {
|
||||
const line = mode === "exports" ? rawLine.trimStart() : rawLine.trim();
|
||||
if (mode === "imports") {
|
||||
const imported =
|
||||
/^(?:0x[\da-f]+\s+)?(.+?)(?:\s+\[([^\]]+)\])?\s+\(from (.+)\)$/iu.exec(
|
||||
line,
|
||||
);
|
||||
if (imported?.[1] !== undefined) {
|
||||
const ambiguousSuffix = hasChainedFixups
|
||||
? /^(.*)\+0x[\da-f]+$/iu.test(imported[1])
|
||||
: false;
|
||||
return [
|
||||
{
|
||||
name: ambiguousSuffix ? null : imported[1],
|
||||
raw_name: ambiguousSuffix ? imported[1] : null,
|
||||
name_status: ambiguousSuffix ? "ambiguous" : "exact",
|
||||
addend: null,
|
||||
address: null,
|
||||
weak: /\bweak-import\b/u.test(imported[2] ?? "") ? true : null,
|
||||
reexport: null,
|
||||
source: imported[3] ?? null,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
// dyld_info prints `_sym (from lib)`, or `_new (_old from lib)` when the
|
||||
// source library names the symbol differently. Split the trailing
|
||||
// parenthesized suffix with string scans so a library path containing
|
||||
// " from ", or a source symbol containing parentheses such as
|
||||
// `operator()`, cannot pull the match into nested lazy quantifiers.
|
||||
const reexport = parseReexportRow(line, knownExportNames);
|
||||
if (reexport !== null)
|
||||
return [
|
||||
{
|
||||
name: imported[1],
|
||||
name: reexport.name,
|
||||
address: null,
|
||||
weak: /\bweak-import\b/u.test(imported[2] ?? "") ? true : null,
|
||||
reexport: null,
|
||||
source: imported[3] ?? null,
|
||||
weak: null,
|
||||
reexport: true,
|
||||
source: reexport.source,
|
||||
...(reexport.sourceSymbol === undefined
|
||||
? {}
|
||||
: { sourceSymbol: reexport.sourceSymbol }),
|
||||
},
|
||||
];
|
||||
return [];
|
||||
}
|
||||
// dyld_info prints `_sym (from lib)`, or `_new (_old from lib)` when the
|
||||
// source library names the symbol differently. Split the trailing
|
||||
// parenthesized suffix with string scans so a library path containing
|
||||
// " from ", or a source symbol containing parentheses such as
|
||||
// `operator()`, cannot pull the match into nested lazy quantifiers.
|
||||
const reexport = parseReexportRow(line, knownExportNames);
|
||||
if (reexport !== null)
|
||||
const exported = /^(0x[\da-f]+)\s+(.+?)(?:\s+\[([^\]]+)\])?$/isu.exec(
|
||||
line,
|
||||
);
|
||||
if (exported?.[1] === undefined || exported[2] === undefined) return [];
|
||||
const rawName = /^0x[\da-f]+\s+(.+)$/isu.exec(line)?.[1];
|
||||
const literalName =
|
||||
rawName !== undefined && knownExportNames.has(rawName);
|
||||
// If nm establishes both interpretations, the textual row cannot bind an address or flags.
|
||||
// Let the caller retain the exact nm names without guessing which one owns this row.
|
||||
if (
|
||||
literalName &&
|
||||
exported[3] !== undefined &&
|
||||
knownExportNames.has(exported[2])
|
||||
)
|
||||
return [];
|
||||
const annotation = literalName ? undefined : exported[3];
|
||||
const absolute = /\babsolute\b/u.test(annotation ?? "");
|
||||
const address =
|
||||
offsets && !absolute
|
||||
? imageBase === null
|
||||
? null
|
||||
: `0x${(BigInt(imageBase) + BigInt(exported[1])).toString(16)}`
|
||||
: canonicalHex(exported[1]);
|
||||
return [
|
||||
{
|
||||
name: reexport.name,
|
||||
address: null,
|
||||
weak: null,
|
||||
reexport: true,
|
||||
source: reexport.source,
|
||||
...(reexport.sourceSymbol === undefined
|
||||
? {}
|
||||
: { sourceSymbol: reexport.sourceSymbol }),
|
||||
name: literalName ? rawName : exported[2],
|
||||
address,
|
||||
weak: /\bweak-def\b/u.test(annotation ?? "") ? true : null,
|
||||
reexport: false,
|
||||
source: null,
|
||||
},
|
||||
];
|
||||
const exported = /^(0x[\da-f]+)\s+(.+?)(?:\s+\[([^\]]+)\])?$/isu.exec(line);
|
||||
if (exported?.[1] === undefined || exported[2] === undefined) return [];
|
||||
const rawName = /^0x[\da-f]+\s+(.+)$/isu.exec(line)?.[1];
|
||||
const literalName = rawName !== undefined && knownExportNames.has(rawName);
|
||||
// If nm establishes both interpretations, the textual row cannot bind an address or flags.
|
||||
// Let the caller retain the exact nm names without guessing which one owns this row.
|
||||
if (
|
||||
literalName &&
|
||||
exported[3] !== undefined &&
|
||||
knownExportNames.has(exported[2])
|
||||
)
|
||||
return [];
|
||||
const annotation = literalName ? undefined : exported[3];
|
||||
const absolute = /\babsolute\b/u.test(annotation ?? "");
|
||||
const address =
|
||||
offsets && !absolute
|
||||
? imageBase === null
|
||||
? null
|
||||
: `0x${(BigInt(imageBase) + BigInt(exported[1])).toString(16)}`
|
||||
: canonicalHex(exported[1]);
|
||||
return [
|
||||
{
|
||||
name: literalName ? rawName : exported[2],
|
||||
address,
|
||||
weak: /\bweak-def\b/u.test(annotation ?? "") ? true : null,
|
||||
reexport: false,
|
||||
source: null,
|
||||
},
|
||||
];
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const canonicalHex = (value: string | undefined): string | null =>
|
||||
value === undefined ? null : `0x${BigInt(value).toString(16)}`;
|
||||
|
||||
@@ -299,6 +299,9 @@ it("retains native inventory facts from captured Apple tool output", async () =>
|
||||
).toEqual([
|
||||
{
|
||||
name: "__Block_copy",
|
||||
raw_name: null,
|
||||
name_status: "exact",
|
||||
addend: null,
|
||||
address: null,
|
||||
weak: null,
|
||||
reexport: null,
|
||||
@@ -306,6 +309,9 @@ it("retains native inventory facts from captured Apple tool output", async () =>
|
||||
},
|
||||
{
|
||||
name: "_objc_bp_assist_cfg_np",
|
||||
raw_name: null,
|
||||
name_status: "exact",
|
||||
addend: null,
|
||||
address: null,
|
||||
weak: true,
|
||||
reexport: null,
|
||||
@@ -335,6 +341,56 @@ it("retains native inventory facts from captured Apple tool output", async () =>
|
||||
});
|
||||
});
|
||||
|
||||
it("reports a chained bind addend separately in native inspection", async () => {
|
||||
const outputs: Record<string, string> = {};
|
||||
for (const tool of ["lipo", "otool", "nm", "dwarfdump", "vtool"])
|
||||
outputs[tool] = await fixture(`dyld-inventory/${tool}.txt`);
|
||||
outputs.otool = withOtoolMachHeader(
|
||||
`${outputs.otool ?? ""}Load command 17
|
||||
cmd LC_DYLD_CHAINED_FIXUPS
|
||||
cmdsize 16
|
||||
dataoff 100
|
||||
datasize 20
|
||||
`,
|
||||
);
|
||||
outputs["dyld_info:-imports"] =
|
||||
"-imports:\n 0x0000 _fixture_data (from libFixture)\n 0x0001 _fixture_data+0x4 (from libFixture)\n";
|
||||
outputs["dyld_info:-exports"] = await fixture("dyld-inventory/exports.txt");
|
||||
const client = new NativeMacOSProvider(
|
||||
{},
|
||||
new FixtureRunner(outputs),
|
||||
"darwin",
|
||||
).createClient(machoTarget("/private/fixture"));
|
||||
const execution = await client.execute("inspect_macho", {});
|
||||
if (!execution.ok) throw execution.error;
|
||||
expect(execution.value.result).toMatchObject({
|
||||
imports: {
|
||||
items: [
|
||||
{
|
||||
name: "_fixture_data",
|
||||
raw_name: null,
|
||||
name_status: "exact",
|
||||
addend: null,
|
||||
address: null,
|
||||
weak: null,
|
||||
reexport: null,
|
||||
source: "libFixture",
|
||||
},
|
||||
{
|
||||
name: null,
|
||||
raw_name: "_fixture_data+0x4",
|
||||
name_status: "ambiguous",
|
||||
addend: null,
|
||||
address: null,
|
||||
weak: null,
|
||||
reexport: null,
|
||||
source: "libFixture",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reports a renamed re-export once under its exported name", async () => {
|
||||
const client = new NativeMacOSProvider(
|
||||
{},
|
||||
@@ -414,6 +470,9 @@ it("retains symbol names containing spaces in dyld inventory rows", () => {
|
||||
).toEqual([
|
||||
{
|
||||
name: "_entry with space",
|
||||
raw_name: null,
|
||||
name_status: "exact",
|
||||
addend: null,
|
||||
address: null,
|
||||
weak: null,
|
||||
reexport: null,
|
||||
@@ -624,6 +683,72 @@ it("keeps true weak annotations when nm establishes the unannotated name", () =>
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains chained import suffixes as ambiguous raw text", () => {
|
||||
expect(
|
||||
parseDyldSymbols(
|
||||
"0x0000 _fixture_data (from libFixture)\n0x0001 _fixture_data+0x4 (from libFixture)\n0x0002 _fixture_weak+0x8 [weak-import] (from libFixture)",
|
||||
"imports",
|
||||
null,
|
||||
new Set(),
|
||||
true,
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
name: "_fixture_data",
|
||||
raw_name: null,
|
||||
name_status: "exact",
|
||||
addend: null,
|
||||
address: null,
|
||||
weak: null,
|
||||
reexport: null,
|
||||
source: "libFixture",
|
||||
},
|
||||
{
|
||||
name: null,
|
||||
raw_name: "_fixture_data+0x4",
|
||||
name_status: "ambiguous",
|
||||
addend: null,
|
||||
address: null,
|
||||
weak: null,
|
||||
reexport: null,
|
||||
source: "libFixture",
|
||||
},
|
||||
{
|
||||
name: null,
|
||||
raw_name: "_fixture_weak+0x8",
|
||||
name_status: "ambiguous",
|
||||
addend: null,
|
||||
address: null,
|
||||
weak: true,
|
||||
reexport: null,
|
||||
source: "libFixture",
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
parseDyldSymbols(
|
||||
"0x0001 _literal+0x4 (from libFixture)",
|
||||
"imports",
|
||||
null,
|
||||
new Set(),
|
||||
true,
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
name: null,
|
||||
raw_name: "_literal+0x4",
|
||||
name_status: "ambiguous",
|
||||
addend: null,
|
||||
address: null,
|
||||
weak: null,
|
||||
reexport: null,
|
||||
source: "libFixture",
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
parseDyldSymbols("0x0001 _literal+0x4 (from libFixture)", "imports"),
|
||||
).toMatchObject([{ name: "_literal+0x4", name_status: "exact" }]);
|
||||
});
|
||||
|
||||
it("leaves an ambiguous short and literal name to the caller's nm inventory", () => {
|
||||
expect(
|
||||
parseDyldSymbols(
|
||||
|
||||
Reference in New Issue
Block a user