[GH-ISSUE #7448] [Bug] observe-native-calls launches the working app when its target is empty #4451

Open
opened 2026-10-31 00:49:54 +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/7448

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

From inside a synthetic macOS application bundle, rea observe-native-calls "" INPUT --json exited 0 and returned successful observe_native_calls Evidence for that bundle's main executable. Both parameters.input_path and caller_input_path remained empty. The result reported a positive process PID, outcome: "duration-elapsed" and terminated: true, establishing that LLDB launched a target despite the empty target argument. Explicit . selected the same executable successfully.

From an ordinary directory, the same empty argument instead returned target_unavailable for that working directory. Neither case rejected the empty target before resolution. The synthetic executable contained only int main(void) { return 0; }; the observation returned no call events, so this report does not claim that execution reached main or produced application side effects.

Reproduction

Use a clean checkout at the source revision below with its locked dependencies and compiled runtime available, Node.js 24.22.1 selected, and macOS command-line tools supplying cc and LLDB. Run this script from the checkout root. It authors the whole fixture, isolates product state and removes its temporary files afterward. No real application or external service is required.

node --input-type=module <<'JS'
import { mkdtempSync, realpathSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join, resolve, relative } from 'node:path';
import { tmpdir } from 'node:os';
import { spawnSync } from 'node:child_process';
const cli = resolve('scripts/rea.mjs');
const root = realpathSync(mkdtempSync(join(tmpdir(), 'rea-empty-target-')));
const bundle = join(root, 'Example.app');
const plain = join(root, 'ordinary');
const env = { ...process.env };
try {
  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',
    XDG_RUNTIME_DIR: 'runtime', TMPDIR: 'temp',
  })) {
    env[key] = join(root, dir);
    mkdirSync(env[key], { recursive: true });
  }
  mkdirSync(join(bundle, 'Contents/MacOS'), { recursive: true });
  mkdirSync(plain);
  writeFileSync(join(root, 'main.c'), 'int main(void) { return 0; }\n');
  writeFileSync(join(bundle, 'Contents/Info.plist'),
    '<?xml version="1.0"?><plist version="1.0"><dict>' +
    '<key>CFBundleExecutable</key><string>Example</string>' +
    '<key>CFBundleIdentifier</key><string>example.fixture</string>' +
    '<key>CFBundlePackageType</key><string>APPL</string></dict></plist>');
  const cc = spawnSync('cc', ['-g', '-o',
    join(bundle, 'Contents/MacOS/Example'), join(root, 'main.c')],
    { env, encoding: 'utf8', timeout: 20000 });
  if (cc.status !== 0) throw new Error(cc.stderr || String(cc.error));
  const input = join(root, 'input.json');
  writeFileSync(input, JSON.stringify({
    breakpoints: [{ kind: 'function', name: 'main' }], duration_ms: 100,
  }));
  const cases = [
    ['empty-app', bundle, ['observe-native-calls', '', input, '--json']],
    ['dot-app', bundle, ['observe-native-calls', '.', input, '--json']],
    ['empty-ordinary', plain, ['observe-native-calls', '', input, '--json']],
    ['empty-control', bundle, ['inspect-model-artifact', '', '--json']],
  ];
  for (const [label, cwd, args] of cases) {
    const result = spawnSync(process.execPath, [cli, ...args], {
      cwd, env, encoding: 'utf8', timeout: 30000, maxBuffer: 8388608,
    });
    if (result.error) throw result.error;
    const data = JSON.parse(result.stdout);
    const processResult = data.normalized_result?.process;
    console.log(JSON.stringify({
      label, exit: result.status, operation: data.operation,
      input_path: data.parameters?.input_path,
      selected: data.subject?.local_path
        ? relative(root, data.subject.local_path) : undefined,
      process: processResult ? {
        has_pid: processResult.pid > 0, outcome: processResult.outcome,
        terminated: processResult.terminated,
      } : undefined,
      code: data.code,
      rejected_path: data.details?.path
        ? relative(root, data.details.path) : undefined,
      field: data.fieldErrors?.[0]?.path,
      issue: data.fieldErrors?.[0]?.code,
    }));
    if (result.stderr) console.error(result.stderr);
  }
} finally {
  rmSync(root, { recursive: true, force: true });
}
JS

The empty-app failure reproduced twice in two independent runs, each with a freshly compiled inert fixture and successful explicit-dot and empty-parser controls. The ordinary-directory failure also reproduced in both runs. The second run executed this exact script; its four projected JSON lines match the output below.

