mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
8b3902d377
Resolve Go pointer descriptors and strings across complete, consistent file-backed spans, including PE headers and adjacent ELF load ranges. Reuse the canonical image-mapping selection for native exports. Preserve existing gaps, alias, zero-fill and capacity refusals. Forward parsed zero-fill ownership through IL2CPP relocation tables, destinations and mapped-span checks. Distinguish missing spans from ambiguous ownership in the shared mapping failure: unresolved relocation targets may remain unknown, while ambiguous raw registration bytes cannot be reused as a resolved count. Validation: parser, provider, service, native-export, IL2CPP and public CLI/MCP regressions cover PE32/PE32+ headers, split ELF descriptors/contents, relocation count conflicts, ordinary unresolved targets and unchanged inputs. Authored carriers establish static parsing, not Windows loading or actual legacy Go linker emission. Fixes #6831 Fixes #6830
375 lines
11 KiB
TypeScript
375 lines
11 KiB
TypeScript
import {
|
|
readFatContainerHeader,
|
|
validateFatSlice,
|
|
} from "../domain/apple/machoContainer.js";
|
|
import {
|
|
BinaryFormatFailure,
|
|
BinaryResourceFailure,
|
|
mappedFileOffset,
|
|
isZeroFilledSpan,
|
|
type BinaryContainer,
|
|
} 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;
|
|
readonly imageBase: bigint;
|
|
readonly unresolvedPointerSlots: ReadonlyMap<bigint, number>;
|
|
readonly zeroFillPointers: ReadonlyMap<bigint, bigint>;
|
|
/** Mach-O loader fixups, used when interpreting method pointer slots. */
|
|
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.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, 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,
|
|
imageBase,
|
|
unresolvedPointerSlots: relocated?.unresolved ?? new Map<bigint, number>(),
|
|
zeroFillPointers: relocated?.zeroFillPointers ?? new Map<bigint, bigint>(),
|
|
...(machoPointerFixups === undefined ? {} : { machoPointerFixups }),
|
|
};
|
|
}
|
|
|
|
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;
|
|
try {
|
|
const offset = mappedFileOffset(
|
|
image.container.mappings,
|
|
address,
|
|
8,
|
|
image.container.zero_fills,
|
|
);
|
|
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 | null => {
|
|
if (hasUnresolvedPointer(image, address, 4)) return null;
|
|
const virtual = image.zeroFillPointers.get(address);
|
|
if (virtual !== undefined) return Number(virtual & 0xffffffffn);
|
|
try {
|
|
return image.bytes.readUInt32LE(
|
|
mappedFileOffset(
|
|
image.container.mappings,
|
|
address,
|
|
4,
|
|
image.container.zero_fills,
|
|
),
|
|
);
|
|
} catch (cause) {
|
|
if (cause instanceof BinaryFormatFailure) return null;
|
|
throw cause;
|
|
}
|
|
};
|
|
|
|
export const isMapped = (image: NativeImage, address: bigint, size: number) => {
|
|
try {
|
|
mappedFileOffset(
|
|
image.container.mappings,
|
|
address,
|
|
size,
|
|
image.container.zero_fills,
|
|
);
|
|
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 {
|
|
mappedFileOffset(
|
|
image.container.mappings,
|
|
cursor,
|
|
size,
|
|
image.container.zero_fills,
|
|
);
|
|
}
|
|
} 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 = mappedFileOffset(
|
|
image.container.mappings,
|
|
address,
|
|
1,
|
|
image.container.zero_fills,
|
|
);
|
|
return (
|
|
offset ===
|
|
mappedFileOffset(image.container.executable_mappings ?? [], address, 1)
|
|
);
|
|
} catch (cause) {
|
|
if (cause instanceof BinaryFormatFailure) return false;
|
|
throw cause;
|
|
}
|
|
};
|
|
|
|
const hasUnresolvedPointer = (
|
|
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 (hasUnresolvedPointer(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);
|
|
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;
|
|
}
|
|
};
|