Files
rea/tests/boundary/il2cpp/il2cppBinaryFile.test.ts
T
morluto 87745af52a fix(il2cpp): consume resolved ELF program declarations
Retain relevant program facts in the shared native directory and consume them for mappings and relocations. Remove redundant table coordinates and presence flags. This preserves extended program counts so PT_DYNAMIC beyond the raw sentinel remains visible, while load and relocation admission keep their distinct policies.
2026-10-24 09:14:21 +08:00

883 lines
30 KiB
TypeScript

import { Il2cppBinaryFormatFailure } from "../../../src/il2cpp/Il2cppBinaryFormat.js";
import { readBinaryContainer } from "../../../src/native/metadata/readBinaryContainer.js";
import {
openImageSteps,
readUInt32,
} from "../../../src/il2cpp/Il2cppBinaryImage.js";
import { expect, it } from "vitest";
import {
linkIl2cppBinary,
linkIl2cppBinarySteps,
} from "../../../src/il2cpp/Il2cppBinaryFile.js";
import { readIl2cppMetadataForLinking } from "../../../src/il2cpp/Il2cppMetadataFile.js";
import {
createElfFixture,
createIl2cppBinaryFixture,
createIl2cppDyldBindFixture,
createIl2cppRegistrationTailFixture,
createUniversalFixture,
createSplitPointerPeFixture,
FIXTURE_BINARY_BASE,
FIXTURE_METHOD_ADDRESSES,
FIXTURE_RETURN_TYPE_NAME,
type Il2cppBinaryFixtureOptions,
} from "../../fixtures/il2cpp/binary.js";
import {
createIl2cppMetadataFixture,
createLinkedIl2cppMetadataFixture,
} from "../../fixtures/il2cpp/metadata.js";
const { metadata, link: facts } = readIl2cppMetadataForLinking(
createLinkedIl2cppMetadataFixture().bytes,
);
const expectedAddress = (value: bigint | "no-body" | "unknown") =>
typeof value === "bigint"
? {
state: "code",
address: `0x${value.toString(16)}`,
rva: `0x${(value - FIXTURE_BINARY_BASE).toString(16)}`,
}
: { state: value, address: null, rva: null };
const expectedAddresses = FIXTURE_METHOD_ADDRESSES.map(expectedAddress);
const link = (bytes: Buffer, architecture?: string) =>
linkIl2cppBinary(bytes, metadata, facts, architecture);
const failure = (bytes: Buffer, architecture?: string) => {
try {
link(bytes, architecture);
} catch (cause) {
if (cause instanceof Il2cppBinaryFormatFailure)
return { reason: cause.reason, kind: cause.kind };
throw cause;
}
throw new Error("expected a link failure");
};
const addElfLoadAlias = (
bytes: Buffer,
fileOffset: number,
address: bigint,
size: number,
) => {
const withAlias = Buffer.from(bytes);
const count = withAlias.readUInt16LE(56);
const at = 64 + count * 56;
withAlias.writeUInt16LE(count + 1, 56);
withAlias.writeUInt32LE(1, at);
withAlias.writeUInt32LE(7, at + 4);
withAlias.writeBigUInt64LE(BigInt(fileOffset), at + 8);
withAlias.writeBigUInt64LE(address, at + 16);
withAlias.writeBigUInt64LE(address, at + 24);
const mappingSize = size === 0 ? withAlias.length : size;
withAlias.writeBigUInt64LE(BigInt(mappingSize), at + 32);
withAlias.writeBigUInt64LE(BigInt(mappingSize), at + 40);
withAlias.writeBigUInt64LE(0x1000n, at + 48);
return withAlias;
};
it("links methods to code through module tokens, not metadata order", () => {
const linked = link(createIl2cppBinaryFixture());
expect(linked).toMatchObject({
format: "macho",
architecture: "arm64",
image_base: FIXTURE_BINARY_BASE,
code_gen_modules_field: FIXTURE_BINARY_BASE + 0x600n,
code_gen_modules_array: FIXTURE_BINARY_BASE + 0x500n,
code_gen_modules: [
{
image: 0,
address: FIXTURE_BINARY_BASE + 0x400n,
method_pointer_count: 3,
},
{
image: 1,
address: FIXTURE_BINARY_BASE + 0x440n,
method_pointer_count: 2,
},
],
});
expect(linked.method_addresses).toEqual(expectedAddresses);
expect(linked).toMatchObject({
types_field: FIXTURE_BINARY_BASE + 0x830n,
types_count: 4,
});
expect(linked.return_types).toEqual(
Array.from({ length: 5 }, () => ({
type: 0,
name: FIXTURE_RETURN_TYPE_NAME,
})),
);
});
it.each([
["an identical range", 0, FIXTURE_BINARY_BASE, 0],
["a containing subrange", 0x400, FIXTURE_BINARY_BASE + 0x400n, 0x80],
] as const)(
"counts one registration through %s only once",
(_, offset, address, size) => {
const bytes = addElfLoadAlias(
createElfFixture(createIl2cppBinaryFixture()),
offset,
address,
size,
);
expect(link(bytes).method_addresses).toEqual(expectedAddresses);
},
);
it("still rejects distinct CodeRegistration module table fields", () => {
expect(
failure(
createElfFixture(
createIl2cppBinaryFixture({ duplicateCodeRegistration: true }),
),
),
).toEqual({ reason: "code-registration-not-found", kind: "malformed" });
});
it("still rejects distinct MetadataRegistration fields", () => {
expect(
failure(
createElfFixture(
createIl2cppBinaryFixture({ duplicateMetadataRegistration: true }),
),
),
).toEqual({ reason: "metadata-registration-not-found", kind: "malformed" });
});
it("rejects reused MethodDef tokens within an image but preserves cross-image reuse and folded addresses", () => {
const duplicateTokens = Uint32Array.from(facts.methodTokens);
duplicateTokens[1] = duplicateTokens[0]!;
expect(() =>
linkIl2cppBinary(createIl2cppBinaryFixture(), metadata, {
...facts,
methodTokens: duplicateTokens,
}),
).toThrow(
expect.objectContaining({
reason: "invalid-method-token",
kind: "malformed",
}),
);
// The fixture deliberately reuses MethodDef row ids in its second image.
expect(link(createIl2cppBinaryFixture()).method_addresses).toEqual(
expectedAddresses,
);
// Distinct rows may still point at the same native address after folding.
const folded = createIl2cppBinaryFixture();
folded.writeBigUInt64LE(folded.readBigUInt64LE(0x308), 0x300);
const linkedFolded = link(folded);
expect(linkedFolded.method_addresses[0]?.address).toBe(
linkedFolded.method_addresses[1]?.address,
);
});
it("requires an explicit slice of a universal Mach-O and reads only that slice", () => {
const thin = createIl2cppBinaryFixture();
const universal = createUniversalFixture([
{ cpu: 0x01000007, bytes: Buffer.alloc(0x100) },
{ cpu: 0x0100000c, bytes: thin },
]);
expect(failure(universal)).toEqual({
reason: "slice-selection-required",
kind: "unsupported",
});
expect(link(universal, "arm64").method_addresses).toEqual(expectedAddresses);
expect(failure(universal, "ppc64").reason).toBe("slice-not-found");
expect(failure(thin, "arm64").reason).toBe("slice-not-found");
});
it.each(["thin", "universal"] as const)(
"links a %s x86-64 Mach-O while retaining the reader's amd64 spelling",
(format) => {
const thin = createIl2cppBinaryFixture();
thin.writeUInt32LE(0x01000007, 4);
thin.writeUInt32LE(3, 8);
const bytes =
format === "thin"
? thin
: createUniversalFixture([
{ cpu: 0x0100000c, bytes: createIl2cppBinaryFixture() },
{ cpu: 0x01000007, bytes: thin },
]);
// Match the selected FAT declaration to CPU_SUBTYPE_X86_64_ALL in its header.
if (format === "universal") bytes.writeUInt32BE(3, 8 + 20 + 4);
const original = Buffer.from(bytes);
const linked = link(bytes, format === "thin" ? undefined : "amd64");
expect(linked).toMatchObject({
format: "macho",
architecture: "amd64",
code_gen_modules_field: FIXTURE_BINARY_BASE + 0x600n,
code_gen_modules_array: FIXTURE_BINARY_BASE + 0x500n,
});
expect(linked.method_addresses).toEqual(expectedAddresses);
expect(linked.return_types).toEqual(
link(createIl2cppBinaryFixture()).return_types,
);
expect(bytes).toEqual(original);
},
);
it.each<
[string, Il2cppBinaryFixtureOptions, string, "unsupported" | "malformed"]
>([
[
"a method count mismatch",
{ firstModuleCount: 4 },
"code-gen-modules-not-found",
"malformed",
],
[
"two matching module records",
{ duplicateModule: true },
"ambiguous-code-gen-module",
"malformed",
],
[
"no CodeRegistration module table",
{ codeRegistration: false },
"code-registration-not-found",
"malformed",
],
[
"no MetadataRegistration type table",
{ metadataRegistration: false },
"metadata-registration-not-found",
"malformed",
],
[
"chained fixups",
{ chainedFixups: true },
"unsupported-pointer-encoding",
"unsupported",
],
])("rejects %s without partial addresses", (_, options, reason, kind) => {
expect(failure(createIl2cppBinaryFixture(options))).toEqual({ reason, kind });
});
it("reports type indices without names for metadata version 27", () => {
const linked = linkIl2cppBinary(
createIl2cppBinaryFixture(),
{ ...metadata, metadata_version: 27 },
facts,
);
const rows = [
...linked.return_types,
...linked.field_types,
...linked.parameter_types,
];
expect(rows.length).toBeGreaterThan(0);
expect(rows.every((row) => row.name === null)).toBe(true);
expect(link(createIl2cppBinaryFixture()).return_types[0]?.name).toBe(
FIXTURE_RETURN_TYPE_NAME,
);
});
it("renders nested generic arguments on the components that declare them", () => {
const renderedName = (
typeNames: Readonly<Record<number, string>>,
genericTypeArgumentCounts: Readonly<Record<number, number>>,
genericArgumentTypeIndices: readonly number[],
) => {
const { metadata: nestedMetadata, link: nestedFacts } =
readIl2cppMetadataForLinking(
createLinkedIl2cppMetadataFixture({
typeNames,
typeNamespaces: { 0: "Game", 1: "" },
genericTypeArgumentCounts,
}).bytes,
);
return linkIl2cppBinary(
createIl2cppBinaryFixture({
genericDefinitionTypeIndex: 1,
genericArgumentTypeIndices,
}),
nestedMetadata,
nestedFacts,
).return_types[0]?.name;
};
expect(
renderedName({ 0: "Outer`1", 1: "Inner`2" }, { 0: 1, 1: 2 }, [3, 3]),
).toBe("Game.Outer<int>+Inner<int>[]");
expect(renderedName({ 0: "Outer`1", 1: "Leaf`1" }, { 0: 1, 1: 1 }, [3])).toBe(
"Game.Outer<int>+Leaf[]",
);
});
it("does not search for an empty image name", () => {
const unnamed = {
...metadata,
images: metadata.images.map((image, index) =>
index === 0 ? { ...image, name: "" } : image,
),
};
expect(() =>
linkIl2cppBinary(createIl2cppBinaryFixture(), unnamed, facts),
).toThrow("code-gen-modules-not-found");
});
it("refuses an image name repeated past the search bound", () => {
const fixture = createIl2cppBinaryFixture();
const flooded = Buffer.concat([
fixture,
Buffer.from("\0mscorlib.dll".repeat(65537) + "\0", "utf8"),
]);
// The Mach-O segment covers the original size; widen it to the new tail.
flooded.writeBigUInt64LE(BigInt(flooded.length), 64);
flooded.writeBigUInt64LE(BigInt(flooded.length), 80);
expect(failure(flooded)).toEqual({
reason: "native-index-capacity",
kind: "capacity",
});
});
it.each([
[{ duplicateRela: true }],
[{ relaEntry: 16n }],
[{ relaSize: 25n }],
] as const)("rejects malformed ELF relocation tables (%o)", (options) => {
expect(
failure(createElfFixture(createIl2cppBinaryFixture(), options)),
).toEqual({ reason: "malformed-container", kind: "malformed" });
});
it("does not apply symbol relocations as RELATIVE ones", () => {
// With every slot left zero, no module table can be found.
expect(() =>
link(
createElfFixture(createIl2cppBinaryFixture(), {
symbolRelocations: true,
}),
),
).toThrow(Il2cppBinaryFormatFailure);
});
it("reads a method pointer array in zero-fill as methods without bodies", () => {
// An iOS-only assembly in an Android build keeps its array in .bss.
const linked = link(
createElfFixture(createIl2cppBinaryFixture(), { zeroFilledTable: true }),
);
expect(linked.method_addresses).toEqual([
...expectedAddresses.slice(0, 3),
expectedAddress("no-body"),
expectedAddress("no-body"),
]);
});
it("reads a PE pointer array across file bytes and the declared zero-fill tail", () => {
const bytes = createSplitPointerPeFixture();
const original = Buffer.from(bytes);
expect(link(bytes).method_addresses).toEqual(expectedAddresses);
expect(bytes).toEqual(original);
expect(failure(createSplitPointerPeFixture(false)).reason).toBe(
"code-gen-modules-not-found",
);
});
it("reports a method no image range claims as unknown, not as without a body", () => {
// No type's method range holds this row, so no module slot describes it.
const orphan = {
...metadata,
methods: [...metadata.methods, { name: "Lost", declaring_type: -1 }],
};
const tokens = Uint32Array.from([...facts.methodTokens, 0x06000001]);
const linked = linkIl2cppBinary(createIl2cppBinaryFixture(), orphan, {
...facts,
methodTokens: tokens,
});
expect(linked.method_addresses).toEqual([
...expectedAddresses,
expectedAddress("unknown"),
]);
});
it("finds an x86-64 module array through the instruction that loads it", () => {
const inlined = createElfFixture(
createIl2cppBinaryFixture({ codeRegistration: false, codeReference: true }),
{ machine: 62 },
);
const linked = link(inlined);
expect(linked.code_gen_modules_field).toBeNull();
expect(linked.code_gen_modules_array).toBe(FIXTURE_BINARY_BASE + 0x500n);
expect(linked.method_addresses).toEqual(
link(createElfFixture(createIl2cppBinaryFixture(), { machine: 62 }))
.method_addresses,
);
});
it.each([
["no instruction loads the array", { machine: 62 }, { codeReference: false }],
["the image is not x86-64", { machine: 183 }, { codeReference: true }],
] as const)("keeps code-registration-not-found when %s", (_, elf, binary) => {
expect(
failure(
createElfFixture(
createIl2cppBinaryFixture({ codeRegistration: false, ...binary }),
elf,
),
).reason,
).toBe("code-registration-not-found");
});
it("refuses to choose among too many candidate module arrays", () => {
const base = createIl2cppBinaryFixture({
codeRegistration: false,
codeReference: true,
});
// Copy the two-slot module array (16 bytes at 0x500) 80 times after the image.
const copies = Buffer.concat(
Array.from({ length: 80 }, () => base.subarray(0x500, 0x510)),
);
const crafted = Buffer.concat([base, copies]);
crafted.writeBigUInt64LE(BigInt(crafted.length), 64);
crafted.writeBigUInt64LE(BigInt(crafted.length), 80);
// Mach-O is arm64 in the fixture; relabel it x86-64 so the fallback runs.
crafted.writeUInt32LE(0x01000007, 4);
crafted.writeUInt32LE(3, 8);
expect(failure(crafted).reason).toBe("native-index-capacity");
});
it("retains overlapping fallback arrays when only the later window is referenced", () => {
const bytes = createIl2cppBinaryFixture({
codeRegistration: false,
codeReference: true,
});
bytes.writeUInt32LE(0x01000007, 4);
bytes.writeUInt32LE(3, 8);
// [B, A, B] contains both [B, A] and [A, B]; code references only [A, B].
bytes.writeBigUInt64LE(FIXTURE_BINARY_BASE + 0x440n, 0x500);
bytes.writeBigUInt64LE(FIXTURE_BINARY_BASE + 0x400n, 0x508);
bytes.writeBigUInt64LE(FIXTURE_BINARY_BASE + 0x440n, 0x510);
bytes.writeInt32LE(0x508 - (0xc00 + 7), 0xc03);
const linked = link(bytes);
expect(linked.code_gen_modules_field).toBeNull();
expect(linked.code_gen_modules_array).toBe(FIXTURE_BINARY_BASE + 0x508n);
expect(linked.method_addresses).toEqual(expectedAddresses);
});
it("finds image names padded with 0xFF as MSVC writes them", () => {
expect(link(createIl2cppBinaryFixture({ namePadding: 0xff }))).toEqual(
link(createIl2cppBinaryFixture()),
);
});
it("does not match a longer string merely because its suffix names an image", () => {
const bytes = createIl2cppBinaryFixture({ namePrefix: "Mono." });
const suffix = bytes.readBigUInt64LE(0x440);
bytes.writeBigUInt64LE(suffix - 5n, 0x440); // The record actually names Mono.mscorlib.dll.
expect(failure(bytes).reason).toBe("code-gen-modules-not-found");
});
it("accepts a module's declared pointer into a suffix-shared C string", () => {
expect(
link(createIl2cppBinaryFixture({ namePrefix: "Mono." })).method_addresses,
).toEqual(expectedAddresses);
});
it("links an ELF shared object after applying its RELATIVE relocations", () => {
const elf = createElfFixture(createIl2cppBinaryFixture());
const fromMacho = link(createIl2cppBinaryFixture());
expect(link(elf)).toEqual({ ...fromMacho, format: "elf" });
});
it.each([183, 62])(
"links ELF relocations using the resolved PN_XNUM program count (machine %s)",
(machine) => {
const original = createElfFixture(createIl2cppBinaryFixture(), { machine });
const count = 65536;
const table = original.length;
const sectionZero = table + count * 56;
const extended = Buffer.concat([original, Buffer.alloc(count * 56 + 64)]);
// Keep mapped payload bytes untouched; the last declaration is beyond
// the raw 0xffff sentinel that the old relocation scan treated as a count.
original.copy(extended, table, 64, 120);
original.copy(extended, table + (count - 1) * 56, 120, 176);
extended.writeBigUInt64LE(BigInt(table), 32);
extended.writeBigUInt64LE(BigInt(sectionZero), 40);
extended.writeUInt16LE(0xffff, 56);
extended.writeUInt16LE(64, 58);
extended.writeUInt16LE(1, 60);
extended.writeUInt32LE(count, sectionZero + 44);
expect(link(extended).method_addresses).toEqual(
link(original).method_addresses,
);
},
);
it("ignores undefined PT_NULL fields while linking ELF relocations", () => {
const original = createElfFixture(createIl2cppBinaryFixture());
const bytes = Buffer.from(original);
bytes.writeUInt16LE(3, 56);
bytes.fill(0xff, 176, 232);
bytes.writeUInt32LE(0, 176);
expect(link(bytes).method_addresses).toEqual(link(original).method_addresses);
});
it.each(["outside", "misaligned", "duplicate"] as const)(
"refuses a %s dynamic declaration at relocation admission",
(shape) => {
const bytes = createElfFixture(createIl2cppBinaryFixture());
if (shape === "outside") bytes.writeBigUInt64LE(1n << 63n, 128);
else if (shape === "misaligned") bytes.writeBigUInt64LE(17n, 152);
else {
bytes.copy(bytes, 176, 120, 176);
bytes.writeUInt16LE(3, 56);
}
expect(() => readBinaryContainer(bytes)).not.toThrow();
expect(() => link(bytes)).toThrow(
shape === "duplicate"
? "multiple PT_DYNAMIC segments"
: "PT_DYNAMIC is truncated or outside the file",
);
},
);
it.each([
[{ dynamic: false }, "unsupported-container"],
[{ machine: 243 }, "unsupported-container"],
[{ androidPacked: true }, "unsupported-pointer-encoding"],
] as const)(
"refuses ELF relocations it cannot apply (%o)",
(options, reason) => {
expect(
failure(createElfFixture(createIl2cppBinaryFixture(), options)),
).toEqual({
reason,
kind: "unsupported",
});
},
);
it.each([
["Assembly CSharp.dll", "mscorlib.dll"],
["程序集.dll", "mscorlib.dll"],
["shared.dll", "shared.dll"],
] as const)(
"preserves module names and all candidate owners: %s / %s",
(first, second) => {
const imageNames = [first, second] as const;
const selected = {
...metadata,
images: metadata.images.map((image, index) => ({
...image,
name: imageNames[index]!,
})),
};
const linked = linkIl2cppBinary(
createIl2cppBinaryFixture({ imageNames }),
selected,
facts,
);
expect(linked.method_addresses).toEqual(expectedAddresses);
},
);
it("requires executable loader coverage before calling a mapped pointer code", () => {
const bytes = createIl2cppBinaryFixture();
bytes.writeUInt32LE(3, 92); // __TEXT is mapped read/write, with no execute permission.
expect(failure(bytes).reason).toBe("invalid-method-pointer");
});
it("does not treat RIP-relative-looking data as executable registration evidence", () => {
const bytes = createIl2cppBinaryFixture({
codeRegistration: false,
codeReference: true,
});
bytes.writeUInt32LE(0x01000007, 4);
bytes.writeUInt32LE(3, 92);
expect(failure(bytes).reason).toBe("code-registration-not-found");
});
it("yields throughout a large scan before reaching a later invalid method token", () => {
const bytes = Buffer.alloc(16 * 1024 * 1024, 0xaa);
createIl2cppBinaryFixture().copy(bytes);
bytes.writeBigUInt64LE(BigInt(bytes.length), 64);
bytes.writeBigUInt64LE(BigInt(bytes.length), 80);
const steps = linkIl2cppBinarySteps(bytes, metadata, {
...facts,
methodTokens: Uint32Array.from([0, ...facts.methodTokens.slice(1)]),
});
for (let index = 0; index < 200; index++)
expect(steps.next().done).toBe(false);
// Abandoning the cooperatively cancellable scan owns no external resources.
steps.return(undefined as never);
});
it("rejects a recursive binary type graph without overflowing the JavaScript stack", () => {
const bytes = createIl2cppBinaryFixture();
bytes.writeBigUInt64LE(FIXTURE_BINARY_BASE + 0x980n, 0x980);
expect(failure(bytes).reason).toBe("invalid-type");
});
it("guards a shared generic expansion before concatenating unchecked text", () => {
const expanded = readIl2cppMetadataForLinking(
createLinkedIl2cppMetadataFixture({
genericTypeArgumentCounts: { 3: 2 },
}).bytes,
);
const bytes = Buffer.alloc(0x4000);
createIl2cppBinaryFixture().copy(bytes);
bytes.writeBigUInt64LE(BigInt(bytes.length), 64);
bytes.writeBigUInt64LE(BigInt(bytes.length), 80);
let prior = FIXTURE_BINARY_BASE + 0x9b0n; // int
for (let level = 0; level < 12; level++) {
const at = 0x1000 + level * 96,
address = FIXTURE_BINARY_BASE + BigInt(at);
bytes.writeBigUInt64LE(address + 16n, at);
bytes.writeUInt32LE(0x15 << 16, at + 8);
bytes.writeBigUInt64LE(FIXTURE_BINARY_BASE + 0x9a0n, at + 16);
bytes.writeBigUInt64LE(address + 32n, at + 24);
bytes.writeUInt32LE(2, at + 32);
bytes.writeBigUInt64LE(address + 48n, at + 40);
bytes.writeBigUInt64LE(prior, at + 48);
bytes.writeBigUInt64LE(prior, at + 56);
prior = address;
}
bytes.writeBigUInt64LE(prior, 0x900);
const stopped = new Error("type text admission");
const steps = linkIl2cppBinarySteps(
bytes,
expanded.metadata,
expanded.link,
undefined,
{
beforeText: (size) => {
if (size > 1024) throw stopped;
},
},
);
expect(() => {
for (;;) {
if (steps.next().done) break;
}
}).toThrow(stopped);
});
it("reports a symbol-relocated method slot as unknown rather than no-body", () => {
const bytes = createElfFixture(createIl2cppBinaryFixture());
const dynamic = Number(bytes.readBigUInt64LE(128));
const relocationBytes = Number(bytes.readBigUInt64LE(dynamic + 24));
for (let at = 0x1000; at < 0x1000 + relocationBytes; at += 24) {
if (bytes.readBigUInt64LE(at) !== FIXTURE_BINARY_BASE + 0x300n) continue;
bytes.writeBigUInt64LE((1n << 32n) | 1025n, at + 8); // unresolved symbol, not RELATIVE
}
const linked = link(bytes);
expect(linked.method_addresses[1]).toEqual(expectedAddress("unknown"));
expect(linked.method_addresses[0]).toEqual(expectedAddresses[0]);
});
it.each([0n, FIXTURE_BINARY_BASE + 0x210n])(
"keeps a dyld-bound method slot unknown regardless of its raw word %s",
(raw) => {
const linked = link(createIl2cppDyldBindFixture(raw));
expect(linked.method_addresses[1]).toEqual(expectedAddress("unknown"));
expect(linked.method_addresses[0]).toEqual(expectedAddresses[0]);
expect(linked.method_addresses[4]).toEqual(expectedAddress("no-body"));
},
);
it("keeps unbound Mach-O method words classified from their values", () => {
const linked = link(createIl2cppBinaryFixture());
expect(linked.method_addresses[1]).toEqual(expectedAddresses[1]);
expect(linked.method_addresses[4]).toEqual(expectedAddress("no-body"));
});
it("refuses an unresolved inner type value instead of rendering its stored placeholder", () => {
expect(
failure(createIl2cppDyldBindFixture(FIXTURE_BINARY_BASE + 0x990n, 0x980)),
).toEqual({
reason: "unsupported-pointer-encoding",
kind: "unsupported",
});
});
it.each([
[0x600, 2n, "code-registration-not-found"],
[0x408, 3n, "code-gen-modules-not-found"],
[0x850, 4n, "metadata-registration-not-found"],
[0x860, 4n, "metadata-registration-not-found"],
[0x988, 0x001d0000n, "unsupported-pointer-encoding"],
] as const)("does not trust a dyld-owned scalar at %s", (slot, raw, reason) => {
expect(failure(createIl2cppDyldBindFixture(raw, slot)).reason).toBe(reason);
});
it.each([
[0x5f8, 0n, 2],
[0x5fc, 2n << 32n, "unknown"],
[0x600, 2n, "unknown"],
[0x604, 0n, 2],
] as const)(
"uses the four-byte count span for a dyld bind at %s",
(slot, raw, expected) => {
const steps = openImageSteps(
createIl2cppDyldBindFixture(raw, slot),
undefined,
);
let step = steps.next();
while (!step.done) step = steps.next();
expect(readUInt32(step.value, FIXTURE_BINARY_BASE + 0x600n)).toBe(expected);
},
);
it.each([
[0x400, "code-gen-modules-not-found"],
[0x410, "code-gen-modules-not-found"],
[0x500, "code-registration-not-found"],
[0x608, "code-registration-not-found"],
[0x838, "metadata-registration-not-found"],
] as const)(
"does not nominate a registration from dyld-owned word at %s",
(slot, reason) => {
const raw = createIl2cppBinaryFixture().readBigUInt64LE(slot);
expect(failure(createIl2cppDyldBindFixture(raw, slot)).reason).toBe(reason);
},
);
it.each([1, 0xff])(
"ignores ABI count padding byte %d without accepting a changed count",
(padding) => {
const bytes = createIl2cppBinaryFixture();
bytes.fill(padding, 0x604, 0x608);
expect(link(bytes)).toEqual(link(createIl2cppBinaryFixture()));
bytes.writeUInt32LE(3, 0x600);
expect(failure(bytes).reason).toBe("code-registration-not-found");
},
);
it.each([8, 16] as const)(
"rejects %d absent registration bytes without declared zero-fill",
(tailBytes) => {
expect(
failure(createIl2cppRegistrationTailFixture(tailBytes, false)).reason,
).toBe("metadata-registration-not-found");
},
);
it.each([
["PC32", 2],
["ABS32", 10],
] as const)(
"keeps a method pointer beside a non-overlapping %s relocation",
(_, kind) => {
const binary = createIl2cppBinaryFixture();
const adjacent = createElfFixture(binary, {
machine: 62,
unresolvedRelocations: [{ offset: 0x2fc, kind }],
});
expect(link(adjacent).method_addresses).toEqual(expectedAddresses);
const overlapping = createElfFixture(binary, {
machine: 62,
unresolvedRelocations: [{ offset: 0x300, kind }],
});
expect(link(overlapping).method_addresses[1]).toEqual(
expectedAddress("unknown"),
);
},
);
it("reads a relative relocation into a zero-filled pointer array as a known value", () => {
const bytes = createElfFixture(createIl2cppBinaryFixture(), {
zeroFilledTable: true,
});
const dynamic = Number(bytes.readBigUInt64LE(128));
const relocationBytes = Number(bytes.readBigUInt64LE(dynamic + 24));
// Reuse the now-unused second module's file-backed first slot relocation.
for (let at = 0x1000; at < 0x1000 + relocationBytes; at += 24) {
if (bytes.readBigUInt64LE(at) === FIXTURE_BINARY_BASE + 0x318n)
bytes.writeBigUInt64LE(FIXTURE_BINARY_BASE + BigInt(bytes.length), at);
}
expect(link(bytes).method_addresses).toEqual([
...expectedAddresses.slice(0, 4),
expectedAddress("no-body"),
]);
});
it("refuses an ambiguous architecture instead of choosing the first FAT slice", () => {
const bytes = createUniversalFixture([
{ cpu: 0x0100000c, bytes: createIl2cppBinaryFixture() },
{ cpu: 0x0100000c, bytes: createIl2cppBinaryFixture() },
]);
expect(failure(bytes, "arm64").reason).toBe("ambiguous-slice-selection");
});
it("checks the observed slice header against the caller-selected FAT architecture", () => {
const bytes = createUniversalFixture([
{ cpu: 0x01000007, bytes: createIl2cppBinaryFixture() },
]);
expect(failure(bytes, "amd64").reason).toBe("malformed-container");
});
it("still validates version 27 type indices when rendered names are unavailable", () => {
const decoded = readIl2cppMetadataForLinking(
createIl2cppMetadataFixture({ version: 27 }).bytes,
);
const input = {
...decoded.link,
returnTypes: Int32Array.from([999, ...decoded.link.returnTypes.slice(1)]),
};
expect(() =>
linkIl2cppBinary(createIl2cppBinaryFixture(), decoded.metadata, input),
).toThrow("invalid-type");
});
it("reads type attributes without requiring file-backed alignment padding", () => {
const bytes = createIl2cppBinaryFixture().subarray(0, 0xffc);
bytes.writeBigUInt64LE(BigInt(bytes.length), 80);
bytes.writeBigUInt64LE(FIXTURE_BINARY_BASE + 0xff0n, 0x900);
bytes.writeBigUInt64LE(0n, 0xff0);
bytes.writeUInt32LE(0x08 << 16, 0xff8);
expect(link(bytes).return_types.map((row) => row.name)).toEqual(
Array.from({ length: 5 }, () => "int"),
);
});
it("ignores CodeRegistration count alignment padding", () => {
const bytes = createIl2cppBinaryFixture();
bytes.fill(0xff, 0x604, 0x608);
expect(link(bytes).method_addresses).toEqual(expectedAddresses);
});
it.each([0, 8, 16])(
"finds registration with %s file-backed trailing zero bytes",
(tail) => {
const original = createIl2cppBinaryFixture();
const bytes = Buffer.alloc(0x1000 + tail);
original.copy(bytes);
original.copy(bytes, 0xf90, 0x800, 0x880);
bytes.fill(0, 0x800, 0x880);
bytes.writeBigUInt64LE(0x2000n, 64);
bytes.writeBigUInt64LE(BigInt(bytes.length), 80);
const linked = link(bytes);
expect(linked.types_field).toBe(FIXTURE_BINARY_BASE + 0xfc0n);
expect(linked.method_addresses).toEqual(expectedAddresses);
expect(linked.return_types).toEqual(link(original).return_types);
if (tail === 0) {
const missingTail = Buffer.from(bytes);
missingTail.writeBigUInt64LE(BigInt(bytes.length), 64);
expect(failure(missingTail).reason).toBe(
"metadata-registration-not-found",
);
}
},
);
it.each([0n, FIXTURE_BINARY_BASE + 0x980n, FIXTURE_BINARY_BASE + 0x9b0n])(
"retains unknown dyld-bound type names regardless of raw word %s",
(raw) => {
const control = link(createIl2cppBinaryFixture());
expect(control.return_types[0]).toEqual({
type: 0,
name: FIXTURE_RETURN_TYPE_NAME,
});
expect(control.method_addresses).toEqual(expectedAddresses);
const bytes = createIl2cppDyldBindFixture(raw, 0x900);
const linked = link(bytes);
for (const collection of [
"return_types",
"field_types",
"parameter_types",
] as const)
expect(linked[collection]).toEqual(
control[collection].map((row) =>
row.type === 0 ? { type: row.type, name: null } : row,
),
);
expect(linked.method_addresses).toEqual(control.method_addresses);
expect(bytes.readBigUInt64LE(0x900)).toBe(raw);
},
);