[GH-ISSUE #7556] [Bug] inspect-macho fails when the executable filename ends in parentheses #4495

Closed
opened 2026-10-31 00:50:17 +00:00 by bsakaguchi · 0 comments
Owner

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

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

node scripts/rea.mjs inspect-macho "$WORK/Example (Beta)" --format json exits 1 with execution_failure for an unchanged copy of the system Mach-O /usr/bin/true. Its captured otool diagnostic says it cannot open Example , dropping the final (Beta) from the selected filename. No normalized inspection result is returned.

The identical bytes named Example-Beta inspect successfully. list-architectures and inspect-signature both succeed on Example (Beta). A small synthetic app bundle whose declared executable is named Example (Beta) also fails at the resolved executable. No target executable or GUI was launched.

Reproduction

  1. On macOS, use Node 24.22.1 and Apple's command-line development tools. Start at the baseline checkout root with its locked dependencies and built runtime already prepared.
  2. Run the following script. It isolates runtime directories, creates the two byte-identical files, attempts the affected command twice, checks the successful controls, and removes its fixtures on exit.
set -eu
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
export HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config"
export XDG_DATA_HOME="$WORK/data" XDG_CACHE_HOME="$WORK/cache"
export XDG_STATE_HOME="$WORK/state" XDG_RUNTIME_DIR="$WORK/runtime"
export TMPDIR="$WORK/temp"
mkdir -p "$HOME" "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_CACHE_HOME" \
  "$XDG_STATE_HOME" "$XDG_RUNTIME_DIR" "$TMPDIR"
cp /usr/bin/true "$WORK/Example (Beta)"
cp "$WORK/Example (Beta)" "$WORK/Example-Beta"
for run in 1 2; do
  if node scripts/rea.mjs inspect-macho "$WORK/Example (Beta)" --format json > "$WORK/failure-$run.json"; then
    echo "Unexpected success"; exit 1
  fi
done
node scripts/rea.mjs inspect-macho "$WORK/Example-Beta" --format json > "$WORK/control.json"
node scripts/rea.mjs list-architectures "$WORK/Example (Beta)" --format json > "$WORK/architectures.json"
node scripts/rea.mjs inspect-signature "$WORK/Example (Beta)" --format json > "$WORK/signature.json"
python3 - "$WORK" <<'PYRESULT'
import json, pathlib, sys
w = pathlib.Path(sys.argv[1])
for run in (1, 2):
    result = json.loads((w / f"failure-{run}.json").read_text())
    assert result["code"] == "execution_failure", result
    d = result["details"]["diagnostics"]
    assert (d["tool"], d["reason"], d["exit_code"]) == ("otool", "nonzero-exit", 1), d
    print(f"parenthesized run {run}: execution_failure, otool nonzero-exit 1")
for name in ("control", "architectures", "signature"):
    result = json.loads((w / f"{name}.json").read_text())
    assert "normalized_result" in result, result
    print(f"{name}: normalized_result returned")
assert (w / "Example (Beta)").read_bytes() == (w / "Example-Beta").read_bytes()
print("file bytes: identical")
PYRESULT
  1. The script completes with the output in Evidence. The parenthesized filename failed 2/2 in this script, after an earlier independent 2/2 reproduction. All three control commands succeeded in both sets.

Expected behavior

Inspect the selected valid Mach-O and return its available facts regardless of this filename suffix. The CLI help accepts a “Mach-O or app path” without excluding parentheses. The native inspection contract promises Mach-O slices, load commands, symbols and provenance, and the repository requires preserving caller-selected paths. The successful identical-byte control establishes that this file's format is supported.

Impact and scope

P2: callers lose the entire inspection result for the demonstrated executable filename. The error points at a shortened path and recommends rea doctor, although the ordinary-name control works in the same environment. The synthetic app test establishes that the problem also applies when bundle resolution selects an executable with this suffix; parentheses only in the bundle directory name were not isolated as a trigger.

A verified workaround is copying the file to a name without the suffix and inspecting the copy. A normally named symlink to the affected file still fails. Example (Beta).bin succeeds, so parentheses elsewhere in a basename do not necessarily prevent inspection. Other host/toolchain versions, all possible parenthesis spellings and real-world frequency were not measured.

REA version

Exact node scripts/rea.mjs --version output: 6.8.0.
Source commit: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1, build 26A434; arm64. Real Apple tools selected through Xcode. The successful control's otool provenance records executable SHA-256 73273f28415f496f37f597290bf7aadfcfbb9f43748bcec10f90950053a4b9e9; its tool-version field is null because REA reports no uniform stable version flag. No shim or deep-analysis provider was used.

Target and artifact context

Unmodified copies of the universal /usr/bin/true. REA's successful control selected arm64e; architecture inventory returned three slices. The copied file's SHA-256 is 511602b8cf083bdff74d43242b71c27484bd63121c07f3b59575d35676e1f6cb. A separate minimal synthetic app declared CFBundleExecutable=Example (Beta) and contained the same Mach-O bytes.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The reproduction script printed:

parenthesized run 1: execution_failure, otool nonzero-exit 1
parenthesized run 2: execution_failure, otool nonzero-exit 1
control: normalized_result returned
architectures: normalized_result returned
signature: normalized_result returned
file bytes: identical

The affected CLI result includes this excerpt, with generated directories replaced by $WORK and the Xcode tool directory abbreviated:

{
  "code": "execution_failure",
  "details": {
    "provider_id": "native-macos",
    "operation": "inspect_macho",
    "diagnostics": {
      "tool": "otool",
      "reason": "nonzero-exit",
      "exit_code": 1,
      "signal": null
    },
    "captured_output": {
      "stdout": "",
      "stderr": "error: <Xcode tool directory>/otool-classic: can't open file: $WORK/Example  (No such file or directory)\n",
      "truncated": false
    }
  }
}

Direct xcrun otool -h on the parenthesized file independently exits 1 with the shortened operand diagnostic; the ordinary-name and .bin controls exit 0. This places the failure before load-command parsing.

Expected source at the baseline: src/cli/utilityCommands.ts:193-214, src/contracts/native/nativeToolContracts.ts:102-104, and AGENTS.md:13.

Failure source: src/native/NativeMachoInspection.ts:69-75,113-131 passes the selected target path to required otool commands. The real runner failure is translated at src/native/NativeMacOSProvider.ts:1306-1311 and src/native/NativeCommandAnalysisError.ts:197-209. src/application/BinaryTargetResolver.ts:152 canonicalizes the symlink, explaining why that attempted workaround still reaches the affected basename.

Analysis or suspected cause

Hypothesis: Apple's otool treats a final (name) as archive-member operand syntax instead of part of a filename. The direct native refusal, shortened diagnostic and successful .bin suffix control support that explanation. Apple's operand parser was not inspected. REA currently forwards this spelling into the required command without a neutral artifact name, and the nonzero exit prevents inspection from completing. No fix was implemented or tested.

Additional context

Open #7391 concerns an LF inside a section name: otool succeeds there and REA rejects its output. Closed #760 concerns an LF in a filename corrupting lipo parsing; closed #765 concerns whitespace trimmed from a plist executable name. Merged #880 strips echoed operand headers from successful tool output, and merged #4067 preserves symlink/parent traversal semantics. Those inputs and closure signals differ from this native operand refusal. Searches of open and closed issues, issue comments and all PR states found no matching report or fix.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7556 ### 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 `node scripts/rea.mjs inspect-macho "$WORK/Example (Beta)" --format json` exits 1 with `execution_failure` for an unchanged copy of the system Mach-O `/usr/bin/true`. Its captured `otool` diagnostic says it cannot open `Example `, dropping the final `(Beta)` from the selected filename. No normalized inspection result is returned. The identical bytes named `Example-Beta` inspect successfully. `list-architectures` and `inspect-signature` both succeed on `Example (Beta)`. A small synthetic app bundle whose declared executable is named `Example (Beta)` also fails at the resolved executable. No target executable or GUI was launched. ### Reproduction 1. On macOS, use Node 24.22.1 and Apple's command-line development tools. Start at the baseline checkout root with its locked dependencies and built runtime already prepared. 2. Run the following script. It isolates runtime directories, creates the two byte-identical files, attempts the affected command twice, checks the successful controls, and removes its fixtures on exit. ```sh set -eu WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT export HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" export XDG_DATA_HOME="$WORK/data" XDG_CACHE_HOME="$WORK/cache" export XDG_STATE_HOME="$WORK/state" XDG_RUNTIME_DIR="$WORK/runtime" export TMPDIR="$WORK/temp" mkdir -p "$HOME" "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_CACHE_HOME" \ "$XDG_STATE_HOME" "$XDG_RUNTIME_DIR" "$TMPDIR" cp /usr/bin/true "$WORK/Example (Beta)" cp "$WORK/Example (Beta)" "$WORK/Example-Beta" for run in 1 2; do if node scripts/rea.mjs inspect-macho "$WORK/Example (Beta)" --format json > "$WORK/failure-$run.json"; then echo "Unexpected success"; exit 1 fi done node scripts/rea.mjs inspect-macho "$WORK/Example-Beta" --format json > "$WORK/control.json" node scripts/rea.mjs list-architectures "$WORK/Example (Beta)" --format json > "$WORK/architectures.json" node scripts/rea.mjs inspect-signature "$WORK/Example (Beta)" --format json > "$WORK/signature.json" python3 - "$WORK" <<'PYRESULT' import json, pathlib, sys w = pathlib.Path(sys.argv[1]) for run in (1, 2): result = json.loads((w / f"failure-{run}.json").read_text()) assert result["code"] == "execution_failure", result d = result["details"]["diagnostics"] assert (d["tool"], d["reason"], d["exit_code"]) == ("otool", "nonzero-exit", 1), d print(f"parenthesized run {run}: execution_failure, otool nonzero-exit 1") for name in ("control", "architectures", "signature"): result = json.loads((w / f"{name}.json").read_text()) assert "normalized_result" in result, result print(f"{name}: normalized_result returned") assert (w / "Example (Beta)").read_bytes() == (w / "Example-Beta").read_bytes() print("file bytes: identical") PYRESULT ``` 3. The script completes with the output in Evidence. The parenthesized filename failed 2/2 in this script, after an earlier independent 2/2 reproduction. All three control commands succeeded in both sets. ### Expected behavior Inspect the selected valid Mach-O and return its available facts regardless of this filename suffix. The CLI help accepts a “Mach-O or app path” without excluding parentheses. The native inspection contract promises Mach-O slices, load commands, symbols and provenance, and the repository requires preserving caller-selected paths. The successful identical-byte control establishes that this file's format is supported. ### Impact and scope P2: callers lose the entire inspection result for the demonstrated executable filename. The error points at a shortened path and recommends `rea doctor`, although the ordinary-name control works in the same environment. The synthetic app test establishes that the problem also applies when bundle resolution selects an executable with this suffix; parentheses only in the bundle directory name were not isolated as a trigger. A verified workaround is copying the file to a name without the suffix and inspecting the copy. A normally named symlink to the affected file still fails. `Example (Beta).bin` succeeds, so parentheses elsewhere in a basename do not necessarily prevent inspection. Other host/toolchain versions, all possible parenthesis spellings and real-world frequency were not measured. ### REA version Exact `node scripts/rea.mjs --version` output: `6.8.0`. Source commit: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js `v24.22.1`; macOS `27.0.1`, build `26A434`; arm64. Real Apple tools selected through Xcode. The successful control's otool provenance records executable SHA-256 `73273f28415f496f37f597290bf7aadfcfbb9f43748bcec10f90950053a4b9e9`; its tool-version field is null because REA reports no uniform stable version flag. No shim or deep-analysis provider was used. ### Target and artifact context Unmodified copies of the universal `/usr/bin/true`. REA's successful control selected `arm64e`; architecture inventory returned three slices. The copied file's SHA-256 is `511602b8cf083bdff74d43242b71c27484bd63121c07f3b59575d35676e1f6cb`. A separate minimal synthetic app declared `CFBundleExecutable=Example (Beta)` and contained the same Mach-O bytes. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The reproduction script printed: ```text parenthesized run 1: execution_failure, otool nonzero-exit 1 parenthesized run 2: execution_failure, otool nonzero-exit 1 control: normalized_result returned architectures: normalized_result returned signature: normalized_result returned file bytes: identical ``` The affected CLI result includes this excerpt, with generated directories replaced by `$WORK` and the Xcode tool directory abbreviated: ```json { "code": "execution_failure", "details": { "provider_id": "native-macos", "operation": "inspect_macho", "diagnostics": { "tool": "otool", "reason": "nonzero-exit", "exit_code": 1, "signal": null }, "captured_output": { "stdout": "", "stderr": "error: <Xcode tool directory>/otool-classic: can't open file: $WORK/Example (No such file or directory)\n", "truncated": false } } } ``` Direct `xcrun otool -h` on the parenthesized file independently exits 1 with the shortened operand diagnostic; the ordinary-name and `.bin` controls exit 0. This places the failure before load-command parsing. Expected source at the baseline: `src/cli/utilityCommands.ts:193-214`, `src/contracts/native/nativeToolContracts.ts:102-104`, and `AGENTS.md:13`. Failure source: `src/native/NativeMachoInspection.ts:69-75,113-131` passes the selected target path to required otool commands. The real runner failure is translated at `src/native/NativeMacOSProvider.ts:1306-1311` and `src/native/NativeCommandAnalysisError.ts:197-209`. `src/application/BinaryTargetResolver.ts:152` canonicalizes the symlink, explaining why that attempted workaround still reaches the affected basename. ### Analysis or suspected cause Hypothesis: Apple's otool treats a final `(name)` as archive-member operand syntax instead of part of a filename. The direct native refusal, shortened diagnostic and successful `.bin` suffix control support that explanation. Apple's operand parser was not inspected. REA currently forwards this spelling into the required command without a neutral artifact name, and the nonzero exit prevents inspection from completing. No fix was implemented or tested. ### Additional context Open #7391 concerns an LF inside a section name: otool succeeds there and REA rejects its output. Closed #760 concerns an LF in a filename corrupting lipo parsing; closed #765 concerns whitespace trimmed from a plist executable name. Merged #880 strips echoed operand headers from successful tool output, and merged #4067 preserves symlink/parent traversal semantics. Those inputs and closure signals differ from this native operand refusal. Searches of open and closed issues, issue comments and all PR states found no matching report or fix.
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#4495
No description provided.