import { BinaryFormatFailure, mappedFileOffset, } from "../native/metadata/BinaryContainer.js"; import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js"; import { isMapped, hasPointerArrayCoverage, readPointer, type NativeImage, } from "./Il2cppBinaryImage.js"; import type { Il2cppMetadataDecoded } from "./Il2cppMetadataFile.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; 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> { 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(); 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 { 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 { const names = yield* nameAddresses( image, metadata.images.map((entry) => entry.name), ); const counts = yield* imageMethodCounts(metadata); const candidates = new Map(); 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); 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 = image.bytes.readUInt32LE(offset + 8); if (count !== counts[index]) continue; const pointers = image.bytes.readBigUInt64LE(offset + 16); 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 { const expected = new Set(modules.map((module) => module.address)); const total = modules.length; const matches: { field: bigint; array: bigint }[] = []; const matchedFields = new Set(); 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; if (image.bytes.readUInt32LE(offset) !== total) continue; const array = readPointer(image, mapping.address + BigInt(at + 8)); if (array === null) continue; if (!isMapped(image, array, modules.length * 8)) continue; const seen = new Set(); 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) { const field = mapping.address + BigInt(at); // 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 }); } } } if (matches.length === 0 && image.container.architecture === "amd64") { 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 x86-64 builds (a Unity 2023.2 Linux GameAssembly.so) 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. Find every * pointer array that lists exactly the located modules, then require exactly * one of them to be the target of a RIP-relative `lea`/`mov` (REX.W 8D/8B with * a disp32 operand). Anything else stays code-registration-not-found. */ const MODULE_ARRAY_CANDIDATES = 64; function* findModuleArrayByCode( image: NativeImage, expected: ReadonlySet, ): Generator { const arrays = new Set(); for (const mapping of image.container.mappings) { const start = Number((8n - (mapping.address % 8n)) % 8n); let runStart = start; const lastPosition = new Map(); for (let at = start; at + 8 <= mapping.size; at += 8) { if ((at & 0xffff) < 8) yield; const value = image.bytes.readBigUInt64LE(mapping.offset + at); if (!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 = new Set(); 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.size === 1 ? [...referenced][0]! : null; } /** * 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. */ export function* findMetadataRegistration( image: NativeImage, typeDefinitions: number, ): Generator< void, { readonly field: bigint; readonly count: number; readonly types: bigint; } > { const matches: { field: bigint; count: number; types: bigint }[] = []; const matchedFields = new Set(); const low = (offset: number) => image.bytes.readUInt32LE(offset); for (const mapping of image.container.mappings) { const start = Number((8n - (mapping.address % 8n)) % 8n); 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 count = low(base + 48); const address = mapping.address + BigInt(at); if (!hasPointerArrayCoverage(image, address, 16)) continue; const types = readPointer(image, address + 56n); const fieldOffsets = readPointer(image, address + 88n); const typeSizes = readPointer(image, address + 104n); if ( 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, }); } } if (matches.length !== 1) throw new Il2cppBinaryFormatFailure( "metadata-registration-not-found", `${matches.length} MetadataRegistration candidates count ${typeDefinitions} type definitions`, ); return matches[0]!; }