[GH-ISSUE #3864] [Bug] Go and Rust inspection read the wrong file when a path has a symlink followed by .. #1483

Closed
opened 2026-10-16 21:06:03 +00:00 by bsakaguchi · 0 comments
Owner

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

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

rea inspect-go-binary and rea inspect-rust-binary resolve the path argument lexically before opening it. When the path contains a directory symlink followed by .., both commands inspect a different file than the one the filesystem resolves, exit 0, and return that other file's SHA-256 (and, for Go, its compiler version) as successful Evidence.

All four commands exit 0 with empty stderr. The link/../sample runs inspect the sibling ./sample instead: they return its digest, and Go reports its compiler version go1.25.0. An absolute spelling containing the same symlink and .. also reproduced the Go failure.

Reproduction

In an empty directory, create two small non-executed ELF64 images with different Go build-info strings and a directory symlink link -> target/child. On POSIX, link/../sample names target/sample, not ./sample; the script asserts that.

python3 - <<'PY'
import pathlib, struct
p = pathlib.Path('.')
(p / 'target/child').mkdir(parents=True)
(p / 'link').symlink_to('target/child', target_is_directory=True)
for path, version in [('sample', 'go1.25.0'), ('target/sample', 'go1.26.0')]:
    b = bytearray(4096)
    b[:7] = b'\x7fELF\x02\x01\x01'
    for off, val in [(16, 2), (18, 62), (52, 64), (54, 56), (56, 1), (58, 64)]:
        struct.pack_into('<H', b, off, val)
    struct.pack_into('<I', b, 20, 1)
    struct.pack_into('<Q', b, 32, 128)
    for off, val in [(128, 1), (132, 6)]:
        struct.pack_into('<I', b, off, val)
    for off, val in [(144, 65536), (160, 4096), (168, 4096)]:
        struct.pack_into('<Q', b, off, val)
    b[1024:1038] = b'\xff Go buildinf:'
    b[1038:1040] = b'\x08\x02'
    v = version.encode()
    m = (bytes.fromhex('3077af0c9274080241e1c107e6d618e6') + b'path\texample.test/tool\n' + bytes.fromhex('f932433186182072008242104116d8f2'))
    payload = bytes([len(v)]) + v + bytes([len(m)]) + m
    b[1056:1056 + len(payload)] = payload
    (p / path).write_bytes(b)
assert (p / 'link/../sample').read_bytes() == (p / 'target/sample').read_bytes()
PY
rea inspect-go-binary link/../sample --json > selected.json
rea inspect-go-binary target/sample --json > control.json
rea inspect-rust-binary link/../sample --json > selected-rust.json
rea inspect-rust-binary target/sample --json > control-rust.json

Compare normalized_result.artifact.sha256 across the four outputs and normalized_result.build_info.go_version across the two Go outputs.

Expected behavior

link/../sample and target/sample identify the same file, so each command returns the same SHA-256 for both spellings, and Go reports go1.26.0 for both. AGENTS.md:13 requires preserving caller-selected paths and digests. docs/go-binaries.md:3 and docs/rust-binaries.md:3 define inspection of one selected local file, and line 24 of each guide binds the result to that artifact's SHA-256.

Impact and scope

Valid paths containing a directory symlink followed by .. produce successful Evidence, digests, and Go compiler conclusions about a different local file; no file is modified.

Workaround: available. Passing a filesystem-resolved path or the direct path target/sample returned the correct digest and version in the reproduction.

Reporter's impact estimate: P2 (supported).

REA version

6.5.0, built from source at main commit 034342ba28 (latest npm release is 6.5.0)

Runtime and platform

Node.js v26.10.0; macOS 27.0.1 arm64

Target and artifact context

ELF64 executables (synthetic Go build-info fixtures) reached through a directory symlink

Interface

CLI

Earliest failure stage

Request parsing or validation

Relevant output or evidence

  • Expected source: AGENTS.md:13 (preserve caller-selected paths and digests); docs/go-binaries.md and docs/rust-binaries.md lines 3 and 24
  • Failure source: observed CLI outputs below; resolve(args.path) at src/cli/goCommands.ts:27 and src/cli/rustCommands.ts:27
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

The filesystem byte-equality assertion passed. Observed result fields:

inspect-go-binary   link/../sample  exit=0 sha256=d7ac65206eba5017fe3e9e94a5e4c53f5c3267f8fa286ef13571171ebaa4bcc3 go_version=go1.25.0
inspect-go-binary   target/sample   exit=0 sha256=5863898d5ffdf71b446c23c66d0b831c6976441c1bc3146b5bb55f9194fd9524 go_version=go1.26.0
inspect-rust-binary link/../sample  exit=0 sha256=d7ac65206eba5017fe3e9e94a5e4c53f5c3267f8fa286ef13571171ebaa4bcc3
inspect-rust-binary target/sample   exit=0 sha256=5863898d5ffdf71b446c23c66d0b831c6976441c1bc3146b5bb55f9194fd9524

Passing the filesystem-resolved path (pathlib.Path('link/../sample').resolve()) to both commands returned the target/sample digest, and Go returned go1.26.0. The Rust command accepts these containers and reports byte identity even though they contain no cargo-auditable metadata.

Source-derived analysis: both CLI handlers pass resolve(args.path) to the shared services. Node's path.resolve collapses link/.. textually, so the services never see the caller's path.

Typed outcome or recovery expected

Both commands should open the file the filesystem resolves for the caller's path and return its SHA-256 and Go version, matching the direct path.

Restoration check:

On the same fixture, rea inspect-go-binary link/../sample --json currently returns sha256 d7ac6520… and go1.25.0; after a fix it should return 5863898d… and go1.26.0, identical to the target/sample control. The same applies to rea inspect-rust-binary.

Analysis or suspected cause

Source-derived: src/cli/goCommands.ts:27 and src/cli/rustCommands.ts:27 pass resolve(args.path), which collapses link/.. lexically before the file is opened.

Additional context

Open #3324 reports the same lexical-resolution mechanism for inspect-sqlite-database only; this report covers the Go and Rust commands, which have their own call sites. Maintainers may prefer to fix all three together. Open #3303 concerns symlink-target identity during process capture, a different operation. Other commands that call lexical resolve were not runtime-tested.

This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3864 ### 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 `rea inspect-go-binary` and `rea inspect-rust-binary` resolve the path argument lexically before opening it. When the path contains a directory symlink followed by `..`, both commands inspect a different file than the one the filesystem resolves, exit 0, and return that other file's SHA-256 (and, for Go, its compiler version) as successful Evidence. All four commands exit 0 with empty stderr. The `link/../sample` runs inspect the sibling `./sample` instead: they return its digest, and Go reports its compiler version `go1.25.0`. An absolute spelling containing the same symlink and `..` also reproduced the Go failure. ### Reproduction In an empty directory, create two small non-executed ELF64 images with different Go build-info strings and a directory symlink `link -> target/child`. On POSIX, `link/../sample` names `target/sample`, not `./sample`; the script asserts that. ```sh python3 - <<'PY' import pathlib, struct p = pathlib.Path('.') (p / 'target/child').mkdir(parents=True) (p / 'link').symlink_to('target/child', target_is_directory=True) for path, version in [('sample', 'go1.25.0'), ('target/sample', 'go1.26.0')]: b = bytearray(4096) b[:7] = b'\x7fELF\x02\x01\x01' for off, val in [(16, 2), (18, 62), (52, 64), (54, 56), (56, 1), (58, 64)]: struct.pack_into('<H', b, off, val) struct.pack_into('<I', b, 20, 1) struct.pack_into('<Q', b, 32, 128) for off, val in [(128, 1), (132, 6)]: struct.pack_into('<I', b, off, val) for off, val in [(144, 65536), (160, 4096), (168, 4096)]: struct.pack_into('<Q', b, off, val) b[1024:1038] = b'\xff Go buildinf:' b[1038:1040] = b'\x08\x02' v = version.encode() m = (bytes.fromhex('3077af0c9274080241e1c107e6d618e6') + b'path\texample.test/tool\n' + bytes.fromhex('f932433186182072008242104116d8f2')) payload = bytes([len(v)]) + v + bytes([len(m)]) + m b[1056:1056 + len(payload)] = payload (p / path).write_bytes(b) assert (p / 'link/../sample').read_bytes() == (p / 'target/sample').read_bytes() PY rea inspect-go-binary link/../sample --json > selected.json rea inspect-go-binary target/sample --json > control.json rea inspect-rust-binary link/../sample --json > selected-rust.json rea inspect-rust-binary target/sample --json > control-rust.json ``` Compare `normalized_result.artifact.sha256` across the four outputs and `normalized_result.build_info.go_version` across the two Go outputs. ### Expected behavior `link/../sample` and `target/sample` identify the same file, so each command returns the same SHA-256 for both spellings, and Go reports `go1.26.0` for both. `AGENTS.md:13` requires preserving caller-selected paths and digests. `docs/go-binaries.md:3` and `docs/rust-binaries.md:3` define inspection of one selected local file, and line 24 of each guide binds the result to that artifact's SHA-256. ### Impact and scope Valid paths containing a directory symlink followed by `..` produce successful Evidence, digests, and Go compiler conclusions about a different local file; no file is modified. Workaround: available. Passing a filesystem-resolved path or the direct path `target/sample` returned the correct digest and version in the reproduction. Reporter's impact estimate: P2 (supported). ### REA version 6.5.0, built from source at main commit 034342ba28c8e56c3746be92b78aa7299afa02bd (latest npm release is 6.5.0) ### Runtime and platform Node.js v26.10.0; macOS 27.0.1 arm64 ### Target and artifact context ELF64 executables (synthetic Go build-info fixtures) reached through a directory symlink ### Interface CLI ### Earliest failure stage Request parsing or validation ### Relevant output or evidence - Expected source: `AGENTS.md:13` (preserve caller-selected paths and digests); `docs/go-binaries.md` and `docs/rust-binaries.md` lines 3 and 24 - Failure source: observed CLI outputs below; `resolve(args.path)` at `src/cli/goCommands.ts:27` and `src/cli/rustCommands.ts:27` - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete The filesystem byte-equality assertion passed. Observed result fields: ```text inspect-go-binary link/../sample exit=0 sha256=d7ac65206eba5017fe3e9e94a5e4c53f5c3267f8fa286ef13571171ebaa4bcc3 go_version=go1.25.0 inspect-go-binary target/sample exit=0 sha256=5863898d5ffdf71b446c23c66d0b831c6976441c1bc3146b5bb55f9194fd9524 go_version=go1.26.0 inspect-rust-binary link/../sample exit=0 sha256=d7ac65206eba5017fe3e9e94a5e4c53f5c3267f8fa286ef13571171ebaa4bcc3 inspect-rust-binary target/sample exit=0 sha256=5863898d5ffdf71b446c23c66d0b831c6976441c1bc3146b5bb55f9194fd9524 ``` Passing the filesystem-resolved path (`pathlib.Path('link/../sample').resolve()`) to both commands returned the `target/sample` digest, and Go returned `go1.26.0`. The Rust command accepts these containers and reports byte identity even though they contain no cargo-auditable metadata. Source-derived analysis: both CLI handlers pass `resolve(args.path)` to the shared services. Node's `path.resolve` collapses `link/..` textually, so the services never see the caller's path. ### Typed outcome or recovery expected Both commands should open the file the filesystem resolves for the caller's path and return its SHA-256 and Go version, matching the direct path. Restoration check: On the same fixture, `rea inspect-go-binary link/../sample --json` currently returns sha256 `d7ac6520…` and `go1.25.0`; after a fix it should return `5863898d…` and `go1.26.0`, identical to the `target/sample` control. The same applies to `rea inspect-rust-binary`. ### Analysis or suspected cause Source-derived: src/cli/goCommands.ts:27 and src/cli/rustCommands.ts:27 pass resolve(args.path), which collapses link/.. lexically before the file is opened. ### Additional context Open #3324 reports the same lexical-resolution mechanism for `inspect-sqlite-database` only; this report covers the Go and Rust commands, which have their own call sites. Maintainers may prefer to fix all three together. Open #3303 concerns symlink-target identity during process capture, a different operation. Other commands that call lexical `resolve` were not runtime-tested. This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.
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#1483
No description provided.