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:
morluto committed 2026-10-21 12:55:35 +08:00
1 parent afa5da745e
commit fa8b87c470
3 files changed
+143 -10

No files matched your search

+13 -3
View File
@@ -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(