[GH-ISSUE #5751] [Bug] IL2CPP version 39 inspection accepts generic-container counts that disagree with section extents #3497

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

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

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

inspect-il2cpp-metadata accepts synthetic version 39 metadata whose genericContainers section claims records without bytes, bytes without records, or one record in one byte. All four malformed inputs exit 0 and return normal complete CLI Evidence, including the contradictory section row, a count-derived serialized index width, and the Widget type. The returned name and ownership remain correct; no native-linking consequence was tested.

Reproduction

Run from the source checkout below with locked dependencies installed and the CLI built; Python 3, Node.js and jq are required. The independent verifier ran this exact reproduction, and submission replay confirmed the same seven outcomes. The generator uses only Python's standard library.

work_dir="$(mktemp -d)"
mkdir -p "$work_dir/home" "$work_dir/cfg" "$work_dir/data" \
  "$work_dir/cache" "$work_dir/state" "$work_dir/tmp"
python3 -I - "$work_dir" <<'PY'
import struct, sys
from pathlib import Path
root = Path(sys.argv[1])
names = '''stringLiterals stringLiteralData strings events properties methods parameterDefaultValues fieldDefaultValues fieldAndParameterDefaultValueData fieldMarshaledSizes parameters fields genericParameters genericParameterConstraints genericContainers nestedTypes interfaces vtableMethods interfaceOffsets typeDefinitions images assemblies fieldRefs referencedAssemblies attributeData attributeDataRanges unresolvedIndirectCallParameterTypes unresolvedIndirectCallParameterRanges windowsRuntimeTypeNames windowsRuntimeStrings exportedTypeDefinitions'''.split()
i32 = lambda n: struct.pack('<i', n)
u32 = lambda n: struct.pack('<I', n)
strings = b'\0Widget\0Fixture.dll\0'

def build(filename, gc_count, gc_data, gc_index=-1, method_count=0):
    g = 1 if gc_count <= 255 else 2 if gc_count <= 65535 else 4
    sentinel = (1 << (8*g))-1
    td = i32(1)+i32(0)+b'\xff'*3
    td += (sentinel if gc_index == -1 else gc_index).to_bytes(g, 'little')
    td += u32(0)+i32(-1)*8
    td += struct.pack('<8H', method_count,0,0,0,0,0,0,0)+u32(0)+u32(0)
    image = i32(8)+i32(0)+b'\0'+u32(1)+b'\xff'+u32(0)
    image += i32(-1)+u32(0)+i32(-1)+u32(0)
    assert len(td) == 71+g and len(image) == 34
    contents = {'strings': (strings,3), 'genericContainers': (gc_data,gc_count),
                'typeDefinitions': (td,1), 'images': (image,1)}
    offset, header, payload = 380, [], []
    for name in names:
        data,count = contents.get(name,(b'',0))
        header.append(i32(offset)+i32(len(data))+i32(count))
        payload.append(data)
        offset += len(data)
    (root/filename).write_bytes(u32(0xfab11baf)+i32(39)
                               +b''.join(header)+b''.join(payload))

build('control.dat',0,b'')
build('extent-300.dat',300,b'')
build('extent-one.dat',1,b'')
build('extent-byte.dat',1,b'\0')
build('extent-zero-count.dat',0,b'\0'*16)
build('control-container.dat',1,struct.pack('<4i',0,0,0,-1),0)
build('control-bad-method.dat',0,b'',method_count=1)
PY
for name in control extent-300 extent-one extent-byte extent-zero-count \
  control-container control-bad-method; do
  env -i PATH="$PATH" HOME="$work_dir/home" XDG_CONFIG_HOME="$work_dir/cfg" \
    XDG_DATA_HOME="$work_dir/data" XDG_CACHE_HOME="$work_dir/cache" \
    XDG_STATE_HOME="$work_dir/state" TMPDIR="$work_dir/tmp" REA_LOG_LEVEL=silent \
    node scripts/rea.mjs inspect-il2cpp-metadata "$work_dir/$name.dat" --json \
    > "$work_dir/$name.json" 2> "$work_dir/$name.stderr"
  result=$?
  printf '%s exit=%s\n' "$name" "$result"
  jq -c 'if .normalized_result then .normalized_result |
    {generic_container_width: .index_widths.generic_container,
     container_section: (.sections[] | select(.name == "genericContainers")),
     type_names: [.types[].name]}
    else {code, category, reason: .details.issues[0].reason} end' \
    "$work_dir/$name.json"
done

Expected behavior

Reject all four extent-* inputs as malformed metadata without returning a complete inventory. docs/il2cpp-metadata.md:342-353 documents input errors with field-level invalid_format and the record-size-mismatch diagnostic for incompatible section records. Lines 156-164 describe counted sections and serialized widths; tests/fixtures/il2cpp/metadata.ts:570 gives generic-container records a 16-byte extent.

Both consistent-extent controls should continue returning Widget; the unrelated malformed method-range control should remain rejected.

Impact and scope

P2: callers receive ordinary observed inventory Evidence for internally contradictory metadata, and the unchecked count selects the reported serialized index width. All four generated malformed inputs reproduced this acceptance. The observed consequence is bounded to accepting damaged metadata; incorrect names, owners, native addresses, security impact and real-game consequences were not established. MCP and other metadata versions were not replayed.

A workaround is available for the demonstrated contradictions: inspect the returned generic-container section count and size independently before trusting the inventory. This does not establish validation of every other field.

REA version

rea-agents 6.7.0; source commit 9d8e7803326a382cbbb3ffdb900f5e93e5442d68. Submission fetched main at d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f; no intervening commits changed the cited documentation, fixture, Il2cppMetadataFile.ts or Il2cppMetadataMembers.ts. Applicability to main is source-inspected; the runtime replay used the pinned baseline.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1 arm64. No Unity runtime, native binary, Hopper, Ghidra or analysis provider is involved.

Target and artifact context

Self-generated version 39 IL2CPP metadata with one Widget type and one Fixture.dll image. Every probe isolates home, config, data, cache, state and temporary directories. No private target data is used.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed CLI results; every successful row returns Widget and the indicated section at offset 400:

input                exit  section size  section count  generic_container width
control                 0             0              0                        1
extent-300              0             0            300                        2
extent-one              0             0              1                        1
extent-byte             0             1              1                        1
extent-zero-count       0            16              0                        1
control-container       0            16              1                        1
control-bad-method      1             0              0                      n/a

The malformed method-range control returns code: invalid_request, category: invalid_input, field-level details.issues[0].reason: invalid_format, and an index-out-of-range diagnostic; its partial observation says no complete inventory was returned. Successful malformed-extent results have ordinary limitations about metadata versions, absent native linking and owner derivation; none identifies the container extent as damaged or unvalidated.

The verifier also ran the existing rejects record sizes that disagree with counts or index widths test in tests/boundary/il2cpp/il2cppMetadataFile.test.ts:251-282: one test passed. That test changes the container count without rewriting dependent records, so a different table's record-size check rejects the width mismatch. The reproduction above supplies matching-width type records and demonstrates same-width failures too. Evidence provenance: observed; local verification: independently reproduced.

Typed outcome or recovery expected

Return invalid_request / invalid_input with field-level invalid_format and the documented record-size-mismatch diagnostic for each inconsistent extent, preserving available partial source facts without normal complete inventory Evidence. A proposed restoration check is to replay all seven inputs: reject the four extent-* files, retain both successful controls, and retain the malformed-method rejection. No fix or restoration was tested.

Analysis or suspected cause

Source-derived reasoning: src/il2cpp/Il2cppMetadataFile.ts:256-292 consumes the generic-container count to select serialized widths. checkSections checks nonnegative fields, file bounds and overlap; record-count validation covers types/images/methods/nested types, while src/il2cpp/Il2cppMetadataMembers.ts:43-80 validates other member tables. Neither validates genericContainers count against its byte extent before publishing the inventory. No bundled dependency was identified as the cause.

Additional context

Prepared with AI assistance and independently verified. The extent-only report was selected over combining a separate container-index validation lead because it has a distinct malformed-record contract and restoration signal. The separate index behavior is outside this issue.

Related reports #5403, #5405, #5460 and #5630 concern truncated-header classification or resource/recovery diagnostics, not acceptance of contradictory container extents. Merged IL2CPP PRs #3807, #5326 and #5327 add capabilities and do not cover this defect. Open PR #5511 changes desktop probe suggestions.

NOT_ENOUGH_INFO: no last-known-good release or regression boundary was established. Installed-package, MCP and real-producer metadata behavior were not replayed.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5751 ### 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 `inspect-il2cpp-metadata` accepts synthetic version 39 metadata whose `genericContainers` section claims records without bytes, bytes without records, or one record in one byte. All four malformed inputs exit 0 and return normal complete CLI Evidence, including the contradictory section row, a count-derived serialized index width, and the `Widget` type. The returned name and ownership remain correct; no native-linking consequence was tested. ### Reproduction Run from the source checkout below with locked dependencies installed and the CLI built; Python 3, Node.js and jq are required. The independent verifier ran this exact reproduction, and submission replay confirmed the same seven outcomes. The generator uses only Python's standard library. ```sh work_dir="$(mktemp -d)" mkdir -p "$work_dir/home" "$work_dir/cfg" "$work_dir/data" \ "$work_dir/cache" "$work_dir/state" "$work_dir/tmp" python3 -I - "$work_dir" <<'PY' import struct, sys from pathlib import Path root = Path(sys.argv[1]) names = '''stringLiterals stringLiteralData strings events properties methods parameterDefaultValues fieldDefaultValues fieldAndParameterDefaultValueData fieldMarshaledSizes parameters fields genericParameters genericParameterConstraints genericContainers nestedTypes interfaces vtableMethods interfaceOffsets typeDefinitions images assemblies fieldRefs referencedAssemblies attributeData attributeDataRanges unresolvedIndirectCallParameterTypes unresolvedIndirectCallParameterRanges windowsRuntimeTypeNames windowsRuntimeStrings exportedTypeDefinitions'''.split() i32 = lambda n: struct.pack('<i', n) u32 = lambda n: struct.pack('<I', n) strings = b'\0Widget\0Fixture.dll\0' def build(filename, gc_count, gc_data, gc_index=-1, method_count=0): g = 1 if gc_count <= 255 else 2 if gc_count <= 65535 else 4 sentinel = (1 << (8*g))-1 td = i32(1)+i32(0)+b'\xff'*3 td += (sentinel if gc_index == -1 else gc_index).to_bytes(g, 'little') td += u32(0)+i32(-1)*8 td += struct.pack('<8H', method_count,0,0,0,0,0,0,0)+u32(0)+u32(0) image = i32(8)+i32(0)+b'\0'+u32(1)+b'\xff'+u32(0) image += i32(-1)+u32(0)+i32(-1)+u32(0) assert len(td) == 71+g and len(image) == 34 contents = {'strings': (strings,3), 'genericContainers': (gc_data,gc_count), 'typeDefinitions': (td,1), 'images': (image,1)} offset, header, payload = 380, [], [] for name in names: data,count = contents.get(name,(b'',0)) header.append(i32(offset)+i32(len(data))+i32(count)) payload.append(data) offset += len(data) (root/filename).write_bytes(u32(0xfab11baf)+i32(39) +b''.join(header)+b''.join(payload)) build('control.dat',0,b'') build('extent-300.dat',300,b'') build('extent-one.dat',1,b'') build('extent-byte.dat',1,b'\0') build('extent-zero-count.dat',0,b'\0'*16) build('control-container.dat',1,struct.pack('<4i',0,0,0,-1),0) build('control-bad-method.dat',0,b'',method_count=1) PY for name in control extent-300 extent-one extent-byte extent-zero-count \ control-container control-bad-method; do env -i PATH="$PATH" HOME="$work_dir/home" XDG_CONFIG_HOME="$work_dir/cfg" \ XDG_DATA_HOME="$work_dir/data" XDG_CACHE_HOME="$work_dir/cache" \ XDG_STATE_HOME="$work_dir/state" TMPDIR="$work_dir/tmp" REA_LOG_LEVEL=silent \ node scripts/rea.mjs inspect-il2cpp-metadata "$work_dir/$name.dat" --json \ > "$work_dir/$name.json" 2> "$work_dir/$name.stderr" result=$? printf '%s exit=%s\n' "$name" "$result" jq -c 'if .normalized_result then .normalized_result | {generic_container_width: .index_widths.generic_container, container_section: (.sections[] | select(.name == "genericContainers")), type_names: [.types[].name]} else {code, category, reason: .details.issues[0].reason} end' \ "$work_dir/$name.json" done ``` ### Expected behavior Reject all four `extent-*` inputs as malformed metadata without returning a complete inventory. `docs/il2cpp-metadata.md:342-353` documents input errors with field-level `invalid_format` and the `record-size-mismatch` diagnostic for incompatible section records. Lines 156-164 describe counted sections and serialized widths; `tests/fixtures/il2cpp/metadata.ts:570` gives generic-container records a 16-byte extent. Both consistent-extent controls should continue returning `Widget`; the unrelated malformed method-range control should remain rejected. ### Impact and scope P2: callers receive ordinary observed inventory Evidence for internally contradictory metadata, and the unchecked count selects the reported serialized index width. All four generated malformed inputs reproduced this acceptance. The observed consequence is bounded to accepting damaged metadata; incorrect names, owners, native addresses, security impact and real-game consequences were not established. MCP and other metadata versions were not replayed. A workaround is available for the demonstrated contradictions: inspect the returned generic-container section count and size independently before trusting the inventory. This does not establish validation of every other field. ### REA version `rea-agents` 6.7.0; source commit `9d8e7803326a382cbbb3ffdb900f5e93e5442d68`. Submission fetched main at `d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f`; no intervening commits changed the cited documentation, fixture, `Il2cppMetadataFile.ts` or `Il2cppMetadataMembers.ts`. Applicability to main is source-inspected; the runtime replay used the pinned baseline. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1 arm64. No Unity runtime, native binary, Hopper, Ghidra or analysis provider is involved. ### Target and artifact context Self-generated version 39 IL2CPP metadata with one `Widget` type and one `Fixture.dll` image. Every probe isolates home, config, data, cache, state and temporary directories. No private target data is used. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed CLI results; every successful row returns `Widget` and the indicated section at offset 400: ```text input exit section size section count generic_container width control 0 0 0 1 extent-300 0 0 300 2 extent-one 0 0 1 1 extent-byte 0 1 1 1 extent-zero-count 0 16 0 1 control-container 0 16 1 1 control-bad-method 1 0 0 n/a ``` The malformed method-range control returns `code: invalid_request`, `category: invalid_input`, field-level `details.issues[0].reason: invalid_format`, and an `index-out-of-range` diagnostic; its partial observation says no complete inventory was returned. Successful malformed-extent results have ordinary limitations about metadata versions, absent native linking and owner derivation; none identifies the container extent as damaged or unvalidated. The verifier also ran the existing `rejects record sizes that disagree with counts or index widths` test in `tests/boundary/il2cpp/il2cppMetadataFile.test.ts:251-282`: one test passed. That test changes the container count without rewriting dependent records, so a different table's record-size check rejects the width mismatch. The reproduction above supplies matching-width type records and demonstrates same-width failures too. Evidence provenance: observed; local verification: independently reproduced. ### Typed outcome or recovery expected Return `invalid_request` / `invalid_input` with field-level `invalid_format` and the documented `record-size-mismatch` diagnostic for each inconsistent extent, preserving available partial source facts without normal complete inventory Evidence. A proposed restoration check is to replay all seven inputs: reject the four `extent-*` files, retain both successful controls, and retain the malformed-method rejection. No fix or restoration was tested. ### Analysis or suspected cause Source-derived reasoning: `src/il2cpp/Il2cppMetadataFile.ts:256-292` consumes the generic-container count to select serialized widths. `checkSections` checks nonnegative fields, file bounds and overlap; record-count validation covers types/images/methods/nested types, while `src/il2cpp/Il2cppMetadataMembers.ts:43-80` validates other member tables. Neither validates `genericContainers` count against its byte extent before publishing the inventory. No bundled dependency was identified as the cause. ### Additional context Prepared with AI assistance and independently verified. The extent-only report was selected over combining a separate container-index validation lead because it has a distinct malformed-record contract and restoration signal. The separate index behavior is outside this issue. Related reports #5403, #5405, #5460 and #5630 concern truncated-header classification or resource/recovery diagnostics, not acceptance of contradictory container extents. Merged IL2CPP PRs #3807, #5326 and #5327 add capabilities and do not cover this defect. Open PR #5511 changes desktop probe suggestions. NOT_ENOUGH_INFO: no last-known-good release or regression boundary was established. Installed-package, MCP and real-producer metadata behavior were not replayed.
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#3497
No description provided.