[GH-ISSUE #3202] [Bug] Zero TypeDef list starts produce contradictory member ownership with complete coverage #1140

Closed
opened 2026-10-15 18:31:25 +00:00 by bsakaguchi · 2 comments
Owner

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

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

node scripts/rea.mjs inspect-managed-members <file> --format json accepts a generated managed PE32/CLI file whose Fixture.Program TypeDef has zero FieldList and MethodList starts. Its preceding type, Fixture.Empty2, reports row 1 in both lists, while the sole field and method still report Fixture.Program as their declaring type. Fixture.Program reports empty lists. The result reports coverage.state: "complete" and issues: [] despite this contradiction.

Both control and modified CLI inspections exited with status 0. The unmodified control puts the sole field and method in Fixture.Program's lists, and both members agree with that owner. Only the low bytes of the two 16-bit list starts changed between the files. The managed static provider inspected the bytes without loading or executing target code.

Reproduction

  1. Start at the stated source revision with Node.js 26 and the repository's pinned npm version. Prepare the clean checkout:
npm ci
npm run build:cached
  1. From the repository root, create isolated reproduction directories and direct configuration into them:
mkdir -p .repro/home/config .repro/home/cache .repro/home/data
export HOME="$PWD/.repro/home"
export XDG_CONFIG_HOME="$HOME/config"
export XDG_CACHE_HOME="$HOME/cache"
export XDG_DATA_HOME="$HOME/data"
  1. Run this inline script. It creates a control file, inspects it through the CLI, copies it with both list starts changed from 1 to 0, inspects the copy through the same CLI, and prints the relevant normalized results. Native TypeScript imports require the Node.js version stated above. The layout assertions ensure that only the intended coordinates are changed.
node --input-type=module <<'JS'
import assert from 'node:assert/strict';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { buildManagedPeFixture } from './src/dotnet/ManagedPe.fixture.ts';

const dir = process.env.REPRO_DIR ?? '.repro';
mkdirSync(dir, { recursive: true });
const inspect = (name) => {
  const run = spawnSync(process.execPath, [
    'scripts/rea.mjs', 'inspect-managed-members', `${dir}/${name}.dll`,
    '--format', 'json',
  ], { encoding: 'utf8' });
  assert.equal(run.status, 0, run.stderr || run.stdout);
  const envelope = JSON.parse(run.stdout);
  writeFileSync(`${dir}/${name}.json`, run.stdout);
  return envelope.normalized_result;
};

const bytes = buildManagedPeFixture({ additionalEmptyTypes: 3 });
writeFileSync(`${dir}/control.dll`, bytes);
const control = inspect('control');
const row = control.types.find((type) => type.full_name === 'Fixture.Program');
assert.equal(row.row_offset, 986);
assert.equal(bytes.readUInt16LE(row.row_offset + 10), 1);
assert.equal(bytes.readUInt16LE(row.row_offset + 12), 1);
const changed = Buffer.from(bytes);
changed.writeUInt16LE(0, row.row_offset + 10);
changed.writeUInt16LE(0, row.row_offset + 12);
writeFileSync(`${dir}/zero-list.dll`, changed);
const modified = inspect('zero-list');

const summarize = (result) => ({
  types: result.types.slice(-2).map(({ full_name, field_list, method_list }) =>
    ({ full_name, field_list, method_list })),
  fields: result.fields.map(({ token, declaring_type, declaring_type_token }) =>
    ({ token, declaring_type, declaring_type_token })),
  methods: result.methods.map(({ token, declaring_type, declaring_type_token }) =>
    ({ token, declaring_type, declaring_type_token })),
  coverage: result.coverage,
});
console.log(JSON.stringify({ control: summarize(control), modified: summarize(modified) }, null, 2));
JS
  1. Compare the printed lists with the printed declaring_type values and coverage. The script stores both complete CLI results in the reproduction directory for inspection.

The independent verifier replayed the inline reproduction using prepared dependencies and the compiled runtime. npm ci and npm run build:cached are clean-checkout prerequisites, not installation/build commands rerun during that verification.

Expected behavior

docs/managed-code-analysis.md:310-314 promises that missing or malformed facts remain explicit in coverage and per-member status. Lines 323-330 document retained field_list and method_list coordinates and distinguish pointer-table list positions from physical definition rows.

This fixture has ordinary Field and MethodDef tables and no pointer tables. Its list positions refer directly to definition rows. Zero starts in these nonempty, one-based lists must produce an explicit malformed-fact signal. The result must preserve readable member facts and identify unresolved ownership rather than certify conflicting ownership as complete. The contract does not require a particular repair algorithm.

Impact and scope

P2. Consumers of managed member inspection can attribute the same field or method to different types depending on whether they read the type's lists or the member's declaring_type. Complete coverage gives them no warning about the malformed coordinates or disagreement. The verified scope is a bounded malformed-metadata input through the CLI, with no pointer tables. No crash, data loss, target mutation, or security boundary crossing was observed. No safe alternative workflow was verified that preserves this input's member facts and establishes ownership or identifies the malformed coordinates.

REA version

6.4.0 (main at 5830088520)

Runtime and platform

Node.js v26.10.0; macOS arm64.

Target and artifact context

Generated PE32/CLI bytes from src/dotnet/ManagedPe.fixture.ts, with four TypeDef rows, one Field row, one MethodDef row, and no pointer tables. additionalEmptyTypes: 3 creates the control. The reproduction derives Fixture.Program's row offset from control inspection, asserts offset 986 and starts of 1, then changes the starts at offsets 996 and 998 to 0 in a copy. The original input stays unchanged and neither assembly is executed.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Modified normalized_result excerpts from the reproduced CLI result:

{
  "types": [
    {
      "full_name": "Fixture.Empty2",
      "field_list": { "first_row": 1, "last_row": 1, "count": 1 },
      "method_list": { "first_row": 1, "last_row": 1, "count": 1 }
    },
    {
      "full_name": "Fixture.Program",
      "field_list": { "first_row": null, "last_row": null, "count": 0 },
      "method_list": { "first_row": null, "last_row": null, "count": 0 }
    }
  ],
  "fields": [
    { "token": "0x04000001", "declaring_type": "Fixture.Program", "declaring_type_token": "0x02000004" }
  ],
  "methods": [
    { "token": "0x06000001", "declaring_type": "Fixture.Program", "declaring_type_token": "0x02000004" }
  ],
  "coverage": { "state": "complete", "issues": [] }
}

Typed outcome or recovery expected

The modified input must identify its malformed list coordinates in coverage and must not present the conflicting ownership as complete. Preserve readable field and method facts and make ownership uncertainty explicit wherever the input cannot support it. Consistent normalization alone does not restore the documented contract if it erases the malformed-fact signal.

The control must retain complete coverage with no issues, empty lists for Fixture.Empty2, row 1 in both lists for Fixture.Program, and both members declared by Fixture.Program.

Analysis or suspected cause

Source-derived reasoning at the reported baseline explains the reproduced disagreement. src/dotnet/ManagedMemberInspectorCore.ts:81-95 treats a zero next start as the end of the member table and a zero current start as an empty range; its parseTypes call sites at lines 196-208 construct the displayed lists. src/dotnet/ManagedMemberOwnership.ts:64-76 treats a zero next start as a literal upper bound and clamps a zero current start to 1.

src/dotnet/ManagedMemberRows.ts:61,96-97,288,352-353 consumes that ownership lookup for field and method identities. src/dotnet/ManagedMemberInspector.ts:278-283 derives complete coverage when no issues or incomplete members were recorded. This explains differing range interpretations; the conflicting CLI output and empty coverage issues were directly reproduced.

Additional context

Related reports and changes are #1953, #1955 and #1886. The first two address lookup performance while preserving malformed first-match behavior, and #1886 addresses pointer-table ownership; this report concerns zero TypeDef list starts without pointer tables and requires an explicit malformed-coordinate signal plus consistent ownership evidence.

The P2 assessment reflects misleading ownership and missing uncertainty on a bounded malformed-input case. Workaround availability remains unverified.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3202 ### 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 `node scripts/rea.mjs inspect-managed-members <file> --format json` accepts a generated managed PE32/CLI file whose `Fixture.Program` TypeDef has zero `FieldList` and `MethodList` starts. Its preceding type, `Fixture.Empty2`, reports row 1 in both lists, while the sole field and method still report `Fixture.Program` as their declaring type. `Fixture.Program` reports empty lists. The result reports `coverage.state: "complete"` and `issues: []` despite this contradiction. Both control and modified CLI inspections exited with status 0. The unmodified control puts the sole field and method in `Fixture.Program`'s lists, and both members agree with that owner. Only the low bytes of the two 16-bit list starts changed between the files. The managed static provider inspected the bytes without loading or executing target code. ### Reproduction 1. Start at the stated source revision with Node.js 26 and the repository's pinned npm version. Prepare the clean checkout: ```sh npm ci npm run build:cached ``` 2. From the repository root, create isolated reproduction directories and direct configuration into them: ```sh mkdir -p .repro/home/config .repro/home/cache .repro/home/data export HOME="$PWD/.repro/home" export XDG_CONFIG_HOME="$HOME/config" export XDG_CACHE_HOME="$HOME/cache" export XDG_DATA_HOME="$HOME/data" ``` 3. Run this inline script. It creates a control file, inspects it through the CLI, copies it with both list starts changed from 1 to 0, inspects the copy through the same CLI, and prints the relevant normalized results. Native TypeScript imports require the Node.js version stated above. The layout assertions ensure that only the intended coordinates are changed. ```sh node --input-type=module <<'JS' import assert from 'node:assert/strict'; import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; import { buildManagedPeFixture } from './src/dotnet/ManagedPe.fixture.ts'; const dir = process.env.REPRO_DIR ?? '.repro'; mkdirSync(dir, { recursive: true }); const inspect = (name) => { const run = spawnSync(process.execPath, [ 'scripts/rea.mjs', 'inspect-managed-members', `${dir}/${name}.dll`, '--format', 'json', ], { encoding: 'utf8' }); assert.equal(run.status, 0, run.stderr || run.stdout); const envelope = JSON.parse(run.stdout); writeFileSync(`${dir}/${name}.json`, run.stdout); return envelope.normalized_result; }; const bytes = buildManagedPeFixture({ additionalEmptyTypes: 3 }); writeFileSync(`${dir}/control.dll`, bytes); const control = inspect('control'); const row = control.types.find((type) => type.full_name === 'Fixture.Program'); assert.equal(row.row_offset, 986); assert.equal(bytes.readUInt16LE(row.row_offset + 10), 1); assert.equal(bytes.readUInt16LE(row.row_offset + 12), 1); const changed = Buffer.from(bytes); changed.writeUInt16LE(0, row.row_offset + 10); changed.writeUInt16LE(0, row.row_offset + 12); writeFileSync(`${dir}/zero-list.dll`, changed); const modified = inspect('zero-list'); const summarize = (result) => ({ types: result.types.slice(-2).map(({ full_name, field_list, method_list }) => ({ full_name, field_list, method_list })), fields: result.fields.map(({ token, declaring_type, declaring_type_token }) => ({ token, declaring_type, declaring_type_token })), methods: result.methods.map(({ token, declaring_type, declaring_type_token }) => ({ token, declaring_type, declaring_type_token })), coverage: result.coverage, }); console.log(JSON.stringify({ control: summarize(control), modified: summarize(modified) }, null, 2)); JS ``` 4. Compare the printed lists with the printed `declaring_type` values and coverage. The script stores both complete CLI results in the reproduction directory for inspection. The independent verifier replayed the inline reproduction using prepared dependencies and the compiled runtime. `npm ci` and `npm run build:cached` are clean-checkout prerequisites, not installation/build commands rerun during that verification. ### Expected behavior `docs/managed-code-analysis.md:310-314` promises that missing or malformed facts remain explicit in coverage and per-member status. Lines 323-330 document retained `field_list` and `method_list` coordinates and distinguish pointer-table list positions from physical definition rows. This fixture has ordinary Field and MethodDef tables and no pointer tables. Its list positions refer directly to definition rows. Zero starts in these nonempty, one-based lists must produce an explicit malformed-fact signal. The result must preserve readable member facts and identify unresolved ownership rather than certify conflicting ownership as complete. The contract does not require a particular repair algorithm. ### Impact and scope P2. Consumers of managed member inspection can attribute the same field or method to different types depending on whether they read the type's lists or the member's `declaring_type`. Complete coverage gives them no warning about the malformed coordinates or disagreement. The verified scope is a bounded malformed-metadata input through the CLI, with no pointer tables. No crash, data loss, target mutation, or security boundary crossing was observed. No safe alternative workflow was verified that preserves this input's member facts and establishes ownership or identifies the malformed coordinates. ### REA version 6.4.0 (main at 58300885207133422af235e0f2b10658cf958b03) ### Runtime and platform Node.js v26.10.0; macOS arm64. ### Target and artifact context Generated PE32/CLI bytes from `src/dotnet/ManagedPe.fixture.ts`, with four TypeDef rows, one Field row, one MethodDef row, and no pointer tables. `additionalEmptyTypes: 3` creates the control. The reproduction derives `Fixture.Program`'s row offset from control inspection, asserts offset 986 and starts of 1, then changes the starts at offsets 996 and 998 to 0 in a copy. The original input stays unchanged and neither assembly is executed. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Modified `normalized_result` excerpts from the reproduced CLI result: ```text { "types": [ { "full_name": "Fixture.Empty2", "field_list": { "first_row": 1, "last_row": 1, "count": 1 }, "method_list": { "first_row": 1, "last_row": 1, "count": 1 } }, { "full_name": "Fixture.Program", "field_list": { "first_row": null, "last_row": null, "count": 0 }, "method_list": { "first_row": null, "last_row": null, "count": 0 } } ], "fields": [ { "token": "0x04000001", "declaring_type": "Fixture.Program", "declaring_type_token": "0x02000004" } ], "methods": [ { "token": "0x06000001", "declaring_type": "Fixture.Program", "declaring_type_token": "0x02000004" } ], "coverage": { "state": "complete", "issues": [] } } ``` ### Typed outcome or recovery expected The modified input must identify its malformed list coordinates in coverage and must not present the conflicting ownership as complete. Preserve readable field and method facts and make ownership uncertainty explicit wherever the input cannot support it. Consistent normalization alone does not restore the documented contract if it erases the malformed-fact signal. The control must retain complete coverage with no issues, empty lists for `Fixture.Empty2`, row 1 in both lists for `Fixture.Program`, and both members declared by `Fixture.Program`. ### Analysis or suspected cause Source-derived reasoning at the reported baseline explains the reproduced disagreement. `src/dotnet/ManagedMemberInspectorCore.ts:81-95` treats a zero next start as the end of the member table and a zero current start as an empty range; its `parseTypes` call sites at lines 196-208 construct the displayed lists. `src/dotnet/ManagedMemberOwnership.ts:64-76` treats a zero next start as a literal upper bound and clamps a zero current start to 1. `src/dotnet/ManagedMemberRows.ts:61,96-97,288,352-353` consumes that ownership lookup for field and method identities. `src/dotnet/ManagedMemberInspector.ts:278-283` derives complete coverage when no issues or incomplete members were recorded. This explains differing range interpretations; the conflicting CLI output and empty coverage issues were directly reproduced. ### Additional context Related reports and changes are #1953, #1955 and #1886. The first two address lookup performance while preserving malformed first-match behavior, and #1886 addresses pointer-table ownership; this report concerns zero TypeDef list starts without pointer tables and requires an explicit malformed-coordinate signal plus consistent ownership evidence. The P2 assessment reflects misleading ownership and missing uncertainty on a bounded malformed-input case. Workaround availability remains unverified.
bsakaguchi 2026-10-15 18:31:25 +00:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@CassieFox23 commented on GitHub (Oct 14, 2026):

Heads up: this looks fixed on main in 3f6b5755b. The change is in src/dotnet/ManagedMemberOwnership.ts, and retains rows but leaves ownership unknown for zero TypeDef list starts covers it. It is listed on tracker #3582. If it still happens on main, please say so here and I'll take it off the list.

🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊

<!-- gh-comment-id:6175023839 --> @CassieFox23 commented on GitHub (Oct 14, 2026): Heads up: this looks fixed on main in 3f6b5755b. The change is in `src/dotnet/ManagedMemberOwnership.ts`, and `retains rows but leaves ownership unknown for zero TypeDef list starts` covers it. It is listed on tracker #3582. If it still happens on main, please say so here and I'll take it off the list. 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊
Author
Owner

@morluto commented on GitHub (Oct 14, 2026):

Resolved on main by 3f6b5755b.

Shared TypeDef member-list facts preserve invalid zero starts as null ownership/ranges with partial coverage, avoiding contradictory owners.

Rechecked on main b8510ee00 in the current focused run: 17 test files passed, 485 tests passed and one platform-dependent test skipped. Relevant regression coverage: src/dotnet/ManagedMemberInspector.test.ts.

<!-- gh-comment-id:6175064182 --> @morluto commented on GitHub (Oct 14, 2026): Resolved on main by 3f6b5755b. Shared TypeDef member-list facts preserve invalid zero starts as null ownership/ranges with partial coverage, avoiding contradictory owners. Rechecked on main b8510ee00 in the current focused run: 17 test files passed, 485 tests passed and one platform-dependent test skipped. Relevant regression coverage: src/dotnet/ManagedMemberInspector.test.ts.
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#1140
No description provided.