[GH-ISSUE #8554] [Bug] Socket resource analysis misclassifies unary arguments #6645

Open
opened 2026-11-01 21:12:54 +00:00 by bsakaguchi · 1 comment
Owner

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

Originally assigned to: @kaoru0822-kitauji on GitHub.

Before submitting

  • I searched open and closed issues for related reports.
  • This report uses synthetic source and contains no credentials or sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

Static JavaScript analysis removes socket acquisition/release facts for valid unary arguments and retains them for arguments that throw before returning a handle. The public CLI succeeds in every case below, so this appears as incorrect semantic graph content rather than an analysis error.

For net.connect(EXPR, '127.0.0.1'), followed by socket.destroy():

EXPR Node 24.22.1, actually executed Built CLI resource graph
0 Returns a socket 1 resource, 2 resolved relations
-0 Returns a socket 0 resources, 0 lifecycle relations
+65536 Throws ERR_SOCKET_BAD_PORT 1 resource, 2 resolved relations
!0 Throws ERR_INVALID_ARG_TYPE 1 resource, 2 resolved relations
~0 Throws ERR_SOCKET_BAD_PORT 1 resource, 2 resolved relations
~4294967295.5 Returns a socket; expression evaluates to 0 0 resources, 0 lifecycle relations
typeof 1.5 Returns a socket; "number" is treated as a Unix socket path 0 resources, 0 lifecycle relations
typeof 1 Returns a socket; same "number" path 1 resource, 2 resolved relations

The retained relations are acquires and releases, both resolution: resolved. The resource-lifecycle coverage is partial in these fixtures; this report does not claim that REA advertises complete runtime coverage. There is no case-specific unknown explaining the incorrect removal or retention.

Reproduction

Build the pinned source checkout with npm run build:cached. Create an extracted application directory with one main.mjs:

import net from 'node:net';
export function run() {
  const socket = net.connect(-0, '127.0.0.1');
  socket.destroy();
}

Run:

node scripts/rea.mjs analyze-javascript-application /absolute/path/to/fixture --json

Inspect normalized_result.semantic_graph: select nodes whose kind is resource and relations whose relation is acquires or releases. Replace only -0 with 0, +65536, or another table expression and repeat.

I ran the compiled CLI for every table row, using a separate temporary application per case. Static analysis did not execute these application functions. Separately, a bounded native Node comparison actually called each API with the expression's value, observed synchronous return/throw, immediately destroyed returned sockets, and waited for close. Unix path comparisons ran under an owned temporary working directory. A successful connection or TLS handshake was not tested or needed to establish handle return and argument validation.

Expected behavior

The filter introduced in #8137 aims to remove a resource fact when an exactly known argument prevents the call from returning a socket. Unary syntax must preserve that distinction: -0 is accepted as zero, +65536 is still out of range, and other operators can produce a different type or value from their numeric operand. Valid forms must not disappear solely because of the operator or operand spelling.

Impact and scope

Observed impact: missing potential resource ownership/teardown facts for valid calls, and resolved acquisition/release relations for calls that throw before assigning the handle.

Sibling checks actually run:

  • Native Node and the production semantic analyzer: all table forms, plus -0e0, underflowing -1e-324, -1, 65535, 65536, +65535, +1.5, and !1.5, across net.connect, net.createConnection, and tls.connect.
  • Built public CLI: -0 and +65536 for all three APIs; the remaining table forms for net.connect.
  • Working comparisons: 0, 65535, and +65535 retain acquisition/release; -1, bare 65536, and +1.5 are correctly filtered. typeof 1 provides a same-runtime-value control for typeof 1.5.

All three supported socket APIs share the same filter. Other numeric interpretation sites inspected did not establish another instance of this failure. No Windows/Linux run, stdio MCP run, published npm package check, real application frequency estimate, or before/after runtime bisect was performed. Last known good version and production frequency: NOT_ENOUGH_INFO.

REA version

Source df25bc8068b4d62c0e8db2b38648648ed831ae0e, package 6.8.0. The runtime and analysis worker were rebuilt successfully before the compiled CLI checks.

Runtime and platform

Node v24.22.1, macOS 26.3, arm64. No external analysis provider required.

Target and artifact context

One small authored ESM source file per application directory.

Interface

CLI, with separate production semantic API and native Node comparisons.

Earliest failure stage

Native analysis or reconstruction: static JavaScript semantic resource collection.

Relevant output or evidence

For -0, the production semantic analyzer returns resourceOperations: []; the built CLI graph likewise has no resource node or lifecycle relation. For +65536, !0, and ~0, it emits acquire and release operations with resolution: complete, projected as resolved graph relations. The table gives the compact executed CLI/native comparison.

Analysis or suspected cause

Source-derived explanation: rejectsSocketPortLiteral has a correct numeric-literal range check, but its unary branch only asks whether the operator is - or the numeric operand is fractional. That both rejects accepted negative zero and ignores the evaluated type/range of other unary expressions. The predicate is applied before both acquisition and release collection, so one decision affects both facts.

Resource projection consumes those facts without another validity decision. This is one producing-boundary issue, not separate bugs for each API or graph consumer.

Additional context

Related merged PR: #8137. Its explanation that unary minus over a numeric literal is always out of range overlooks negative zero. The expanded cases also expose the separate positive/type mistakes within that same unary branch.

Researched Node through Context7 and DeepWiki, then verified against Node v24.22.1 validatePort, its net caller, and the actual pinned runtime. Node checks the evaluated value and accepts zero by default. Directly executing its validator accepted -0 as 0 and rejected +65536.

Proposed verification for a fix: interpret only supported exact unary values at the owning boundary, apply one argument-validity rule, and retain conservative behavior for undecidable expressions. Run the table and all three supported APIs through semantic collection and public CLI/MCP projection, preserving correct boundary controls. Do not execute target code to infer these static facts. No implementation or provider behavior change is included in this report.

Originally created by @morluto on GitHub (Nov 1, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8554 Originally assigned to: @kaoru0822-kitauji on GitHub. ### Before submitting - [x] I searched open and closed issues for related reports. - [x] This report uses synthetic source and contains no credentials or sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior Static JavaScript analysis removes socket acquisition/release facts for valid unary arguments and retains them for arguments that throw before returning a handle. The public CLI succeeds in every case below, so this appears as incorrect semantic graph content rather than an analysis error. For `net.connect(EXPR, '127.0.0.1')`, followed by `socket.destroy()`: | EXPR | Node 24.22.1, actually executed | Built CLI resource graph | | --- | --- | --- | | `0` | Returns a socket | 1 resource, 2 resolved relations | | `-0` | Returns a socket | 0 resources, 0 lifecycle relations | | `+65536` | Throws `ERR_SOCKET_BAD_PORT` | 1 resource, 2 resolved relations | | `!0` | Throws `ERR_INVALID_ARG_TYPE` | 1 resource, 2 resolved relations | | `~0` | Throws `ERR_SOCKET_BAD_PORT` | 1 resource, 2 resolved relations | | `~4294967295.5` | Returns a socket; expression evaluates to `0` | 0 resources, 0 lifecycle relations | | `typeof 1.5` | Returns a socket; `"number"` is treated as a Unix socket path | 0 resources, 0 lifecycle relations | | `typeof 1` | Returns a socket; same `"number"` path | 1 resource, 2 resolved relations | The retained relations are `acquires` and `releases`, both `resolution: resolved`. The resource-lifecycle coverage is `partial` in these fixtures; this report does **not** claim that REA advertises complete runtime coverage. There is no case-specific unknown explaining the incorrect removal or retention. ### Reproduction Build the pinned source checkout with `npm run build:cached`. Create an extracted application directory with one `main.mjs`: ```js import net from 'node:net'; export function run() { const socket = net.connect(-0, '127.0.0.1'); socket.destroy(); } ``` Run: ```sh node scripts/rea.mjs analyze-javascript-application /absolute/path/to/fixture --json ``` Inspect `normalized_result.semantic_graph`: select nodes whose `kind` is `resource` and relations whose `relation` is `acquires` or `releases`. Replace only `-0` with `0`, `+65536`, or another table expression and repeat. I ran the compiled CLI for every table row, using a separate temporary application per case. Static analysis did not execute these application functions. Separately, a bounded native Node comparison actually called each API with the expression's value, observed synchronous return/throw, immediately destroyed returned sockets, and waited for close. Unix path comparisons ran under an owned temporary working directory. A successful connection or TLS handshake was not tested or needed to establish handle return and argument validation. ### Expected behavior The filter introduced in #8137 aims to remove a resource fact when an exactly known argument prevents the call from returning a socket. Unary syntax must preserve that distinction: `-0` is accepted as zero, `+65536` is still out of range, and other operators can produce a different type or value from their numeric operand. Valid forms must not disappear solely because of the operator or operand spelling. ### Impact and scope Observed impact: missing potential resource ownership/teardown facts for valid calls, and resolved acquisition/release relations for calls that throw before assigning the handle. Sibling checks actually run: - Native Node and the production semantic analyzer: all table forms, plus `-0e0`, underflowing `-1e-324`, `-1`, `65535`, `65536`, `+65535`, `+1.5`, and `!1.5`, across `net.connect`, `net.createConnection`, and `tls.connect`. - Built public CLI: `-0` and `+65536` for all three APIs; the remaining table forms for `net.connect`. - Working comparisons: `0`, `65535`, and `+65535` retain acquisition/release; `-1`, bare `65536`, and `+1.5` are correctly filtered. `typeof 1` provides a same-runtime-value control for `typeof 1.5`. All three supported socket APIs share the same filter. Other numeric interpretation sites inspected did not establish another instance of this failure. No Windows/Linux run, stdio MCP run, published npm package check, real application frequency estimate, or before/after runtime bisect was performed. Last known good version and production frequency: **NOT_ENOUGH_INFO**. ### REA version Source `df25bc8068b4d62c0e8db2b38648648ed831ae0e`, package 6.8.0. The runtime and analysis worker were rebuilt successfully before the compiled CLI checks. ### Runtime and platform Node v24.22.1, macOS 26.3, arm64. No external analysis provider required. ### Target and artifact context One small authored ESM source file per application directory. ### Interface CLI, with separate production semantic API and native Node comparisons. ### Earliest failure stage Native analysis or reconstruction: static JavaScript semantic resource collection. ### Relevant output or evidence For `-0`, the production semantic analyzer returns `resourceOperations: []`; the built CLI graph likewise has no resource node or lifecycle relation. For `+65536`, `!0`, and `~0`, it emits `acquire` and `release` operations with `resolution: complete`, projected as resolved graph relations. The table gives the compact executed CLI/native comparison. ### Analysis or suspected cause Source-derived explanation: [rejectsSocketPortLiteral](https://github.com/morluto/rea/blob/df25bc8068b4d62c0e8db2b38648648ed831ae0e/src/domain/javascript/javascriptSemanticResources.ts#L253-L271) has a correct numeric-literal range check, but its unary branch only asks whether the operator is `-` or the numeric operand is fractional. That both rejects accepted negative zero and ignores the evaluated type/range of other unary expressions. The predicate is applied before both acquisition and release collection, so one decision affects both facts. [Resource projection](https://github.com/morluto/rea/blob/df25bc8068b4d62c0e8db2b38648648ed831ae0e/src/application/javascript/JavaScriptSemanticGraphResourceProjection.ts#L14-L35) consumes those facts without another validity decision. This is one producing-boundary issue, not separate bugs for each API or graph consumer. ### Additional context Related merged PR: #8137. Its explanation that unary minus over a numeric literal is always out of range overlooks negative zero. The expanded cases also expose the separate positive/type mistakes within that same unary branch. Researched Node through Context7 and DeepWiki, then verified against [Node v24.22.1 validatePort](https://github.com/nodejs/node/blob/v24.22.1/lib/internal/validators.js#L404-L413), [its net caller](https://github.com/nodejs/node/blob/v24.22.1/lib/net.js#L1632-L1639), and the actual pinned runtime. Node checks the evaluated value and accepts zero by default. Directly executing its validator accepted `-0` as `0` and rejected `+65536`. Proposed verification for a fix: interpret only supported exact unary values at the owning boundary, apply one argument-validity rule, and retain conservative behavior for undecidable expressions. Run the table and all three supported APIs through semantic collection and public CLI/MCP projection, preserving correct boundary controls. Do not execute target code to infer these static facts. No implementation or provider behavior change is included in this report.
Author
Owner

@XiaoBinGan commented on GitHub (Nov 2, 2026):

Every row of this table is fixed by #8266 (branch fix/net-port-zero), which decides a socket port and flag by the result of the unary operator rather than by its operand.

Re-checked against the branch:

EXPR Node 24.22.1 branch fix/net-port-zero
0 socket kept, 2 relations ✓
-0 socket kept, 2 relations ✓
+65536 ERR_SOCKET_BAD_PORT dropped, 0 ✓
!0 ERR_INVALID_ARG_TYPE dropped, 0 ✓
~0 ERR_SOCKET_BAD_PORT dropped, 0 ✓
~4294967295.5 socket, evaluates to 0 kept, 2 relations ✓
typeof 1.5 socket, "number" is a Unix path kept, 2 relations ✓
typeof 1 socket kept, 2 relations ✓

The mechanism is the one this report names: net.connect validates the evaluated argument. staticPortBits computes -operand, +operand, ~operand, and treats typeof as a non-empty path string, so -0 and ~4294967295.5 no longer lose their socket while +65536, !0 and ~0 no longer gain one. The same guard is also what the net.connect({ port }) object form and the tls options merge use.

Two boundaries the branch leaves deliberately undecided rather than guessing: a spread before the port shifts the runtime position, and 2^31..2^32-1 aborts the Node process rather than throwing.

Seven tests in javascriptSemanticAnalysis.socket_port_literal.test.ts cover this table. If it would help review, I can rebase #8266 onto current main and point the commit message at this issue instead of the generic description.

<!-- gh-comment-id:6572700518 --> @XiaoBinGan commented on GitHub (Nov 2, 2026): Every row of this table is fixed by #8266 (branch `fix/net-port-zero`), which decides a socket port and flag by the **result** of the unary operator rather than by its operand. Re-checked against the branch: | EXPR | Node 24.22.1 | branch `fix/net-port-zero` | | --- | --- | --- | | `0` | socket | kept, 2 relations ✓ | | `-0` | socket | **kept**, 2 relations ✓ | | `+65536` | `ERR_SOCKET_BAD_PORT` | **dropped**, 0 ✓ | | `!0` | `ERR_INVALID_ARG_TYPE` | **dropped**, 0 ✓ | | `~0` | `ERR_SOCKET_BAD_PORT` | **dropped**, 0 ✓ | | `~4294967295.5` | socket, evaluates to `0` | **kept**, 2 relations ✓ | | `typeof 1.5` | socket, `"number"` is a Unix path | **kept**, 2 relations ✓ | | `typeof 1` | socket | kept, 2 relations ✓ | The mechanism is the one this report names: `net.connect` validates the **evaluated** argument. `staticPortBits` computes `-operand`, `+operand`, `~operand`, and treats `typeof` as a non-empty path string, so `-0` and `~4294967295.5` no longer lose their socket while `+65536`, `!0` and `~0` no longer gain one. The same guard is also what the `net.connect({ port })` object form and the `tls` options merge use. Two boundaries the branch leaves deliberately undecided rather than guessing: a spread before the port shifts the runtime position, and `2^31..2^32-1` aborts the Node process rather than throwing. Seven tests in `javascriptSemanticAnalysis.socket_port_literal.test.ts` cover this table. If it would help review, I can rebase #8266 onto current `main` and point the commit message at this issue instead of the generic description.
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#6645
No description provided.