mirror of
https://github.com/morluto/rea
synced 2026-10-26 22:28:27 -04:00
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:
@@ -1,11 +1,6 @@
|
||||
import type { Il2cppMethodAddress } from "../domain/il2cpp/il2cppMetadata.js";
|
||||
import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js";
|
||||
import {
|
||||
openImageSteps,
|
||||
readPointer,
|
||||
readSlot,
|
||||
isExecutable,
|
||||
} from "./Il2cppBinaryImage.js";
|
||||
import { openImageSteps, readSlot, isExecutable } from "./Il2cppBinaryImage.js";
|
||||
import {
|
||||
findModules,
|
||||
findCodeRegistration,
|
||||
@@ -180,8 +175,13 @@ export function* linkIl2cppBinarySteps(
|
||||
);
|
||||
// 27.0/27.1 and 27.2 share header version 27 but place byref differently.
|
||||
if (version === 27) return { type, name: null };
|
||||
const address =
|
||||
readPointer(image, registration.types + BigInt(type * 8)) ?? 0n;
|
||||
const address = readSlot(image, registration.types + BigInt(type * 8));
|
||||
if (address === "unknown") return { type, name: null };
|
||||
if (address === null)
|
||||
throw new Il2cppBinaryFormatFailure(
|
||||
"invalid-type",
|
||||
`type ${type} has no mapped type-table slot`,
|
||||
);
|
||||
if (address === 0n)
|
||||
throw new Il2cppBinaryFormatFailure(
|
||||
"invalid-type",
|
||||
|
||||
@@ -76,7 +76,7 @@ export interface NativeImage {
|
||||
readonly imageBase: bigint;
|
||||
readonly unresolvedPointerSlots: ReadonlyMap<bigint, number>;
|
||||
readonly zeroFillPointers: ReadonlyMap<bigint, bigint>;
|
||||
/** Mach-O loader fixups, used when interpreting method pointer slots. */
|
||||
/** Mach-O loader fixups shared by every native pointer consumer. */
|
||||
readonly machoPointerFixups?: PointerFixups;
|
||||
}
|
||||
|
||||
@@ -191,13 +191,8 @@ export function* openImageSteps(
|
||||
};
|
||||
}
|
||||
|
||||
export const readPointer = (
|
||||
image: NativeImage,
|
||||
address: bigint,
|
||||
): bigint | null => {
|
||||
if (hasUnresolvedPointer(image, address)) return null;
|
||||
const virtual = image.zeroFillPointers.get(address);
|
||||
if (virtual !== undefined) return virtual;
|
||||
/** Stored bytes only; loader interpretation belongs to readSlot. */
|
||||
const readStoredWord = (image: NativeImage, address: bigint): bigint | null => {
|
||||
try {
|
||||
const offset = mappedFileOffset(
|
||||
image.container.mappings,
|
||||
@@ -216,8 +211,12 @@ export const readPointer = (
|
||||
export const readUInt32 = (
|
||||
image: NativeImage,
|
||||
address: bigint,
|
||||
): number | null => {
|
||||
if (hasUnresolvedPointer(image, address, 4)) return null;
|
||||
): number | "unknown" | null => {
|
||||
if (
|
||||
hasUnresolvedElfBytes(image, address, 4) ||
|
||||
image.machoPointerFixups?.hasUnresolvedLegacyBytes(address, 4) === true
|
||||
)
|
||||
return "unknown";
|
||||
const virtual = image.zeroFillPointers.get(address);
|
||||
if (virtual !== undefined) return Number(virtual & 0xffffffffn);
|
||||
try {
|
||||
@@ -329,7 +328,7 @@ export const isExecutable = (image: NativeImage, address: bigint): boolean => {
|
||||
}
|
||||
};
|
||||
|
||||
const hasUnresolvedPointer = (
|
||||
const hasUnresolvedElfBytes = (
|
||||
image: NativeImage,
|
||||
address: bigint,
|
||||
size = 8,
|
||||
@@ -346,13 +345,13 @@ export const readSlot = (
|
||||
image: NativeImage,
|
||||
address: bigint,
|
||||
): bigint | "unknown" | null => {
|
||||
if (hasUnresolvedPointer(image, address)) return "unknown";
|
||||
if (hasUnresolvedElfBytes(image, address)) return "unknown";
|
||||
try {
|
||||
const virtual = image.zeroFillPointers.get(address);
|
||||
let raw: bigint | null;
|
||||
if (virtual !== undefined) raw = virtual;
|
||||
else if (isZeroFilled(image, address, 8)) raw = 0n;
|
||||
else raw = readPointer(image, address);
|
||||
else raw = readStoredWord(image, address);
|
||||
if (raw === null) return null;
|
||||
if (image.machoPointerFixups !== undefined) {
|
||||
const resolution = image.machoPointerFixups.decode(address, raw);
|
||||
@@ -372,3 +371,12 @@ export const readSlot = (
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
|
||||
/** Require an observed resolved pointer; unknown and unmapped slots stay null. */
|
||||
export const readPointer = (
|
||||
image: NativeImage,
|
||||
address: bigint,
|
||||
): bigint | null => {
|
||||
const value = readSlot(image, address);
|
||||
return typeof value === "bigint" ? value : null;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js";
|
||||
import {
|
||||
readPointer,
|
||||
readSlot,
|
||||
readUInt32,
|
||||
isMapped,
|
||||
type NativeImage,
|
||||
@@ -46,12 +46,26 @@ export const createTypeNamer = (
|
||||
const fail = (message: string): never => {
|
||||
throw new Il2cppBinaryFormatFailure("invalid-type", message);
|
||||
};
|
||||
const pointer = (address: bigint, label: string) =>
|
||||
readPointer(image, address) ??
|
||||
fail(`${label} at 0x${address.toString(16)} is not file mapped`);
|
||||
const u32 = (address: bigint, label: string) =>
|
||||
readUInt32(image, address) ??
|
||||
fail(`${label} at 0x${address.toString(16)} is not file mapped`);
|
||||
const unresolved = (address: bigint, label: string): never => {
|
||||
throw new Il2cppBinaryFormatFailure(
|
||||
"unsupported-pointer-encoding",
|
||||
`${label} at 0x${address.toString(16)} is loader-unresolved; its stored bytes are not a resolved type value`,
|
||||
);
|
||||
};
|
||||
const pointer = (address: bigint, label: string): bigint => {
|
||||
const value = readSlot(image, address);
|
||||
if (value === "unknown") return unresolved(address, label);
|
||||
return (
|
||||
value ?? fail(`${label} at 0x${address.toString(16)} is not file mapped`)
|
||||
);
|
||||
};
|
||||
const u32 = (address: bigint, label: string): number => {
|
||||
const value = readUInt32(image, address);
|
||||
if (value === "unknown") return unresolved(address, label);
|
||||
return (
|
||||
value ?? fail(`${label} at 0x${address.toString(16)} is not file mapped`)
|
||||
);
|
||||
};
|
||||
let cachedBytes = 0;
|
||||
let work = 0;
|
||||
const guard = (bytes: number) => {
|
||||
|
||||
@@ -531,7 +531,7 @@ export class Il2cppMetadataProvider implements Il2cppMetadataPort {
|
||||
? "Field, parameter and return types are not recovered without binary_path; type indices refer to the native binary."
|
||||
: decoded.metadata_version === 27
|
||||
? "Field, parameter and return type names are null for metadata version 27: versions 27.0 to 27.2 share that header number but place the Il2CppType byref flag differently, so a name could gain or lose & without warning; type indices are still reported."
|
||||
: "Field, parameter and return type names are rendered from the binary's Il2CppType table in a C#-like form (keywords for primitives, Namespace.Outer+Inner, generic parameters by metadata name); nested generic arguments stay with the type component that declares them. Custom modifiers, array bounds and attributes are not reported.",
|
||||
: "Field, parameter and return type names are rendered from the binary's Il2CppType table in a C#-like form (keywords for primitives, Namespace.Outer+Inner, generic parameters by metadata name); nested generic arguments stay with the type component that declares them. An unresolved type-table slot retains its type index with a null name; a loader-unresolved value inside the type graph is refused rather than rendered from stored placeholder bytes. Custom modifiers, array bounds and attributes are not reported.",
|
||||
...(linked.binary !== null &&
|
||||
linked.binary.code_gen_modules_field === null
|
||||
? [
|
||||
|
||||
Reference in New Issue
Block a user