[GH-ISSUE #3362] [Bug] Reassigned child handles retain resolved signal and exit-listener ownership #1228

Closed
opened 2026-10-16 21:03:04 +00:00 by bsakaguchi · 1 comment
Owner

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

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

Static application analysis attributes kill and exit-listener operations to a spawned child after its result binding has been replaced with an unrelated object. Both stale ownership relationships are resolved, which can misidentify signal forwarding and lifecycle handling for the recovered child process.

The public analyze-javascript-application CLI exits with code 0. In normalized_result.semantic_graph.relations, a resolved forwards-signal relation runs from the line-4 SIGTERM signal node to the child-process node on line 2. A resolved listens-exit relation runs from that same child to the listener on line 5. The unchanged stable control also gets its expected resolved signal relation. No child-process ownership unknown is emitted for the replacement object. This public CLI behavior was observed and locally reproduced.

Reproduction

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

The reported observation used a checkout with dependencies installed and the runtime built. These clean-checkout commands supply those prerequisites; installation and building were not separately rerun for this report.

  1. From the repository root, create the inline source fixture and isolated configuration home, invoke the public CLI, and print the affected facts from its complete Evidence.
work=$(mktemp -d)
mkdir "$work/app" "$work/home"
cat > "$work/app/main.mjs" <<'JS'
import { spawn } from 'node:child_process';
let child = spawn('program');
child = { kill() {}, on() {} };
child.kill();
child.on('exit', () => {});
const stable = spawn('program');
stable.kill();
JS
python3 - "$work" <<'PY'
import json, os, pathlib, subprocess, sys
work = pathlib.Path(sys.argv[1])
child_environment = dict(os.environ, HOME=str(work / 'home'))
result = subprocess.run(
    ['node', 'scripts/rea.mjs', 'analyze-javascript-application',
     str(work / 'app'), '--format', 'json'],
    env=child_environment, capture_output=True, text=True, check=True)
evidence = json.loads(result.stdout)
(work / 'evidence.json').write_text(result.stdout)
graph = evidence['normalized_result']['semantic_graph']
print('exit:', result.returncode)
nodes = {node['node_id']: node for node in graph['nodes']}
for node in sorted(graph['nodes'], key=lambda n: ((n['identity']['source_range'] or {}).get('start', {}).get('line', 0), n['node_id'])):
    if node['kind'] in ['child-process', 'signal']:
        print('node', node['kind'], node['identity']['source_range'], node['properties'])
for relation in graph['relations']:
    if relation['relation'] in ['forwards-signal', 'listens-exit']:
        source = nodes[relation['source_node_id']]
        target = nodes[relation['target_node_id']]
        print('relation', relation['relation'], relation['resolution'], source['kind'], source['node_id'], source['identity']['source_range'], '=>', target['kind'], target['node_id'], target['identity']['source_range'])
print('effect_unknowns', [(u['family'], u['detail']) for u in graph['unknowns'] if u['family'] in ['timer', 'child-process', 'event', 'request', 'boundary']])
PY

The analyzer reads the target as text and does not execute its calls. The fixture includes the unchanged stable binding as a discriminating control.

  1. Inspect the endpoint source ranges and resolutions for forwards-signal and listens-exit, and the child-process entries in graph.unknowns. The replacement object's line-4 and line-5 calls are resolved to the child spawned on line 2; the stable line-7 signal resolves to the child spawned on line 6.

Expected behavior

Calls on the replacement object on lines 4 and 5 must not resolve as operations on the child spawned on line 2. Preserve uncertainty if the extractor does not model the assignment precisely. The unchanged stable child on lines 6 and 7 should retain its resolved signal relationship.

docs/javascript-application-workflows.md:72-76 promises asynchronous child-process creation and ownership extraction. docs/javascript-application-workflows.md:96-106 requires unresolved handles and unsupported shapes to stay explicit unknowns, and defines a resolved relationship as a unique admitted static target. Resolution does not prove execution.

Impact and scope

Impact level P2, supported by the observed bounded reassignment. Child-process lifecycle analysis returns incorrect signal and listener ownership for an explicit replacement of a child handle. Analysis does not actually spawn a process or deliver a signal.

Workaround status is unverified. The unchanged-binding control was verified, but no caller-selectable option that restores the affected ownership result on the original artifact was verified.

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

A local directory containing the inline main.mjs fixture. The CLI returns application Evidence containing normalized_result.semantic_graph, and the reproduction retains the complete Evidence in evidence.json. No native engine, browser, or external service is needed, and the analyzed source does not execute.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The following is a compact transcription of the reported graph observations, not a raw JSON excerpt.

CLI exit code: 0
forwards-signal: resolved, line-4 SIGTERM signal => line-2 child-process
listens-exit: resolved, line-2 child-process => line-5 listener
stable control: resolved signal relationship from line 7 to the child on line 6
child-process ownership unknown for the replacement object: none

Typed outcome or recovery expected

Rerun the exact fixture. The replacement object's line-4 and line-5 calls must not have resolved ownership links to the line-2 child. Any conservative candidate retained for that child must include child-process uncertainty. Preserve the resolved link from the stable line-7 signal to the child on line 6; removing all child-process interaction relationships would fail the control.

Analysis or suspected cause

Source-derived reasoning: src/domain/javascript/javascriptSemanticChildProcesses.ts:164-234 inventories spawned children by binding ID, resolves each interaction receiver against that inventory, and treats a unique direct binding match as complete. src/domain/javascript/javascriptSemanticChildProcesses.ts:292-303 selects the spawn inventory for the receiver's binding ID without checking the intervening replacement. This can retain the old child's identity after assignment to an unrelated object. The executed CLI Evidence and its relation endpoint source ranges establish the failure; this explanation derives from the source.

Additional context

Related #3313 concerns resolved stream-release ownership after handle replacement in the resource extractor; this report concerns child-process signal and exit-listener ownership in a separate extractor.

Related #2572, closed as completed, concerns an unknown signal argument becoming SIGTERM while the child link remains correct; this report concerns a stale child link with a valid omitted-argument SIGTERM default.

Related #2571, closed as completed, concerns environment and stdio options when args are omitted; this report concerns ownership after replacing the returned handle.

Related #2746, closed as completed, concerns exact argv counts for spread arrays; this report concerns the identity of a signal or listener receiver.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3362 ### 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 Static application analysis attributes `kill` and exit-listener operations to a spawned child after its result binding has been replaced with an unrelated object. Both stale ownership relationships are resolved, which can misidentify signal forwarding and lifecycle handling for the recovered child process. The public `analyze-javascript-application` CLI exits with code 0. In `normalized_result.semantic_graph.relations`, a resolved `forwards-signal` relation runs from the line-4 SIGTERM signal node to the child-process node on line 2. A resolved `listens-exit` relation runs from that same child to the listener on line 5. The unchanged `stable` control also gets its expected resolved signal relation. No child-process ownership unknown is emitted for the replacement object. This public CLI behavior was observed and locally reproduced. ### Reproduction 1. Create a clean checkout at the reproduced baseline, install its locked dependencies, and build the runtime. ```sh git clone https://github.com/morluto/rea.git rea-child-handle-repro cd rea-child-handle-repro git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063 npm ci npm run build:cached ``` The reported observation used a checkout with dependencies installed and the runtime built. These clean-checkout commands supply those prerequisites; installation and building were not separately rerun for this report. 2. From the repository root, create the inline source fixture and isolated configuration home, invoke the public CLI, and print the affected facts from its complete Evidence. ```bash work=$(mktemp -d) mkdir "$work/app" "$work/home" cat > "$work/app/main.mjs" <<'JS' import { spawn } from 'node:child_process'; let child = spawn('program'); child = { kill() {}, on() {} }; child.kill(); child.on('exit', () => {}); const stable = spawn('program'); stable.kill(); JS python3 - "$work" <<'PY' import json, os, pathlib, subprocess, sys work = pathlib.Path(sys.argv[1]) child_environment = dict(os.environ, HOME=str(work / 'home')) result = subprocess.run( ['node', 'scripts/rea.mjs', 'analyze-javascript-application', str(work / 'app'), '--format', 'json'], env=child_environment, capture_output=True, text=True, check=True) evidence = json.loads(result.stdout) (work / 'evidence.json').write_text(result.stdout) graph = evidence['normalized_result']['semantic_graph'] print('exit:', result.returncode) nodes = {node['node_id']: node for node in graph['nodes']} for node in sorted(graph['nodes'], key=lambda n: ((n['identity']['source_range'] or {}).get('start', {}).get('line', 0), n['node_id'])): if node['kind'] in ['child-process', 'signal']: print('node', node['kind'], node['identity']['source_range'], node['properties']) for relation in graph['relations']: if relation['relation'] in ['forwards-signal', 'listens-exit']: source = nodes[relation['source_node_id']] target = nodes[relation['target_node_id']] print('relation', relation['relation'], relation['resolution'], source['kind'], source['node_id'], source['identity']['source_range'], '=>', target['kind'], target['node_id'], target['identity']['source_range']) print('effect_unknowns', [(u['family'], u['detail']) for u in graph['unknowns'] if u['family'] in ['timer', 'child-process', 'event', 'request', 'boundary']]) PY ``` The analyzer reads the target as text and does not execute its calls. The fixture includes the unchanged `stable` binding as a discriminating control. 3. Inspect the endpoint source ranges and resolutions for `forwards-signal` and `listens-exit`, and the child-process entries in `graph.unknowns`. The replacement object's line-4 and line-5 calls are resolved to the child spawned on line 2; the stable line-7 signal resolves to the child spawned on line 6. ### Expected behavior Calls on the replacement object on lines 4 and 5 must not resolve as operations on the child spawned on line 2. Preserve uncertainty if the extractor does not model the assignment precisely. The unchanged `stable` child on lines 6 and 7 should retain its resolved signal relationship. `docs/javascript-application-workflows.md:72-76` promises asynchronous child-process creation and ownership extraction. `docs/javascript-application-workflows.md:96-106` requires unresolved handles and unsupported shapes to stay explicit unknowns, and defines a resolved relationship as a unique admitted static target. Resolution does not prove execution. ### Impact and scope Impact level P2, supported by the observed bounded reassignment. Child-process lifecycle analysis returns incorrect signal and listener ownership for an explicit replacement of a child handle. Analysis does not actually spawn a process or deliver a signal. Workaround status is unverified. The unchanged-binding control was verified, but no caller-selectable option that restores the affected ownership result on the original artifact was verified. ### 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 A local directory containing the inline `main.mjs` fixture. The CLI returns application Evidence containing `normalized_result.semantic_graph`, and the reproduction retains the complete Evidence in `evidence.json`. No native engine, browser, or external service is needed, and the analyzed source does not execute. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The following is a compact transcription of the reported graph observations, not a raw JSON excerpt. ```text CLI exit code: 0 forwards-signal: resolved, line-4 SIGTERM signal => line-2 child-process listens-exit: resolved, line-2 child-process => line-5 listener stable control: resolved signal relationship from line 7 to the child on line 6 child-process ownership unknown for the replacement object: none ``` ### Typed outcome or recovery expected Rerun the exact fixture. The replacement object's line-4 and line-5 calls must not have resolved ownership links to the line-2 child. Any conservative candidate retained for that child must include child-process uncertainty. Preserve the resolved link from the stable line-7 signal to the child on line 6; removing all child-process interaction relationships would fail the control. ### Analysis or suspected cause Source-derived reasoning: `src/domain/javascript/javascriptSemanticChildProcesses.ts:164-234` inventories spawned children by binding ID, resolves each interaction receiver against that inventory, and treats a unique direct binding match as complete. `src/domain/javascript/javascriptSemanticChildProcesses.ts:292-303` selects the spawn inventory for the receiver's binding ID without checking the intervening replacement. This can retain the old child's identity after assignment to an unrelated object. The executed CLI Evidence and its relation endpoint source ranges establish the failure; this explanation derives from the source. ### Additional context Related [#3313](https://github.com/morluto/rea/issues/3313) concerns resolved stream-release ownership after handle replacement in the resource extractor; this report concerns child-process signal and exit-listener ownership in a separate extractor. Related [#2572](https://github.com/morluto/rea/issues/2572), closed as completed, concerns an unknown signal argument becoming SIGTERM while the child link remains correct; this report concerns a stale child link with a valid omitted-argument SIGTERM default. Related [#2571](https://github.com/morluto/rea/issues/2571), closed as completed, concerns environment and stdio options when args are omitted; this report concerns ownership after replacing the returned handle. Related [#2746](https://github.com/morluto/rea/issues/2746), closed as completed, concerns exact argv counts for spread arrays; this report concerns the identity of a signal or listener receiver.
Author
Owner

@Xinglan233 commented on GitHub (Oct 15, 2026):

I've opened #3819 to fix this, with regression tests for reassigned handles and instance field initializers.

<!-- gh-comment-id:6183886470 --> @Xinglan233 commented on GitHub (Oct 15, 2026): I've opened #3819 to fix this, with regression tests for reassigned handles and instance field initializers.
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#1228
No description provided.