mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
3ce311757d
* feat(il2cpp): read generic containers on demand and render method spec names Generic container indices and records are demand readers on the same metadata snapshot, so links that do not ask for generic instances allocate nothing. Type rendering keeps one graph walk with its workspace, depth and cycle guards; the C#-like names and the Il2CppDumper GetMethodSpecName names are two styles over it. * feat(il2cpp): link generic method instances with admitted rows and kept issues generic_instances links every genericMethodTable row through methodSpecs, genericInsts and genericMethodPointers, choosing the one CodeRegistration layout whose counts bound every row. The table is reserved against the report budget before any row is resolved; rows and issues are admitted before retention, and a contradicting row stays unknown with an address-backed issue instead of failing the link. Views page the rows by definition, state, address and name, unrequested and historical collections stay unknown, and Ghidra seeds fold instances after the definitions. * feat(il2cpp): accept --generic-instances on the CLI and describe it in tool contracts * docs(il2cpp): describe generic method instances and their limits * fix(il2cpp): keep issued generic rows unknown and name their row A method spec that contradicted the metadata still reported its pointer slot as code, while the schema and provider limitation promised unknown. Such rows now keep no address, every issue carries its genericMethodTable row, the domain validator requires unknown exactly on issued rows, and shared specs name one other row instead of all of them. The row-size lower bound moves to the metadata admission owner, the hex helper is shared, and budget wording says checked rather than reserved. * test(il2cpp): reject ambiguous generic layouts and pin budget, yield and limit paths * fix(il2cpp): keep unmapped generic slots per row and reject undeclared arguments Layout selection no longer requires every genericMethodPointers slot to be mapped; each row reads its own slot, so one unmapped slot leaves only that row unknown with an issue. Method specs whose class or method argument count differs from the owner's declared generic container are issued and unnamed. Fixtures now declare the containers their generic rows fill. * fix(il2cpp): page generic method issues instead of retaining them in summaries Summaries keep generic_methods without its issues and count them in counts.generic_method_issues; the generic_method_issues collection pages them by rows, indices and fields, as code_gen_modules already does. Docs also name the method_indices filter the request schema accepts. * fix(il2cpp): read split generic arguments per slot and method containers on demand Generic argument arrays may span adjacent loader ranges: admit them with hasPointerArrayCoverage and read each slot through readPointer, instead of requiring the whole array in one mapping. Method generic container indices are read on demand from the method record, like type containers, so no link path allocates a per-method column. An owner whose container index is out of range now leaves the method spec unnamed with an issue instead of naming it unchecked. * fix(il2cpp): report generic entries past the address space at their row A damaged methodSpecs or genericMethodPointers pointer could make a spec or slot address exceed 64 bits. The issue kept that address, which the address schema rejects, so inspection failed instead of leaving the row unknown. Check the entry range first and attach the issue to the genericMethodTable row (or its slot field). * chore(il2cpp): remove unrelated formatting changes * fix: restore adb float pattern and mobile fixture from main The merge commit's lint-staged run rewrote these two main files: oxlint --fix dropped the escape in [+\-], which is invalid under the RegExp v flag. * fix(il2cpp): preserve unreadable generic method identities * docs(il2cpp): record generic-instance workflow verification * docs(il2cpp): qualify final integration checks --------- Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
1433 lines
48 KiB
TypeScript
1433 lines
48 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 {
|
|
GENERIC_TABLE_LIMIT,
|
|
linkGenericMethodsSteps,
|
|
} from "../../../src/il2cpp/Il2cppBinaryGenerics.js";
|
|
import {
|
|
createElfFixture,
|
|
createIl2cppBinaryFixture,
|
|
createIl2cppDyldBindFixture,
|
|
createIl2cppRegistrationTailFixture,
|
|
createUniversalFixture,
|
|
createGenericRegistrationImage,
|
|
GENERIC_IMAGE_BASE,
|
|
type GenericRegistrationImageOptions,
|
|
createSplitPointerPeFixture,
|
|
FIXTURE_BINARY_BASE,
|
|
FIXTURE_GENERIC_INSTANCES,
|
|
FIXTURE_METHOD_ADDRESSES,
|
|
type GenericMethodFixtureOptions,
|
|
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"),
|
|
);
|
|
});
|
|
|
|
// The generic owners declare the containers the fixture's instances fill.
|
|
const genericFacts = (version: 24 | 27 | 31 | 39) =>
|
|
readIl2cppMetadataForLinking(
|
|
createIl2cppMetadataFixture({ version, genericOwners: true }).bytes,
|
|
);
|
|
it("allocates no generic container column, reading method containers on demand", () => {
|
|
// Only the per-row columns every link uses are typed arrays; type rendering
|
|
// keeps each type's generic argument count, but no container index.
|
|
const { metadata: decoded, link } = genericFacts(31);
|
|
const columns = Object.entries(link)
|
|
.filter(([, value]) => ArrayBuffer.isView(value))
|
|
.map(([key]) => key)
|
|
.sort();
|
|
expect(columns).toEqual([
|
|
"fieldTypes",
|
|
"genericTypeArgumentCounts",
|
|
"methodTokens",
|
|
"parameterTypes",
|
|
"returnTypes",
|
|
]);
|
|
const count = decoded.methods.length;
|
|
expect(link.methodGenericContainer(-1)).toBeUndefined();
|
|
expect(link.methodGenericContainer(count)).toBeUndefined();
|
|
// ToString<int> (method 4) is generic; .ctor (method 3) is not.
|
|
expect(link.methodGenericContainer(3)).toBe(-1);
|
|
expect(link.methodGenericContainer(4)).toBeGreaterThanOrEqual(0);
|
|
});
|
|
const linkGeneric = (
|
|
options: GenericMethodFixtureOptions = {},
|
|
version: 24 | 27 | 31 | 39 = 31,
|
|
bytes = createIl2cppBinaryFixture({ genericMethods: options }),
|
|
) => {
|
|
const decoded = genericFacts(version);
|
|
return linkIl2cppBinary(bytes, decoded.metadata, decoded.link, undefined, {
|
|
genericInstances: true,
|
|
});
|
|
};
|
|
const genericFailure = (
|
|
options: GenericMethodFixtureOptions,
|
|
version: 24 | 27 | 31 | 39 = 31,
|
|
) => {
|
|
try {
|
|
linkGeneric(options, version);
|
|
} catch (cause) {
|
|
if (cause instanceof Il2cppBinaryFormatFailure)
|
|
return { reason: cause.reason, kind: cause.kind };
|
|
throw cause;
|
|
}
|
|
throw new Error("expected a link failure");
|
|
};
|
|
const at = (offset: bigint) =>
|
|
`0x${(FIXTURE_BINARY_BASE + offset).toString(16)}`;
|
|
const unknownRow = { state: "unknown", address: null, rva: null } as const;
|
|
// Smallest serialized generic row, checked per table entry before resolving.
|
|
const minimumGenericRow = JSON.stringify({
|
|
...unknownRow,
|
|
method_spec: 0,
|
|
method: 0,
|
|
name: null,
|
|
}).length;
|
|
|
|
it("links generic method instances only when asked", () => {
|
|
// Not requested is unknown, not an empty table.
|
|
expect(
|
|
link(createIl2cppBinaryFixture({ genericMethods: {} })).generic_methods,
|
|
).toBeNull();
|
|
const linked = linkGeneric();
|
|
expect(linked.method_addresses).toEqual(expectedAddresses);
|
|
expect(linked.generic_methods).toEqual({
|
|
generic_method_pointers_field: FIXTURE_BINARY_BASE + 0x600n - 104n + 16n,
|
|
generic_method_pointers_count: 3,
|
|
generic_method_table: FIXTURE_BINARY_BASE + 0xbc0n,
|
|
generic_method_table_count: 3,
|
|
generic_method_table_stride: 16,
|
|
method_specs_count: 3,
|
|
generic_insts_count: 2,
|
|
instances: FIXTURE_GENERIC_INSTANCES,
|
|
issues: [],
|
|
});
|
|
});
|
|
|
|
it("admits every generic row and issue before retention", () => {
|
|
const decoded = genericFacts(31);
|
|
const rows: Readonly<Record<string, unknown>>[] = [];
|
|
const checked: number[] = [];
|
|
linkIl2cppBinary(
|
|
createIl2cppBinaryFixture({ genericMethods: { firstSpecMethod: 9 } }),
|
|
decoded.metadata,
|
|
decoded.link,
|
|
undefined,
|
|
{
|
|
genericInstances: true,
|
|
onRow: (row) => rows.push(row),
|
|
beforeText: (bytes) => checked.push(bytes),
|
|
},
|
|
);
|
|
// The whole table is checked before the first row is resolved.
|
|
expect(checked).toContain(3 * minimumGenericRow);
|
|
expect(rows.filter((row) => "method_spec" in row)).toHaveLength(3);
|
|
expect(rows).toContainEqual({
|
|
row: 0,
|
|
address: at(0xb80n),
|
|
message: "method spec 0 names method 9 outside the 5 metadata methods",
|
|
});
|
|
// A refused budget check stops before any generic row is retained.
|
|
const refused: unknown[] = [];
|
|
expect(() =>
|
|
linkIl2cppBinary(
|
|
createIl2cppBinaryFixture({ genericMethods: {} }),
|
|
decoded.metadata,
|
|
decoded.link,
|
|
undefined,
|
|
{
|
|
genericInstances: true,
|
|
onRow: (row) => refused.push(row),
|
|
beforeText: (bytes) => {
|
|
if (bytes === 3 * minimumGenericRow) throw new Error("budget");
|
|
},
|
|
},
|
|
),
|
|
).toThrow("budget");
|
|
expect(
|
|
refused.some(
|
|
(row) => typeof row === "object" && row !== null && "method_spec" in row,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("reports entries past the 64-bit address space at their table row", () => {
|
|
const top = 0xffffffffffffffffn;
|
|
// Spec 1 from the last address is 0x1000000000000000b; every row stays
|
|
// unknown and each issue keeps an address inside the binary.
|
|
const specs = linkGeneric({ methodSpecs: top }).generic_methods!;
|
|
expect(specs.instances.map((row) => row.state)).toEqual([
|
|
"unknown",
|
|
"unknown",
|
|
"unknown",
|
|
]);
|
|
expect(specs.issues.map((entry) => entry.address)).toEqual([
|
|
at(0xbc0n),
|
|
at(0xbd0n),
|
|
at(0xbe0n),
|
|
]);
|
|
expect(specs.issues[1]!.message).toBe(
|
|
"method spec 1 lies outside the 64-bit address space from methodSpecs at 0xffffffffffffffff",
|
|
);
|
|
const slots = linkGeneric({ genericMethodPointers: top }).generic_methods!;
|
|
expect(slots.instances.map((row) => row.state)).toEqual([
|
|
"unknown",
|
|
"unknown",
|
|
"unknown",
|
|
]);
|
|
expect(slots.issues).toContainEqual({
|
|
row: 1,
|
|
address: at(0xbd4n),
|
|
message:
|
|
"genericMethodPointers slot 1 lies outside the 64-bit address space from genericMethodPointers at 0xffffffffffffffff",
|
|
});
|
|
});
|
|
|
|
it("keeps unreadable method-definition indexes unknown instead of inventing -1", () => {
|
|
const generic = linkGeneric({
|
|
methodSpecs: 0xffffffffffffffffn,
|
|
}).generic_methods!;
|
|
expect(generic.instances.map((row) => row.method)).toEqual([
|
|
null,
|
|
null,
|
|
null,
|
|
]);
|
|
expect(generic.instances.every((row) => row.state === "unknown")).toBe(true);
|
|
});
|
|
|
|
it("retains a recorded negative method-definition index as source evidence", () => {
|
|
const generic = linkGeneric({ firstSpecMethod: -1 }).generic_methods!;
|
|
expect(generic.instances[0]).toMatchObject({
|
|
method: -1,
|
|
state: "unknown",
|
|
name: null,
|
|
});
|
|
expect(generic.issues[0]?.message).toContain("method -1");
|
|
});
|
|
|
|
it("selects the 27.0 layout by its 12-byte rows and agreeing counts", () => {
|
|
const linked = linkGeneric({ stride: 12 }, 27);
|
|
expect(linked.generic_methods).toMatchObject({
|
|
generic_method_pointers_field: FIXTURE_BINARY_BASE + 0x600n - 96n + 16n,
|
|
generic_method_table_stride: 12,
|
|
instances: FIXTURE_GENERIC_INSTANCES,
|
|
});
|
|
// Header version 27 also covers 27.1 and 27.2, which add adjustor thunks.
|
|
expect(linkGeneric({ stride: 16 }, 27).generic_methods).toMatchObject({
|
|
generic_method_pointers_field: FIXTURE_BINARY_BASE + 0x600n - 104n + 16n,
|
|
generic_method_table_stride: 16,
|
|
instances: FIXTURE_GENERIC_INSTANCES,
|
|
});
|
|
// Version 31 has no 12-byte-row layout to try.
|
|
expect(genericFailure({ stride: 12 }, 31).reason).toBe(
|
|
"generic-method-registration-not-found",
|
|
);
|
|
});
|
|
|
|
it.each<
|
|
[string, GenericMethodFixtureOptions, 24 | 27 | 31 | 39, string, string]
|
|
>([
|
|
[
|
|
"a binary without CodeRegistration generic fields",
|
|
{ codeRegistration: false },
|
|
31,
|
|
"generic-method-registration-not-found",
|
|
"malformed",
|
|
],
|
|
[
|
|
"a row past genericMethodPointersCount",
|
|
{ firstPointerSlot: 3 },
|
|
31,
|
|
"generic-method-registration-not-found",
|
|
"malformed",
|
|
],
|
|
[
|
|
// Its type-definition container offset and generic class layout are
|
|
// unchecked, although a 24.2 CodeRegistration layout would agree here.
|
|
"metadata version 24",
|
|
{ stride: 12 },
|
|
24,
|
|
"generic-methods-unsupported",
|
|
"unsupported",
|
|
],
|
|
["metadata version 39", {}, 39, "generic-methods-unsupported", "unsupported"],
|
|
])("refuses generic linking for %s", (_, options, version, reason, kind) => {
|
|
expect(genericFailure(options, version)).toEqual({ reason, kind });
|
|
});
|
|
|
|
it("keeps contradicting rows unknown and reports the record address", () => {
|
|
const issues = (options: GenericMethodFixtureOptions) => {
|
|
const generic = linkGeneric(options).generic_methods!;
|
|
return { first: generic.instances[0], issues: generic.issues };
|
|
};
|
|
// A contradicting method spec keeps no address, although its slot is valid.
|
|
expect(issues({ firstSpecMethod: 9 })).toEqual({
|
|
first: {
|
|
...FIXTURE_GENERIC_INSTANCES[0],
|
|
...unknownRow,
|
|
method: 9,
|
|
name: null,
|
|
},
|
|
issues: [
|
|
{
|
|
row: 0,
|
|
address: at(0xb80n),
|
|
message: "method spec 0 names method 9 outside the 5 metadata methods",
|
|
},
|
|
],
|
|
});
|
|
expect(issues({ firstSpecClass: 2 })).toEqual({
|
|
first: { ...FIXTURE_GENERIC_INSTANCES[0], ...unknownRow, name: null },
|
|
issues: [
|
|
{
|
|
row: 0,
|
|
address: at(0xb80n),
|
|
message:
|
|
"method spec 0 names generic instances 2 and -1, outside the 2 readable genericInsts",
|
|
},
|
|
],
|
|
});
|
|
const outside = FIXTURE_BINARY_BASE + 0x10000n;
|
|
expect(issues({ firstSlot: outside })).toEqual({
|
|
first: { ...FIXTURE_GENERIC_INSTANCES[0], ...unknownRow },
|
|
issues: [
|
|
{
|
|
row: 0,
|
|
address: at(0xd00n),
|
|
message: `genericMethodPointers slot 0 points outside unambiguous executable file data at 0x${outside.toString(16)}`,
|
|
},
|
|
],
|
|
});
|
|
// Il2CppDumper keys pointers by method spec and refuses a second row, so
|
|
// neither row's name or address is kept.
|
|
const duplicate = linkGeneric({ duplicateSpec: true }).generic_methods!;
|
|
expect(
|
|
duplicate.instances.map(({ name, state }) => ({ name, state })),
|
|
).toEqual([
|
|
{ name: null, state: "unknown" },
|
|
{ name: FIXTURE_GENERIC_INSTANCES[1]!.name, state: "code" },
|
|
{ name: null, state: "unknown" },
|
|
]);
|
|
expect(duplicate.issues).toEqual([
|
|
{
|
|
row: 0,
|
|
address: at(0xbc0n),
|
|
message:
|
|
"method spec 0 is shared by 2 genericMethodTable rows, including row 2",
|
|
},
|
|
{
|
|
row: 2,
|
|
address: at(0xbc0n + 32n),
|
|
message:
|
|
"method spec 0 is shared by 2 genericMethodTable rows, including row 0",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it.each<[string, GenericMethodFixtureOptions, string]>([
|
|
[
|
|
"class arguments for a type without a generic container",
|
|
{ firstSpecMethod: 2 },
|
|
"method spec 0 gives type 1 1 generic arguments, but it declares 0",
|
|
],
|
|
[
|
|
"class arguments that disagree with the container's count",
|
|
{ firstSpecClass: 1 },
|
|
"method spec 0 gives type 3 2 generic arguments, but it declares 1",
|
|
],
|
|
[
|
|
"method arguments for a method without a generic container",
|
|
{ firstSpecMethodInstance: 0 },
|
|
"method spec 0 gives method 3 1 generic arguments, but it declares 0",
|
|
],
|
|
])("keeps a method spec with %s unnamed", (_, options, message) => {
|
|
const generic = linkGeneric(options).generic_methods!;
|
|
expect(generic.instances[0]).toMatchObject({ name: null, ...unknownRow });
|
|
expect(generic.instances.slice(1)).toEqual(
|
|
FIXTURE_GENERIC_INSTANCES.slice(1),
|
|
);
|
|
expect(generic.issues).toEqual([{ row: 0, address: at(0xb80n), message }]);
|
|
});
|
|
|
|
it("keeps a method spec unnamed when its owner's container is out of range", () => {
|
|
// Method 4 (ToString<int>) points past the decoded generic containers.
|
|
const decoded = genericFacts(31);
|
|
const link = {
|
|
...decoded.link,
|
|
methodGenericContainer: (index: number) =>
|
|
index === 4 ? 99 : decoded.link.methodGenericContainer(index),
|
|
};
|
|
const generic = linkIl2cppBinary(
|
|
createIl2cppBinaryFixture({ genericMethods: {} }),
|
|
decoded.metadata,
|
|
link,
|
|
undefined,
|
|
{ genericInstances: true },
|
|
).generic_methods!;
|
|
expect(generic.instances).toEqual([
|
|
FIXTURE_GENERIC_INSTANCES[0],
|
|
FIXTURE_GENERIC_INSTANCES[1],
|
|
{ ...FIXTURE_GENERIC_INSTANCES[2], name: null, ...unknownRow },
|
|
]);
|
|
expect(generic.issues).toEqual([
|
|
{
|
|
row: 2,
|
|
address: at(0xb80n + 24n),
|
|
message:
|
|
"method spec 2 gives method 4 1 generic arguments, but its generic container 99 is out of range",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps a generic slot with an unresolved relocation unknown", () => {
|
|
// An ELF symbol relocation on slot 1 leaves its pointer unresolved.
|
|
const bytes = createElfFixture(
|
|
createIl2cppBinaryFixture({ genericMethods: {} }),
|
|
);
|
|
const dynamic = Number(bytes.readBigUInt64LE(128));
|
|
const relocationBytes = Number(bytes.readBigUInt64LE(dynamic + 24));
|
|
for (let entry = 0x1000; entry < 0x1000 + relocationBytes; entry += 24)
|
|
if (bytes.readBigUInt64LE(entry) === FIXTURE_BINARY_BASE + 0xd08n)
|
|
bytes.writeBigUInt64LE((1n << 32n) | 1025n, entry + 8);
|
|
const generic = linkGeneric({}, 31, bytes).generic_methods!;
|
|
expect(generic.instances[1]).toEqual({
|
|
...FIXTURE_GENERIC_INSTANCES[1],
|
|
...unknownRow,
|
|
});
|
|
expect(generic.issues).toEqual([
|
|
{
|
|
row: 1,
|
|
address: at(0xd08n),
|
|
message: "genericMethodPointers slot 1 has an unresolved relocation",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps the other generic rows when one pointer slot is unmapped", () => {
|
|
// Slot 2 lies past the only segment; rows 0 and 1 keep their code.
|
|
const generic = linkGeneric({ unmappedLastSlot: true }).generic_methods!;
|
|
expect(generic.instances).toEqual([
|
|
FIXTURE_GENERIC_INSTANCES[0],
|
|
FIXTURE_GENERIC_INSTANCES[1],
|
|
{ ...FIXTURE_GENERIC_INSTANCES[2], ...unknownRow },
|
|
]);
|
|
expect(generic.issues).toEqual([
|
|
{
|
|
row: 2,
|
|
address: at(0x1000n),
|
|
message: "genericMethodPointers slot 2 is not file mapped",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("names generic instances whose argument array spans two segments", () => {
|
|
// <Größe<int>, int> keeps its arguments at 0xb68 and 0xb70, one per segment.
|
|
const bytes = createIl2cppBinaryFixture({
|
|
genericMethods: {},
|
|
splitSegmentAt: 0xb70,
|
|
});
|
|
const generic = linkGeneric({}, 31, bytes).generic_methods!;
|
|
expect(generic.instances).toEqual(FIXTURE_GENERIC_INSTANCES);
|
|
expect(generic.issues).toEqual([]);
|
|
});
|
|
|
|
it("returns no instances for an empty genericMethodTable without picking a layout", () => {
|
|
// With no rows every readable layout bounds the table, so none is chosen.
|
|
expect(linkGeneric({ emptyTable: true }).generic_methods).toEqual({
|
|
generic_method_pointers_field: null,
|
|
generic_method_pointers_count: null,
|
|
generic_method_table: FIXTURE_BINARY_BASE + 0xbc0n,
|
|
generic_method_table_count: 0,
|
|
generic_method_table_stride: null,
|
|
method_specs_count: 3,
|
|
generic_insts_count: 2,
|
|
instances: [],
|
|
issues: [],
|
|
});
|
|
});
|
|
|
|
const drain = <T>(steps: Generator<void, T>, onYield = () => {}): T => {
|
|
for (;;) {
|
|
const step = steps.next();
|
|
if (step.done === true) return step.value;
|
|
onYield();
|
|
}
|
|
};
|
|
// Link a low-base image of generic tables only, recording yields and rows.
|
|
const linkLowBase = (
|
|
options: GenericRegistrationImageOptions = {},
|
|
events: string[] = [],
|
|
checked: number[] = [],
|
|
) => {
|
|
const fixture = createGenericRegistrationImage(options);
|
|
const decoded = genericFacts(31);
|
|
return drain(
|
|
linkGenericMethodsSteps(
|
|
drain(openImageSteps(fixture.bytes, undefined)),
|
|
decoded.metadata,
|
|
decoded.link,
|
|
fixture.modulesField,
|
|
fixture.metadataRegistration,
|
|
{
|
|
beforeText: (bytes) => checked.push(bytes),
|
|
onRow: (row) => events.push("method_spec" in row ? "row" : "issue"),
|
|
},
|
|
),
|
|
() => events.push("yield"),
|
|
);
|
|
};
|
|
const lowBaseFailure = (options: GenericRegistrationImageOptions) => {
|
|
try {
|
|
linkLowBase(options);
|
|
} catch (cause) {
|
|
if (cause instanceof Il2cppBinaryFormatFailure) return cause.reason;
|
|
throw cause;
|
|
}
|
|
throw new Error("expected a link failure");
|
|
};
|
|
|
|
it("refuses generic rows that two CodeRegistration layouts both bound", () => {
|
|
// The 120-byte layout alone is chosen when the 104-byte one disagrees.
|
|
expect(linkLowBase()).toMatchObject({
|
|
generic_method_pointers_field: GENERIC_IMAGE_BASE + 0x600n - 120n + 16n,
|
|
generic_method_table_stride: 16,
|
|
});
|
|
// A low base lets a pointer double as a count, so both 29/31 layouts agree.
|
|
expect(lowBaseFailure({ ambiguous: true })).toBe(
|
|
"ambiguous-generic-method-registration",
|
|
);
|
|
});
|
|
|
|
it("treats a zero-filled method spec as unobserved, not as method 0", () => {
|
|
const generic = linkLowBase({ zeroFilledSpecs: true });
|
|
expect(generic.instances.map(({ state, name }) => ({ state, name }))).toEqual(
|
|
Array.from({ length: 3 }, () => ({ state: "unknown", name: null })),
|
|
);
|
|
expect(generic.issues.map(({ row, message }) => ({ row, message }))).toEqual(
|
|
[0, 1, 2].map((row) => ({
|
|
row,
|
|
message: `method spec ${row} is not file backed`,
|
|
})),
|
|
);
|
|
});
|
|
|
|
it("yields before each pass over the genericMethodTable", () => {
|
|
const events: string[] = [];
|
|
linkLowBase({}, events);
|
|
// Two candidate layouts, the shared-spec pass, then rows with their issues.
|
|
expect(events).toEqual([
|
|
"yield",
|
|
"yield",
|
|
"yield",
|
|
"yield",
|
|
...Array.from({ length: 3 }, () => ["issue", "row"]).flat(),
|
|
]);
|
|
});
|
|
|
|
it("refuses a genericMethodTable count past the table limit before checking the budget", () => {
|
|
const checked: number[] = [];
|
|
expect(() =>
|
|
linkLowBase({ tableCount: BigInt(GENERIC_TABLE_LIMIT) + 1n }, [], checked),
|
|
).toThrow("generic-method-registration-not-found");
|
|
expect(checked).toEqual([]);
|
|
});
|
|
|
|
it("keeps a capacity refusal while naming a generic row a refusal", () => {
|
|
const decoded = genericFacts(31);
|
|
let tableChecked = false;
|
|
const failure = (() => {
|
|
try {
|
|
linkIl2cppBinary(
|
|
createIl2cppBinaryFixture({ genericMethods: {} }),
|
|
decoded.metadata,
|
|
decoded.link,
|
|
undefined,
|
|
{
|
|
genericInstances: true,
|
|
beforeText: (bytes) => {
|
|
if (tableChecked)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"native-type-text-capacity",
|
|
"budget",
|
|
);
|
|
tableChecked = bytes === 3 * minimumGenericRow;
|
|
},
|
|
},
|
|
);
|
|
} catch (cause) {
|
|
return cause;
|
|
}
|
|
return undefined;
|
|
})();
|
|
expect(tableChecked).toBe(true);
|
|
expect(failure).toBeInstanceOf(Il2cppBinaryFormatFailure);
|
|
expect((failure as Il2cppBinaryFormatFailure).kind).toBe("capacity");
|
|
});
|
|
|
|
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);
|
|
},
|
|
);
|