[GH-ISSUE #7436] [Bug] Eight binary CLI commands resolve an empty target before rejecting it #4443

Closed
opened 2026-10-31 00:49:49 +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/7436

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

An empty target reaches filesystem admission in inspect, inspect-managed-artifact, inspect-managed-members, list-architectures, search-bytes, address-to-file-offset, inspect-native-instruction and the left operand of compare-managed-members. Seven commands report target_unavailable for a plain working directory. From an app-bundle working directory, inspect "" instead selects Contents/MacOS/Demo and reports its unsupported format, exactly like explicit inspect .. All included calls exit 1; none rejects the empty selection before resolution.

Reproduction

Use Node 24.22.1 at the source commit below. From a fresh checkout run npm ci && npm run build:cached, then run this block from its root. The verifier rebuilt the supplied checkout with its unchanged lockfile. The script authors only a synthetic plist and an inert text file, isolates every product state directory below $WORK, and invokes the public dispatcher sequentially. Each affected invocation was reproduced once in the initial replay and once with this reduced script.

WORK="$(mktemp -d)" node --input-type=module <<'JS'
import { mkdirSync, realpathSync, writeFileSync } from 'node:fs';
import { join, relative, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
const cli = resolve('scripts/rea.mjs');
const root = realpathSync(process.env.WORK);
const env = { ...process.env, REA_LOG_LEVEL: 'silent' };
for (const [key, dir] of Object.entries({ HOME: 'home', XDG_CONFIG_HOME: 'config', XDG_DATA_HOME: 'data', XDG_CACHE_HOME: 'cache', XDG_STATE_HOME: 'state', TMPDIR: 'temp' })) {
  env[key] = join(root, dir);
  mkdirSync(env[key], { recursive: true });
}
const plain = join(root, 'plain');
const bundle = join(root, 'Demo.app');
mkdirSync(plain);
mkdirSync(join(bundle, 'Contents/MacOS'), { recursive: true });
writeFileSync(join(bundle, 'Contents/Info.plist'), '<plist><dict><key>CFBundleExecutable</key><string>Demo</string></dict></plist>');
writeFileSync(join(bundle, 'Contents/MacOS/Demo'), 'not an executable image\n');
const cases = [
  ['inspect-managed-artifact', '', plain],
  ['inspect-managed-members', '', plain],
  ['list-architectures', '', plain],
  ['search-bytes', '', plain, '00'],
  ['address-to-file-offset', '', plain, '0x1000'],
  ['inspect-native-instruction', '', plain, '0x1000'],
  ['compare-managed-members', '', plain, '.'],
  ['inspect', '', bundle],
  ['inspect', '.', bundle],
  ['inspect-artifact', '', plain],
];
for (const [command, input, cwd, ...extra] of cases) {
  const result = spawnSync(process.execPath, [cli, command, input, ...extra, '--json'], { cwd, env, encoding: 'utf8', timeout: 15000 });
  const data = JSON.parse(result.stdout);
  console.log(JSON.stringify({ command, input, exit: result.status, code: data.code, selected: data.details?.path ? relative(root, data.details.path) : undefined, constraint: data.details?.constraint, field: data.fieldErrors?.[0]?.path, issue: data.fieldErrors?.[0]?.code }));
}
JS

Expected behavior

Reject each empty path at request validation before resolving or reading cwd, identifying path or leftPath. Preserve explicit nonempty selection. docs/cli.md:42-45 promises that local inspection rejects empty paths before resolution and that an empty string never selects the working directory. docs/tool-design.md:339-343 applies the same rule to CLI translations. The observed inspect-artifact control already returns VALIDATION_ERROR, field path, issue too_small.

Impact and scope

P2: an empty shell variable selects an unintended directory or bundle executable and produces misleading target-repair advice instead of identifying the invalid argument. The observed effect is incorrect admission and diagnostics; no successful binary analysis, target mutation or execution is claimed. A caller can safely reject an empty variable before invocation. The executed scope is these eight binary entrypoints on macOS; only the left comparison operand was exercised. Other platforms, real providers, MCP and downstream consequences were not tested. No security boundary crossing was observed.

REA version

Exact node scripts/rea.mjs --version stdout: 6.8.0 followed by a newline. Built source commit: a6a69ec63e0b9f2742d9b39534e2dfc02b6a0814.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1, build 26A434, Darwin arm64. No Hopper, Ghidra, IDA, device or live analysis service was used.

Target and artifact context

An empty ordinary directory and a synthetic Demo.app with CFBundleExecutable=Demo and a deliberately unsupported text file at Contents/MacOS/Demo. Format rejection keeps the reproduction before provider analysis; the marker is never executed.

Interface

CLI

Earliest failure stage

Request parsing or validation

Relevant output or evidence

Observed projected output from the reduced replay:

{"command":"inspect-managed-artifact","input":"","exit":1,"code":"target_unavailable","selected":"plain","constraint":"directory_requires_file"}
{"command":"inspect-managed-members","input":"","exit":1,"code":"target_unavailable","selected":"plain","constraint":"directory_requires_file"}
{"command":"list-architectures","input":"","exit":1,"code":"target_unavailable","selected":"plain","constraint":"directory_requires_file"}
{"command":"search-bytes","input":"","exit":1,"code":"target_unavailable","selected":"plain","constraint":"directory_requires_file"}
{"command":"address-to-file-offset","input":"","exit":1,"code":"target_unavailable","selected":"plain","constraint":"directory_requires_file"}
{"command":"inspect-native-instruction","input":"","exit":1,"code":"target_unavailable","selected":"plain","constraint":"directory_requires_file"}
{"command":"compare-managed-members","input":"","exit":1,"code":"target_unavailable","selected":"plain","constraint":"directory_requires_file"}
{"command":"inspect","input":"","exit":1,"code":"target_unavailable","selected":"Demo.app/Contents/MacOS/Demo","constraint":"file_format_rejected"}
{"command":"inspect","input":".","exit":1,"code":"target_unavailable","selected":"Demo.app/Contents/MacOS/Demo","constraint":"file_format_rejected"}
{"command":"inspect-artifact","input":"","exit":1,"code":"VALIDATION_ERROR","field":"path","issue":"too_small"}

The first seven responses also give directory-specific advice to select an executable or use JavaScript analysis. The two inspect responses show the same executable selection for empty input and explicit dot. Projection comes from complete CLI JSON responses; each stderr was empty in the initial replay.

Typed outcome or recovery expected

A nonzero input-validation error for the empty path before target access. Proposed restoration check: replay these cases, require empty-path validation for the eight affected invocations, and preserve the explicit-dot control's existing format error for the authored marker.

Analysis or suspected cause

Source-derived: bare string arguments admit the empty input in src/cli/coreBinaryCommands.ts:174-187, src/cli/managedCommands.ts:41-44,65-68,127-130, src/cli/utilityCommands.ts:193-213 and src/cli/coreNativeCommands.ts:35-39,130-145,253-275. Their paths reach the shared binary resolver; managed comparison calls it for both operands at src/application/managed/ManagedMemberComparisonService.ts:107-120. src/application/BinaryTargetResolver.ts:142-159 anchors the argument through src/filesystem/callerPath.ts:35-39 before access, realpath and bundle selection. The shared closure rule for this report is to reject these empty binary selections before that resolver runs.

Additional context

Excluded commands already reported elsewhere: inspect-keyed-archive in #7377; decompile-all in #7280; inspect-plist (including its auxiliary path), inspect-macho and inspect-signature in #7404; extract-artifact, inspect-asset-catalog, decode-interface-builder and trace-dylib-resolution in #7406; inspect-native-dispatch-metadata and inspect-apple-ips in #7421. This report covers none of those commands. Closed #6757 covered desktop inspection, while merged PR #2687 covered analyze and inspect-artifact. The eight different declarations above still reproduce on current main. Evidence-file import/export/comparison use a separate reader workflow and are outside this report.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7436 ## 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 An empty target reaches filesystem admission in `inspect`, `inspect-managed-artifact`, `inspect-managed-members`, `list-architectures`, `search-bytes`, `address-to-file-offset`, `inspect-native-instruction` and the left operand of `compare-managed-members`. Seven commands report `target_unavailable` for a plain working directory. From an app-bundle working directory, `inspect ""` instead selects `Contents/MacOS/Demo` and reports its unsupported format, exactly like explicit `inspect .`. All included calls exit 1; none rejects the empty selection before resolution. ## Reproduction Use Node 24.22.1 at the source commit below. From a fresh checkout run `npm ci && npm run build:cached`, then run this block from its root. The verifier rebuilt the supplied checkout with its unchanged lockfile. The script authors only a synthetic plist and an inert text file, isolates every product state directory below `$WORK`, and invokes the public dispatcher sequentially. Each affected invocation was reproduced once in the initial replay and once with this reduced script. ```sh WORK="$(mktemp -d)" node --input-type=module <<'JS' import { mkdirSync, realpathSync, writeFileSync } from 'node:fs'; import { join, relative, resolve } from 'node:path'; import { spawnSync } from 'node:child_process'; const cli = resolve('scripts/rea.mjs'); const root = realpathSync(process.env.WORK); const env = { ...process.env, REA_LOG_LEVEL: 'silent' }; for (const [key, dir] of Object.entries({ HOME: 'home', XDG_CONFIG_HOME: 'config', XDG_DATA_HOME: 'data', XDG_CACHE_HOME: 'cache', XDG_STATE_HOME: 'state', TMPDIR: 'temp' })) { env[key] = join(root, dir); mkdirSync(env[key], { recursive: true }); } const plain = join(root, 'plain'); const bundle = join(root, 'Demo.app'); mkdirSync(plain); mkdirSync(join(bundle, 'Contents/MacOS'), { recursive: true }); writeFileSync(join(bundle, 'Contents/Info.plist'), '<plist><dict><key>CFBundleExecutable</key><string>Demo</string></dict></plist>'); writeFileSync(join(bundle, 'Contents/MacOS/Demo'), 'not an executable image\n'); const cases = [ ['inspect-managed-artifact', '', plain], ['inspect-managed-members', '', plain], ['list-architectures', '', plain], ['search-bytes', '', plain, '00'], ['address-to-file-offset', '', plain, '0x1000'], ['inspect-native-instruction', '', plain, '0x1000'], ['compare-managed-members', '', plain, '.'], ['inspect', '', bundle], ['inspect', '.', bundle], ['inspect-artifact', '', plain], ]; for (const [command, input, cwd, ...extra] of cases) { const result = spawnSync(process.execPath, [cli, command, input, ...extra, '--json'], { cwd, env, encoding: 'utf8', timeout: 15000 }); const data = JSON.parse(result.stdout); console.log(JSON.stringify({ command, input, exit: result.status, code: data.code, selected: data.details?.path ? relative(root, data.details.path) : undefined, constraint: data.details?.constraint, field: data.fieldErrors?.[0]?.path, issue: data.fieldErrors?.[0]?.code })); } JS ``` ## Expected behavior Reject each empty path at request validation before resolving or reading cwd, identifying `path` or `leftPath`. Preserve explicit nonempty selection. `docs/cli.md:42-45` promises that local inspection rejects empty paths before resolution and that an empty string never selects the working directory. `docs/tool-design.md:339-343` applies the same rule to CLI translations. The observed `inspect-artifact` control already returns `VALIDATION_ERROR`, field `path`, issue `too_small`. ## Impact and scope P2: an empty shell variable selects an unintended directory or bundle executable and produces misleading target-repair advice instead of identifying the invalid argument. The observed effect is incorrect admission and diagnostics; no successful binary analysis, target mutation or execution is claimed. A caller can safely reject an empty variable before invocation. The executed scope is these eight binary entrypoints on macOS; only the left comparison operand was exercised. Other platforms, real providers, MCP and downstream consequences were not tested. No security boundary crossing was observed. ## REA version Exact `node scripts/rea.mjs --version` stdout: `6.8.0` followed by a newline. Built source commit: `a6a69ec63e0b9f2742d9b39534e2dfc02b6a0814`. ## Runtime and platform Node.js `v24.22.1`; macOS `27.0.1`, build `26A434`, Darwin `arm64`. No Hopper, Ghidra, IDA, device or live analysis service was used. ## Target and artifact context An empty ordinary directory and a synthetic Demo.app with CFBundleExecutable=Demo and a deliberately unsupported text file at Contents/MacOS/Demo. Format rejection keeps the reproduction before provider analysis; the marker is never executed. ## Interface CLI ## Earliest failure stage Request parsing or validation ## Relevant output or evidence Observed projected output from the reduced replay: ```jsonl {"command":"inspect-managed-artifact","input":"","exit":1,"code":"target_unavailable","selected":"plain","constraint":"directory_requires_file"} {"command":"inspect-managed-members","input":"","exit":1,"code":"target_unavailable","selected":"plain","constraint":"directory_requires_file"} {"command":"list-architectures","input":"","exit":1,"code":"target_unavailable","selected":"plain","constraint":"directory_requires_file"} {"command":"search-bytes","input":"","exit":1,"code":"target_unavailable","selected":"plain","constraint":"directory_requires_file"} {"command":"address-to-file-offset","input":"","exit":1,"code":"target_unavailable","selected":"plain","constraint":"directory_requires_file"} {"command":"inspect-native-instruction","input":"","exit":1,"code":"target_unavailable","selected":"plain","constraint":"directory_requires_file"} {"command":"compare-managed-members","input":"","exit":1,"code":"target_unavailable","selected":"plain","constraint":"directory_requires_file"} {"command":"inspect","input":"","exit":1,"code":"target_unavailable","selected":"Demo.app/Contents/MacOS/Demo","constraint":"file_format_rejected"} {"command":"inspect","input":".","exit":1,"code":"target_unavailable","selected":"Demo.app/Contents/MacOS/Demo","constraint":"file_format_rejected"} {"command":"inspect-artifact","input":"","exit":1,"code":"VALIDATION_ERROR","field":"path","issue":"too_small"} ``` The first seven responses also give directory-specific advice to select an executable or use JavaScript analysis. The two inspect responses show the same executable selection for empty input and explicit dot. Projection comes from complete CLI JSON responses; each stderr was empty in the initial replay. ## Typed outcome or recovery expected A nonzero input-validation error for the empty path before target access. Proposed restoration check: replay these cases, require empty-path validation for the eight affected invocations, and preserve the explicit-dot control's existing format error for the authored marker. ## Analysis or suspected cause Source-derived: bare string arguments admit the empty input in `src/cli/coreBinaryCommands.ts:174-187`, `src/cli/managedCommands.ts:41-44,65-68,127-130`, `src/cli/utilityCommands.ts:193-213` and `src/cli/coreNativeCommands.ts:35-39,130-145,253-275`. Their paths reach the shared binary resolver; managed comparison calls it for both operands at `src/application/managed/ManagedMemberComparisonService.ts:107-120`. `src/application/BinaryTargetResolver.ts:142-159` anchors the argument through `src/filesystem/callerPath.ts:35-39` before access, realpath and bundle selection. The shared closure rule for this report is to reject these empty binary selections before that resolver runs. ## Additional context Excluded commands already reported elsewhere: `inspect-keyed-archive` in [#7377](https://github.com/morluto/rea/issues/7377); `decompile-all` in [#7280](https://github.com/morluto/rea/issues/7280); `inspect-plist` (including its auxiliary path), `inspect-macho` and `inspect-signature` in [#7404](https://github.com/morluto/rea/issues/7404); `extract-artifact`, `inspect-asset-catalog`, `decode-interface-builder` and `trace-dylib-resolution` in [#7406](https://github.com/morluto/rea/issues/7406); `inspect-native-dispatch-metadata` and `inspect-apple-ips` in [#7421](https://github.com/morluto/rea/issues/7421). This report covers none of those commands. Closed [#6757](https://github.com/morluto/rea/issues/6757) covered desktop inspection, while merged [PR #2687](https://github.com/morluto/rea/pull/2687) covered analyze and inspect-artifact. The eight different declarations above still reproduce on current main. Evidence-file import/export/comparison use a separate reader workflow and are outside this report.
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#4443
No description provided.