fix(native): parse renamed dyld_info re-exports (#2364)
* fix(native): parse renamed dyld_info re-exports dyld_info prints a re-export whose name differs from the source library's symbol as `[re-export] _foo_renamed (_foo from libfoo)`. The export parser only accepted `(from libfoo)`, so inspect_macho reported a nonexistent export named `_foo_renamed (_foo from libfoo)` with no source, and the nm row for `_foo_renamed` survived the name-based merge as a second export with reexport null. Accept the optional source symbol, trying the plain `(from lib)` form first so a library name containing ` from ` keeps its text. The row now reports `_foo_renamed` re-exported from libfoo and replaces the nm duplicate. The export schema has no field for the source symbol name, so exports coverage records a limitation when a renamed re-export is present. The golden fixture is a dyld_info capture of a synthetic dylib linked with -Wl,-alias,_foo,_foo_renamed. * fix(native): split dyld re-export suffix without nested quantifiers Replace the (?:(.+?)\s+)??from lazy-optional group with a linear string scan so malformed lines cannot backtrack and a library path containing " from " still keeps its whole text via the plain-form-first check. * test(native): cover nested re-export library names * fix(native): retain reexports missing from nm inventory --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> Co-authored-by: morluto <17176810+morluto@users.noreply.github.com>
This commit is contained in:
parent
f652709cac
commit
1d5f95dc4a
|
|
@ -266,6 +266,11 @@ const normalizeMacho = (
|
|||
]),
|
||||
exports: covered(exports, false, [
|
||||
"Merged nm/dyld_info results may be incomplete for stripped binaries.",
|
||||
...(dyldExports.some(({ sourceSymbol }) => sourceSymbol !== undefined)
|
||||
? [
|
||||
"Renamed re-exports are listed under their exported name; the source library's symbol name is not reported.",
|
||||
]
|
||||
: []),
|
||||
]),
|
||||
segments: covered(
|
||||
load.segments.map((segment) => ({
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ interface ParsedDyldSymbol {
|
|||
readonly weak: boolean | null;
|
||||
readonly reexport: boolean | null;
|
||||
readonly source: string | null;
|
||||
/** 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. */
|
||||
|
|
@ -33,15 +35,23 @@ export const parseDyldSymbols = (
|
|||
];
|
||||
return [];
|
||||
}
|
||||
const reexport = /^\[re-export\]\s+(.+?)(?:\s+\(from (.+)\))?$/u.exec(line);
|
||||
if (reexport?.[1] !== undefined)
|
||||
// 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: reexport[1],
|
||||
name: reexport.name,
|
||||
address: null,
|
||||
weak: null,
|
||||
reexport: true,
|
||||
source: reexport[2] ?? null,
|
||||
source: reexport.source,
|
||||
...(reexport.sourceSymbol === undefined
|
||||
? {}
|
||||
: { sourceSymbol: reexport.sourceSymbol }),
|
||||
},
|
||||
];
|
||||
const exported = /^(0x[\da-f]+)\s+(.+?)(?:\s+\[([^\]]+)\])?$/iu.exec(line);
|
||||
|
|
@ -78,3 +88,70 @@ export const parseDyldSymbols = (
|
|||
|
||||
const canonicalHex = (value: string | undefined): string | null =>
|
||||
value === undefined ? null : `0x${BigInt(value).toString(16)}`;
|
||||
|
||||
interface ReexportRow {
|
||||
readonly name: string;
|
||||
readonly source: string | null;
|
||||
readonly sourceSymbol?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a `[re-export]` row without nested lazy quantifiers.
|
||||
*
|
||||
* Returns null when the line is not a re-export row so callers fall through
|
||||
* to the plain export parser. Known names disambiguate nested parentheses;
|
||||
* when that inventory is incomplete, retain the suffix-based fallback.
|
||||
*/
|
||||
const parseReexportRow = (
|
||||
line: string,
|
||||
knownNames: ReadonlySet<string>,
|
||||
): ReexportRow | null => {
|
||||
const prefix = /^\[re-export\]\s+/u.exec(line);
|
||||
if (prefix === null) return null;
|
||||
const rest = line.slice(prefix[0].length);
|
||||
if (rest.length === 0) return null;
|
||||
if (!rest.endsWith(")")) return { name: rest, source: null };
|
||||
const candidates: ReexportRow[] = [];
|
||||
for (const match of rest.matchAll(/\s+\(/gu)) {
|
||||
const name = rest.slice(0, match.index);
|
||||
if (!knownNames.has(name)) continue;
|
||||
const parsed = reexportSuffix(
|
||||
name,
|
||||
rest.slice(match.index + match[0].length, -1),
|
||||
);
|
||||
if (parsed !== null) candidates.push(parsed);
|
||||
}
|
||||
// Exact nm identities establish a boundary even when names contain parentheses.
|
||||
// Competing known interpretations remain unknown rather than creating a new name.
|
||||
if (candidates.length > 0)
|
||||
return candidates.length === 1 ? candidates[0]! : null;
|
||||
if (knownNames.has(rest)) return { name: rest, source: null };
|
||||
let depth = 0;
|
||||
for (let index = rest.length - 1; index >= 0; index--) {
|
||||
if (rest[index] === ")") depth++;
|
||||
else if (rest[index] === "(" && --depth === 0) {
|
||||
if (index === 0 || !/\s/u.test(rest[index - 1]!)) break;
|
||||
const name = rest.slice(0, index).trimEnd();
|
||||
return (
|
||||
reexportSuffix(name, rest.slice(index + 1, -1)) ?? {
|
||||
name: rest,
|
||||
source: null,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
return { name: rest, source: null };
|
||||
};
|
||||
|
||||
const reexportSuffix = (name: string, inner: string): ReexportRow | null => {
|
||||
if (name.length === 0) return null;
|
||||
if (inner.startsWith("from ") && inner.length > 5)
|
||||
return { name, source: inner.slice(5) };
|
||||
const separator = /\s+from /u.exec(inner);
|
||||
if (separator === null) return null;
|
||||
const sourceSymbol = inner.slice(0, separator.index);
|
||||
const source = inner.slice(separator.index + separator[0].length);
|
||||
return sourceSymbol.length > 0 && source.length > 0
|
||||
? { name, source, sourceSymbol }
|
||||
: null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -225,6 +225,79 @@ it("retains native inventory facts from captured Apple tool output", async () =>
|
|||
});
|
||||
});
|
||||
|
||||
it("reports a renamed re-export once under its exported name", async () => {
|
||||
const client = new NativeMacOSProvider(
|
||||
{},
|
||||
new FixtureRunner({
|
||||
"dyld_info:-exports": await fixture(
|
||||
"dyld-inventory/renamed-reexports.txt",
|
||||
),
|
||||
nm: "_foo_renamed\n_own\n",
|
||||
}),
|
||||
"darwin",
|
||||
).createClient(machoTarget("/private/fixture"));
|
||||
const execution = await client.execute("inspect_macho", {});
|
||||
if (!execution.ok) throw execution.error;
|
||||
expect(execution.value.result).toMatchObject({
|
||||
exports: {
|
||||
items: [
|
||||
{
|
||||
name: "_foo_renamed",
|
||||
address: null,
|
||||
weak: null,
|
||||
reexport: true,
|
||||
source: "libfoo",
|
||||
},
|
||||
{ name: "_own", reexport: false, source: null },
|
||||
],
|
||||
limitations: [
|
||||
"Merged nm/dyld_info results may be incomplete for stripped binaries.",
|
||||
"Renamed re-exports are listed under their exported name; the source library's symbol name is not reported.",
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("splits re-export rows at the source library", () => {
|
||||
// dyld_info -exports rows for synthetic dylibs: a source symbol named
|
||||
// `operator()`, and a library whose install name contains ` from `.
|
||||
expect(
|
||||
parseDyldSymbols(
|
||||
[
|
||||
"[re-export] _call_renamed (operator() from libfoo)",
|
||||
"[re-export] _g (from lib from foo)",
|
||||
"[re-export] _nested (from lib (from foo))",
|
||||
].join("\n"),
|
||||
"exports",
|
||||
null,
|
||||
new Set(["_call_renamed", "_g", "_nested"]),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
name: "_call_renamed",
|
||||
address: null,
|
||||
weak: null,
|
||||
reexport: true,
|
||||
source: "libfoo",
|
||||
sourceSymbol: "operator()",
|
||||
},
|
||||
{
|
||||
name: "_g",
|
||||
address: null,
|
||||
weak: null,
|
||||
reexport: true,
|
||||
source: "lib from foo",
|
||||
},
|
||||
{
|
||||
name: "_nested",
|
||||
address: null,
|
||||
weak: null,
|
||||
reexport: true,
|
||||
source: "lib (from foo)",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains symbol names containing spaces in dyld inventory rows", () => {
|
||||
expect(
|
||||
parseDyldSymbols(" 0x0000 _entry with space (from fixture)", "imports"),
|
||||
|
|
@ -449,6 +522,25 @@ it("leaves an ambiguous short and literal name to the caller's nm inventory", ()
|
|||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("retains unmatched re-export rows when the known export inventory is incomplete", () => {
|
||||
expect(
|
||||
parseDyldSymbols(
|
||||
"[re-export] _g (from libfoo)",
|
||||
"exports",
|
||||
null,
|
||||
new Set(["_own"]),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
name: "_g",
|
||||
address: null,
|
||||
weak: null,
|
||||
reexport: true,
|
||||
source: "libfoo",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains annotation parsing without nm name evidence", () => {
|
||||
expect(parseDyldSymbols("0x120 _weak [weak-def]", "exports")).toEqual([
|
||||
{
|
||||
|
|
|
|||
|
|
@ -37,6 +37,19 @@ import rows from `xcrun dyld_info -imports /usr/lib/libobjc.A.dylib`.
|
|||
`xcrun dyld_info -exports /usr/lib/libc++.1.dylib`. These are separate library
|
||||
captures used to check their respective row formats.
|
||||
|
||||
`renamed-reexports.txt` is `xcrun dyld_info -exports` for a dylib that
|
||||
re-exports `_foo` from `libfoo` under the name `_foo_renamed`:
|
||||
|
||||
```sh
|
||||
printf 'int foo(void){return 1;}\n' > /tmp/foo.c
|
||||
printf 'int own(void){return 2;}\n' > /tmp/re.c
|
||||
xcrun clang -dynamiclib /tmp/foo.c -o /tmp/libfoo.dylib -install_name @rpath/libfoo.dylib
|
||||
xcrun clang -dynamiclib /tmp/re.c -o /tmp/libre.dylib -install_name @rpath/libre.dylib \
|
||||
/tmp/libfoo.dylib -Wl,-alias,_foo,_foo_renamed
|
||||
xcrun dyld_info -exports /tmp/libre.dylib
|
||||
xcrun nm -gjU /tmp/libre.dylib
|
||||
```
|
||||
|
||||
`codesign-missing.txt` is stderr from `codesign -d --verbose=4` on a nonexistent
|
||||
path (exit 1). `codesign-unsigned.txt` is stderr from the same command on a copy
|
||||
of the fixture after `codesign --remove-signature` (exit 1). Only the latter is
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
fixture [arm64]:
|
||||
-exports:
|
||||
offset symbol
|
||||
[re-export] _foo_renamed (_foo from libfoo)
|
||||
0x00000308 _own
|
||||
Loading…
Reference in New Issue