[GH-ISSUE #5521] [Bug] Artifact commands describe a stable symlink loop as a target change #3358

Closed
opened 2026-10-31 00:39:31 +00:00 by bsakaguchi · 1 comment
Owner

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

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

inspect-artifact and extract-artifact reject an already looping symbolic-link path with target changed while being opened: ELOOP: too many symbolic links encountered, access. The unsupported change explanation appears in message, remediation.action and details.reason, even though no concurrent writer was introduced and both links retained their original targets. Both commands still fail safely with exit 1, target_unavailable, unavailable and retryable: false.

Reproduction

An independent verifier ran each loop variant twice on the built source baseline, including replaying this exact shell block. Run from the built checkout with Node v24.22.0 on PATH, without shell errexit. Configuration, fixtures and captured output use the generated directory; no external analysis provider is needed.

cli="$PWD/scripts/rea.mjs"
case_dir=$(mktemp -d)
mkdir -p "$case_dir/home" "$case_dir/config" "$case_dir/data" "$case_dir/cache" "$case_dir/state" "$case_dir/temp"
ln -s loop.db "$case_dir/loop.db"
ln -s tree "$case_dir/tree"
ln -s nowhere "$case_dir/missing"
python3 - "$case_dir/valid.zip" <<'PYZIP'
import sys, zipfile
with zipfile.ZipFile(sys.argv[1], "w") as archive:
    archive.writestr("hello.txt", "hello")
PYZIP
run_case() {
  label=$1
  shift
  env HOME="$case_dir/home" XDG_CONFIG_HOME="$case_dir/config" XDG_DATA_HOME="$case_dir/data" XDG_CACHE_HOME="$case_dir/cache" XDG_STATE_HOME="$case_dir/state" TMPDIR="$case_dir/temp" node "$cli" "$@" > "$case_dir/$label.json" 2> "$case_dir/$label.stderr"
  result=$?
  printf '%s: exit %s\n' "$label" "$result"
}
run_case loop-inspect inspect-artifact "$case_dir/loop.db" --json --evidence_output "$case_dir/loop-inspect.evidence.json"
run_case loop-extract extract-artifact "$case_dir/tree/file" --json
run_case missing-inspect inspect-artifact "$case_dir/missing" --json --evidence_output "$case_dir/missing-inspect.evidence.json"
run_case valid-inspect inspect-artifact "$case_dir/valid.zip" --json --evidence_output "$case_dir/valid-inspect.evidence.json"
cat "$case_dir/loop-inspect.json" "$case_dir/loop-extract.json" "$case_dir/missing-inspect.json"

Both loop cases exit 1 with the change explanation. The dangling-link control exits 1 with an ENOENT explanation; the valid ZIP control exits 0 and returns inspection Evidence.

Expected behavior

Describe the initial path-resolution failure truthfully while retaining the selected path and useful ELOOP diagnostic. AGENTS.md:31 requires preserving meaningful failure reasons. Source-derived: src/application/BinaryTargetResolver.ts:89 fails at the initial access check, before canonicalization or file opening, so this failure does not establish a change while opening.

Impact and scope

P3 wording defect: both commands reject the path safely and retain the useful ELOOP diagnosis. No evidence corruption, admitted unstable artifact or consequential behavior change was observed. Callers can identify the loop from the retained diagnostic. Observed twice for each of a self-loop and a looping directory component on macOS; MCP and other platforms were not tested.

REA version

6.7.0; built source commit 074768901f3cdc8205b61773fb1faf6f6c7b3ca8.

Runtime and platform

Node v24.22.0; macOS (Darwin), arm64. The exact repository Node v24.18.0 pin was not used. NOT_ENOUGH_INFO: the exact macOS release was not recorded. No Hopper/Ghidra provider was involved.

Target and artifact context

Disposable self-referential links loop.db -> loop.db and tree -> tree, selected as loop.db and tree/file. Controls use a dangling link and a locally generated ZIP containing hello.txt.

Interface

CLI

Earliest failure stage

Request parsing or validation

Relevant output or evidence

The verifier's stored JSON for both loop cases contains the following fields (selected host path replaced with <selected-path>):

{
  "code": "target_unavailable",
  "category": "unavailable",
  "retryable": false,
  "details": {
    "path": "<selected-path>",
    "reason": "target changed while being opened: ELOOP: too many symbolic links encountered, access '<selected-path>'"
  }
}

message and remediation.action repeat that reason. The submitter read both stored responses. The verifier also checked that both link targets remained unchanged after execution.

Upstream source re-check at c374c2fe7290b914d73d9e27af38b498753ec593: no post-baseline changes to BinaryTargetResolver.ts, artifactCommands.ts or the cited contract. The intervening DirectAnalysis.ts changes concern execution Evidence and caching, not this resolution path. Current-main applicability is source-inspected; runtime reproduction was at the baseline above.

Typed outcome or recovery expected

Continue rejecting the unresolvable target with its selected path and a truthful resolution diagnostic. This report requests no change to the structured code, category or retryability, and does not request weakening genuine replacement detection.

Analysis or suspected cause

Source-derived: src/cli/artifactCommands.ts:213,246 reaches direct analysis; src/application/DirectAnalysis.ts:550 resolves the target. The resolver catches the initial access failure and routes it through classifyBinaryTargetFilesystemFailure (src/application/BinaryTargetResolver.ts:249-259). targetFailureReason (:279-280) assigns the change explanation to every ELOOP without establishing a change.

Additional context

Prepared with AI assistance and independently reproduced. Related #5479 concerns a valid symlink alias omitted from successful Evidence; #5492 concerns reading the wrong plist after symlink-parent normalization; #4188 concerns SQLite error classification. These have different triggers and closure checks. NOT_ENOUGH_INFO: no last known good version or regression range was established.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5521 ### 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 `inspect-artifact` and `extract-artifact` reject an already looping symbolic-link path with `target changed while being opened: ELOOP: too many symbolic links encountered, access`. The unsupported change explanation appears in `message`, `remediation.action` and `details.reason`, even though no concurrent writer was introduced and both links retained their original targets. Both commands still fail safely with exit 1, `target_unavailable`, `unavailable` and `retryable: false`. ### Reproduction An independent verifier ran each loop variant twice on the built source baseline, including replaying this exact shell block. Run from the built checkout with Node v24.22.0 on PATH, without shell errexit. Configuration, fixtures and captured output use the generated directory; no external analysis provider is needed. ```sh cli="$PWD/scripts/rea.mjs" case_dir=$(mktemp -d) mkdir -p "$case_dir/home" "$case_dir/config" "$case_dir/data" "$case_dir/cache" "$case_dir/state" "$case_dir/temp" ln -s loop.db "$case_dir/loop.db" ln -s tree "$case_dir/tree" ln -s nowhere "$case_dir/missing" python3 - "$case_dir/valid.zip" <<'PYZIP' import sys, zipfile with zipfile.ZipFile(sys.argv[1], "w") as archive: archive.writestr("hello.txt", "hello") PYZIP run_case() { label=$1 shift env HOME="$case_dir/home" XDG_CONFIG_HOME="$case_dir/config" XDG_DATA_HOME="$case_dir/data" XDG_CACHE_HOME="$case_dir/cache" XDG_STATE_HOME="$case_dir/state" TMPDIR="$case_dir/temp" node "$cli" "$@" > "$case_dir/$label.json" 2> "$case_dir/$label.stderr" result=$? printf '%s: exit %s\n' "$label" "$result" } run_case loop-inspect inspect-artifact "$case_dir/loop.db" --json --evidence_output "$case_dir/loop-inspect.evidence.json" run_case loop-extract extract-artifact "$case_dir/tree/file" --json run_case missing-inspect inspect-artifact "$case_dir/missing" --json --evidence_output "$case_dir/missing-inspect.evidence.json" run_case valid-inspect inspect-artifact "$case_dir/valid.zip" --json --evidence_output "$case_dir/valid-inspect.evidence.json" cat "$case_dir/loop-inspect.json" "$case_dir/loop-extract.json" "$case_dir/missing-inspect.json" ``` Both loop cases exit 1 with the change explanation. The dangling-link control exits 1 with an ENOENT explanation; the valid ZIP control exits 0 and returns inspection Evidence. ### Expected behavior Describe the initial path-resolution failure truthfully while retaining the selected path and useful ELOOP diagnostic. `AGENTS.md:31` requires preserving meaningful failure reasons. Source-derived: `src/application/BinaryTargetResolver.ts:89` fails at the initial `access` check, before canonicalization or file opening, so this failure does not establish a change while opening. ### Impact and scope P3 wording defect: both commands reject the path safely and retain the useful ELOOP diagnosis. No evidence corruption, admitted unstable artifact or consequential behavior change was observed. Callers can identify the loop from the retained diagnostic. Observed twice for each of a self-loop and a looping directory component on macOS; MCP and other platforms were not tested. ### REA version 6.7.0; built source commit `074768901f3cdc8205b61773fb1faf6f6c7b3ca8`. ### Runtime and platform Node v24.22.0; macOS (Darwin), arm64. The exact repository Node v24.18.0 pin was not used. NOT_ENOUGH_INFO: the exact macOS release was not recorded. No Hopper/Ghidra provider was involved. ### Target and artifact context Disposable self-referential links `loop.db -> loop.db` and `tree -> tree`, selected as `loop.db` and `tree/file`. Controls use a dangling link and a locally generated ZIP containing `hello.txt`. ### Interface CLI ### Earliest failure stage Request parsing or validation ### Relevant output or evidence The verifier's stored JSON for both loop cases contains the following fields (selected host path replaced with `<selected-path>`): ```json { "code": "target_unavailable", "category": "unavailable", "retryable": false, "details": { "path": "<selected-path>", "reason": "target changed while being opened: ELOOP: too many symbolic links encountered, access '<selected-path>'" } } ``` `message` and `remediation.action` repeat that reason. The submitter read both stored responses. The verifier also checked that both link targets remained unchanged after execution. Upstream source re-check at `c374c2fe7290b914d73d9e27af38b498753ec593`: no post-baseline changes to `BinaryTargetResolver.ts`, `artifactCommands.ts` or the cited contract. The intervening `DirectAnalysis.ts` changes concern execution Evidence and caching, not this resolution path. Current-main applicability is source-inspected; runtime reproduction was at the baseline above. ### Typed outcome or recovery expected Continue rejecting the unresolvable target with its selected path and a truthful resolution diagnostic. This report requests no change to the structured code, category or retryability, and does not request weakening genuine replacement detection. ### Analysis or suspected cause Source-derived: `src/cli/artifactCommands.ts:213,246` reaches direct analysis; `src/application/DirectAnalysis.ts:550` resolves the target. The resolver catches the initial access failure and routes it through `classifyBinaryTargetFilesystemFailure` (`src/application/BinaryTargetResolver.ts:249-259`). `targetFailureReason` (`:279-280`) assigns the change explanation to every ELOOP without establishing a change. ### Additional context Prepared with AI assistance and independently reproduced. Related #5479 concerns a valid symlink alias omitted from successful Evidence; #5492 concerns reading the wrong plist after symlink-parent normalization; #4188 concerns SQLite error classification. These have different triggers and closure checks. NOT_ENOUGH_INFO: no last known good version or regression range was established.
Author
Owner

@matbrgz commented on GitHub (Oct 20, 2026):

Cross-referencing with canonical issue #5103 () and siblings #5258, #5479, and #5492. All address symbolic link resolution, loop handling, and preserving caller-specified path representations across artifact commands. Cataloged under Cluster X in consolidation tracker #4330.

<!-- gh-comment-id:6292615819 --> @matbrgz commented on GitHub (Oct 20, 2026): Cross-referencing with canonical issue #5103 () and siblings #5258, #5479, and #5492. All address symbolic link resolution, loop handling, and preserving caller-specified path representations across artifact commands. Cataloged under Cluster X in consolidation tracker #4330.
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#3358
No description provided.