diff --git a/src/native/NativeMachoInspection.ts b/src/native/NativeMachoInspection.ts index c981e4138a..9d85fa2f82 100644 --- a/src/native/NativeMachoInspection.ts +++ b/src/native/NativeMachoInspection.ts @@ -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); }); }; diff --git a/src/native/parsers/dyldInfo.ts b/src/native/parsers/dyldInfo.ts index 8ba8909939..9bb8074c3b 100644 --- a/src/native/parsers/dyldInfo.ts +++ b/src/native/parsers/dyldInfo.ts @@ -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, ): 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; diff --git a/tests/boundary/providers/native/nativeInventory.golden.test.ts b/tests/boundary/providers/native/nativeInventory.golden.test.ts index 86eccf6858..2ab84014f6 100644 --- a/tests/boundary/providers/native/nativeInventory.golden.test.ts +++ b/tests/boundary/providers/native/nativeInventory.golden.test.ts @@ -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";