[GH-ISSUE #7564] [Bug] Metadata-only IL2CPP inspection accepts partial generic-parameter records #4501

Closed
opened 2026-10-31 00:50:20 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 27, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/7564

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

node scripts/rea.mjs inspect-il2cpp-metadata <metadata.dat> --json returns an ordinary successful inventory when the selected metadata declares a genericParameters section that cannot contain whole records. In version 31, changing its size from 64 to 60 bytes still exits 0; in version 39, changing 52 to 51 bytes while retaining count 4 also exits 0. The returned section rows expose those sizes without a limitation identifying the malformed extent or skipped check.

Adding --binary <GameAssembly.bin> to those same files exits 1 with invalid_request, category invalid_input, and an invalid_format issue naming record-size-mismatch. The error identifies 16-byte records for version 31 and 13-byte records for version 39. Both intact files succeed in both modes. This comparison uses generated fixtures and the public compiled CLI, without executing either input.

Reproduction

  1. Use the source baseline below with locked dependencies and its compiled runtime already available. Put Node.js 24.22.1 on PATH, then run this snippet from the repository root. It generates its fixtures outside the checkout and isolates the CLI subprocesses' home and state directories.
node --import tsx --input-type=module <<'JS'
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { spawnSync } from 'node:child_process';
import { createIl2cppMetadataFixture } from './tests/fixtures/il2cpp/metadata.ts';
import { createIl2cppBinaryFixture } from './tests/fixtures/il2cpp/binary.ts';
const root = mkdtempSync(join(tmpdir(), 'generic-extent-'));
const env = { PATH: dirname(process.execPath) + ':/usr/bin:/bin', REA_LOG_LEVEL: 'silent' };
for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME', 'TMPDIR']) {
  env[key] = join(root, key);
  mkdirSync(env[key]);
}
const binary = join(root, 'GameAssembly.bin');
writeFileSync(binary, createIl2cppBinaryFixture());
for (const version of [31, 39]) {
  const fixture = createIl2cppMetadataFixture({ version, genericOwners: true });
  for (const damaged of [false, true]) {
    const bytes = Buffer.from(fixture.bytes);
    if (damaged) bytes.writeInt32LE(version === 39 ? 51 : 60, fixture.header('genericParameters', 'size'));
    const file = join(root, `${version}-${damaged ? 'damaged' : 'intact'}.dat`);
    writeFileSync(file, bytes);
    for (const linked of [false, true]) {
      const args = ['scripts/rea.mjs', 'inspect-il2cpp-metadata', file, '--json'];
      if (linked) args.push('--binary', binary);
      const run = spawnSync(process.execPath, args, { env, encoding: 'utf8', timeout: 30000 });
      if (run.error) throw run.error;
      const result = JSON.parse(run.stdout), n = result.normalized_result;
      console.log(JSON.stringify({ version, damaged, linked, exit: run.status,
        section: n?.sections.find(s => s.name === 'genericParameters'),
        types: n?.types.length, methods: n?.methods.length,
        code: result.code, category: result.category,
        reason: result.details?.issues?.[0]?.reason,
        diagnostic: result.details?.issues?.[0]?.message.split(' is corrupt: ')[1]
      }));
    }
  }
}
JS
  1. Observe the two damaged metadata-only rows exit 0, while their linked counterparts exit 1 with record-size-mismatch. All four intact controls exit 0.

The exact eight-case snippet above ran once. An expanded matrix including versions 24, 27 and 29 and a version-39 generic-container rejection control ran twice with identical outcomes.

Expected behavior

Reject these partial-record extents in metadata-only inspection with invalid_format and record-size-mismatch, preserving successful intact controls. The metadata guide supports these versions without a native binary and explicitly describes section sizes that do not divide into records as malformed input. Supplying a binary should not be necessary to detect this structural contradiction in the selected metadata header.

This expectation concerns the section extent. Generic-parameter names may still be read on demand as documented; the report does not require decoding unused names or native-dependent values during metadata-only inspection.

Impact and scope

