[GH-ISSUE #8861] [Bug] inspect-managed-members keeps a known owner when TypeDef list starts decrease across an invalid row #7777

Open
opened 2026-11-04 04:56:21 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Nov 4, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/8861

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-managed-members <assembly> --format json assigns a field and method to Fixture.Program when the three TypeDef rows have FieldList and MethodList starts 2, 0, 1. The valid starts decrease from 2 to 1 across an invalid zero. Program retains a one-member range for both lists, and both member rows retain its declaring-type name and token.

Coverage is partial, but the only two issues describe the middle row's zero starts. No issue reports the provable decrease. An adjacent-decrease control with starts 1, 2, 1 instead makes Program's lists empty and both declaring types null, with explicit decreasing-start issues. The valid control succeeds with complete coverage.

I reproduced these results through the unmodified source CLI and the separately installed published 6.8.0 CLI. Every command exited 0. The outputs below are from normalized_result in the CLI Evidence envelope.

Reproduction

Use a source checkout at the revision below and Node.js v24.22.1. A fresh checkout needs npm ci and npm run build:cached; my verification used the already prepared dependencies and compiled runtime, so those preparation commands were not rerun.

From the checkout root, run the following script. It builds four small PE/CLI fixtures, changes only their TypeDef list columns, isolates configuration/output locations, and invokes the real CLI for each. I executed this script against both source and published runtimes; the code below defaults to the source CLI. Setting REPRO_CLI to the installed package's scripts/rea.mjs selects its CLI instead.

export REPRO_DIR="$(mktemp -d)"
node --input-type=module <<'JS'
import assert from 'node:assert/strict';
import { mkdirSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { spawnSync } from 'node:child_process';
const load = name => import(pathToFileURL(resolve(name)).href);
const { buildManagedPeFixture } = await load('src/dotnet/ManagedPe.fixture.ts');
const { readManagedPeLayout } = await load('dist/dotnet/ManagedPeReader.js');
const { readManagedMetadataLayout } = await load('dist/dotnet/ManagedMetadataLayout.js');
const { readTypeDefinitionRowFacts } = await load('dist/dotnet/ManagedTypeDefinitionRows.js');
const dir = resolve(process.env.REPRO_DIR);
const home = resolve(dir, 'home');
mkdirSync(home, { recursive: true });
for (const [name, starts] of Object.entries({
  valid: [1, 1, 1], invalid_only: [1, 0, 1],
  adjacent: [1, 2, 1], across_invalid: [2, 0, 1],
})) {
  const bytes = buildManagedPeFixture({ additionalEmptyTypes: 2 });
  const pe = readManagedPeLayout(bytes);
  const { rva, size } = pe.cli.metadata;
  const layout = readManagedMetadataLayout(bytes, pe.rvaToOffset(rva, size, 'cli.metadata'), size);
  starts.forEach((start, index) => {
    const row = readTypeDefinitionRowFacts(bytes, layout, index + 1);
    bytes.writeUInt16LE(start, row.fieldListOffset);
    bytes.writeUInt16LE(start, row.methodListOffset);
  });
  const file = resolve(dir, name + '.dll');
  writeFileSync(file, bytes);
  const run = spawnSync(process.execPath, [
    process.env.REPRO_CLI || 'scripts/rea.mjs',
    'inspect-managed-members', file, '--format', 'json',
  ], { encoding: 'utf8', env: {
    PATH: process.env.PATH, HOME: home, TMPDIR: dir,
    XDG_CONFIG_HOME: resolve(home, 'config'),
    XDG_CACHE_HOME: resolve(home, 'cache'),
    XDG_STATE_HOME: resolve(home, 'state'),
    XDG_DATA_HOME: resolve(home, 'data'),
  }});
  assert.equal(run.status, 0, run.stderr || run.stdout);
  writeFileSync(resolve(dir, name + '.json'), run.stdout);
  const result = JSON.parse(run.stdout).normalized_result;
  console.log(JSON.stringify({ name, starts,
    program: result.types[2],
    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,
  }));
}
JS

Compare the printed Program ranges, field/method declaring types and coverage:

Case Starts for both lists Observed field and method owner Program lists Coverage
valid 1,1,1 Fixture.Program Row 1, count 1 Complete; no issues
invalid_only 1,0,1 Fixture.Program Row 1, count 1 Partial; two zero-start issues
adjacent 1,2,1 null Empty Partial; two decreasing-start issues
across_invalid 2,0,1 Fixture.Program Row 1, count 1 Partial; only two zero-start issues

The invalid_only control separates an invalid coordinate from a provable ordering violation; it is not presented as an additional bug. The script retains all four complete CLI envelopes in REPRO_DIR.

Expected behavior

When known TypeDef starts decrease across an invalid coordinate, report the malformed ordering and leave the affected member ownership unknown while preserving readable member facts. For 2, 0, 1, no replacement for the invalid middle value can make the list nondecreasing. The final start therefore participates in a provable decrease, just as it does in the adjacent control.

Expected source: the managed ownership contract specifies unknown affected owners and partial coverage for invalid or nonmonotonic list coordinates. The existing adjacent-decrease regression establishes this treatment for ordinary definition tables as well as the documentation's pointer-table context.

This expectation does not require invalidating every type after any malformed row or choosing a particular recovery algorithm.

Impact and scope

P2. A caller receives asserted declaring-type identities and member ranges for a type involved in provably nonmonotonic metadata. The top-level partial coverage provides a warning, but its issues identify only the invalid middle starts and do not identify the ordering problem affecting the later owner.

The verified scope is a generated three-TypeDef assembly with ordinary Field and MethodDef tables, one row in each, and no pointer tables. Both source and published CLI runs reproduced it. Real-world frequency, pointer-table variants and downstream comparison/application-graph effects were not tested. No crash, input mutation, target code execution, data loss or security boundary crossing was observed. No workaround that establishes the correct ownership for this malformed input was demonstrated.

REA version

Published rea-agents 6.8.0; its CLI printed 6.8.0. Source reproduction: c3647ade1e557c4179a8b825a37e0c2193cd2526, package version 6.8.0. npm view rea-agents version returned 6.8.0 during verification.

Runtime and platform

Node.js v24.22.1; macOS, Darwin arm64. Managed static provider only; no Hopper or Ghidra required.

Target and artifact context

A generated 4 KiB PE32/CLI assembly with three TypeDefs named Fixture.Empty0, Fixture.Empty1 and Fixture.Program, one Field named counter, one MethodDef named Main, and no pointer tables. Each case changes only the three FieldList and MethodList coordinates. The assembly is inspected as bytes.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Command actually run for each generated file: node scripts/rea.mjs inspect-managed-members <fixture> --format json. Equivalent commands used the published package's own CLI for the published-release check.

Observed across_invalid result excerpt:

{
  "types": [
    {
      "token": "0x02000001",
      "full_name": "Fixture.Empty0",
      "field_list": {
        "first_row": null,
        "last_row": null,
        "count": 0
      },
      "method_list": {
        "first_row": null,
        "last_row": null,
        "count": 0
      }
    },
    {
      "token": "0x02000002",
      "full_name": "Fixture.Empty1",
      "field_list": {
        "first_row": null,
        "last_row": null,
        "count": 0
      },
      "method_list": {
        "first_row": null,
        "last_row": null,
        "count": 0
      }
    },
    {
      "token": "0x02000003",
      "full_name": "Fixture.Program",
      "field_list": {
        "first_row": 1,
        "last_row": 1,
        "count": 1
      },
      "method_list": {
        "first_row": 1,
        "last_row": 1,
        "count": 1
      }
    }
  ],
  "fields": [
    {
      "token": "0x04000001",
      "declaring_type": "Fixture.Program",
      "declaring_type_token": "0x02000003"
    }
  ],
  "methods": [
    {
      "token": "0x06000001",
      "declaring_type": "Fixture.Program",
      "declaring_type_token": "0x02000003"
    }
  ],
  "coverage": {
    "state": "partial",
    "issues": [
      {
        "code": "invalid-row",
        "scope": "metadata.TypeDef:0x02000002",
        "offset": 968,
        "detail": "TypeDef FieldList start 0 is outside the one-based list range 1..2"
      },
      {
        "code": "invalid-row",
        "scope": "metadata.TypeDef:0x02000002",
        "offset": 970,
        "detail": "TypeDef MethodList start 0 is outside the one-based list range 1..2"
      }
    ]
  }
}

The two zero-start issues are the complete coverage.issues array. Both source and published CLI results have this ownership and issue content. The adjacent control instead has null declaring-type names/tokens and zero-count Program lists, with FieldList start 1 decreases from preceding start 2 and its MethodList counterpart.

npm run test:focused -- src/dotnet/ManagedMemberOwnership.test.ts src/dotnet/ManagedMemberInspector.test.ts passed: two files, 196 tests. No source or test changes were made.

Typed outcome or recovery expected

Keep the field and method rows, names, signatures and tokens readable. Represent the affected declaring-type identity as unknown and avoid presenting an established nonempty list for that affected type. Preserve partial coverage and add an invalid-row diagnostic identifying the known decrease. The exact diagnostic wording and number of diagnostics are implementation choices.

Analysis or suspected cause

Source-derived explanation: readTypeMemberListFacts validates each start, then checks ordering only between adjacent rows whose values are both non-null. For 2, 0, 1, the pairs become 2/null and null/1; neither reaches the decreasing-start branch. The final row keeps start 1 and the one-past-end sentinel 2.

The shared ownership index accepts that final interval because both endpoints remain known. The displayed range and declaring-type projections therefore agree with each other but violate the documented uncertainty treatment for nonmonotonic metadata. This explanation follows the source; the public CLI behavior was directly reproduced.

Additional context

Related closed reports are #3202, #1953 and #5909. #3202 was closed after shared list facts corrected zero-start ownership and top-level coverage; that correction is present here. #1953 and its merged fix address ownership lookup performance. #5909 concerns selected user-string completeness when ownership is already unknown. None describes a valid-start decrease hidden by an invalid intervening row.

I searched open and closed issues and PRs for TypeDef/list starts, FieldList, MethodList/decrease, nonmonotonic/non-monotonic, and the distinctive decreasing-start diagnostics. No open exact duplicate was found.

After fetching origin/main at d7df9df3393f77c78a76a06144ff4f515d5db928, comparison showed no changes to the affected managed parser, ownership, consumer, CLI/application or documentation paths. Main was compared statically; runtime reproduction used the stated source baseline and published package.

Originally created by @coygeek on GitHub (Nov 4, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8861 ### 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-managed-members <assembly> --format json` assigns a field and method to `Fixture.Program` when the three TypeDef rows have FieldList and MethodList starts `2, 0, 1`. The valid starts decrease from 2 to 1 across an invalid zero. Program retains a one-member range for both lists, and both member rows retain its declaring-type name and token. Coverage is partial, but the only two issues describe the middle row's zero starts. No issue reports the provable decrease. An adjacent-decrease control with starts `1, 2, 1` instead makes Program's lists empty and both declaring types null, with explicit decreasing-start issues. The valid control succeeds with complete coverage. I reproduced these results through the unmodified source CLI and the separately installed published 6.8.0 CLI. Every command exited 0. The outputs below are from `normalized_result` in the CLI Evidence envelope. ### Reproduction Use a source checkout at the revision below and Node.js v24.22.1. A fresh checkout needs `npm ci` and `npm run build:cached`; my verification used the already prepared dependencies and compiled runtime, so those preparation commands were not rerun. From the checkout root, run the following script. It builds four small PE/CLI fixtures, changes only their TypeDef list columns, isolates configuration/output locations, and invokes the real CLI for each. I executed this script against both source and published runtimes; the code below defaults to the source CLI. Setting `REPRO_CLI` to the installed package's `scripts/rea.mjs` selects its CLI instead. ```sh export REPRO_DIR="$(mktemp -d)" node --input-type=module <<'JS' import assert from 'node:assert/strict'; import { mkdirSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { spawnSync } from 'node:child_process'; const load = name => import(pathToFileURL(resolve(name)).href); const { buildManagedPeFixture } = await load('src/dotnet/ManagedPe.fixture.ts'); const { readManagedPeLayout } = await load('dist/dotnet/ManagedPeReader.js'); const { readManagedMetadataLayout } = await load('dist/dotnet/ManagedMetadataLayout.js'); const { readTypeDefinitionRowFacts } = await load('dist/dotnet/ManagedTypeDefinitionRows.js'); const dir = resolve(process.env.REPRO_DIR); const home = resolve(dir, 'home'); mkdirSync(home, { recursive: true }); for (const [name, starts] of Object.entries({ valid: [1, 1, 1], invalid_only: [1, 0, 1], adjacent: [1, 2, 1], across_invalid: [2, 0, 1], })) { const bytes = buildManagedPeFixture({ additionalEmptyTypes: 2 }); const pe = readManagedPeLayout(bytes); const { rva, size } = pe.cli.metadata; const layout = readManagedMetadataLayout(bytes, pe.rvaToOffset(rva, size, 'cli.metadata'), size); starts.forEach((start, index) => { const row = readTypeDefinitionRowFacts(bytes, layout, index + 1); bytes.writeUInt16LE(start, row.fieldListOffset); bytes.writeUInt16LE(start, row.methodListOffset); }); const file = resolve(dir, name + '.dll'); writeFileSync(file, bytes); const run = spawnSync(process.execPath, [ process.env.REPRO_CLI || 'scripts/rea.mjs', 'inspect-managed-members', file, '--format', 'json', ], { encoding: 'utf8', env: { PATH: process.env.PATH, HOME: home, TMPDIR: dir, XDG_CONFIG_HOME: resolve(home, 'config'), XDG_CACHE_HOME: resolve(home, 'cache'), XDG_STATE_HOME: resolve(home, 'state'), XDG_DATA_HOME: resolve(home, 'data'), }}); assert.equal(run.status, 0, run.stderr || run.stdout); writeFileSync(resolve(dir, name + '.json'), run.stdout); const result = JSON.parse(run.stdout).normalized_result; console.log(JSON.stringify({ name, starts, program: result.types[2], 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, })); } JS ``` Compare the printed Program ranges, field/method declaring types and coverage: | Case | Starts for both lists | Observed field and method owner | Program lists | Coverage | | --- | --- | --- | --- | --- | | valid | 1,1,1 | Fixture.Program | Row 1, count 1 | Complete; no issues | | invalid_only | 1,0,1 | Fixture.Program | Row 1, count 1 | Partial; two zero-start issues | | adjacent | 1,2,1 | null | Empty | Partial; two decreasing-start issues | | across_invalid | 2,0,1 | Fixture.Program | Row 1, count 1 | Partial; only two zero-start issues | The `invalid_only` control separates an invalid coordinate from a provable ordering violation; it is not presented as an additional bug. The script retains all four complete CLI envelopes in `REPRO_DIR`. ### Expected behavior When known TypeDef starts decrease across an invalid coordinate, report the malformed ordering and leave the affected member ownership unknown while preserving readable member facts. For `2, 0, 1`, no replacement for the invalid middle value can make the list nondecreasing. The final start therefore participates in a provable decrease, just as it does in the adjacent control. Expected source: the [managed ownership contract](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/docs/managed-code-analysis.md#L717-L727) specifies unknown affected owners and partial coverage for invalid or nonmonotonic list coordinates. The existing [adjacent-decrease regression](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/dotnet/ManagedMemberInspector.test.ts#L2215-L2270) establishes this treatment for ordinary definition tables as well as the documentation's pointer-table context. This expectation does not require invalidating every type after any malformed row or choosing a particular recovery algorithm. ### Impact and scope P2. A caller receives asserted declaring-type identities and member ranges for a type involved in provably nonmonotonic metadata. The top-level partial coverage provides a warning, but its issues identify only the invalid middle starts and do not identify the ordering problem affecting the later owner. The verified scope is a generated three-TypeDef assembly with ordinary Field and MethodDef tables, one row in each, and no pointer tables. Both source and published CLI runs reproduced it. Real-world frequency, pointer-table variants and downstream comparison/application-graph effects were not tested. No crash, input mutation, target code execution, data loss or security boundary crossing was observed. No workaround that establishes the correct ownership for this malformed input was demonstrated. ### REA version Published `rea-agents` 6.8.0; its CLI printed `6.8.0`. Source reproduction: `c3647ade1e557c4179a8b825a37e0c2193cd2526`, package version 6.8.0. `npm view rea-agents version` returned `6.8.0` during verification. ### Runtime and platform Node.js v24.22.1; macOS, Darwin arm64. Managed static provider only; no Hopper or Ghidra required. ### Target and artifact context A generated 4 KiB PE32/CLI assembly with three TypeDefs named `Fixture.Empty0`, `Fixture.Empty1` and `Fixture.Program`, one Field named `counter`, one MethodDef named `Main`, and no pointer tables. Each case changes only the three FieldList and MethodList coordinates. The assembly is inspected as bytes. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Command actually run for each generated file: `node scripts/rea.mjs inspect-managed-members <fixture> --format json`. Equivalent commands used the published package's own CLI for the published-release check. Observed `across_invalid` result excerpt: ```json { "types": [ { "token": "0x02000001", "full_name": "Fixture.Empty0", "field_list": { "first_row": null, "last_row": null, "count": 0 }, "method_list": { "first_row": null, "last_row": null, "count": 0 } }, { "token": "0x02000002", "full_name": "Fixture.Empty1", "field_list": { "first_row": null, "last_row": null, "count": 0 }, "method_list": { "first_row": null, "last_row": null, "count": 0 } }, { "token": "0x02000003", "full_name": "Fixture.Program", "field_list": { "first_row": 1, "last_row": 1, "count": 1 }, "method_list": { "first_row": 1, "last_row": 1, "count": 1 } } ], "fields": [ { "token": "0x04000001", "declaring_type": "Fixture.Program", "declaring_type_token": "0x02000003" } ], "methods": [ { "token": "0x06000001", "declaring_type": "Fixture.Program", "declaring_type_token": "0x02000003" } ], "coverage": { "state": "partial", "issues": [ { "code": "invalid-row", "scope": "metadata.TypeDef:0x02000002", "offset": 968, "detail": "TypeDef FieldList start 0 is outside the one-based list range 1..2" }, { "code": "invalid-row", "scope": "metadata.TypeDef:0x02000002", "offset": 970, "detail": "TypeDef MethodList start 0 is outside the one-based list range 1..2" } ] } } ``` The two zero-start issues are the complete `coverage.issues` array. Both source and published CLI results have this ownership and issue content. The adjacent control instead has null declaring-type names/tokens and zero-count Program lists, with `FieldList start 1 decreases from preceding start 2` and its MethodList counterpart. `npm run test:focused -- src/dotnet/ManagedMemberOwnership.test.ts src/dotnet/ManagedMemberInspector.test.ts` passed: two files, 196 tests. No source or test changes were made. ### Typed outcome or recovery expected Keep the field and method rows, names, signatures and tokens readable. Represent the affected declaring-type identity as unknown and avoid presenting an established nonempty list for that affected type. Preserve partial coverage and add an `invalid-row` diagnostic identifying the known decrease. The exact diagnostic wording and number of diagnostics are implementation choices. ### Analysis or suspected cause Source-derived explanation: [readTypeMemberListFacts](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/dotnet/ManagedTypeMemberLists.ts#L64-L151) validates each start, then checks ordering only between adjacent rows whose values are both non-null. For `2, 0, 1`, the pairs become `2/null` and `null/1`; neither reaches the decreasing-start branch. The final row keeps start 1 and the one-past-end sentinel 2. The [shared ownership index](https://github.com/morluto/rea/blob/c3647ade1e557c4179a8b825a37e0c2193cd2526/src/dotnet/ManagedMemberOwnership.ts#L43-L88) accepts that final interval because both endpoints remain known. The displayed range and declaring-type projections therefore agree with each other but violate the documented uncertainty treatment for nonmonotonic metadata. This explanation follows the source; the public CLI behavior was directly reproduced. ### Additional context Related closed reports are [#3202](https://github.com/morluto/rea/issues/3202), [#1953](https://github.com/morluto/rea/issues/1953) and [#5909](https://github.com/morluto/rea/issues/5909). #3202 was closed after shared list facts corrected zero-start ownership and top-level coverage; that correction is present here. #1953 and its merged fix address ownership lookup performance. #5909 concerns selected user-string completeness when ownership is already unknown. None describes a valid-start decrease hidden by an invalid intervening row. I searched open and closed issues and PRs for TypeDef/list starts, FieldList, MethodList/decrease, nonmonotonic/non-monotonic, and the distinctive decreasing-start diagnostics. No open exact duplicate was found. After fetching origin/main at `d7df9df3393f77c78a76a06144ff4f515d5db928`, comparison showed no changes to the affected managed parser, ownership, consumer, CLI/application or documentation paths. Main was compared statically; runtime reproduction used the stated source baseline and published package.
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#7777
No description provided.