fix(macho): preserve leading spaces in export identities

This commit is contained in:
morluto committed 2026-10-27 08:34:42 +08:00
1 parent 0781c3e99c
commit 54fbfacfb1
3 files changed
+62 -13

No files matched your search

+6 -2
View File
@@ -17,7 +17,11 @@ import {
MACHO_ARCHITECTURE_POLICIES,
selectMachoArchitectureName,
} from "./MachoArchitectureSelection.js";
import { parseDyldExports, parseDyldImports } from "./parsers/dyldInfo.js";
import {
parseDyldExportAddressRow,
parseDyldExports,
parseDyldImports,
} from "./parsers/dyldInfo.js";
import { parseLipoCapture, type LipoArchitecture } from "./parsers/lipo.js";
import { withoutEchoedPathHeader } from "./parsers/echoedPath.js";
import { parseOtoolLoadCommands } from "./parsers/otool.js";
@@ -476,7 +480,7 @@ const namesEveryExport = (
const rows = parseDyldExports(line, null, names);
// The parser withholds a row only when nm names both of its readings.
if (rows.length === 0)
return names.has(/^0x[\da-f]+\s+(.+)$/isu.exec(line)?.[1] ?? "");
return names.has(parseDyldExportAddressRow(line)?.text ?? "");
return rows.length === 1 && names.has(rows[0]!.name);
});
};
+23 -11
View File
@@ -103,6 +103,16 @@ export const parseDyldImports = (
return { items, needsNameInventory };
};
/** Apple prints two spaces after the address; one-space captures remain readable. */
export const parseDyldExportAddressRow = (
line: string,
): { readonly address: string; readonly text: string } | null => {
const match = /^(0x[\da-f]+)(?: {2}|[ \t])(.+)$/isu.exec(line);
return match?.[1] === undefined || match[2] === undefined
? null
: { address: match[1], text: match[2] };
};
/** Decode Apple dyld export rows, projecting export offsets to VM addresses. */
export const parseDyldExports = (
output: string,
@@ -133,29 +143,31 @@ export const parseDyldExports = (
: { sourceSymbol: reexport.sourceSymbol }),
},
];
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);
const row = parseDyldExportAddressRow(line);
if (row === null) return [];
const exported = /^(.+?)(?:\s+\[([^\]]+)\])?$/su.exec(row.text);
if (exported?.[1] === undefined) return [];
const rawName = row.text;
const literalName = 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])
exported[2] !== undefined &&
knownExportNames.has(exported[1])
)
return [];
const annotation = literalName ? undefined : exported[3];
const annotation = literalName ? undefined : exported[2];
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]);
: `0x${(BigInt(imageBase) + BigInt(row.address)).toString(16)}`
: canonicalHex(row.address);
return [
{
name: literalName ? rawName : exported[2],
name: literalName ? rawName : exported[1],
address,
weak: /\bweak-def\b/u.test(annotation ?? "") ? true : null,
reexport: false,
@@ -185,7 +197,7 @@ const parseReexportRow = (
line: string,
knownNames: ReadonlySet<string>,
): ReexportRow | null => {
const prefix = /^\[re-export\]\s+/u.exec(line);
const prefix = /^\[re-export\] /u.exec(line);
if (prefix === null) return null;
const rest = line.slice(prefix[0].length);
if (rest.length === 0) return null;
@@ -977,6 +977,39 @@ it("reads exports from the symbol table without llvm-nm's dyld-info pass", async
expect(nmCommands(execution.value.result)).toEqual(nmArguments);
});
it("binds leading-space exports to their exact nm identities", async () => {
// Real clang dylib: .globl " _spaced"; " _spaced": ret; .globl _spaced; _spaced: ret.
// dyld_info separates the address from the literal symbol with two spaces.
const names = " _spaced\n_spaced\n";
const { runner, nmArguments } = symbolTableRunner(
{
"dyld_info:-exports": [
"fixture [arm64]:",
" -exports:",
" offset symbol",
" 0x000002A0 _spaced",
" 0x000002A4 _spaced",
"",
].join("\n"),
},
{ symbolTable: names, withDyldInfo: names },
);
const execution = await new NativeMacOSProvider({}, runner, "darwin")
.createClient(machoTarget("/private/fixture"))
.execute("inspect_macho", {});
if (!execution.ok) throw execution.error;
expect(nmArguments).toHaveLength(1);
const exported = (
execution.value.result as {
exports: { items: { name: string; address: string | null }[] };
}
).exports.items;
expect(exported).toEqual([
expect.objectContaining({ name: " _spaced", address: "0x1000002a0" }),
expect.objectContaining({ name: "_spaced", address: "0x1000002a4" }),
]);
});
it("keeps an export name ending in a carriage return distinct", async () => {
// Captured from a clang executable exporting "trailing\r" and "trailing".
const names = "__mh_execute_header\n_main\ntrailing\ntrailing\r\n";