P2: a caller receives successful inventory Evidence for a structurally impossible generic-parameter extent, with no diagnostic distinguishing it from the intact input. Metadata-only acceptance was observed for versions 24, 27, 29, 31 and 39. The eight inventory collections were equal to their intact controls, so this verification demonstrates malformed-input admission rather than incorrect decoded names, owners or addresses.

A practical check for this specific defect is available from the returned facts: require genericParameters.size to be divisible by index_widths.generic_container + 12; when count is non-null, require that quotient to equal count. Those conditions identify all five damaged fixtures. Supplying the matching binary also detects the tested version-31 and version-39 cases. Availability of such a binary for other callers is unknown.

Real-game incidence, MCP transport, the published npm package, other mutations, the first affected release and downstream consumer consequences were not tested. No data loss or security-boundary crossing was demonstrated.

REA version

Exact output of node scripts/rea.mjs --version:

6.8.0

Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; Darwin arm64. Repository-locked dependencies and the supplied prebuilt CLI runtime. No Unity installation or external analysis provider was involved.

Target and artifact context

Repository-generated IL2CPP metadata with generic owners and a synthetic thin arm64 Mach-O with matching registrations. Each intact/damaged pair differs in one header byte, with unchanged file length and record data. Shrinking the declared section leaves a permitted gap; the section remains in bounds and does not overlap another section. Version 31 uses 64 intact generic-parameter bytes; version 39 uses 52 bytes for four records. The additional version-24, -27 and -29 cases each change 64 to 60 bytes; version 24 uses the supported 264-byte header layout.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed public CLI outcomes from the reproduction; every successful inventory has four types and five methods:

version  genericParameters.size  count  binary supplied  exit  result
31       64                      null   no               0     inventory
31       64                      null   yes              0     inventory
31       60                      null   no               0     inventory
31       60                      null   yes              1     record-size-mismatch
39       52                      4      no               0     inventory
39       52                      4      yes              0     inventory
39       51                      4      no               0     inventory
39       51                      4      yes              1     record-size-mismatch

The rejected version-31 case returns an issue at ["path"] with reason: "invalid_format"; its diagnostic is:

record-size-mismatch: section genericParameters size 60 is not a whole multiple of its 16-byte records

The version-39 diagnostic is:

record-size-mismatch: section genericParameters size 51 is not a whole multiple of its 13-byte records

Both rejections return code: "invalid_request" and category: "invalid_input". The accepted damaged version-39 result includes:

{"name":"genericParameters","offset":671,"size":51,"count":4}

The expanded runs also accepted size 60 in metadata-only versions 24, 27 and 29, with intact controls succeeding. All eight collections—images, types, methods, fields, properties, events, parameters and string literals—were compared with their intact metadata-only controls and were equal for all five versions. Limitations describe unavailable native observations and derived ownership, without identifying the generic-parameter extent as malformed or unvalidated. No test-suite result or implemented fix is claimed.

Contract sources at the baseline: docs/il2cpp-metadata.md:3-21 documents binary-free inspection and supported versions; :174-182 describes sizes, counts and serialized widths; :514-523 defines malformed-input and record-size diagnostics. The demand-read name behavior is documented at :456.

Reachability and guard sources: src/cli/il2cppCommands.ts:22-71 exposes metadata-only and optional binary inspection. src/il2cpp/Il2cppMetadataProvider.ts:480-485 selects the metadata reader from binary_path presence. src/il2cpp/Il2cppMetadataFile.ts:845-855 validates generic-parameter divisibility and count agreement, but :937-950 constructs that reader only for linking. The prebuilt reader contains the same conditional.

Typed outcome or recovery expected

Return a nonzero CLI exit with invalid_request / invalid_input and an invalid_format issue for the metadata path, identifying record-size-mismatch. Keep intact controls successful. Proposed restoration coverage is to reject these partial generic-parameter extents without requiring --binary; no restoration was tested.

Analysis or suspected cause

Source-derived explanation: createGenericParameterReader owns the generic-parameter record width and validates the declared extent before returning its lazy name accessor. Its construction is conditional on binary linking. Metadata-only decoding therefore bypasses the structural check along with name-accessor setup. The shared bounds/overlap checks and other table readers do not validate this section's record shape.

