[GH-ISSUE #7563] [Bug] IL2CPP inspection accepts partial interface-offset records in versions 24–31 #4502

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/7563

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 Evidence when a supported fixed-width metadata file declares six bytes for its interface-offset table. Each record requires eight bytes. The successful result exposes that six-byte extent and a four-byte TypeIndex width, without a limitation identifying the malformed table.

I observed this for versions 24, 27, 29 and 31 without a native binary. For version 31, adding --binary <GameAssembly.bin> also succeeds on the damaged file. The intact controls succeed, and the damaged files retain the same decoded inventory rows. This demonstrates admission of a partial structural record, without demonstrating wrong names or owners.

Reproduction

  1. From the stated source baseline with locked dependencies and the compiled runtime available, use Node.js 24.22.1 and run this snippet. It creates its synthetic metadata and matching native binary outside the checkout, isolates each REA subprocess's home/XDG directories, and only reads the supplied target files.
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 work = mkdtempSync(join(tmpdir(), 'rea-io-check-'));
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(work, key.toLowerCase());
  mkdirSync(env[key]);
}
const binary = join(work, 'GameAssembly.bin');
writeFileSync(binary, createIl2cppBinaryFixture());
const fixture = createIl2cppMetadataFixture({ version: 31, genericOwners: true });
for (const size of [8, 6]) {
  const data = Buffer.from(fixture.bytes);
  data.writeInt32LE(size, fixture.header('interfaceOffsets', 'size'));
  const input = join(work, `interface-offsets-${size}.dat`);
  writeFileSync(input, data);
  for (const link of [false, true]) {
    const args = ['scripts/rea.mjs', 'inspect-il2cpp-metadata', input, '--json'];
    if (link) 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);
    const n = result.normalized_result;
    console.log(JSON.stringify({ size, link, exit: run.status,
      section: n?.sections.find(s => s.name === 'interfaceOffsets'),
      typeWidth: n?.index_widths.type, types: n?.types.length, methods: n?.methods.length,
      code: result.code, issue: result.details?.issues?.[0] }));
  }
}
JS
  1. Both six-byte cases print "exit":0, "typeWidth":4, four types and five methods. Their section rows retain "size":6. The eight-byte controls also succeed.

This exact snippet ran once after an independent version matrix, with the same version-31 outcomes in both runs.

Expected behavior

Refuse the six-byte interface-offset extent as malformed metadata before returning ordinary complete inventory Evidence. A fixed-width record contains a four-byte TypeIndex and four-byte offset. The IL2CPP metadata guide defines record-size-mismatch for section sizes that cannot divide into their records and does not document a binary-dependent exception for this structural check. It supports inspection of these versions without supplying a native binary.

Impact and scope

P2: bounded input-validation and Evidence-integrity defect. A caller receives successful structural metadata observations for a section that cannot hold whole records. Four supported fixed-width versions reproduced the metadata-only failure; version 31 also reproduced it while linking. Version 24 coverage is the admitted 264-byte header layout.

The eight projected collections—images, types, methods, fields, properties, events, parameters and string literals—were equal to their intact controls. No incorrect decoded names, owners or addresses, target modification, data loss or security boundary crossing was demonstrated. A caller can detect this exact malformed extent by checking that the returned fixed-width interfaceOffsets.size is divisible by eight. Supplying a binary did not detect it in the tested version-31 case.

NOT_ENOUGH_INFO: real-producer incidence, other damaged extent values, fixed-width binary-linked versions other than 31, MCP transport, published-package behavior and the first affected release were not 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 prebuilt baseline runtime. No Unity installation or disassembler was involved.

Target and artifact context

Repository-generated IL2CPP metadata with four types and five methods. The version-31 fixture initially declares one eight-byte interface-offset record; the malformed variant changes only its declared size to six, leaving the file length and other sections unchanged. Shrinking the extent leaves an in-bounds nonoverlapping gap. The optional binary is the fixture's thin arm64 Mach-O with matching IL2CPP registrations. No target code executes.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The exact snippet printed:

{"size":8,"link":false,"exit":0,"section":{"name":"interfaceOffsets","offset":736,"size":8,"count":null},"typeWidth":4,"types":4,"methods":5}
{"size":8,"link":true,"exit":0,"section":{"name":"interfaceOffsets","offset":736,"size":8,"count":null},"typeWidth":4,"types":4,"methods":5}
{"size":6,"link":false,"exit":0,"section":{"name":"interfaceOffsets","offset":736,"size":6,"count":null},"typeWidth":4,"types":4,"methods":5}
{"size":6,"link":true,"exit":0,"section":{"name":"interfaceOffsets","offset":736,"size":6,"count":null},"typeWidth":4,"types":4,"methods":5}

The independent matrix also ran intact/damaged pairs for versions 24, 27 and 29 without a binary: all six calls exited 0. Each damaged row declared six bytes. Full successful responses retained ordinary limitations about supported versions, native observations and owner derivation, without identifying this unchecked section. The eight inventory collections were compared against each corresponding intact control and matched, including version 31 with a binary.

A comparative version-31 probe changed only genericContainers.size from 48 to 44: metadata-only inspection succeeded, while the same binary mode rejected it with invalid_request, invalid_input, field-level invalid_format, and record-size-mismatch. That existing validation does not detect the separate damaged interface-offset extent.

A counted version-39 control with one five-byte interface-offset record succeeded; shrinking it to four bytes failed with invalid_request, invalid_input, invalid_format and type-index-width. That control exercises width inference rather than fixed-width extent admission.

Contract and source references at the stated baseline:

  • docs/il2cpp-metadata.md:3-21 supports binary-free inspection and the named versions; :514-523 documents malformed metadata and the record-size diagnostic; :539-543 describes incomplete-inventory handling.
  • src/il2cpp/Il2cppMetadataFile.ts:249-255 supplies fixed four-byte widths. At :278-283, the counted reader derives TypeIndex width by subtracting a four-byte offset from the interface-offset record size. tests/fixtures/il2cpp/metadata.ts:706-710 encodes the corresponding 4 + T record size.
  • src/cli/il2cppCommands.ts:22-71 exposes metadata inspection and optional linking. src/il2cpp/Il2cppMetadataProvider.ts:480-485 selects the inventory or linking reader. Both reach the shared metadata core.

No test-suite pass or fix is claimed; the verification evidence comes from direct public CLI calls and source inspection.

Typed outcome or recovery expected

Return nonzero with invalid_request / invalid_input, field-level invalid_format for the metadata path, and record-size-mismatch for the six-byte extent. Preserve successful eight-byte controls in both modes. This is a proposed restoration check; no fix was applied.

Analysis or suspected cause

Source-derived explanation: checkSections in src/il2cpp/Il2cppMetadataFile.ts:171-210 checks bounds and overlap. The shared core at :906-907 selects fixed widths for versions 24–31, bypassing resolveWidths, whose :278 call is the interface-offset record-size check. Fixed-layout recordCounts at :307-346 checks other named tables, and no later fixed-width interface-offset decoder or validator supplies the missing division check.

The generic-container validation at :524-527 is a separate 16-byte check activated by linking facts. Adding that check to metadata-only inspection would leave interfaceOffsets unchecked. A broader table-validation refactor could address both defects, but their necessary section-specific checks and restoration cases differ.

Additional context

Related open #7532 reports skipped generic-container validation. Its expected outcome specifically requires whole generic-container records, and its impact section leaves other damaged sections unestablished. It cites the same general diagnostic contract, but does not request validation of every section. Its 44-byte generic-container case must be rejected to close it; that change alone would leave this six-byte interface-offset case accepted. The existing version-31 binary path already demonstrates that separation.

Closed #5751 concerns version-39 generic-container counts disagreeing with byte extents. Its closing commit fa8b87c4705ae5bafd6cbe6992b3e4d06365fd3b adds that table's validation before counted width selection. This report does not identify a regression of that fix.

