[GH-ISSUE #4013] [Bug] Symlinks into unreadable subtrees abort reference-source imports #1599

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

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

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

import-reference-source aborts the entire import when a readable symlink resolves to an existing file under a directory that permits search but denies enumeration. The CLI exits 1 with a graph-validation error and returns no graph, discarding readable sibling observations.

Reproduction

Using the compiled checkout runtime, run this as a non-root account on a filesystem that enforces directory permissions:

fixture_root="$(mktemp -d)"
fixture_home="$(mktemp -d)"
mkdir "$fixture_root/hidden"
printf 'visible\n' > "$fixture_root/visible.txt"
printf 'target\n' > "$fixture_root/hidden/target.txt"
ln -s hidden/target.txt "$fixture_root/alias.txt"
chmod 111 "$fixture_root/hidden"
HOME="$fixture_home" node scripts/rea.mjs import-reference-source "$fixture_root" --json

The executed probe used the same fixture and CLI through a Node driver with an isolated home. The failure occurred once. Removing only alias.txt succeeded with hidden.tree_state: "unreadable" and a hashed visible.txt. Restoring the link and changing only hidden to mode 0755 also succeeded, retaining all four entries. Restore directory permissions before deleting the fixture.

Expected behavior

Return a partial HistoricalSourceGraph that preserves readable files, the unreadable-directory diagnostic, and truthful uncertainty about the unenumerated symlink target. tests/boundary/filesystem/referenceSourceUnreadableDirectories.test.ts:23 requires unreadable descendants to preserve a successful partial graph and readable sibling content.

Impact and scope

P2. A single unreadable subtree reachable through a symlink blocks the entire reference-source import. The failure and both controls were observed once on macOS. Linux was not exercised. No source bytes were modified. Restoring directory read permission worked when the caller had permission to do so.

REA version

rea 6.5.0 at source commit 4692bb3330; source rechecked on origin/main 6fcfc5b5e0

Runtime and platform

Node 24.18.0; macOS arm64

Target and artifact context

Local source tree with visible.txt, hidden/target.txt, and alias.txt -> hidden/target.txt; hidden has mode 0111. No external provider.

Interface

CLI

Earliest failure stage

Artifact or evidence registration

Relevant output or evidence

The failing CLI returned error: "Import failed", category: "execution_failure", and this diagnostic:

Internal symlink target must resolve to an inventoried entry or a recorded exclusion
path: ["entries", 0, "target"]

The no-link and readable-directory controls exited 0 with a HistoricalSourceGraph. The existing unreadable-directory test file passed all three tests in the saved verification.

Typed outcome or recovery expected

With the link present and hidden still mode 0111, retain the usable partial graph and unreadable-directory diagnostic without claiming the target is absent or inventoried. Preserve both successful controls.

Analysis or suspected cause

Source-derived: src/reference/ReferenceSourceReaderEntries.ts:128 records the failed directory read, while readSymlinkTarget at line 354 resolves the target and labels it internal. The symlink conversion in src/application/ReferenceSourceImportEntries.ts preserves that internal target. checkSymlinks in src/domain/referenceSourceGraph.ts:325 requires an inventoried target or recorded exclusion, so the unenumerated file is rejected. src/application/ReferenceSourceImport.ts:290 converts validation failure to the observed import error.

Additional context

Related closed issue #2651 covered deliberately excluded symlink targets; this target is omitted because directory enumeration fails, not because an exclusion matches. Open #3401 concerns an unreadable selected root with no symlink, rather than a readable root with an unreadable descendant.

Still present by source inspection on current origin/main 6fcfc5b5e0. Commit 034342ba2 changed package-entry resolution in the entry converter; its symlink conversion, reader behavior and graph guard remain unchanged. This freshness check did not rerun the reproduction.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4013 ### 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 `import-reference-source` aborts the entire import when a readable symlink resolves to an existing file under a directory that permits search but denies enumeration. The CLI exits 1 with a graph-validation error and returns no graph, discarding readable sibling observations. ### Reproduction Using the compiled checkout runtime, run this as a non-root account on a filesystem that enforces directory permissions: ```sh fixture_root="$(mktemp -d)" fixture_home="$(mktemp -d)" mkdir "$fixture_root/hidden" printf 'visible\n' > "$fixture_root/visible.txt" printf 'target\n' > "$fixture_root/hidden/target.txt" ln -s hidden/target.txt "$fixture_root/alias.txt" chmod 111 "$fixture_root/hidden" HOME="$fixture_home" node scripts/rea.mjs import-reference-source "$fixture_root" --json ``` The executed probe used the same fixture and CLI through a Node driver with an isolated home. The failure occurred once. Removing only `alias.txt` succeeded with `hidden.tree_state: "unreadable"` and a hashed `visible.txt`. Restoring the link and changing only `hidden` to mode `0755` also succeeded, retaining all four entries. Restore directory permissions before deleting the fixture. ### Expected behavior Return a partial `HistoricalSourceGraph` that preserves readable files, the unreadable-directory diagnostic, and truthful uncertainty about the unenumerated symlink target. `tests/boundary/filesystem/referenceSourceUnreadableDirectories.test.ts:23` requires unreadable descendants to preserve a successful partial graph and readable sibling content. ### Impact and scope P2. A single unreadable subtree reachable through a symlink blocks the entire reference-source import. The failure and both controls were observed once on macOS. Linux was not exercised. No source bytes were modified. Restoring directory read permission worked when the caller had permission to do so. ### REA version rea 6.5.0 at source commit 4692bb33300c7cb77b095b6cc0c33da861c1b860; source rechecked on origin/main 6fcfc5b5e0c063300cd5278dd8bad5b1ab1374f6 ### Runtime and platform Node 24.18.0; macOS arm64 ### Target and artifact context Local source tree with `visible.txt`, `hidden/target.txt`, and `alias.txt -> hidden/target.txt`; `hidden` has mode `0111`. No external provider. ### Interface CLI ### Earliest failure stage Artifact or evidence registration ### Relevant output or evidence The failing CLI returned `error: "Import failed"`, `category: "execution_failure"`, and this diagnostic: ```text Internal symlink target must resolve to an inventoried entry or a recorded exclusion path: ["entries", 0, "target"] ``` The no-link and readable-directory controls exited 0 with a `HistoricalSourceGraph`. The existing unreadable-directory test file passed all three tests in the saved verification. ### Typed outcome or recovery expected With the link present and `hidden` still mode `0111`, retain the usable partial graph and unreadable-directory diagnostic without claiming the target is absent or inventoried. Preserve both successful controls. ### Analysis or suspected cause Source-derived: `src/reference/ReferenceSourceReaderEntries.ts:128` records the failed directory read, while `readSymlinkTarget` at line 354 resolves the target and labels it internal. The symlink conversion in `src/application/ReferenceSourceImportEntries.ts` preserves that internal target. `checkSymlinks` in `src/domain/referenceSourceGraph.ts:325` requires an inventoried target or recorded exclusion, so the unenumerated file is rejected. `src/application/ReferenceSourceImport.ts:290` converts validation failure to the observed import error. ### Additional context Related closed issue [#2651](https://github.com/morluto/rea/issues/2651) covered deliberately excluded symlink targets; this target is omitted because directory enumeration fails, not because an exclusion matches. Open [#3401](https://github.com/morluto/rea/issues/3401) concerns an unreadable selected root with no symlink, rather than a readable root with an unreadable descendant. Still present by source inspection on current origin/main 6fcfc5b5e0c063300cd5278dd8bad5b1ab1374f6. Commit `034342ba2` changed package-entry resolution in the entry converter; its symlink conversion, reader behavior and graph guard remain unchanged. This freshness check did not rerun the reproduction.
Author
Owner

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

Confirmed: the failure mode is a validation-time hard stop, not an ingestion-time degradation. readSymlinkTarget resolves the target and labels it internal even though the parent directory denied enumeration, so checkSymlinks later has no inventoried entry or exclusion to compare against. The partial-graph contract in the unreadable-directories test suggests the symlink branch should keep an unresolved/uncertain target state (like hidden.tree_state: "unreadable") instead of demanding a resolution that enumeration cannot provide. That preserves the readable siblings and the diagnostic without claiming the target is absent.

<!-- gh-comment-id:6187051696 --> @XinwangTao commented on GitHub (Oct 15, 2026): Confirmed: the failure mode is a validation-time hard stop, not an ingestion-time degradation. readSymlinkTarget resolves the target and labels it internal even though the parent directory denied enumeration, so checkSymlinks later has no inventoried entry or exclusion to compare against. The partial-graph contract in the unreadable-directories test suggests the symlink branch should keep an unresolved/uncertain target state (like hidden.tree_state: "unreadable") instead of demanding a resolution that enumeration cannot provide. That preserves the readable siblings and the diagnostic without claiming the target is absent.
Author
Owner

@coygeek commented on GitHub (Oct 18, 2026):

Addressed on main by github.com/morluto/rea@d6e4fe4a4d, with diagnostic provenance retained by github.com/morluto/rea@25c22222ab. Links into unenumerated unreadable directories now retain an unknown/unreadable target instead of aborting the import, while readable siblings remain in a partial HistoricalSourceGraph. The added filesystem regression covers this case. Verified by reviewing the current source, regression assertions and commit ancestry; I have not rerun the reproduction. Closing as addressed on main.

<!-- gh-comment-id:6244124308 --> @coygeek commented on GitHub (Oct 18, 2026): Addressed on main by https://github.com/morluto/rea/commit/d6e4fe4a4dd8914ec3db50d2a7fc3d0802649262, with diagnostic provenance retained by https://github.com/morluto/rea/commit/25c22222abe7a681491bdb4d7316250fdcf32973. Links into unenumerated unreadable directories now retain an unknown/unreadable target instead of aborting the import, while readable siblings remain in a partial HistoricalSourceGraph. The added filesystem regression covers this case. Verified by reviewing the current source, regression assertions and commit ancestry; I have not rerun the reproduction. Closing as addressed on main.
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#1599
No description provided.