This is distinct from intentionally deferred value checks. For example, tests/boundary/il2cpp/il2cppMetadataFile.test.ts:308-319 deliberately allows negative generic argument counts in inventory but rejects them during linking. The demonstrated inconsistency is an impossible record extent, not an unused generic name or native-dependent argument value.

Additional context

Related #7532 concerns genericContainers extents on fixed-width versions 24–31. Both reports concern structural extent admission, but their closure signals differ: validating only generic-container records would leave these generic-parameter cases failing. Their container sections remain intact. Version 39 already validates containers and still accepts the damaged generic-parameter extent, demonstrating the independent gap.

Closed #5751 was fixed by commit fa8b87c4705ae5bafd6cbe6992b3e4d06365fd3b, which adds a version-39 generic-container extent check before width selection. That change does not validate generic parameters. A version-39 control with genericContainers.size: 44 and count 3 was independently rejected twice at this baseline, while the generic-parameter case above succeeded. This is not an observed regression of #5751's container fix.

Open and closed issue/PR searches covered generic parameters, record-size mismatch, IL2CPP extents, malformed metadata and validation, including issue comments. Related #6409 and merged #6425 address attribution of deferred generic-name errors, preserving deferred name reads; #7353 addresses imported owner relationships. Other reviewed linking, string and generic-name reports did not describe partial generic-parameter records admitted by metadata-only inspection. No matching report was found.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The public CLI repeatedly returns successful inventory Evidence for malformed generic-parameter extents in all five tested supported metadata versions. The result has no damage limitation, but the projected inventory collections remain equal to intact controls and no severe consequence was demonstrated.
  • Workaround status: available
  • Workaround basis: The returned section sizes/counts and generic-container index width expose the exact divisibility/count conditions needed to detect this bounded defect; these conditions were checked against all five damaged fixtures. A matching binary additionally triggers the reader's existing rejection for the tested 31/39 inputs.
Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7564 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior `node scripts/rea.mjs inspect-il2cpp-metadata <metadata.dat> --json` returns an ordinary successful inventory when the selected metadata declares a `genericParameters` section that cannot contain whole records. In version 31, changing its size from 64 to 60 bytes still exits 0; in version 39, changing 52 to 51 bytes while retaining count 4 also exits 0. The returned section rows expose those sizes without a limitation identifying the malformed extent or skipped check. Adding `--binary <GameAssembly.bin>` to those same files exits 1 with `invalid_request`, category `invalid_input`, and an `invalid_format` issue naming `record-size-mismatch`. The error identifies 16-byte records for version 31 and 13-byte records for version 39. Both intact files succeed in both modes. This comparison uses generated fixtures and the public compiled CLI, without executing either input. ### Reproduction 1. Use the source baseline below with locked dependencies and its compiled runtime already available. Put Node.js 24.22.1 on PATH, then run this snippet from the repository root. It generates its fixtures outside the checkout and isolates the CLI subprocesses' home and state directories. ```sh node --import tsx --input-type=module <<'JS' import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { spawnSync } from 'node:child_process'; import { createIl2cppMetadataFixture } from './tests/fixtures/il2cpp/metadata.ts'; import { createIl2cppBinaryFixture } from './tests/fixtures/il2cpp/binary.ts'; const root = mkdtempSync(join(tmpdir(), 'generic-extent-')); const env = { PATH: dirname(process.execPath) + ':/usr/bin:/bin', REA_LOG_LEVEL: 'silent' }; for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME', 'TMPDIR']) { env[key] = join(root, key); mkdirSync(env[key]); } const binary = join(root, 'GameAssembly.bin'); writeFileSync(binary, createIl2cppBinaryFixture()); for (const version of [31, 39]) { const fixture = createIl2cppMetadataFixture({ version, genericOwners: true }); for (const damaged of [false, true]) { const bytes = Buffer.from(fixture.bytes); if (damaged) bytes.writeInt32LE(version === 39 ? 51 : 60, fixture.header('genericParameters', 'size')); const file = join(root, `${version}-${damaged ? 'damaged' : 'intact'}.dat`); writeFileSync(file, bytes); for (const linked of [false, true]) { const args = ['scripts/rea.mjs', 'inspect-il2cpp-metadata', file, '--json']; if (linked) args.push('--binary', binary); const run = spawnSync(process.execPath, args, { env, encoding: 'utf8', timeout: 30000 }); if (run.error) throw run.error; const result = JSON.parse(run.stdout), n = result.normalized_result; console.log(JSON.stringify({ version, damaged, linked, exit: run.status, section: n?.sections.find(s => s.name === 'genericParameters'), types: n?.types.length, methods: n?.methods.length, code: result.code, category: result.category, reason: result.details?.issues?.[0]?.reason, diagnostic: result.details?.issues?.[0]?.message.split(' is corrupt: ')[1] })); } } } JS ``` 2. Observe the two damaged metadata-only rows exit 0, while their linked counterparts exit 1 with `record-size-mismatch`. All four intact controls exit 0. The exact eight-case snippet above ran once. An expanded matrix including versions 24, 27 and 29 and a version-39 generic-container rejection control ran twice with identical outcomes. ### Expected behavior Reject these partial-record extents in metadata-only inspection with `invalid_format` and `record-size-mismatch`, preserving successful intact controls. The metadata guide supports these versions without a native binary and explicitly describes section sizes that do not divide into records as malformed input. Supplying a binary should not be necessary to detect this structural contradiction in the selected metadata header. This expectation concerns the section extent. Generic-parameter names may still be read on demand as documented; the report does not require decoding unused names or native-dependent values during metadata-only inspection. ### Impact and scope P2: a caller receives successful inventory Evidence for a structurally impossible generic-parameter extent, with no diagnostic distinguishing it from the intact input. Metadata-only acceptance was observed for versions 24, 27, 29, 31 and 39. The eight inventory collections were equal to their intact controls, so this verification demonstrates malformed-input admission rather than incorrect decoded names, owners or addresses. A practical check for this specific defect is available from the returned facts: require `genericParameters.size` to be divisible by `index_widths.generic_container + 12`; when `count` is non-null, require that quotient to equal `count`. Those conditions identify all five damaged fixtures. Supplying the matching binary also detects the tested version-31 and version-39 cases. Availability of such a binary for other callers is unknown. Real-game incidence, MCP transport, the published npm package, other mutations, the first affected release and downstream consumer consequences were not tested. No data loss or security-boundary crossing was demonstrated. ### REA version Exact output of `node scripts/rea.mjs --version`: ```text 6.8.0 ``` Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1; Darwin arm64. Repository-locked dependencies and the supplied prebuilt CLI runtime. No Unity installation or external analysis provider was involved. ### Target and artifact context Repository-generated IL2CPP metadata with generic owners and a synthetic thin arm64 Mach-O with matching registrations. Each intact/damaged pair differs in one header byte, with unchanged file length and record data. Shrinking the declared section leaves a permitted gap; the section remains in bounds and does not overlap another section. Version 31 uses 64 intact generic-parameter bytes; version 39 uses 52 bytes for four records. The additional version-24, -27 and -29 cases each change 64 to 60 bytes; version 24 uses the supported 264-byte header layout. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed public CLI outcomes from the reproduction; every successful inventory has four types and five methods: ```text version genericParameters.size count binary supplied exit result 31 64 null no 0 inventory 31 64 null yes 0 inventory 31 60 null no 0 inventory 31 60 null yes 1 record-size-mismatch 39 52 4 no 0 inventory 39 52 4 yes 0 inventory 39 51 4 no 0 inventory 39 51 4 yes 1 record-size-mismatch ``` The rejected version-31 case returns an issue at `["path"]` with `reason: "invalid_format"`; its diagnostic is: ```text record-size-mismatch: section genericParameters size 60 is not a whole multiple of its 16-byte records ``` The version-39 diagnostic is: ```text record-size-mismatch: section genericParameters size 51 is not a whole multiple of its 13-byte records ``` Both rejections return `code: "invalid_request"` and `category: "invalid_input"`. The accepted damaged version-39 result includes: ```json {"name":"genericParameters","offset":671,"size":51,"count":4} ``` The expanded runs also accepted size 60 in metadata-only versions 24, 27 and 29, with intact controls succeeding. All eight collections—images, types, methods, fields, properties, events, parameters and string literals—were compared with their intact metadata-only controls and were equal for all five versions. Limitations describe unavailable native observations and derived ownership, without identifying the generic-parameter extent as malformed or unvalidated. No test-suite result or implemented fix is claimed. Contract sources at the baseline: `docs/il2cpp-metadata.md:3-21` documents binary-free inspection and supported versions; `:174-182` describes sizes, counts and serialized widths; `:514-523` defines malformed-input and record-size diagnostics. The demand-read name behavior is documented at `:456`. Reachability and guard sources: `src/cli/il2cppCommands.ts:22-71` exposes metadata-only and optional binary inspection. `src/il2cpp/Il2cppMetadataProvider.ts:480-485` selects the metadata reader from `binary_path` presence. `src/il2cpp/Il2cppMetadataFile.ts:845-855` validates generic-parameter divisibility and count agreement, but `:937-950` constructs that reader only for linking. The prebuilt reader contains the same conditional. ### Typed outcome or recovery expected Return a nonzero CLI exit with `invalid_request` / `invalid_input` and an `invalid_format` issue for the metadata path, identifying `record-size-mismatch`. Keep intact controls successful. Proposed restoration coverage is to reject these partial generic-parameter extents without requiring `--binary`; no restoration was tested. ### Analysis or suspected cause Source-derived explanation: `createGenericParameterReader` owns the generic-parameter record width and validates the declared extent before returning its lazy name accessor. Its construction is conditional on binary linking. Metadata-only decoding therefore bypasses the structural check along with name-accessor setup. The shared bounds/overlap checks and other table readers do not validate this section's record shape. This is distinct from intentionally deferred value checks. For example, `tests/boundary/il2cpp/il2cppMetadataFile.test.ts:308-319` deliberately allows negative generic argument counts in inventory but rejects them during linking. The demonstrated inconsistency is an impossible record extent, not an unused generic name or native-dependent argument value. ### Additional context Related [#7532](https://github.com/morluto/rea/issues/7532) concerns `genericContainers` extents on fixed-width versions 24–31. Both reports concern structural extent admission, but their closure signals differ: validating only generic-container records would leave these generic-parameter cases failing. Their container sections remain intact. Version 39 already validates containers and still accepts the damaged generic-parameter extent, demonstrating the independent gap. Closed [#5751](https://github.com/morluto/rea/issues/5751) was fixed by commit `fa8b87c4705ae5bafd6cbe6992b3e4d06365fd3b`, which adds a version-39 generic-container extent check before width selection. That change does not validate generic parameters. A version-39 control with `genericContainers.size: 44` and count 3 was independently rejected twice at this baseline, while the generic-parameter case above succeeded. This is not an observed regression of #5751's container fix. Open and closed issue/PR searches covered generic parameters, record-size mismatch, IL2CPP extents, malformed metadata and validation, including issue comments. Related #6409 and merged #6425 address attribution of deferred generic-name errors, preserving deferred name reads; #7353 addresses imported owner relationships. Other reviewed linking, string and generic-name reports did not describe partial generic-parameter records admitted by metadata-only inspection. No matching report was found. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The public CLI repeatedly returns successful inventory Evidence for malformed generic-parameter extents in all five tested supported metadata versions. The result has no damage limitation, but the projected inventory collections remain equal to intact controls and no severe consequence was demonstrated. - Workaround status: available - Workaround basis: The returned section sizes/counts and generic-container index width expose the exact divisibility/count conditions needed to detect this bounded defect; these conditions were checked against all five damaged fixtures. A matching binary additionally triggers the reader's existing rejection for the tested 31/39 inputs.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4501
No description provided.