[GH-ISSUE #5909] [Bug] Selected managed user strings claim complete coverage when method ownership is unknown #3587

Closed
opened 2026-10-31 00:42:08 +00:00 by bsakaguchi · 0 comments
Owner

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

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 reports complete user-string coverage for a selected type when malformed MethodList coordinates prevent it from establishing which methods belong to that type. A generated PE/CLI fixture contains a readable method loading coverage-marker. Changing its TypeDef MethodList start from 1 to 0 and inspecting with --type Fixture.Program returns a matched selection, no methods, and user_strings: { "state": "complete", "literals": [], "issues": [] }.

The top-level member coverage correctly reports partial with an invalid-row issue. The defect concerns the independent user-string completeness claim. Inspecting the same malformed file without --type recovers the literal and retains the method with declaring_type: null.

Reproduction

I executed the built public CLI against generated fixtures; no target assembly was loaded or executed. The following inline script was also replayed successfully from the repository root with prepared dependencies and compiled runtime. It creates the fixture, runs the selected control, changes only the MethodList start, and runs selected and unfiltered inspections.

For a clean source checkout, npm ci and npm run build:cached prepare dependencies and the runtime; those preparation commands were not rerun for this check. Run the script with node --input-type=module on standard input:

node --input-type=module <<'JS'
import assert from 'node:assert/strict';
import { mkdirSync, writeFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { buildManagedPeFixture, fixtureUserStringHeap } from './src/dotnet/ManagedPe.fixture.ts';

const dir = process.env.REPRO_DIR ?? '.repro-managed-strings';
mkdirSync(dir, { recursive: true });
const { heap, tokens } = fixtureUserStringHeap(['coverage-marker']);
// Tiny method: ldstr 0x70000001; pop; ret.
const ilBody = Buffer.from([0x1e, 0x72, 0, 0, 0, 0, 0x26, 0x2a]);
ilBody.writeUInt32LE(Number.parseInt(tokens[0], 16), 2);
const bytes = buildManagedPeFixture({ ilBody, userStringHeap: heap });

function inspect(name, input, selected) {
  const path = `${dir}/${name}.dll`;
  writeFileSync(path, input);
  const run = spawnSync(process.execPath, [
    'scripts/rea.mjs', 'inspect-managed-members', path,
    ...(selected ? ['--type', 'Fixture.Program'] : []), '--format', 'json',
  ], { encoding: 'utf8', timeout: 30000 });
  assert.equal(run.status, 0, run.stderr || run.stdout);
  const result = JSON.parse(run.stdout).normalized_result;
  console.log(JSON.stringify({ name, selection: result.selection,
    methods: result.methods.map(({ token, declaring_type }) => ({ token, declaring_type })),
    coverage: result.coverage, user_strings: result.user_strings }, null, 2));
  return result;
}

const control = inspect('control-selected', bytes, true);
assert.equal(control.user_strings.literals[0].value, 'coverage-marker');
const row = control.types.find(({ full_name }) => full_name === 'Fixture.Program');
// This small fixture uses two-byte indexes: MethodList follows
// Flags(4), Name(2), Namespace(2), Extends(2), and FieldList(2).
const methodListOffset = row.row_offset + 12;
assert.equal(bytes.readUInt16LE(methodListOffset), 1);
const malformed = Buffer.from(bytes);
malformed.writeUInt16LE(0, methodListOffset);
inspect('zero-selected', malformed, true);
inspect('zero-unfiltered', malformed, false);
JS

All three CLI invocations exit 0. The selected control returns the literal. The malformed selected result returns no methods or literals yet calls user-string coverage complete. The malformed unfiltered result recovers coverage-marker.

I also ran a separate generated fixture with two TypeDefs, two MethodDefs, and MethodList starts 2 then 1. Selecting Fixture.Program again returned no methods and complete/empty user strings, alongside a partial member-coverage issue stating that adjacent member ranges are unknown.

Expected behavior

User-string coverage must remain partial when unresolved method ownership prevents the inspection from ruling out unscanned bodies in the selected scope. A readable selected type name alone does not establish that every possibly applicable body was inspected.

The user-string coverage contract says the state is partial when a method that may belong to the inspected types was not inspected because its ldstr instructions were not observed. The method-accounting comment explicitly includes unknown owners.

Impact and scope

P2. A consumer of user_strings.state can mistake an unobserved literal inventory for a complete empty inventory under a type selection. This was reproduced with zero and decreasing MethodList starts in synthetic malformed metadata. The top-level partial member coverage provides a warning. On the zero-start fixture, omitting the type filter recovered the literal text, but ownership remained unknown, so that does not establish a complete inventory attributed to the selected type. No effect on well-formed ordinary metadata, crash, target mutation, or security boundary crossing was demonstrated.

REA version

6.7.0; source revision 5241597594.

Runtime and platform

macOS 27.0.1 arm64; Node.js v26.12.0.

Target and artifact context

Synthetic PE32/CLI files from the repository's managed fixture builder. The minimal case has one readable TypeDef, one MethodDef whose CIL loads 0x70000001, and a valid #US entry containing coverage-marker. The malformed copy changes the TypeDef MethodList start from 1 to 0. The separate decreasing-start case uses two TypeDefs and two MethodDefs with starts 2 then 1. All inspections are static.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Compact excerpts from the executed zero-start CLI comparison:

{
  "control-selected": {
    "methods": [{ "token": "0x06000001", "declaring_type": "Fixture.Program" }],
    "coverage": { "state": "complete", "issues": [] },
    "user_strings": {
      "state": "complete",
      "literals": [{ "token": "0x70000001", "offset": 1205, "value": "coverage-marker" }],
      "issues": []
    }
  },
  "zero-selected": {
    "selection": { "type_names": ["Fixture.Program"], "unmatched_type_names": [] },
    "methods": [],
    "coverage": {
      "state": "partial",
      "issues": [{
        "code": "invalid-row",
        "scope": "metadata.TypeDef:0x02000001",
        "offset": 968,
        "detail": "TypeDef MethodList start 0 is outside the one-based list range 1..2"
      }]
    },
    "user_strings": { "state": "complete", "literals": [], "issues": [] }
  },
  "zero-unfiltered": {
    "methods": [{ "token": "0x06000001", "declaring_type": null }],
    "user_strings": {
      "state": "complete",
      "literals": [{ "token": "0x70000001", "offset": 1205, "value": "coverage-marker" }],
      "issues": []
    }
  }
}

Typed outcome or recovery expected

user_strings.state should be partial when potentially selected method bodies remain unscanned because their ownership is unresolved. Preserve readable literals and the existing malformed-list diagnostics without inventing a declaring type.

Analysis or suspected cause

Source-derived explanation of the observed result:

Additional context

Related issue #3202 fixed contradictory ownership and complete top-level member coverage for zero list starts. That correction is present here: ownership is unknown and member coverage is partial. This report concerns the remaining independent user-string coverage accounting. Issue #2158 and PR #2162 introduced user-string decoding; issue #3010 and PR #3061 added retained delivery rather than correcting this producer behavior.

After fetching current main at c324d31b45, the relevant ownership, list, method, user-string, and documentation files had no changes from the reported baseline. Runtime reproduction was performed at the stated baseline.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5909 ### 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` reports complete user-string coverage for a selected type when malformed MethodList coordinates prevent it from establishing which methods belong to that type. A generated PE/CLI fixture contains a readable method loading `coverage-marker`. Changing its TypeDef MethodList start from 1 to 0 and inspecting with `--type Fixture.Program` returns a matched selection, no methods, and `user_strings: { "state": "complete", "literals": [], "issues": [] }`. The top-level member coverage correctly reports partial with an invalid-row issue. The defect concerns the independent user-string completeness claim. Inspecting the same malformed file without `--type` recovers the literal and retains the method with `declaring_type: null`. ### Reproduction I executed the built public CLI against generated fixtures; no target assembly was loaded or executed. The following inline script was also replayed successfully from the repository root with prepared dependencies and compiled runtime. It creates the fixture, runs the selected control, changes only the MethodList start, and runs selected and unfiltered inspections. For a clean source checkout, `npm ci` and `npm run build:cached` prepare dependencies and the runtime; those preparation commands were not rerun for this check. Run the script with `node --input-type=module` on standard input: ```sh node --input-type=module <<'JS' import assert from 'node:assert/strict'; import { mkdirSync, writeFileSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; import { buildManagedPeFixture, fixtureUserStringHeap } from './src/dotnet/ManagedPe.fixture.ts'; const dir = process.env.REPRO_DIR ?? '.repro-managed-strings'; mkdirSync(dir, { recursive: true }); const { heap, tokens } = fixtureUserStringHeap(['coverage-marker']); // Tiny method: ldstr 0x70000001; pop; ret. const ilBody = Buffer.from([0x1e, 0x72, 0, 0, 0, 0, 0x26, 0x2a]); ilBody.writeUInt32LE(Number.parseInt(tokens[0], 16), 2); const bytes = buildManagedPeFixture({ ilBody, userStringHeap: heap }); function inspect(name, input, selected) { const path = `${dir}/${name}.dll`; writeFileSync(path, input); const run = spawnSync(process.execPath, [ 'scripts/rea.mjs', 'inspect-managed-members', path, ...(selected ? ['--type', 'Fixture.Program'] : []), '--format', 'json', ], { encoding: 'utf8', timeout: 30000 }); assert.equal(run.status, 0, run.stderr || run.stdout); const result = JSON.parse(run.stdout).normalized_result; console.log(JSON.stringify({ name, selection: result.selection, methods: result.methods.map(({ token, declaring_type }) => ({ token, declaring_type })), coverage: result.coverage, user_strings: result.user_strings }, null, 2)); return result; } const control = inspect('control-selected', bytes, true); assert.equal(control.user_strings.literals[0].value, 'coverage-marker'); const row = control.types.find(({ full_name }) => full_name === 'Fixture.Program'); // This small fixture uses two-byte indexes: MethodList follows // Flags(4), Name(2), Namespace(2), Extends(2), and FieldList(2). const methodListOffset = row.row_offset + 12; assert.equal(bytes.readUInt16LE(methodListOffset), 1); const malformed = Buffer.from(bytes); malformed.writeUInt16LE(0, methodListOffset); inspect('zero-selected', malformed, true); inspect('zero-unfiltered', malformed, false); JS ``` All three CLI invocations exit 0. The selected control returns the literal. The malformed selected result returns no methods or literals yet calls user-string coverage complete. The malformed unfiltered result recovers `coverage-marker`. I also ran a separate generated fixture with two TypeDefs, two MethodDefs, and MethodList starts 2 then 1. Selecting `Fixture.Program` again returned no methods and complete/empty user strings, alongside a partial member-coverage issue stating that adjacent member ranges are unknown. ### Expected behavior User-string coverage must remain partial when unresolved method ownership prevents the inspection from ruling out unscanned bodies in the selected scope. A readable selected type name alone does not establish that every possibly applicable body was inspected. The [user-string coverage contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/managed-code-analysis.md#L193-L213) says the state is partial when a method that may belong to the inspected types was not inspected because its `ldstr` instructions were not observed. The [method-accounting comment](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/dotnet/ManagedMemberRows.ts#L305-L320) explicitly includes unknown owners. ### Impact and scope P2. A consumer of `user_strings.state` can mistake an unobserved literal inventory for a complete empty inventory under a type selection. This was reproduced with zero and decreasing MethodList starts in synthetic malformed metadata. The top-level partial member coverage provides a warning. On the zero-start fixture, omitting the type filter recovered the literal text, but ownership remained unknown, so that does not establish a complete inventory attributed to the selected type. No effect on well-formed ordinary metadata, crash, target mutation, or security boundary crossing was demonstrated. ### REA version 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e. ### Runtime and platform macOS 27.0.1 arm64; Node.js v26.12.0. ### Target and artifact context Synthetic PE32/CLI files from the repository's managed fixture builder. The minimal case has one readable TypeDef, one MethodDef whose CIL loads `0x70000001`, and a valid #US entry containing `coverage-marker`. The malformed copy changes the TypeDef MethodList start from 1 to 0. The separate decreasing-start case uses two TypeDefs and two MethodDefs with starts 2 then 1. All inspections are static. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Compact excerpts from the executed zero-start CLI comparison: ```json { "control-selected": { "methods": [{ "token": "0x06000001", "declaring_type": "Fixture.Program" }], "coverage": { "state": "complete", "issues": [] }, "user_strings": { "state": "complete", "literals": [{ "token": "0x70000001", "offset": 1205, "value": "coverage-marker" }], "issues": [] } }, "zero-selected": { "selection": { "type_names": ["Fixture.Program"], "unmatched_type_names": [] }, "methods": [], "coverage": { "state": "partial", "issues": [{ "code": "invalid-row", "scope": "metadata.TypeDef:0x02000001", "offset": 968, "detail": "TypeDef MethodList start 0 is outside the one-based list range 1..2" }] }, "user_strings": { "state": "complete", "literals": [], "issues": [] } }, "zero-unfiltered": { "methods": [{ "token": "0x06000001", "declaring_type": null }], "user_strings": { "state": "complete", "literals": [{ "token": "0x70000001", "offset": 1205, "value": "coverage-marker" }], "issues": [] } } } ``` ### Typed outcome or recovery expected `user_strings.state` should be `partial` when potentially selected method bodies remain unscanned because their ownership is unresolved. Preserve readable literals and the existing malformed-list diagnostics without inventing a declaring type. ### Analysis or suspected cause Source-derived explanation of the observed result: - [TypeDef list parsing](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/dotnet/ManagedTypeMemberLists.ts#L64-L145) invalidates malformed or decreasing method ranges. - The [ownership index](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/dotnet/ManagedMemberOwnership.ts#L42-L80) returns 0 for unclaimed definition rows. - [Method filtering and scan accounting](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/dotnet/ManagedMemberRows.ts#L305-L376) treats `undefined` ownership as potentially selected but does not include owner 0. For owner 0, the declaring range is absent, the body is skipped, and the unscanned-method count stays zero. - The [user-string state calculation](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/dotnet/ManagedUserStrings.ts#L25-L54) consequently treats the empty inventory as complete. ### Additional context Related issue #3202 fixed contradictory ownership and complete top-level member coverage for zero list starts. That correction is present here: ownership is unknown and member coverage is partial. This report concerns the remaining independent user-string coverage accounting. Issue #2158 and PR #2162 introduced user-string decoding; issue #3010 and PR #3061 added retained delivery rather than correcting this producer behavior. After fetching current main at c324d31b45fcab77e4ed7320ed60f0bf5a4ba188, the relevant ownership, list, method, user-string, and documentation files had no changes from the reported baseline. Runtime reproduction was performed at the stated baseline.
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#3587
No description provided.