[GH-ISSUE #7532] [Bug] Metadata-only IL2CPP inspection skips generic-container record-size validation for versions 24–31 #4475

Closed
opened 2026-10-31 00:50:08 +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/7532

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 successful complete CLI Evidence for supported version-24, -27, -29 and -31 files whose generic-container table declares 44 bytes. That extent cannot hold whole 16-byte records. The returned section exposes the 44-byte size, but no limitation identifies it as malformed or unvalidated.

For the same damaged version-31 input, adding --binary <GameAssembly.bin> changes the outcome to exit 1, invalid_request, with a field-level invalid_format issue and a record-size-mismatch diagnostic. Intact version-31 metadata succeeds with that same binary. Damaged version 39 is already rejected without a binary.

The demonstrated failure is silent admission of a damaged section. The projected inventory rows remained equal to those from the intact files; this verification did not demonstrate incorrect names, owners or addresses.

Reproduction

  1. Use the source baseline below with its locked dependencies and compiled CLI already available, on Node.js 24.22.1. Run the following from the repository root. It generates synthetic inputs outside the checkout, isolates each REA subprocess's home and XDG directories, and invokes the public CLI without executing target code.
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(), 'rea-gc-size-'));
const env = { PATH: dirname(process.execPath) + ':/usr/bin:/bin', REA_LOG_LEVEL: 'silent' };
for (const [key, dir] of Object.entries({
  HOME: 'home', XDG_CONFIG_HOME: 'config', XDG_DATA_HOME: 'data',
  XDG_CACHE_HOME: 'cache', XDG_STATE_HOME: 'state', TMPDIR: 'temp'
})) {
  env[key] = join(root, dir);
  mkdirSync(env[key]);
}
const binary = join(root, 'GameAssembly.bin');
writeFileSync(binary, createIl2cppBinaryFixture());
for (const version of [24, 27, 29, 31, 39]) {
  const fixture = createIl2cppMetadataFixture({ version, genericOwners: true });
  for (const damaged of [false, true]) {
    const bytes = Buffer.from(fixture.bytes);
    if (damaged) bytes.writeInt32LE(44, fixture.header('genericContainers', 'size'));
    const path = join(root, `${version}-${damaged ? 'damaged' : 'intact'}.dat`);
    writeFileSync(path, bytes);
    for (const linked of version === 31 ? [false, true] : [false]) {
      const args = ['scripts/rea.mjs', 'inspect-il2cpp-metadata', path];
      if (linked) args.push('--binary', binary);
      args.push('--json');
      const run = spawnSync(process.execPath, args, { env, encoding: 'utf8', timeout: 30000 });
      if (run.error) throw run.error;
      const parsed = JSON.parse(run.stdout);
      const n = parsed.normalized_result;
      const label = `${version}-${damaged ? 'damaged' : 'intact'}-${linked ? 'binary' : 'metadata'}`;
      writeFileSync(join(root, `${label}.json`), run.stdout);
      writeFileSync(join(root, `${label}.stderr`), run.stderr);
      const summary = n ? {
        types: n.types.length, methods: n.methods.length,
        genericContainers: n.sections.find(s => s.name === 'genericContainers')
      } : {
        code: parsed.code, category: parsed.category,
        issue: parsed.details?.issues?.[0]
      };
      console.log(JSON.stringify({ label, exit: run.status, ...summary }));
    }
  }
}
JS
  1. Inspect the printed results: each damaged fixed-width version exits 0 in metadata mode. Version 31 with a binary and version 39 in metadata mode reject the damaged extent. Every intact control exits 0.

The initial matrix ran twice with identical outcomes for all twelve cases. The exact snippet above was then replayed once with the same outcomes. Version 24 uses the supported 264-byte header layout; this does not cover earlier 24.x variants.

Expected behavior

Reject a section extent that cannot hold whole generic-container records with the documented malformed-input diagnostic before returning successful inventory Evidence. The IL2CPP metadata guide supports these versions without requiring a native binary and defines record-size-mismatch for a section size that does not divide into records. Its descriptions of binary-dependent observations do not document an exception to this structural record-size guarantee.

Impact and scope

P2: bounded input-validation and evidence-trust defect. A caller receives successful inventory Evidence for the demonstrated damaged section without a limitation identifying the skipped check. All four supported fixed-width versions reproduced it in the synthetic fixtures. The existing version-39 check and the tested version-31 binary mode reject the same kind of damage.

