mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(il2cpp): validate structural record extents (#7861)
This commit is contained in:
1 parent
5119d08daf
commit
183e2e2d1e
6 files changed
+227
-81
No files matched your search
@@ -528,6 +528,12 @@ message names one reason:
|
||||
| `image-range-overlap` | Two images claim the same type definitions. |
|
||||
| `invalid-string` | A name index is outside the strings section, its string is unterminated, or it is not valid UTF-8. |
|
||||
|
||||
Record extents are checked for the decoded images, types, methods, fields,
|
||||
properties, events, parameters, string literals, nested types, generic
|
||||
containers, generic parameters and interface offsets. Other header sections
|
||||
are checked for file bounds and overlap; their internal record layouts are not
|
||||
decoded or validated by this inventory.
|
||||
|
||||
A version field that disagrees with the file's layout, for example a version
|
||||
39 file relabeled as 29, usually surfaces as `unexpected-header-size` or
|
||||
`record-size-mismatch`.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
checkRange,
|
||||
checkedSectionCount,
|
||||
readIndex,
|
||||
Il2cppMetadataFormatFailure,
|
||||
} from "./Il2cppMetadataFormat.js";
|
||||
@@ -236,16 +237,6 @@ const recordBytes = (section: Il2cppMetadataSection): number | null => {
|
||||
return section.size / count;
|
||||
};
|
||||
|
||||
/** Entry count of a section whose header records only its size. */
|
||||
const entriesIn = (section: Il2cppMetadataSection, entryBytes: number) => {
|
||||
if (section.size % entryBytes !== 0)
|
||||
throw new Il2cppMetadataFormatFailure(
|
||||
"record-size-mismatch",
|
||||
`section ${section.name} size ${section.size} is not a whole multiple of its ${entryBytes}-byte records`,
|
||||
);
|
||||
return section.size / entryBytes;
|
||||
};
|
||||
|
||||
const FIXED_WIDTHS: Il2cppMetadataDecoded["index_widths"] = {
|
||||
type_definition: 4,
|
||||
generic_container: 4,
|
||||
@@ -260,15 +251,6 @@ 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")),
|
||||
);
|
||||
@@ -308,36 +290,54 @@ const recordCounts = (
|
||||
version: Il2cppMetadataVersion,
|
||||
section: (name: SectionName) => Il2cppMetadataSection,
|
||||
expected: Readonly<Record<RecordSection, number>>,
|
||||
): Readonly<Record<RecordSection | "nestedTypes", number>> => {
|
||||
widths: Il2cppMetadataDecoded["index_widths"],
|
||||
): Readonly<
|
||||
Record<
|
||||
RecordSection | "nestedTypes" | "genericContainers" | "genericParameters",
|
||||
number
|
||||
>
|
||||
> => {
|
||||
const genericContainers = checkedSectionCount(
|
||||
section("genericContainers"),
|
||||
GENERIC_CONTAINER_RECORD_BYTES,
|
||||
);
|
||||
const genericParameters = checkedSectionCount(
|
||||
section("genericParameters"),
|
||||
widths.generic_container + 12,
|
||||
);
|
||||
const names = ["typeDefinitions", "images", "methods"] as const;
|
||||
if (VERSION_LAYOUTS[version].indexing === "fixed") {
|
||||
checkedSectionCount(section("interfaceOffsets"), widths.type + 4);
|
||||
const sized = Object.fromEntries(
|
||||
names.map((name) => [name, entriesIn(section(name), expected[name])]),
|
||||
names.map((name) => [
|
||||
name,
|
||||
checkedSectionCount(section(name), expected[name]),
|
||||
]),
|
||||
) as Record<RecordSection, number>;
|
||||
return {
|
||||
...sized,
|
||||
nestedTypes: entriesIn(section("nestedTypes"), NESTED_TYPE_ENTRY_BYTES),
|
||||
nestedTypes: checkedSectionCount(
|
||||
section("nestedTypes"),
|
||||
NESTED_TYPE_ENTRY_BYTES,
|
||||
),
|
||||
genericContainers,
|
||||
genericParameters,
|
||||
};
|
||||
}
|
||||
for (const name of names) {
|
||||
const actual = recordBytes(section(name));
|
||||
if (actual !== null && actual !== expected[name])
|
||||
throw new Il2cppMetadataFormatFailure(
|
||||
"record-size-mismatch",
|
||||
`section ${name} records are ${actual} bytes, but the index widths imply ${expected[name]}`,
|
||||
);
|
||||
}
|
||||
const nestedTypes = section("nestedTypes");
|
||||
if (nestedTypes.size !== headerCount(nestedTypes) * NESTED_TYPE_ENTRY_BYTES)
|
||||
throw new Il2cppMetadataFormatFailure(
|
||||
"record-size-mismatch",
|
||||
`section nestedTypes size ${nestedTypes.size} does not hold ${String(nestedTypes.count)} 4-byte entries`,
|
||||
);
|
||||
const counted = Object.fromEntries(
|
||||
names.map((name) => [
|
||||
name,
|
||||
checkedSectionCount(section(name), expected[name]),
|
||||
]),
|
||||
) as Record<RecordSection, number>;
|
||||
return {
|
||||
typeDefinitions: headerCount(section("typeDefinitions")),
|
||||
images: headerCount(section("images")),
|
||||
methods: headerCount(section("methods")),
|
||||
nestedTypes: headerCount(nestedTypes),
|
||||
...counted,
|
||||
nestedTypes: checkedSectionCount(
|
||||
section("nestedTypes"),
|
||||
NESTED_TYPE_ENTRY_BYTES,
|
||||
),
|
||||
genericContainers,
|
||||
genericParameters,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -522,9 +522,7 @@ function* readTypes(tables: MetadataTables): Generator<
|
||||
events: [] as MetadataOwnerRange[],
|
||||
};
|
||||
const genericContainerCount =
|
||||
tables.linkFacts === undefined
|
||||
? 0
|
||||
: entriesIn(section("genericContainers"), GENERIC_CONTAINER_RECORD_BYTES);
|
||||
tables.linkFacts === undefined ? 0 : tables.counts.genericContainers;
|
||||
for (let index = 0; index < counts.typeDefinitions; index++) {
|
||||
if (index % 256 === 0) yield;
|
||||
const at = typeDefinitions.offset + index * sizes.typeDefinitions;
|
||||
@@ -810,11 +808,8 @@ const readGenericContainer = (tables: MetadataTables, index: number) => {
|
||||
/** Type and method container indices and container records, read on demand. */
|
||||
const createGenericContainerReaders = (tables: MetadataTables) => {
|
||||
const typeCount = tables.counts.typeDefinitions;
|
||||
const containers = tables.section("genericContainers");
|
||||
const containerCount =
|
||||
tables.indexing === "counted"
|
||||
? 0
|
||||
: Math.floor(containers.size / GENERIC_CONTAINER_RECORD_BYTES);
|
||||
tables.indexing === "counted" ? 0 : tables.counts.genericContainers;
|
||||
const inRange = (index: number, count: number) =>
|
||||
Number.isInteger(index) && index >= 0 && index < count;
|
||||
const methods = tables.section("methods"),
|
||||
@@ -845,12 +840,7 @@ const createGenericContainerReaders = (tables: MetadataTables) => {
|
||||
const createGenericParameterReader = (tables: MetadataTables) => {
|
||||
const parameters = tables.section("genericParameters"),
|
||||
width = tables.widths.generic_container + 12;
|
||||
const count = entriesIn(parameters, width);
|
||||
if (parameters.count !== null && count !== parameters.count)
|
||||
throw new Il2cppMetadataFormatFailure(
|
||||
"record-size-mismatch",
|
||||
"generic parameter count disagrees with its extent",
|
||||
);
|
||||
const count = tables.counts.genericParameters;
|
||||
return (index: number): string | undefined => {
|
||||
if (!Number.isInteger(index) || index < 0 || index >= count)
|
||||
return undefined;
|
||||
@@ -911,7 +901,7 @@ function* readMetadataCoreSteps(
|
||||
section,
|
||||
widths,
|
||||
recordBytes: expected,
|
||||
counts: recordCounts(version, section, expected),
|
||||
counts: recordCounts(version, section, expected, widths),
|
||||
typeDefinitionTypeIndices: layout.typeDefinitionTypeIndices,
|
||||
indexing: layout.indexing,
|
||||
methodReturnParameterToken: layout.methodReturnParameterToken,
|
||||
|
||||
@@ -38,6 +38,25 @@ export class Il2cppMetadataFormatFailure extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** Validate a section against a known serialized record width once. */
|
||||
export const checkedSectionCount = (
|
||||
section: Il2cppMetadataSection,
|
||||
recordBytes: number,
|
||||
): number => {
|
||||
if (section.size % recordBytes !== 0)
|
||||
throw new Il2cppMetadataFormatFailure(
|
||||
"record-size-mismatch",
|
||||
`section ${section.name} size ${section.size} is not a whole multiple of its ${recordBytes}-byte records`,
|
||||
);
|
||||
const count = section.size / recordBytes;
|
||||
if (section.count !== null && count !== section.count)
|
||||
throw new Il2cppMetadataFormatFailure(
|
||||
"record-size-mismatch",
|
||||
`section ${section.name} size ${section.size} does not hold ${section.count} ${recordBytes}-byte records`,
|
||||
);
|
||||
return count;
|
||||
};
|
||||
|
||||
export const checkRange = (
|
||||
label: string,
|
||||
start: number,
|
||||
@@ -64,3 +83,4 @@ export const readIndex = (bytes: Buffer, offset: number, width: 1 | 2 | 4) => {
|
||||
}
|
||||
return bytes.readInt32LE(offset);
|
||||
};
|
||||
import type { Il2cppMetadataSection } from "../domain/il2cpp/il2cppMetadata.js";
|
||||
@@ -1,9 +1,7 @@
|
||||
import type {
|
||||
Il2cppMetadata,
|
||||
Il2cppMetadataSection,
|
||||
} from "../domain/il2cpp/il2cppMetadata.js";
|
||||
import type { Il2cppMetadata } from "../domain/il2cpp/il2cppMetadata.js";
|
||||
import {
|
||||
checkRange,
|
||||
checkedSectionCount,
|
||||
readIndex,
|
||||
Il2cppMetadataFormatFailure,
|
||||
} from "./Il2cppMetadataFormat.js";
|
||||
@@ -24,21 +22,6 @@ export interface MetadataMemberOwners {
|
||||
readonly parameters: readonly MetadataOwnerRange[];
|
||||
}
|
||||
|
||||
const entries = (
|
||||
indexing: MetadataTables["indexing"],
|
||||
section: Il2cppMetadataSection,
|
||||
recordBytes: number,
|
||||
) => {
|
||||
const count =
|
||||
indexing === "counted" ? (section.count ?? 0) : section.size / recordBytes;
|
||||
if (!Number.isInteger(count) || section.size !== count * recordBytes)
|
||||
throw new Il2cppMetadataFormatFailure(
|
||||
"record-size-mismatch",
|
||||
`section ${section.name} size ${section.size} does not hold whole ${recordBytes}-byte records`,
|
||||
);
|
||||
return count;
|
||||
};
|
||||
|
||||
/** Own member record widths and validate all counts before any row allocation. */
|
||||
export const readIl2cppMemberTables = ({
|
||||
widths,
|
||||
@@ -52,8 +35,7 @@ export const readIl2cppMemberTables = ({
|
||||
parameters: 8 + widths.type,
|
||||
stringLiterals: indexing === "counted" ? 4 : 8,
|
||||
};
|
||||
const literalRecords = entries(
|
||||
indexing,
|
||||
const literalRecords = checkedSectionCount(
|
||||
section("stringLiterals"),
|
||||
recordBytes.stringLiterals,
|
||||
);
|
||||
@@ -61,15 +43,13 @@ export const readIl2cppMemberTables = ({
|
||||
recordBytes,
|
||||
literalRecords,
|
||||
counts: {
|
||||
fields: entries(indexing, section("fields"), recordBytes.fields),
|
||||
properties: entries(
|
||||
indexing,
|
||||
fields: checkedSectionCount(section("fields"), recordBytes.fields),
|
||||
properties: checkedSectionCount(
|
||||
section("properties"),
|
||||
recordBytes.properties,
|
||||
),
|
||||
events: entries(indexing, section("events"), recordBytes.events),
|
||||
parameters: entries(
|
||||
indexing,
|
||||
events: checkedSectionCount(section("events"), recordBytes.events),
|
||||
parameters: checkedSectionCount(
|
||||
section("parameters"),
|
||||
recordBytes.parameters,
|
||||
),
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
import { createIl2cppMetadataFixture } from "../fixtures/il2cpp/metadata.js";
|
||||
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
|
||||
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { cliTest, runFileCli } from "../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"rejects partial IL2CPP table extents through CLI and MCP without a binary",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-il2cpp-extent-");
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
for (const [version, name] of [
|
||||
[31, "genericContainers"],
|
||||
[31, "genericParameters"],
|
||||
[31, "interfaceOffsets"],
|
||||
[39, "genericParameters"],
|
||||
] as const) {
|
||||
const fixture = createIl2cppMetadataFixture({
|
||||
version,
|
||||
genericOwners: true,
|
||||
});
|
||||
const path = join(root, `${version}-${name}.dat`);
|
||||
await writeFile(path, fixture.bytes);
|
||||
const intact = await runFileCli(cli, {
|
||||
arguments: ["inspect-il2cpp-metadata", path, "--json"],
|
||||
});
|
||||
expect(intact.exitCode, intact.stderr).toBe(0);
|
||||
|
||||
const damaged = Buffer.from(fixture.bytes);
|
||||
damaged.writeInt32LE(
|
||||
fixture.section(name).size - 1,
|
||||
fixture.header(name, "size"),
|
||||
);
|
||||
await writeFile(path, damaged);
|
||||
const invalid = await runFileCli(cli, {
|
||||
arguments: ["inspect-il2cpp-metadata", path, "--json"],
|
||||
});
|
||||
const expected = {
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
issues: [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: expect.stringContaining("record-size-mismatch"),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(invalid.exitCode).toBe(1);
|
||||
expect(invalid.json).toMatchObject(expected);
|
||||
const result = await call("inspect_il2cpp_metadata", { path });
|
||||
expect(result.isError).toBe(true);
|
||||
expect(parseMcpToolError(result).error).toMatchObject(expected);
|
||||
}
|
||||
},
|
||||
30000,
|
||||
);
|
||||
@@ -0,0 +1,89 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Il2cppMetadataFormatFailure } from "../../../src/il2cpp/Il2cppMetadataFormat.js";
|
||||
import { createIl2cppMetadataFixture } from "../../fixtures/il2cpp/metadata.js";
|
||||
import {
|
||||
readIl2cppMetadata,
|
||||
readIl2cppMetadataForLinking,
|
||||
} from "../../fixtures/il2cpp/readers.js";
|
||||
|
||||
describe("IL2CPP structural section extents", () => {
|
||||
it.each([24, 27, 29, 31, 39] as const)(
|
||||
"validates generic records before inventory or linking for version %i",
|
||||
(version) => {
|
||||
const fixture = createIl2cppMetadataFixture({
|
||||
version,
|
||||
genericOwners: true,
|
||||
});
|
||||
for (const reader of [readIl2cppMetadata, readIl2cppMetadataForLinking]) {
|
||||
expect(() => reader(fixture.bytes)).not.toThrow();
|
||||
for (const name of [
|
||||
"genericContainers",
|
||||
"genericParameters",
|
||||
] as const) {
|
||||
const damaged = Buffer.from(fixture.bytes);
|
||||
damaged.writeInt32LE(
|
||||
fixture.section(name).size - 1,
|
||||
fixture.header(name, "size"),
|
||||
);
|
||||
expect(() => reader(damaged)).toThrow(Il2cppMetadataFormatFailure);
|
||||
expect(() => reader(damaged)).toThrow(/record-size-mismatch/);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each([24, 27, 29, 31] as const)(
|
||||
"rejects partial fixed-width interface offsets in version %i",
|
||||
(version) => {
|
||||
const fixture = createIl2cppMetadataFixture({
|
||||
version,
|
||||
genericOwners: true,
|
||||
});
|
||||
const damaged = Buffer.from(fixture.bytes);
|
||||
damaged.writeInt32LE(6, fixture.header("interfaceOffsets", "size"));
|
||||
for (const reader of [readIl2cppMetadata, readIl2cppMetadataForLinking]) {
|
||||
expect(() => reader(fixture.bytes)).not.toThrow();
|
||||
expect(() => reader(damaged)).toThrow(/record-size-mismatch/);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects a counted generic-parameter extent with the wrong declared count", () => {
|
||||
const fixture = createIl2cppMetadataFixture({
|
||||
version: 39,
|
||||
genericOwners: true,
|
||||
});
|
||||
const damaged = Buffer.from(fixture.bytes);
|
||||
damaged.writeInt32LE(3, fixture.header("genericParameters", "count"));
|
||||
expect(() => readIl2cppMetadata(damaged)).toThrow(/record-size-mismatch/);
|
||||
});
|
||||
|
||||
it.each([31, 39] as const)(
|
||||
"uses the same extent validation for decoded member sections in version %i",
|
||||
(version) => {
|
||||
const fixture = createIl2cppMetadataFixture({
|
||||
version,
|
||||
genericOwners: true,
|
||||
propertiesAndEvents: true,
|
||||
});
|
||||
for (const name of [
|
||||
"fields",
|
||||
"properties",
|
||||
"events",
|
||||
"parameters",
|
||||
"stringLiterals",
|
||||
"nestedTypes",
|
||||
] as const) {
|
||||
expect(fixture.section(name).size, name).toBeGreaterThan(0);
|
||||
const damaged = Buffer.from(fixture.bytes);
|
||||
damaged.writeInt32LE(
|
||||
fixture.section(name).size - 1,
|
||||
fixture.header(name, "size"),
|
||||
);
|
||||
expect(() => readIl2cppMetadata(damaged), name).toThrow(
|
||||
/record-size-mismatch/,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user