[GH-ISSUE #6808] [Bug] IL2CPP metadata rejects valid shared-suffix string offsets as corrupt #4157

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

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

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

The production Il2cppMetadataProvider.inspect refuses a metadata name whose byte offset points to the terminated UTF-8 suffix Player within stored xPlayer\0, reporting invalid-string: type 0 name string index 76 starts inside another string. Both the offset and terminator are within the declared strings section. A standalone Player\0 name and the unshifted xPlayer\0 name are accepted. The same result occurs with synthetic metadata versions 29 and 39.

These are executed source-provider observations on inert local metadata, not CLI/MCP, Unity loading, or target execution. The valid-offset premise is source-derived from the runtime reader cited below; no Unity-generated example using suffix sharing was collected.

Reproduction

Use the stated source checkout with locked dependencies installed at /tmp/rea-il2cpp-repro/rea, or substitute that import prefix. Save the following as /tmp/rea-il2cpp-repro/probe.mts. It creates small metadata fixtures using the committed fixture builder, changes only the first type's name offset for the suffix case, and calls the production provider. Each final matrix case ran once. Earlier exploratory suffix probes also failed.

mkdir -p /tmp/rea-il2cpp-repro/work /tmp/rea-il2cpp-repro/home /tmp/rea-il2cpp-repro/config
HOME=/tmp/rea-il2cpp-repro/home XDG_CONFIG_HOME=/tmp/rea-il2cpp-repro/config node --import /tmp/rea-il2cpp-repro/rea/node_modules/tsx/dist/loader.mjs /tmp/rea-il2cpp-repro/probe.mts
import { writeFile } from 'node:fs/promises';
import { createIl2cppMetadataFixture } from '/tmp/rea-il2cpp-repro/rea/tests/fixtures/il2cpp/metadata.ts';
import { Il2cppMetadataProvider } from '/tmp/rea-il2cpp-repro/rea/src/il2cpp/Il2cppMetadataProvider.ts';
const path='/tmp/rea-il2cpp-repro/work/global-metadata.dat';
for(const version of [29,39] as const){
 for(const [label,typeName,delta] of [['boundary-control','Player',0],['prefixed-control','xPlayer',0],['shared-suffix','xPlayer',1]] as const){
  const fixture=createIl2cppMetadataFixture({version,typeNames:{0:typeName}});
  const bytes=Buffer.from(fixture.bytes),at=fixture.record('typeDefinitions',0);
  const original=bytes.readInt32LE(at);bytes.writeInt32LE(original+delta,at);
  const start=fixture.section('strings').offset+original+delta;
  const runtimeName=bytes.subarray(start,bytes.indexOf(0,start)).toString('utf8');
  await writeFile(path,bytes);
  const provider=new Il2cppMetadataProvider();
  const result=await provider.inspect({path});await provider.close();
  console.log(JSON.stringify({version,label,delta,runtimeName,bytes:bytes.length,index:original+delta,previousByte:bytes[start-1],result:result.ok?{ok:true,name:result.value.types[0]?.name}:{ok:false,name:result.error.name,message:result.error.message,cause:result.error.cause instanceof Error?result.error.cause.message:undefined}}));
 }
}

Expected behavior

Decode a valid in-range UTF-8 string from its declared byte offset through its terminator, returning Player for both representations. A preceding nonzero byte does not make that offset invalid. Keep bounds, termination, UTF-8, and representation-budget checks.

The first-party HybridCLR documentation's IL2CPP GetStringFromIndex example shows byte-offset lookup without a preceding-NUL requirement. The runtime implementation at immutable source revision likewise bounds the index, then returns stringOffset + index; normal file-backed indices bypass its interpreter hook. This establishes offset semantics by source inspection, not execution of that runtime. REA advertises metadata versions 29 and 39.

Impact and scope

An otherwise admitted metadata file cannot be inventoried when one of the names examined by this reader uses a shared suffix. The complete provider request fails instead of returning its type/member inventory. Both suffix cases failed and all four boundary controls passed. Observed scope is synthetic versions 29 and 39 and a type name; frequency in Unity-generated files, other name columns, and other versions was not measured. No target bytes or global configuration were changed.

REA version

Executed source: e2efd10b70a2b5f15a7ac1b5a457a9a1dbcab4d4, package manifest 6.7.0. Compared with latest published tag rea-agents-6.7.0 (27dc42d10249f0410904a75c0997842eb03b931c): this tag has no src/il2cpp implementation. This report therefore concerns current main's unreleased IL2CPP capability; the published package runtime was not executed.

Runtime and platform

Node.js v26.12.1; Linux x64; repository-locked tsx loader. No Unity, CLR, Hopper, Ghidra, or IDA process was used. Node 26.12.1 is admitted by the package engines; the repository's pinned Node 24.22.1 was not separately exercised.

Target and artifact context

Self-authored inert global-metadata.dat fixtures, 1030–1046 bytes, with valid supported layouts, unchanged section extents and row ownership, and a NUL-terminated UTF-8 strings section. The suffix input points type 0 at byte index 76 (Player) inside xPlayer\0; it does not shift the section or point into a multibyte code point.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The executed six-case production-provider matrix exited 0; it prints refusal results as data. Relevant output, with redundant fields omitted:

{"version":29,"label":"boundary-control","runtimeName":"Player","result":{"ok":true,"name":"Player"}}
{"version":29,"label":"prefixed-control","runtimeName":"xPlayer","result":{"ok":true,"name":"xPlayer"}}
{"version":29,"label":"shared-suffix","runtimeName":"Player","index":76,"previousByte":120,"result":{"ok":false,"cause":"invalid-string: type 0 name string index 76 starts inside another string"}}
{"version":39,"label":"boundary-control","runtimeName":"Player","result":{"ok":true,"name":"Player"}}
{"version":39,"label":"prefixed-control","runtimeName":"xPlayer","result":{"ok":true,"name":"xPlayer"}}
{"version":39,"label":"shared-suffix","runtimeName":"Player","index":76,"previousByte":120,"result":{"ok":false,"cause":"invalid-string: type 0 name string index 76 starts inside another string"}}

runtimeName is a local byte-offset/terminator read following the cited source semantics; it is not output from an executed Unity runtime. The production name reader rejects pool[index - 1] !== 0 before UTF-8 decoding. Its existing regression also treats an interior name offset as corrupt, so the disputed invariant is explicit rather than an incidental parser exception.

Typed outcome or recovery expected

Successful metadata inventory containing types[0].name: "Player", with the original artifact identity preserved. If suffix offsets are intentionally outside supported coverage, distinguish that restriction from corrupt UTF-8 or an out-of-range index.

Analysis or suspected cause

Source-derived: the preceding-NUL check imposes a string-pool entry-boundary rule that the cited IL2CPP runtime reader does not impose. A string byte offset need not be the first byte after a different string's terminator. This finding does not require weakening actual range or UTF-8 validation.

Additional context

Open titles and bodies were searched first. All-state searches for IL2CPP with string index, string section, and suffix found no matching report. Related #6409 concerns attribution of genuinely invalid metadata errors to a selected native binary; this reproduction supplies no native binary and concerns valid string-offset interpretation.

AI assistance: a delegated Codex researcher authored the fixture probe, inspected the cited source, and drafted this report. No implementation fix was made. Unity loading and real-game prevalence remain untested.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Both supported-layout suffix fixtures cause the production provider to refuse an otherwise readable inventory; four controls succeed. The affected-input frequency in real builds is unknown.
  • Workaround status: unverified
  • Workaround basis: No workaround preserving the exact selected bytes was tested. Reading with a conforming offset-based parser or repacking a disposable copy is a possible alternative, not a verified REA recovery.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6808 ## 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 The production `Il2cppMetadataProvider.inspect` refuses a metadata name whose byte offset points to the terminated UTF-8 suffix `Player` within stored `xPlayer\0`, reporting `invalid-string: type 0 name string index 76 starts inside another string`. Both the offset and terminator are within the declared strings section. A standalone `Player\0` name and the unshifted `xPlayer\0` name are accepted. The same result occurs with synthetic metadata versions 29 and 39. These are executed source-provider observations on inert local metadata, not CLI/MCP, Unity loading, or target execution. The valid-offset premise is source-derived from the runtime reader cited below; no Unity-generated example using suffix sharing was collected. ## Reproduction Use the stated source checkout with locked dependencies installed at `/tmp/rea-il2cpp-repro/rea`, or substitute that import prefix. Save the following as `/tmp/rea-il2cpp-repro/probe.mts`. It creates small metadata fixtures using the committed fixture builder, changes only the first type's name offset for the suffix case, and calls the production provider. Each final matrix case ran once. Earlier exploratory suffix probes also failed. ```sh mkdir -p /tmp/rea-il2cpp-repro/work /tmp/rea-il2cpp-repro/home /tmp/rea-il2cpp-repro/config HOME=/tmp/rea-il2cpp-repro/home XDG_CONFIG_HOME=/tmp/rea-il2cpp-repro/config node --import /tmp/rea-il2cpp-repro/rea/node_modules/tsx/dist/loader.mjs /tmp/rea-il2cpp-repro/probe.mts ``` ```ts import { writeFile } from 'node:fs/promises'; import { createIl2cppMetadataFixture } from '/tmp/rea-il2cpp-repro/rea/tests/fixtures/il2cpp/metadata.ts'; import { Il2cppMetadataProvider } from '/tmp/rea-il2cpp-repro/rea/src/il2cpp/Il2cppMetadataProvider.ts'; const path='/tmp/rea-il2cpp-repro/work/global-metadata.dat'; for(const version of [29,39] as const){ for(const [label,typeName,delta] of [['boundary-control','Player',0],['prefixed-control','xPlayer',0],['shared-suffix','xPlayer',1]] as const){ const fixture=createIl2cppMetadataFixture({version,typeNames:{0:typeName}}); const bytes=Buffer.from(fixture.bytes),at=fixture.record('typeDefinitions',0); const original=bytes.readInt32LE(at);bytes.writeInt32LE(original+delta,at); const start=fixture.section('strings').offset+original+delta; const runtimeName=bytes.subarray(start,bytes.indexOf(0,start)).toString('utf8'); await writeFile(path,bytes); const provider=new Il2cppMetadataProvider(); const result=await provider.inspect({path});await provider.close(); console.log(JSON.stringify({version,label,delta,runtimeName,bytes:bytes.length,index:original+delta,previousByte:bytes[start-1],result:result.ok?{ok:true,name:result.value.types[0]?.name}:{ok:false,name:result.error.name,message:result.error.message,cause:result.error.cause instanceof Error?result.error.cause.message:undefined}})); } } ``` ## Expected behavior Decode a valid in-range UTF-8 string from its declared byte offset through its terminator, returning `Player` for both representations. A preceding nonzero byte does not make that offset invalid. Keep bounds, termination, UTF-8, and representation-budget checks. The first-party [HybridCLR documentation's IL2CPP `GetStringFromIndex` example](https://www.hybridclr.cn/en/docs/8.15.0/basic/sourceinspect) shows byte-offset lookup without a preceding-NUL requirement. The [runtime implementation at immutable source revision](https://github.com/focus-creative-games/il2cpp_plus/blob/051744be70e91667c14c0d93f021598a8452076e/libil2cpp/vm/GlobalMetadata.cpp#L93-L101) likewise bounds the index, then returns `stringOffset + index`; normal file-backed indices bypass its interpreter hook. This establishes offset semantics by source inspection, not execution of that runtime. REA [advertises metadata versions 29 and 39](https://github.com/morluto/rea/blob/e2efd10b70a2b5f15a7ac1b5a457a9a1dbcab4d4/docs/il2cpp-metadata.md). ## Impact and scope An otherwise admitted metadata file cannot be inventoried when one of the names examined by this reader uses a shared suffix. The complete provider request fails instead of returning its type/member inventory. Both suffix cases failed and all four boundary controls passed. Observed scope is synthetic versions 29 and 39 and a type name; frequency in Unity-generated files, other name columns, and other versions was not measured. No target bytes or global configuration were changed. ## REA version Executed source: `e2efd10b70a2b5f15a7ac1b5a457a9a1dbcab4d4`, package manifest 6.7.0. Compared with latest published tag `rea-agents-6.7.0` (`27dc42d10249f0410904a75c0997842eb03b931c`): this tag has no `src/il2cpp` implementation. This report therefore concerns current main's unreleased IL2CPP capability; the published package runtime was not executed. ## Runtime and platform Node.js v26.12.1; Linux x64; repository-locked tsx loader. No Unity, CLR, Hopper, Ghidra, or IDA process was used. Node 26.12.1 is admitted by the package engines; the repository's pinned Node 24.22.1 was not separately exercised. ## Target and artifact context Self-authored inert `global-metadata.dat` fixtures, 1030–1046 bytes, with valid supported layouts, unchanged section extents and row ownership, and a NUL-terminated UTF-8 strings section. The suffix input points type 0 at byte index 76 (`Player`) inside `xPlayer\0`; it does not shift the section or point into a multibyte code point. ## Interface Other ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence The executed six-case production-provider matrix exited 0; it prints refusal results as data. Relevant output, with redundant fields omitted: ```json {"version":29,"label":"boundary-control","runtimeName":"Player","result":{"ok":true,"name":"Player"}} {"version":29,"label":"prefixed-control","runtimeName":"xPlayer","result":{"ok":true,"name":"xPlayer"}} {"version":29,"label":"shared-suffix","runtimeName":"Player","index":76,"previousByte":120,"result":{"ok":false,"cause":"invalid-string: type 0 name string index 76 starts inside another string"}} {"version":39,"label":"boundary-control","runtimeName":"Player","result":{"ok":true,"name":"Player"}} {"version":39,"label":"prefixed-control","runtimeName":"xPlayer","result":{"ok":true,"name":"xPlayer"}} {"version":39,"label":"shared-suffix","runtimeName":"Player","index":76,"previousByte":120,"result":{"ok":false,"cause":"invalid-string: type 0 name string index 76 starts inside another string"}} ``` `runtimeName` is a local byte-offset/terminator read following the cited source semantics; it is not output from an executed Unity runtime. The [production name reader](https://github.com/morluto/rea/blob/e2efd10b70a2b5f15a7ac1b5a457a9a1dbcab4d4/src/il2cpp/Il2cppMetadataFile.ts#L349-L384) rejects `pool[index - 1] !== 0` before UTF-8 decoding. Its [existing regression](https://github.com/morluto/rea/blob/e2efd10b70a2b5f15a7ac1b5a457a9a1dbcab4d4/tests/boundary/il2cpp/il2cppMetadataFile.test.ts#L411-L442) also treats an interior name offset as corrupt, so the disputed invariant is explicit rather than an incidental parser exception. ## Typed outcome or recovery expected Successful metadata inventory containing `types[0].name: "Player"`, with the original artifact identity preserved. If suffix offsets are intentionally outside supported coverage, distinguish that restriction from corrupt UTF-8 or an out-of-range index. ## Analysis or suspected cause Source-derived: the preceding-NUL check imposes a string-pool entry-boundary rule that the cited IL2CPP runtime reader does not impose. A string byte offset need not be the first byte after a different string's terminator. This finding does not require weakening actual range or UTF-8 validation. ## Additional context Open titles and bodies were searched first. All-state searches for IL2CPP with `string index`, `string section`, and `suffix` found no matching report. Related [#6409](https://github.com/morluto/rea/issues/6409) concerns attribution of genuinely invalid metadata errors to a selected native binary; this reproduction supplies no native binary and concerns valid string-offset interpretation. AI assistance: a delegated Codex researcher authored the fixture probe, inspected the cited source, and drafted this report. No implementation fix was made. Unity loading and real-game prevalence remain untested. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Both supported-layout suffix fixtures cause the production provider to refuse an otherwise readable inventory; four controls succeed. The affected-input frequency in real builds is unknown. - Workaround status: unverified - Workaround basis: No workaround preserving the exact selected bytes was tested. Reading with a conforming offset-based parser or repacking a disposable copy is a possible alternative, not a verified REA recovery.
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#4157
No description provided.