A practical workaround for this exact defect is to check that the returned genericContainers.size is divisible by 16 before accepting a fixed-width metadata result. Supplying a matching binary also caught it in the tested version-31 case, but callers may not have that binary. No data loss, incorrect projected inventory rows, real-game incidence, MCP behavior, published-package behavior, other damaged sections or first affected release was established.

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 baseline's prebuilt runtime. No Unity installation or disassembler was used.

Target and artifact context

Synthetic IL2CPP metadata for versions 24, 27, 29, 31 and 39. Each intact input has three 16-byte generic-container records. The damaged variant changes only the header's declared section size from 48 to 44; each fixture pair differs in one header byte, with unchanged file length. Section extents remain in bounds and do not overlap; shrinking the declared size leaves a permitted gap. The binary control is a synthetic thin arm64 Mach-O containing matching IL2CPP registrations.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed outcomes from the public CLI; each successful inventory contains four types and five methods:

version   genericContainers.size   binary supplied   exit   outcome
24        48                      no                 0     inventory
24        44                      no                 0     inventory
27        48                      no                 0     inventory
27        44                      no                 0     inventory
29        48                      no                 0     inventory
29        44                      no                 0     inventory
31        48                      no                 0     inventory
31        44                      no                 0     inventory
31        48                      yes                0     inventory
31        44                      yes                1     record-size-mismatch
39        48                      no                 0     inventory
39        44                      no                 1     record-size-mismatch

The successful damaged version-31 result contains:

{"name":"genericContainers","offset":684,"size":44,"count":null}

Both rejected cases return code: "invalid_request", category: "invalid_input", and an issue at ["path"] with reason: "invalid_format". The version-31 linked diagnostic ends with section genericContainers size 44 is not a whole multiple of its 16-byte records; the version-39 diagnostic ends with section genericContainers size 44 is not a whole multiple of its 3 records.

All eight projected collections—images, types, methods, fields, properties, events, parameters and string literals—were compared between each accepted damaged fixed-width input and its intact control and were equal. Successful limitations discuss unavailable binary observations and owner derivation, without identifying the damaged generic-container extent.

Contract sources at the baseline: docs/il2cpp-metadata.md:3-21 supports binary-free inspection and the named versions; :174-182 describes fixed-width indices and size-derived counts; :514-523 defines the damaged-metadata and record-size failure contract. The binary-dependent observations are described at :208 onward and :337-338.

An additional attempt to run npm run test:local -- tests/boundary/il2cpp/il2cppMetadataFile.test.ts was refused by the repository dispatcher before executing tests: Use test:focused for built test projects. No passing test-suite result is claimed; the evidence above comes from direct CLI runs.

Typed outcome or recovery expected

Exit nonzero with invalid_request / invalid_input, a field-level invalid_format issue for the metadata path and the record-size-mismatch diagnostic. Keep intact controls successful. No fix or restoration was tested.

Analysis or suspected cause

Source-derived explanation: src/il2cpp/Il2cppMetadataFile.ts:259-304 validates generic-container record extent while resolving counted widths. The branch at :906-907 skips this for fixed-width layouts. The fixed-width record-count checks at :313-327 cover other tables. The generic-container division check at :524-527 depends on linking facts, which are only created in linking mode at :933-950. src/il2cpp/Il2cppMetadataProvider.ts:480-485 selects the reader from the presence of binary_path; src/cli/il2cppCommands.ts:22-71 exposes that choice through the supported CLI.

This report does not demand identical validation of every unused metadata value in both modes. For example, tests/boundary/il2cpp/il2cppMetadataFile.test.ts:308-319 deliberately admits negative generic argument counts during inventory and rejects them during linking. The reported gap concerns structural record extents specifically.

Additional context

Open and closed issue searches covered genericContainers, record-size-mismatch, generic-container, and IL2CPP record/malformed/validation terms; all-state PR searches covered IL2CPP, generic containers and the related issue number.

Closed #5751 concerns version-39 count/extent contradictions. Its closure commit fa8b87c4705ae5bafd6cbe6992b3e4d06365fd3b explicitly adds validation before version-39 width selection. The current reproduction confirms that version-39 rejection works. The fixed-width metadata-only gap has a different admission path and requires additional restoration coverage; this is not an observed regression of that version-39 fix.

