[GH-ISSUE #6873] [Bug] IL2CPP CodeRegistration search includes ABI padding in the module count #4210

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

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 ... --binary ... --json rejects an authored arm64 Mach-O after changing one byte of alignment padding following Il2CppCodeRegistration.codeGenModulesCount. The 32-bit count remains 1, and the module pointer array, metadata, methods, and all other bytes remain unchanged. The zero-padding control succeeds; the nonzero-padding input exits 1 with code-registration-not-found.

Reproduction

From the checkout with its locked dependencies and compiled runtime prepared, save this as probe.mjs in a disposable directory. Run node /path/to/probe.mjs "$PWD" from the checkout using Node.js 24.22.1. The inline compressed fixtures contain a complete 439-byte version-29 metadata file and a 2,176-byte thin arm64 Mach-O. Only Node built-ins are needed. The runner isolates CLI state and does not execute the target binary.

import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import {inflateSync} from 'node:zlib';
import {spawnSync} from 'node:child_process';
const repo = process.argv[2];
if (!repo) throw new Error('Pass the checkout directory');
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rea-padding-'));
const unpack = text => inflateSync(Buffer.from(text, 'base64'));
const metadata = unpack('eJxbL73xlywDEDAyQAASLQ6kxKF8ZFoBSJtD+fSgI4B0P5DWANLboeLUohkcCwr0UnJyGFxSc/MZQlKLSxiCSvMYhBkQ4D8UAHWwwcT4gJgTTR4GYGLY5JDMwgBAMSZGVD6KOch8APCOVUI=');
const control = unpack('eJw7/+vtPx4GBkYGIGBjgDA8GBBAEsqPjw9xjQhhQAFgTQKoYg0cEJoVigc7cCwo0EvJyRloZwwYOMAcf22g3TCggAmajkcqYIT4Hx4GzCMsPFhGmH/RANzvrCMzHATYcfl7JIYGYQAPFRzhBhNzYEctVwJwqAcAEasMdQ==');
const padded = Buffer.from(control);
padded[0x604] = 1; // Padding after uint32 at 0x600, before pointer at 0x608.
fs.writeFileSync(path.join(dir, 'global-metadata.dat'), metadata);
for (const name of ['home', 'config', 'cache', 'data', 'state', 'tmp'])
  fs.mkdirSync(path.join(dir, name));
const env = {...process.env, HOME:path.join(dir,'home'),
  XDG_CONFIG_HOME:path.join(dir,'config'), XDG_CACHE_HOME:path.join(dir,'cache'),
  XDG_DATA_HOME:path.join(dir,'data'), XDG_STATE_HOME:path.join(dir,'state'),
  TMPDIR:path.join(dir,'tmp'), REA_LOG_LEVEL:'silent'};
for (const [name, bytes] of [['control', control], ['padding', padded]]) {
  const binary = path.join(dir, name + '.dylib');
  fs.writeFileSync(binary, bytes);
  const result = spawnSync(process.execPath, [path.join(repo,'scripts/rea.mjs'),
    'inspect-il2cpp-metadata', path.join(dir,'global-metadata.dat'),
    '--binary', binary, '--json'], {cwd:repo,env,encoding:'utf8'});
  const output = JSON.parse(result.stdout);
  console.log(JSON.stringify({name, exit:result.status,
    count32:bytes.readUInt32LE(0x600),
    countWithPadding:bytes.readBigUInt64LE(0x600).toString(),
    moduleArray:'0x'+bytes.readBigUInt64LE(0x608).toString(16),
    code:output.code,
    reason:output.details?.issues?.[0]?.message.split(': ').slice(-2).join(': '),
    methods:output.normalized_result?.method_addresses,
    stderr:result.stderr}));
}

The modified input reproduced once in the reduced one-module fixture. A separate earlier two-module fixture also showed the same refusal after changing only padding byte 0x604; its control succeeded. The one-module zero-padding control had already succeeded during the preceding fixture investigation; it was not rerun after a draft was saved. The script above combines the independently observed control and variant into a procedure maintainers can run.

Expected behavior

Both inputs should resolve the same one-module native inventory. Il2CppCodeRegistration.codeGenModulesCount is a uint32_t; the following four bytes align the next pointer and are not part of that count. The Unity 2022.3.8f1 structure declaration declares the count followed by const Il2CppCodeGenModule** codeGenModules. Native registration discovery should interpret the declared field width independently of padding contents.

REA's readUInt32 helper in src/il2cpp/Il2cppBinaryImage.ts likewise explicitly distinguishes a UInt32 field's four owned bytes from unrelated pointer-alignment padding. The native-linking guide describes resolving the module array from the registration's count and pointer.

Impact and scope

Native linking is refused for the authored nonzero-padding records despite unchanged declared counts and pointers. The demonstrated failure is bounded to synthetic inputs. No real Unity-generated binary with nonzero padding was examined, and frequency in compiler output is unknown. No target code, Unity runtime, or live loader was executed. This report concerns field interpretation, with no security claim.

REA version

Source commit 8d8d85ba22463b80715a1af1fdb51acc9b5d760b (package version 6.7.0).

Runtime and platform

Node.js v24.22.1; Linux 7.2.8-9-omarchy; x86_64. No external reverse-engineering provider was used.

Target and artifact context

Authored version-29 IL2CPP metadata and a thin arm64 Mach-O with one code-generation module and one method. The complete registration and referenced tables are file-backed. At file offset 0x600, both variants contain UInt32 count 1; 0x604 is padding, and 0x608 points to the module array at 0x100000500.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed zero-padding control: exit 0, empty stderr, one method address { "state": "code", "address": "0x100000200", "rva": "0x200" } and return type void.

Observed reduced padding variant: exit 1, empty stderr, code: "invalid_request", and a binary_path issue ending in:

code-registration-not-found: 0 CodeRegistration module tables list the 1 located modules

The only changed byte is 0x604: 00 -> 01. Reading four bytes at 0x600 yields 1 for both files. Reading eight bytes yields 1 for the control and 4294967297 for the variant. Their module-array pointer is identical. These facts come from the saved fixture bytes and CLI outputs; no post-draft runtime check was performed.

Typed outcome or recovery expected

Successful IL2CPP metadata Evidence containing the same module and method inventory as the control, rather than an invalid-format refusal of the binary/metadata association.

Analysis or suspected cause

Source-derived: findCodeRegistration in src/il2cpp/Il2cppBinaryRegistration.ts, line 218, compares image.bytes.readBigUInt64LE(offset) with the located module count. This includes the four bytes of padding after the UInt32 count. The pointer is read separately at offset + 8, so ignoring padding for the count would preserve its alignment. No fix was implemented.

Additional context

NOT_ENOUGH_INFO: no real-producer occurrence, last-known-good revision, MCP reproduction, or identity-preserving workaround was established. Metadata-only inspection does not provide the requested native facts. The zero-padding control changes the artifact and is not presented as a verified recovery workflow. Prepared with AI assistance.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Two authored fixtures with unchanged count/pointer fields are refused solely when padding is nonzero; the reduced public CLI case blocks native results. Real-producer occurrence and wider reach remain unknown.
  • Workaround status: unverified
  • Workaround basis: Metadata-only inspection omits the native inventory, and changing padding changes artifact identity. No identity-preserving recovery was tested.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6873 ### 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 ... --binary ... --json` rejects an authored arm64 Mach-O after changing one byte of alignment padding following `Il2CppCodeRegistration.codeGenModulesCount`. The 32-bit count remains 1, and the module pointer array, metadata, methods, and all other bytes remain unchanged. The zero-padding control succeeds; the nonzero-padding input exits 1 with `code-registration-not-found`. ### Reproduction From the checkout with its locked dependencies and compiled runtime prepared, save this as `probe.mjs` in a disposable directory. Run `node /path/to/probe.mjs "$PWD"` from the checkout using Node.js 24.22.1. The inline compressed fixtures contain a complete 439-byte version-29 metadata file and a 2,176-byte thin arm64 Mach-O. Only Node built-ins are needed. The runner isolates CLI state and does not execute the target binary. ```js import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import {inflateSync} from 'node:zlib'; import {spawnSync} from 'node:child_process'; const repo = process.argv[2]; if (!repo) throw new Error('Pass the checkout directory'); const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rea-padding-')); const unpack = text => inflateSync(Buffer.from(text, 'base64')); const metadata = unpack('eJxbL73xlywDEDAyQAASLQ6kxKF8ZFoBSJtD+fSgI4B0P5DWANLboeLUohkcCwr0UnJyGFxSc/MZQlKLSxiCSvMYhBkQ4D8UAHWwwcT4gJgTTR4GYGLY5JDMwgBAMSZGVD6KOch8APCOVUI='); const control = unpack('eJw7/+vtPx4GBkYGIGBjgDA8GBBAEsqPjw9xjQhhQAFgTQKoYg0cEJoVigc7cCwo0EvJyRloZwwYOMAcf22g3TCggAmajkcqYIT4Hx4GzCMsPFhGmH/RANzvrCMzHATYcfl7JIYGYQAPFRzhBhNzYEctVwJwqAcAEasMdQ=='); const padded = Buffer.from(control); padded[0x604] = 1; // Padding after uint32 at 0x600, before pointer at 0x608. fs.writeFileSync(path.join(dir, 'global-metadata.dat'), metadata); for (const name of ['home', 'config', 'cache', 'data', 'state', 'tmp']) fs.mkdirSync(path.join(dir, name)); const env = {...process.env, HOME:path.join(dir,'home'), XDG_CONFIG_HOME:path.join(dir,'config'), XDG_CACHE_HOME:path.join(dir,'cache'), XDG_DATA_HOME:path.join(dir,'data'), XDG_STATE_HOME:path.join(dir,'state'), TMPDIR:path.join(dir,'tmp'), REA_LOG_LEVEL:'silent'}; for (const [name, bytes] of [['control', control], ['padding', padded]]) { const binary = path.join(dir, name + '.dylib'); fs.writeFileSync(binary, bytes); const result = spawnSync(process.execPath, [path.join(repo,'scripts/rea.mjs'), 'inspect-il2cpp-metadata', path.join(dir,'global-metadata.dat'), '--binary', binary, '--json'], {cwd:repo,env,encoding:'utf8'}); const output = JSON.parse(result.stdout); console.log(JSON.stringify({name, exit:result.status, count32:bytes.readUInt32LE(0x600), countWithPadding:bytes.readBigUInt64LE(0x600).toString(), moduleArray:'0x'+bytes.readBigUInt64LE(0x608).toString(16), code:output.code, reason:output.details?.issues?.[0]?.message.split(': ').slice(-2).join(': '), methods:output.normalized_result?.method_addresses, stderr:result.stderr})); } ``` The modified input reproduced once in the reduced one-module fixture. A separate earlier two-module fixture also showed the same refusal after changing only padding byte `0x604`; its control succeeded. The one-module zero-padding control had already succeeded during the preceding fixture investigation; it was not rerun after a draft was saved. The script above combines the independently observed control and variant into a procedure maintainers can run. ### Expected behavior Both inputs should resolve the same one-module native inventory. `Il2CppCodeRegistration.codeGenModulesCount` is a `uint32_t`; the following four bytes align the next pointer and are not part of that count. The [Unity 2022.3.8f1 structure declaration](https://github.com/MlgmXyysd/libil2cpp/blob/master/libil2cpp/Unity_2022.3/2022.3.8f1/il2cpp-class-internals.h#L596-L615) declares the count followed by `const Il2CppCodeGenModule** codeGenModules`. Native registration discovery should interpret the declared field width independently of padding contents. REA's `readUInt32` helper in `src/il2cpp/Il2cppBinaryImage.ts` likewise explicitly distinguishes a UInt32 field's four owned bytes from unrelated pointer-alignment padding. The [native-linking guide](https://github.com/morluto/rea/blob/main/docs/il2cpp-metadata.md#native-method-addresses) describes resolving the module array from the registration's count and pointer. ### Impact and scope Native linking is refused for the authored nonzero-padding records despite unchanged declared counts and pointers. The demonstrated failure is bounded to synthetic inputs. No real Unity-generated binary with nonzero padding was examined, and frequency in compiler output is unknown. No target code, Unity runtime, or live loader was executed. This report concerns field interpretation, with no security claim. ### REA version Source commit `8d8d85ba22463b80715a1af1fdb51acc9b5d760b` (package version 6.7.0). ### Runtime and platform Node.js v24.22.1; Linux 7.2.8-9-omarchy; x86_64. No external reverse-engineering provider was used. ### Target and artifact context Authored version-29 IL2CPP metadata and a thin arm64 Mach-O with one code-generation module and one method. The complete registration and referenced tables are file-backed. At file offset `0x600`, both variants contain UInt32 count 1; `0x604` is padding, and `0x608` points to the module array at `0x100000500`. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed zero-padding control: exit 0, empty stderr, one method address `{ "state": "code", "address": "0x100000200", "rva": "0x200" }` and return type `void`. Observed reduced padding variant: exit 1, empty stderr, `code: "invalid_request"`, and a `binary_path` issue ending in: ```text code-registration-not-found: 0 CodeRegistration module tables list the 1 located modules ``` The only changed byte is `0x604: 00 -> 01`. Reading four bytes at `0x600` yields 1 for both files. Reading eight bytes yields 1 for the control and 4294967297 for the variant. Their module-array pointer is identical. These facts come from the saved fixture bytes and CLI outputs; no post-draft runtime check was performed. ### Typed outcome or recovery expected Successful IL2CPP metadata Evidence containing the same module and method inventory as the control, rather than an invalid-format refusal of the binary/metadata association. ### Analysis or suspected cause Source-derived: `findCodeRegistration` in `src/il2cpp/Il2cppBinaryRegistration.ts`, line 218, compares `image.bytes.readBigUInt64LE(offset)` with the located module count. This includes the four bytes of padding after the UInt32 count. The pointer is read separately at `offset + 8`, so ignoring padding for the count would preserve its alignment. No fix was implemented. ### Additional context NOT_ENOUGH_INFO: no real-producer occurrence, last-known-good revision, MCP reproduction, or identity-preserving workaround was established. Metadata-only inspection does not provide the requested native facts. The zero-padding control changes the artifact and is not presented as a verified recovery workflow. Prepared with AI assistance. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Two authored fixtures with unchanged count/pointer fields are refused solely when padding is nonzero; the reduced public CLI case blocks native results. Real-producer occurrence and wider reach remain unknown. - Workaround status: unverified - Workaround basis: Metadata-only inspection omits the native inventory, and changing padding changes artifact identity. No identity-preserving recovery was tested.
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#4210
No description provided.