fix(il2cpp): validate structural record extents (#7861)

This commit is contained in:
morluto authored and GitHub committed 2026-10-29 01:55:47 +08:00
1 parent 5119d08daf
commit 183e2e2d1e
6 files changed
+227 -81

No files matched your search

+44 -54
View File
@@ -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,
+20
View File
@@ -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";
+7 -27
View File
@@ -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,
),