fix(native): name chained imports from the binary's import table

dyld_info prints chained-fixup rows by import ordinal, and a row with two
spaces after the ordinal has two readings. REA settled them against the
nm undefined-symbol list, which omits weak-def coalesced names, so such
imports got null or ambiguous names. inspect-macho now reads the
chained-fixup import table from the binary and matches rows by ordinal,
falling back to the symbol table with a recorded limitation when the
table is unusable.

Fixes #8723

Squashed from #8724 and rebased onto main.

Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
Lewis Wongandgensokyo-dev authored and Ysopwnable committed 2026-11-14 12:13:56 +01:00
1 parent ffc8318da3
commit 4bdd6d10c9
9 files changed
+469 -87

No files matched your search

+1
View File
@@ -815,6 +815,7 @@ jobs:
tests/boundary/providers/native/nativeUiOffscreenWindow.test.ts
tests/boundary/process/nativeUiVerifierLane.test.ts
tests/boundary/cli/nativeSignatureCli.test.ts
tests/boundary/cli/nativeMachoImportNamesCli.test.ts
tests/boundary/cli/nativeMachoArm64_32Cli.test.ts
tests/boundary/cli/nativeMachoEncryptionCli.test.ts
tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts
+1
View File
@@ -168,6 +168,7 @@ const PATH_SCOPES = {
"tests/boundary/process/nativeUiVerifierLane.test.ts",
"tests/conformance/native/ui-offscreen.swift",
"tests/boundary/cli/nativeSignatureCli.test.ts",
"tests/boundary/cli/nativeMachoImportNamesCli.test.ts",
"tests/boundary/cli/nativeMachoArm64_32Cli.test.ts",
"tests/boundary/cli/nativeMachoEncryptionCli.test.ts",
"tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts",
+24 -1
View File
@@ -1,6 +1,9 @@
import { describe, expect, it, vi } from "vitest";
import { decodeChainedPointer } from "./AppleMachoFixups.js";
import {
chainedImportNames,
decodeChainedPointer,
} from "./AppleMachoFixups.js";
import { parsePointerFixups } from "./AppleMachoFixups.fixture.js";
import type { Segment } from "./AppleMachoSelection.js";
@@ -147,6 +150,26 @@ const chainedData = (format: 1 | 2 | 3): Buffer => {
return data;
};
describe("chained import names", () => {
it.each([1, 2, 3] as const)(
"reads format %i import-table names without resolving libraries",
(format) => {
// Ordinals 1 and 2 name dependencies that this reader does not need.
expect(chainedImportNames(chainedData(format))).toEqual([
"_OBJC_CLASS_$_NSObject",
"_free",
]);
},
);
it("rejects a malformed import table", () => {
const data = chainedData(1);
data.writeUInt32LE(4, 20);
expect(() => chainedImportNames(data)).toThrow(RangeError);
expect(() => chainedImportNames(Buffer.alloc(0))).toThrow(RangeError);
});
});
const SEGMENTS: Segment[] = [
{ address: BASE, size: 0x4000n, offset: 0, executable: true },
{ address: BASE + 0x4000n, size: 0x4000n, offset: 0x4000, executable: false },
+116 -67
View File
@@ -210,22 +210,29 @@ const sameImport = (left: Import, right: Import): boolean =>
left.weak === right.weak &&
left.addend === right.addend;
const readImport = (
data: {
readonly bytes: Buffer;
readonly base: number;
readonly end: number;
readonly dylibs: FixupCommands["dylibs"];
},
header: {
readonly offset: number;
readonly count: number;
readonly format: number;
readonly symbols: number;
},
/** Validated location of a chained-fixup import table and its symbol pool. */
interface ChainedImportTable {
readonly offset: number;
readonly count: number;
readonly format: number;
readonly symbols: number;
}
/** One import-table record before its library ordinal is resolved. */
interface ChainedImportEntry {
readonly nameOffset: number;
readonly ordinal: number;
readonly weak: boolean;
readonly addend: bigint;
}
const chainedImportEntry = (
bytes: Buffer,
base: number,
end: number,
header: ChainedImportTable,
index: number,
): Import => {
const { bytes, base, end, dylibs } = data;
): ChainedImportEntry => {
const size = header.format === 3 ? 16 : header.format === 2 ? 8 : 4;
if (header.format < 1 || header.format > 3)
throw new RangeError(`Unsupported chained import format ${header.format}`);
@@ -240,35 +247,46 @@ const readImport = (
if (header.format === 3) {
const word = bytes.readBigUInt64LE(at);
const encodedOrdinal = Number(bits(word, 0, 16));
const ordinal =
encodedOrdinal > 0xfff0 ? encodedOrdinal - 0x10000 : encodedOrdinal;
return {
...cString(
bytes,
base + header.symbols + Number(bits(word, 32, 32)),
end,
),
binding_source: "chained",
library_ordinal: ordinal,
...libraryFacts(ordinal, dylibs),
nameOffset: Number(bits(word, 32, 32)),
ordinal:
encodedOrdinal > 0xfff0 ? encodedOrdinal - 0x10000 : encodedOrdinal,
weak: bits(word, 16, 1) === 1n,
addend: bytes.readBigInt64LE(at + 8),
};
}
const word = bytes.readUInt32LE(at);
const encodedOrdinal = word & 0xff;
const ordinal =
encodedOrdinal > 0xf0 ? encodedOrdinal - 0x100 : encodedOrdinal;
return {
...cString(bytes, base + header.symbols + (word >>> 9), end),
binding_source: "chained",
library_ordinal: ordinal,
...libraryFacts(ordinal, dylibs),
nameOffset: word >>> 9,
ordinal: encodedOrdinal > 0xf0 ? encodedOrdinal - 0x100 : encodedOrdinal,
weak: ((word >>> 8) & 1) === 1,
addend: header.format === 2 ? BigInt(bytes.readInt32LE(at + 4)) : 0n,
};
};
const readImport = (
data: {
readonly bytes: Buffer;
readonly base: number;
readonly end: number;
readonly dylibs: FixupCommands["dylibs"];
},
header: ChainedImportTable,
index: number,
): Import => {
const { bytes, base, end, dylibs } = data;
const entry = chainedImportEntry(bytes, base, end, header, index);
return {
...cString(bytes, base + header.symbols + entry.nameOffset, end),
binding_source: "chained",
library_ordinal: entry.ordinal,
...libraryFacts(entry.ordinal, dylibs),
weak: entry.weak,
addend: entry.addend,
};
};
/** Decode one raw chained pointer for its segment's pointer format. */
export const decodeChainedPointer = (
raw: bigint,
@@ -379,6 +397,68 @@ const readChainedPage = (
return { locations, lastOffset, failure };
};
/** Validate one `LC_DYLD_CHAINED_FIXUPS` header and its import table. */
const chainedFixupsHeader = (
bytes: Buffer,
base: number,
size: number,
): { readonly startsOffset: number; readonly imports: ChainedImportTable } => {
const end = base + size;
if (
!Number.isSafeInteger(base) ||
base < 0 ||
!Number.isSafeInteger(size) ||
size < 28 ||
end > bytes.length
)
throw new RangeError("Chained fixup header exceeds the target");
if (bytes.readUInt32LE(base) !== 0)
throw new RangeError("Unsupported chained fixup header version");
const imports = {
offset: bytes.readUInt32LE(base + 8),
symbols: bytes.readUInt32LE(base + 12),
count: bytes.readUInt32LE(base + 16),
format: bytes.readUInt32LE(base + 20),
};
const importSize = imports.format === 3 ? 16 : imports.format === 2 ? 8 : 4;
const importTableEnd = base + imports.offset + imports.count * importSize;
if (imports.format < 1 || imports.format > 3)
throw new RangeError(`Unsupported chained import format ${imports.format}`);
if (
imports.offset < 28 ||
imports.symbols < imports.offset ||
importTableEnd > base + imports.symbols ||
base + imports.symbols > end
)
throw new RangeError("Chained import table exceeds the fixup data");
if (bytes.readUInt32LE(base + 24) !== 0)
throw new RangeError(
"Compressed chained-fixup symbol names are not supported",
);
return { startsOffset: bytes.readUInt32LE(base + 4), imports };
};
/**
* Symbol names of one `LC_DYLD_CHAINED_FIXUPS` payload's import table, indexed
* by import ordinal. A name without a lossless text projection is null.
*/
export const chainedImportNames = (
fixups: Buffer,
): readonly (string | null)[] => {
const { imports } = chainedFixupsHeader(fixups, 0, fixups.length);
const names: (string | null)[] = [];
for (let index = 0; index < imports.count; index++) {
const entry = chainedImportEntry(fixups, 0, fixups.length, imports, index);
const { symbol } = cString(
fixups,
imports.symbols + entry.nameOffset,
fixups.length,
);
names.push(symbol);
}
return names;
};
/** A known eight-byte chain word starting elsewhere overlaps this pointer read. */
const hasPartialChainedPointer = (
chain: ChainedSegment,
@@ -410,44 +490,13 @@ const chainedFixups = (
const { bytes, segments, dylibs, baseAddress } = image;
const base = range.offset;
const end = range.offset + range.size;
if (
!Number.isSafeInteger(base) ||
base < 0 ||
!Number.isSafeInteger(range.size) ||
range.size < 28 ||
end > bytes.length
)
throw new RangeError("Chained fixup header exceeds the target");
if (bytes.readUInt32LE(base) !== 0)
throw new RangeError("Unsupported chained fixup header version");
const startsOffset = bytes.readUInt32LE(base + 4);
const importHeader = {
offset: bytes.readUInt32LE(base + 8),
symbols: bytes.readUInt32LE(base + 12),
count: bytes.readUInt32LE(base + 16),
format: bytes.readUInt32LE(base + 20),
};
const importSize =
importHeader.format === 3 ? 16 : importHeader.format === 2 ? 8 : 4;
const importTableEnd =
base + importHeader.offset + importHeader.count * importSize;
if (importHeader.format < 1 || importHeader.format > 3)
throw new RangeError(
`Unsupported chained import format ${importHeader.format}`,
);
if (
importHeader.offset < 28 ||
importHeader.symbols < importHeader.offset ||
importTableEnd > base + importHeader.symbols ||
base + importHeader.symbols > end
)
throw new RangeError("Chained import table exceeds the fixup data");
const { startsOffset, imports: importHeader } = chainedFixupsHeader(
bytes,
base,
range.size,
);
const imports = (index: number) =>
readImport({ bytes, base, end, dylibs }, importHeader, index);
if (bytes.readUInt32LE(base + 24) !== 0)
throw new RangeError(
"Compressed chained-fixup symbol names are not supported",
);
const starts = base + startsOffset;
const startsEnd = base + importHeader.offset;
const requireStartsRange = (at: number, size: number): void => {
+7
View File
@@ -1414,6 +1414,13 @@ class NativeMacOSClient implements AnalysisClient {
selected,
Math.floor(NATIVE_COMMAND_OUTPUT_BUDGET_BYTES / 256),
),
readSliceRange: (offset, size, at, length) =>
source.readSlice(
offset,
size ?? source.bytes - offset,
at,
length,
),
readCommandNames: async (offset, size, selected) => {
const length = size ?? source.bytes - offset;
// Binary strings can expand to hex and escaped JSON, and each
+77 -9
View File
@@ -12,7 +12,11 @@ import {
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import { machoRuntimeSymbolName } from "../domain/native/machoSymbolName.js";
import { err, ok, type Result } from "../domain/result.js";
import type { NativeCommandCapture } from "./CommandRunner.js";
import {
NATIVE_COMMAND_OUTPUT_BUDGET_BYTES,
type NativeCommandCapture,
} from "./CommandRunner.js";
import { chainedImportNames } from "./AppleMachoFixups.js";
import {
MACHO_ARCHITECTURE_POLICIES,
machoToolArchitectureName,
@@ -81,6 +85,13 @@ interface NativeMachoInspectionContext {
readonly resolveSlice: (
selected: LipoArchitecture,
) => Promise<ResolvedMachoSlice>;
/** Read an exact selected-slice range from the same bytes as the tools. */
readonly readSliceRange?: (
offset: number,
size: number | null,
at: number,
length: number,
) => Promise<Uint8Array>;
/** Borrow binary framing from the same identity-bound target as the tools. */
readonly readCommandNames: (
offset: number,
@@ -89,6 +100,44 @@ interface NativeMachoInspectionContext {
) => Promise<MachoCommandNames>;
}
const LC_DYLD_CHAINED_FIXUPS = 0x80000034;
/**
* Exact names from the selected slice's chained-fixup import table by import
* ordinal, or why that table cannot stand in for the symbol-table inventory.
* Undefined when the context has no slice reader.
*/
const readChainedImportNames = async (
context: NativeMachoInspectionContext,
binary: MachoCommandNames,
offset: number,
size: number | null,
): Promise<Result<readonly (string | null)[], string> | undefined> => {
if (context.readSliceRange === undefined) return undefined;
const fields = binary.commands.find(
({ command }) => command === LC_DYLD_CHAINED_FIXUPS,
)?.fixedFields;
const at = fields?.dataoff;
const length = fields?.datasize;
if (at === undefined || length === undefined)
return err("its load command fields are not decoded");
if (length > NATIVE_COMMAND_OUTPUT_BUDGET_BYTES)
return err(
`its ${length}-byte payload exceeds the ${NATIVE_COMMAND_OUTPUT_BUDGET_BYTES}-byte native read budget`,
);
const bytes = await context.readSliceRange(offset, size, at, length);
try {
return ok(
chainedImportNames(
Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength),
),
);
} catch (cause) {
if (cause instanceof RangeError) return err(cause.message);
throw cause;
}
};
const DISCOVERY_COMMANDS = [["lipo", ["-detailed_info"]]] as const;
// llvm-nm's default dyld-info pass fakes up symbols the symbol table lacks
@@ -313,14 +362,33 @@ export const inspectNativeMacho = async (
hasChainedFixups,
);
if (parsedImports.needsNameInventory) {
const importNames = await runSliceCommand(SYMBOL_TABLE_IMPORTS);
if (!importNames.ok) return importNames;
captures.push(importNames.value);
parsedImports = parseDyldImports(
sliceOutput(dyldImports),
hasChainedFixups,
new Set(parseNmSymbolNames(importNames.value.stdout)),
);
// Chained rows print entries of the binary's own import table by ordinal.
// Its exact names settle the ordinal spacing even for imports that the
// symbol table omits or lists as defined, such as weak-def coalescing.
const chainedTable = hasChainedFixups
? await readChainedImportNames(context, binary, offset, size)
: undefined;
if (chainedTable?.ok === true)
parsedImports = parseDyldImports(
sliceOutput(dyldImports),
hasChainedFixups,
undefined,
chainedTable.value,
);
else {
if (chainedTable !== undefined)
limitations.push(
`The chained-fixup import table is unusable (${chainedTable.error}); chained import names are matched against the undefined symbol table instead.`,
);
const importNames = await runSliceCommand(SYMBOL_TABLE_IMPORTS);
if (!importNames.ok) return importNames;
captures.push(importNames.value);
parsedImports = parseDyldImports(
sliceOutput(dyldImports),
hasChainedFixups,
new Set(parseNmSymbolNames(importNames.value.stdout)),
);
}
}
const encryption = await context.readEncryption(context.signal);
const selectedArchitectureName =
+23 -5
View File
@@ -19,11 +19,16 @@ interface ParsedDyldExport extends ParsedDyldSymbolFields {
readonly sourceSymbol?: string;
}
/** Decode Apple's literal import framing; request nm facts for competing names. */
/**
* Decode Apple's literal import framing. Competing readings of a chained row
* are settled by the import-table name at the row's ordinal when the caller
* supplies the table, otherwise by symbol-table names, requested when needed.
*/
export const parseDyldImports = (
output: string,
hasChainedFixups: boolean,
knownImportNames?: ReadonlySet<string>,
chainedImportTable?: readonly (string | null)[],
): {
readonly items: ParsedDyldImport[];
readonly needsNameInventory: boolean;
@@ -64,12 +69,25 @@ export const parseDyldImports = (
source,
});
}
if (readings.length > 1 && knownImportNames === undefined)
// A chained row's ordinal indexes the slice's own import table.
const tableName =
ordinal === null || chainedImportTable === undefined
? undefined
: chainedImportTable[Number(ordinal[0].trimEnd())];
if (
readings.length > 1 &&
knownImportNames === undefined &&
tableName === undefined
)
needsNameInventory = true;
const matching =
readings.length > 1 && knownImportNames !== undefined
? readings.filter(({ name }) => knownImportNames.has(name))
: readings;
readings.length <= 1
? readings
: tableName !== undefined
? readings.filter(({ name }) => name === tableName)
: knownImportNames !== undefined
? readings.filter(({ name }) => knownImportNames.has(name))
: readings;
const reading = matching.length === 1 ? matching[0] : undefined;
const ambiguousSuffix =
reading !== undefined &&
@@ -0,0 +1,72 @@
import { execFile } from "node:child_process";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { describe, expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const run = promisify(execFile);
describe.skipIf(process.platform !== "darwin")(
"native Mach-O CLI chained import names",
() => {
cliTest(
"names a weak-def-coalesce import that the undefined symbol table omits",
async ({ cli }) => {
const directory = await createTestTempDirectory("rea-macho-imports-");
const source = join(directory, "coalesce.cpp");
// Taking an exported inline function's address makes the linker bind
// it through <weak-def-coalesce>, as a defined weak symbol.
await writeFile(
source,
[
'inline __attribute__((visibility("default"))) int inl(int x) { return x * 3; }',
'__attribute__((visibility("default"))) int (*getinl())(int) { return &inl; }',
"",
].join("\n"),
);
const library = join(directory, "libcoalesce.dylib");
await run("/usr/bin/xcrun", [
"clang++",
"-arch",
"arm64",
"-dynamiclib",
source,
"-o",
library,
]);
const { stdout } = await run("/usr/bin/xcrun", [
"dyld_info",
"-imports",
library,
]);
expect(stdout).toMatch(/__Z3inli {2}\(from <weak-def-coalesce>\)/u);
const result = await cli.run({
arguments: ["inspect-macho", library, "--json"],
environment: {
REA_LOG_LEVEL: "silent",
REA_ANALYSIS_PROVIDER: "auto",
},
});
expect(result.exitCode, result.stdout).toBe(0);
expect(result.json).toMatchObject({
normalized_result: {
imports: {
items: [
{
name: "__Z3inli",
name_status: "exact",
source: "<weak-def-coalesce>",
},
],
},
},
});
},
60_000,
);
},
);
@@ -27,7 +27,26 @@ import {
} from "../../../fixtures/nativeCommands.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
const inventoryTarget = async (chained = false) => {
/** One format-1 `LC_DYLD_CHAINED_FIXUPS` payload holding only import names. */
const chainedImportPayload = (names: readonly string[]): Buffer => {
const pool = Buffer.from(`\0${names.join("\0")}\0`);
const imports = Buffer.alloc(names.length * 4);
let nameOffset = 1;
names.forEach((name, index) => {
imports.writeUInt32LE((nameOffset << 9) >>> 0, index * 4);
nameOffset += Buffer.byteLength(name) + 1;
});
const starts = Buffer.alloc(4);
const header = Buffer.alloc(28);
header.writeUInt32LE(28, 4);
header.writeUInt32LE(28 + starts.length, 8);
header.writeUInt32LE(28 + starts.length + imports.length, 12);
header.writeUInt32LE(names.length, 16);
header.writeUInt32LE(1, 20);
return Buffer.concat([header, starts, imports, pool]);
};
const inventoryTarget = async (chained = false, payload?: Buffer) => {
const path = join(
await createTestTempDirectory("rea-native-inventory-"),
"fixture",
@@ -41,17 +60,21 @@ const inventoryTarget = async (chained = false) => {
segment.writeBigUInt64LE(0x4000n, 48);
segment.writeUInt32LE(7, 56);
segment.writeUInt32LE(5, 60);
const payloadOffset = 0x2000;
const fixups = Buffer.alloc(16);
fixups.writeUInt32LE(0x80000034, 0);
fixups.writeUInt32LE(16, 4);
if (payload !== undefined) {
fixups.writeUInt32LE(payloadOffset, 8);
fixups.writeUInt32LE(payload.length, 12);
}
const image = machoImage({
cpu: CPU.arm64,
commands: chained ? [segment, fixups] : [segment],
});
await writeFile(
path,
Buffer.concat([image, Buffer.alloc(0x4000 - image.byteLength)]),
);
const file = Buffer.concat([image, Buffer.alloc(0x4000 - image.byteLength)]);
payload?.copy(file, payloadOffset);
await writeFile(path, file);
return nativeMachoTargetForFile(path);
};
@@ -793,6 +816,126 @@ it("uses independent undefined names to resolve chained producer spacing", async
});
});
it("names chained imports from the binary's import table when the symbol table omits them", async () => {
const outputs: Record<string, string> = {};
for (const tool of ["lipo", "nm", "dwarfdump", "vtool"])
outputs[tool] = await fixture(`dyld-inventory/${tool}.txt`);
// Weak-def coalescing binds a defined symbol, and Swift images can import
// names that have no symbol-table entry, so nm -gju lists neither.
outputs["nm:-gju"] = "";
outputs["dyld_info:-imports"] = [
" -imports:",
" 0x0000 __Z3inli (from <weak-def-coalesce>)",
" 0x0001 _$s10AppIntents0A4EnumMp (from AppIntents)",
].join("\n");
const client = new NativeMacOSProvider(
{},
inventoryRunner(outputs),
"darwin",
).createClient(
await inventoryTarget(
true,
chainedImportPayload(["__Z3inli", "_$s10AppIntents0A4EnumMp"]),
),
);
const execution = await client.execute("inspect_macho", {});
if (!execution.ok) throw execution.error;
expect(execution.value.result).toMatchObject({
imports: {
items: [
{
name: "__Z3inli",
name_status: "exact",
source: "<weak-def-coalesce>",
},
{
name: "_$s10AppIntents0A4EnumMp",
name_status: "exact",
source: "AppIntents",
},
],
},
});
// The binary-owned table settles the spacing without the nm inventory.
expect(JSON.stringify(execution.value.result)).not.toContain('"-gju"');
});
it("names each chained row by its ordinal when the table holds both spacing readings", async () => {
const outputs: Record<string, string> = {};
for (const tool of ["lipo", "nm", "dwarfdump", "vtool"])
outputs[tool] = await fixture(`dyld-inventory/${tool}.txt`);
outputs["nm:-gju"] = "_foo\n";
// Ordinal 1 names " _foo"; installed dyld_info prints two spaces before it.
outputs["dyld_info:-imports"] = [
" -imports:",
" 0x0000 _foo (from <weak-def-coalesce>)",
" 0x0001 _foo (from <weak-def-coalesce>)",
].join("\n");
const client = new NativeMacOSProvider(
{},
inventoryRunner(outputs),
"darwin",
).createClient(
await inventoryTarget(true, chainedImportPayload(["_foo", " _foo"])),
);
const execution = await client.execute("inspect_macho", {});
if (!execution.ok) throw execution.error;
expect(execution.value.result).toMatchObject({
imports: {
items: [
{ name: "_foo", name_status: "exact" },
{ name: " _foo", name_status: "exact" },
],
},
});
});
it("records why an unusable chained import table falls back to the symbol table", async () => {
const outputs: Record<string, string> = {};
for (const tool of ["lipo", "nm", "dwarfdump", "vtool"])
outputs[tool] = await fixture(`dyld-inventory/${tool}.txt`);
outputs["nm:-gju"] = "__Z3inli\n";
outputs["dyld_info:-imports"] = [
" -imports:",
" 0x0000 __Z3inli (from libc++)",
].join("\n");
// A compressed symbol pool has no decodable names.
const payload = chainedImportPayload(["__Z3inli"]);
payload.writeUInt32LE(1, 24);
const client = new NativeMacOSProvider(
{},
inventoryRunner(outputs),
"darwin",
).createClient(await inventoryTarget(true, payload));
const execution = await client.execute("inspect_macho", {});
if (!execution.ok) throw execution.error;
expect(execution.value.result).toMatchObject({
imports: { items: [{ name: "__Z3inli", name_status: "exact" }] },
limitations: expect.arrayContaining([
expect.stringContaining(
"Compressed chained-fixup symbol names are not supported",
),
]),
});
expect(JSON.stringify(execution.value.result)).toContain('"-gju"');
});
it("keeps a chained row unknown when its ordinal is outside the import table", () => {
const output = " 0x0005 _foo (from library)";
expect(parseDyldImports(output, true, undefined, ["_foo"]).items).toEqual([
{
name: null,
raw_name: output.slice(6),
name_status: "ambiguous",
addend: null,
address: null,
weak: null,
reexport: null,
source: "library",
},
]);
});
it("keeps missing and competing undefined-name matches unknown", () => {
const output = " 0x0000 _literal (from library)";
expect(parseDyldImports(output, true).needsNameInventory).toBe(true);