[GH-ISSUE #5492] [Bug] inspect-plist --relative-path reads the wrong file when .. follows a directory symlink #3343

Closed
opened 2026-10-31 00:39:21 +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/5492

Before submitting

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

Area

Native analysis or decompilation

Observed behavior

inspect-plist --relative-path successfully reads a different plist when the selected path contains a directory symlink followed by ... Both relative and absolute selections return CFBundleName: Lexical from plists/Info.plist, although filesystem resolution selects Fixture.app/Info.plist, whose value is BundleRoot. The direct intended-path control returns BundleRoot. All three CLI calls exit 0 with empty stderr.

Reproduction

Run from a built source checkout (npm ci, npm run build:cached). This exact reduced script was replayed once for this report, after independent verification of the original fixture. It uses inert XML, a copied system executable that is not launched, isolated HOME/XDG state, and automatically removed fixtures.

python3 - <<'PY'
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile

repo = Path.cwd()
node = shutil.which("node")
with tempfile.TemporaryDirectory(prefix="rea-paths-") as directory:
    work = Path(directory).resolve()
    fixture = work / "fixture"
    for name in ["Fixture.app/Contents/MacOS", "plists"]:
        (fixture / name).mkdir(parents=True, exist_ok=True)
    shutil.copyfile(shutil.which("echo"),
                    fixture / "Fixture.app/Contents/MacOS/Fixture")
    for name, value in [
        ("Fixture.app/Contents/Info.plist", "BundleDefault"),
        ("Fixture.app/Info.plist", "BundleRoot"),
        ("plists/Info.plist", "Lexical"),
    ]:
        executable = (
            "<key>CFBundleExecutable</key><string>Fixture</string>"
            if name == "Fixture.app/Contents/Info.plist" else ""
        )
        (fixture / name).write_text(
            '<?xml version="1.0" encoding="UTF-8"?>'
            '<plist version="1.0"><dict>' + executable +
            '<key>CFBundleName</key><string>' + value +
            '</string></dict></plist>'
        )
    (fixture / "plists/contents").symlink_to(
        "../Fixture.app/Contents", target_is_directory=True)
    env = {"PATH": os.environ["PATH"], "REA_LOG_LEVEL": "silent",
           "REA_ANALYSIS_PROVIDER": "auto"}
    for key in ["HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME",
                "XDG_CACHE_HOME", "XDG_STATE_HOME", "TMPDIR"]:
        destination = work / key.lower()
        destination.mkdir()
        env[key] = str(destination)
    print("plist target:",
          (fixture / "plists/contents/../Info.plist").resolve().relative_to(fixture))
    app = str(fixture / "Fixture.app")
    commands = [
        ("plist-relative", ["inspect-plist", app, "--relative-path",
                            "../plists/contents/../Info.plist", "--json"]),
        ("plist-absolute", ["inspect-plist", app, "--relative-path",
                            str(fixture / "plists/contents") + "/../Info.plist",
                            "--json"]),
        ("plist-control", ["inspect-plist", app, "--relative-path",
                           str(fixture / "Fixture.app/Info.plist"), "--json"]),
    ]
    for label, arguments in commands:
        result = subprocess.run(
            [node, str(repo / "scripts/rea.mjs"), *arguments],
            cwd=fixture, env=env, capture_output=True, text=True, timeout=90)
        data = json.loads(result.stdout)
        normalized = data.get("normalized_result", data)
        selected = normalized.get("value")
        print(label, "exit:", result.returncode, "selected:", selected,
              "stderr:", repr(result.stderr))
PY

Expected behavior

Both symlink-containing selections should return CFBundleName: BundleRoot, matching the filesystem target and direct-path control. inspect-plist --help explicitly permits a plist path relative to the app root or an absolute path. AGENTS.md requires preserving caller-selected inputs, including paths; src/filesystem/callerPath.ts:3-11 documents why POSIX parent components must resolve after following the preceding symlink.

Impact and scope

The command attributes plist facts to an unintended input while reporting success. The bounded fixture reproduces for both relative and absolute path forms on macOS; the independent verifier observed the same results. Passing the resolved intended target path is a demonstrated workaround. No input mutation or access-control boundary crossing was demonstrated. Other platforms were not exercised.

REA version

6.7.0; source commit 074768901f3cdc8205b61773fb1faf6f6c7b3ca8.

Runtime and platform

Node v26.12.0; macOS 27.0.1; arm64. Built-in native provider using system file/plutil; no external analysis provider required.

Target and artifact context

Disposable macOS app-bundle fixture with a copied system Mach-O executable and three self-authored XML property lists. plists/contents points to ../Fixture.app/Contents.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed from the script above:

plist target: Fixture.app/Info.plist
plist-relative exit: 0 selected: {'CFBundleName': 'Lexical'} stderr: ''
plist-absolute exit: 0 selected: {'CFBundleName': 'Lexical'} stderr: ''
plist-control exit: 0 selected: {'CFBundleName': 'BundleRoot'} stderr: ''

The current upstream re-check at 00a055be0fad728e1ba183c8de968d5538ba11ef found no changes since the tested baseline to src/native/NativePlistSource.ts or src/filesystem/callerPath.ts. Runtime reproduction is at the baseline above.

Typed outcome or recovery expected

Return the decoded value of the caller-selected plist, preserving filesystem path semantics. The successful direct-target control demonstrates the intended result without changing fixture contents.

Analysis or suspected cause

Source-derived: resolveNativePlistSelection in src/native/NativePlistSource.ts:45-46,58-59 uses node:path.resolve for both absolute and app-relative selections. That removes contents/.. lexically before the filesystem follows contents, selecting the sibling plists/Info.plist instead of the bundle-root file.

Additional context

Prepared with AI assistance and independently reproduced. NOT_ENOUGH_INFO: the last known good version is unknown; no regression range was established. Windows behavior was not tested.

Related: the same ..-after-symlink pattern in import-reference-source is tracked by https://github.com/morluto/rea/pull/3562

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5492 ### 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 Native analysis or decompilation ### Observed behavior `inspect-plist --relative-path` successfully reads a different plist when the selected path contains a directory symlink followed by `..`. Both relative and absolute selections return `CFBundleName: Lexical` from `plists/Info.plist`, although filesystem resolution selects `Fixture.app/Info.plist`, whose value is `BundleRoot`. The direct intended-path control returns `BundleRoot`. All three CLI calls exit 0 with empty stderr. ### Reproduction Run from a built source checkout (`npm ci`, `npm run build:cached`). This exact reduced script was replayed once for this report, after independent verification of the original fixture. It uses inert XML, a copied system executable that is not launched, isolated HOME/XDG state, and automatically removed fixtures. ```sh python3 - <<'PY' import json import os from pathlib import Path import shutil import subprocess import tempfile repo = Path.cwd() node = shutil.which("node") with tempfile.TemporaryDirectory(prefix="rea-paths-") as directory: work = Path(directory).resolve() fixture = work / "fixture" for name in ["Fixture.app/Contents/MacOS", "plists"]: (fixture / name).mkdir(parents=True, exist_ok=True) shutil.copyfile(shutil.which("echo"), fixture / "Fixture.app/Contents/MacOS/Fixture") for name, value in [ ("Fixture.app/Contents/Info.plist", "BundleDefault"), ("Fixture.app/Info.plist", "BundleRoot"), ("plists/Info.plist", "Lexical"), ]: executable = ( "<key>CFBundleExecutable</key><string>Fixture</string>" if name == "Fixture.app/Contents/Info.plist" else "" ) (fixture / name).write_text( '<?xml version="1.0" encoding="UTF-8"?>' '<plist version="1.0"><dict>' + executable + '<key>CFBundleName</key><string>' + value + '</string></dict></plist>' ) (fixture / "plists/contents").symlink_to( "../Fixture.app/Contents", target_is_directory=True) env = {"PATH": os.environ["PATH"], "REA_LOG_LEVEL": "silent", "REA_ANALYSIS_PROVIDER": "auto"} for key in ["HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_STATE_HOME", "TMPDIR"]: destination = work / key.lower() destination.mkdir() env[key] = str(destination) print("plist target:", (fixture / "plists/contents/../Info.plist").resolve().relative_to(fixture)) app = str(fixture / "Fixture.app") commands = [ ("plist-relative", ["inspect-plist", app, "--relative-path", "../plists/contents/../Info.plist", "--json"]), ("plist-absolute", ["inspect-plist", app, "--relative-path", str(fixture / "plists/contents") + "/../Info.plist", "--json"]), ("plist-control", ["inspect-plist", app, "--relative-path", str(fixture / "Fixture.app/Info.plist"), "--json"]), ] for label, arguments in commands: result = subprocess.run( [node, str(repo / "scripts/rea.mjs"), *arguments], cwd=fixture, env=env, capture_output=True, text=True, timeout=90) data = json.loads(result.stdout) normalized = data.get("normalized_result", data) selected = normalized.get("value") print(label, "exit:", result.returncode, "selected:", selected, "stderr:", repr(result.stderr)) PY ``` ### Expected behavior Both symlink-containing selections should return `CFBundleName: BundleRoot`, matching the filesystem target and direct-path control. `inspect-plist --help` explicitly permits a plist path relative to the app root or an absolute path. `AGENTS.md` requires preserving caller-selected inputs, including paths; `src/filesystem/callerPath.ts:3-11` documents why POSIX parent components must resolve after following the preceding symlink. ### Impact and scope The command attributes plist facts to an unintended input while reporting success. The bounded fixture reproduces for both relative and absolute path forms on macOS; the independent verifier observed the same results. Passing the resolved intended target path is a demonstrated workaround. No input mutation or access-control boundary crossing was demonstrated. Other platforms were not exercised. ### REA version 6.7.0; source commit `074768901f3cdc8205b61773fb1faf6f6c7b3ca8`. ### Runtime and platform Node v26.12.0; macOS 27.0.1; arm64. Built-in native provider using system file/plutil; no external analysis provider required. ### Target and artifact context Disposable macOS app-bundle fixture with a copied system Mach-O executable and three self-authored XML property lists. `plists/contents` points to `../Fixture.app/Contents`. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed from the script above: ```text plist target: Fixture.app/Info.plist plist-relative exit: 0 selected: {'CFBundleName': 'Lexical'} stderr: '' plist-absolute exit: 0 selected: {'CFBundleName': 'Lexical'} stderr: '' plist-control exit: 0 selected: {'CFBundleName': 'BundleRoot'} stderr: '' ``` The current upstream re-check at `00a055be0fad728e1ba183c8de968d5538ba11ef` found no changes since the tested baseline to `src/native/NativePlistSource.ts` or `src/filesystem/callerPath.ts`. Runtime reproduction is at the baseline above. ### Typed outcome or recovery expected Return the decoded value of the caller-selected plist, preserving filesystem path semantics. The successful direct-target control demonstrates the intended result without changing fixture contents. ### Analysis or suspected cause Source-derived: `resolveNativePlistSelection` in `src/native/NativePlistSource.ts:45-46,58-59` uses `node:path.resolve` for both absolute and app-relative selections. That removes `contents/..` lexically before the filesystem follows `contents`, selecting the sibling `plists/Info.plist` instead of the bundle-root file. ### Additional context Prepared with AI assistance and independently reproduced. NOT_ENOUGH_INFO: the last known good version is unknown; no regression range was established. Windows behavior was not tested. Related: the same `..`-after-symlink pattern in `import-reference-source` is tracked by https://github.com/morluto/rea/pull/3562
Author
Owner

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

Cross-referencing with sibling path resolution issues in Cluster X: #5103, #5258, and #5479. Lexical collapse of .. before resolving directory symlinks causes inspect-plist --relative-path to target the wrong filesystem node.

Consolidated under Cluster X (Application Target Path Symlink Resolution & Caller-Selected Path Loss) in tracker #4330.

<!-- gh-comment-id:6290886269 --> @matbrgz commented on GitHub (Oct 20, 2026): Cross-referencing with sibling path resolution issues in **Cluster X**: #5103, #5258, and #5479. Lexical collapse of `..` before resolving directory symlinks causes `inspect-plist --relative-path` to target the wrong filesystem node. Consolidated under **Cluster X (Application Target Path Symlink Resolution & Caller-Selected Path Loss)** in 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#3343
No description provided.