[PR #964] [MERGED] feat(native): observe function and Objective-C method calls under LLDB #2538

Closed
opened 2026-10-17 21:07:18 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/964
Author: @kaoru0822-kitauji
Created: 10/7/2026
Status: ✅ Merged
Merged: 10/8/2026
Merged by: @kaoru0822-kitauji

Base: main ← Head: feat/native-call-observation


📝 Commits (5)

  • 9837203 feat(native): observe function and Objective-C method calls under LLDB
  • a0f5e33 Merge remote-tracking branch 'origin/main' into feat/native-call-observation
  • a11c6bd Merge origin/main into feat/native-call-observation
  • 90b25f2 Merge origin/main into feat/native-call-observation
  • 2a7dcb7 chore: merge main into feat/native-call-observation

📊 Changes

29 files changed (+1968 additions, -24 deletions)

View changed files

📝 README_ar.md (+1 -1)
📝 README_ja.md (+1 -1)
📝 README_ko.md (+1 -1)
📝 README_zh.md (+1 -1)
➕ bridge/native/rea_lldb_tracer.py (+303 -0)
📝 docs/native-investigation.md (+41 -0)
📝 docs/product-catalog.json (+12 -9)
📝 docs/testing.md (+11 -0)
📝 docs/verification/managed-conformance-manifest.json (+2 -2)
📝 package.json (+2 -0)
➕ scripts/verify-native-calls.mjs (+240 -0)
📝 skills/reverse-engineer-anything/SKILL.md (+3 -3)
📝 skills/reverse-engineer-anything/references/native-and-artifacts.md (+6 -0)
➕ src/cli/nativeCallCommands.ts (+57 -0)
📝 src/cli/utilityCommands.ts (+2 -0)
📝 src/cliCommandNames.ts (+1 -0)
📝 src/contracts/native/nativeToolContracts.ts (+19 -0)
📝 src/contracts/toolEffects.ts (+9 -0)
📝 src/contracts/toolOutputSchemaGroups.ts (+2 -0)
➕ src/domain/native/nativeCallObservation.ts (+198 -0)

...and 9 more files

📄 Description

Summary

This adds observe_native_calls, a bounded runtime observe tool for the LLDB part of #385. It answers the question "which of these functions or Objective-C methods does this Mach-O actually call in one run, and with what?"

  • REA launches the active Mach-O as a new, owned process under lldb --batch, using the Python bridge bridge/native/rea_lldb_tracer.py. It stops at caller-selected entries, records them and continues automatically:

    • functions by exact symbol name, optionally limited to one image;
    • Objective-C methods by selector, optionally limited to one class and to instance or class methods.
  • Recorded per entry (observed):

    • thread, image, symbol, load and file address;
    • raw integer argument registers;
    • optional caller frames;
    • for methods, the selector read from _cmd and the receiver's dynamic class.

    The receiver class comes from LLDB's Objective-C runtime reader with eDynamicDontRunTarget. Breakpoints are placed at the symbol itself (target.skip-prologue false), so the registers hold the ABI arguments.

  • Authority:

    • no expression evaluation;
    • no target code runs on REA's behalf;
    • no attachment to a process REA did not launch;
    • no approval flags, because selecting the operation is the intent.

    The effects are declared: it launches a process, writes temporary files, and the target may show UI or use the network. The result is never served from the snapshot cache.

  • Lifecycle:

    • The run ends on process exit, max_events, or duration_ms. The last two are resource-safety bounds, and each is explained in limitations.
    • REA kills the process, confirms it is gone, and kills a survivor only when it is still the traced executable.
    • Cancellation and a hard deadline kill LLDB's process group; debugserver then takes the target with it.
  • Results:

    • exit status and description;
    • stdout and stderr, with 1 MiB kept per stream and the remainder counted;
    • signal and exception stops;
    • every breakpoint's resolved locations, with location_count;
    • partial coverage when the event limit is reached or a request matched no loaded code;
    • limitations: entry only, no floating-point or stack arguments, inlined and objc_direct calls are not observed, ASLR is disabled by LLDB, and the duration is checked about once a second.
  • Failures keep their reason:

    • LLDB missing → capability_unavailable;
    • a target LLDB may not debug → capability_unavailable with reason debugger-attach-denied: … and remediation (Developer Tools access, or get-task-allow for hardened-runtime targets; see inspect_signature's debugger-attach facet);
    • a changed target digest → evidence integrity error;
    • malformed input → invalid_request.

Contract

New native tool: observe_native_calls. The native family goes from 7 to 8 tools; READMEs and the generated catalog are updated.

{
  "name": "observe_native_calls",
  "arguments": {
    "breakpoints": [
      { "kind": "objc-method", "class_name": "ReaCallGreeter", "selector": "greet:times:", "method_type": "instance" },
      { "kind": "function", "name": "rea_call_add" }
    ],
    "environment": { "REA_CALLS_MARKER": "observed" },
    "argument_registers": 4,
    "backtrace_frames": 1
  }
}

The result excerpt below is from the conformance fixture. Paths are trimmed, and only one event of each kind is shown.

{
  "process": { "outcome": "exited", "exit_status": 0, "terminated": true,
               "stdout": { "text": "world x0 40\nworld x1 41\nworld x2 42\nmarker observed\n", "truncated": false } },
  "events": [
    { "symbol": "-[ReaCallGreeter greet:times:]", "file_address": "0x100000928", "module": "ReaCalls",
      "receiver_class": "ReaCallGreeter", "selector": "greet:times:",
      "registers": [{ "name": "x0", "value": "0x…" }, { "name": "x1", "value": "0x…" }, { "name": "x2", "value": "0x…" }, { "name": "x3", "value": "0x0" }],
      "backtrace": [{ "symbol": "main", "module": "ReaCalls" }] },
    { "symbol": "rea_call_add", "registers": [{ "name": "x0", "value": "0x0" }, { "name": "x1", "value": "0x28" }] }
  ],
  "coverage": { "status": "complete", "event_limit_reached": false, "unresolved_breakpoints": [] }
}

CLI: rea observe-native-calls <app-or-mach-o> <input-json>. The input is the same object as the MCP arguments, inline or as a JSON file path.

Out of scope (rest of #385)

  • Frida;
  • return-value capture;
  • unified-log capture;
  • attaching to processes REA did not launch.

Verification

  • Real lane: npm run verify:native-calls passed on macOS 26 arm64 (lldb-2100.0.17.203). It compiles tests/conformance/native/calls.m and runs the tool through both the packaged CLI and stdio MCP. It checks:
    • the receiver class, selector and argument registers of all six entries;
    • breakpoint file addresses equal to nm's symbol addresses;
    • captured stdout and an environment override;
    • CLI and MCP parity;
    • the event-limit and duration-elapsed outcomes, with the process confirmed gone;
    • that a hardened-runtime copy is refused with debugger-attach-denied, while the same copy signed with get-task-allow is traced.
  • Boundary tests:
    • tests/boundary/providers/native/nativeCallObservation.test.ts (fake tracer) covers projection, partial coverage, input rejection before any launch, changed and non-Mach-O targets, passthrough of tracer failures, and cancellation.
    • tests/boundary/mcp/nativeCallObservationMcp.test.ts covers MCP routing and input-schema rejection; it runs on Linux too.
  • npm run check:changed: 295 files and 2,987 tests passed.
  • npm run docs:generate && npm run docs:check passed.
  • The contract, catalog and schema guards passed: contractPresentation, cliToolCoverage, productCatalog, toolSchemaValidity and conformancePackage.
  • npm pack --dry-run includes bridge/native/rea_lldb_tracer.py.

No Hopper, Ghidra or IDA code is touched, so I didn't run any real provider verification.

🤖 Generated with Claude Code


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/964 **Author:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Created:** 10/7/2026 **Status:** ✅ Merged **Merged:** 10/8/2026 **Merged by:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Base:** `main` ← **Head:** `feat/native-call-observation` --- ### 📝 Commits (5) - [`9837203`](https://github.com/morluto/rea/commit/9837203234348ffbfaa94ff6b9b10c87ebc546d1) feat(native): observe function and Objective-C method calls under LLDB - [`a0f5e33`](https://github.com/morluto/rea/commit/a0f5e33bd8c93e22db7b0fa833698f710671e404) Merge remote-tracking branch 'origin/main' into feat/native-call-observation - [`a11c6bd`](https://github.com/morluto/rea/commit/a11c6bd5e42094d2868d6c817cf38567ded7e1db) Merge origin/main into feat/native-call-observation - [`90b25f2`](https://github.com/morluto/rea/commit/90b25f2e306d663f83ecbde714eb3c7cfd560768) Merge origin/main into feat/native-call-observation - [`2a7dcb7`](https://github.com/morluto/rea/commit/2a7dcb77c14f17d10c491cb9c37a1c22a1af696c) chore: merge main into feat/native-call-observation ### 📊 Changes **29 files changed** (+1968 additions, -24 deletions) <details> <summary>View changed files</summary> 📝 `README_ar.md` (+1 -1) 📝 `README_ja.md` (+1 -1) 📝 `README_ko.md` (+1 -1) 📝 `README_zh.md` (+1 -1) ➕ `bridge/native/rea_lldb_tracer.py` (+303 -0) 📝 `docs/native-investigation.md` (+41 -0) 📝 `docs/product-catalog.json` (+12 -9) 📝 `docs/testing.md` (+11 -0) 📝 `docs/verification/managed-conformance-manifest.json` (+2 -2) 📝 `package.json` (+2 -0) ➕ `scripts/verify-native-calls.mjs` (+240 -0) 📝 `skills/reverse-engineer-anything/SKILL.md` (+3 -3) 📝 `skills/reverse-engineer-anything/references/native-and-artifacts.md` (+6 -0) ➕ `src/cli/nativeCallCommands.ts` (+57 -0) 📝 `src/cli/utilityCommands.ts` (+2 -0) 📝 `src/cliCommandNames.ts` (+1 -0) 📝 `src/contracts/native/nativeToolContracts.ts` (+19 -0) 📝 `src/contracts/toolEffects.ts` (+9 -0) 📝 `src/contracts/toolOutputSchemaGroups.ts` (+2 -0) ➕ `src/domain/native/nativeCallObservation.ts` (+198 -0) _...and 9 more files_ </details> ### 📄 Description ## Summary This adds `observe_native_calls`, a bounded runtime **observe** tool for the LLDB part of #385. It answers the question "which of these functions or Objective-C methods does this Mach-O actually call in one run, and with what?" - REA launches the **active Mach-O** as a new, owned process under `lldb --batch`, using the Python bridge `bridge/native/rea_lldb_tracer.py`. It stops at caller-selected entries, records them and continues automatically: - **functions** by exact symbol name, optionally limited to one image; - **Objective-C methods** by selector, optionally limited to one class and to instance or class methods. - **Recorded per entry (observed):** - thread, image, symbol, load and file address; - raw integer argument registers; - optional caller frames; - for methods, the selector read from `_cmd` and the receiver's dynamic class. The receiver class comes from LLDB's Objective-C runtime reader with `eDynamicDontRunTarget`. Breakpoints are placed at the symbol itself (`target.skip-prologue false`), so the registers hold the ABI arguments. - **Authority:** - no expression evaluation; - no target code runs on REA's behalf; - no attachment to a process REA did not launch; - no approval flags, because selecting the operation is the intent. The effects are declared: it launches a process, writes temporary files, and the target may show UI or use the network. The result is never served from the snapshot cache. - **Lifecycle:** - The run ends on process exit, `max_events`, or `duration_ms`. The last two are resource-safety bounds, and each is explained in limitations. - REA kills the process, confirms it is gone, and kills a survivor only when it is still the traced executable. - Cancellation and a hard deadline kill LLDB's process group; debugserver then takes the target with it. - **Results:** - exit status and description; - stdout and stderr, with 1 MiB kept per stream and the remainder counted; - signal and exception stops; - every breakpoint's resolved locations, with `location_count`; - partial coverage when the event limit is reached or a request matched no loaded code; - limitations: entry only, no floating-point or stack arguments, inlined and `objc_direct` calls are not observed, ASLR is disabled by LLDB, and the duration is checked about once a second. - **Failures keep their reason:** - LLDB missing → `capability_unavailable`; - a target LLDB may not debug → `capability_unavailable` with reason `debugger-attach-denied: …` and remediation (Developer Tools access, or `get-task-allow` for hardened-runtime targets; see `inspect_signature`'s debugger-attach facet); - a changed target digest → evidence integrity error; - malformed input → `invalid_request`. ### Contract New native tool: `observe_native_calls`. The native family goes from 7 to 8 tools; READMEs and the generated catalog are updated. ```json { "name": "observe_native_calls", "arguments": { "breakpoints": [ { "kind": "objc-method", "class_name": "ReaCallGreeter", "selector": "greet:times:", "method_type": "instance" }, { "kind": "function", "name": "rea_call_add" } ], "environment": { "REA_CALLS_MARKER": "observed" }, "argument_registers": 4, "backtrace_frames": 1 } } ``` The result excerpt below is from the conformance fixture. Paths are trimmed, and only one event of each kind is shown. ```json { "process": { "outcome": "exited", "exit_status": 0, "terminated": true, "stdout": { "text": "world x0 40\nworld x1 41\nworld x2 42\nmarker observed\n", "truncated": false } }, "events": [ { "symbol": "-[ReaCallGreeter greet:times:]", "file_address": "0x100000928", "module": "ReaCalls", "receiver_class": "ReaCallGreeter", "selector": "greet:times:", "registers": [{ "name": "x0", "value": "0x…" }, { "name": "x1", "value": "0x…" }, { "name": "x2", "value": "0x…" }, { "name": "x3", "value": "0x0" }], "backtrace": [{ "symbol": "main", "module": "ReaCalls" }] }, { "symbol": "rea_call_add", "registers": [{ "name": "x0", "value": "0x0" }, { "name": "x1", "value": "0x28" }] } ], "coverage": { "status": "complete", "event_limit_reached": false, "unresolved_breakpoints": [] } } ``` CLI: `rea observe-native-calls <app-or-mach-o> <input-json>`. The input is the same object as the MCP arguments, inline or as a JSON file path. ### Out of scope (rest of #385) - Frida; - return-value capture; - unified-log capture; - attaching to processes REA did not launch. ## Verification - **Real lane:** `npm run verify:native-calls` passed on macOS 26 arm64 (lldb-2100.0.17.203). It compiles `tests/conformance/native/calls.m` and runs the tool through both the packaged CLI and stdio MCP. It checks: - the receiver class, selector and argument registers of all six entries; - breakpoint file addresses equal to `nm`'s symbol addresses; - captured stdout and an environment override; - CLI and MCP parity; - the `event-limit` and `duration-elapsed` outcomes, with the process confirmed gone; - that a hardened-runtime copy is refused with `debugger-attach-denied`, while the same copy signed with `get-task-allow` is traced. - **Boundary tests:** - `tests/boundary/providers/native/nativeCallObservation.test.ts` (fake tracer) covers projection, partial coverage, input rejection before any launch, changed and non-Mach-O targets, passthrough of tracer failures, and cancellation. - `tests/boundary/mcp/nativeCallObservationMcp.test.ts` covers MCP routing and input-schema rejection; it runs on Linux too. - `npm run check:changed`: 295 files and 2,987 tests passed. - `npm run docs:generate && npm run docs:check` passed. - The contract, catalog and schema guards passed: `contractPresentation`, `cliToolCoverage`, `productCatalog`, `toolSchemaValidity` and `conformancePackage`. - `npm pack --dry-run` includes `bridge/native/rea_lldb_tracer.py`. No Hopper, Ghidra or IDA code is touched, so I didn't run any real provider verification. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-17 21:07:18 +00:00
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#2538
No description provided.