From d31e42699f5ffb3c39b4b696844125b6dd5c1dad Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:02:54 +0800 Subject: [PATCH] fix(il2cpp): deduplicate consistent ELF registration aliases Fixes #6715 --- docs/il2cpp-metadata.md | 5 +++ src/il2cpp/Il2cppBinaryRegistration.ts | 29 +++++++++++++- .../boundary/il2cpp/il2cppBinaryFile.test.ts | 38 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/docs/il2cpp-metadata.md b/docs/il2cpp-metadata.md index db25d25798..ffc1428fe6 100644 --- a/docs/il2cpp-metadata.md +++ b/docs/il2cpp-metadata.md @@ -212,6 +212,11 @@ linked method table or seed output is published. Row ids may repeat across different images, and distinct rows may still resolve to the same address when the binary uses identical-code folding. +For ELF, a repeated load mapping that resolves a complete module record to the +same virtual address and file bytes contributes one record. Distinct virtual +record addresses remain distinct candidates, and conflicting file mappings +remain malformed. + Module records establish names through their declared NUL-terminated string pointers. NUL/0xFF padding, spaces, Unicode and suffix-shared storage are supported. A pointer to a longer string does not match just its suffix; a pointer directly into a shared suffix may name it. Exact reversed UTF-8 matching avoids rescanning the file for every name and retains duplicate-name owners until module records resolve them. A contributor investigated a Linux `GameAssembly.so` from Unity 2023.2.13f1 whose static registration was not located. When no static pair is resolved on x86-64, the reader searches executable ranges for bytes consistent with RIP-relative LEA/MOV references to module-array candidates. Exactly one referenced array candidate must remain. `code_gen_modules_field` is then `null`, and Evidence is **inferred**: instruction boundaries, the absence of a static registration elsewhere, and runtime use were not verified. The module records and slots themselves remain file observations. Static-table associations are **derived**. Otherwise the result is `code-registration-not-found`. diff --git a/src/il2cpp/Il2cppBinaryRegistration.ts b/src/il2cpp/Il2cppBinaryRegistration.ts index c60e4be6d3..258d031a2d 100644 --- a/src/il2cpp/Il2cppBinaryRegistration.ts +++ b/src/il2cpp/Il2cppBinaryRegistration.ts @@ -1,3 +1,7 @@ +import { + BinaryFormatFailure, + mappedFileOffset, +} from "../native/metadata/BinaryContainer.js"; import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js"; import { isMapped, @@ -131,6 +135,28 @@ export function* findModules( 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; @@ -139,6 +165,7 @@ export function* findModules( 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", @@ -146,7 +173,7 @@ export function* findModules( ); list.push({ image: index, - address: mapping.address + BigInt(at), + address, method_pointer_count: count, method_pointers: pointers, }); diff --git a/tests/boundary/il2cpp/il2cppBinaryFile.test.ts b/tests/boundary/il2cpp/il2cppBinaryFile.test.ts index bd54aa8f52..c3917f9d7b 100644 --- a/tests/boundary/il2cpp/il2cppBinaryFile.test.ts +++ b/tests/boundary/il2cpp/il2cppBinaryFile.test.ts @@ -41,6 +41,27 @@ const failure = (bytes: Buffer, architecture?: string) => { } 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()); @@ -76,6 +97,23 @@ it("links methods to code through module tokens, not metadata order", () => { ); }); +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("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]!;