mirror of
https://github.com/morluto/rea.git
synced 2026-10-23 06:12:29 -05:00
fix(errors): derive fixed-allowance recovery from a typed refusal field
inspect_plist refused a binary plist above its fixed 64 MiB decode
estimate, or nested deeper than 128 levels, with resource "memory" and no
recovery, so the shared presentation told the caller to add worker or
host memory. Neither raises that allowance. The 64 MiB source-size refusal
likewise got the worker file-size advice for evidence replies.
AnalysisResourceConstraintError now carries a typed fixedAllowance
{ name, kind }. fixedAllowanceOptions sets it instead of a prebuilt
string, and the presentation derives the recovery from it, so every
existing fixed-allowance producer moves to the same field. Operation
re-attribution, IDA startup re-attribution and explicit redaction copy the
field through one resourceConstraintRecovery helper; redaction still
applies to the allowance name.
The plist decode, nesting and input refusals use the field. A nesting
refusal now reports maximum_nesting_depth instead of the decode byte
limit it did not exceed.
Fixes #5847
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
246 lines
7.3 KiB
TypeScript
246 lines
7.3 KiB
TypeScript
import {
|
|
AnalysisInputError,
|
|
AnalysisResourceConstraintError,
|
|
} from "../../../src/domain/analysisErrorCore.js";
|
|
import { expect, it } from "vitest";
|
|
import { parseGhidraSeeds } from "../../../src/ghidra/GhidraAnalysisSeeds.js";
|
|
import {
|
|
ghidraDefaultBase,
|
|
il2cppGhidraSeeds,
|
|
} from "../../../src/il2cpp/Il2cppGhidraSeeds.js";
|
|
import {
|
|
IL2CPP_METADATA_MAGIC,
|
|
IL2CPP_METADATA_SECTION_NAMES,
|
|
type Il2cppMetadata,
|
|
} from "../../../src/domain/il2cpp/il2cppMetadata.js";
|
|
import { analysisErrorRemediationAction } from "../../../src/domain/analysisErrorPresentation.js";
|
|
|
|
const type = (
|
|
name: string,
|
|
namespace: string,
|
|
method_start: number,
|
|
method_count: number,
|
|
declaring_type: number,
|
|
) => ({
|
|
name,
|
|
namespace,
|
|
method_start,
|
|
method_count,
|
|
nested_type_start: -1,
|
|
nested_type_count: 0,
|
|
declaring_type,
|
|
});
|
|
|
|
const address = (rva: string): Il2cppMetadata["method_addresses"][number] =>
|
|
rva === "no-body" || rva === "unknown"
|
|
? { state: rva, address: null, rva: null }
|
|
: { state: "code", address: rva, rva };
|
|
|
|
const report: Il2cppMetadata = {
|
|
artifact: {
|
|
path: "/artifacts/global-metadata.dat",
|
|
sha256: "a".repeat(64),
|
|
bytes: 1024,
|
|
},
|
|
magic: IL2CPP_METADATA_MAGIC,
|
|
metadata_version: 31,
|
|
index_widths: {
|
|
type_definition: 4,
|
|
generic_container: 4,
|
|
parameter: 4,
|
|
type: 4,
|
|
type_width_source: "fixed-width-version",
|
|
},
|
|
sections: IL2CPP_METADATA_SECTION_NAMES.map((name) => ({
|
|
name,
|
|
offset: 256,
|
|
size: 0,
|
|
count: 0,
|
|
})),
|
|
images: [{ name: "Assembly-CSharp.dll", type_start: 0, type_count: 3 }],
|
|
types: [
|
|
type("Player", "Game", 0, 2, -1),
|
|
type("Slot", "", 2, 2, 0),
|
|
type("<>c", "", 4, 1, 1),
|
|
],
|
|
methods: [
|
|
{ name: "Update", declaring_type: 0 },
|
|
{ name: "Abstract", declaring_type: 0 },
|
|
{ name: "Add", declaring_type: 1 },
|
|
{ name: "Folded", declaring_type: 1 },
|
|
{ name: "<Run>b__0 x", declaring_type: 2 },
|
|
],
|
|
fields: [],
|
|
properties: [],
|
|
events: [],
|
|
parameters: [],
|
|
string_literals: [],
|
|
binary: {
|
|
artifact: {
|
|
path: "/artifacts/libil2cpp.so",
|
|
sha256: "b".repeat(64),
|
|
bytes: 4096,
|
|
},
|
|
format: "elf",
|
|
architecture: "arm64",
|
|
image_base: "0x0",
|
|
code_gen_modules_field: "0x800",
|
|
code_gen_modules_array: "0x900",
|
|
code_gen_modules: [{ image: 0, address: "0x700", method_pointer_count: 5 }],
|
|
types_field: "0x900",
|
|
types_count: 0,
|
|
},
|
|
method_addresses: [
|
|
address("0x100"),
|
|
address("no-body"),
|
|
address("0x110"),
|
|
address("0x100"),
|
|
address("0x120"),
|
|
],
|
|
return_types: [],
|
|
field_types: [],
|
|
parameter_types: [],
|
|
limitations: [],
|
|
};
|
|
|
|
it("names linked methods Type$$Method with nested types and shifts by the base", () => {
|
|
const seeds = il2cppGhidraSeeds(report, 0x100000n);
|
|
expect(seeds).toEqual({
|
|
text:
|
|
"0x100100\tfunction\tGame.Player$$Update\n" +
|
|
"0x100110\tfunction\tGame.Player.Slot$$Add\n" +
|
|
"0x100120\tfunction\tGame.Player.Slot.<>c$$<Run>b__0_x\n" +
|
|
"# folded 0x100100 Game.Player$$Update also Game.Player.Slot$$Folded\n" +
|
|
`# metadata_sha256 ${report.artifact.sha256}\n# binary_sha256 ${report.binary!.artifact.sha256}\n# link_base 0x0\n# seed_base 0x100000\n`,
|
|
functions: 3,
|
|
folded: 1,
|
|
folded_groups: [
|
|
{
|
|
address: "0x100100",
|
|
kept: "Game.Player$$Update",
|
|
also: ["Game.Player.Slot$$Folded"],
|
|
},
|
|
],
|
|
without_address: 1,
|
|
no_body: 1,
|
|
unknown: 0,
|
|
});
|
|
const parsed = parseGhidraSeeds(seeds.text);
|
|
expect(parsed.ok && parsed.value.length).toBe(3);
|
|
});
|
|
|
|
it("seeds neither a method without a body nor one no module claims", () => {
|
|
const unknown = {
|
|
...report,
|
|
method_addresses: report.method_addresses.map((row, index) =>
|
|
index === 1 ? address("unknown") : row,
|
|
),
|
|
};
|
|
const seeds = il2cppGhidraSeeds(unknown, 0x100000n);
|
|
expect(seeds.text).toBe(il2cppGhidraSeeds(report, 0x100000n).text);
|
|
expect(seeds).toMatchObject({ without_address: 1, no_body: 0, unknown: 1 });
|
|
});
|
|
|
|
const inputFailure = (run: () => unknown): AnalysisInputError => {
|
|
try {
|
|
run();
|
|
} catch (cause) {
|
|
if (cause instanceof AnalysisInputError) return cause;
|
|
throw cause;
|
|
}
|
|
throw new Error("Expected seed input rejection");
|
|
};
|
|
it("refuses an unlinked report", () => {
|
|
const failure = inputFailure(() =>
|
|
il2cppGhidraSeeds({ ...report, binary: null }, 0n),
|
|
);
|
|
expect(failure.issues[0]?.path).toEqual(["binary_path"]);
|
|
});
|
|
|
|
it("defaults to Ghidra's load base: 0x100000 for an ELF linked at 0, else the link base", () => {
|
|
const binary = report.binary!;
|
|
expect(ghidraDefaultBase(binary)).toBe(0x100000n);
|
|
expect(ghidraDefaultBase({ ...binary, image_base: "0x200000" })).toBe(
|
|
0x200000n,
|
|
);
|
|
expect(ghidraDefaultBase({ ...binary, format: "macho" })).toBe(0n);
|
|
expect(
|
|
ghidraDefaultBase({ ...binary, format: "pe", image_base: "0x180000000" }),
|
|
).toBe(0x180000000n);
|
|
});
|
|
|
|
it("bounds folded-name retention against the actual Ghidra file admission", () => {
|
|
const count = 35000;
|
|
const repeated = {
|
|
...report,
|
|
methods: Array.from({ length: count }, () => ({
|
|
name: "M".repeat(1000),
|
|
declaring_type: 0,
|
|
})),
|
|
method_addresses: Array.from({ length: count }, () => address("0x100")),
|
|
};
|
|
let caught: unknown;
|
|
try {
|
|
il2cppGhidraSeeds(repeated, 0n);
|
|
} catch (error) {
|
|
caught = error;
|
|
}
|
|
expect(caught).toBeInstanceOf(AnalysisResourceConstraintError);
|
|
if (!(caught instanceof AnalysisResourceConstraintError))
|
|
throw new Error("Expected seed-file allowance refusal");
|
|
expect(caught.message).toMatch(/byte admission/);
|
|
expect(analysisErrorRemediationAction(caught)).toContain(
|
|
"The IL2CPP Ghidra seed-file allowance",
|
|
);
|
|
expect(analysisErrorRemediationAction(caught)).toContain(
|
|
"fixed byte allowance",
|
|
);
|
|
expect(analysisErrorRemediationAction(caught)).not.toContain("fixed count");
|
|
expect(analysisErrorRemediationAction(caught)).not.toContain(
|
|
"seed-name allowance",
|
|
);
|
|
expect(caught.retryable).toBe(false);
|
|
});
|
|
it("refuses cyclic type ancestry before emitting a seed", () => {
|
|
const cyclic = {
|
|
...report,
|
|
types: report.types.map((type, index) => ({
|
|
...type,
|
|
declaring_type: index === 0 ? 1 : 0,
|
|
})),
|
|
};
|
|
expect(
|
|
inputFailure(() => il2cppGhidraSeeds(cyclic, 0n)).issues[0]?.message,
|
|
).toContain("Cyclic type ancestry");
|
|
});
|
|
it("refuses seed addresses that overflow after rebasing", () => {
|
|
expect(
|
|
inputFailure(() => il2cppGhidraSeeds(report, 0xffffffffffffffffn)).issues[0]
|
|
?.path,
|
|
).toEqual(["seed_base"]);
|
|
});
|
|
it("refuses names the real Ghidra seed parser cannot admit", () => {
|
|
const oversized = {
|
|
...report,
|
|
methods: [{ name: "M".repeat(2001), declaring_type: 0 }],
|
|
};
|
|
let caught: unknown;
|
|
try {
|
|
il2cppGhidraSeeds(oversized, 0n);
|
|
} catch (error) {
|
|
caught = error;
|
|
}
|
|
expect(caught).toBeInstanceOf(AnalysisResourceConstraintError);
|
|
if (!(caught instanceof AnalysisResourceConstraintError))
|
|
throw new Error("Expected seed-name allowance refusal");
|
|
expect(caught.message).toMatch(/character admission/);
|
|
expect(analysisErrorRemediationAction(caught)).toContain(
|
|
"The IL2CPP Ghidra seed-name allowance",
|
|
);
|
|
expect(analysisErrorRemediationAction(caught)).toContain("fixed count");
|
|
expect(analysisErrorRemediationAction(caught)).not.toContain(
|
|
"fixed byte allowance",
|
|
);
|
|
expect(caught.retryable).toBe(false);
|
|
});
|