mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
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:
1 parent
ffc8318da3
commit
4bdd6d10c9
9 files changed
+469
-87
No files matched your search
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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 => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user