[GH-ISSUE #6826] [Bug] import-reference-source imports the wrong tree when .. follows a directory symlink #4173

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

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

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

CLI

Observed behavior

import-reference-source silently imports a different source directory when the selected POSIX path contains a directory symlink followed by ... The public CLI exits 0 with empty stderr and returns a committed HistoricalSourceGraph containing other.js, although the filesystem resolves the selected directory to one containing only intended.js.

Reproduction

Run from the source checkout at the revision below. This is the executed fixture/probe reduced to its relevant operations; /tmp/rea-repro stands in for the scratch directory. All files contain inert JavaScript and are never executed.

mkdir -p /tmp/rea-repro
node --input-type=module <<'JS'
import { mkdir, writeFile, symlink, realpath } from 'node:fs/promises';
import { spawnSync } from 'node:child_process';
import { join } from 'node:path';
const repo = process.cwd();
const root = '/tmp/rea-repro';
await mkdir(join(root, 'physical/inner'), { recursive: true });
await mkdir(join(root, 'physical/source'), { recursive: true });
await mkdir(join(root, 'source'), { recursive: true });
await writeFile(join(root, 'physical/source/intended.js'), 'export const intended = 1;\n');
await writeFile(join(root, 'source/other.js'), 'export const other = 2;\n');
await symlink('physical/inner', join(root, 'link'));
const selected = root + '/link/../source';
console.log('filesystem realpath:', await realpath(selected));
for (const [label, path] of [
  ['selected', selected],
  ['canonical-control', await realpath(selected)],
  ['lexical-control', join(root, 'source')],
]) {
  const r = spawnSync(process.execPath, [join(repo, 'scripts/rea.mjs'),
    'import-reference-source', path, '--json'], {
    encoding: 'utf8', env: { ...process.env, REA_LOG_LEVEL: 'silent' },
  });
  const graph = JSON.parse(r.stdout);
  console.log(JSON.stringify({ label, status: r.status,
    entries: graph.entries.map(e => e.path), stderr: r.stderr }));
}
JS

One selected-path run and two controls were executed. The selected graph was also compared with the saved controls: it equals the complete lexical-control JSON and differs from the canonical-control JSON.

Expected behavior

Import the caller-selected directory as the historical reference. The selected path and its filesystem realpath identify the same directory and should return the same inventory. The source-import guide describes importing the chosen tree; AGENTS.md requires preserving caller-selected paths and evidence.

Impact and scope

A valid POSIX directory spelling produces a graph and source digests for a different tree, misleading downstream historical-source comparisons. No input is modified. Observed on Linux; macOS and Windows were not tested. The inventory_state is partial for both controls as well, with the existing generic pathname-race limitation; that does not explain selecting the wrong directory in this stable fixture.

Workaround verified: pass the filesystem-resolved directory, which returns intended.js.

REA version

6.7.0; source revision 7fd722e15bea35255f5eadbd406948e28538d25f. The latest published release is 6.7.0; the published-package runtime was not separately tested.

Runtime and platform

Node.js v26.12.1; Linux 7.2.8-9-omarchy x64.

Target and artifact context

Two synthetic local source directories, each with one inert JavaScript file; one relative directory symlink.

Interface

CLI

Earliest failure stage

Request parsing or validation

Relevant output or evidence

Evidence provenance: observed public CLI execution and source review. Local verification: reproduced. Reproduction completeness: complete for this stable Linux fixture.

filesystem realpath: /tmp/rea-repro/physical/source
selected          status=0 entries=["other.js"]    stderr=""
canonical-control status=0 entries=["intended.js"] stderr=""
lexical-control   status=0 entries=["other.js"]    stderr=""
Selected graph equals lexical control: true
Selected graph equals canonical control: false

Typed outcome or recovery expected

Return the graph for physical/source; preserve the selected path semantics while canonicalizing the actual filesystem target.

Analysis or suspected cause

Source-derived: resolveRoot first calls stat(requestedRoot), then calls realpath(resolve(requestedRoot)). resolve collapses link/.. before realpath sees it, changing the selected directory. Rejection of symlinks inside the imported tree is a separate policy and does not justify reading a different root.

Additional context

Related historical issue #3864 covered this mechanism in Go/Rust CLI handlers and is closed; #5492 concerns inspect-plist --relative-path. Neither report names this importer boundary. Open and closed searches for import-reference-source symlinks, ReferenceSourceImportPolicy, wrong reference directories, and realpath normalization found no matching importer-root report. A fresh open-issue listing was checked immediately before submission.

AI assistance disclosure: this report was prepared by a Codex research agent using source review and the public CLI reproduction above. No real user configuration or targets were used. No fix was made.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A valid stable directory selection returns committed historical-source facts for a different source tree; the public CLI reproduction demonstrates the substitution.
  • Workaround status: available
  • Workaround basis: The filesystem-realpath control returned the intended source inventory in the executed reproduction.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6826 ### 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 CLI ### Observed behavior `import-reference-source` silently imports a different source directory when the selected POSIX path contains a directory symlink followed by `..`. The public CLI exits 0 with empty stderr and returns a committed `HistoricalSourceGraph` containing `other.js`, although the filesystem resolves the selected directory to one containing only `intended.js`. ### Reproduction Run from the source checkout at the revision below. This is the executed fixture/probe reduced to its relevant operations; `/tmp/rea-repro` stands in for the scratch directory. All files contain inert JavaScript and are never executed. ```sh mkdir -p /tmp/rea-repro node --input-type=module <<'JS' import { mkdir, writeFile, symlink, realpath } from 'node:fs/promises'; import { spawnSync } from 'node:child_process'; import { join } from 'node:path'; const repo = process.cwd(); const root = '/tmp/rea-repro'; await mkdir(join(root, 'physical/inner'), { recursive: true }); await mkdir(join(root, 'physical/source'), { recursive: true }); await mkdir(join(root, 'source'), { recursive: true }); await writeFile(join(root, 'physical/source/intended.js'), 'export const intended = 1;\n'); await writeFile(join(root, 'source/other.js'), 'export const other = 2;\n'); await symlink('physical/inner', join(root, 'link')); const selected = root + '/link/../source'; console.log('filesystem realpath:', await realpath(selected)); for (const [label, path] of [ ['selected', selected], ['canonical-control', await realpath(selected)], ['lexical-control', join(root, 'source')], ]) { const r = spawnSync(process.execPath, [join(repo, 'scripts/rea.mjs'), 'import-reference-source', path, '--json'], { encoding: 'utf8', env: { ...process.env, REA_LOG_LEVEL: 'silent' }, }); const graph = JSON.parse(r.stdout); console.log(JSON.stringify({ label, status: r.status, entries: graph.entries.map(e => e.path), stderr: r.stderr })); } JS ``` One selected-path run and two controls were executed. The selected graph was also compared with the saved controls: it equals the complete lexical-control JSON and differs from the canonical-control JSON. ### Expected behavior Import the caller-selected directory as the historical reference. The selected path and its filesystem `realpath` identify the same directory and should return the same inventory. [The source-import guide](https://github.com/morluto/rea/blob/7fd722e15bea35255f5eadbd406948e28538d25f/docs/cli.md#L367-L381) describes importing the chosen tree; [AGENTS.md](https://github.com/morluto/rea/blob/7fd722e15bea35255f5eadbd406948e28538d25f/AGENTS.md#L13) requires preserving caller-selected paths and evidence. ### Impact and scope A valid POSIX directory spelling produces a graph and source digests for a different tree, misleading downstream historical-source comparisons. No input is modified. Observed on Linux; macOS and Windows were not tested. The `inventory_state` is `partial` for both controls as well, with the existing generic pathname-race limitation; that does not explain selecting the wrong directory in this stable fixture. Workaround verified: pass the filesystem-resolved directory, which returns `intended.js`. ### REA version 6.7.0; source revision `7fd722e15bea35255f5eadbd406948e28538d25f`. The latest published release is 6.7.0; the published-package runtime was not separately tested. ### Runtime and platform Node.js v26.12.1; Linux 7.2.8-9-omarchy x64. ### Target and artifact context Two synthetic local source directories, each with one inert JavaScript file; one relative directory symlink. ### Interface CLI ### Earliest failure stage Request parsing or validation ### Relevant output or evidence Evidence provenance: observed public CLI execution and source review. Local verification: reproduced. Reproduction completeness: complete for this stable Linux fixture. ```text filesystem realpath: /tmp/rea-repro/physical/source selected status=0 entries=["other.js"] stderr="" canonical-control status=0 entries=["intended.js"] stderr="" lexical-control status=0 entries=["other.js"] stderr="" Selected graph equals lexical control: true Selected graph equals canonical control: false ``` ### Typed outcome or recovery expected Return the graph for `physical/source`; preserve the selected path semantics while canonicalizing the actual filesystem target. ### Analysis or suspected cause Source-derived: [`resolveRoot`](https://github.com/morluto/rea/blob/7fd722e15bea35255f5eadbd406948e28538d25f/src/application/ReferenceSourceImportPolicy.ts#L78-L92) first calls `stat(requestedRoot)`, then calls `realpath(resolve(requestedRoot))`. `resolve` collapses `link/..` before `realpath` sees it, changing the selected directory. Rejection of symlinks inside the imported tree is a separate policy and does not justify reading a different root. ### Additional context Related historical issue #3864 covered this mechanism in Go/Rust CLI handlers and is closed; #5492 concerns `inspect-plist --relative-path`. Neither report names this importer boundary. Open and closed searches for import-reference-source symlinks, ReferenceSourceImportPolicy, wrong reference directories, and realpath normalization found no matching importer-root report. A fresh open-issue listing was checked immediately before submission. AI assistance disclosure: this report was prepared by a Codex research agent using source review and the public CLI reproduction above. No real user configuration or targets were used. No fix was made. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A valid stable directory selection returns committed historical-source facts for a different source tree; the public CLI reproduction demonstrates the substitution. - Workaround status: available - Workaround basis: The filesystem-realpath control returned the intended source inventory in the executed reproduction.
Author
Owner

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

Resolved on main in 5e8f8b316. Root admission canonicalizes requestedRoot without lexical resolve(). The executed production import control selects intended.js via the physical source instead of the lexical decoy. Validation: tests/boundary/filesystem/referenceSourceImport.test.ts — imports the physical source selected through a symlink and ..; recorded execution at a732bd065. One selected filesystem import test passed,25 skipped. This is the producing import boundary; no packaged CLI replay. Later changes to diagnostics and ignore policy are outside selected-root resolution.

<!-- gh-comment-id:6376124509 --> @morluto commented on GitHub (Oct 23, 2026): Resolved on main in [5e8f8b316](https://github.com/morluto/rea/commit/5e8f8b3163e4ff731b2c391a897e5b7e6a0f1a90). Root admission canonicalizes requestedRoot without lexical resolve(). The executed production import control selects intended.js via the physical source instead of the lexical decoy. Validation: `tests/boundary/filesystem/referenceSourceImport.test.ts` — imports the physical source selected through a symlink and ..; recorded execution at `a732bd065`. One selected filesystem import test passed,25 skipped. This is the producing import boundary; no packaged CLI replay. Later changes to diagnostics and ignore policy are outside selected-root resolution.
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#4173
No description provided.