[GH-ISSUE #3313] [Bug] Resource ownership tracing reports a resolved release after handle replacement #1183

Closed
opened 2026-10-15 18:31:49 +00:00 by bsakaguchi · 0 comments
Owner

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

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

Semantic resource ownership tracing links a destroy() call to an earlier stream acquisition after the stream variable is unconditionally replaced with an unrelated local object. The call targets the replacement object and cannot release the original stream through that binding, but analysis retains a resolved releases edge to the original createReadStream resource and admits it into the default ownership trace without candidate-edge opt-in.

Both analyze-javascript-application and trace-javascript-semantics succeed. The ownership trace reports status: "found", three traversed nodes, two traversed relations, and zero relevant unknowns. It contains a resolved acquires edge at line 2 and the false resolved releases edge at sample.mjs, line 4, columns 0 through 16. Overall coverage is partial, but the release relationship remains resolved. The public CLI reproduction was observed and locally reproduced.

Reproduction

  1. Clone a clean checkout, select the reproduced baseline, install the locked dependencies, and build the runtime.
git clone https://github.com/morluto/rea.git rea-resource-repro
cd rea-resource-repro
git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063
npm ci
npm run build:cached

The observed reproduction used an installed and built runtime. The clean-checkout preparation above supplies the prerequisites and is not a claim that installation or building was independently rerun during the reproduction.

  1. From the repository root, create the inline source fixture and isolated home, analyze it, create the trace input from the unchanged returned application Evidence, and inspect the ownership trace.
work="$(mktemp -d)"
mkdir -p "$work/app" "$work/home"
cat > "$work/app/sample.mjs" <<'JS'
import fs from 'node:fs';
let stream = fs.createReadStream('input.txt');
stream = { destroy() {} };
stream.destroy();
JS
env HOME="$work/home" node scripts/rea.mjs analyze-javascript-application "$work/app" --json > "$work/application.json"
node --input-type=module - "$work" <<'JS'
import { readFileSync, writeFileSync } from 'node:fs';
const root = process.argv[2];
const application = JSON.parse(readFileSync(`${root}/application.json`, 'utf8'));
const resource = application.normalized_result.semantic_graph.nodes.find(
  node => node.kind === 'resource' && node.properties.method === 'createReadStream'
);
writeFileSync(`${root}/trace-input.json`, JSON.stringify({
  application,
  query: {
    seed: { kind: 'semantic-node', node_id: resource.node_id },
    direction: 'ownership'
  }
}));
JS
env HOME="$work/home" node scripts/rea.mjs trace-javascript-semantics "$work/trace-input.json" --json > "$work/trace.json"
node --input-type=module - "$work/trace.json" <<'JS'
import { readFileSync } from 'node:fs';
const result = JSON.parse(readFileSync(process.argv[2], 'utf8')).normalized_result;
for (const relation of result.relations) {
  console.log(JSON.stringify({
    relation: relation.relation,
    resolution: relation.resolution,
    location: relation.evidence.location.value
  }));
}
console.log(JSON.stringify({status: result.status, summary: result.summary}));
JS

REA only parses this source. It does not open the stream, and input.txt need not exist.

  1. For the discriminating unchanged-handle control, replace the source file with the following content, then repeat the analysis, trace-input creation, trace, and result-printing commands from step 2 without recreating the reassigned fixture.
cat > "$work/app/sample.mjs" <<'JS'
import fs from 'node:fs';
const stream = fs.createReadStream('input.txt');
stream.destroy();
JS

Analysis of this unchanged-handle control was also executed and retains a resolved release. The control's trace is part of the restoration check; a separately observed control trace is not claimed.

Expected behavior

The reassigned fixture must not claim a resolved release of the original stream at the final call. If analysis cannot establish the replacement's identity, it should retain uncertainty rather than prove the wrong resource target. The unchanged-handle control should keep its valid acquisition/release relationship. This requirement does not ask static analysis to prove that any path executed.

docs/javascript-application-workflows.md:76 includes built-in resource acquisition/release analysis. docs/javascript-application-workflows.md:96 through line 99 require unresolved handles and unsupported API shapes to remain explicit unknowns. docs/javascript-application-workflows.md:101 through line 105 define a resolved edge as unique static target identity, distinguish it from evidence of execution, and exclude candidate edges by default.

Impact and scope

Impact level P2, supported by the observed bounded reproduction. A known replacement of a resource handle produces an incorrect resolved lifecycle relationship in retained Evidence and default ownership tracing. This can mislead resource-lifetime analysis into associating cleanup with a stream that the call no longer targets.

Workaround status is unverified. No alternative REA ownership query was verified to preserve valid release relationships while removing this false association. Inspecting the original source reveals the replacement but leaves the stored semantic relationship incorrect.

REA version

6.4.0 (main at 8dde6370d0)

Runtime and platform

Node.js v26.10.0; macOS 27.0.1. Architecture was not recorded for this reproduction.

Target and artifact context

An extracted JavaScript source directory containing the inline sample.mjs above. Analysis returns application Evidence containing the semantic graph; the trace input uses that unchanged Evidence and the actual createReadStream resource node ID. No filesystem stream or native analysis engine is launched by this reproduction.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

The following is a compact transcription of the reported ownership-trace observations, rather than a complete raw JSON result.

status: "found"
traversed nodes: 3
traversed relations: 2
relevant unknowns: 0
acquires: resolved at sample.mjs line 2
releases: resolved to the original createReadStream resource at sample.mjs line 4, columns 0 through 16
overall coverage: partial

Typed outcome or recovery expected

Repeat the reassigned-handle fixture and verify that the final call no longer has a resolved releases edge to the original stream. Any retained possible lifecycle relation must carry the applicable uncertainty. Repeat the unchanged-handle control and preserve its resolved release, so restoration cannot simply remove all lifecycle relationships.

Analysis or suspected cause

Source-derived reasoning: src/domain/javascript/javascriptSemanticResources.ts:96 resolves the release receiver's lexical binding and links acquisitions by binding ID without testing the intervening replacement. src/application/javascript/JavaScriptSemanticGraphResourceProjection.ts:61 emits a resolved releases edge for that complete operation. These source locators explain how unchanged lexical binding identity can be mistaken for unchanged resource identity; the executed public ownership trace supplies the failure evidence.

Additional context

Related #1842, closed as completed, concerns Electron interop helper names attributing unrelated constructors to Electron; this report concerns acquisition/release matching after replacement of a resource handle rather than helper-derived module identity.

Related #3207, closed as completed, concerns local export specifiers creating false resolved writes relations; this report concerns a releases target chosen from stale resource binding identity rather than reference-role classification.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3313 ### 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 Semantic resource ownership tracing links a `destroy()` call to an earlier stream acquisition after the stream variable is unconditionally replaced with an unrelated local object. The call targets the replacement object and cannot release the original stream through that binding, but analysis retains a resolved `releases` edge to the original `createReadStream` resource and admits it into the default ownership trace without candidate-edge opt-in. Both `analyze-javascript-application` and `trace-javascript-semantics` succeed. The ownership trace reports `status: "found"`, three traversed nodes, two traversed relations, and zero relevant unknowns. It contains a resolved `acquires` edge at line 2 and the false resolved `releases` edge at `sample.mjs`, line 4, columns 0 through 16. Overall coverage is partial, but the release relationship remains resolved. The public CLI reproduction was observed and locally reproduced. ### Reproduction 1. Clone a clean checkout, select the reproduced baseline, install the locked dependencies, and build the runtime. ```sh git clone https://github.com/morluto/rea.git rea-resource-repro cd rea-resource-repro git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063 npm ci npm run build:cached ``` The observed reproduction used an installed and built runtime. The clean-checkout preparation above supplies the prerequisites and is not a claim that installation or building was independently rerun during the reproduction. 2. From the repository root, create the inline source fixture and isolated home, analyze it, create the trace input from the unchanged returned application Evidence, and inspect the ownership trace. ```bash work="$(mktemp -d)" mkdir -p "$work/app" "$work/home" cat > "$work/app/sample.mjs" <<'JS' import fs from 'node:fs'; let stream = fs.createReadStream('input.txt'); stream = { destroy() {} }; stream.destroy(); JS env HOME="$work/home" node scripts/rea.mjs analyze-javascript-application "$work/app" --json > "$work/application.json" node --input-type=module - "$work" <<'JS' import { readFileSync, writeFileSync } from 'node:fs'; const root = process.argv[2]; const application = JSON.parse(readFileSync(`${root}/application.json`, 'utf8')); const resource = application.normalized_result.semantic_graph.nodes.find( node => node.kind === 'resource' && node.properties.method === 'createReadStream' ); writeFileSync(`${root}/trace-input.json`, JSON.stringify({ application, query: { seed: { kind: 'semantic-node', node_id: resource.node_id }, direction: 'ownership' } })); JS env HOME="$work/home" node scripts/rea.mjs trace-javascript-semantics "$work/trace-input.json" --json > "$work/trace.json" node --input-type=module - "$work/trace.json" <<'JS' import { readFileSync } from 'node:fs'; const result = JSON.parse(readFileSync(process.argv[2], 'utf8')).normalized_result; for (const relation of result.relations) { console.log(JSON.stringify({ relation: relation.relation, resolution: relation.resolution, location: relation.evidence.location.value })); } console.log(JSON.stringify({status: result.status, summary: result.summary})); JS ``` REA only parses this source. It does not open the stream, and `input.txt` need not exist. 3. For the discriminating unchanged-handle control, replace the source file with the following content, then repeat the analysis, trace-input creation, trace, and result-printing commands from step 2 without recreating the reassigned fixture. ```sh cat > "$work/app/sample.mjs" <<'JS' import fs from 'node:fs'; const stream = fs.createReadStream('input.txt'); stream.destroy(); JS ``` Analysis of this unchanged-handle control was also executed and retains a resolved release. The control's trace is part of the restoration check; a separately observed control trace is not claimed. ### Expected behavior The reassigned fixture must not claim a resolved release of the original stream at the final call. If analysis cannot establish the replacement's identity, it should retain uncertainty rather than prove the wrong resource target. The unchanged-handle control should keep its valid acquisition/release relationship. This requirement does not ask static analysis to prove that any path executed. `docs/javascript-application-workflows.md:76` includes built-in resource acquisition/release analysis. `docs/javascript-application-workflows.md:96` through line 99 require unresolved handles and unsupported API shapes to remain explicit unknowns. `docs/javascript-application-workflows.md:101` through line 105 define a resolved edge as unique static target identity, distinguish it from evidence of execution, and exclude candidate edges by default. ### Impact and scope Impact level P2, supported by the observed bounded reproduction. A known replacement of a resource handle produces an incorrect resolved lifecycle relationship in retained Evidence and default ownership tracing. This can mislead resource-lifetime analysis into associating cleanup with a stream that the call no longer targets. Workaround status is unverified. No alternative REA ownership query was verified to preserve valid release relationships while removing this false association. Inspecting the original source reveals the replacement but leaves the stored semantic relationship incorrect. ### REA version 6.4.0 (main at 8dde6370d0809ad04cda3342d4e6f290f48aa063) ### Runtime and platform Node.js v26.10.0; macOS 27.0.1. Architecture was not recorded for this reproduction. ### Target and artifact context An extracted JavaScript source directory containing the inline `sample.mjs` above. Analysis returns application Evidence containing the semantic graph; the trace input uses that unchanged Evidence and the actual `createReadStream` resource node ID. No filesystem stream or native analysis engine is launched by this reproduction. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence The following is a compact transcription of the reported ownership-trace observations, rather than a complete raw JSON result. ```text status: "found" traversed nodes: 3 traversed relations: 2 relevant unknowns: 0 acquires: resolved at sample.mjs line 2 releases: resolved to the original createReadStream resource at sample.mjs line 4, columns 0 through 16 overall coverage: partial ``` ### Typed outcome or recovery expected Repeat the reassigned-handle fixture and verify that the final call no longer has a resolved `releases` edge to the original stream. Any retained possible lifecycle relation must carry the applicable uncertainty. Repeat the unchanged-handle control and preserve its resolved release, so restoration cannot simply remove all lifecycle relationships. ### Analysis or suspected cause Source-derived reasoning: `src/domain/javascript/javascriptSemanticResources.ts:96` resolves the release receiver's lexical binding and links acquisitions by binding ID without testing the intervening replacement. `src/application/javascript/JavaScriptSemanticGraphResourceProjection.ts:61` emits a resolved `releases` edge for that complete operation. These source locators explain how unchanged lexical binding identity can be mistaken for unchanged resource identity; the executed public ownership trace supplies the failure evidence. ### Additional context Related [#1842](https://github.com/morluto/rea/issues/1842), closed as completed, concerns Electron interop helper names attributing unrelated constructors to Electron; this report concerns acquisition/release matching after replacement of a resource handle rather than helper-derived module identity. Related [#3207](https://github.com/morluto/rea/issues/3207), closed as completed, concerns local export specifiers creating false resolved `writes` relations; this report concerns a `releases` target chosen from stale resource binding identity rather than reference-role classification.
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#1183
No description provided.