All-state issue searches covered interfaceOffsets, IL2CPP record-size-mismatch and IL2CPP extent; additional searches covered interface-offset wording and issue comments. All-state PR searches covered interfaceOffsets, IL2CPP metadata/extents and #7532. The plausible related reports and reader/linking PRs did not identify an existing report or fix for this fixed-width interface-offset extent admission.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7563 ### 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 Evidence when a supported fixed-width metadata file declares six bytes for its interface-offset table. Each record requires eight bytes. The successful result exposes that six-byte extent and a four-byte TypeIndex width, without a limitation identifying the malformed table. I observed this for versions 24, 27, 29 and 31 without a native binary. For version 31, adding `--binary <GameAssembly.bin>` also succeeds on the damaged file. The intact controls succeed, and the damaged files retain the same decoded inventory rows. This demonstrates admission of a partial structural record, without demonstrating wrong names or owners. ### Reproduction 1. From the stated source baseline with locked dependencies and the compiled runtime available, use Node.js 24.22.1 and run this snippet. It creates its synthetic metadata and matching native binary outside the checkout, isolates each REA subprocess's home/XDG directories, and only reads the supplied target files. ```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 work = mkdtempSync(join(tmpdir(), 'rea-io-check-')); 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(work, key.toLowerCase()); mkdirSync(env[key]); } const binary = join(work, 'GameAssembly.bin'); writeFileSync(binary, createIl2cppBinaryFixture()); const fixture = createIl2cppMetadataFixture({ version: 31, genericOwners: true }); for (const size of [8, 6]) { const data = Buffer.from(fixture.bytes); data.writeInt32LE(size, fixture.header('interfaceOffsets', 'size')); const input = join(work, `interface-offsets-${size}.dat`); writeFileSync(input, data); for (const link of [false, true]) { const args = ['scripts/rea.mjs', 'inspect-il2cpp-metadata', input, '--json']; if (link) 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); const n = result.normalized_result; console.log(JSON.stringify({ size, link, exit: run.status, section: n?.sections.find(s => s.name === 'interfaceOffsets'), typeWidth: n?.index_widths.type, types: n?.types.length, methods: n?.methods.length, code: result.code, issue: result.details?.issues?.[0] })); } } JS ``` 2. Both six-byte cases print `"exit":0`, `"typeWidth":4`, four types and five methods. Their section rows retain `"size":6`. The eight-byte controls also succeed. This exact snippet ran once after an independent version matrix, with the same version-31 outcomes in both runs. ### Expected behavior Refuse the six-byte interface-offset extent as malformed metadata before returning ordinary complete inventory Evidence. A fixed-width record contains a four-byte TypeIndex and four-byte offset. The IL2CPP metadata guide defines `record-size-mismatch` for section sizes that cannot divide into their records and does not document a binary-dependent exception for this structural check. It supports inspection of these versions without supplying a native binary. ### Impact and scope P2: bounded input-validation and Evidence-integrity defect. A caller receives successful structural metadata observations for a section that cannot hold whole records. Four supported fixed-width versions reproduced the metadata-only failure; version 31 also reproduced it while linking. Version 24 coverage is the admitted 264-byte header layout. The eight projected collections—images, types, methods, fields, properties, events, parameters and string literals—were equal to their intact controls. No incorrect decoded names, owners or addresses, target modification, data loss or security boundary crossing was demonstrated. A caller can detect this exact malformed extent by checking that the returned fixed-width `interfaceOffsets.size` is divisible by eight. Supplying a binary did not detect it in the tested version-31 case. NOT_ENOUGH_INFO: real-producer incidence, other damaged extent values, fixed-width binary-linked versions other than 31, MCP transport, published-package behavior and the first affected release were not 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 prebuilt baseline runtime. No Unity installation or disassembler was involved. ### Target and artifact context Repository-generated IL2CPP metadata with four types and five methods. The version-31 fixture initially declares one eight-byte interface-offset record; the malformed variant changes only its declared size to six, leaving the file length and other sections unchanged. Shrinking the extent leaves an in-bounds nonoverlapping gap. The optional binary is the fixture's thin arm64 Mach-O with matching IL2CPP registrations. No target code executes. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The exact snippet printed: ```jsonl {"size":8,"link":false,"exit":0,"section":{"name":"interfaceOffsets","offset":736,"size":8,"count":null},"typeWidth":4,"types":4,"methods":5} {"size":8,"link":true,"exit":0,"section":{"name":"interfaceOffsets","offset":736,"size":8,"count":null},"typeWidth":4,"types":4,"methods":5} {"size":6,"link":false,"exit":0,"section":{"name":"interfaceOffsets","offset":736,"size":6,"count":null},"typeWidth":4,"types":4,"methods":5} {"size":6,"link":true,"exit":0,"section":{"name":"interfaceOffsets","offset":736,"size":6,"count":null},"typeWidth":4,"types":4,"methods":5} ``` The independent matrix also ran intact/damaged pairs for versions 24, 27 and 29 without a binary: all six calls exited 0. Each damaged row declared six bytes. Full successful responses retained ordinary limitations about supported versions, native observations and owner derivation, without identifying this unchecked section. The eight inventory collections were compared against each corresponding intact control and matched, including version 31 with a binary. A comparative version-31 probe changed only `genericContainers.size` from 48 to 44: metadata-only inspection succeeded, while the same binary mode rejected it with `invalid_request`, `invalid_input`, field-level `invalid_format`, and `record-size-mismatch`. That existing validation does not detect the separate damaged interface-offset extent. A counted version-39 control with one five-byte interface-offset record succeeded; shrinking it to four bytes failed with `invalid_request`, `invalid_input`, `invalid_format` and `type-index-width`. That control exercises width inference rather than fixed-width extent admission. Contract and source references at the stated baseline: - `docs/il2cpp-metadata.md:3-21` supports binary-free inspection and the named versions; `:514-523` documents malformed metadata and the record-size diagnostic; `:539-543` describes incomplete-inventory handling. - `src/il2cpp/Il2cppMetadataFile.ts:249-255` supplies fixed four-byte widths. At `:278-283`, the counted reader derives TypeIndex width by subtracting a four-byte offset from the interface-offset record size. `tests/fixtures/il2cpp/metadata.ts:706-710` encodes the corresponding `4 + T` record size. - `src/cli/il2cppCommands.ts:22-71` exposes metadata inspection and optional linking. `src/il2cpp/Il2cppMetadataProvider.ts:480-485` selects the inventory or linking reader. Both reach the shared metadata core. No test-suite pass or fix is claimed; the verification evidence comes from direct public CLI calls and source inspection. ### Typed outcome or recovery expected Return nonzero with `invalid_request` / `invalid_input`, field-level `invalid_format` for the metadata path, and `record-size-mismatch` for the six-byte extent. Preserve successful eight-byte controls in both modes. This is a proposed restoration check; no fix was applied. ### Analysis or suspected cause Source-derived explanation: `checkSections` in `src/il2cpp/Il2cppMetadataFile.ts:171-210` checks bounds and overlap. The shared core at `:906-907` selects fixed widths for versions 24–31, bypassing `resolveWidths`, whose `:278` call is the interface-offset record-size check. Fixed-layout `recordCounts` at `:307-346` checks other named tables, and no later fixed-width interface-offset decoder or validator supplies the missing division check. The generic-container validation at `:524-527` is a separate 16-byte check activated by linking facts. Adding that check to metadata-only inspection would leave `interfaceOffsets` unchecked. A broader table-validation refactor could address both defects, but their necessary section-specific checks and restoration cases differ. ### Additional context Related open [#7532](https://github.com/morluto/rea/issues/7532) reports skipped generic-container validation. Its expected outcome specifically requires whole generic-container records, and its impact section leaves other damaged sections unestablished. It cites the same general diagnostic contract, but does not request validation of every section. Its 44-byte generic-container case must be rejected to close it; that change alone would leave this six-byte interface-offset case accepted. The existing version-31 binary path already demonstrates that separation. Closed [#5751](https://github.com/morluto/rea/issues/5751) concerns version-39 generic-container counts disagreeing with byte extents. Its closing commit `fa8b87c4705ae5bafd6cbe6992b3e4d06365fd3b` adds that table's validation before counted width selection. This report does not identify a regression of that fix. All-state issue searches covered `interfaceOffsets`, `IL2CPP record-size-mismatch` and `IL2CPP extent`; additional searches covered interface-offset wording and issue comments. All-state PR searches covered interfaceOffsets, IL2CPP metadata/extents and #7532. The plausible related reports and reader/linking PRs did not identify an existing report or fix for this fixed-width interface-offset extent admission.
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#4502
No description provided.