[GH-ISSUE #3358] [Bug] Overwritten timer handles retain resolved cancellation links #1225

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

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

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 links a cancellation call to a timer after its handle binding has been overwritten with null. The semantic graph marks this stale cancels-timer relation resolved and records no timer-ownership unknown. A trace can therefore attribute cleanup to a timer whose handle is no longer passed to the cancellation call.

The public CLI reproduction exits with code 0 and emits two resolved cancels-timer relations: the cancellation on line 4 to the timer on line 2, and the unchanged-handle control on line 6 to the timer on line 5. The timer family has no unknown entries. General data-flow uncertainty for the overwritten binding does not downgrade the stale cancellation relation. This failure 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-timer-repro
cd rea-timer-repro
git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063
npm ci
npm run build:cached

The observed reproduction used an installed and built runtime. These clean-checkout commands provide its prerequisites; installation and building are not additional claimed observations.

  1. From the repository root, create the disposable input and isolated configuration home, invoke the public CLI, and print the affected facts from its complete Evidence. Every input file is created inline below.
work=$(mktemp -d)
mkdir "$work/app" "$work/home"
cat > "$work/app/main.mjs" <<'JS'
function work() {}
let handle = setTimeout(work, 100);
handle = null;
clearTimeout(handle);
const stable = setTimeout(work, 100);
clearTimeout(stable);
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 ['timer']:
        print('node', node['kind'], node['identity']['source_range'], node['properties'])
for relation in graph['relations']:
    if relation['relation'] in ['cancels-timer']:
        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 contains the overwritten handle on lines 2 through 4 and the unchanged stable handle control on lines 5 and 6.

  1. Inspect the relation endpoint source ranges, relation resolution, and timer-family unknown inventory. The line-4 cancellation incorrectly resolves to the line-2 timer; the line-6 control correctly resolves to the line-5 timer.

Expected behavior

The cancellation on line 4 must not have a resolved link to the timer created on line 2. The explicit intervening assignment makes the argument null, not that timer's handle. If reaching values are outside the admitted analysis, the cancellation target must remain unknown or a candidate. The unchanged stable handle should retain its resolved cancellation link.

docs/javascript-application-workflows.md:72-76 promises timer/cancellation-handle extraction, and docs/javascript-application-workflows.md:96-106 requires explicit unknowns for unresolved handles and defines resolved as a unique admitted static target. A resolved static identity does not prove execution. The semantic graph also advertises exact local-handle cancellation in limitations.

Impact and scope

Impact level P2, supported by the observed bounded handle-reassignment reproduction. The static semantic Evidence contains a false cleanup relationship, and its resolved relationship is eligible for default traces. The original artifact remains unchanged.

Workaround status is unverified. The unchanged-binding control was verified, but no public analysis option that distinguishes the overwritten handle while preserving the original source Evidence 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 JavaScript fixture main.mjs. The command returns application Evidence, and the reproduction saves its complete JSON as evidence.json. The affected facts are in normalized_result.semantic_graph.relations, with relation: "cancels-timer", endpoint source ranges, and the timer-family unknown inventory. No native engine, browser, or external service is needed.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

The following is a compact transcription of the observed facts, not a complete raw CLI result.

CLI exit: 0
cancels-timer: resolved, cancellation line 4 => timer line 2
cancels-timer: resolved, cancellation line 6 => timer line 5
timer-family unknown entries: 0

Typed outcome or recovery expected

Rerun the exact fixture. The line-4 cancellation must have no resolved relation to the line-2 timer, and any retained speculative link must carry candidate resolution and explicit timer uncertainty. The line-6 control must still resolve to the line-5 timer; removing all cancellation relations does not restore the contract.

Analysis or suspected cause

Source-derived reasoning: src/domain/javascript/javascriptSemanticAsyncEffects.ts:242-288 pairs schedule/cancellation operations by binding ID without admitting intervening non-timer assignments. resolveTimers looks up schedules using the cancellation's handle binding ID and treats a single match as complete. The same lexical binding can hold null by the time of cancellation, so a unique binding match does not establish the current timer target. The executed application Evidence supplies the failure observation.

Additional context

Related #3313 reports a resolved resource releases relation after a stream handle is replaced; this report concerns timer cancels-timer relations in a separate extractor, so repairing the resource-release matcher alone does not restore timer cancellation identity.

Related #2574, closed as completed, concerns event-dispatch data appearing as a listener in the same source file; this report concerns timer schedule/cancellation matching after handle reassignment.

Related #404, closed as completed, introduced the broader semantic-tracing capability including timers and cancellation handles; this report concerns a reproduced correctness failure within that capability.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3358 ### 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` links a cancellation call to a timer after its handle binding has been overwritten with `null`. The semantic graph marks this stale `cancels-timer` relation resolved and records no timer-ownership unknown. A trace can therefore attribute cleanup to a timer whose handle is no longer passed to the cancellation call. The public CLI reproduction exits with code 0 and emits two resolved `cancels-timer` relations: the cancellation on line 4 to the timer on line 2, and the unchanged-handle control on line 6 to the timer on line 5. The timer family has no unknown entries. General data-flow uncertainty for the overwritten binding does not downgrade the stale cancellation relation. This failure 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-timer-repro cd rea-timer-repro git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063 npm ci npm run build:cached ``` The observed reproduction used an installed and built runtime. These clean-checkout commands provide its prerequisites; installation and building are not additional claimed observations. 2. From the repository root, create the disposable input and isolated configuration home, invoke the public CLI, and print the affected facts from its complete Evidence. Every input file is created inline below. ```bash work=$(mktemp -d) mkdir "$work/app" "$work/home" cat > "$work/app/main.mjs" <<'JS' function work() {} let handle = setTimeout(work, 100); handle = null; clearTimeout(handle); const stable = setTimeout(work, 100); clearTimeout(stable); 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 ['timer']: print('node', node['kind'], node['identity']['source_range'], node['properties']) for relation in graph['relations']: if relation['relation'] in ['cancels-timer']: 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 contains the overwritten handle on lines 2 through 4 and the unchanged `stable` handle control on lines 5 and 6. 3. Inspect the relation endpoint source ranges, relation resolution, and timer-family unknown inventory. The line-4 cancellation incorrectly resolves to the line-2 timer; the line-6 control correctly resolves to the line-5 timer. ### Expected behavior The cancellation on line 4 must not have a resolved link to the timer created on line 2. The explicit intervening assignment makes the argument `null`, not that timer's handle. If reaching values are outside the admitted analysis, the cancellation target must remain unknown or a candidate. The unchanged `stable` handle should retain its resolved cancellation link. `docs/javascript-application-workflows.md:72-76` promises timer/cancellation-handle extraction, and `docs/javascript-application-workflows.md:96-106` requires explicit unknowns for unresolved handles and defines `resolved` as a unique admitted static target. A resolved static identity does not prove execution. The semantic graph also advertises exact local-handle cancellation in `limitations`. ### Impact and scope Impact level P2, supported by the observed bounded handle-reassignment reproduction. The static semantic Evidence contains a false cleanup relationship, and its resolved relationship is eligible for default traces. The original artifact remains unchanged. Workaround status is unverified. The unchanged-binding control was verified, but no public analysis option that distinguishes the overwritten handle while preserving the original source Evidence 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 JavaScript fixture `main.mjs`. The command returns application Evidence, and the reproduction saves its complete JSON as `evidence.json`. The affected facts are in `normalized_result.semantic_graph.relations`, with `relation: "cancels-timer"`, endpoint source ranges, and the timer-family unknown inventory. No native engine, browser, or external service is needed. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence The following is a compact transcription of the observed facts, not a complete raw CLI result. ```text CLI exit: 0 cancels-timer: resolved, cancellation line 4 => timer line 2 cancels-timer: resolved, cancellation line 6 => timer line 5 timer-family unknown entries: 0 ``` ### Typed outcome or recovery expected Rerun the exact fixture. The line-4 cancellation must have no resolved relation to the line-2 timer, and any retained speculative link must carry candidate resolution and explicit timer uncertainty. The line-6 control must still resolve to the line-5 timer; removing all cancellation relations does not restore the contract. ### Analysis or suspected cause Source-derived reasoning: `src/domain/javascript/javascriptSemanticAsyncEffects.ts:242-288` pairs schedule/cancellation operations by binding ID without admitting intervening non-timer assignments. `resolveTimers` looks up schedules using the cancellation's handle binding ID and treats a single match as complete. The same lexical binding can hold `null` by the time of cancellation, so a unique binding match does not establish the current timer target. The executed application Evidence supplies the failure observation. ### Additional context Related [#3313](https://github.com/morluto/rea/issues/3313) reports a resolved resource `releases` relation after a stream handle is replaced; this report concerns timer `cancels-timer` relations in a separate extractor, so repairing the resource-release matcher alone does not restore timer cancellation identity. Related [#2574](https://github.com/morluto/rea/issues/2574), closed as completed, concerns event-dispatch data appearing as a listener in the same source file; this report concerns timer schedule/cancellation matching after handle reassignment. Related [#404](https://github.com/morluto/rea/issues/404), closed as completed, introduced the broader semantic-tracing capability including timers and cancellation handles; this report concerns a reproduced correctness failure within that capability.
Author
Owner

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

I've opened #3794 to fix this, with regression tests for overwritten and unchanged timer handles.

<!-- gh-comment-id:6182704619 --> @Xinglan233 commented on GitHub (Oct 15, 2026): I've opened #3794 to fix this, with regression tests for overwritten and unchanged timer handles.
Author
Owner

@morluto commented on GitHub (Oct 16, 2026):

Fixed on main in 6183544e8. Timer cancellation resolves the handle at the cancellation read, removing stale resolved links after handle replacement while retaining stable and uncertain candidates.

Binding and CLI/MCP graph controls cover overwritten and stable handles without executing analyzed targets.

Hosted main CI is still pending; this closure is based on the implemented fix and the scoped regression evidence.

<!-- gh-comment-id:6206030132 --> @morluto commented on GitHub (Oct 16, 2026): Fixed on `main` in [6183544e8](https://github.com/morluto/rea/commit/6183544e8b42b65feaf4f674f860b1fb3b0c95eb). Timer cancellation resolves the handle at the cancellation read, removing stale resolved links after handle replacement while retaining stable and uncertain candidates. Binding and CLI/MCP graph controls cover overwritten and stable handles without executing analyzed targets. Hosted main CI is still pending; this closure is based on the implemented fix and the scoped regression evidence.
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#1225
No description provided.