[GH-ISSUE #5298] [Bug] inspect-macho and observe-native-calls name the same Mach-O function differently (_main vs main) #3230

Closed
opened 2026-10-31 00:38:07 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @DanielCvz1 on GitHub (Oct 19, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5298

Originally assigned to: @kaoru0822-kitauji on GitHub.

Before submitting

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

Area

Process capture or runtime observation

Observed behavior

observe-native-calls treats a breakpoint as unresolved only when location_count === 0. LLDB resolves an unscoped function name in every loaded image, so a breakpoint can resolve to locations that are all outside the requested executable. REA then reports unresolved_breakpoints: [], coverage.status: "complete" and no limitation, so calls from another image can be read as entries into the requested function.

The existing limitation already states the rule: breakpoints are unresolved when they "matched no code in any image loaded while observing". A match in AppKit counts as resolved for a different target.

Executed with the CLI against real LLDB (not a source-only finding). Request:

{"breakpoints": [
  {"kind": "function", "name": "main"},
  {"kind": "function", "name": "_main"},
  {"kind": "function", "name": "main", "module": "objetivo"}
]}
index request location_count locations in the target images
0 main 27 1 12 (AppKit, AVFCore, AVRouting, …)
1 _main 1 0 AppKit only
2 main, module: objetivo 1 1 objetivo
"coverage": {"status": "complete", "event_limit_reached": false, "resource_limit_reached": false, "unresolved_breakpoints": []}

No limitation mentions index 1. _main is the name as it appears in the Mach-O symbol table (nm), which is a natural name for a caller arriving from static analysis.

Reproduction

From a REA checkout at the revision below, after npm ci and npm run build:cached, with rea_root set to the checkout:

cat > objetivo.c <<'C'
#include <stdio.h>
int rea_add(int a, int b) { return a + b; }
int main(void) { printf("%d\n", rea_add(2, 3)); return 0; }
C
xcrun clang -arch arm64 -O0 -framework AppKit objetivo.c -o objetivo
cat > request.json <<'J'
{"breakpoints": [
  {"kind": "function", "name": "main"},
  {"kind": "function", "name": "_main"},
  {"kind": "function", "name": "main", "module": "objetivo"}
], "max_events": 8, "duration_ms": 10000}
J
node "$rea_root/scripts/rea.mjs" observe-native-calls "$PWD/objetivo" request.json --format json

Reproduced every time it was run: once on the published 6.5.0 with a different arm64 executable, and once on source revision 71ae02c.

Expected behavior

When every retained location of a resolved breakpoint is in a known image other than the target, the observation should not read as complete for that breakpoint: coverage should be partial, with a limitation naming the breakpoint, in the same style as unresolved breakpoints. Cases that cannot be decided (a truncated location list, a location without module_path, an unknown target identity) should stay undecided, since existing limitations already report them.

This expectation is derived from the existing coverage model; it is proposed, not documented.

Impact and scope

Evidence integrity: calls from another image can be attributed to the requested function while coverage reads complete. It affects unscoped names that also exist in loaded system images. Observed only on macOS arm64; x86_64 and other hosts were not tested. Severity beyond this is unknown.

REA version

Source revision 71ae02c (main, 2026-10-19, package version 6.7.0). Also observed on the published 6.5.0.

Runtime and platform

Node.js v26.11.1, macOS 26.5.2, arm64, LLDB lldb-2100.0.17.203.

Target and artifact context

Mach-O 64-bit arm64 executable built locally with clang and linked against AppKit. Not an app bundle.

Interface

CLI. The MCP tool uses the same provider projection, but it was not exercised separately.

Earliest failure stage

Transport or result projection. LLDB's readback already carries the correct module_path for each location; the incorrect conclusion appears where the run is projected into the result (nativeCallFacts in src/native/NativeCallObservation.ts).

Relevant output or evidence

A fix with a regression test is in a linked draft pull request. With it applied, the same run reports:

"coverage": {"status": "partial", "event_limit_reached": false, "resource_limit_reached": false, "unresolved_breakpoints": []}
Breakpoints 1 resolved only to code outside the target image; their calls in the target could not be observed.

Observed By Axon powered by Opus 5.5

Originally created by @DanielCvz1 on GitHub (Oct 19, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5298 Originally assigned to: @kaoru0822-kitauji on GitHub. ### 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 Process capture or runtime observation ### Observed behavior `observe-native-calls` treats a breakpoint as unresolved only when `location_count === 0`. LLDB resolves an unscoped function name in every loaded image, so a breakpoint can resolve to locations that are **all outside the requested executable**. REA then reports `unresolved_breakpoints: []`, `coverage.status: "complete"` and no limitation, so calls from another image can be read as entries into the requested function. The existing limitation already states the rule: breakpoints are unresolved when they *"matched no code in any image loaded while observing"*. A match in AppKit counts as resolved for a different target. Executed with the CLI against real LLDB (not a source-only finding). Request: ```json {"breakpoints": [ {"kind": "function", "name": "main"}, {"kind": "function", "name": "_main"}, {"kind": "function", "name": "main", "module": "objetivo"} ]} ``` | index | request | `location_count` | locations in the target | images | |---|---|---|---|---| | 0 | `main` | 27 | 1 | 12 (AppKit, AVFCore, AVRouting, …) | | 1 | `_main` | 1 | **0** | **AppKit only** | | 2 | `main`, `module: objetivo` | 1 | 1 | `objetivo` | ```json "coverage": {"status": "complete", "event_limit_reached": false, "resource_limit_reached": false, "unresolved_breakpoints": []} ``` No limitation mentions index 1. `_main` is the name as it appears in the Mach-O symbol table (`nm`), which is a natural name for a caller arriving from static analysis. ### Reproduction From a REA checkout at the revision below, after `npm ci` and `npm run build:cached`, with `rea_root` set to the checkout: ```sh cat > objetivo.c <<'C' #include <stdio.h> int rea_add(int a, int b) { return a + b; } int main(void) { printf("%d\n", rea_add(2, 3)); return 0; } C xcrun clang -arch arm64 -O0 -framework AppKit objetivo.c -o objetivo cat > request.json <<'J' {"breakpoints": [ {"kind": "function", "name": "main"}, {"kind": "function", "name": "_main"}, {"kind": "function", "name": "main", "module": "objetivo"} ], "max_events": 8, "duration_ms": 10000} J node "$rea_root/scripts/rea.mjs" observe-native-calls "$PWD/objetivo" request.json --format json ``` Reproduced every time it was run: once on the published 6.5.0 with a different arm64 executable, and once on source revision `71ae02c`. ### Expected behavior When every retained location of a resolved breakpoint is in a known image other than the target, the observation should not read as complete for that breakpoint: coverage should be `partial`, with a limitation naming the breakpoint, in the same style as unresolved breakpoints. Cases that cannot be decided (a truncated location list, a location without `module_path`, an unknown target identity) should stay undecided, since existing limitations already report them. This expectation is derived from the existing coverage model; it is proposed, not documented. ### Impact and scope Evidence integrity: calls from another image can be attributed to the requested function while coverage reads `complete`. It affects unscoped names that also exist in loaded system images. Observed only on macOS arm64; x86_64 and other hosts were not tested. Severity beyond this is unknown. ### REA version Source revision `71ae02c` (`main`, 2026-10-19, package version 6.7.0). Also observed on the published 6.5.0. ### Runtime and platform Node.js v26.11.1, macOS 26.5.2, arm64, LLDB `lldb-2100.0.17.203`. ### Target and artifact context Mach-O 64-bit arm64 executable built locally with clang and linked against AppKit. Not an app bundle. ### Interface CLI. The MCP tool uses the same provider projection, but it was not exercised separately. ### Earliest failure stage Transport or result projection. LLDB's readback already carries the correct `module_path` for each location; the incorrect conclusion appears where the run is projected into the result (`nativeCallFacts` in `src/native/NativeCallObservation.ts`). ### Relevant output or evidence A fix with a regression test is in a linked draft pull request. With it applied, the same run reports: ```json "coverage": {"status": "partial", "event_limit_reached": false, "resource_limit_reached": false, "unresolved_breakpoints": []} ``` ``` Breakpoints 1 resolved only to code outside the target image; their calls in the target could not be observed. ``` --- Observed By Axon powered by Opus 5.5
Author
Owner

@DanielCvz1 commented on GitHub (Oct 19, 2026):

Correction after review of #5299: the coverage framing in this report is wrong. Unscoped function breakpoints are documented to match any image, and the main documented use (open in libsystem_kernel.dylib, NSURLSession methods) resolves entirely outside the executable by design. Marking those as missed coverage would be a false report, so I withdrew the PR.

What I actually hit is a naming mismatch between two REA tools on the same Mach-O (arm64, source 71ae02c, macOS 26.5.2, LLDB lldb-2100.0.17.203):

  • rea inspect-macho <executable> lists the executable's entry function as _main, the Mach-O symbol-table name.
  • rea observe-native-calls resolves that same function as main: the target location's symbol is main.
  • Passing the name from inspect-macho (_main) to observe-native-calls resolves one location in AppKit and none in the executable, and the result reads as a normally resolved breakpoint.

So a caller who takes a symbol name from REA's own static output gets a different function at runtime, without any signal. Possible directions, for maintainers to choose:

  1. Document the Darwin leading-underscore convention next to the function breakpoint input.
  2. Add a limitation when a requested name with a leading underscore resolves outside the target while the underscore-stripped name exists in the target.
  3. Accept symbol-table names and translate them.

Not verified: other platforms, Swift or C++ mangled names, and whether other REA tools emit stripped names.


Observed By Axon powered by Opus 5.5

<!-- gh-comment-id:6277184627 --> @DanielCvz1 commented on GitHub (Oct 19, 2026): **Correction after review of #5299:** the coverage framing in this report is wrong. Unscoped function breakpoints are documented to match any image, and the main documented use (`open` in `libsystem_kernel.dylib`, `NSURLSession` methods) resolves entirely outside the executable by design. Marking those as missed coverage would be a false report, so I withdrew the PR. What I actually hit is a naming mismatch between two REA tools on the same Mach-O (arm64, source `71ae02c`, macOS 26.5.2, LLDB `lldb-2100.0.17.203`): - `rea inspect-macho <executable>` lists the executable's entry function as `_main`, the Mach-O symbol-table name. - `rea observe-native-calls` resolves that same function as `main`: the target location's symbol is `main`. - Passing the name from `inspect-macho` (`_main`) to `observe-native-calls` resolves one location in AppKit and none in the executable, and the result reads as a normally resolved breakpoint. So a caller who takes a symbol name from REA's own static output gets a different function at runtime, without any signal. Possible directions, for maintainers to choose: 1. Document the Darwin leading-underscore convention next to the function breakpoint input. 2. Add a limitation when a requested name with a leading underscore resolves outside the target while the underscore-stripped name exists in the target. 3. Accept symbol-table names and translate them. Not verified: other platforms, Swift or C++ mangled names, and whether other REA tools emit stripped names. --- Observed By Axon powered by Opus 5.5
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#3230
No description provided.