mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(il2cpp): reject duplicate method tokens per image
Reject duplicate MethodDef row identities within one owning image before publishing native addresses or Ghidra seeds, while retaining cross-image reuse and folded code addresses. Fixes #6610
This commit is contained in:
+20
-16
@@ -206,7 +206,11 @@ image, the single record whose name pointer reaches that image's name string and
|
||||
whose count equals the image's method count. It then requires exactly one
|
||||
`codeGenModulesCount`/`codeGenModules` pair that lists exactly those records. No
|
||||
symbols are needed, so stripped builds work. A method's token row id selects its
|
||||
slot in the module's method pointer array.
|
||||
slot in the module's method pointer array. Within one image, each method must
|
||||
claim a distinct MethodDef token row; duplicate rows are refused before any
|
||||
linked method table or seed output is published. Row ids may repeat across
|
||||
different images, and distinct rows may still resolve to the same address when
|
||||
the binary uses identical-code folding.
|
||||
|
||||
Module records establish names through their declared NUL-terminated string pointers. NUL/0xFF padding, spaces, Unicode and suffix-shared storage are supported. A pointer to a longer string does not match just its suffix; a pointer directly into a shared suffix may name it. Exact reversed UTF-8 matching avoids rescanning the file for every name and retains duplicate-name owners until module records resolve them. A contributor investigated a Linux
|
||||
`GameAssembly.so` from Unity 2023.2.13f1 whose static registration was not located. When no static pair is resolved on x86-64, the reader searches executable ranges for bytes consistent with RIP-relative LEA/MOV references to module-array candidates. Exactly one referenced array candidate must remain. `code_gen_modules_field` is then `null`, and Evidence is **inferred**: instruction boundaries, the absence of a static registration elsewhere, and runtime use were not verified. The module records and slots themselves remain file observations. Static-table associations are **derived**. Otherwise the result is `code-registration-not-found`.
|
||||
@@ -269,21 +273,21 @@ Il2CppType byref flag in different bits, so a rendered name could gain or lose
|
||||
|
||||
The builds checked are listed after the module search rules above.
|
||||
|
||||
| Reason | Category | Meaning |
|
||||
| --------------------------------- | ----------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `unsupported-container` | unsupported | Not a 64-bit little-endian Mach-O, PE or ELF image (ELF: AArch64 or x86-64 with dynamic section). |
|
||||
| `slice-selection-required` | unsupported | A universal Mach-O was given without `binary_architecture`. |
|
||||
| `slice-not-found` | unsupported | The requested slice is absent, or a thin binary was given an architecture. |
|
||||
| `unsupported-pointer-encoding` | unsupported | Mach-O chained fixups or Android packed (APS2) ELF relocations; pointers not decoded. |
|
||||
| `malformed-container` | input error | The container headers are damaged. |
|
||||
| `code-gen-modules-not-found` | input error | An image has no module record; often a binary from a different build. |
|
||||
| `ambiguous-code-gen-module` | input error | More than one module record matches an image. |
|
||||
| `code-registration-not-found` | input error | No unique module table or executable-byte array candidate was resolved. |
|
||||
| `invalid-method-token` | input error | A method token is not a method row inside its module's pointer count. |
|
||||
| `invalid-method-pointer` | input error | A slot is unavailable or its nonzero address lacks unambiguous executable file coverage. |
|
||||
| `metadata-registration-not-found` | input error | No single MetadataRegistration matches the type-definition count. |
|
||||
| `invalid-type` | input error | A type index or `Il2CppType` record is out of range, unmapped or malformed. |
|
||||
| `unsupported-type-kind` | unsupported | A valid type kind has no implemented rendering. |
|
||||
| Reason | Category | Meaning |
|
||||
| --------------------------------- | ----------- | --------------------------------------------------------------------------------------------------- |
|
||||
| `unsupported-container` | unsupported | Not a 64-bit little-endian Mach-O, PE or ELF image (ELF: AArch64 or x86-64 with dynamic section). |
|
||||
| `slice-selection-required` | unsupported | A universal Mach-O was given without `binary_architecture`. |
|
||||
| `slice-not-found` | unsupported | The requested slice is absent, or a thin binary was given an architecture. |
|
||||
| `unsupported-pointer-encoding` | unsupported | Mach-O chained fixups or Android packed (APS2) ELF relocations; pointers not decoded. |
|
||||
| `malformed-container` | input error | The container headers are damaged. |
|
||||
| `code-gen-modules-not-found` | input error | An image has no module record; often a binary from a different build. |
|
||||
| `ambiguous-code-gen-module` | input error | More than one module record matches an image. |
|
||||
| `code-registration-not-found` | input error | No unique module table or executable-byte array candidate was resolved. |
|
||||
| `invalid-method-token` | input error | A token is outside its module's method-pointer rows, or a MethodDef row is reused within one image. |
|
||||
| `invalid-method-pointer` | input error | A slot is unavailable or its nonzero address lacks unambiguous executable file coverage. |
|
||||
| `metadata-registration-not-found` | input error | No single MetadataRegistration matches the type-definition count. |
|
||||
| `invalid-type` | input error | A type index or `Il2CppType` record is out of range, unmapped or malformed. |
|
||||
| `unsupported-type-kind` | unsupported | A valid type kind has no implemented rendering. |
|
||||
|
||||
Format and selection failures name `binary_path`; capacity refusals remain typed resource errors. Failed linking preserves the metadata source identity, validated counts, selected binary path/architecture and its SHA-256 when observed, plus prior producer observations. It never publishes an incomplete native table as complete. The binary input is bounded to1GiB.
|
||||
|
||||
|
||||
@@ -90,6 +90,25 @@ export function* linkIl2cppBinarySteps(
|
||||
methodImage[type.method_start + method] = index;
|
||||
}
|
||||
}
|
||||
const methodByImageAndRow = metadata.images.map(
|
||||
() => new Map<number, number>(),
|
||||
);
|
||||
for (let index = 0; index < methodImage.length; index++) {
|
||||
if (index % LINK_BATCH === 0) yield;
|
||||
const owner = methodImage[index]!;
|
||||
if (owner === -1) continue;
|
||||
const token = facts.methodTokens[index]!;
|
||||
const row = token & 0xffffff;
|
||||
if (token >>> 24 !== 0x06 || row === 0) continue;
|
||||
const methodsByRow = methodByImageAndRow[owner]!;
|
||||
const earlier = methodsByRow.get(row);
|
||||
if (earlier !== undefined)
|
||||
throw new Il2cppBinaryFormatFailure(
|
||||
"invalid-method-token",
|
||||
`methods ${earlier} and ${index} in image ${metadata.images[owner]!.name} share MethodDef token 0x${token.toString(16)}`,
|
||||
);
|
||||
methodsByRow.set(row, index);
|
||||
}
|
||||
yield;
|
||||
const methodAddress = (index: number): Il2cppMethodCode => {
|
||||
const owner = methodImage[index]!;
|
||||
|
||||
@@ -76,6 +76,35 @@ it("links methods to code through module tokens, not metadata order", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects reused MethodDef tokens within an image but preserves cross-image reuse and folded addresses", () => {
|
||||
const duplicateTokens = Uint32Array.from(facts.methodTokens);
|
||||
duplicateTokens[1] = duplicateTokens[0]!;
|
||||
expect(() =>
|
||||
linkIl2cppBinary(createIl2cppBinaryFixture(), metadata, {
|
||||
...facts,
|
||||
methodTokens: duplicateTokens,
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
reason: "invalid-method-token",
|
||||
kind: "malformed",
|
||||
}),
|
||||
);
|
||||
|
||||
// The fixture deliberately reuses MethodDef row ids in its second image.
|
||||
expect(link(createIl2cppBinaryFixture()).method_addresses).toEqual(
|
||||
expectedAddresses,
|
||||
);
|
||||
|
||||
// Distinct rows may still point at the same native address after folding.
|
||||
const folded = createIl2cppBinaryFixture();
|
||||
folded.writeBigUInt64LE(folded.readBigUInt64LE(0x308), 0x300);
|
||||
const linkedFolded = link(folded);
|
||||
expect(linkedFolded.method_addresses[0]?.address).toBe(
|
||||
linkedFolded.method_addresses[1]?.address,
|
||||
);
|
||||
});
|
||||
|
||||
it("requires an explicit slice of a universal Mach-O and reads only that slice", () => {
|
||||
const thin = createIl2cppBinaryFixture();
|
||||
const universal = createUniversalFixture([
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { setImmediate } from "node:timers/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
AnalysisResourceConstraintError,
|
||||
} from "../../../src/domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
||||
import { Il2cppMetadataProvider } from "../../../src/il2cpp/Il2cppMetadataProvider.js";
|
||||
import { Il2cppGhidraSeedWriter } from "../../../src/il2cpp/Il2cppGhidraSeedWriter.js";
|
||||
import { Il2cppMetadataService } from "../../../src/application/il2cpp/Il2cppMetadataService.js";
|
||||
import { il2cppMetadataSchema } from "../../../src/domain/il2cpp/il2cppMetadata.js";
|
||||
import {
|
||||
createElfFixture,
|
||||
createIl2cppBinaryFixture,
|
||||
@@ -548,6 +552,65 @@ it("accepts cancellation between binary linking batches and keeps the metadata s
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses duplicate MethodDef tokens before publishing linked Evidence or Ghidra seeds", async () => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-il2cpp-duplicate-method-token-",
|
||||
);
|
||||
const path = join(root, "global-metadata.dat");
|
||||
const binaryPath = join(root, "GameAssembly.dylib");
|
||||
const duplicateSeeds = join(root, "duplicate.tsv");
|
||||
const foldedSeeds = join(root, "folded.tsv");
|
||||
const fixture = createIl2cppMetadataFixture({ version: 31 });
|
||||
const duplicate = Buffer.from(fixture.bytes);
|
||||
const firstTokenAt = fixture.record("methods", 0) + 24;
|
||||
const secondTokenAt = fixture.record("methods", 1) + 24;
|
||||
duplicate.writeUInt32LE(duplicate.readUInt32LE(firstTokenAt), secondTokenAt);
|
||||
await writeFile(path, duplicate);
|
||||
await writeFile(binaryPath, createIl2cppBinaryFixture());
|
||||
|
||||
const service = new Il2cppMetadataService(
|
||||
new Il2cppMetadataProvider(),
|
||||
new Il2cppGhidraSeedWriter(),
|
||||
);
|
||||
try {
|
||||
const refused = await service.inspectWithGhidraSeeds(
|
||||
{ path, binary_path: binaryPath, detail: "complete" },
|
||||
{ path: duplicateSeeds },
|
||||
);
|
||||
expect(refused.ok).toBe(false);
|
||||
if (refused.ok)
|
||||
throw new Error("Duplicate MethodDef identity was accepted");
|
||||
if (!(refused.error instanceof AnalysisInputError))
|
||||
throw new Error("Duplicate MethodDef identity was not an input refusal");
|
||||
expect(refused.error.issues[0]?.message).toContain("invalid-method-token");
|
||||
expect(refused.error.issues[0]?.message).toContain("share MethodDef token");
|
||||
expect(refused.error.partialObservation).toMatchObject({
|
||||
normalized_result: { artifact: { path } },
|
||||
});
|
||||
await expect(readFile(duplicateSeeds)).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
|
||||
const foldedBinary = createIl2cppBinaryFixture();
|
||||
foldedBinary.writeBigUInt64LE(foldedBinary.readBigUInt64LE(0x308), 0x300);
|
||||
await writeFile(path, fixture.bytes);
|
||||
await writeFile(binaryPath, foldedBinary);
|
||||
const accepted = await service.inspectWithGhidraSeeds(
|
||||
{ path, binary_path: binaryPath, detail: "complete" },
|
||||
{ path: foldedSeeds },
|
||||
);
|
||||
expect(accepted.ok).toBe(true);
|
||||
if (!accepted.ok) throw accepted.error;
|
||||
const result = il2cppMetadataSchema.parse(accepted.value.normalized_result);
|
||||
expect(result.method_addresses[0]?.address).toBe(
|
||||
result.method_addresses[1]?.address,
|
||||
);
|
||||
expect(await readFile(foldedSeeds, "utf8")).toContain("# folded");
|
||||
} finally {
|
||||
await service.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("retains both observed file identities and the selected architecture when native linking fails", async () => {
|
||||
const root = await createTestTempDirectory("rea-il2cpp-failed-pair-");
|
||||
const path = join(root, "global-metadata.dat"),
|
||||
|
||||
Reference in New Issue
Block a user