[GH-ISSUE #6610] [Bug] Duplicate IL2CPP method tokens produce incorrect native links and folded-code seeds #3996

Closed
opened 2026-10-31 00:45:53 +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/6610

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 IL2CPP application service accepts two method definitions in the same image with the same MethodDef token, publishes concrete native addresses with confidence: "derived", and exports them as folded code.

In the synthetic version-31 control, Game.Player::.ctor and Update resolve to 0x100000200 and 0x100000210. Changing one metadata byte makes both tokens 0x06000002; the unchanged native binary then produces 0x100000200 for both and the seed comment # folded 0x100000200 Game.Player$$.ctor also Game.Player$$Update. The actual Update slot remains present in the binary, but loses its seed. Duplicating a token across two types within the same image has the same result.

Reproduction

From the source revision below with locked dependencies installed, save this complete probe as /tmp/rea-il2cpp-token-probe.mts and run:

node node_modules/tsx/dist/cli.mjs /tmp/rea-il2cpp-token-probe.mts

This exact probe ran once. It uses repository fixture generators, the real production composition/application/reader/seed writer, regular temporary files, and no mocked provider. It never loads or executes the native fixture.

import { mkdtemp, writeFile, readFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { createHash } from 'node:crypto';
const { createIl2cppMetadataFixture } = await import(pathToFileURL(resolve('tests/fixtures/il2cpp/metadata.ts')).href);
const { createIl2cppBinaryFixture } = await import(pathToFileURL(resolve('tests/fixtures/il2cpp/binary.ts')).href);
const { createIl2cppMetadataService } = await import(pathToFileURL(resolve('src/composition/il2cpp.ts')).href);
const directory = await mkdtemp(join(tmpdir(), 'rea-il2cpp-tokens-'));
const fixture = createIl2cppMetadataFixture({ version: 31 });
// v31: name, declaring type, return type, return token, parameter start,
// generic container, then the four-byte method token.
const tokenAt = (index: number) => fixture.record('methods', index) + 24;
const baselineBinary = createIl2cppBinaryFixture();
const binaryPath = join(directory, 'GameAssembly.dylib');
await writeFile(binaryPath, baselineBinary);
const service = createIl2cppMetadataService();
try {
  for (const name of ['baseline', 'duplicate-same-type', 'duplicate-other-type', 'zero-token', 'out-of-range-token', 'real-shared-code']) {
    const metadata = Buffer.from(fixture.bytes);
    if (name === 'duplicate-same-type') metadata.writeUInt32LE(metadata.readUInt32LE(tokenAt(0)), tokenAt(1));
    if (name === 'duplicate-other-type') metadata.writeUInt32LE(metadata.readUInt32LE(tokenAt(0)), tokenAt(2));
    if (name === 'zero-token') metadata.writeUInt32LE(0, tokenAt(1));
    if (name === 'out-of-range-token') metadata.writeUInt32LE(0x06000004, tokenAt(1));
    const binary = Buffer.from(baselineBinary);
    if (name === 'real-shared-code') binary.writeBigUInt64LE(binary.readBigUInt64LE(0x308), 0x300);
    await writeFile(binaryPath, binary);
    const path = join(directory, name + '.dat');
    const output = join(directory, name + '.tsv');
    await writeFile(path, metadata);
    const result = await service.inspectWithGhidraSeeds({path, binary_path:binaryPath, detail:'complete'}, {path:output});
    const tokens = Array.from({length:5}, (_, index) => '0x' + metadata.readUInt32LE(tokenAt(index)).toString(16));
    if (result.ok) {
      const report = result.value.normalized_result;
      const seeds = await readFile(output, 'utf8');
      console.log(JSON.stringify({name,ok:true,tokens,confidence:result.value.confidence,methods:report.methods,method_addresses:report.method_addresses,foldedLines:seeds.split('\n').filter(line=>line.startsWith('# folded')),functions:seeds.split('\n').filter(line=>line.includes('\tfunction\t')).length,binarySha256:createHash('sha256').update(binary).digest('hex'),changedMetadataOffsets:[...metadata.keys()].filter(index=>metadata[index]!==fixture.bytes[index])}));
    } else {
      console.log(JSON.stringify({name,ok:false,tokens,code:result.error.code,issues:result.error.details?.issues ?? result.error.issues,message:result.error.message}));
    }
  }
} finally { await service.close(); }
console.log(JSON.stringify({fixtureDirectory:directory}));

Expected behavior

Refuse contradictory per-image method identity before publishing concrete native links or seeds. A MethodDef token identifies one metadata row within a module; it cannot identify two distinct method definitions. Identical code folding is different: two distinct tokens may legitimately select separate pointer slots holding the same address. Tokens repeated across different images must also remain valid.

The native-link contract uses each token row ID to select a module slot. Microsoft's metadata-token explanation specifies table and row identity.

Impact and scope

P2: bounded acceptance of damaged metadata yields an incorrect successful native-link inventory and misleading persistent seed output. Both same-type and different-type duplicates within one image reproduced it. A genuine shared-code control with distinct tokens remains accepted and produces the same folded comment, so exported results do not distinguish the invalid identity from real folding.

The valid baseline already repeats token values across the two different images; those repetitions are valid and are not this defect. Real-game incidence, other metadata versions, and actual Ghidra import behavior were not tested. No source artifact was modified by REA; the probe deliberately changes only its synthetic input.

REA version

Source commit 572d0c234089605f120dcfea340f11e38a3c3705, package manifest rea-agents@6.7.0. This is unreleased main behavior: tag rea-agents-6.7.0 at 27dc42d10249f0410904a75c0997842eb03b931c contains no IL2CPP implementation files. The published 6.7.0 runtime was not reproduced.

Runtime and platform

Node.js v24.22.1; Linux 7.2.8-9-omarchy x86_64; repository-locked dependencies. No Unity, Hopper, Ghidra or IDA runtime was used.

Target and artifact context

Repository-generated version-31 metadata with two assembly images, four types and five methods, paired with a 4,096-byte thin arm64 Mach-O registration fixture. Its unchanged SHA-256 in both duplicate-token cases is f3afa13e37c01bfe0dfbd0a6b5de28482284d7fddf9d3e3ecac723b31e71e47f.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed production-service and actual exported-file results:

case                  first image tokens       result   first three RVAs      function seeds
baseline              [2, 1, 3]                success  [0x200,0x210,0x220]   4
duplicate-same-type   [2, 2, 3]                success  [0x200,0x200,0x220]   3
duplicate-other-type  [2, 1, 2]                success  [0x200,0x210,0x200]   3
zero-token            [2, 0, 3]                refused  invalid-method-token
out-of-range-token    [2, 4, 3]                refused  invalid-method-token
real-shared-code      [2, 1, 3]                success  [0x200,0x200,0x220]   3

The first three tokens above have table prefix 0x06; zero-token actually writes 0. The same-type case changes only metadata byte offset 458; the other-type case changes only offset 494. All four successful cases return confidence: "derived". The shared-code control changes only the binary's slot 0 to equal slot 1; metadata remains unchanged.

These observations establish application/producer/export behavior with synthetic files, not public CLI/MCP transport or real-provider behavior.

Typed outcome or recovery expected

Return a typed malformed-metadata/native-link refusal for duplicate (image, method token) identities, preserving available source evidence without a complete native table or seed file. Keep valid cross-image token reuse and distinct-token shared addresses working. A general preflight/recovery tool was not tested.

Analysis or suspected cause

Source-derived: method decoding retains token values. Native linking checks token kind and range but never uniqueness per image, then resolves both definitions through the same slot. Seed export interprets any repeated resolved address as folding.

Additional context

The initial full open snapshot and live open/closed searches for IL2CPP, IL2CPP token, IL2CPP duplicate and IL2CPP generic found no matching report. Related #5112 introduced native linking; #5751 concerns generic-container section extents, #6409 artifact attribution, and #6534 array-kind rendering. They do not cover duplicate method identity. No last-known-good revision was established. No fix was attempted.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Two one-byte synthetic metadata corruptions within one image were accepted and published incorrect derived method addresses plus misleading folded-code seeds, while an unchanged native binary still held the distinct original slots.
  • Workaround status: unverified
  • Workaround basis: The trusted baseline produces correct links, but a general per-image token preflight or recovery workflow for affected user artifacts was not exercised.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6610 ## 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 IL2CPP application service accepts two method definitions in the same image with the same MethodDef token, publishes concrete native addresses with `confidence: "derived"`, and exports them as folded code. In the synthetic version-31 control, `Game.Player::.ctor` and `Update` resolve to `0x100000200` and `0x100000210`. Changing one metadata byte makes both tokens `0x06000002`; the unchanged native binary then produces `0x100000200` for both and the seed comment `# folded 0x100000200 Game.Player$$.ctor also Game.Player$$Update`. The actual `Update` slot remains present in the binary, but loses its seed. Duplicating a token across two types within the same image has the same result. ## Reproduction From the source revision below with locked dependencies installed, save this complete probe as `/tmp/rea-il2cpp-token-probe.mts` and run: ```sh node node_modules/tsx/dist/cli.mjs /tmp/rea-il2cpp-token-probe.mts ``` This exact probe ran once. It uses repository fixture generators, the real production composition/application/reader/seed writer, regular temporary files, and no mocked provider. It never loads or executes the native fixture. ```ts import { mkdtemp, writeFile, readFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { createHash } from 'node:crypto'; const { createIl2cppMetadataFixture } = await import(pathToFileURL(resolve('tests/fixtures/il2cpp/metadata.ts')).href); const { createIl2cppBinaryFixture } = await import(pathToFileURL(resolve('tests/fixtures/il2cpp/binary.ts')).href); const { createIl2cppMetadataService } = await import(pathToFileURL(resolve('src/composition/il2cpp.ts')).href); const directory = await mkdtemp(join(tmpdir(), 'rea-il2cpp-tokens-')); const fixture = createIl2cppMetadataFixture({ version: 31 }); // v31: name, declaring type, return type, return token, parameter start, // generic container, then the four-byte method token. const tokenAt = (index: number) => fixture.record('methods', index) + 24; const baselineBinary = createIl2cppBinaryFixture(); const binaryPath = join(directory, 'GameAssembly.dylib'); await writeFile(binaryPath, baselineBinary); const service = createIl2cppMetadataService(); try { for (const name of ['baseline', 'duplicate-same-type', 'duplicate-other-type', 'zero-token', 'out-of-range-token', 'real-shared-code']) { const metadata = Buffer.from(fixture.bytes); if (name === 'duplicate-same-type') metadata.writeUInt32LE(metadata.readUInt32LE(tokenAt(0)), tokenAt(1)); if (name === 'duplicate-other-type') metadata.writeUInt32LE(metadata.readUInt32LE(tokenAt(0)), tokenAt(2)); if (name === 'zero-token') metadata.writeUInt32LE(0, tokenAt(1)); if (name === 'out-of-range-token') metadata.writeUInt32LE(0x06000004, tokenAt(1)); const binary = Buffer.from(baselineBinary); if (name === 'real-shared-code') binary.writeBigUInt64LE(binary.readBigUInt64LE(0x308), 0x300); await writeFile(binaryPath, binary); const path = join(directory, name + '.dat'); const output = join(directory, name + '.tsv'); await writeFile(path, metadata); const result = await service.inspectWithGhidraSeeds({path, binary_path:binaryPath, detail:'complete'}, {path:output}); const tokens = Array.from({length:5}, (_, index) => '0x' + metadata.readUInt32LE(tokenAt(index)).toString(16)); if (result.ok) { const report = result.value.normalized_result; const seeds = await readFile(output, 'utf8'); console.log(JSON.stringify({name,ok:true,tokens,confidence:result.value.confidence,methods:report.methods,method_addresses:report.method_addresses,foldedLines:seeds.split('\n').filter(line=>line.startsWith('# folded')),functions:seeds.split('\n').filter(line=>line.includes('\tfunction\t')).length,binarySha256:createHash('sha256').update(binary).digest('hex'),changedMetadataOffsets:[...metadata.keys()].filter(index=>metadata[index]!==fixture.bytes[index])})); } else { console.log(JSON.stringify({name,ok:false,tokens,code:result.error.code,issues:result.error.details?.issues ?? result.error.issues,message:result.error.message})); } } } finally { await service.close(); } console.log(JSON.stringify({fixtureDirectory:directory})); ``` ## Expected behavior Refuse contradictory per-image method identity before publishing concrete native links or seeds. A MethodDef token identifies one metadata row within a module; it cannot identify two distinct method definitions. Identical code folding is different: two distinct tokens may legitimately select separate pointer slots holding the same address. Tokens repeated across different images must also remain valid. The [native-link contract](https://github.com/morluto/rea/blob/572d0c234089605f120dcfea340f11e38a3c3705/docs/il2cpp-metadata.md#native-method-addresses) uses each token row ID to select a module slot. Microsoft's [metadata-token explanation](https://learn.microsoft.com/en-us/dotnet/standard/metadata-and-self-describing-components#metadata-tokens) specifies table and row identity. ## Impact and scope P2: bounded acceptance of damaged metadata yields an incorrect successful native-link inventory and misleading persistent seed output. Both same-type and different-type duplicates within one image reproduced it. A genuine shared-code control with distinct tokens remains accepted and produces the same folded comment, so exported results do not distinguish the invalid identity from real folding. The valid baseline already repeats token values across the two different images; those repetitions are valid and are not this defect. Real-game incidence, other metadata versions, and actual Ghidra import behavior were not tested. No source artifact was modified by REA; the probe deliberately changes only its synthetic input. ## REA version Source commit `572d0c234089605f120dcfea340f11e38a3c3705`, package manifest `rea-agents@6.7.0`. This is unreleased main behavior: tag `rea-agents-6.7.0` at `27dc42d10249f0410904a75c0997842eb03b931c` contains no IL2CPP implementation files. The published 6.7.0 runtime was not reproduced. ## Runtime and platform Node.js v24.22.1; Linux 7.2.8-9-omarchy x86_64; repository-locked dependencies. No Unity, Hopper, Ghidra or IDA runtime was used. ## Target and artifact context Repository-generated version-31 metadata with two assembly images, four types and five methods, paired with a 4,096-byte thin arm64 Mach-O registration fixture. Its unchanged SHA-256 in both duplicate-token cases is `f3afa13e37c01bfe0dfbd0a6b5de28482284d7fddf9d3e3ecac723b31e71e47f`. ## Interface Other ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence Observed production-service and actual exported-file results: ```text case first image tokens result first three RVAs function seeds baseline [2, 1, 3] success [0x200,0x210,0x220] 4 duplicate-same-type [2, 2, 3] success [0x200,0x200,0x220] 3 duplicate-other-type [2, 1, 2] success [0x200,0x210,0x200] 3 zero-token [2, 0, 3] refused invalid-method-token out-of-range-token [2, 4, 3] refused invalid-method-token real-shared-code [2, 1, 3] success [0x200,0x200,0x220] 3 ``` The first three tokens above have table prefix `0x06`; zero-token actually writes `0`. The same-type case changes only metadata byte offset 458; the other-type case changes only offset 494. All four successful cases return `confidence: "derived"`. The shared-code control changes only the binary's slot 0 to equal slot 1; metadata remains unchanged. These observations establish application/producer/export behavior with synthetic files, not public CLI/MCP transport or real-provider behavior. ## Typed outcome or recovery expected Return a typed malformed-metadata/native-link refusal for duplicate `(image, method token)` identities, preserving available source evidence without a complete native table or seed file. Keep valid cross-image token reuse and distinct-token shared addresses working. A general preflight/recovery tool was not tested. ## Analysis or suspected cause Source-derived: [method decoding](https://github.com/morluto/rea/blob/572d0c234089605f120dcfea340f11e38a3c3705/src/il2cpp/Il2cppMetadataFile.ts#L610-L619) retains token values. [Native linking](https://github.com/morluto/rea/blob/572d0c234089605f120dcfea340f11e38a3c3705/src/il2cpp/Il2cppBinaryFile.ts#L94-L109) checks token kind and range but never uniqueness per image, then resolves both definitions through the same slot. [Seed export](https://github.com/morluto/rea/blob/572d0c234089605f120dcfea340f11e38a3c3705/src/il2cpp/Il2cppGhidraSeeds.ts#L183-L194) interprets any repeated resolved address as folding. ## Additional context The initial full open snapshot and live open/closed searches for `IL2CPP`, `IL2CPP token`, `IL2CPP duplicate` and `IL2CPP generic` found no matching report. Related #5112 introduced native linking; #5751 concerns generic-container section extents, #6409 artifact attribution, and #6534 array-kind rendering. They do not cover duplicate method identity. No last-known-good revision was established. No fix was attempted. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Two one-byte synthetic metadata corruptions within one image were accepted and published incorrect derived method addresses plus misleading folded-code seeds, while an unchanged native binary still held the distinct original slots. - Workaround status: unverified - Workaround basis: The trusted baseline produces correct links, but a general per-image token preflight or recovery workflow for affected user artifacts was not exercised.
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#3996
No description provided.