mirror of
https://github.com/morluto/rea.git
synced 2026-10-29 10:55:57 +00:00
* fix(il2cpp): accept arm64 module arrays loaded through adrp/add pairs A Unity 2022.3 macOS Master (LTO) arm64 build keeps no static CodeRegistration, so the module-array fallback that only decoded x86-64 RIP-relative references reported code-registration-not-found. The registration owner now also decodes aligned adrp + add pairs in executable ranges and keeps the same exact-module-set and single-reference rules. A minimal Mach-O fixture failed on the previous reader (47 passed / 1 failed) and passes here with four fail-closed variants (48/48 binary tests). * fix(il2cpp): name the matched instruction form in inferred module-array limits The inferred-array limitation always described RIP-relative LEA/MOV, which is wrong for arm64 results. The provider now names the adrp/add pair on arm64, and the tool contract and docs cover both forms and state that a located array alone does not finish linking a Master build. The new arm64 provider case failed before the wording change (1 passed / 1 failed) and both cases pass now. * fix(il2cpp): accept type tables loaded without a static MetadataRegistration A Unity 2022.3.62f3 macOS Master build inlines MetadataRegistration as well as CodeRegistration, so linking stopped at metadata-registration-not-found after the module array was found. Registration now infers the only field linking reads, the Il2CppType table, from the one run of type-shaped pointers that covers every referenced type index and whose start is loaded by the existing RIP-relative or adrp/add forms; types_field is null and the Evidence is inferred. The new arm64 and x86-64 fixtures failed on the previous reader (52 passed / 2 failed). Four run-selection variants (interior reference, run extended before the loaded address, two loaded runs, run shorter than the referenced indices) keep metadata-registration-not-found; instruction decoding and record-shape checks are not covered by these fixtures. * test(il2cpp): pin adrp page deltas, register pairing and type-record shape Every adrp fixture targeted its own 4 KiB page, so dropping the page delta or its sign extension, the add register check, the type-kind bound or the type-definition bound left all binary tests passing. Fixtures now reach a later and an earlier page, pair an add with a different destination or source register, break the type run with an out-of-range kind or type-definition index, and bound type-table candidates when the metadata references few type indices. Each of those five source mutations now fails 1 to 3 of 85 binary and provider tests. * refactor(il2cpp): share Il2CppType kind decoding and document type-table bounds The type-table fallback decoded the Il2CppType kind and class/value-type kinds separately from the type namer; both now use the Types owner. Zero and unaligned words are rejected before mapping lookup (Master sample metadata-registration search 92 to 68 ms). Docs and the limitation state that types_count is an upper bound, that a run spanning mappings or a small referenced type range can fail, and credit Cpp2IL StrippedCodeRegSupport and the data-side MetadataRegistration searches of Il2CppDumper and Cpp2IL. Master sample output is unchanged (11,031 code addresses, 37,025 type rows). * fix(il2cpp): refuse generic instances when the type table was inferred An inferred type table has no static MetadataRegistration, so the genericMethodTable it would anchor cannot be located. Report generic-methods-unsupported instead of reading from a null field. * fix(il2cpp): fail closed when an inferred type table is not unique Adversarial review found that the type-table fallback could pick another type-shaped pointer table when the real one was unloaded or excluded, could replace a static MetadataRegistration whose counts matched, and accepted a run cut short by a mapping end. Require exactly one long-enough run in the image, ending inside its mapping and loaded by code; skip the fallback when a record's two counts matched; accept only assigned Il2CppTypeEnum kinds; and look records up through the indexed file mapping so many mappings cannot multiply the scan. Docs and limitations now describe the run length as at least every referenced type index rather than an upper bound. * fix(il2cpp): keep the type-table fallback cheap and its refusals exact Review follow-ups: check that an address can lie in a mapping before the throwing indexed lookup, so words pointing into gaps or zero-fill no longer build an exception each (64 MiB: 74 s to under 2 s); treat a run starting at its mapping start like one reaching its end; stop at the second long-enough run instead of a 64-candidate capacity error; ignore ADRP/ADD pairs on register 31; and say whether a counted MetadataRegistration was unusable or no code-loaded run was found. Docs no longer generalize from one build or state tool history they do not source. * fix(il2cpp): judge type-table slots by their loaded value The run scan read stored words, so a dyld-bound slot's placeholder could decide whether a run qualified. Confirm each slot through readSlot and give a run bounded by a bound or unresolved slot an unknown extent, so the placeholder no longer changes the choice. * docs(il2cpp): keep the failure-reason table layout Shorten the two changed meanings so the table keeps its column widths and the diff shows only those rows. * docs(il2cpp): keep the reason table at its formatted width * fix(il2cpp): read type-table record fields through the loader too The previous change confirmed table slots through readSlot but still read each record's kind and definition index from stored bytes. Read them with readUInt32 and readSlot, and treat a record field the loader binds or leaves unresolved like an unresolved slot: the run it bounds has an unknown extent. * perf(il2cpp): share image mappings across loader-aware reads --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
377 lines
12 KiB
TypeScript
377 lines
12 KiB
TypeScript
import {
|
|
readFatContainerHeader,
|
|
validateFatSlice,
|
|
} from "../domain/apple/machoContainer.js";
|
|
import { binaryMetadataArchitecture } from "../domain/binaryMetadataArchitecture.js";
|
|
import {
|
|
BinaryFormatFailure,
|
|
BinaryResourceFailure,
|
|
createFileMappingIndex,
|
|
mappedFileOffset,
|
|
isZeroFilledSpan,
|
|
type BinaryContainer,
|
|
type FileMappingIndex,
|
|
} from "../native/metadata/BinaryContainer.js";
|
|
import { readBinaryContainer } from "../native/metadata/readBinaryContainer.js";
|
|
import {
|
|
parsePointerFixups,
|
|
type PointerFixups,
|
|
} from "../native/AppleMachoFixups.js";
|
|
import {
|
|
parseMachoLayout,
|
|
UnsupportedMachoMetadataFormatError,
|
|
} from "../native/AppleMachoSelection.js";
|
|
import { applyElfRelativeRelocationsSteps } from "./ElfRelocations.js";
|
|
import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js";
|
|
const MACHO_CPU = new Map([
|
|
[0x01000007, "amd64"],
|
|
[0x0100000c, "arm64"],
|
|
]);
|
|
|
|
/** Select one thin image; a universal Mach-O needs the caller's architecture. */
|
|
const selectImage = (
|
|
bytes: Buffer,
|
|
architecture: string | undefined,
|
|
): Buffer => {
|
|
const fat = readFatContainerHeader(bytes, bytes.length);
|
|
if (fat.state === "not-fat") {
|
|
if (architecture !== undefined)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"slice-not-found",
|
|
`architecture ${architecture} was requested but the binary is not a universal Mach-O`,
|
|
);
|
|
return bytes;
|
|
}
|
|
if (fat.state !== "parsed")
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"malformed-container",
|
|
fat.state === "malformed" ? fat.reason : "universal header is truncated",
|
|
);
|
|
const available = fat.slices.map(
|
|
(slice) => MACHO_CPU.get(slice.cpuType) ?? `macho-cpu-${slice.cpuType}`,
|
|
);
|
|
if (architecture === undefined)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"slice-selection-required",
|
|
`universal Mach-O holds ${available.join(", ")}; select one architecture`,
|
|
);
|
|
if (available.filter((value) => value === architecture).length > 1)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"ambiguous-slice-selection",
|
|
`multiple universal slices declare ${architecture}; no slice was selected`,
|
|
);
|
|
const index = available.indexOf(architecture);
|
|
const slice = fat.slices[index];
|
|
if (slice === undefined)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"slice-not-found",
|
|
`universal Mach-O holds ${available.join(", ")}, not ${architecture}`,
|
|
);
|
|
const issue = validateFatSlice(slice, bytes.length, fat.tableEnd);
|
|
if (issue !== null)
|
|
throw new Il2cppBinaryFormatFailure("malformed-container", issue);
|
|
return bytes.subarray(slice.offset, slice.offset + slice.size);
|
|
};
|
|
|
|
export interface NativeImage {
|
|
readonly bytes: Buffer;
|
|
readonly container: BinaryContainer;
|
|
/** 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>;
|
|
/** Mach-O loader fixups shared by every native pointer consumer. */
|
|
readonly machoPointerFixups?: PointerFixups;
|
|
}
|
|
|
|
export function* openImageSteps(
|
|
bytes: Buffer,
|
|
architecture: string | undefined,
|
|
): Generator<void, NativeImage> {
|
|
let image = selectImage(bytes, architecture);
|
|
let container: BinaryContainer;
|
|
try {
|
|
container = readBinaryContainer(image);
|
|
} catch (cause) {
|
|
if (cause instanceof BinaryResourceFailure)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"native-index-capacity",
|
|
cause.message,
|
|
);
|
|
if (cause instanceof BinaryFormatFailure)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
cause.kind === "unsupported"
|
|
? "unsupported-container"
|
|
: "malformed-container",
|
|
cause.message,
|
|
);
|
|
throw cause;
|
|
}
|
|
if (
|
|
architecture !== undefined &&
|
|
(container.format !== "macho" || container.architecture !== architecture)
|
|
)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"malformed-container",
|
|
"Selected FAT declaration disagrees with the slice header architecture.",
|
|
);
|
|
if (container.bits !== 64 || container.byte_order !== "little")
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"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;
|
|
try {
|
|
if (container.format === "elf")
|
|
relocated = yield* applyElfRelativeRelocationsSteps(
|
|
image,
|
|
container.elf,
|
|
container.mappings,
|
|
container.zero_fills,
|
|
);
|
|
} catch (cause) {
|
|
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",
|
|
"Mach-O chained fixups are not decoded",
|
|
);
|
|
let machoPointerFixups: PointerFixups | undefined;
|
|
if (container.format === "macho") {
|
|
try {
|
|
const layout = parseMachoLayout(
|
|
image,
|
|
binaryMetadataArchitecture(container.architecture) ??
|
|
container.architecture,
|
|
);
|
|
machoPointerFixups = parsePointerFixups(
|
|
image,
|
|
layout.fixups,
|
|
layout.segments,
|
|
layout.baseAddress,
|
|
);
|
|
} catch (cause) {
|
|
if (cause instanceof UnsupportedMachoMetadataFormatError)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"unsupported-container",
|
|
cause.message,
|
|
);
|
|
if (cause instanceof RangeError || cause instanceof TypeError)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"malformed-container",
|
|
cause.message,
|
|
);
|
|
throw cause;
|
|
}
|
|
}
|
|
if (container.format === "pe" && container.image_base === undefined)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"malformed-container",
|
|
"PE image base was not observed",
|
|
);
|
|
const imageBase =
|
|
container.image_base ??
|
|
container.mappings.find((mapping) => mapping.offset === 0)?.address ??
|
|
container.mappings.reduce<bigint | undefined>(
|
|
(lowest, mapping) =>
|
|
lowest === undefined || mapping.address < lowest
|
|
? mapping.address
|
|
: lowest,
|
|
undefined,
|
|
) ??
|
|
0n;
|
|
return {
|
|
bytes: image,
|
|
container,
|
|
fileMappings: createFileMappingIndex(
|
|
container.mappings,
|
|
container.zero_fills,
|
|
),
|
|
imageBase,
|
|
unresolvedPointerSlots: relocated?.unresolved ?? new Map<bigint, number>(),
|
|
zeroFillPointers: relocated?.zeroFillPointers ?? new Map<bigint, bigint>(),
|
|
...(machoPointerFixups === undefined ? {} : { machoPointerFixups }),
|
|
};
|
|
}
|
|
|
|
/** Stored bytes only; loader interpretation belongs to readSlot. */
|
|
const readStoredWord = (image: NativeImage, address: bigint): bigint | null => {
|
|
try {
|
|
const offset = image.fileMappings.offset(address, 8);
|
|
return image.bytes.readBigUInt64LE(offset);
|
|
} catch (cause) {
|
|
if (cause instanceof BinaryFormatFailure) return null;
|
|
throw cause;
|
|
}
|
|
};
|
|
|
|
/** A UInt32 field owns four bytes; reading a pointer would also require unrelated padding. */
|
|
export const readUInt32 = (
|
|
image: NativeImage,
|
|
address: bigint,
|
|
): 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 {
|
|
return image.bytes.readUInt32LE(image.fileMappings.offset(address, 4));
|
|
} catch (cause) {
|
|
if (cause instanceof BinaryFormatFailure) return null;
|
|
throw cause;
|
|
}
|
|
};
|
|
|
|
/** Exact lowercase hexadecimal form of an address in reports and issues. */
|
|
export const hexAddress = (value: bigint) => `0x${value.toString(16)}`;
|
|
|
|
export const isMapped = (image: NativeImage, address: bigint, size: number) => {
|
|
try {
|
|
image.fileMappings.offset(address, size);
|
|
return true;
|
|
} catch (cause) {
|
|
if (cause instanceof BinaryFormatFailure) return false;
|
|
throw cause;
|
|
}
|
|
};
|
|
|
|
// An array of slots that all lack a body can sit wholly in zero-fill (.bss):
|
|
// an iOS-only assembly in an Android build has its pointers there.
|
|
export const isZeroFilled = (
|
|
image: NativeImage,
|
|
address: bigint,
|
|
size: number,
|
|
) =>
|
|
isZeroFilledSpan(
|
|
image.container.mappings,
|
|
image.container.zero_fills,
|
|
address,
|
|
size,
|
|
);
|
|
|
|
/** Admit readable slots across loader ranges without walking every pointer.
|
|
* Each chunk uses the format owner's alias checks; a gap or split slot is refused.
|
|
*/
|
|
export const hasPointerArrayCoverage = (
|
|
image: NativeImage,
|
|
address: bigint,
|
|
count: number,
|
|
): boolean => {
|
|
const end = address + BigInt(count) * 8n;
|
|
let cursor = address;
|
|
while (cursor < end) {
|
|
let fileEnd = cursor;
|
|
for (const range of image.container.mappings)
|
|
if (
|
|
range.address <= cursor &&
|
|
range.address + BigInt(range.size) > fileEnd
|
|
)
|
|
fileEnd = range.address + BigInt(range.size);
|
|
let coveredEnd = fileEnd;
|
|
if (fileEnd === cursor)
|
|
for (const range of image.container.zero_fills)
|
|
if (range.address <= cursor && range.address + range.size > coveredEnd)
|
|
coveredEnd = range.address + range.size;
|
|
const stop = coveredEnd < end ? coveredEnd : end;
|
|
const size = Number(((stop - cursor) / 8n) * 8n);
|
|
if (size <= 0) return false;
|
|
try {
|
|
if (fileEnd === cursor) {
|
|
if (!isZeroFilled(image, cursor, size)) return false;
|
|
} else {
|
|
image.fileMappings.offset(cursor, size);
|
|
}
|
|
} catch (cause) {
|
|
if (cause instanceof BinaryFormatFailure) return false;
|
|
throw cause;
|
|
}
|
|
cursor += BigInt(size);
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/** Both executable coverage and the unique full loader mapping must agree. */
|
|
export const isExecutable = (image: NativeImage, address: bigint): boolean => {
|
|
try {
|
|
const offset = image.fileMappings.offset(address, 1);
|
|
return (
|
|
offset ===
|
|
mappedFileOffset(image.container.executable_mappings ?? [], address, 1)
|
|
);
|
|
} catch (cause) {
|
|
if (cause instanceof BinaryFormatFailure) return false;
|
|
throw cause;
|
|
}
|
|
};
|
|
|
|
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 (raw === null) return null;
|
|
if (image.machoPointerFixups !== undefined) {
|
|
const resolution = image.machoPointerFixups.decode(address, raw);
|
|
if (resolution.kind === "bind" || resolution.kind === "unknown")
|
|
return "unknown";
|
|
if (resolution.kind === "unsupported")
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"unsupported-pointer-encoding",
|
|
resolution.reason,
|
|
);
|
|
return resolution.target;
|
|
}
|
|
return raw;
|
|
} catch (cause) {
|
|
if (cause instanceof BinaryFormatFailure)
|
|
throw new Il2cppBinaryFormatFailure("malformed-container", cause.message);
|
|
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;
|
|
};
|