fix(il2cpp): preserve loader ownership and ABI field boundaries

Resolve pointer and scalar facts through their shared loader-aware owners before registration search or type rendering. Preserve unresolved names and attributes, read count fields at their declared width, and admit declared zero-fill registration tails without accepting unmapped bytes.

Cover relocation overlap and adjacency, loader-bound fields, ABI padding, nullable names, and CLI/MCP behavior; document format and verification limits.

Fixes #6881
Fixes #6873
Fixes #6871
This commit is contained in:
morluto committed 2026-10-24 04:42:09 +08:00
1 parent f2db494bfc
commit 52ca222bd0
14 files changed
+420 -59

No files matched your search

+35 -15
View File
@@ -7,6 +7,7 @@ import {
isMapped,
hasPointerArrayCoverage,
readPointer,
readUInt32,
type NativeImage,
} from "./Il2cppBinaryImage.js";
import type { Il2cppMetadataDecoded } from "./Il2cppMetadataFile.js";
@@ -136,6 +137,8 @@ export function* findModules(
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(
@@ -160,9 +163,10 @@ export function* findModules(
let work = 0;
for (const index of indices) {
if (++work % 256 === 0) yield;
const count = image.bytes.readUInt32LE(offset + 8);
const count = readUInt32(image, address + 8n);
if (count !== counts[index]) continue;
const pointers = image.bytes.readBigUInt64LE(offset + 16);
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;
@@ -207,7 +211,6 @@ export function* findCodeRegistration(
modules: readonly Il2cppCodeGenModule[],
): Generator<void, { readonly field: bigint | null; readonly array: bigint }> {
const expected = new Set(modules.map((module) => module.address));
const total = BigInt(modules.length);
const matches: { field: bigint; array: bigint }[] = [];
const matchedFields = new Set<bigint>();
for (const mapping of image.container.mappings) {
@@ -215,9 +218,13 @@ export function* findCodeRegistration(
for (let at = start; at + 16 <= mapping.size; at += 8) {
if ((at & 0xffff) < 8) yield;
const offset = mapping.offset + at;
if (image.bytes.readBigUInt64LE(offset) !== total) continue;
const array = image.bytes.readBigUInt64LE(offset + 8);
if (!isMapped(image, array, modules.length * 8)) continue;
// 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;
@@ -226,7 +233,6 @@ export function* findCodeRegistration(
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);
@@ -272,8 +278,11 @@ function* findModuleArrayByCode(
const lastPosition = new Map<bigint, number>();
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)) {
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;
@@ -333,21 +342,32 @@ export function* findMetadataRegistration(
const matches: { field: bigint; count: number; types: bigint }[] = [];
const matchedFields = new Set<bigint>();
const low = (offset: number) => image.bytes.readUInt32LE(offset);
const word = (offset: number) => image.bytes.readBigUInt64LE(offset);
for (const mapping of image.container.mappings) {
const start = Number((8n - (mapping.address % 8n)) % 8n);
for (let at = start; at + 128 <= mapping.size; at += 8) {
// 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 count = low(base + 48);
const types = word(base + 56);
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;
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 ||
!isMapped(image, word(base + 88), 8) ||
!isMapped(image, word(base + 104), 8) ||
types === null ||
fieldOffsets === null ||
typeSizes === null ||
!isMapped(image, fieldOffsets, 8) ||
!isMapped(image, typeSizes, 8) ||
!isMapped(image, types, count * 8)
)
continue;