Expected behavior

Reject the empty target with a nonzero input-validation error identifying path, before resolving cwd or launching a target. Preserve explicit . for intentional selection. REA documents that an empty string never selects the working directory and that CLI translations reject empty selections before normalization.

Expected source: docs/cli.md:42 and docs/tool-design.md:339.

Expected source: docs/native-investigation.md:910 documents this command's app-or-Mach-O target and owned LLDB launch. The general CLI admission rule therefore applies before that launch.

Impact and scope

P2: a script passing an empty target variable can receive successful observation Evidence for an unintended executable and cause an unintended LLDB launch. The affected input is an empty positional target; successful wrong-target observation was established with a readable macOS app bundle as cwd. From an ordinary directory the observed effect was a misleading later target error. No severe harm, data loss or security boundary crossing was demonstrated.

A practical caller workaround is to reject empty target variables before invocation and supply a nonempty intended path. The explicit-dot control verified intentional working-directory selection. Other operating systems, MCP and other commands were not exercised for this report.

REA version

Exact node scripts/rea.mjs --version stdout: 6.8.0 followed by a newline. Source commit: f8f7ac96de8a66a4ab9a23d87e04779bfcd11045.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; arm64. Native observation used the host's LLDB; no Hopper, Ghidra, IDA or GUI session was used.

Target and artifact context

Synthetic Example.app containing an XML Info.plist and an inert arm64 Mach-O compiled from the inline C program. The command launched only that synthetic executable. HOME, XDG directories and TMPDIR were isolated below the disposable fixture root.

Interface

CLI

Earliest failure stage

Request parsing or validation

Relevant output or evidence

Observed compact results, with selected paths relative to the fixture root:

{"label":"empty-app","exit":0,"operation":"observe_native_calls","input_path":"","selected":"Example.app/Contents/MacOS/Example","process":{"has_pid":true,"outcome":"duration-elapsed","terminated":true}}
{"label":"dot-app","exit":0,"operation":"observe_native_calls","input_path":".","selected":"Example.app/Contents/MacOS/Example","process":{"has_pid":true,"outcome":"duration-elapsed","terminated":true}}
{"label":"empty-ordinary","exit":1,"code":"target_unavailable","rejected_path":"ordinary"}
{"label":"empty-control","exit":1,"code":"VALIDATION_ERROR","field":"path","issue":"too_small"}

The complete empty-target observation also retained caller_input_path: "" and named the same executable in normalized_result.target.path; its events array was empty. The explicit-dot observation selected the same digest. The ordinary-directory error had constraint directory_requires_file. A separate omitted-arguments call, observe-native-calls --json, returned VALIDATION_ERROR identifying both missing positional arguments.

Failure source: src/cli/nativeCallCommands.ts:27 accepts an unconstrained target string; its handler at line 50 forwards that original argument after validating observation JSON.

Failure source: src/application/DirectAnalysis.ts:455, src/application/binary/BinarySessionOpen.ts:90 and src/application/BinaryTargetResolver.ts:144 form the target-open path. The resolver anchors the selection before bundle executable admission at line 155.

Failure source: src/filesystem/callerPath.ts:35 prefixes relative selections with cwd, so the admitted empty string becomes the working directory. src/native/NativeCallObservation.ts:71 passes the resolved executable to the tracer after target and input checks.

Typed outcome or recovery expected

A nonzero empty-path validation error before target admission, with no launch or observation Evidence. The observed sibling control provides the existing parser shape: VALIDATION_ERROR with field path and issue too_small. Restoration should reject the empty selection while preserving explicit-dot observation.

Analysis or suspected cause

Source-derived: this command's separate argument declaration omits the existing nonempty local-path admission constraint. Its earlier guards validate the observation JSON and the resolved Mach-O's identity; those guards cannot recover the original empty selection after normalization. This diagnosis is corroborated by the public CLI results rather than inferred from test-only reachability.

Additional context

