[GH-ISSUE #6617] [Bug] catch-reference writes retain an incorrect return literal #4000

Closed
opened 2026-10-31 00:45:56 +00:00 by bsakaguchi · 0 comments
Owner

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

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

The public analyze-javascript-application CLI reports /value as state: "literal", value: 1 after the function throws a local object and assigns 2 through its catch parameter. Independently executing this benign function returns {value: 2}. Passing the catch parameter to a mutating callback also leaves the old literal, while passing the original object to the same callback produces conservative unknown.

Both the source build and the published runtime produced these results and exited 0. Direct writes, including a direct write to the original object inside the catch, correctly produce unknown. The intact, throw-only and catch-rebinding controls retain literal 1 and return {value: 1} when independently executed.

Reproduction

From the prepared source checkout, run the following with the pinned Node runtime on PATH. The first matrix contained these eight cases plus two related setter controls; the second removed those controls and used exactly the fixture below. Both matrix sizes reproduced the catch-reference failure on each tested build. The exact inline script below was also replayed successfully on the source build. REA analyzes inert source; the separate final Node command executes only these self-contained benign functions to establish their results.

node_bin="$(dirname "$(node -p process.execPath)")"
work="$(mktemp -d)"
mkdir -p "$work/app" "$work/home" "$work/temp"
cat > "$work/app/package.json" <<'JSON'
{"name":"catch-binding-control","version":"1.0.0","type":"module","main":"index.mjs"}
JSON
cat > "$work/app/index.mjs" <<'JS'
export function intact() {
 const shared = { value: 1 };
 return { value: shared.value };
}
export function directWrite() {
 const shared = { value: 1 };
 shared.value = 2;
 return { value: shared.value };
}
export function catchWrite() {
 const shared = { value: 1 };
 try { throw shared; } catch (error) { error.value = 2; }
 return { value: shared.value };
}
export function throwOnly() {
 const shared = { value: 1 };
 try { throw shared; } catch (error) { void error; }
 return { value: shared.value };
}
export function catchRebind() {
 const shared = { value: 1 };
 try { throw shared; } catch (error) { error = { value: 0 }; error.value = 2; }
 return { value: shared.value };
}
export function callEscape(consume) {
 const shared = { value: 1 };
 consume(shared);
 return { value: shared.value };
}
export function catchCallEscape(consume) {
 const shared = { value: 1 };
 try { throw shared; } catch (error) { consume(error); }
 return { value: shared.value };
}
export function directWriteInCatch() {
 const shared = { value: 1 };
 try { throw shared; } catch (error) { shared.value = 2; }
 return { value: shared.value };
}
JS
env -i PATH="$node_bin:$PATH" HOME="$work/home" TMPDIR="$work/temp" \
  XDG_CONFIG_HOME="$work/home/config" XDG_CACHE_HOME="$work/home/cache" \
  npm_config_prefix="$work/home/npm-global" REA_LOG_LEVEL=silent NO_COLOR=1 \
  node scripts/rea.mjs analyze-javascript-application "$work/app" --format json \
  > "$work/result.json" 2> "$work/stderr.txt"
node --input-type=module - "$work" <<'JS'
import { readFileSync } from 'node:fs';
import { pathToFileURL } from 'node:url';
const root = process.argv[2];
const evidence = JSON.parse(readFileSync(`${root}/result.json`, 'utf8'));
const shapes = [];
for (const node of evidence.normalized_result.graph.nodes) {
  for (const { properties: p } of node.observations ?? []) {
    if (p?.semantic_role === 'export-return-shapes') {
      const field = p.static_return_shapes[0].fields.find(f => f.path === '/value');
      shapes.push([p.exported_name, field.state, field.value]);
    }
  }
}
for (const row of shapes.sort((a, b) => a[0].localeCompare(b[0]))) console.log(...row);
const fixture = await import(pathToFileURL(`${root}/app/index.mjs`).href);
for (const name of Object.keys(fixture).sort()) {
  console.log('runtime', name, JSON.stringify(fixture[name](value => { value.value = 2; })));
}
JS

The published-package observation used the same public launcher from an extracted official npm tarball, with its registry SHA-512 integrity verified and dependencies supplied by the prepared checkout. It used the same isolated environment and inputs; no package installer or client registration was run.

Expected behavior

After the object is handed to the catch parameter and mutated or exposed to a callback through that reference, its subsequently read field must stop claiming the old literal 1. Conservative unknown is sufficient; recovering the new literal 2 is not required. The analyzer need not execute a handler, resolve arbitrary exception flow, or interpret callback bodies to retain this uncertainty.

A catch parameter is initialized with the thrown value, so the primary fixture's parameter and original binding refer to the same object. REA documents conservative treatment of explicit member effects, exposed mutable references and unresolved ordering. Its stated lack of control-flow-sensitive reaching definitions allows loss of precision, but does not justify retaining an unchanged exact value after the unmodeled reference effect. The same catch block already invalidates a direct write to the original object.

Impact and scope

The demonstrated impact is an incorrect exact exported return field for the constructed same-function throw/catch write and catch-reference call-escape cases. Both matrix sizes reproduced on each build; a final replay of the inline script also reproduced on the source build, giving three source runs and two published-runtime runs. This can mislead consumers reconstructing returned data. Production frequency, downstream export-comparison behavior, other exception-flow forms and other platforms were not measured. No security boundary crossing or severe downstream incident was established.

REA version

Source revision bcd1f95e688a2ac020a9d13f9c85e0ce321cb6b6 and the official npm rea-agents@6.7.0 runtime. Both public launchers report 6.7.0; npm latest and the latest published GitHub release identify that package version. The published runtime was executed separately because the source implementation differs from the release tag.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1 arm64; prepared locked dependencies, with package manager pinned to npm 11.16.0. No native provider was involved.

Target and artifact context

An extracted application directory containing the complete self-authored JavaScript ES module and package manifest above. No external application, device, browser or native binary was used.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: docs/javascript-application-workflows.md:214-221 documents member mutation tracking and conservative call escapes; :367-391 documents the return-value lattice and unresolved ordering. ECMAScript sections 14.14.1 and 14.15.2 define throwing the evaluated operand and initializing the catch parameter from that thrown value in the ECMAScript specification. The fixture's independent Node execution confirms the concrete result.
  • Failure source: Executed public CLI output from both tested runtimes, selecting normalized_result.graph.nodes[].observations[].properties with semantic_role: "export-return-shapes"; catchWrite and catchCallEscape retain /value literal 1. Source breadcrumbs are src/domain/javascript/javascriptSemanticAnalysis.ts:1434-1442, src/domain/javascript/javascriptSemanticMemberMutations.ts:499-513 and :876-929. The explicit control-flow limitation is at src/application/javascript/JavaScriptSemanticGraphBuilder.ts:418; projection completeness is computed from omissions in src/domain/javascript/javascriptExportReturnShapeProjection.ts:35-80.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

Abbreviated fields from both tested builds, followed by independently executed results:

case                 CLI /value       Node result
catchWrite           literal 1        {"value":2}
catchCallEscape      literal 1        {"value":2}
callEscape           unknown          {"value":2}
directWrite          unknown          {"value":2}
directWriteInCatch    unknown          {"value":2}
intact               literal 1        {"value":1}
throwOnly            literal 1        {"value":1}
catchRebind          literal 1        {"value":1}

Unknown controls use value: null and reason Own property value is unknown-coverage. Every retained return has return_shape_coverage.status: "complete", zero omitted return sites/fields/property-coverage records and projection_complete: true; each wrapper has complete parent-property coverage. These coverage fields establish retained projection, not complete semantic knowledge. The violation is the incorrect literal state. All five primary/control CLI invocations exited 0; no process-inspection cleanup failure occurred.

Typed outcome or recovery expected

Retain uncertainty in the affected return field rather than asserting its pre-effect value. Keep the direct-write and ordinary call-escape controls conservative, and retain precision for the intact fixture. No new result type or target execution is required by this report.

Analysis or suspected cause

Source-derived explanation: catch parameters are registered without an initializer linking them to a thrown operand. Member writes and call escapes propagate through known binding origins, and the mutation traversal does not mark thrown operands as escapes. Effects recorded on the catch parameter therefore do not reach the original object. The executed CLI behavior establishes the defect independently of this explanation; the report leaves the repair design open.

Additional context

Fresh open/closed issue searches and the local issue snapshot found no matching catch-reference mutation report. Open #6522 concerns a catch-shadowed var initializer losing a local call target; this fixture has no redeclaration and its closure signal is invalidation of an original object's return field. Closed #2646 concerns logical assignment retaining an incoming catch value, with no thrown-object member effect.

Closed #3312 covers accessor effects. Its merged setter fix explicitly leaves the supplementary getter-read variant unresolved; a current public replay still retains that variant's literal 1, while the ordinary object-setter control is unknown. Open #4630 covers an array-element setter and remains a separate existing failure. Neither accessor is present here, and restoring setter/getter handling would not by itself establish the catch-reference restoration check. These related cases are not proposed as new issues in this report.

NOT_ENOUGH_INFO: no last-known-good revision, production incidence, MCP replay or verified input-preserving workaround was established.

Restoration check

Reanalyze the exact fixture. catchWrite and catchCallEscape must stop reporting stale literal 1; conservative unknown satisfies the contract, or a justified exact post-effect value may be retained. directWrite, directWriteInCatch and callEscape must retain their existing conservative behavior. intact must remain literal 1. The throw-only and catch-rebinding controls independently return 1 and must not acquire a false literal 2; their current exactness is a useful precision check, without prescribing precise exception-flow analysis.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Two bounded valid input forms produce an incorrect exact field through the public static-analysis workflow on both tested runtimes, with direct-write and call-escape controls distinguishing the lost catch-reference effect. No severe downstream harm was observed.
  • Workaround status: unverified
  • Workaround basis: No public workflow preserving the analyzed input was verified to retain the missing uncertainty. Changing the target to a control fixture or executing these benign functions establishes an oracle rather than a practical substitute for static analysis of arbitrary targets.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6617 ## 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 The public `analyze-javascript-application` CLI reports `/value` as `state: "literal", value: 1` after the function throws a local object and assigns `2` through its catch parameter. Independently executing this benign function returns `{value: 2}`. Passing the catch parameter to a mutating callback also leaves the old literal, while passing the original object to the same callback produces conservative `unknown`. Both the source build and the published runtime produced these results and exited `0`. Direct writes, including a direct write to the original object inside the catch, correctly produce `unknown`. The intact, throw-only and catch-rebinding controls retain literal `1` and return `{value: 1}` when independently executed. ## Reproduction From the prepared source checkout, run the following with the pinned Node runtime on PATH. The first matrix contained these eight cases plus two related setter controls; the second removed those controls and used exactly the fixture below. Both matrix sizes reproduced the catch-reference failure on each tested build. The exact inline script below was also replayed successfully on the source build. REA analyzes inert source; the separate final Node command executes only these self-contained benign functions to establish their results. ```sh node_bin="$(dirname "$(node -p process.execPath)")" work="$(mktemp -d)" mkdir -p "$work/app" "$work/home" "$work/temp" cat > "$work/app/package.json" <<'JSON' {"name":"catch-binding-control","version":"1.0.0","type":"module","main":"index.mjs"} JSON cat > "$work/app/index.mjs" <<'JS' export function intact() { const shared = { value: 1 }; return { value: shared.value }; } export function directWrite() { const shared = { value: 1 }; shared.value = 2; return { value: shared.value }; } export function catchWrite() { const shared = { value: 1 }; try { throw shared; } catch (error) { error.value = 2; } return { value: shared.value }; } export function throwOnly() { const shared = { value: 1 }; try { throw shared; } catch (error) { void error; } return { value: shared.value }; } export function catchRebind() { const shared = { value: 1 }; try { throw shared; } catch (error) { error = { value: 0 }; error.value = 2; } return { value: shared.value }; } export function callEscape(consume) { const shared = { value: 1 }; consume(shared); return { value: shared.value }; } export function catchCallEscape(consume) { const shared = { value: 1 }; try { throw shared; } catch (error) { consume(error); } return { value: shared.value }; } export function directWriteInCatch() { const shared = { value: 1 }; try { throw shared; } catch (error) { shared.value = 2; } return { value: shared.value }; } JS env -i PATH="$node_bin:$PATH" HOME="$work/home" TMPDIR="$work/temp" \ XDG_CONFIG_HOME="$work/home/config" XDG_CACHE_HOME="$work/home/cache" \ npm_config_prefix="$work/home/npm-global" REA_LOG_LEVEL=silent NO_COLOR=1 \ node scripts/rea.mjs analyze-javascript-application "$work/app" --format json \ > "$work/result.json" 2> "$work/stderr.txt" node --input-type=module - "$work" <<'JS' import { readFileSync } from 'node:fs'; import { pathToFileURL } from 'node:url'; const root = process.argv[2]; const evidence = JSON.parse(readFileSync(`${root}/result.json`, 'utf8')); const shapes = []; for (const node of evidence.normalized_result.graph.nodes) { for (const { properties: p } of node.observations ?? []) { if (p?.semantic_role === 'export-return-shapes') { const field = p.static_return_shapes[0].fields.find(f => f.path === '/value'); shapes.push([p.exported_name, field.state, field.value]); } } } for (const row of shapes.sort((a, b) => a[0].localeCompare(b[0]))) console.log(...row); const fixture = await import(pathToFileURL(`${root}/app/index.mjs`).href); for (const name of Object.keys(fixture).sort()) { console.log('runtime', name, JSON.stringify(fixture[name](value => { value.value = 2; }))); } JS ``` The published-package observation used the same public launcher from an extracted official npm tarball, with its registry SHA-512 integrity verified and dependencies supplied by the prepared checkout. It used the same isolated environment and inputs; no package installer or client registration was run. ## Expected behavior After the object is handed to the catch parameter and mutated or exposed to a callback through that reference, its subsequently read field must stop claiming the old literal `1`. Conservative `unknown` is sufficient; recovering the new literal `2` is not required. The analyzer need not execute a handler, resolve arbitrary exception flow, or interpret callback bodies to retain this uncertainty. A catch parameter is initialized with the thrown value, so the primary fixture's parameter and original binding refer to the same object. REA documents conservative treatment of explicit member effects, exposed mutable references and unresolved ordering. Its stated lack of control-flow-sensitive reaching definitions allows loss of precision, but does not justify retaining an unchanged exact value after the unmodeled reference effect. The same catch block already invalidates a direct write to the original object. ## Impact and scope The demonstrated impact is an incorrect exact exported return field for the constructed same-function throw/catch write and catch-reference call-escape cases. Both matrix sizes reproduced on each build; a final replay of the inline script also reproduced on the source build, giving three source runs and two published-runtime runs. This can mislead consumers reconstructing returned data. Production frequency, downstream export-comparison behavior, other exception-flow forms and other platforms were not measured. No security boundary crossing or severe downstream incident was established. ## REA version Source revision `bcd1f95e688a2ac020a9d13f9c85e0ce321cb6b6` and the official npm `rea-agents@6.7.0` runtime. Both public launchers report `6.7.0`; npm latest and the latest published GitHub release identify that package version. The published runtime was executed separately because the source implementation differs from the release tag. ## Runtime and platform Node.js `v24.22.1`; macOS `27.0.1` arm64; prepared locked dependencies, with package manager pinned to npm `11.16.0`. No native provider was involved. ## Target and artifact context An extracted application directory containing the complete self-authored JavaScript ES module and package manifest above. No external application, device, browser or native binary was used. ## Interface CLI ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence - Expected source: `docs/javascript-application-workflows.md:214-221` documents member mutation tracking and conservative call escapes; `:367-391` documents the return-value lattice and unresolved ordering. ECMAScript sections 14.14.1 and 14.15.2 define throwing the evaluated operand and initializing the catch parameter from that thrown value in the [ECMAScript specification](https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-runtime-semantics-catchclauseevaluation). The fixture's independent Node execution confirms the concrete result. - Failure source: Executed public CLI output from both tested runtimes, selecting `normalized_result.graph.nodes[].observations[].properties` with `semantic_role: "export-return-shapes"`; `catchWrite` and `catchCallEscape` retain `/value` literal `1`. Source breadcrumbs are `src/domain/javascript/javascriptSemanticAnalysis.ts:1434-1442`, `src/domain/javascript/javascriptSemanticMemberMutations.ts:499-513` and `:876-929`. The explicit control-flow limitation is at `src/application/javascript/JavaScriptSemanticGraphBuilder.ts:418`; projection completeness is computed from omissions in `src/domain/javascript/javascriptExportReturnShapeProjection.ts:35-80`. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete Abbreviated fields from both tested builds, followed by independently executed results: ```text case CLI /value Node result catchWrite literal 1 {"value":2} catchCallEscape literal 1 {"value":2} callEscape unknown {"value":2} directWrite unknown {"value":2} directWriteInCatch unknown {"value":2} intact literal 1 {"value":1} throwOnly literal 1 {"value":1} catchRebind literal 1 {"value":1} ``` Unknown controls use `value: null` and reason `Own property value is unknown-coverage.` Every retained return has `return_shape_coverage.status: "complete"`, zero omitted return sites/fields/property-coverage records and `projection_complete: true`; each wrapper has complete parent-property coverage. These coverage fields establish retained projection, not complete semantic knowledge. The violation is the incorrect `literal` state. All five primary/control CLI invocations exited `0`; no process-inspection cleanup failure occurred. ## Typed outcome or recovery expected Retain uncertainty in the affected return field rather than asserting its pre-effect value. Keep the direct-write and ordinary call-escape controls conservative, and retain precision for the intact fixture. No new result type or target execution is required by this report. ## Analysis or suspected cause Source-derived explanation: catch parameters are registered without an initializer linking them to a thrown operand. Member writes and call escapes propagate through known binding origins, and the mutation traversal does not mark thrown operands as escapes. Effects recorded on the catch parameter therefore do not reach the original object. The executed CLI behavior establishes the defect independently of this explanation; the report leaves the repair design open. ## Additional context Fresh open/closed issue searches and the local issue snapshot found no matching catch-reference mutation report. Open [#6522](https://github.com/morluto/rea/issues/6522) concerns a catch-shadowed `var` initializer losing a local call target; this fixture has no redeclaration and its closure signal is invalidation of an original object's return field. Closed [#2646](https://github.com/morluto/rea/issues/2646) concerns logical assignment retaining an incoming catch value, with no thrown-object member effect. Closed [#3312](https://github.com/morluto/rea/issues/3312) covers accessor effects. Its merged setter fix explicitly leaves the supplementary getter-read variant unresolved; a current public replay still retains that variant's literal `1`, while the ordinary object-setter control is `unknown`. Open [#4630](https://github.com/morluto/rea/issues/4630) covers an array-element setter and remains a separate existing failure. Neither accessor is present here, and restoring setter/getter handling would not by itself establish the catch-reference restoration check. These related cases are not proposed as new issues in this report. NOT_ENOUGH_INFO: no last-known-good revision, production incidence, MCP replay or verified input-preserving workaround was established. ## Restoration check Reanalyze the exact fixture. `catchWrite` and `catchCallEscape` must stop reporting stale literal `1`; conservative `unknown` satisfies the contract, or a justified exact post-effect value may be retained. `directWrite`, `directWriteInCatch` and `callEscape` must retain their existing conservative behavior. `intact` must remain literal `1`. The throw-only and catch-rebinding controls independently return `1` and must not acquire a false literal `2`; their current exactness is a useful precision check, without prescribing precise exception-flow analysis. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Two bounded valid input forms produce an incorrect exact field through the public static-analysis workflow on both tested runtimes, with direct-write and call-escape controls distinguishing the lost catch-reference effect. No severe downstream harm was observed. - Workaround status: unverified - Workaround basis: No public workflow preserving the analyzed input was verified to retain the missing uncertainty. Changing the target to a control fixture or executing these benign functions establishes an oracle rather than a practical substitute for static analysis of arbitrary targets.
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#4000
No description provided.