[GH-ISSUE #4202] [Bug] Arithmetic native-addon assignments are reported as direct re-exports #1648

Closed
opened 2026-10-16 21:08:26 +00:00 by bsakaguchi · 2 comments
Owner

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

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

analyze-javascript-application reports a native-addon re-export when a CommonJS export property uses +=, -=, or *= with an ordinary native require on the right. Each tested input produces a binding_kind: "re-export" observation and a high-confidence exposes edge from the JavaScript asset to the requested binding. These operators compute an arithmetic or concatenation result rather than forwarding the addon namespace. The addon request itself is legitimate and remains a resolved loads dependency.

Reproduction

Use the built source checkout at either tested revision below with its pinned Node.js runtime on PATH. Run this block from the repository root. It creates five inert fixtures and isolated home/config/cache/data directories, invokes the real public CLI sequentially, prints the relevant results, and removes the temporary inputs. It does not execute main.cjs or load fake.node.

python3 - <<'PY'
import json
import os
from pathlib import Path
import subprocess
from tempfile import TemporaryDirectory

cases = {
    "plus": 'exports.value += require("./fake.node");\n',
    "minus": 'exports.value -= require("./fake.node");\n',
    "multiply": 'exports.value *= require("./fake.node");\n',
    "plain": 'exports.value = require("./fake.node");\n',
    "request": 'const addon = require("./fake.node");\n',
}
with TemporaryDirectory(prefix="rea-native-assignment-") as temporary:
    root = Path(temporary)
    environment = dict(os.environ)
    for key in ("HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_DATA_HOME"):
        folder = root / key.lower()
        folder.mkdir()
        environment[key] = str(folder)
    for name, source in cases.items():
        app = root / name
        app.mkdir()
        (app / "main.cjs").write_text(source)
        (app / "fake.node").write_bytes(b"fixture bytes, never executed")
        result = subprocess.run(
            ["node", "scripts/rea.mjs", "analyze-javascript-application",
             str(app), "--format", "json", "--full-output"],
            env=environment, text=True, capture_output=True, timeout=60,
        )
        envelope = json.loads(result.stdout)
        assert result.returncode == 0 and envelope["ok"], envelope
        analysis = envelope["data"]["normalized_result"]
        graph = analysis["graph"]
        binding = [observation for node in graph["nodes"]
                   if node["kind"] == "native-export"
                   for observation in node["observations"]]
        mapped = [edge for edge in graph["edges"]
                  if edge["evidence"]["extractor"]["operation"]
                  == "map-native-addon-binding"]
        loads = [edge for edge in graph["edges"]
                 if edge["relation"] == "loads"
                 and edge["properties"].get("specifier") == "./fake.node"]
        assert len(binding) == len(mapped) == len(loads) == 1
        print(name, "exit", result.returncode,
              "bindings", analysis["summary"]["native_addon_bindings"],
              "resolved", analysis["summary"]["resolved_native_addon_bindings"],
              "kind", binding[0]["properties"]["binding_kind"],
              "relation", mapped[0]["relation"],
              "loads", len(loads),
              "coverage", mapped[0]["evidence"]["coverage"]["status"])
PY

The plus, minus, and multiply inputs test arithmetic compound assignments. plain controls direct forwarding of the addon namespace, and request controls a legitimate native binding with no re-export.

Expected behavior

The native binding kind and the JavaScript asset's relationship should distinguish requesting an addon from re-exporting its value. The three arithmetic assignments may identify the native request, but they should not identify direct namespace re-exports or create source-to-binding exposes edges on that basis. Preserve the resolved loads dependency. The plain-assignment control should retain its re-export and source-to-binding exposes edge, and the variable-binding control should retain its require binding and imports edge.

This expectation concerns the static meaning of the written operations, not proof that the program executes or that the native binary implements any export. Logical assignments such as ||=, &&=, and ??= can conditionally forward a value and are outside this report's tested scope.

The native-binding type distinguishes a requested binding from a re-export. The reconstruction contract recovers native requests as inert data; its authority limits distinguish syntax from static relationships. The graph documentation distinguishes source imports from the addon association, and the existing control preserves plain-assignment re-exports.

Impact and scope

P2, supported. The public static reconstruction result contains a wrong typed binding and a wrong high-confidence graph relationship for three supported arithmetic assignment forms. This changes machine-readable graph behavior, beyond wording or presentation. The observed defect is bounded; the valid native dependency survives, and no broader application or downstream consumer failure was tested.

Workaround status is unverified. The outputs retain a source range and resolved dependency, so an analyst can inspect the operator manually. No caller option or other supported operation was verified to return a corrected native-binding graph while preserving the exact input.

REA version

Reproduced from source revision 62bf09809ef7bb224028916424f8f07652a20f4f and freshly fetched main 1b828dbb1011d80f44b3ae99efb9554991d71261. Both checkouts were built with npm run build:cached, their locked dependencies, and the pinned toolchain. The launcher prints 6.5.0; the published package and release tag were not executed.

Runtime and platform

Node.js v24.18.0; npm 11.16.0; macOS arm64. Real public CLI, inert target source, and placeholder .node bytes. No MCP, external provider, target-source execution or native-addon loading was performed.

Target and artifact context

Five isolated directories, each containing one main.cjs and a sibling fake.node whose bytes are fixture bytes, never executed. The three arithmetic assignments and the two controls are inline above.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction. The assignment classifier records a direct native re-export before graph projection.

Relevant output or evidence

The verifier ran two baseline passes. The publication review independently reran the exact five-case fixture once at the baseline and once in a separate freshly built current-main checkout. Every pass returned the same classification and source-edge results. Both publication-review passes completed successfully. Every call exited 0, retained one resolved native binding, parsed one JavaScript file without parse failures, and retained one resolved loads dependency. The standalone reproduction printed:

plus exit 0 bindings 1 resolved 1 kind re-export relation exposes loads 1 coverage complete
minus exit 0 bindings 1 resolved 1 kind re-export relation exposes loads 1 coverage complete
multiply exit 0 bindings 1 resolved 1 kind re-export relation exposes loads 1 coverage complete
plain exit 0 bindings 1 resolved 1 kind re-export relation exposes loads 1 coverage complete
request exit 0 bindings 1 resolved 1 kind require relation imports loads 1 coverage complete

For every arithmetic case, the native-export observation has namespace_access: true, requested_members: [], resolved_path: "fake.node", and native_export_verification: "not-performed". The incorrect JavaScript-source edge has extractor operation map-native-addon-binding, state: "inferred", confidence: "high", and complete coverage. The distinct native-addon-to-binding edge with operation associate-native-addon-requested-exports also uses exposes; that association exists in the legitimate variable-binding control and is not the failing relationship in this report.

Observed public-entry evidence; local verification reproduced the failure with complete inline fixtures. The original verifier also passed all nine existing native-loader tests. Source inspection confirms that the native extractor and graph projection remain unchanged on main. Shared helpers changed from the baseline, so applicability at the tested main revision was established by the additional public CLI reproduction rather than inferred from unchanged cited files alone. Main advanced again to 5e30969cc5f1255d44ec7a1f1ca8dbac0af5e7c4 during final review. All inspected files in this report are unchanged from the tested main revision; applicability to that newer revision is source-inspected only, without another runtime run.

Typed outcome or recovery expected

Run the same public reproduction. None of the three arithmetic inputs should report direct namespace re-export or the resulting map-native-addon-binding exposes edge. All should retain their resolved native loads dependency. The plain = control must still report a re-export with the source-to-binding exposes relationship, and the variable-binding control must still report a require binding with an imports relationship. Keep the independent addon-to-requested-binding association distinct when evaluating the edges. No particular implementation or classification of untested logical assignments is prescribed.

Analysis or suspected cause

Source-derived explanation: assignment handling checks the native-request RHS and export target without distinguishing the assignment operator. The graph projection maps that recorded kind to the JavaScript-source exposes edge and separately creates the legitimate addon-to-binding association. Arithmetic can coerce values or fail; it does not directly forward the addon namespace. No actual addon behavior is inferred and no specific implementation is prescribed.

Additional context

Open #3884 concerns local methods mistaken for native loaders; this report uses an ordinary unshadowed require and preserves the real request. Open #3885 concerns omitted optional-call bindings, #3952 concerns operator-insensitive structural fingerprints, and #3948 concerns timeout-scope ownership. Closed #2667 concerns erased TypeScript names, and #2646 concerns logical-assignment value inference. Each has a different trigger and restoration signal. Logical assignments, real addons, real applications, other platforms and graph-restoring workarounds were not verified. NOT_ENOUGH_INFO: no last-known-good revision or production prevalence is established.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4202 ### 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 Evidence, artifacts, or reconstruction ### Observed behavior `analyze-javascript-application` reports a native-addon re-export when a CommonJS export property uses `+=`, `-=`, or `*=` with an ordinary native `require` on the right. Each tested input produces a `binding_kind: "re-export"` observation and a high-confidence `exposes` edge from the JavaScript asset to the requested binding. These operators compute an arithmetic or concatenation result rather than forwarding the addon namespace. The addon request itself is legitimate and remains a resolved `loads` dependency. ### Reproduction Use the built source checkout at either tested revision below with its pinned Node.js runtime on PATH. Run this block from the repository root. It creates five inert fixtures and isolated home/config/cache/data directories, invokes the real public CLI sequentially, prints the relevant results, and removes the temporary inputs. It does not execute `main.cjs` or load `fake.node`. ```sh python3 - <<'PY' import json import os from pathlib import Path import subprocess from tempfile import TemporaryDirectory cases = { "plus": 'exports.value += require("./fake.node");\n', "minus": 'exports.value -= require("./fake.node");\n', "multiply": 'exports.value *= require("./fake.node");\n', "plain": 'exports.value = require("./fake.node");\n', "request": 'const addon = require("./fake.node");\n', } with TemporaryDirectory(prefix="rea-native-assignment-") as temporary: root = Path(temporary) environment = dict(os.environ) for key in ("HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_DATA_HOME"): folder = root / key.lower() folder.mkdir() environment[key] = str(folder) for name, source in cases.items(): app = root / name app.mkdir() (app / "main.cjs").write_text(source) (app / "fake.node").write_bytes(b"fixture bytes, never executed") result = subprocess.run( ["node", "scripts/rea.mjs", "analyze-javascript-application", str(app), "--format", "json", "--full-output"], env=environment, text=True, capture_output=True, timeout=60, ) envelope = json.loads(result.stdout) assert result.returncode == 0 and envelope["ok"], envelope analysis = envelope["data"]["normalized_result"] graph = analysis["graph"] binding = [observation for node in graph["nodes"] if node["kind"] == "native-export" for observation in node["observations"]] mapped = [edge for edge in graph["edges"] if edge["evidence"]["extractor"]["operation"] == "map-native-addon-binding"] loads = [edge for edge in graph["edges"] if edge["relation"] == "loads" and edge["properties"].get("specifier") == "./fake.node"] assert len(binding) == len(mapped) == len(loads) == 1 print(name, "exit", result.returncode, "bindings", analysis["summary"]["native_addon_bindings"], "resolved", analysis["summary"]["resolved_native_addon_bindings"], "kind", binding[0]["properties"]["binding_kind"], "relation", mapped[0]["relation"], "loads", len(loads), "coverage", mapped[0]["evidence"]["coverage"]["status"]) PY ``` The `plus`, `minus`, and `multiply` inputs test arithmetic compound assignments. `plain` controls direct forwarding of the addon namespace, and `request` controls a legitimate native binding with no re-export. ### Expected behavior The native binding kind and the JavaScript asset's relationship should distinguish requesting an addon from re-exporting its value. The three arithmetic assignments may identify the native request, but they should not identify direct namespace re-exports or create source-to-binding `exposes` edges on that basis. Preserve the resolved `loads` dependency. The plain-assignment control should retain its re-export and source-to-binding `exposes` edge, and the variable-binding control should retain its `require` binding and `imports` edge. This expectation concerns the static meaning of the written operations, not proof that the program executes or that the native binary implements any export. Logical assignments such as `||=`, `&&=`, and `??=` can conditionally forward a value and are outside this report's tested scope. The [native-binding type](https://github.com/morluto/rea/blob/1b828dbb1011d80f44b3ae99efb9554991d71261/src/domain/javascript/electronStaticAnalysisTypes.ts#L176-L187) distinguishes a requested binding from a re-export. The [reconstruction contract](https://github.com/morluto/rea/blob/1b828dbb1011d80f44b3ae99efb9554991d71261/docs/javascript-artifact-reconstruction.md#L219-L242) recovers native requests as inert data; its [authority limits](https://github.com/morluto/rea/blob/1b828dbb1011d80f44b3ae99efb9554991d71261/docs/javascript-artifact-reconstruction.md#L313-L319) distinguish syntax from static relationships. The [graph documentation](https://github.com/morluto/rea/blob/1b828dbb1011d80f44b3ae99efb9554991d71261/docs/javascript-application-graph.md#L93-L103) distinguishes source imports from the addon association, and the [existing control](https://github.com/morluto/rea/blob/1b828dbb1011d80f44b3ae99efb9554991d71261/src/domain/javascript/electronNativeLoaderPaths.test.ts#L66-L96) preserves plain-assignment re-exports. ### Impact and scope P2, supported. The public static reconstruction result contains a wrong typed binding and a wrong high-confidence graph relationship for three supported arithmetic assignment forms. This changes machine-readable graph behavior, beyond wording or presentation. The observed defect is bounded; the valid native dependency survives, and no broader application or downstream consumer failure was tested. Workaround status is unverified. The outputs retain a source range and resolved dependency, so an analyst can inspect the operator manually. No caller option or other supported operation was verified to return a corrected native-binding graph while preserving the exact input. ### REA version Reproduced from source revision `62bf09809ef7bb224028916424f8f07652a20f4f` and freshly fetched main `1b828dbb1011d80f44b3ae99efb9554991d71261`. Both checkouts were built with `npm run build:cached`, their locked dependencies, and the pinned toolchain. The launcher prints `6.5.0`; the published package and release tag were not executed. ### Runtime and platform Node.js v24.18.0; npm 11.16.0; macOS arm64. Real public CLI, inert target source, and placeholder `.node` bytes. No MCP, external provider, target-source execution or native-addon loading was performed. ### Target and artifact context Five isolated directories, each containing one `main.cjs` and a sibling `fake.node` whose bytes are `fixture bytes, never executed`. The three arithmetic assignments and the two controls are inline above. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction. The assignment classifier records a direct native re-export before graph projection. ### Relevant output or evidence The verifier ran two baseline passes. The publication review independently reran the exact five-case fixture once at the baseline and once in a separate freshly built current-main checkout. Every pass returned the same classification and source-edge results. Both publication-review passes completed successfully. Every call exited 0, retained one resolved native binding, parsed one JavaScript file without parse failures, and retained one resolved `loads` dependency. The standalone reproduction printed: ```text plus exit 0 bindings 1 resolved 1 kind re-export relation exposes loads 1 coverage complete minus exit 0 bindings 1 resolved 1 kind re-export relation exposes loads 1 coverage complete multiply exit 0 bindings 1 resolved 1 kind re-export relation exposes loads 1 coverage complete plain exit 0 bindings 1 resolved 1 kind re-export relation exposes loads 1 coverage complete request exit 0 bindings 1 resolved 1 kind require relation imports loads 1 coverage complete ``` For every arithmetic case, the `native-export` observation has `namespace_access: true`, `requested_members: []`, `resolved_path: "fake.node"`, and `native_export_verification: "not-performed"`. The incorrect JavaScript-source edge has extractor operation `map-native-addon-binding`, `state: "inferred"`, `confidence: "high"`, and complete coverage. The distinct native-addon-to-binding edge with operation `associate-native-addon-requested-exports` also uses `exposes`; that association exists in the legitimate variable-binding control and is not the failing relationship in this report. Observed public-entry evidence; local verification reproduced the failure with complete inline fixtures. The original verifier also passed all nine existing native-loader tests. Source inspection confirms that the native extractor and graph projection remain unchanged on main. Shared helpers changed from the baseline, so applicability at the tested main revision was established by the additional public CLI reproduction rather than inferred from unchanged cited files alone. Main advanced again to `5e30969cc5f1255d44ec7a1f1ca8dbac0af5e7c4` during final review. All inspected files in this report are unchanged from the tested main revision; applicability to that newer revision is source-inspected only, without another runtime run. ### Typed outcome or recovery expected Run the same public reproduction. None of the three arithmetic inputs should report direct namespace re-export or the resulting `map-native-addon-binding` `exposes` edge. All should retain their resolved native `loads` dependency. The plain `=` control must still report a re-export with the source-to-binding `exposes` relationship, and the variable-binding control must still report a `require` binding with an `imports` relationship. Keep the independent addon-to-requested-binding association distinct when evaluating the edges. No particular implementation or classification of untested logical assignments is prescribed. ### Analysis or suspected cause Source-derived explanation: [assignment handling](https://github.com/morluto/rea/blob/1b828dbb1011d80f44b3ae99efb9554991d71261/src/domain/javascript/electronStaticAnalysisNative.ts#L156-L171) checks the native-request RHS and export target without distinguishing the assignment operator. The [graph projection](https://github.com/morluto/rea/blob/1b828dbb1011d80f44b3ae99efb9554991d71261/src/application/javascript/ElectronBoundaryGraphNative.ts#L98-L145) maps that recorded kind to the JavaScript-source `exposes` edge and separately creates the legitimate addon-to-binding association. Arithmetic can coerce values or fail; it does not directly forward the addon namespace. No actual addon behavior is inferred and no specific implementation is prescribed. ### Additional context Open [#3884](https://github.com/morluto/rea/issues/3884) concerns local methods mistaken for native loaders; this report uses an ordinary unshadowed `require` and preserves the real request. Open [#3885](https://github.com/morluto/rea/issues/3885) concerns omitted optional-call bindings, [#3952](https://github.com/morluto/rea/issues/3952) concerns operator-insensitive structural fingerprints, and [#3948](https://github.com/morluto/rea/issues/3948) concerns timeout-scope ownership. Closed [#2667](https://github.com/morluto/rea/issues/2667) concerns erased TypeScript names, and [#2646](https://github.com/morluto/rea/issues/2646) concerns logical-assignment value inference. Each has a different trigger and restoration signal. Logical assignments, real addons, real applications, other platforms and graph-restoring workarounds were not verified. NOT_ENOUGH_INFO: no last-known-good revision or production prevalence is established.
Author
Owner

@x1mm4nu3l commented on GitHub (Oct 20, 2026):

Reproduced on main 2538214e2, Linux/Node 24.18.0, using the locked node-pty addon. Executing exports.value += require("./addon.node") returns a string, but CLI and stdio MCP analysis label it a re-export; the CLI adds an exposes edge. Plain assignment returns the original addon object. The subtraction/multiplication fixtures also misclassify, while a variable-bound require correctly remains an import. This reaches a real loadable addon.

<!-- gh-comment-id:6286504708 --> @x1mm4nu3l commented on GitHub (Oct 20, 2026): Reproduced on main 2538214e2, Linux/Node 24.18.0, using the locked node-pty addon. Executing `exports.value += require("./addon.node")` returns a string, but CLI and stdio MCP analysis label it a re-export; the CLI adds an exposes edge. Plain assignment returns the original addon object. The subtraction/multiplication fixtures also misclassify, while a variable-bound require correctly remains an import. This reaches a real loadable addon.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

The CommonJS export owner selects an RHS only for operator =; every compound assignment yields null. The native re-export extractor consumes that owned value and exits when null, so +=, -= and *= cannot assert direct forwarding. The public regression includes +=, asserts the complete binding set omits it, and preserves its loads edge alongside plain assignment and variable/optional controls.

Implementation: javascriptCommonJsExportReceivers.ts, electronStaticAnalysisNative.ts.

The relevant regression checks in javascriptNativeBindingOwnership.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380734042 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. The CommonJS export owner selects an RHS only for operator =; every compound assignment yields null. The native re-export extractor consumes that owned value and exits when null, so +=, -= and *= cannot assert direct forwarding. The public regression includes +=, asserts the complete binding set omits it, and preserves its loads edge alongside plain assignment and variable/optional controls. Implementation: [javascriptCommonJsExportReceivers.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/javascript/javascriptCommonJsExportReceivers.ts#L307), [electronStaticAnalysisNative.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/javascript/electronStaticAnalysisNative.ts#L220). The relevant regression checks in [javascriptNativeBindingOwnership.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/acceptance/applications/javascriptNativeBindingOwnership.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
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#1648
No description provided.