mirror of
https://github.com/morluto/rea.git
synced 2026-10-29 10:55:57 +00:00
* feat(il2cpp): read generic containers on demand and render method spec names Generic container indices and records are demand readers on the same metadata snapshot, so links that do not ask for generic instances allocate nothing. Type rendering keeps one graph walk with its workspace, depth and cycle guards; the C#-like names and the Il2CppDumper GetMethodSpecName names are two styles over it. * feat(il2cpp): link generic method instances with admitted rows and kept issues generic_instances links every genericMethodTable row through methodSpecs, genericInsts and genericMethodPointers, choosing the one CodeRegistration layout whose counts bound every row. The table is reserved against the report budget before any row is resolved; rows and issues are admitted before retention, and a contradicting row stays unknown with an address-backed issue instead of failing the link. Views page the rows by definition, state, address and name, unrequested and historical collections stay unknown, and Ghidra seeds fold instances after the definitions. * feat(il2cpp): accept --generic-instances on the CLI and describe it in tool contracts * docs(il2cpp): describe generic method instances and their limits * fix(il2cpp): keep issued generic rows unknown and name their row A method spec that contradicted the metadata still reported its pointer slot as code, while the schema and provider limitation promised unknown. Such rows now keep no address, every issue carries its genericMethodTable row, the domain validator requires unknown exactly on issued rows, and shared specs name one other row instead of all of them. The row-size lower bound moves to the metadata admission owner, the hex helper is shared, and budget wording says checked rather than reserved. * test(il2cpp): reject ambiguous generic layouts and pin budget, yield and limit paths * fix(il2cpp): keep unmapped generic slots per row and reject undeclared arguments Layout selection no longer requires every genericMethodPointers slot to be mapped; each row reads its own slot, so one unmapped slot leaves only that row unknown with an issue. Method specs whose class or method argument count differs from the owner's declared generic container are issued and unnamed. Fixtures now declare the containers their generic rows fill. * fix(il2cpp): page generic method issues instead of retaining them in summaries Summaries keep generic_methods without its issues and count them in counts.generic_method_issues; the generic_method_issues collection pages them by rows, indices and fields, as code_gen_modules already does. Docs also name the method_indices filter the request schema accepts. * fix(il2cpp): read split generic arguments per slot and method containers on demand Generic argument arrays may span adjacent loader ranges: admit them with hasPointerArrayCoverage and read each slot through readPointer, instead of requiring the whole array in one mapping. Method generic container indices are read on demand from the method record, like type containers, so no link path allocates a per-method column. An owner whose container index is out of range now leaves the method spec unnamed with an issue instead of naming it unchecked. * fix(il2cpp): report generic entries past the address space at their row A damaged methodSpecs or genericMethodPointers pointer could make a spec or slot address exceed 64 bits. The issue kept that address, which the address schema rejects, so inspection failed instead of leaving the row unknown. Check the entry range first and attach the issue to the genericMethodTable row (or its slot field). * chore(il2cpp): remove unrelated formatting changes * fix: restore adb float pattern and mobile fixture from main The merge commit's lint-staged run rewrote these two main files: oxlint --fix dropped the escape in [+\-], which is invalid under the RegExp v flag. * fix(il2cpp): preserve unreadable generic method identities * docs(il2cpp): record generic-instance workflow verification * docs(il2cpp): qualify final integration checks --------- Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
260 lines
8.8 KiB
TypeScript
260 lines
8.8 KiB
TypeScript
import {
|
|
MAX_GHIDRA_SEED_BYTES,
|
|
MAX_GHIDRA_SEED_ENTRIES,
|
|
MAX_GHIDRA_SEED_NAME_LENGTH,
|
|
} from "../ghidra/GhidraAnalysisSeeds.js";
|
|
import {
|
|
AnalysisInputError,
|
|
AnalysisResourceConstraintError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
|
|
const OPERATION = "inspect_il2cpp_metadata";
|
|
const capacity = (
|
|
message: string,
|
|
allowance: string,
|
|
kind: "bytes" | "count",
|
|
) =>
|
|
new AnalysisResourceConstraintError(
|
|
OPERATION,
|
|
"capacity",
|
|
message,
|
|
{
|
|
maximum_seed_bytes: MAX_GHIDRA_SEED_BYTES,
|
|
maximum_seed_entries: MAX_GHIDRA_SEED_ENTRIES,
|
|
maximum_seed_name_characters: MAX_GHIDRA_SEED_NAME_LENGTH,
|
|
},
|
|
fixedAllowanceOptions(allowance, kind),
|
|
);
|
|
import type { Il2cppMetadata } from "../domain/il2cpp/il2cppMetadata.js";
|
|
|
|
/** A linked metadata report reduced to Ghidra function seeds. */
|
|
export interface Il2cppGhidraSeeds {
|
|
/** `address<TAB>function<TAB>name` lines in the REA_GHIDRA_SEED_FILE format. */
|
|
readonly text: string;
|
|
readonly functions: number;
|
|
/** Methods whose code address another method already seeded (identical code folding). */
|
|
readonly folded: number;
|
|
/** Each folded address with the name its function seed kept and the names it shares. */
|
|
readonly folded_groups: readonly {
|
|
readonly address: string;
|
|
readonly kept: string;
|
|
readonly also: readonly string[];
|
|
}[];
|
|
/** Methods without native code: abstract, interface, or generic definitions. */
|
|
readonly without_address: number;
|
|
readonly no_body: number;
|
|
readonly unknown: number;
|
|
/** Generic instance rows seeded or folded; null when the report did not link them. */
|
|
readonly generic_instances: number | null;
|
|
/** Generic instance rows left unseeded (no code or no name); null as above. */
|
|
readonly generic_without_seed: number | null;
|
|
}
|
|
|
|
/**
|
|
* Where Ghidra's default loaders place the binary: an ELF linked at 0 goes to
|
|
* 0x100000, while Mach-O and PE images keep their link base.
|
|
*/
|
|
export const ghidraDefaultBase = (
|
|
binary: NonNullable<Il2cppMetadata["binary"]>,
|
|
): bigint => {
|
|
const linked = BigInt(binary.image_base);
|
|
return binary.format === "elf" && linked === 0n ? 0x100000n : linked;
|
|
};
|
|
|
|
/** `Namespace.Outer.Inner` for a type, following declaring_type to the outermost type. */
|
|
const typeName = (report: Il2cppMetadata, index: number): string => {
|
|
const parts: string[] = [];
|
|
let namespace = "";
|
|
const seen = new Set<number>();
|
|
let length = 0;
|
|
for (let at = index; at !== -1;) {
|
|
if (seen.has(at))
|
|
throw new AnalysisInputError(OPERATION, undefined, [
|
|
{
|
|
path: ["types", at],
|
|
reason: "invalid_format",
|
|
message: "Cyclic type ancestry cannot name a seed.",
|
|
},
|
|
]);
|
|
seen.add(at);
|
|
const type = report.types[at];
|
|
if (type === undefined)
|
|
throw new AnalysisInputError(OPERATION, undefined, [
|
|
{
|
|
path: ["types", at],
|
|
reason: "invalid_format",
|
|
message: "Seed type owner is unavailable.",
|
|
},
|
|
]);
|
|
length += type.name.length + 1;
|
|
if (length > MAX_GHIDRA_SEED_NAME_LENGTH)
|
|
throw capacity(
|
|
"Qualified seed type name exceeds the Ghidra name limit.",
|
|
"The IL2CPP Ghidra seed-name allowance",
|
|
"count",
|
|
);
|
|
parts.unshift(type.name);
|
|
namespace = type.namespace;
|
|
if (length + namespace.length > MAX_GHIDRA_SEED_NAME_LENGTH)
|
|
throw capacity(
|
|
"Qualified seed type name exceeds the Ghidra name limit.",
|
|
"The IL2CPP Ghidra seed-name allowance",
|
|
"count",
|
|
);
|
|
at = type.declaring_type;
|
|
}
|
|
return namespace === "" ? parts.join(".") : `${namespace}.${parts.join(".")}`;
|
|
};
|
|
|
|
/**
|
|
* Name every linked method `Type$$Method`, the Il2CppDumper convention, at
|
|
* `base + rva`. Whitespace becomes `_` because seed names cannot contain it.
|
|
* The first method at an address names its function, since a seed file holds
|
|
* one function seed per address. The other methods at that address are listed
|
|
* in `#` comment lines after the seeds, so a reader of the file or the
|
|
* decompilation can see that the kept name is one of several.
|
|
*/
|
|
export const il2cppGhidraSeeds = (
|
|
report: Il2cppMetadata,
|
|
base: bigint,
|
|
): Il2cppGhidraSeeds => {
|
|
if (report.binary === null)
|
|
throw new AnalysisInputError(OPERATION, undefined, [
|
|
{
|
|
path: ["binary_path"],
|
|
reason: "invalid_value",
|
|
message: "Ghidra seeds need a report linked with binary_path.",
|
|
},
|
|
]);
|
|
const seeded = new Map<bigint, { kept: string; also: string[] }>();
|
|
const lines: string[] = [];
|
|
let folded = 0;
|
|
let withoutAddress = 0,
|
|
noBody = 0,
|
|
unknown = 0,
|
|
bytes = 0;
|
|
const typeNames = new Map<number, string>();
|
|
const reserve = (length: number) => {
|
|
if (length > MAX_GHIDRA_SEED_BYTES - bytes)
|
|
throw capacity(
|
|
"Seed file exceeds Ghidra byte admission.",
|
|
"The IL2CPP Ghidra seed-file allowance",
|
|
"bytes",
|
|
);
|
|
bytes += length;
|
|
};
|
|
const emit = (text: string) => {
|
|
reserve(Buffer.byteLength(text));
|
|
lines.push(text);
|
|
};
|
|
/** Seed `name` at base + rva, or fold it under the name already there. */
|
|
const seed = (rva: string, raw: string, path: (string | number)[]) => {
|
|
const address = base + BigInt(rva);
|
|
if (address < 0n || address > 0xffffffffffffffffn)
|
|
throw new AnalysisInputError(OPERATION, undefined, [
|
|
{
|
|
path: ["seed_base"],
|
|
reason: "invalid_value",
|
|
message: "Rebased seed address exceeds 64 bits.",
|
|
},
|
|
]);
|
|
if (raw.length > MAX_GHIDRA_SEED_NAME_LENGTH)
|
|
throw capacity(
|
|
"Qualified method seed name exceeds Ghidra character admission.",
|
|
"The IL2CPP Ghidra seed-name allowance",
|
|
"count",
|
|
);
|
|
const name = raw.replace(/\s/gu, "_");
|
|
if (name.includes("\0"))
|
|
throw new AnalysisInputError(OPERATION, undefined, [
|
|
{
|
|
path,
|
|
reason: "invalid_format",
|
|
message: "Seed names cannot contain NUL.",
|
|
},
|
|
]);
|
|
const group = seeded.get(address);
|
|
if (group !== undefined) {
|
|
folded++;
|
|
if (group.also.length === 0)
|
|
reserve(
|
|
Buffer.byteLength(
|
|
`# folded 0x${address.toString(16)} ${group.kept} also\n`,
|
|
),
|
|
);
|
|
reserve(Buffer.byteLength(` ${name}`));
|
|
group.also.push(name);
|
|
return;
|
|
}
|
|
seeded.set(address, { kept: name, also: [] });
|
|
if (seeded.size > MAX_GHIDRA_SEED_ENTRIES)
|
|
throw capacity(
|
|
"Seed function count exceeds Ghidra entry admission.",
|
|
"The IL2CPP Ghidra seed-entry allowance",
|
|
"count",
|
|
);
|
|
emit(`0x${address.toString(16)}\tfunction\t${name}\n`);
|
|
};
|
|
report.methods.forEach((method, index) => {
|
|
const rva = report.method_addresses[index]?.rva ?? null;
|
|
if (rva === null) {
|
|
withoutAddress++;
|
|
if (report.method_addresses[index]?.state === "no-body") noBody++;
|
|
else unknown++;
|
|
return;
|
|
}
|
|
let ownerName = typeNames.get(method.declaring_type);
|
|
if (ownerName === undefined) {
|
|
ownerName = typeName(report, method.declaring_type);
|
|
typeNames.set(method.declaring_type, ownerName);
|
|
}
|
|
if (ownerName.length + 2 + method.name.length > MAX_GHIDRA_SEED_NAME_LENGTH)
|
|
throw capacity(
|
|
"Qualified method seed name exceeds Ghidra character admission.",
|
|
"The IL2CPP Ghidra seed-name allowance",
|
|
"count",
|
|
);
|
|
seed(rva, `${ownerName}$$${method.name}`, ["methods", index, "name"]);
|
|
});
|
|
// Generic instances follow the definitions, so an instance sharing a
|
|
// definition's code is folded under the definition's name.
|
|
let genericInstances = 0,
|
|
genericWithoutSeed = 0;
|
|
report.generic_method_instances?.forEach((row, index) => {
|
|
if (row.rva === null || row.name === null) {
|
|
genericWithoutSeed++;
|
|
return;
|
|
}
|
|
genericInstances++;
|
|
seed(row.rva, row.name, ["generic_method_instances", index, "name"]);
|
|
});
|
|
const foldedGroups = [...seeded]
|
|
.filter(([, group]) => group.also.length > 0)
|
|
.map(([address, group]) => ({
|
|
address: `0x${address.toString(16)}`,
|
|
kept: group.kept,
|
|
also: group.also,
|
|
}));
|
|
for (const group of foldedGroups) {
|
|
lines.push(`# folded ${group.address} ${group.kept} also`);
|
|
for (const name of group.also) lines.push(` ${name}`);
|
|
lines.push("\n");
|
|
}
|
|
emit(
|
|
`# metadata_sha256 ${report.artifact.sha256}\n# binary_sha256 ${report.binary.artifact.sha256}\n# link_base ${report.binary.image_base}\n# seed_base 0x${base.toString(16)}\n`,
|
|
);
|
|
return {
|
|
text: lines.join(""),
|
|
functions: seeded.size,
|
|
folded,
|
|
folded_groups: foldedGroups,
|
|
without_address: withoutAddress,
|
|
no_body: noBody,
|
|
unknown,
|
|
generic_instances:
|
|
report.generic_method_instances == null ? null : genericInstances,
|
|
generic_without_seed:
|
|
report.generic_method_instances == null ? null : genericWithoutSeed,
|
|
};
|
|
};
|