mirror of
https://github.com/morluto/rea.git
synced 2026-10-23 06:12:29 -05:00
* fix(errors): name fixed allowances in resource-constraint recovery Fixed parser, count, report, input, and capture refusals inherited host-memory, wait-and-retry, or retained-Evidence advice. Producers now attach allowance-specific recovery, and deterministic capacity caps are not retryable. IL2CPP reader occupancy stays retryable capacity. Refs #5556, #5405, #5456, #5459, #5460, #5504, #5505, #5506, #5537, #5628, #5630, #5631, #5632 * fix(il2cpp): keep a closing metadata reader non-retryable close() stops admission before the active inspection finishes. A second inspect in that window was projected as retryable capacity. Closing and closed readers now report capability_unavailable. Occupied readers that remain open stay retryable. * fix(errors): align fixed-cap recovery with retained facts IL2CPP report refusals keep observed source facts instead of denying Evidence. Command-output caps use the capture allowance. Ghidra seed recovery names the limit that was exceeded. * fix(errors): call byte caps a byte allowance WASM representation limits and the IL2CPP Ghidra seed-file cap are byte budgets. Their recovery no longer says the exceeded allowance is a fixed count. * test(ida): keep unresolved procedure lookups non-retryable Main now treats an unchanged invalid request as non-retryable. The IDA lookup tests follow that projection. * fix(errors): qualify capture recovery for live output A fixed capture allowance still rejects the same output. Live captures such as adb logcat can change between requests, so recovery no longer says every unchanged request fails again. * fix(adb): keep a truncated live capture retryable Device output can shrink between calls, so an adb capture that exceeds the allowance stays retryable. Deterministic capture and version-output limits stay non-retryable. * fix(adb): keep truncated mutations non-retryable Only live observations opt into retrying a capture that exceeded the allowance. Push, install, uninstall, and other mutations stay non-retryable. * fix(errors): keep live capture recovery off deterministic limits Artifact-derived capture allowances again say the unchanged request fails. Only volatile adb observations use the live-output sentence. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
239 lines
7.1 KiB
TypeScript
239 lines
7.1 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";
|
|
|
|
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(caught.remediationAction).toContain(
|
|
"The IL2CPP Ghidra seed-file allowance",
|
|
);
|
|
expect(caught.remediationAction).toContain("fixed byte allowance");
|
|
expect(caught.remediationAction).not.toContain("fixed count");
|
|
expect(caught.remediationAction).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(caught.remediationAction).toContain(
|
|
"The IL2CPP Ghidra seed-name allowance",
|
|
);
|
|
expect(caught.remediationAction).toContain("fixed count");
|
|
expect(caught.remediationAction).not.toContain("fixed byte allowance");
|
|
expect(caught.retryable).toBe(false);
|
|
});
|