mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(il2cpp): validate generic-container extents before index widths
Validate the fixed 16-byte version-39 records before using their count to select the serialized index width. Reject contradictory count and byte extents while preserving valid zero, one and 300-record controls. Validation: 89 focused reader/provider/compiled CLI and stdio MCP tests; check:ci; seven reproduced CLI and SDK-validated MCP cases with unchanged fixture hashes. Version-specific source and independent parser evidence distinguish fixed records from compressed indices. Synthetic metadata only; no real-game or target-execution qualification. Fixes #5751
This commit is contained in:
1 parent
afa5da745e
commit
fa8b87c470
3 files changed
+143
-10
No files matched your search
@@ -253,15 +253,25 @@ const FIXED_WIDTHS: Il2cppMetadataDecoded["index_widths"] = {
|
||||
type_width_source: "fixed-width-version",
|
||||
};
|
||||
|
||||
const GENERIC_CONTAINER_RECORD_BYTES = 16;
|
||||
|
||||
const resolveWidths = (
|
||||
section: (name: SectionName) => Il2cppMetadataSection,
|
||||
): Il2cppMetadataDecoded["index_widths"] => {
|
||||
const genericContainers = section("genericContainers");
|
||||
const genericContainerBytes = recordBytes(genericContainers);
|
||||
if (
|
||||
genericContainerBytes !== null &&
|
||||
genericContainerBytes !== GENERIC_CONTAINER_RECORD_BYTES
|
||||
)
|
||||
throw new Il2cppMetadataFormatFailure(
|
||||
"record-size-mismatch",
|
||||
`section genericContainers records are ${genericContainerBytes} bytes, but each record is ${GENERIC_CONTAINER_RECORD_BYTES}`,
|
||||
);
|
||||
const type_definition = il2cppIndexWidth(
|
||||
headerCount(section("typeDefinitions")),
|
||||
);
|
||||
const generic_container = il2cppIndexWidth(
|
||||
headerCount(section("genericContainers")),
|
||||
);
|
||||
const generic_container = il2cppIndexWidth(headerCount(genericContainers));
|
||||
const parameter = il2cppIndexWidth(headerCount(section("parameters")));
|
||||
// TypeIndex counts native types, so its width is recovered from records.
|
||||
const interfaceOffset = recordBytes(section("interfaceOffsets"));
|
||||
|
||||
@@ -52,6 +52,80 @@ const parseMetadataSummary = (value: unknown) =>
|
||||
parseEvidence(value).normalized_result,
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"validates generic-container extents before returning CLI or MCP metadata",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-il2cpp-container-extents-");
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
for (const count of [0, 1, 300]) {
|
||||
const { bytes } = createIl2cppMetadataFixture({
|
||||
wide: { fillerTypes: 0, genericContainers: count, parameters: 0 },
|
||||
});
|
||||
const path = join(root, `valid-${count}.dat`);
|
||||
await writeFile(path, bytes);
|
||||
const output = await cli.run({
|
||||
arguments: ["inspect-il2cpp-metadata", path, "--json"],
|
||||
});
|
||||
expect(output.exitCode, output.stderr).toBe(0);
|
||||
const report = parseCliMetadata(output.json);
|
||||
expect(report.index_widths.generic_container).toBe(count > 255 ? 2 : 1);
|
||||
const response = await call("inspect_il2cpp_metadata", {
|
||||
path,
|
||||
detail: "complete",
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
expect(parseCliMetadata(response.structuredContent)).toEqual(report);
|
||||
expect(await readFile(path)).toEqual(bytes);
|
||||
}
|
||||
for (const [records, declaredCount, declaredBytes] of [
|
||||
[0, 1, 0],
|
||||
[0, 300, 0],
|
||||
[1, 1, 1],
|
||||
[1, 0, 16],
|
||||
] as const) {
|
||||
const fixture = createIl2cppMetadataFixture({
|
||||
wide: { fillerTypes: 0, genericContainers: records, parameters: 0 },
|
||||
});
|
||||
fixture.bytes.writeInt32LE(
|
||||
declaredCount,
|
||||
fixture.header("genericContainers", "count"),
|
||||
);
|
||||
fixture.bytes.writeInt32LE(
|
||||
declaredBytes,
|
||||
fixture.header("genericContainers", "size"),
|
||||
);
|
||||
const path = join(root, `invalid-${declaredCount}-${declaredBytes}.dat`);
|
||||
await writeFile(path, fixture.bytes);
|
||||
const output = await cli.run({
|
||||
arguments: ["inspect-il2cpp-metadata", path, "--json"],
|
||||
});
|
||||
expect(output.exitCode).toBe(1);
|
||||
const expected = {
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
operation: "inspect_il2cpp_metadata",
|
||||
issues: [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: expect.stringContaining("record-size-mismatch"),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(output.json).toMatchObject(expected);
|
||||
const response = await call("inspect_il2cpp_metadata", {
|
||||
path,
|
||||
detail: "complete",
|
||||
});
|
||||
expect(response.isError).toBe(true);
|
||||
expect(parseMcpToolError(response).error).toMatchObject(expected);
|
||||
expect(await readFile(path)).toEqual(fixture.bytes);
|
||||
}
|
||||
},
|
||||
30000,
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"returns identical complete inventories through CLI and MCP and pages a retained summary after removal",
|
||||
async ({ cli }) => {
|
||||
|
||||
@@ -261,13 +261,6 @@ describe("rejected IL2CPP global-metadata", () => {
|
||||
),
|
||||
"record-size-mismatch",
|
||||
);
|
||||
// 300 generic containers widen GenericContainerIndex to two bytes.
|
||||
rejects(
|
||||
mutated((fixture, bytes) =>
|
||||
bytes.writeInt32LE(300, fixture.header("genericContainers", "count")),
|
||||
),
|
||||
"record-size-mismatch",
|
||||
);
|
||||
rejects(
|
||||
mutated((fixture, bytes) =>
|
||||
bytes.writeInt32LE(2, fixture.header("nestedTypes", "count")),
|
||||
@@ -290,6 +283,62 @@ describe("rejected IL2CPP global-metadata", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("IL2CPP generic-container extents", () => {
|
||||
it("checks generic-container count against its fixed 16-byte records before resolving widths", () => {
|
||||
// A positive count with no record bytes.
|
||||
for (const count of [1, 300])
|
||||
rejects(
|
||||
mutated((fixture, bytes) =>
|
||||
bytes.writeInt32LE(
|
||||
count,
|
||||
fixture.header("genericContainers", "count"),
|
||||
),
|
||||
),
|
||||
"record-size-mismatch",
|
||||
);
|
||||
const oneRecord = createIl2cppMetadataFixture({
|
||||
wide: { fillerTypes: 0, genericContainers: 1, parameters: 0 },
|
||||
});
|
||||
// A positive count with a partial record.
|
||||
rejects(
|
||||
mutated(
|
||||
(fixture, bytes) =>
|
||||
bytes.writeInt32LE(1, fixture.header("genericContainers", "size")),
|
||||
oneRecord,
|
||||
),
|
||||
"record-size-mismatch",
|
||||
);
|
||||
// Bytes without a corresponding record.
|
||||
rejects(
|
||||
mutated(
|
||||
(fixture, bytes) =>
|
||||
bytes.writeInt32LE(0, fixture.header("genericContainers", "count")),
|
||||
oneRecord,
|
||||
),
|
||||
"record-size-mismatch",
|
||||
);
|
||||
});
|
||||
|
||||
it("retains width selection for empty and complete generic-container records", () => {
|
||||
const empty = readIl2cppMetadata(createIl2cppMetadataFixture().bytes);
|
||||
expect(empty.index_widths.generic_container).toBe(1);
|
||||
|
||||
const one = readIl2cppMetadata(
|
||||
createIl2cppMetadataFixture({
|
||||
wide: { fillerTypes: 0, genericContainers: 1, parameters: 0 },
|
||||
}).bytes,
|
||||
);
|
||||
expect(one.index_widths.generic_container).toBe(1);
|
||||
|
||||
const wide = readIl2cppMetadata(
|
||||
createIl2cppMetadataFixture({
|
||||
wide: { fillerTypes: 0, genericContainers: 300, parameters: 0 },
|
||||
}).bytes,
|
||||
);
|
||||
expect(wide.index_widths.generic_container).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rejected IL2CPP metadata references", () => {
|
||||
it("rejects image, type, method and nested-type indices outside their tables", () => {
|
||||
rejects(
|
||||
|
||||
Reference in new issue
Block a user