mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 18:05:45 +00:00
* fix(il2cpp): accept arm64 module arrays loaded through adrp/add pairs A Unity 2022.3 macOS Master (LTO) arm64 build keeps no static CodeRegistration, so the module-array fallback that only decoded x86-64 RIP-relative references reported code-registration-not-found. The registration owner now also decodes aligned adrp + add pairs in executable ranges and keeps the same exact-module-set and single-reference rules. A minimal Mach-O fixture failed on the previous reader (47 passed / 1 failed) and passes here with four fail-closed variants (48/48 binary tests). * fix(il2cpp): name the matched instruction form in inferred module-array limits The inferred-array limitation always described RIP-relative LEA/MOV, which is wrong for arm64 results. The provider now names the adrp/add pair on arm64, and the tool contract and docs cover both forms and state that a located array alone does not finish linking a Master build. The new arm64 provider case failed before the wording change (1 passed / 1 failed) and both cases pass now. * fix(il2cpp): accept type tables loaded without a static MetadataRegistration A Unity 2022.3.62f3 macOS Master build inlines MetadataRegistration as well as CodeRegistration, so linking stopped at metadata-registration-not-found after the module array was found. Registration now infers the only field linking reads, the Il2CppType table, from the one run of type-shaped pointers that covers every referenced type index and whose start is loaded by the existing RIP-relative or adrp/add forms; types_field is null and the Evidence is inferred. The new arm64 and x86-64 fixtures failed on the previous reader (52 passed / 2 failed). Four run-selection variants (interior reference, run extended before the loaded address, two loaded runs, run shorter than the referenced indices) keep metadata-registration-not-found; instruction decoding and record-shape checks are not covered by these fixtures. * test(il2cpp): pin adrp page deltas, register pairing and type-record shape Every adrp fixture targeted its own 4 KiB page, so dropping the page delta or its sign extension, the add register check, the type-kind bound or the type-definition bound left all binary tests passing. Fixtures now reach a later and an earlier page, pair an add with a different destination or source register, break the type run with an out-of-range kind or type-definition index, and bound type-table candidates when the metadata references few type indices. Each of those five source mutations now fails 1 to 3 of 85 binary and provider tests. * refactor(il2cpp): share Il2CppType kind decoding and document type-table bounds The type-table fallback decoded the Il2CppType kind and class/value-type kinds separately from the type namer; both now use the Types owner. Zero and unaligned words are rejected before mapping lookup (Master sample metadata-registration search 92 to 68 ms). Docs and the limitation state that types_count is an upper bound, that a run spanning mappings or a small referenced type range can fail, and credit Cpp2IL StrippedCodeRegSupport and the data-side MetadataRegistration searches of Il2CppDumper and Cpp2IL. Master sample output is unchanged (11,031 code addresses, 37,025 type rows). * fix(il2cpp): refuse generic instances when the type table was inferred An inferred type table has no static MetadataRegistration, so the genericMethodTable it would anchor cannot be located. Report generic-methods-unsupported instead of reading from a null field. * fix(il2cpp): fail closed when an inferred type table is not unique Adversarial review found that the type-table fallback could pick another type-shaped pointer table when the real one was unloaded or excluded, could replace a static MetadataRegistration whose counts matched, and accepted a run cut short by a mapping end. Require exactly one long-enough run in the image, ending inside its mapping and loaded by code; skip the fallback when a record's two counts matched; accept only assigned Il2CppTypeEnum kinds; and look records up through the indexed file mapping so many mappings cannot multiply the scan. Docs and limitations now describe the run length as at least every referenced type index rather than an upper bound. * fix(il2cpp): keep the type-table fallback cheap and its refusals exact Review follow-ups: check that an address can lie in a mapping before the throwing indexed lookup, so words pointing into gaps or zero-fill no longer build an exception each (64 MiB: 74 s to under 2 s); treat a run starting at its mapping start like one reaching its end; stop at the second long-enough run instead of a 64-candidate capacity error; ignore ADRP/ADD pairs on register 31; and say whether a counted MetadataRegistration was unusable or no code-loaded run was found. Docs no longer generalize from one build or state tool history they do not source. * fix(il2cpp): judge type-table slots by their loaded value The run scan read stored words, so a dyld-bound slot's placeholder could decide whether a run qualified. Confirm each slot through readSlot and give a run bounded by a bound or unresolved slot an unknown extent, so the placeholder no longer changes the choice. * docs(il2cpp): keep the failure-reason table layout Shorten the two changed meanings so the table keeps its column widths and the diff shows only those rows. * docs(il2cpp): keep the reason table at its formatted width * fix(il2cpp): read type-table record fields through the loader too The previous change confirmed table slots through readSlot but still read each record's kind and definition index from stored bytes. Read them with readUInt32 and readSlot, and treat a record field the loader binds or leaves unresolved like an unresolved slot: the run it bounds has an unknown extent. * perf(il2cpp): share image mappings across loader-aware reads --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
637 lines
24 KiB
TypeScript
637 lines
24 KiB
TypeScript
import {
|
|
BinaryFormatFailure,
|
|
mappedFileOffset,
|
|
} from "../native/metadata/BinaryContainer.js";
|
|
import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js";
|
|
import {
|
|
isMapped,
|
|
hasPointerArrayCoverage,
|
|
readPointer,
|
|
readSlot,
|
|
readUInt32,
|
|
type NativeImage,
|
|
} from "./Il2cppBinaryImage.js";
|
|
import type { Il2cppMetadataDecoded } from "./Il2cppMetadataFile.js";
|
|
import {
|
|
IL2CPP_TYPE_KIND_MAX,
|
|
il2cppTypeKind,
|
|
isDefinitionTypeKind,
|
|
} from "./Il2cppBinaryTypes.js";
|
|
import type { Il2cppCodeGenModule } from "./Il2cppBinaryFile.js";
|
|
// Bound the retained name-address index independently of binary size. No
|
|
// repeated name is evidence of ambiguous module ownership by itself.
|
|
const TOTAL_NAME_OCCURRENCES = 65536;
|
|
const SCAN_BYTES = 65536;
|
|
|
|
interface NameTrie {
|
|
readonly next: Map<number, NameTrie>;
|
|
readonly owners: number[];
|
|
}
|
|
|
|
/** Reverse byte matching permits C-string suffix sharing. Only a module's
|
|
* declared pointer later establishes which exact candidate it actually names.
|
|
* No decoding/alphabet guess or per-name binary rescan is needed.
|
|
*/
|
|
function* nameAddresses(
|
|
image: NativeImage,
|
|
names: readonly string[],
|
|
): Generator<void, Map<bigint, readonly number[]>> {
|
|
const root: NameTrie = { next: new Map(), owners: [] };
|
|
let nodes = 1,
|
|
work = 0;
|
|
for (const [index, name] of names.entries()) {
|
|
if (++work % 256 === 0) yield;
|
|
if (name === "") continue;
|
|
if (Buffer.byteLength(name) > TOTAL_NAME_OCCURRENCES)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"native-index-capacity",
|
|
"module-name trie exceeds its65536-node workspace",
|
|
);
|
|
let node = root;
|
|
const bytes = Buffer.from(name, "utf8");
|
|
for (let at = bytes.length - 1; at >= 0; at--) {
|
|
if (++work % 4096 === 0) yield;
|
|
let next = node.next.get(bytes[at]!);
|
|
if (next === undefined) {
|
|
if (nodes >= TOTAL_NAME_OCCURRENCES)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"native-index-capacity",
|
|
"module-name trie exceeds its65536-node workspace",
|
|
);
|
|
next = { next: new Map(), owners: [] };
|
|
node.next.set(bytes[at]!, next);
|
|
nodes++;
|
|
}
|
|
node = next;
|
|
}
|
|
node.owners.push(index);
|
|
}
|
|
const found = new Map<bigint, readonly number[]>();
|
|
for (const mapping of image.container.mappings) {
|
|
const region = image.bytes.subarray(
|
|
mapping.offset,
|
|
mapping.offset + mapping.size,
|
|
);
|
|
for (let chunk = 0; chunk < region.length; chunk += SCAN_BYTES) {
|
|
yield;
|
|
const part = region.subarray(
|
|
chunk,
|
|
Math.min(chunk + SCAN_BYTES, region.length),
|
|
);
|
|
for (
|
|
let end = part.indexOf(0);
|
|
end >= 0;
|
|
end = part.indexOf(0, end + 1)
|
|
) {
|
|
let node: NameTrie | undefined = root;
|
|
for (let at = chunk + end - 1; at >= 0; at--) {
|
|
if (++work % 4096 === 0) yield;
|
|
node = node.next.get(region[at]!);
|
|
if (node === undefined) break;
|
|
if (node.owners.length === 0) continue;
|
|
const address = mapping.address + BigInt(at);
|
|
if (!found.has(address) && found.size >= TOTAL_NAME_OCCURRENCES)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"native-index-capacity",
|
|
`native name-address index exceeds ${TOTAL_NAME_OCCURRENCES} entries`,
|
|
);
|
|
found.set(address, node.owners);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
/** Method count per image from its contiguous type-definition range. */
|
|
function* imageMethodCounts(
|
|
metadata: Il2cppMetadataDecoded,
|
|
): Generator<void, number[]> {
|
|
const counts: number[] = [];
|
|
for (const image of metadata.images) {
|
|
yield;
|
|
let total = 0;
|
|
for (let offset = 0; offset < image.type_count; offset++) {
|
|
if (offset % 256 === 0) yield;
|
|
total += metadata.types[image.type_start + offset]!.method_count;
|
|
}
|
|
counts.push(total);
|
|
}
|
|
return counts;
|
|
}
|
|
|
|
/**
|
|
* Find Il2CppCodeGenModule records: a moduleName pointer to an image name
|
|
* followed by methodPointerCount equal to that image's method count.
|
|
*/
|
|
export function* findModules(
|
|
image: NativeImage,
|
|
metadata: Il2cppMetadataDecoded,
|
|
): Generator<void, Il2cppCodeGenModule[]> {
|
|
const names = yield* nameAddresses(
|
|
image,
|
|
metadata.images.map((entry) => entry.name),
|
|
);
|
|
const counts = yield* imageMethodCounts(metadata);
|
|
const candidates = new Map<number, Il2cppCodeGenModule[]>();
|
|
for (const mapping of image.container.mappings) {
|
|
const start = Number((8n - (mapping.address % 8n)) % 8n);
|
|
for (let at = start; at + 24 <= mapping.size; at += 8) {
|
|
if ((at & 0xffff) < 8) yield;
|
|
const offset = mapping.offset + at;
|
|
const target = image.bytes.readBigUInt64LE(offset);
|
|
const indices = names.get(target);
|
|
if (indices === undefined) continue;
|
|
const address = mapping.address + BigInt(at);
|
|
// Matching stored bytes nominate a candidate, not a resolved pointer.
|
|
if (readPointer(image, address) !== target) continue;
|
|
let recordOffset: number;
|
|
try {
|
|
recordOffset = mappedFileOffset(
|
|
image.container.mappings,
|
|
address,
|
|
24,
|
|
image.container.zero_fills,
|
|
);
|
|
} catch (cause) {
|
|
if (cause instanceof BinaryFormatFailure)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"malformed-container",
|
|
cause.message,
|
|
);
|
|
throw cause;
|
|
}
|
|
if (recordOffset !== offset)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"malformed-container",
|
|
"code generation module record disagrees with its load mapping",
|
|
);
|
|
let work = 0;
|
|
for (const index of indices) {
|
|
if (++work % 256 === 0) yield;
|
|
const count = readUInt32(image, address + 8n);
|
|
if (count !== counts[index]) continue;
|
|
const pointers = readPointer(image, address + 16n);
|
|
if (pointers === null) continue;
|
|
if (!hasPointerArrayCoverage(image, pointers, count)) continue;
|
|
const list = candidates.get(index) ?? [];
|
|
if (list.some((candidate) => candidate.address === address)) continue;
|
|
if (list.length !== 0)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"ambiguous-code-gen-module",
|
|
`multiple records match code generation module ${metadata.images[index]!.name}`,
|
|
);
|
|
list.push({
|
|
image: index,
|
|
address,
|
|
method_pointer_count: count,
|
|
method_pointers: pointers,
|
|
});
|
|
candidates.set(index, list);
|
|
}
|
|
}
|
|
}
|
|
return metadata.images.map((entry, index) => {
|
|
const list = candidates.get(index) ?? [];
|
|
if (list.length === 0)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"code-gen-modules-not-found",
|
|
`no code generation module names ${entry.name} with ${counts[index]} method pointers`,
|
|
);
|
|
if (list.length > 1)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"ambiguous-code-gen-module",
|
|
`${list.length} records match code generation module ${entry.name}`,
|
|
);
|
|
return list[0]!;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Find the CodeRegistration codeGenModulesCount/codeGenModules pair: a count
|
|
* equal to the image count followed by a pointer to an array holding exactly
|
|
* the located module records.
|
|
*/
|
|
export function* findCodeRegistration(
|
|
image: NativeImage,
|
|
modules: readonly Il2cppCodeGenModule[],
|
|
): Generator<void, { readonly field: bigint | null; readonly array: bigint }> {
|
|
const expected = new Set(modules.map((module) => module.address));
|
|
const matches: { field: bigint; array: bigint }[] = [];
|
|
const matchedFields = new Set<bigint>();
|
|
for (const mapping of image.container.mappings) {
|
|
const start = Number((8n - (mapping.address % 8n)) % 8n);
|
|
for (let at = start; at + 16 <= mapping.size; at += 8) {
|
|
if ((at & 0xffff) < 8) yield;
|
|
const offset = mapping.offset + at;
|
|
// The count owns four bytes; the following four only align its pointer.
|
|
if (image.bytes.readUInt32LE(offset) !== modules.length) continue;
|
|
const field = mapping.address + BigInt(at);
|
|
if (readUInt32(image, field) !== modules.length) continue;
|
|
const array = readPointer(image, field + 8n);
|
|
if (array === null || !isMapped(image, array, modules.length * 8))
|
|
continue;
|
|
const seen = new Set<bigint>();
|
|
for (let slot = 0; slot < modules.length; slot++) {
|
|
if (slot % 256 === 0) yield;
|
|
const pointer = readPointer(image, array + BigInt(slot * 8));
|
|
if (pointer === null || !expected.has(pointer)) break;
|
|
seen.add(pointer);
|
|
}
|
|
if (seen.size === modules.length) {
|
|
// Exact load aliases can visit one virtual registration field twice.
|
|
if (matchedFields.has(field)) continue;
|
|
matchedFields.add(field);
|
|
if (matches.length !== 0)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"code-registration-not-found",
|
|
"multiple CodeRegistration module tables match",
|
|
);
|
|
matches.push({ field, array });
|
|
}
|
|
}
|
|
}
|
|
const architecture = image.container.architecture;
|
|
if (
|
|
matches.length === 0 &&
|
|
(architecture === "amd64" || architecture === "arm64")
|
|
) {
|
|
const array = yield* findModuleArrayByCode(image, expected);
|
|
if (array !== null) return { field: null, array };
|
|
}
|
|
if (matches.length !== 1)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"code-registration-not-found",
|
|
`${matches.length} CodeRegistration module tables list the ${modules.length} located modules`,
|
|
);
|
|
return matches[0]!;
|
|
}
|
|
|
|
/**
|
|
* Some builds keep no static CodeRegistration: the compiler folded the module
|
|
* array and its count into the one loop that walks it, so no data word points
|
|
* at the array. Seen on x86-64 (a Unity 2023.2 Linux GameAssembly.so) and on
|
|
* arm64 (a Unity 2022.3 macOS Master/LTO GameAssembly.dylib). Find every
|
|
* pointer array that lists exactly the located modules, then require exactly
|
|
* one of them to be the target of an address-forming instruction in an
|
|
* executable range: on x86-64 a RIP-relative `lea`/`mov` (REX.W 8D/8B with a
|
|
* disp32 operand), on arm64 an `adrp` immediately followed by an `add` of its
|
|
* page offset to the same register. Anything else stays
|
|
* code-registration-not-found.
|
|
*/
|
|
const MODULE_ARRAY_CANDIDATES = 64;
|
|
|
|
function* findModuleArrayByCode(
|
|
image: NativeImage,
|
|
expected: ReadonlySet<bigint>,
|
|
): Generator<void, bigint | null> {
|
|
const arrays = new Set<bigint>();
|
|
for (const mapping of image.container.mappings) {
|
|
const start = Number((8n - (mapping.address % 8n)) % 8n);
|
|
let runStart = start;
|
|
const lastPosition = new Map<bigint, number>();
|
|
for (let at = start; at + 8 <= mapping.size; at += 8) {
|
|
if ((at & 0xffff) < 8) yield;
|
|
const raw = image.bytes.readBigUInt64LE(mapping.offset + at);
|
|
const value = expected.has(raw)
|
|
? readPointer(image, mapping.address + BigInt(at))
|
|
: null;
|
|
if (value === null || !expected.has(value)) {
|
|
runStart = at + 8;
|
|
lastPosition.clear();
|
|
continue;
|
|
}
|
|
const previous = lastPosition.get(value);
|
|
if (previous !== undefined && previous >= runStart)
|
|
runStart = previous + 8;
|
|
lastPosition.set(value, at);
|
|
if (at - runStart === (expected.size - 1) * 8) {
|
|
arrays.add(mapping.address + BigInt(runStart));
|
|
// Retain overlapping windows; at most one position per expected module.
|
|
if (arrays.size > MODULE_ARRAY_CANDIDATES)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"native-index-capacity",
|
|
`module-array index exceeds ${MODULE_ARRAY_CANDIDATES} entries`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if (arrays.size === 0) return null;
|
|
const referenced =
|
|
image.container.architecture === "arm64"
|
|
? yield* arm64PageReferences(image, arrays)
|
|
: yield* ripRelativeReferences(image, arrays);
|
|
return referenced.size === 1 ? [...referenced][0]! : null;
|
|
}
|
|
|
|
function* ripRelativeReferences(
|
|
image: NativeImage,
|
|
arrays: ReadonlySet<bigint>,
|
|
): Generator<void, Set<bigint>> {
|
|
const referenced = new Set<bigint>();
|
|
for (const mapping of image.container.executable_mappings ?? []) {
|
|
const bytes = image.bytes;
|
|
const end = mapping.offset + mapping.size - 7;
|
|
for (let at = mapping.offset; at <= end; at++) {
|
|
if ((at & 0xffff) < 8) yield;
|
|
const rex = bytes[at]!;
|
|
if ((rex & 0xfb) !== 0x48) continue; // REX.W, optionally REX.R
|
|
const opcode = bytes[at + 1]!;
|
|
if (opcode !== 0x8d && opcode !== 0x8b) continue;
|
|
if ((bytes[at + 2]! & 0xc7) !== 0x05) continue; // mod 00, rm 101: RIP + disp32
|
|
const next = mapping.address + BigInt(at - mapping.offset + 7);
|
|
const target = next + BigInt(bytes.readInt32LE(at + 3));
|
|
if (arrays.has(target)) referenced.add(target);
|
|
}
|
|
}
|
|
return referenced;
|
|
}
|
|
|
|
/**
|
|
* arm64 instructions are 4-byte aligned words, so only aligned addresses are
|
|
* decoded. `adrp xd, page` forms the 4 KiB page of pc plus a signed 21-bit
|
|
* page delta; the next word must be `add xd, xd, #imm12` (64-bit, unshifted),
|
|
* which adds the offset within that page. Pairs split by other instructions or
|
|
* loads through `ldr` are not followed.
|
|
*/
|
|
function* arm64PageReferences(
|
|
image: NativeImage,
|
|
arrays: ReadonlySet<bigint>,
|
|
): Generator<void, Set<bigint>> {
|
|
const referenced = new Set<bigint>();
|
|
const bytes = image.bytes;
|
|
for (const mapping of image.container.executable_mappings ?? []) {
|
|
const start = Number((4n - (mapping.address % 4n)) % 4n);
|
|
for (let at = start; at + 8 <= mapping.size; at += 4) {
|
|
if ((at & 0xffff) < 4) yield;
|
|
const adrp = bytes.readUInt32LE(mapping.offset + at);
|
|
if ((adrp & 0x9f000000) >>> 0 !== 0x90000000) continue;
|
|
const add = bytes.readUInt32LE(mapping.offset + at + 4);
|
|
if ((add & 0xffc00000) >>> 0 !== 0x91000000) continue;
|
|
const register = adrp & 0x1f;
|
|
// Register 31 is xzr for ADRP and sp for ADD, so no address is formed.
|
|
if (register === 31) continue;
|
|
if ((add & 0x1f) !== register || ((add >>> 5) & 0x1f) !== register)
|
|
continue;
|
|
const pages =
|
|
(((((adrp >>> 5) & 0x7ffff) << 2) | ((adrp >>> 29) & 3)) << 11) >> 11;
|
|
const pc = mapping.address + BigInt(at);
|
|
const target =
|
|
(pc & ~0xfffn) + (BigInt(pages) << 12n) + BigInt((add >>> 10) & 0xfff);
|
|
if (arrays.has(target)) referenced.add(target);
|
|
}
|
|
}
|
|
return referenced;
|
|
}
|
|
|
|
/**
|
|
* Find MetadataRegistration by its two TypeDefinitionIndex-sized counts:
|
|
* fieldOffsetsCount and typeDefinitionsSizesCount (words 10 and 12) both equal
|
|
* the metadata type-definition count and are followed by mapped pointers.
|
|
* typesCount and types are words 6 and 7. Counts are int32 in 8-byte slots.
|
|
* Linking reads only the type table, so when no static registration matches on
|
|
* x86-64 or arm64 the table is inferred from code instead (field null).
|
|
*/
|
|
export function* findMetadataRegistration(
|
|
image: NativeImage,
|
|
typeDefinitions: number,
|
|
requiredTypes: number,
|
|
): Generator<
|
|
void,
|
|
{
|
|
readonly field: bigint | null;
|
|
readonly count: number;
|
|
readonly types: bigint;
|
|
}
|
|
> {
|
|
const matches: { field: bigint; count: number; types: bigint }[] = [];
|
|
const matchedFields = new Set<bigint>();
|
|
// A record whose two counts match is static evidence even when its other
|
|
// fields are unusable; code is then not asked to stand in for it.
|
|
let countsMatched = false;
|
|
const low = (offset: number) => image.bytes.readUInt32LE(offset);
|
|
for (const mapping of image.container.mappings) {
|
|
const start = Number((8n - (mapping.address % 8n)) % 8n);
|
|
// The fields used below end at byte 112. The full 128-byte registration
|
|
// must still have loader coverage, including any declared zero-fill tail.
|
|
for (let at = start; at + 112 <= mapping.size; at += 8) {
|
|
if ((at & 0xffff) < 8) yield;
|
|
const base = mapping.offset + at;
|
|
if (low(base + 80) !== typeDefinitions) continue;
|
|
if (low(base + 96) !== typeDefinitions) continue;
|
|
const address = mapping.address + BigInt(at);
|
|
if (!hasPointerArrayCoverage(image, address, 16)) continue;
|
|
if (readUInt32(image, address + 80n) !== typeDefinitions) continue;
|
|
if (readUInt32(image, address + 96n) !== typeDefinitions) continue;
|
|
countsMatched = true;
|
|
const count = readUInt32(image, address + 48n);
|
|
const types = readPointer(image, address + 56n);
|
|
const fieldOffsets = readPointer(image, address + 88n);
|
|
const typeSizes = readPointer(image, address + 104n);
|
|
if (
|
|
typeof count !== "number" ||
|
|
count === 0 ||
|
|
count > 0x7fffffff ||
|
|
types === null ||
|
|
fieldOffsets === null ||
|
|
typeSizes === null ||
|
|
!isMapped(image, fieldOffsets, 8) ||
|
|
!isMapped(image, typeSizes, 8) ||
|
|
!isMapped(image, types, count * 8)
|
|
)
|
|
continue;
|
|
const field = mapping.address + BigInt(at + 48);
|
|
// Exact load aliases can visit one virtual registration field twice.
|
|
if (matchedFields.has(field)) continue;
|
|
matchedFields.add(field);
|
|
if (matches.length !== 0)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"metadata-registration-not-found",
|
|
"multiple MetadataRegistration candidates match",
|
|
);
|
|
matches.push({
|
|
field,
|
|
count,
|
|
types,
|
|
});
|
|
}
|
|
}
|
|
const architecture = image.container.architecture;
|
|
let inferred = false;
|
|
if (
|
|
!countsMatched &&
|
|
requiredTypes > 0 &&
|
|
(architecture === "amd64" || architecture === "arm64")
|
|
) {
|
|
inferred = true;
|
|
const table = yield* findTypeTableByCode(
|
|
image,
|
|
typeDefinitions,
|
|
requiredTypes,
|
|
);
|
|
if (table !== null) return { field: null, ...table };
|
|
}
|
|
if (matches.length !== 1)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"metadata-registration-not-found",
|
|
countsMatched && matches.length === 0
|
|
? `a MetadataRegistration counts ${typeDefinitions} type definitions, but its type table, field offsets or type sizes are not mapped`
|
|
: inferred
|
|
? `no MetadataRegistration counts ${typeDefinitions} type definitions, and no single code-loaded run of Il2CppType pointers covers every referenced type index`
|
|
: `${matches.length} MetadataRegistration candidates count ${typeDefinitions} type definitions`,
|
|
);
|
|
return matches[0]!;
|
|
}
|
|
|
|
/**
|
|
* An inlined MetadataRegistration leaves the Il2CppType pointer table loaded
|
|
* directly by code. Candidates are maximal runs of pointers to Il2CppType-shaped
|
|
* records (a defined Il2CppTypeEnum kind; class and value-type data below the
|
|
* metadata type-definition count) long enough for every type index the
|
|
* metadata references. Exactly one such run may exist in the image, it must
|
|
* end inside its mapping, and code must load its start with the same
|
|
* instruction forms as the module-array fallback; anything else fails closed.
|
|
* The run length becomes the count: never below what the metadata needs, but
|
|
* not an observed typesCount.
|
|
*/
|
|
/** Il2CppTypeEnum values 0x01 to 0x1e; 0x17 and 0x1a are unassigned. */
|
|
const isDefinedTypeKind = (kind: number) =>
|
|
kind >= 1 && kind <= IL2CPP_TYPE_KIND_MAX && kind !== 0x17 && kind !== 0x1a;
|
|
|
|
function* findTypeTableByCode(
|
|
image: NativeImage,
|
|
typeDefinitions: number,
|
|
requiredTypes: number,
|
|
): Generator<void, { readonly count: number; readonly types: bigint } | null> {
|
|
const { bytes, container } = image;
|
|
// Nomination and loader-aware confirmation share the image's mapping index.
|
|
// Most words are not record addresses; reject those without the throwing
|
|
// lookup. Starts are sorted, and reach[i] is the furthest end among the
|
|
// first i + 1 mappings, so one binary search finds a possible container.
|
|
const spans = container.mappings
|
|
.filter((mapping) => mapping.size > 0)
|
|
.map((mapping) => ({
|
|
start: mapping.address,
|
|
end: mapping.address + BigInt(mapping.size),
|
|
}))
|
|
.sort((left, right) =>
|
|
left.start < right.start ? -1 : left.start > right.start ? 1 : 0,
|
|
);
|
|
const reach: bigint[] = [];
|
|
for (const [index, span] of spans.entries())
|
|
reach.push(
|
|
index === 0 || span.end > reach[index - 1]!
|
|
? span.end
|
|
: reach[index - 1]!,
|
|
);
|
|
const maybeMapped = (address: bigint) => {
|
|
let lowIndex = 0;
|
|
let highIndex = spans.length - 1;
|
|
let found = -1;
|
|
while (lowIndex <= highIndex) {
|
|
const middle = (lowIndex + highIndex) >> 1;
|
|
if (spans[middle]!.start <= address) {
|
|
found = middle;
|
|
lowIndex = middle + 1;
|
|
} else highIndex = middle - 1;
|
|
}
|
|
return found >= 0 && reach[found]! >= address + 12n;
|
|
};
|
|
const recordOffset = (address: bigint) => {
|
|
if (!maybeMapped(address)) return -1;
|
|
try {
|
|
return image.fileMappings.offset(address, 12);
|
|
} catch (cause) {
|
|
// Unmapped, zero-filled or ambiguously mapped records are not types.
|
|
if (cause instanceof BinaryFormatFailure) return -1;
|
|
throw cause;
|
|
}
|
|
};
|
|
const isType = (address: bigint) => {
|
|
// Records hold an 8-byte pointer first; zero and unaligned words are not types.
|
|
if (address === 0n || (address & 7n) !== 0n) return false;
|
|
const offset = recordOffset(address);
|
|
if (offset < 0) return false;
|
|
const kind = il2cppTypeKind(bytes.readUInt32LE(offset + 8));
|
|
if (!isDefinedTypeKind(kind)) return false;
|
|
if (!isDefinitionTypeKind(kind)) return true;
|
|
return bytes.readBigUInt64LE(offset) < BigInt(typeDefinitions);
|
|
};
|
|
// A run that touches either end of its mapping may continue in a
|
|
// neighbouring one, so its length and start are unknown; it still counts as
|
|
// a candidate but cannot be chosen.
|
|
const runs = new Map<bigint, number | null>();
|
|
// Stored words only nominate slots: a slot the loader binds or leaves
|
|
// unresolved has no known value, so a run it bounds has an unknown extent
|
|
// whatever its placeholder holds.
|
|
// The same holds for a record's kind word and its definition index.
|
|
const recordState = (target: bigint): "type" | "other" | "unknown" => {
|
|
if (recordOffset(target) < 0) return "other";
|
|
const attributes = readUInt32(image, target + 8n);
|
|
if (attributes === "unknown") return "unknown";
|
|
if (attributes === null) return "other";
|
|
const kind = il2cppTypeKind(attributes);
|
|
if (!isDefinedTypeKind(kind)) return "other";
|
|
if (!isDefinitionTypeKind(kind)) return "type";
|
|
const data = readSlot(image, target);
|
|
if (data === "unknown") return "unknown";
|
|
return typeof data === "bigint" && data < BigInt(typeDefinitions)
|
|
? "type"
|
|
: "other";
|
|
};
|
|
// A word whose slot or record the loader leaves unknown bounds a run with
|
|
// an unknown extent.
|
|
const unresolved = (address: bigint) => {
|
|
const slot = readSlot(image, address);
|
|
return (
|
|
slot === "unknown" ||
|
|
(typeof slot === "bigint" && recordState(slot) === "unknown")
|
|
);
|
|
};
|
|
for (const mapping of container.mappings) {
|
|
const start = Number((8n - (mapping.address % 8n)) % 8n);
|
|
let runStart = -1;
|
|
let startUnknown = false;
|
|
for (let at = start; at <= mapping.size; at += 8) {
|
|
if ((at & 0xffff) < 8) yield;
|
|
const address = mapping.address + BigInt(at);
|
|
let endUnknown = at + 8 > mapping.size;
|
|
if (!endUnknown) {
|
|
const raw = bytes.readBigUInt64LE(mapping.offset + at);
|
|
if (isType(raw)) {
|
|
const slot = readSlot(image, address);
|
|
const state = slot === raw ? recordState(slot) : "other";
|
|
if (state === "type") {
|
|
if (runStart < 0) {
|
|
runStart = at;
|
|
startUnknown = at === start || unresolved(address - 8n);
|
|
}
|
|
continue;
|
|
}
|
|
endUnknown = slot === "unknown" || state === "unknown";
|
|
} else if (runStart >= 0) endUnknown = unresolved(address);
|
|
}
|
|
if (runStart >= 0 && (at - runStart) / 8 >= requiredTypes) {
|
|
runs.set(
|
|
mapping.address + BigInt(runStart),
|
|
endUnknown || startUnknown ? null : (at - runStart) / 8,
|
|
);
|
|
// Another long-enough run could be the real table while code loads
|
|
// only this one, so a second candidate fails closed at once.
|
|
if (runs.size > 1) return null;
|
|
}
|
|
runStart = -1;
|
|
}
|
|
}
|
|
if (runs.size !== 1) return null;
|
|
const [start, count] = [...runs][0]!;
|
|
if (count === null) return null;
|
|
const starts = new Set([start]);
|
|
const referenced =
|
|
container.architecture === "arm64"
|
|
? yield* arm64PageReferences(image, starts)
|
|
: yield* ripRelativeReferences(image, starts);
|
|
if (!referenced.has(start)) return null;
|
|
return { count, types: start };
|
|
}
|