Related open reports cover other command declarations: #7280, #7377, #7404, #7406, #7421 , #7436 and #7443. None names observe-native-calls. Closed #6757 concerns desktop inspection. Open/closed issue searches and the latest-100 issue list found no existing report for this command's empty target.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7448 ## 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 From inside a synthetic macOS application bundle, `rea observe-native-calls "" INPUT --json` exited 0 and returned successful `observe_native_calls` Evidence for that bundle's main executable. Both `parameters.input_path` and `caller_input_path` remained empty. The result reported a positive process PID, `outcome: "duration-elapsed"` and `terminated: true`, establishing that LLDB launched a target despite the empty target argument. Explicit `.` selected the same executable successfully. From an ordinary directory, the same empty argument instead returned `target_unavailable` for that working directory. Neither case rejected the empty target before resolution. The synthetic executable contained only `int main(void) { return 0; }`; the observation returned no call events, so this report does not claim that execution reached main or produced application side effects. ## Reproduction Use a clean checkout at the source revision below with its locked dependencies and compiled runtime available, Node.js 24.22.1 selected, and macOS command-line tools supplying `cc` and LLDB. Run this script from the checkout root. It authors the whole fixture, isolates product state and removes its temporary files afterward. No real application or external service is required. ```sh node --input-type=module <<'JS' import { mkdtempSync, realpathSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { join, resolve, relative } from 'node:path'; import { tmpdir } from 'node:os'; import { spawnSync } from 'node:child_process'; const cli = resolve('scripts/rea.mjs'); const root = realpathSync(mkdtempSync(join(tmpdir(), 'rea-empty-target-'))); const bundle = join(root, 'Example.app'); const plain = join(root, 'ordinary'); const env = { ...process.env }; try { 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', XDG_RUNTIME_DIR: 'runtime', TMPDIR: 'temp', })) { env[key] = join(root, dir); mkdirSync(env[key], { recursive: true }); } mkdirSync(join(bundle, 'Contents/MacOS'), { recursive: true }); mkdirSync(plain); writeFileSync(join(root, 'main.c'), 'int main(void) { return 0; }\n'); writeFileSync(join(bundle, 'Contents/Info.plist'), '<?xml version="1.0"?><plist version="1.0"><dict>' + '<key>CFBundleExecutable</key><string>Example</string>' + '<key>CFBundleIdentifier</key><string>example.fixture</string>' + '<key>CFBundlePackageType</key><string>APPL</string></dict></plist>'); const cc = spawnSync('cc', ['-g', '-o', join(bundle, 'Contents/MacOS/Example'), join(root, 'main.c')], { env, encoding: 'utf8', timeout: 20000 }); if (cc.status !== 0) throw new Error(cc.stderr || String(cc.error)); const input = join(root, 'input.json'); writeFileSync(input, JSON.stringify({ breakpoints: [{ kind: 'function', name: 'main' }], duration_ms: 100, })); const cases = [ ['empty-app', bundle, ['observe-native-calls', '', input, '--json']], ['dot-app', bundle, ['observe-native-calls', '.', input, '--json']], ['empty-ordinary', plain, ['observe-native-calls', '', input, '--json']], ['empty-control', bundle, ['inspect-model-artifact', '', '--json']], ]; for (const [label, cwd, args] of cases) { const result = spawnSync(process.execPath, [cli, ...args], { cwd, env, encoding: 'utf8', timeout: 30000, maxBuffer: 8388608, }); if (result.error) throw result.error; const data = JSON.parse(result.stdout); const processResult = data.normalized_result?.process; console.log(JSON.stringify({ label, exit: result.status, operation: data.operation, input_path: data.parameters?.input_path, selected: data.subject?.local_path ? relative(root, data.subject.local_path) : undefined, process: processResult ? { has_pid: processResult.pid > 0, outcome: processResult.outcome, terminated: processResult.terminated, } : undefined, code: data.code, rejected_path: data.details?.path ? relative(root, data.details.path) : undefined, field: data.fieldErrors?.[0]?.path, issue: data.fieldErrors?.[0]?.code, })); if (result.stderr) console.error(result.stderr); } } finally { rmSync(root, { recursive: true, force: true }); } JS ``` The empty-app failure reproduced twice in two independent runs, each with a freshly compiled inert fixture and successful explicit-dot and empty-parser controls. The ordinary-directory failure also reproduced in both runs. The second run executed this exact script; its four projected JSON lines match the output below. ## Expected behavior Reject the empty target with a nonzero input-validation error identifying `path`, before resolving cwd or launching a target. Preserve explicit `.` for intentional selection. REA documents that an empty string never selects the working directory and that CLI translations reject empty selections before normalization. Expected source: `docs/cli.md:42` and `docs/tool-design.md:339`. Expected source: `docs/native-investigation.md:910` documents this command's app-or-Mach-O target and owned LLDB launch. The general CLI admission rule therefore applies before that launch. ## Impact and scope P2: a script passing an empty target variable can receive successful observation Evidence for an unintended executable and cause an unintended LLDB launch. The affected input is an empty positional target; successful wrong-target observation was established with a readable macOS app bundle as cwd. From an ordinary directory the observed effect was a misleading later target error. No severe harm, data loss or security boundary crossing was demonstrated. A practical caller workaround is to reject empty target variables before invocation and supply a nonempty intended path. The explicit-dot control verified intentional working-directory selection. Other operating systems, MCP and other commands were not exercised for this report. ## REA version Exact `node scripts/rea.mjs --version` stdout: `6.8.0` followed by a newline. Source commit: `f8f7ac96de8a66a4ab9a23d87e04779bfcd11045`. ## Runtime and platform Node.js `v24.22.1`; macOS `27.0.1`; `arm64`. Native observation used the host's LLDB; no Hopper, Ghidra, IDA or GUI session was used. ## Target and artifact context Synthetic `Example.app` containing an XML Info.plist and an inert arm64 Mach-O compiled from the inline C program. The command launched only that synthetic executable. HOME, XDG directories and TMPDIR were isolated below the disposable fixture root. ## Interface CLI ## Earliest failure stage Request parsing or validation ## Relevant output or evidence Observed compact results, with selected paths relative to the fixture root: ```jsonl {"label":"empty-app","exit":0,"operation":"observe_native_calls","input_path":"","selected":"Example.app/Contents/MacOS/Example","process":{"has_pid":true,"outcome":"duration-elapsed","terminated":true}} {"label":"dot-app","exit":0,"operation":"observe_native_calls","input_path":".","selected":"Example.app/Contents/MacOS/Example","process":{"has_pid":true,"outcome":"duration-elapsed","terminated":true}} {"label":"empty-ordinary","exit":1,"code":"target_unavailable","rejected_path":"ordinary"} {"label":"empty-control","exit":1,"code":"VALIDATION_ERROR","field":"path","issue":"too_small"} ``` The complete empty-target observation also retained `caller_input_path: ""` and named the same executable in `normalized_result.target.path`; its events array was empty. The explicit-dot observation selected the same digest. The ordinary-directory error had constraint `directory_requires_file`. A separate omitted-arguments call, `observe-native-calls --json`, returned `VALIDATION_ERROR` identifying both missing positional arguments. Failure source: `src/cli/nativeCallCommands.ts:27` accepts an unconstrained target string; its handler at line 50 forwards that original argument after validating observation JSON. Failure source: `src/application/DirectAnalysis.ts:455`, `src/application/binary/BinarySessionOpen.ts:90` and `src/application/BinaryTargetResolver.ts:144` form the target-open path. The resolver anchors the selection before bundle executable admission at line 155. Failure source: `src/filesystem/callerPath.ts:35` prefixes relative selections with cwd, so the admitted empty string becomes the working directory. `src/native/NativeCallObservation.ts:71` passes the resolved executable to the tracer after target and input checks. ## Typed outcome or recovery expected A nonzero empty-path validation error before target admission, with no launch or observation Evidence. The observed sibling control provides the existing parser shape: `VALIDATION_ERROR` with field `path` and issue `too_small`. Restoration should reject the empty selection while preserving explicit-dot observation. ## Analysis or suspected cause Source-derived: this command's separate argument declaration omits the existing nonempty local-path admission constraint. Its earlier guards validate the observation JSON and the resolved Mach-O's identity; those guards cannot recover the original empty selection after normalization. This diagnosis is corroborated by the public CLI results rather than inferred from test-only reachability. ## Additional context Related open reports cover other command declarations: [#7280](https://github.com/morluto/rea/issues/7280), [#7377](https://github.com/morluto/rea/issues/7377), [#7404](https://github.com/morluto/rea/issues/7404), [#7406](https://github.com/morluto/rea/issues/7406), [#7421](https://github.com/morluto/rea/issues/7421) , [#7436](https://github.com/morluto/rea/issues/7436) and [#7443](https://github.com/morluto/rea/issues/7443). None names `observe-native-calls`. Closed [#6757](https://github.com/morluto/rea/issues/6757) concerns desktop inspection. Open/closed issue searches and the latest-100 issue list found no existing report for this command's empty target.
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#4451
No description provided.