mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
* fix(il2cpp): report the exceeded native-link maximum in the public refusal * fix(il2cpp): report each native-link budget's maximum in the unit its producer counts * refactor(elf): report pointer relocation failures through ParseFailure (#10748)
371 lines
12 KiB
TypeScript
371 lines
12 KiB
TypeScript
import { parseFailureFromCause } from "../domain/parseFailure.js";
|
|
import { binaryMetadataArchitecture } from "../domain/binaryMetadataArchitecture.js";
|
|
import {
|
|
IL2CPP_SLICE_SELECTORS,
|
|
type Il2cppBinaryArchitecture,
|
|
} from "../domain/il2cpp/il2cppMetadata.js";
|
|
import {
|
|
BinaryFormatFailure,
|
|
BinaryMappingFailure,
|
|
BinaryResourceFailure,
|
|
createFileMappingIndex,
|
|
mappedFileOffset,
|
|
isZeroFilledSpan,
|
|
type BinaryContainer,
|
|
type FileMappingIndex,
|
|
} from "../native/metadata/BinaryContainer.js";
|
|
import { readBinaryContainer } from "../native/metadata/readBinaryContainer.js";
|
|
import {
|
|
requireSelectedNativeImage,
|
|
type SelectedNativeImage,
|
|
type SliceSelectionInput,
|
|
} from "../native/metadata/MachoSliceSelection.js";
|
|
import {
|
|
parsePointerFixupsSteps,
|
|
type PointerFixups,
|
|
} from "../native/AppleMachoFixups.js";
|
|
import {
|
|
parseMachoLayout,
|
|
UnsupportedMachoMetadataFormatError,
|
|
} from "../native/AppleMachoSelection.js";
|
|
import {
|
|
readElfPointerValuesSteps,
|
|
ElfPointerRelocationFailure,
|
|
type ElfVirtualPointerWrites,
|
|
} from "../native/metadata/ElfPointerRelocations.js";
|
|
import {
|
|
Il2cppBinaryFormatFailure,
|
|
IL2CPP_BINARY_BYTES,
|
|
} from "./Il2cppBinaryFormat.js";
|
|
|
|
/** Selection failures name the binary's inputs and IL2CPP's public spellings. */
|
|
const IL2CPP_SLICE_SELECTION_INPUT: SliceSelectionInput = {
|
|
pathField: "binary_path",
|
|
architectureField: "binary_architecture",
|
|
spelling: Object.fromEntries(
|
|
Object.entries(IL2CPP_SLICE_SELECTORS).map(([publicName, selector]) => [
|
|
selector,
|
|
publicName,
|
|
]),
|
|
),
|
|
};
|
|
|
|
/**
|
|
* Select the thin image of the caller's binary: IL2CPP's `binary_architecture`
|
|
* maps onto the shared slice selector here, once, and a selection failure
|
|
* names `binary_architecture` or `binary_path`.
|
|
*/
|
|
export const requireIl2cppBinaryImage = (
|
|
operation: string,
|
|
path: string,
|
|
bytes: Buffer,
|
|
architecture: Il2cppBinaryArchitecture | undefined,
|
|
): SelectedNativeImage =>
|
|
requireSelectedNativeImage(
|
|
operation,
|
|
path,
|
|
bytes,
|
|
architecture === undefined
|
|
? undefined
|
|
: IL2CPP_SLICE_SELECTORS[architecture],
|
|
IL2CPP_SLICE_SELECTION_INPUT,
|
|
);
|
|
|
|
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;
|
|
/** 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;
|
|
}
|
|
|
|
/** Open one thin image; a universal Mach-O slice is selected before this. */
|
|
export function* openImageSteps(image: Buffer): Generator<void, NativeImage> {
|
|
let container: BinaryContainer;
|
|
try {
|
|
container = readBinaryContainer(image);
|
|
} catch (cause) {
|
|
if (cause instanceof BinaryResourceFailure)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"native-index-capacity",
|
|
cause.message,
|
|
cause.failure.limit.maximum,
|
|
"bytes",
|
|
);
|
|
if (cause instanceof BinaryFormatFailure)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
cause.kind === "unsupported"
|
|
? "unsupported-container"
|
|
: "malformed-container",
|
|
cause.message,
|
|
);
|
|
throw cause;
|
|
}
|
|
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 elfPointerOverlay: ElfVirtualPointerWrites | undefined;
|
|
try {
|
|
if (container.format === "elf")
|
|
elfPointerOverlay = yield* readElfPointerValuesSteps(
|
|
image,
|
|
{
|
|
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) {
|
|
if (cause.failure.kind === "limit" && cause.unit !== undefined)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"native-index-capacity",
|
|
cause.message,
|
|
cause.failure.limit.maximum,
|
|
cause.unit,
|
|
);
|
|
if (cause.reason !== "native-index-capacity")
|
|
throw new Il2cppBinaryFormatFailure(cause.reason, cause.message);
|
|
}
|
|
if (cause instanceof BinaryFormatFailure)
|
|
throw new Il2cppBinaryFormatFailure("malformed-container", cause.message);
|
|
throw cause;
|
|
}
|
|
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 = yield* parsePointerFixupsSteps(
|
|
image,
|
|
layout.fixups,
|
|
layout.segments,
|
|
layout.baseAddress,
|
|
);
|
|
} catch (cause) {
|
|
if (cause instanceof UnsupportedMachoMetadataFormatError)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"unsupported-container",
|
|
cause.message,
|
|
);
|
|
const failure = parseFailureFromCause(cause);
|
|
if (failure !== undefined)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
failure.kind === "unsupported"
|
|
? "unsupported-container"
|
|
: "malformed-container",
|
|
failure.message,
|
|
);
|
|
// Mach-O slice selection reports an inconsistent FAT or CPU identity as
|
|
// a TypeError.
|
|
if (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,
|
|
...(elfPointerOverlay === undefined ? {} : { elfPointerOverlay }),
|
|
...(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 (image.machoPointerFixups?.hasUnresolvedLegacyBytes(address, 4) === true)
|
|
return "unknown";
|
|
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;
|
|
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;
|
|
}
|
|
};
|
|
|
|
export const readSlot = (
|
|
image: NativeImage,
|
|
address: bigint,
|
|
): bigint | "unknown" | null => {
|
|
try {
|
|
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);
|
|
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 BinaryMappingFailure) return null;
|
|
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;
|
|
};
|