[GH-ISSUE #6701] [Bug] IL2CPP loses method addresses beside non-overlapping 32-bit ELF relocations #4074

Closed
opened 2026-10-31 00:46:36 +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/6701

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

A four-byte ELF relocation ending immediately before an IL2CPP method-pointer slot makes that separate slot unknown. Inspection succeeds, but the known method address disappears and Ghidra seed generation omits that method. The control returns four function seeds; adding the adjacent PC32 or ABS32 relocation returns three.

Observed through the production Il2cppMetadataProvider and il2cppGhidraSeeds on synthetic x86-64 ELF registration fixtures. CLI, MCP, Unity-generated artifacts and Ghidra execution were not tested.

Reproduction

From the source revision below with locked dependencies installed, run the following complete probe. It uses repository-owned fixture builders, creates only temporary synthetic files, and never executes a target or native analysis engine.

node --import tsx --input-type=module <<'JS'
import { mkdtemp, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createIl2cppMetadataFixture } from './tests/fixtures/il2cpp/metadata.ts';
import { createIl2cppBinaryFixture, createElfFixture,
  FIXTURE_BINARY_BASE as base } from './tests/fixtures/il2cpp/binary.ts';
import { Il2cppMetadataProvider } from './src/il2cpp/Il2cppMetadataProvider.ts';
import { il2cppGhidraSeeds } from './src/il2cpp/Il2cppGhidraSeeds.ts';
const dir = await mkdtemp(join(tmpdir(), 'rea-relocation-width-'));
const path = join(dir, 'global-metadata.dat');
await writeFile(path, createIl2cppMetadataFixture({ version: 31 }).bytes);
const original = createElfFixture(createIl2cppBinaryFixture(), { machine: 62 });
function addRelocation(offset, kind) {
  const oldDynamic = Number(original.readBigUInt64LE(128));
  const record = Buffer.alloc(24);
  record.writeBigUInt64LE(base + BigInt(offset), 0);
  record.writeBigUInt64LE((1n << 32n) | BigInt(kind), 8);
  const bytes = Buffer.concat([original.subarray(0, oldDynamic), record,
    original.subarray(oldDynamic)]);
  const dynamic = oldDynamic + 24;
  bytes.writeBigUInt64LE(BigInt(bytes.length), 96);
  bytes.writeBigUInt64LE(BigInt(bytes.length), 104);
  bytes.writeBigUInt64LE(BigInt(dynamic), 128);
  bytes.writeBigUInt64LE(base + BigInt(dynamic), 136);
  bytes.writeBigUInt64LE(base + BigInt(dynamic), 144);
  bytes.writeBigUInt64LE(original.readBigUInt64LE(oldDynamic + 24) + 24n,
    dynamic + 24);
  return bytes;
}
console.log(JSON.stringify({ node: process.version, platform: process.platform,
  architecture: process.arch }));
const provider = new Il2cppMetadataProvider();
try {
  for (const [name, bytes] of [
    ['control', original],
    ['PC32 ends four bytes before slot', addRelocation(0x2f8, 2)],
    ['PC32 ends exactly at slot', addRelocation(0x2fc, 2)],
    ['ABS32 ends exactly at slot', addRelocation(0x2fc, 10)],
    ['ABS64 overlaps slot', addRelocation(0x2fc, 1)],
  ]) {
    const binary_path = join(dir, name + '.so');
    await writeFile(binary_path, bytes);
    const result = await provider.inspect({ path, binary_path });
    if (!result.ok) throw result.error;
    const report = result.value;
    const seeds = il2cppGhidraSeeds(report, BigInt(report.binary.image_base));
    console.log(JSON.stringify({ name, method: report.methods[1].name,
      address: report.method_addresses[1], functions: seeds.functions,
      unknown: seeds.unknown, hasUpdateSeed: seeds.text.includes('Game.Player$$Update') }));
  }
} finally { await provider.close(); }
JS

The method slot occupies [0x100000300, 0x100000308). The adjacent four-byte relocation occupies [0x1000002fc, 0x100000300), so these ranges do not overlap. Each listed case was observed in this complete probe; additional focused reader/provider probes gave the same adjacent-relocation result before this draft was written.

Expected behavior

Preserve Update at 0x100000210 and its function seed when only a disjoint neighboring field is relocated. An unresolved relocation that actually overlaps the pointer should still yield unknown.

The AMD64 ABI relocation table defines PC32 and ABS32 as word32, unlike ABS64's word64. REA's documented contract describes unknown when the method's slot has an unresolved relocation. Here the unresolved field is adjacent to that slot.

Impact and scope

The reproduced inspection loses an independently resolved method address and its generated Ghidra seed. All other method addresses remain available. Reach in Unity-produced binaries is NOT_ENOUGH_INFO: no such binary was tested. AArch64, other relocation kinds, CLI/MCP delivery and real Ghidra import remain untested. The input artifact is not modified.

REA version

Source main 8c4f96ef4759fd78da66c4b9726825ca71d7ee0c; package version 6.7.0.

Runtime and platform

Node.js v24.22.1; Linux x86_64. Locked repository dependencies; no external native analysis provider.

Target and artifact context

Synthetic metadata version 31 and 64-bit little-endian x86-64 ELF registration data from the repository fixture builders, extended with one Elf64_Rela record. The fixtures exercise the static parser; they were not executed by a loader and do not establish compiler-generated artifact prevalence or full loader validity.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Exact output from the inline probe above:

{"node":"v24.22.1","platform":"linux","architecture":"x64"}
{"name":"control","method":"Update","address":{"state":"code","address":"0x100000210","rva":"0x210"},"functions":4,"unknown":0,"hasUpdateSeed":true}
{"name":"PC32 ends four bytes before slot","method":"Update","address":{"state":"code","address":"0x100000210","rva":"0x210"},"functions":4,"unknown":0,"hasUpdateSeed":true}
{"name":"PC32 ends exactly at slot","method":"Update","address":{"state":"unknown","address":null,"rva":null},"functions":3,"unknown":1,"hasUpdateSeed":false}
{"name":"ABS32 ends exactly at slot","method":"Update","address":{"state":"unknown","address":null,"rva":null},"functions":3,"unknown":1,"hasUpdateSeed":false}
{"name":"ABS64 overlaps slot","method":"Update","address":{"state":"unknown","address":null,"rva":null},"functions":3,"unknown":1,"hasUpdateSeed":false}

PC32 ends four bytes before slot retains the address. Moving that four-byte field forward four bytes, so it ends exactly at the slot, loses it. ABS32 shows the same loss; the overlapping ABS64 control correctly remains unknown.

Typed outcome or recovery expected

The disjoint PC32/ABS32 cases should retain { "state": "code", "address": "0x100000210", "rva": "0x210" }, four function seeds, and zero unknown methods. This is an expected correction, not a tested fix.

Analysis or suspected cause

Source-derived: ElfRelocations.ts records only each unresolved relocation's start and clears eight bytes regardless of kind. Il2cppBinaryImage.ts then treats every recorded relocation as potentially eight bytes wide. The ABI gives these particular kinds a known four-byte width; losing that width makes the neighboring pointer appear unresolved.

Additional context

Open and closed duplicate searches included IL2CPP in:title, IL2CPP relocation, and relocation adjacent. No matching report was found. NOT_ENOUGH_INFO: no last-known-good version or real Unity artifact showing the same layout was identified.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Production static inspection succeeds while losing one resolved method address and its generated function seed in the bounded synthetic x86-64 ELF case; prevalence in compiler-produced inputs is unknown.
  • Workaround status: unverified
  • Workaround basis: An external ELF/IL2CPP reader could potentially recover the address, but no alternative workflow was executed. The control only isolates the cause and is not a workaround for an existing selected artifact.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6701 ### 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 Evidence, artifacts, or reconstruction ### Observed behavior A four-byte ELF relocation ending immediately before an IL2CPP method-pointer slot makes that separate slot `unknown`. Inspection succeeds, but the known method address disappears and Ghidra seed generation omits that method. The control returns four function seeds; adding the adjacent PC32 or ABS32 relocation returns three. Observed through the production `Il2cppMetadataProvider` and `il2cppGhidraSeeds` on synthetic x86-64 ELF registration fixtures. CLI, MCP, Unity-generated artifacts and Ghidra execution were not tested. ### Reproduction From the source revision below with locked dependencies installed, run the following complete probe. It uses repository-owned fixture builders, creates only temporary synthetic files, and never executes a target or native analysis engine. ```sh node --import tsx --input-type=module <<'JS' import { mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createIl2cppMetadataFixture } from './tests/fixtures/il2cpp/metadata.ts'; import { createIl2cppBinaryFixture, createElfFixture, FIXTURE_BINARY_BASE as base } from './tests/fixtures/il2cpp/binary.ts'; import { Il2cppMetadataProvider } from './src/il2cpp/Il2cppMetadataProvider.ts'; import { il2cppGhidraSeeds } from './src/il2cpp/Il2cppGhidraSeeds.ts'; const dir = await mkdtemp(join(tmpdir(), 'rea-relocation-width-')); const path = join(dir, 'global-metadata.dat'); await writeFile(path, createIl2cppMetadataFixture({ version: 31 }).bytes); const original = createElfFixture(createIl2cppBinaryFixture(), { machine: 62 }); function addRelocation(offset, kind) { const oldDynamic = Number(original.readBigUInt64LE(128)); const record = Buffer.alloc(24); record.writeBigUInt64LE(base + BigInt(offset), 0); record.writeBigUInt64LE((1n << 32n) | BigInt(kind), 8); const bytes = Buffer.concat([original.subarray(0, oldDynamic), record, original.subarray(oldDynamic)]); const dynamic = oldDynamic + 24; bytes.writeBigUInt64LE(BigInt(bytes.length), 96); bytes.writeBigUInt64LE(BigInt(bytes.length), 104); bytes.writeBigUInt64LE(BigInt(dynamic), 128); bytes.writeBigUInt64LE(base + BigInt(dynamic), 136); bytes.writeBigUInt64LE(base + BigInt(dynamic), 144); bytes.writeBigUInt64LE(original.readBigUInt64LE(oldDynamic + 24) + 24n, dynamic + 24); return bytes; } console.log(JSON.stringify({ node: process.version, platform: process.platform, architecture: process.arch })); const provider = new Il2cppMetadataProvider(); try { for (const [name, bytes] of [ ['control', original], ['PC32 ends four bytes before slot', addRelocation(0x2f8, 2)], ['PC32 ends exactly at slot', addRelocation(0x2fc, 2)], ['ABS32 ends exactly at slot', addRelocation(0x2fc, 10)], ['ABS64 overlaps slot', addRelocation(0x2fc, 1)], ]) { const binary_path = join(dir, name + '.so'); await writeFile(binary_path, bytes); const result = await provider.inspect({ path, binary_path }); if (!result.ok) throw result.error; const report = result.value; const seeds = il2cppGhidraSeeds(report, BigInt(report.binary.image_base)); console.log(JSON.stringify({ name, method: report.methods[1].name, address: report.method_addresses[1], functions: seeds.functions, unknown: seeds.unknown, hasUpdateSeed: seeds.text.includes('Game.Player$$Update') })); } } finally { await provider.close(); } JS ``` The method slot occupies `[0x100000300, 0x100000308)`. The adjacent four-byte relocation occupies `[0x1000002fc, 0x100000300)`, so these ranges do not overlap. Each listed case was observed in this complete probe; additional focused reader/provider probes gave the same adjacent-relocation result before this draft was written. ### Expected behavior Preserve `Update` at `0x100000210` and its function seed when only a disjoint neighboring field is relocated. An unresolved relocation that actually overlaps the pointer should still yield `unknown`. The [AMD64 ABI relocation table](https://gitlab.com/x86-psABIs/x86-64-ABI/-/blob/master/x86-64-ABI/object-files.tex#L423-434) defines PC32 and ABS32 as `word32`, unlike ABS64's `word64`. REA's [documented contract](https://github.com/morluto/rea/blob/8c4f96ef4759fd78da66c4b9726825ca71d7ee0c/docs/il2cpp-metadata.md#L241-L248) describes `unknown` when the method's slot has an unresolved relocation. Here the unresolved field is adjacent to that slot. ### Impact and scope The reproduced inspection loses an independently resolved method address and its generated Ghidra seed. All other method addresses remain available. Reach in Unity-produced binaries is NOT_ENOUGH_INFO: no such binary was tested. AArch64, other relocation kinds, CLI/MCP delivery and real Ghidra import remain untested. The input artifact is not modified. ### REA version Source main `8c4f96ef4759fd78da66c4b9726825ca71d7ee0c`; package version 6.7.0. ### Runtime and platform Node.js v24.22.1; Linux x86_64. Locked repository dependencies; no external native analysis provider. ### Target and artifact context Synthetic metadata version 31 and 64-bit little-endian x86-64 ELF registration data from the repository fixture builders, extended with one Elf64_Rela record. The fixtures exercise the static parser; they were not executed by a loader and do not establish compiler-generated artifact prevalence or full loader validity. ### Interface Other ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Exact output from the inline probe above: ```jsonl {"node":"v24.22.1","platform":"linux","architecture":"x64"} {"name":"control","method":"Update","address":{"state":"code","address":"0x100000210","rva":"0x210"},"functions":4,"unknown":0,"hasUpdateSeed":true} {"name":"PC32 ends four bytes before slot","method":"Update","address":{"state":"code","address":"0x100000210","rva":"0x210"},"functions":4,"unknown":0,"hasUpdateSeed":true} {"name":"PC32 ends exactly at slot","method":"Update","address":{"state":"unknown","address":null,"rva":null},"functions":3,"unknown":1,"hasUpdateSeed":false} {"name":"ABS32 ends exactly at slot","method":"Update","address":{"state":"unknown","address":null,"rva":null},"functions":3,"unknown":1,"hasUpdateSeed":false} {"name":"ABS64 overlaps slot","method":"Update","address":{"state":"unknown","address":null,"rva":null},"functions":3,"unknown":1,"hasUpdateSeed":false} ``` `PC32 ends four bytes before slot` retains the address. Moving that four-byte field forward four bytes, so it ends exactly at the slot, loses it. ABS32 shows the same loss; the overlapping ABS64 control correctly remains unknown. ### Typed outcome or recovery expected The disjoint PC32/ABS32 cases should retain `{ "state": "code", "address": "0x100000210", "rva": "0x210" }`, four function seeds, and zero unknown methods. This is an expected correction, not a tested fix. ### Analysis or suspected cause Source-derived: [ElfRelocations.ts](https://github.com/morluto/rea/blob/8c4f96ef4759fd78da66c4b9726825ca71d7ee0c/src/il2cpp/ElfRelocations.ts#L151-L162) records only each unresolved relocation's start and clears eight bytes regardless of kind. [Il2cppBinaryImage.ts](https://github.com/morluto/rea/blob/8c4f96ef4759fd78da66c4b9726825ca71d7ee0c/src/il2cpp/Il2cppBinaryImage.ts#L292-L310) then treats every recorded relocation as potentially eight bytes wide. The ABI gives these particular kinds a known four-byte width; losing that width makes the neighboring pointer appear unresolved. ### Additional context Open and closed duplicate searches included `IL2CPP in:title`, `IL2CPP relocation`, and `relocation adjacent`. No matching report was found. NOT_ENOUGH_INFO: no last-known-good version or real Unity artifact showing the same layout was identified. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Production static inspection succeeds while losing one resolved method address and its generated function seed in the bounded synthetic x86-64 ELF case; prevalence in compiler-produced inputs is unknown. - Workaround status: unverified - Workaround basis: An external ELF/IL2CPP reader could potentially recover the address, but no alternative workflow was executed. The control only isolates the cause and is not a workaround for an existing selected artifact.
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#4074
No description provided.