fix(elf): preserve virtual relocation identity (#9990)

* fix(elf): keep compressed storage out of decoded metadata facts

* fix(elf): preserve virtual relocation identity

Keep linked initializer backing and relocation values in their owning virtual
or ET_REL section coordinates. Replace the IL2CPP physical-copy materializer
with compact virtual value facts while preserving file-backed capacity.

Reuse initializer repairs and original regressions from ShuaixinHuang PRs
repair retains independent alias values instead of blanket alias refusal.

Co-authored-by: 帅鑫 Huang <1836656514@qq.com>

* fix(elf): admit compressed string tables the profile does not read

SHF_COMPRESSED string tables are rejected only when they name sections
or symbols. An unreferenced string table is not decoded, so it no longer
makes an otherwise analyzable ELF unsupported.

* fix(il2cpp): admit virtual pointer workspace before retention

* fix(elf): reject null relocation target sections

* fix(il2cpp): avoid decoding unrelated Mach-O words

* fix(il2cpp): preserve uncertainty in candidate scans

* fix(il2cpp): bound uncertain array candidates

* fix(il2cpp): preserve unknown registration candidates

* fix(il2cpp): classify type kinds before bound data

* fix(il2cpp): bind candidate counts to matching names

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 帅鑫 Huang <1836656514@qq.com>
This commit is contained in:
authored and GitHub committed 2026-11-11 18:13:18 +08:00
1 parent 3c0e9d8a02
commit 8052f8daf0
18 files changed
+1807 -419

No files matched your search

-102
View File
@@ -1,102 +0,0 @@
import type {
FileMapping,
ZeroFillMapping,
} from "../domain/virtualFileMapping.js";
import {
readElfRelocationWritesSteps,
ElfPointerRelocationFailure,
} from "../native/metadata/ElfPointerRelocations.js";
import type { BinaryContainer } from "../native/metadata/BinaryContainer.js";
import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js";
/** Materialize the shared loader facts only for IL2CPP's existing private reader. */
export function* applyElfRelativeRelocationsSteps(
image: Buffer,
elf: Extract<BinaryContainer, { format: "elf" }>["elf"],
mappings: readonly FileMapping[],
zeroFills: readonly ZeroFillMapping[],
): Generator<
void,
{
readonly bytes: Buffer;
readonly unresolved: ReadonlyMap<bigint, number>;
readonly zeroFillPointers: ReadonlyMap<bigint, bigint>;
}
> {
try {
const steps = readElfRelocationWritesSteps(
image,
{
bits: 64,
little: true,
machine: elf.machine,
dynamicSegments: elf.dynamicSegments,
mappings,
zeroFills,
},
{ requireDynamic: true },
);
const bytes = Buffer.from(image),
zeroFillPointers = new Map<bigint, bigint>(),
unresolved = new Map<bigint, number>();
// One bit per original file offset replaces per-relocation objects. Reads
// of implicit addends continue using image, never this mutable private copy.
const starts = Buffer.alloc(Math.ceil(image.length / 8));
const wasWritten = (offset: number) =>
offset >= 0 &&
offset < image.length &&
((starts[Math.floor(offset / 8)] ?? 0) & (1 << (offset % 8))) !== 0;
const admitState = () => {
if (unresolved.size + zeroFillPointers.size >= 65_536)
throw new Il2cppBinaryFormatFailure(
"native-index-capacity",
"ELF unresolved and zero-fill overlay exceeds 65536 entries",
);
};
const unknown = (address: bigint, width: number) => {
if (!unresolved.has(address)) admitState();
unresolved.set(address, Math.max(unresolved.get(address) ?? 0, width));
};
let next = steps.next();
while (!next.done) {
const write = next.value;
if (write === undefined) {
yield;
next = steps.next();
continue;
}
const at = write.file_offset;
if (write.value === null) {
unknown(write.address, write.width);
if (at !== null) bytes.fill(0, at, at + write.width);
} else if (at === null) {
for (let delta = -7; delta <= 7; delta++) {
const previous = zeroFillPointers.get(write.address + BigInt(delta));
if (
previous !== undefined &&
(delta !== 0 || previous !== write.value)
)
unknown(write.address, 8);
}
if (!zeroFillPointers.has(write.address)) admitState();
zeroFillPointers.set(write.address, write.value);
} else {
for (let delta = -7; delta <= 7; delta++)
if (
wasWritten(at + delta) &&
(delta !== 0 || bytes.readBigUInt64LE(at) !== write.value)
)
unknown(write.address, 8);
const byte = Math.floor(at / 8);
starts[byte] = (starts[byte] ?? 0) | (1 << (at % 8));
bytes.writeBigUInt64LE(write.value, at);
}
next = steps.next();
}
return { bytes, unresolved, zeroFillPointers };
} catch (cause) {
if (cause instanceof ElfPointerRelocationFailure)
throw new Il2cppBinaryFormatFailure(cause.reason, cause.message);
throw cause;
}
}
+3
View File
@@ -1,3 +1,6 @@
/** Existing selected-binary allowance also bounds its retained pointer workspace. */
export const IL2CPP_BINARY_BYTES = 1024 * 1024 * 1024;
export const IL2CPP_BINARY_FAILURE_REASONS = {
unsupported: [
"unsupported-container",
+41 -47
View File
@@ -5,6 +5,7 @@ import {
import { binaryMetadataArchitecture } from "../domain/binaryMetadataArchitecture.js";
import {
BinaryFormatFailure,
BinaryMappingFailure,
BinaryResourceFailure,
createFileMappingIndex,
mappedFileOffset,
@@ -21,8 +22,15 @@ import {
parseMachoLayout,
UnsupportedMachoMetadataFormatError,
} from "../native/AppleMachoSelection.js";
import { applyElfRelativeRelocationsSteps } from "./ElfRelocations.js";
import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js";
import {
readElfPointerValuesSteps,
ElfPointerRelocationFailure,
type ElfVirtualPointerWrites,
} from "../native/metadata/ElfPointerRelocations.js";
import {
Il2cppBinaryFormatFailure,
IL2CPP_BINARY_BYTES,
} from "./Il2cppBinaryFormat.js";
const MACHO_CPU = new Map([
[0x01000007, "amd64"],
[0x0100000c, "arm64"],
@@ -83,8 +91,8 @@ export interface NativeImage {
/** One image's immutable load mappings, shared by stored and loader-aware reads. */
readonly fileMappings: FileMappingIndex;
readonly imageBase: bigint;
readonly unresolvedPointerSlots: ReadonlyMap<bigint, number>;
readonly zeroFillPointers: ReadonlyMap<bigint, bigint>;
/** ELF loader writes retain virtual identity; source bytes stay unchanged. */
readonly elfPointerOverlay?: ElfVirtualPointerWrites;
/** Mach-O loader fixups shared by every native pointer consumer. */
readonly machoPointerFixups?: PointerFixups;
}
@@ -93,7 +101,7 @@ export function* openImageSteps(
bytes: Buffer,
architecture: string | undefined,
): Generator<void, NativeImage> {
let image = selectImage(bytes, architecture);
const image = selectImage(bytes, architecture);
let container: BinaryContainer;
try {
container = readBinaryContainer(image);
@@ -117,27 +125,28 @@ export function* openImageSteps(
"unsupported-container",
`only 64-bit little-endian images are supported, not ${container.bits}-bit ${container.byte_order}-endian`,
);
let relocated:
| {
bytes: Buffer;
unresolved: ReadonlyMap<bigint, number>;
zeroFillPointers: ReadonlyMap<bigint, bigint>;
}
| undefined;
let elfPointerOverlay: ElfVirtualPointerWrites | undefined;
try {
if (container.format === "elf")
relocated = yield* applyElfRelativeRelocationsSteps(
elfPointerOverlay = yield* readElfPointerValuesSteps(
image,
container.elf,
container.mappings,
container.zero_fills,
{
bits: 64,
little: true,
machine: container.elf.machine,
dynamicSegments: container.elf.dynamicSegments,
mappings: container.mappings,
zeroFills: container.zero_fills,
},
IL2CPP_BINARY_BYTES,
);
} catch (cause) {
if (cause instanceof ElfPointerRelocationFailure)
throw new Il2cppBinaryFormatFailure(cause.reason, cause.message);
if (cause instanceof BinaryFormatFailure)
throw new Il2cppBinaryFormatFailure("malformed-container", cause.message);
throw cause;
}
if (relocated !== undefined) image = relocated.bytes;
if (container.format === "macho" && container.macho_chained_fixups === true)
throw new Il2cppBinaryFormatFailure(
"unsupported-pointer-encoding",
@@ -195,8 +204,7 @@ export function* openImageSteps(
container.zero_fills,
),
imageBase,
unresolvedPointerSlots: relocated?.unresolved ?? new Map<bigint, number>(),
zeroFillPointers: relocated?.zeroFillPointers ?? new Map<bigint, bigint>(),
...(elfPointerOverlay === undefined ? {} : { elfPointerOverlay }),
...(machoPointerFixups === undefined ? {} : { machoPointerFixups }),
};
}
@@ -217,14 +225,13 @@ export const readUInt32 = (
image: NativeImage,
address: bigint,
): number | "unknown" | null => {
if (
hasUnresolvedElfBytes(image, address, 4) ||
image.machoPointerFixups?.hasUnresolvedLegacyBytes(address, 4) === true
)
if (image.machoPointerFixups?.hasUnresolvedLegacyBytes(address, 4) === true)
return "unknown";
const virtual = image.zeroFillPointers.get(address);
if (virtual !== undefined) return Number(virtual & 0xffffffffn);
try {
if (image.elfPointerOverlay?.hasWrites(address, 4) === true) {
const value = image.elfPointerOverlay.unsignedAt(address, 4);
return value === undefined ? "unknown" : Number(value);
}
return image.bytes.readUInt32LE(image.fileMappings.offset(address, 4));
} catch (cause) {
if (cause instanceof BinaryFormatFailure) return null;
@@ -314,34 +321,20 @@ export const isExecutable = (image: NativeImage, address: bigint): boolean => {
}
};
const hasUnresolvedElfBytes = (
image: NativeImage,
address: bigint,
size = 8,
): boolean => {
if (image.unresolvedPointerSlots.size === 0) return false;
for (let delta = -7; delta < size; delta++) {
const width = image.unresolvedPointerSlots.get(address + BigInt(delta));
if (width !== undefined && delta + width > 0) return true;
}
return false;
};
export const readSlot = (
image: NativeImage,
address: bigint,
): bigint | "unknown" | null => {
if (hasUnresolvedElfBytes(image, address)) return "unknown";
try {
const virtual = image.zeroFillPointers.get(address);
let raw: bigint | null;
if (virtual !== undefined) raw = virtual;
else {
// A successful indexed read already excludes zero-fill aliases. Only a
// non-file-backed slot needs the zero-fill ownership walk.
raw = readStoredWord(image, address);
if (raw === null && isZeroFilled(image, address, 8)) raw = 0n;
if (image.elfPointerOverlay !== undefined) {
const value = image.elfPointerOverlay.unsignedAt(address, 8);
if (value !== undefined) return value;
if (image.elfPointerOverlay.hasWrites(address, 8)) return "unknown";
}
// A successful indexed read already excludes zero-fill aliases. Only a
// non-file-backed slot needs the zero-fill ownership walk.
let raw = readStoredWord(image, address);
if (raw === null && isZeroFilled(image, address, 8)) raw = 0n;
if (raw === null) return null;
if (image.machoPointerFixups !== undefined) {
const resolution = image.machoPointerFixups.decode(address, raw);
@@ -356,6 +349,7 @@ export const readSlot = (
}
return raw;
} catch (cause) {
if (cause instanceof BinaryMappingFailure) return null;
if (cause instanceof BinaryFormatFailure)
throw new Il2cppBinaryFormatFailure("malformed-container", cause.message);
throw cause;
+344 -64
View File
@@ -23,6 +23,72 @@ import type { Il2cppCodeGenModule } from "./Il2cppBinaryFile.js";
const TOTAL_NAME_OCCURRENCES = 65536;
const SCAN_BYTES = 65536;
/** Index candidates by untouched bytes, retaining bounded representation space. */
const unresolvedPointerMatcher = (
image: NativeImage,
candidates: Iterable<bigint>,
candidateCounts?: (candidate: bigint) => Iterable<number>,
): ((
address: bigint,
raw: bigint,
count?: number,
) => number | null | undefined) => {
const fixups = image.machoPointerFixups;
if (fixups === undefined) return () => undefined;
const values = [...candidates];
const masks = new Map<
bigint,
Map<bigint, { counts: Set<number>; minimum: number }> | null
>();
const maximumEntries = 8 * TOTAL_NAME_OCCURRENCES;
let indexedEntries = 0;
return (address, raw, count) => {
if (!fixups.hasUnresolvedLegacyBytes(address, 8)) return undefined;
let knownMask = 0n;
for (let byte = 0; byte < 8; byte++)
if (!fixups.hasUnresolvedLegacyBytes(address + BigInt(byte), 1))
knownMask |= 0xffn << BigInt(byte * 8);
let matches = masks.get(knownMask);
if (matches === undefined) {
// Beyond the retained index budget applicability remains unknown.
const index = new Map<bigint, { counts: Set<number>; minimum: number }>();
let entries = 0;
for (const candidate of values) {
const key = candidate & knownMask;
let candidateSet = index.get(key);
if (candidateSet === undefined) {
candidateSet = {
counts: new Set(),
minimum: Number.POSITIVE_INFINITY,
};
index.set(key, candidateSet);
entries++;
}
for (const value of candidateCounts?.(candidate) ?? [])
if (!candidateSet.counts.has(value)) {
candidateSet.counts.add(value);
candidateSet.minimum = Math.min(candidateSet.minimum, value);
entries++;
}
if (entries > maximumEntries - indexedEntries) {
masks.set(knownMask, null);
return null;
}
}
matches = index;
indexedEntries += entries;
masks.set(knownMask, matches);
}
if (matches === null) return null;
const candidateSet = matches.get(raw & knownMask);
if (candidateSet === undefined) return undefined;
if (candidateCounts === undefined) return null;
if (count !== undefined)
return candidateSet.counts.has(count) ? count : undefined;
return candidateSet.minimum;
};
};
interface NameTrie {
readonly next: Map<number, NameTrie>;
readonly owners: number[];
@@ -90,6 +156,8 @@ function* nameAddresses(
if (node === undefined) break;
if (node.owners.length === 0) continue;
const address = mapping.address + BigInt(at);
if (image.elfPointerOverlay?.hasWrites(address, chunk + end - at + 1))
continue;
if (!found.has(address) && found.size >= TOTAL_NAME_OCCURRENCES)
throw new Il2cppBinaryFormatFailure(
"native-index-capacity",
@@ -133,18 +201,73 @@ export function* findModules(
metadata.images.map((entry) => entry.name),
);
const counts = yield* imageMethodCounts(metadata);
const countSet = new Set(counts);
const minimumCount = counts.reduce(
(minimum, count) => Math.min(minimum, count),
Number.POSITIVE_INFINITY,
);
const candidates = new Map<number, Il2cppCodeGenModule[]>();
const unresolvedName = unresolvedPointerMatcher(image, names.keys(), (name) =>
(names.get(name) ?? []).map((index) => counts[index]!),
);
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 address = mapping.address + BigInt(at);
const raw = image.bytes.readBigUInt64LE(offset);
const elfSlot =
image.elfPointerOverlay === undefined
? undefined
: readSlot(image, address);
const unresolvedLegacy =
image.machoPointerFixups?.hasUnresolvedLegacyBytes(address, 8) === true;
if (elfSlot === "unknown" || unresolvedLegacy) {
const count = readUInt32(image, address + 8n);
let plausibleCount =
count === "unknown"
? minimumCount
: count !== null && countSet.has(count)
? count
: Number.POSITIVE_INFINITY;
if (elfSlot !== "unknown" && Number.isFinite(plausibleCount)) {
const matched = unresolvedName(
address,
raw,
count === "unknown" || count === null ? undefined : count,
);
plausibleCount =
matched === undefined
? Number.POSITIVE_INFINITY
: (matched ?? plausibleCount);
}
if (Number.isFinite(plausibleCount)) {
const pointers = readSlot(image, address + 16n);
if (
pointers === "unknown" ||
(typeof pointers === "bigint" &&
hasPointerArrayCoverage(image, pointers, plausibleCount))
)
throw new Il2cppBinaryFormatFailure(
"code-gen-modules-not-found",
"A loader-owned module name pointer could match an additional module record",
);
}
}
if (unresolvedLegacy) continue;
// Non-ELF images retain their stored bytes. Nominate before decoding so
// unrelated partial fixups do not invalidate the module search.
if (image.elfPointerOverlay === undefined && !names.has(raw)) continue;
const target =
elfSlot === undefined
? readPointer(image, address)
: typeof elfSlot === "bigint"
? elfSlot
: null;
if (target === null) continue;
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(
@@ -170,10 +293,20 @@ export function* findModules(
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 (count !== "unknown" && count !== counts[index]) continue;
const pointers = readSlot(image, address + 16n);
if (pointers === "unknown")
throw new Il2cppBinaryFormatFailure(
"code-gen-modules-not-found",
"A matching module record has an unresolved method pointer array",
);
if (pointers === null) continue;
if (!hasPointerArrayCoverage(image, pointers, count)) continue;
if (!hasPointerArrayCoverage(image, pointers, counts[index]!)) continue;
if (count === "unknown")
throw new Il2cppBinaryFormatFailure(
"code-gen-modules-not-found",
"A matching module record has an unresolved method count",
);
const list = candidates.get(index) ?? [];
if (list.some((candidate) => candidate.address === address)) continue;
if (list.length !== 0)
@@ -223,22 +356,41 @@ export function* findCodeRegistration(
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);
const count = readUInt32(image, field);
if (count !== "unknown" && count !== modules.length) continue;
const arraySlot = readSlot(image, field + 8n);
if (arraySlot === "unknown")
throw new Il2cppBinaryFormatFailure(
"code-registration-not-found",
"A possible CodeRegistration has an unresolved module array",
);
const array = arraySlot;
if (array === null || !isMapped(image, array, modules.length * 8))
continue;
const seen = new Set<bigint>();
let uncertain = count === "unknown";
let viable = true;
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;
const pointer = readSlot(image, array + BigInt(slot * 8));
if (pointer === "unknown") {
uncertain = true;
continue;
}
if (pointer === null || !expected.has(pointer) || seen.has(pointer)) {
viable = false;
break;
}
seen.add(pointer);
}
if (seen.size === modules.length) {
if (viable && uncertain)
throw new Il2cppBinaryFormatFailure(
"code-registration-not-found",
"A possible CodeRegistration has unresolved candidate fields",
);
if (viable && seen.size === modules.length) {
// Exact load aliases can visit one virtual registration field twice.
if (matchedFields.has(field)) continue;
matchedFields.add(field);
@@ -286,16 +438,83 @@ function* findModuleArrayByCode(
expected: ReadonlySet<bigint>,
): Generator<void, bigint | null> {
const arrays = new Set<bigint>();
const unresolvedModule = unresolvedPointerMatcher(image, expected);
let expectedXor = 0n;
for (const candidate of expected) expectedXor ^= candidate;
for (const mapping of image.container.mappings) {
const start = Number((8n - (mapping.address % 8n)) % 8n);
let runStart = start;
const lastPosition = new Map<bigint, number>();
const possible = new Map<number, bigint | null>();
const occurrences = new Map<bigint, number>();
const unknownPositions = new Set<number>();
let possibleXor = 0n;
let duplicateValues = 0;
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;
const address = mapping.address + BigInt(at);
const elfSlot =
image.elfPointerOverlay === undefined
? undefined
: readSlot(image, address);
const unknown =
elfSlot === "unknown" || unresolvedModule(address, raw) !== undefined;
const value =
!unknown && (image.elfPointerOverlay !== undefined || expected.has(raw))
? elfSlot === undefined
? readPointer(image, address)
: typeof elfSlot === "bigint"
? elfSlot
: null
: null;
if (!unknown && (value === null || !expected.has(value))) {
possible.clear();
occurrences.clear();
unknownPositions.clear();
possibleXor = 0n;
duplicateValues = 0;
} else {
const candidate = unknown ? null : value;
possible.set(at, candidate);
if (candidate === null) unknownPositions.add(at);
else {
possibleXor ^= candidate;
const count = (occurrences.get(candidate) ?? 0) + 1;
occurrences.set(candidate, count);
if (count === 2) duplicateValues++;
}
const expired = at - expected.size * 8;
if (possible.has(expired)) {
const previous = possible.get(expired)!;
possible.delete(expired);
if (previous === null) unknownPositions.delete(expired);
else {
possibleXor ^= previous;
const count = occurrences.get(previous)!;
if (count === 2) duplicateValues--;
if (count === 1) occurrences.delete(previous);
else occurrences.set(previous, count - 1);
}
}
if (
possible.size === expected.size &&
unknownPositions.size > 0 &&
duplicateValues === 0
) {
if (unknownPositions.size === 1) {
const unknownAt = unknownPositions.values().next().value!;
if (
image.elfPointerOverlay !== undefined ||
unresolvedPointerMatcher(image, [expectedXor ^ possibleXor])(
mapping.address + BigInt(unknownAt),
image.bytes.readBigUInt64LE(mapping.offset + unknownAt),
) !== undefined
)
return null;
} else return null;
}
}
if (value === null || !expected.has(value)) {
runStart = at + 8;
lastPosition.clear();
@@ -331,16 +550,28 @@ function* ripRelativeReferences(
const referenced = new Set<bigint>();
for (const mapping of image.container.executable_mappings ?? []) {
const bytes = image.bytes;
const loaded =
image.elfPointerOverlay?.hasWrites(mapping.address, mapping.size) ===
true;
const end = mapping.offset + mapping.size - 7;
for (let at = mapping.offset; at <= end; at++) {
if ((at & 0xffff) < 8) yield;
const rex = bytes[at]!;
const address = mapping.address + BigInt(at - mapping.offset);
const prefix = loaded
? readUInt32(image, address)
: bytes.readUInt32LE(at);
if (typeof prefix !== "number") continue;
const rex = prefix & 0xff;
if ((rex & 0xfb) !== 0x48) continue; // REX.W, optionally REX.R
const opcode = bytes[at + 1]!;
const opcode = (prefix >>> 8) & 0xff;
if (opcode !== 0x8d && opcode !== 0x8b) continue;
if ((bytes[at + 2]! & 0xc7) !== 0x05) continue; // mod 00, rm 101: RIP + disp32
if (((prefix >>> 16) & 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));
const displacement = loaded
? readUInt32(image, address + 3n)
: bytes.readUInt32LE(at + 3);
if (typeof displacement !== "number") continue;
const target = next + BigInt(displacement | 0);
if (arrays.has(target)) referenced.add(target);
}
}
@@ -361,12 +592,22 @@ function* arm64PageReferences(
const referenced = new Set<bigint>();
const bytes = image.bytes;
for (const mapping of image.container.executable_mappings ?? []) {
const loaded =
image.elfPointerOverlay?.hasWrites(mapping.address, mapping.size) ===
true;
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);
const address = mapping.address + BigInt(at);
const adrp = loaded
? readUInt32(image, address)
: bytes.readUInt32LE(mapping.offset + at);
if (typeof adrp !== "number") continue;
if ((adrp & 0x9f000000) >>> 0 !== 0x90000000) continue;
const add = bytes.readUInt32LE(mapping.offset + at + 4);
const add = loaded
? readUInt32(image, address + 4n)
: bytes.readUInt32LE(mapping.offset + at + 4);
if (typeof add !== "number") continue;
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.
@@ -409,37 +650,49 @@ export function* findMetadataRegistration(
// 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);
const fieldCount = readUInt32(image, address + 80n);
if (fieldCount !== "unknown" && fieldCount !== typeDefinitions) continue;
const sizeCount = readUInt32(image, address + 96n);
if (sizeCount !== "unknown" && sizeCount !== typeDefinitions) continue;
if (!hasPointerArrayCoverage(image, address, 16)) continue;
if (readUInt32(image, address + 80n) !== typeDefinitions) continue;
if (readUInt32(image, address + 96n) !== typeDefinitions) continue;
countsMatched = true;
if (fieldCount !== "unknown" && sizeCount !== "unknown")
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);
const types = readSlot(image, address + 56n);
const fieldOffsets = readSlot(image, address + 88n);
const typeSizes = readSlot(image, address + 104n);
if (
typeof count !== "number" ||
count === 0 ||
count > 0x7fffffff ||
count === null ||
(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)
(typeof fieldOffsets === "bigint" &&
!isMapped(image, fieldOffsets, 8)) ||
(typeof typeSizes === "bigint" && !isMapped(image, typeSizes, 8)) ||
(typeof types === "bigint" &&
!isMapped(image, types, typeof count === "number" ? count * 8 : 8))
)
continue;
if (
fieldCount === "unknown" ||
sizeCount === "unknown" ||
count === "unknown" ||
types === "unknown" ||
fieldOffsets === "unknown" ||
typeSizes === "unknown"
)
throw new Il2cppBinaryFormatFailure(
"metadata-registration-not-found",
"A possible MetadataRegistration has unresolved candidate fields",
);
const field = mapping.address + BigInt(at + 48);
// Exact load aliases can visit one virtual registration field twice.
if (matchedFields.has(field)) continue;
@@ -503,7 +756,7 @@ function* findTypeTableByCode(
typeDefinitions: number,
requiredTypes: number,
): Generator<void, { readonly count: number; readonly types: bigint } | null> {
const { bytes, container } = image;
const { 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
@@ -547,25 +800,15 @@ function* findTypeTableByCode(
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.
// 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 (target === 0n || (target & 7n) !== 0n) return "other";
if (recordOffset(target) < 0) return "other";
const attributes = readUInt32(image, target + 8n);
if (attributes === "unknown") return "unknown";
@@ -592,24 +835,61 @@ function* findTypeTableByCode(
const start = Number((8n - (mapping.address % 8n)) % 8n);
let runStart = -1;
let startUnknown = false;
let possibleLength = 0;
let possibleUnknown = 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);
const raw = image.bytes.readBigUInt64LE(mapping.offset + at);
let state: "type" | "other" | "unknown";
if (image.elfPointerOverlay === undefined) {
if (
image.machoPointerFixups?.hasUnresolvedLegacyBytes(address, 8) ===
true
)
state = "unknown";
else {
const rawState = recordState(raw);
if (rawState !== "type") state = rawState;
else {
const slot = readSlot(image, address);
state =
slot === raw
? rawState
: typeof slot === "bigint"
? recordState(slot)
: slot === "unknown"
? "unknown"
: "other";
}
continue;
}
endUnknown = slot === "unknown" || state === "unknown";
} else if (runStart >= 0) endUnknown = unresolved(address);
} else {
const slot = readSlot(image, address);
state =
typeof slot === "bigint"
? recordState(slot)
: slot === "unknown"
? "unknown"
: "other";
}
if (state === "other") {
possibleLength = 0;
possibleUnknown = false;
} else {
possibleLength += 1;
possibleUnknown ||= state === "unknown";
if (possibleUnknown && possibleLength >= requiredTypes) return null;
}
if (state === "type") {
if (runStart < 0) {
runStart = at;
startUnknown = at === start || unresolved(address - 8n);
}
continue;
}
endUnknown = state === "unknown";
}
if (runStart >= 0 && (at - runStart) / 8 >= requiredTypes) {
runs.set(
+6 -5
View File
@@ -1,6 +1,9 @@
import { IL2CPP_METADATA_PROVIDER_IDENTITY } from "./Il2CppMetadataProviderIdentity.js";
import { isSelectedRegularFileError } from "../filesystem/RegularFile.js";
import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js";
import {
Il2cppBinaryFormatFailure,
IL2CPP_BINARY_BYTES,
} from "./Il2cppBinaryFormat.js";
import { Il2cppMetadataFormatFailure } from "./Il2cppMetadataFormat.js";
import type { AnalysisPartialObservation } from "../domain/analysisErrorBase.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
@@ -59,8 +62,6 @@ import {
const OPERATION = "inspect_il2cpp_metadata";
const INPUT_BYTES = 256 * 1024 * 1024;
// GameAssembly binaries of large games reach several hundred megabytes.
const BINARY_BYTES = 1024 * 1024 * 1024;
// A 12.9 MB Unity 6000.4 file with 112,232 methods serializes to 7.8 MB.
const OUTPUT_BYTES = 64 * 1024 * 1024;
/** Instruction form the module-array fallback matched, per architecture. */
@@ -523,7 +524,7 @@ export class Il2cppMetadataProvider implements Il2cppMetadataPort {
try {
const binary = await this.#readArtifact(
binaryPath,
BINARY_BYTES,
IL2CPP_BINARY_BYTES,
options?.signal,
(artifact) => {
observed.binary = artifact;
@@ -556,7 +557,7 @@ export class Il2cppMetadataProvider implements Il2cppMetadataPort {
cause,
binaryPath,
"binary_path",
BINARY_BYTES,
IL2CPP_BINARY_BYTES,
input.path,
);
failure.partialObservation = metadataSourceEvidence(