[GH-ISSUE #7628] [Bug] Native export byte-limit recovery describes the limit as a count #4556

Open
opened 2026-10-31 00:50:45 +00:00 by bsakaguchi · 0 comments
Owner

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

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

With a synthetic PE containing 4100 aliases of one 4096-byte export name, node scripts/rea.mjs inspect-native-exports "$TARGET" --format json exits 1 and correctly identifies exhaustion of the 16777216-byte retained-text allowance. Its remediation.action, however, says the native export inventory allowance is a fixed count. The entry count is below the documented 262144-entry ceiling.

I observed this on two runs. A control with 4096 identical name entries succeeds and reports all 4096 exports with complete coverage. No external analysis engine is needed.

Reproduction

  1. Start at the source revision below with locked dependencies and the built CLI. Put Node 24.22.1 on PATH. Run this Python script, which constructs the PE bytes directly, runs the supported CLI with isolated HOME/XDG paths, and deletes its fixtures and state afterward:
python3 - <<'PY'
import json, os, pathlib, struct, subprocess, tempfile
repo = pathlib.Path.cwd()
def pe(n):
    names=0x480; ordinals=names+4*n; text=ordinals+2*n
    b=bytearray(text+4097)
    def h(at,v): struct.pack_into('<H',b,at,v)
    def i(at,v): struct.pack_into('<I',b,at,v)
    def rva(at): return at-0x400+0x1000
    b[:2]=b'MZ'; i(60,128); b[128:132]=bytes([80,69,0,0])
    h(132,0x8664); h(134,1); h(148,240); h(150,0x2022)
    op=152; h(op,0x20b); struct.pack_into('<Q',b,op+24,0x180000000)
    i(op+32,0x1000); i(op+36,0x200); i(op+56,0x9000); i(op+60,0x400)
    i(op+108,16); i(op+112,0x1000); i(op+116,0x80)
    sec=op+240; b[sec:sec+6]=b'.edata'; i(sec+8,len(b)-0x400)
    i(sec+12,0x1000); i(sec+16,len(b)-0x400); i(sec+20,0x400); i(sec+36,0x40000040)
    i(0x400+12,rva(0x460)); i(0x400+16,1); i(0x400+20,1); i(0x400+24,n)
    i(0x400+28,rva(0x440)); i(0x400+32,rva(names)); i(0x400+36,rva(ordinals))
    i(0x440,0x1300); b[0x460:0x46c]=b'verifier.dll'
    for at in range(n): i(names+4*at,rva(text))
    b[text:text+4096]=b'x'*4096
    return b

with tempfile.TemporaryDirectory(prefix="rea-export-check-") as directory:
    work = pathlib.Path(directory)
    env = {k: v for k, v in os.environ.items()
           if not k.startswith(("REA_", "HOPPER_", "GHIDRA_", "IDA_"))
           and k != "NODE_OPTIONS"}
    for key, name in [("HOME", "home"), ("XDG_CONFIG_HOME", "config"),
                      ("XDG_DATA_HOME", "data"), ("XDG_CACHE_HOME", "cache"),
                      ("XDG_STATE_HOME", "state"), ("TMPDIR", "temp")]:
        (work / name).mkdir()
        env[key] = str(work / name)
    for count in (4096, 4100, 4100):
        target = work / f"names-{count}.dll"
        target.write_bytes(pe(count))
        result = subprocess.run(
            ["node", "scripts/rea.mjs", "inspect-native-exports", str(target),
             "--format", "json"], cwd=repo, env=env,
            capture_output=True, text=True, timeout=60)
        assert not result.stderr, result.stderr
        value = json.loads(result.stdout)
        if count == 4096:
            assert result.returncode == 0
            assert len(value["normalized_result"]["exports"]) == count
            assert value["normalized_result"]["coverage"]["status"] == "complete"
            print("4096 names: success, complete coverage")
        else:
            assert result.returncode == 1
            assert value["code"] == "resource_constraint"
            assert value["retryable"] is False
            assert value["details"]["reported_limits"]["maximum"] == 16777216
            assert "fixed count" in value["remediation"]["action"]
            print("4100 names:", value["remediation"]["action"])
PY
  1. Observe the first success line, followed twice by recovery that calls the exceeded byte allowance a fixed count. The script asserts the correct resource-constraint code, non-retryable flag, and byte maximum as well as the incorrect recovery wording.

Expected behavior

The recovery should identify the exhausted retained-name byte allowance accurately. The fixed refusal and its non-retryable flag should remain intact.

This is an existing distinction, not a request to make the allowance adjustable. The merged recovery change explicitly specifies that byte budgets say fixed byte allowance and count limits stay fixed count. The recovery guide also requires the affected limit and its units. The binary guide documents retained-name bytes separately from the entry/row ceiling. Source locators and the merged change are listed below.

Impact and scope

P3: misleading diagnostic wording only. The CLI refuses the oversized inventory correctly, reports the actual byte ceiling, and does not recommend retrying. The successful control and failed requests produced no stderr. I observed no crash, hang, data loss, incorrect successful inventory, or consequential caller behavior. Callers can read the accurate message and reported maximum rather than interpreting the recovery sentence as the boundary identity.

Executed coverage is limited to the built CLI on synthetic PE inputs. Live MCP, ELF, other platforms, installed-package behavior, and real-world frequency were not tested.

REA version

Exact node scripts/rea.mjs --version stdout: 6.8.0 followed by a newline.

Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; macOS, darwin arm64. Built-in PE inspection; no Hopper, Ghidra, or other analysis engine.

Target and artifact context

Hand-built PE32+ metadata image. One export-address slot has 4100 name-table entries pointing to the same 4096-byte ASCII string, with zero-based ordinal indices selecting that slot. The file is 29849 bytes, below the default input-size budget. The 4096-entry control is 29825 bytes and retains exactly 16 MiB of name text.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Both oversized CLI runs returned:

{
  "code": "resource_constraint",
  "category": "resource_constraint",
  "retryable": false,
  "remediation": {
    "action": "The native export inventory allowance is a fixed count. Waiting for an active analysis to finish does not raise it. Repeating the unchanged request fails the same way."
  },
  "details": {
    "resource": "capacity",
    "reported_limits": {"boundary": "structure", "maximum": 16777216}
  }
}

The artifact-path-prefixed message ended with Export names exceed the 16777216-byte retained-text allowance. The excerpt omits that temporary path and other context fields.

At the stated baseline:

  • docs/binary-diagnostics.md:464-467 distinguishes the entry/row and retained-name allowances.
  • docs/design/limits-and-recovery.md:250-279 requires honest fixed-ceiling recovery, including the limit identity, effective value and unit.
  • Merged PR #5633 says byte budgets use fixed-byte wording and count limits use fixed-count wording.
  • src/native/exports/NativeExportParser.ts:36-52 enforces the 16 MiB retained-text bound; the PE name loop charges each name before storing it.
  • src/native/exports/NativeExportsProvider.ts:115-128 assigns count recovery to every binary resource refusal.
  • src/domain/fixedAllowanceRemediation.ts:17-28 has separate byte and count sentences; its regression tests forbid fixed-count text for the byte kind.

Typed outcome or recovery expected

Retain resource_constraint, retryable: false, the actual byte maximum, and refusal without partial success. Recovery should describe the fixed retained-name byte limit rather than the independent count ceiling. No exact replacement sentence is required.

Analysis or suspected cause

Source-derived explanation: one provider branch labels all parser resource failures with the count recovery kind. That branch also handles the retained-text byte refusal. The error message preserves the actual byte boundary, while the recovery helper receives the count label.

Additional context

Open/closed issues, all PR states, and comments were searched for native-export recovery, retained-text allowances, fixed counts, and inspection budgets. Related #5556, #5631, and #5632 concern other producers and misleading memory/report/retry advice repaired by #5633. The current case already has the correct non-retryable refusal; its remaining error is naming a byte boundary as a count. #5808 is a general recovery audit; #5910 covered other naming controls; #7531 concerns PE certificate budgets incorrectly classified as malformed input. None reports this native-export mismatch. No last-known-good version or regression range was established.

Triage assessment

  • Impact level: P3
  • Assessment status: supported
  • Impact basis: The independently executed CLI returns misleading recovery wording, while the refusal, actual byte maximum, and non-retryable flag remain correct; no consequential behavior was observed.
  • Workaround status: available
  • Workaround basis: The same returned diagnostic correctly identifies the retained-text byte allowance in its message and reports maximum 16777216. Callers can use those facts without changing the target or attempting above-limit success.
Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7628 ### 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 With a synthetic PE containing 4100 aliases of one 4096-byte export name, `node scripts/rea.mjs inspect-native-exports "$TARGET" --format json` exits 1 and correctly identifies exhaustion of the 16777216-byte retained-text allowance. Its `remediation.action`, however, says the native export inventory allowance is a fixed count. The entry count is below the documented 262144-entry ceiling. I observed this on two runs. A control with 4096 identical name entries succeeds and reports all 4096 exports with complete coverage. No external analysis engine is needed. ### Reproduction 1. Start at the source revision below with locked dependencies and the built CLI. Put Node 24.22.1 on PATH. Run this Python script, which constructs the PE bytes directly, runs the supported CLI with isolated HOME/XDG paths, and deletes its fixtures and state afterward: ```sh python3 - <<'PY' import json, os, pathlib, struct, subprocess, tempfile repo = pathlib.Path.cwd() def pe(n): names=0x480; ordinals=names+4*n; text=ordinals+2*n b=bytearray(text+4097) def h(at,v): struct.pack_into('<H',b,at,v) def i(at,v): struct.pack_into('<I',b,at,v) def rva(at): return at-0x400+0x1000 b[:2]=b'MZ'; i(60,128); b[128:132]=bytes([80,69,0,0]) h(132,0x8664); h(134,1); h(148,240); h(150,0x2022) op=152; h(op,0x20b); struct.pack_into('<Q',b,op+24,0x180000000) i(op+32,0x1000); i(op+36,0x200); i(op+56,0x9000); i(op+60,0x400) i(op+108,16); i(op+112,0x1000); i(op+116,0x80) sec=op+240; b[sec:sec+6]=b'.edata'; i(sec+8,len(b)-0x400) i(sec+12,0x1000); i(sec+16,len(b)-0x400); i(sec+20,0x400); i(sec+36,0x40000040) i(0x400+12,rva(0x460)); i(0x400+16,1); i(0x400+20,1); i(0x400+24,n) i(0x400+28,rva(0x440)); i(0x400+32,rva(names)); i(0x400+36,rva(ordinals)) i(0x440,0x1300); b[0x460:0x46c]=b'verifier.dll' for at in range(n): i(names+4*at,rva(text)) b[text:text+4096]=b'x'*4096 return b with tempfile.TemporaryDirectory(prefix="rea-export-check-") as directory: work = pathlib.Path(directory) env = {k: v for k, v in os.environ.items() if not k.startswith(("REA_", "HOPPER_", "GHIDRA_", "IDA_")) and k != "NODE_OPTIONS"} for key, name in [("HOME", "home"), ("XDG_CONFIG_HOME", "config"), ("XDG_DATA_HOME", "data"), ("XDG_CACHE_HOME", "cache"), ("XDG_STATE_HOME", "state"), ("TMPDIR", "temp")]: (work / name).mkdir() env[key] = str(work / name) for count in (4096, 4100, 4100): target = work / f"names-{count}.dll" target.write_bytes(pe(count)) result = subprocess.run( ["node", "scripts/rea.mjs", "inspect-native-exports", str(target), "--format", "json"], cwd=repo, env=env, capture_output=True, text=True, timeout=60) assert not result.stderr, result.stderr value = json.loads(result.stdout) if count == 4096: assert result.returncode == 0 assert len(value["normalized_result"]["exports"]) == count assert value["normalized_result"]["coverage"]["status"] == "complete" print("4096 names: success, complete coverage") else: assert result.returncode == 1 assert value["code"] == "resource_constraint" assert value["retryable"] is False assert value["details"]["reported_limits"]["maximum"] == 16777216 assert "fixed count" in value["remediation"]["action"] print("4100 names:", value["remediation"]["action"]) PY ``` 2. Observe the first success line, followed twice by recovery that calls the exceeded byte allowance a fixed count. The script asserts the correct resource-constraint code, non-retryable flag, and byte maximum as well as the incorrect recovery wording. ### Expected behavior The recovery should identify the exhausted retained-name byte allowance accurately. The fixed refusal and its non-retryable flag should remain intact. This is an existing distinction, not a request to make the allowance adjustable. The merged recovery change explicitly specifies that byte budgets say fixed byte allowance and count limits stay fixed count. The recovery guide also requires the affected limit and its units. The binary guide documents retained-name bytes separately from the entry/row ceiling. Source locators and the merged change are listed below. ### Impact and scope P3: misleading diagnostic wording only. The CLI refuses the oversized inventory correctly, reports the actual byte ceiling, and does not recommend retrying. The successful control and failed requests produced no stderr. I observed no crash, hang, data loss, incorrect successful inventory, or consequential caller behavior. Callers can read the accurate message and reported maximum rather than interpreting the recovery sentence as the boundary identity. Executed coverage is limited to the built CLI on synthetic PE inputs. Live MCP, ELF, other platforms, installed-package behavior, and real-world frequency were not tested. ### REA version Exact `node scripts/rea.mjs --version` stdout: `6.8.0` followed by a newline. Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js v24.22.1; macOS, darwin arm64. Built-in PE inspection; no Hopper, Ghidra, or other analysis engine. ### Target and artifact context Hand-built PE32+ metadata image. One export-address slot has 4100 name-table entries pointing to the same 4096-byte ASCII string, with zero-based ordinal indices selecting that slot. The file is 29849 bytes, below the default input-size budget. The 4096-entry control is 29825 bytes and retains exactly 16 MiB of name text. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Both oversized CLI runs returned: ```json { "code": "resource_constraint", "category": "resource_constraint", "retryable": false, "remediation": { "action": "The native export inventory allowance is a fixed count. Waiting for an active analysis to finish does not raise it. Repeating the unchanged request fails the same way." }, "details": { "resource": "capacity", "reported_limits": {"boundary": "structure", "maximum": 16777216} } } ``` The artifact-path-prefixed message ended with `Export names exceed the 16777216-byte retained-text allowance`. The excerpt omits that temporary path and other context fields. At the stated baseline: - `docs/binary-diagnostics.md:464-467` distinguishes the entry/row and retained-name allowances. - `docs/design/limits-and-recovery.md:250-279` requires honest fixed-ceiling recovery, including the limit identity, effective value and unit. - [Merged PR #5633](https://github.com/morluto/rea/pull/5633) says byte budgets use fixed-byte wording and count limits use fixed-count wording. - `src/native/exports/NativeExportParser.ts:36-52` enforces the 16 MiB retained-text bound; the PE name loop charges each name before storing it. - `src/native/exports/NativeExportsProvider.ts:115-128` assigns count recovery to every binary resource refusal. - `src/domain/fixedAllowanceRemediation.ts:17-28` has separate byte and count sentences; its regression tests forbid fixed-count text for the byte kind. ### Typed outcome or recovery expected Retain `resource_constraint`, `retryable: false`, the actual byte maximum, and refusal without partial success. Recovery should describe the fixed retained-name byte limit rather than the independent count ceiling. No exact replacement sentence is required. ### Analysis or suspected cause Source-derived explanation: one provider branch labels all parser resource failures with the count recovery kind. That branch also handles the retained-text byte refusal. The error message preserves the actual byte boundary, while the recovery helper receives the count label. ### Additional context Open/closed issues, all PR states, and comments were searched for native-export recovery, retained-text allowances, fixed counts, and inspection budgets. Related [#5556](https://github.com/morluto/rea/issues/5556), [#5631](https://github.com/morluto/rea/issues/5631), and [#5632](https://github.com/morluto/rea/issues/5632) concern other producers and misleading memory/report/retry advice repaired by #5633. The current case already has the correct non-retryable refusal; its remaining error is naming a byte boundary as a count. [#5808](https://github.com/morluto/rea/issues/5808) is a general recovery audit; [#5910](https://github.com/morluto/rea/issues/5910) covered other naming controls; [#7531](https://github.com/morluto/rea/issues/7531) concerns PE certificate budgets incorrectly classified as malformed input. None reports this native-export mismatch. No last-known-good version or regression range was established. ## Triage assessment - Impact level: P3 - Assessment status: supported - Impact basis: The independently executed CLI returns misleading recovery wording, while the refusal, actual byte maximum, and non-retryable flag remain correct; no consequential behavior was observed. - Workaround status: available - Workaround basis: The same returned diagnostic correctly identifies the retained-text byte allowance in its message and reports maximum 16777216. Callers can use those facts without changing the target or attempting above-limit success.
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#4556
No description provided.