Related issues #6871, #7353, #6409, #6687 and #6610 concern registration discovery, imported ownership, error attribution, rendered generic names and duplicate method tokens. Related PRs #7261, #5785 and #6425 concern generic linking and error attribution. None reports the fixed-width record-size admission behavior reproduced here.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7532 ### 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 successful complete CLI Evidence for supported version-24, -27, -29 and -31 files whose generic-container table declares 44 bytes. That extent cannot hold whole 16-byte records. The returned section exposes the 44-byte size, but no limitation identifies it as malformed or unvalidated. For the same damaged version-31 input, adding `--binary <GameAssembly.bin>` changes the outcome to exit 1, `invalid_request`, with a field-level `invalid_format` issue and a `record-size-mismatch` diagnostic. Intact version-31 metadata succeeds with that same binary. Damaged version 39 is already rejected without a binary. The demonstrated failure is silent admission of a damaged section. The projected inventory rows remained equal to those from the intact files; this verification did not demonstrate incorrect names, owners or addresses. ### Reproduction 1. Use the source baseline below with its locked dependencies and compiled CLI already available, on Node.js 24.22.1. Run the following from the repository root. It generates synthetic inputs outside the checkout, isolates each REA subprocess's home and XDG directories, and invokes the public CLI without executing target code. ```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(), 'rea-gc-size-')); const env = { PATH: dirname(process.execPath) + ':/usr/bin:/bin', REA_LOG_LEVEL: 'silent' }; for (const [key, dir] of Object.entries({ HOME: 'home', XDG_CONFIG_HOME: 'config', XDG_DATA_HOME: 'data', XDG_CACHE_HOME: 'cache', XDG_STATE_HOME: 'state', TMPDIR: 'temp' })) { env[key] = join(root, dir); mkdirSync(env[key]); } const binary = join(root, 'GameAssembly.bin'); writeFileSync(binary, createIl2cppBinaryFixture()); for (const version of [24, 27, 29, 31, 39]) { const fixture = createIl2cppMetadataFixture({ version, genericOwners: true }); for (const damaged of [false, true]) { const bytes = Buffer.from(fixture.bytes); if (damaged) bytes.writeInt32LE(44, fixture.header('genericContainers', 'size')); const path = join(root, `${version}-${damaged ? 'damaged' : 'intact'}.dat`); writeFileSync(path, bytes); for (const linked of version === 31 ? [false, true] : [false]) { const args = ['scripts/rea.mjs', 'inspect-il2cpp-metadata', path]; if (linked) args.push('--binary', binary); args.push('--json'); const run = spawnSync(process.execPath, args, { env, encoding: 'utf8', timeout: 30000 }); if (run.error) throw run.error; const parsed = JSON.parse(run.stdout); const n = parsed.normalized_result; const label = `${version}-${damaged ? 'damaged' : 'intact'}-${linked ? 'binary' : 'metadata'}`; writeFileSync(join(root, `${label}.json`), run.stdout); writeFileSync(join(root, `${label}.stderr`), run.stderr); const summary = n ? { types: n.types.length, methods: n.methods.length, genericContainers: n.sections.find(s => s.name === 'genericContainers') } : { code: parsed.code, category: parsed.category, issue: parsed.details?.issues?.[0] }; console.log(JSON.stringify({ label, exit: run.status, ...summary })); } } } JS ``` 2. Inspect the printed results: each damaged fixed-width version exits 0 in metadata mode. Version 31 with a binary and version 39 in metadata mode reject the damaged extent. Every intact control exits 0. The initial matrix ran twice with identical outcomes for all twelve cases. The exact snippet above was then replayed once with the same outcomes. Version 24 uses the supported 264-byte header layout; this does not cover earlier 24.x variants. ### Expected behavior Reject a section extent that cannot hold whole generic-container records with the documented malformed-input diagnostic before returning successful inventory Evidence. The IL2CPP metadata guide supports these versions without requiring a native binary and defines `record-size-mismatch` for a section size that does not divide into records. Its descriptions of binary-dependent observations do not document an exception to this structural record-size guarantee. ### Impact and scope P2: bounded input-validation and evidence-trust defect. A caller receives successful inventory Evidence for the demonstrated damaged section without a limitation identifying the skipped check. All four supported fixed-width versions reproduced it in the synthetic fixtures. The existing version-39 check and the tested version-31 binary mode reject the same kind of damage. A practical workaround for this exact defect is to check that the returned `genericContainers.size` is divisible by 16 before accepting a fixed-width metadata result. Supplying a matching binary also caught it in the tested version-31 case, but callers may not have that binary. No data loss, incorrect projected inventory rows, real-game incidence, MCP behavior, published-package behavior, other damaged sections or first affected release was established. ### 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 baseline's prebuilt runtime. No Unity installation or disassembler was used. ### Target and artifact context Synthetic IL2CPP metadata for versions 24, 27, 29, 31 and 39. Each intact input has three 16-byte generic-container records. The damaged variant changes only the header's declared section size from 48 to 44; each fixture pair differs in one header byte, with unchanged file length. Section extents remain in bounds and do not overlap; shrinking the declared size leaves a permitted gap. The binary control is a synthetic thin arm64 Mach-O containing matching IL2CPP registrations. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed outcomes from the public CLI; each successful inventory contains four types and five methods: ```text version genericContainers.size binary supplied exit outcome 24 48 no 0 inventory 24 44 no 0 inventory 27 48 no 0 inventory 27 44 no 0 inventory 29 48 no 0 inventory 29 44 no 0 inventory 31 48 no 0 inventory 31 44 no 0 inventory 31 48 yes 0 inventory 31 44 yes 1 record-size-mismatch 39 48 no 0 inventory 39 44 no 1 record-size-mismatch ``` The successful damaged version-31 result contains: ```json {"name":"genericContainers","offset":684,"size":44,"count":null} ``` Both rejected cases return `code: "invalid_request"`, `category: "invalid_input"`, and an issue at `["path"]` with `reason: "invalid_format"`. The version-31 linked diagnostic ends with `section genericContainers size 44 is not a whole multiple of its 16-byte records`; the version-39 diagnostic ends with `section genericContainers size 44 is not a whole multiple of its 3 records`. All eight projected collections—images, types, methods, fields, properties, events, parameters and string literals—were compared between each accepted damaged fixed-width input and its intact control and were equal. Successful limitations discuss unavailable binary observations and owner derivation, without identifying the damaged generic-container extent. Contract sources at the baseline: `docs/il2cpp-metadata.md:3-21` supports binary-free inspection and the named versions; `:174-182` describes fixed-width indices and size-derived counts; `:514-523` defines the damaged-metadata and record-size failure contract. The binary-dependent observations are described at `:208` onward and `:337-338`. An additional attempt to run `npm run test:local -- tests/boundary/il2cpp/il2cppMetadataFile.test.ts` was refused by the repository dispatcher before executing tests: `Use test:focused for built test projects`. No passing test-suite result is claimed; the evidence above comes from direct CLI runs. ### Typed outcome or recovery expected Exit nonzero with `invalid_request` / `invalid_input`, a field-level `invalid_format` issue for the metadata path and the `record-size-mismatch` diagnostic. Keep intact controls successful. No fix or restoration was tested. ### Analysis or suspected cause Source-derived explanation: `src/il2cpp/Il2cppMetadataFile.ts:259-304` validates generic-container record extent while resolving counted widths. The branch at `:906-907` skips this for fixed-width layouts. The fixed-width record-count checks at `:313-327` cover other tables. The generic-container division check at `:524-527` depends on linking facts, which are only created in linking mode at `:933-950`. `src/il2cpp/Il2cppMetadataProvider.ts:480-485` selects the reader from the presence of `binary_path`; `src/cli/il2cppCommands.ts:22-71` exposes that choice through the supported CLI. This report does not demand identical validation of every unused metadata value in both modes. For example, `tests/boundary/il2cpp/il2cppMetadataFile.test.ts:308-319` deliberately admits negative generic argument counts during inventory and rejects them during linking. The reported gap concerns structural record extents specifically. ### Additional context Open and closed issue searches covered `genericContainers`, `record-size-mismatch`, generic-container, and IL2CPP record/malformed/validation terms; all-state PR searches covered IL2CPP, generic containers and the related issue number. Closed [#5751](https://github.com/morluto/rea/issues/5751) concerns version-39 count/extent contradictions. Its closure commit `fa8b87c4705ae5bafd6cbe6992b3e4d06365fd3b` explicitly adds validation before version-39 width selection. The current reproduction confirms that version-39 rejection works. The fixed-width metadata-only gap has a different admission path and requires additional restoration coverage; this is not an observed regression of that version-39 fix. Related issues #6871, #7353, #6409, #6687 and #6610 concern registration discovery, imported ownership, error attribution, rendered generic names and duplicate method tokens. Related PRs #7261, #5785 and #6425 concern generic linking and error attribution. None reports the fixed-width record-size admission behavior reproduced here.
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#4475
No description provided.