[GH-ISSUE #8408] [Design] Connect indirect call investigations to Mach-O slot bindings #6589

Open
opened 2026-11-01 21:12:29 +00:00 by bsakaguchi · 3 comments
Owner

Originally created by @morluto on GitHub (Nov 1, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/8408

Before submitting

  • I searched the open and closed issues and pull requests for a related discussion.
  • I understand the contribution scope must be reviewed before integration; open questions are welcome here.
  • This discussion does not include credentials, private samples, or sensitive target data.

Decision needed

When investigating a call through a selected Mach-O pointer slot, should REA expose the slot's file-declared loader binding as evidence, so an analyst can correlate the observed call and its imported symbol without inferring identity from a generated provider name? If so, should this extend an existing inspection or be composed into a task-level call-site workflow?

Context and evidence

On REA main 6e9ee97a, Ghidra 12.1.4, macOS arm64 and MCP SDK 2.3.1, I statically investigated the shipped Homebrew Brotli 1.2.0 libbrotlidec.1.2.0.dylib (SHA-256 d7ac1e69b6c443341fb4302de0169d187489d58d8a641c8eadd6db9ca2cd0cbf). Its _BrotliDecoderDecompress reads slot 0xc000 and calls through a register at 0x14dc.

  • procedure_references reported the read of 0xc000 and Ghidra's computed-call reference to a ___chkstk_darwin thunk. inspect_address_context named the slot PTR____chkstk_darwin_0000c000, and xrefs returned the read site. These are useful provider observations; the generated name is not itself a file-declared binding record.
  • inspect_macho included the ___chkstk_darwin import from libSystem, but its address was null. The tested public results did not provide an explicit slot-to-binding record for 0xc000.
  • Independently, dyld_info -fixups reported 0xc000 as a bind to libSystem/___chkstk_darwin. A direct local invocation of REA's internal Mach-O fixup decoder also returned a chained bind, symbol ___chkstk_darwin, library /usr/lib/libSystem.B.dylib, ordinal 1, weak false and addend 0 at 0xc000; adjacent slots 0xc008 and 0xc010 decoded as different imports. This confirms one producer can decode this input, not that the fact is currently exposed by a public REA tool.

All successful public MCP calls used the fetched compact advertised schemas. The target hash was unchanged and it was not executed. One image, one architecture and one provider were tested. The trace_native_values control was limited to depth zero and reported that limit; it does not establish whole-program value flow. Existing Ghidra names and thunk references are usable alternatives, and the native dyld_info command can provide independent file evidence.

Expected interaction with targets

Read existing files or captured data; launch local analysis tools when the caller selects a provider. No target execution or mutation is proposed.

Boundary details (optional)

A binding is a static loader declaration, not proof of the pointer's runtime value, call reachability, or an exhaustive computed-target set. Preserve selected slice, original symbol/library identity, ordinal, weak/addend and fixup provenance, with unsupported/unknown/partial states. Do not infer a binding by matching display names or automatically exempt import-slot calls from #8340's coverage limitation.

Options and tradeoffs

Consider exact requested slot addresses in an existing native inspection, or an address/call-site investigation that composes provider read/call observations with separately identified file binding evidence. A whole-image binding inventory might be useful for other tasks but is not required by this example. The existing Objective-C class-binding input is scoped to class imports; a generic binding should not be mislabeled as an Objective-C class. Any approach needs bounded input/result handling and clear slice/target identity.

Verification available so far

I have observed results; there is no product prototype. Suggested checks: this chained-bind example, a distinct bind, rebased/raw/unbound slots, unsupported or malformed fixups, non-UTF-8 names, slice selection, and public CLI/MCP Evidence provenance. Legacy and arm64e cases, other providers and actual autonomous-agent behavior remain untested.

Success criteria and open questions

An analyst can distinguish a provider's computed-call candidate, the file's declared slot binding, and unknown runtime state without parsing another tool's display text. It remains open which existing contract owns that handoff and which inputs/formats deserve support.

References

Related: #8277, #8200, and the shipped-library control on #8340. The reusable producer is AppleMachoFixups.ts; Objective-C-only projection is AppleObjcClassImportBindings.ts.

Maintainers: @gensokyo-dev @kaoru0822-kitauji @N0zoM1z0

Kuna 1.988 import-slot path control

On REA bafe3784c plus the version-only #8509 change and the previously described owned import-slot fixture, real Kuna 1.988 compact stdio MCP returned incoming slot references at 0x1360, 0x1364, and 0x1368, where the direct Kuna JSON labels them data/address formation, read, and indirect call. procedure_callees(_audit_import_call) returned 0x4000 with partial indirect-call coverage; 1.986 returned no callee for the same fixture. Direct Kuna 1.988's graph labels the 0x4000 node kind: import named malloc. REA trace_call_path(_audit_import_call, malloc) returned reached on its observed edge to 0x4000, with partial coverage and explicit residual unknowns. Its path nodes carry addresses rather than the graph's import classification; inspect_address_context(0x4000) reports the provider name malloc but no separately established file binding. The same path on #8443 head b7160bd7 reports observed-graph reach with discovery unknown. The target was unchanged and not run. This is not a claim about the runtime pointer, runtime call, or exhaustive targets; the existing binding question remains distinct from an upstream provider's inferred call edge.

Originally created by @morluto on GitHub (Nov 1, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8408 ### Before submitting - [x] I searched the open and closed issues and pull requests for a related discussion. - [x] I understand the contribution scope must be reviewed before integration; open questions are welcome here. - [x] This discussion does not include credentials, private samples, or sensitive target data. ### Decision needed When investigating a call through a selected Mach-O pointer slot, should REA expose the slot's **file-declared loader binding** as evidence, so an analyst can correlate the observed call and its imported symbol without inferring identity from a generated provider name? If so, should this extend an existing inspection or be composed into a task-level call-site workflow? ### Context and evidence On REA main `6e9ee97a`, Ghidra 12.1.4, macOS arm64 and MCP SDK 2.3.1, I statically investigated the shipped Homebrew Brotli 1.2.0 `libbrotlidec.1.2.0.dylib` (SHA-256 `d7ac1e69b6c443341fb4302de0169d187489d58d8a641c8eadd6db9ca2cd0cbf`). Its `_BrotliDecoderDecompress` reads slot `0xc000` and calls through a register at `0x14dc`. - `procedure_references` reported the read of `0xc000` and Ghidra's computed-call reference to a `___chkstk_darwin` thunk. `inspect_address_context` named the slot `PTR____chkstk_darwin_0000c000`, and `xrefs` returned the read site. These are useful provider observations; the generated name is not itself a file-declared binding record. - `inspect_macho` included the `___chkstk_darwin` import from `libSystem`, but its `address` was null. The tested public results did not provide an explicit slot-to-binding record for `0xc000`. - Independently, `dyld_info -fixups` reported `0xc000` as a bind to `libSystem/___chkstk_darwin`. A direct local invocation of REA's **internal** Mach-O fixup decoder also returned a chained bind, symbol `___chkstk_darwin`, library `/usr/lib/libSystem.B.dylib`, ordinal 1, weak false and addend 0 at `0xc000`; adjacent slots `0xc008` and `0xc010` decoded as different imports. This confirms one producer can decode this input, not that the fact is currently exposed by a public REA tool. All successful public MCP calls used the fetched compact advertised schemas. The target hash was unchanged and it was not executed. One image, one architecture and one provider were tested. The `trace_native_values` control was limited to depth zero and reported that limit; it does not establish whole-program value flow. Existing Ghidra names and thunk references are usable alternatives, and the native `dyld_info` command can provide independent file evidence. ### Expected interaction with targets Read existing files or captured data; launch local analysis tools when the caller selects a provider. No target execution or mutation is proposed. ### Boundary details (optional) A binding is a static loader declaration, not proof of the pointer's runtime value, call reachability, or an exhaustive computed-target set. Preserve selected slice, original symbol/library identity, ordinal, weak/addend and fixup provenance, with unsupported/unknown/partial states. Do not infer a binding by matching display names or automatically exempt import-slot calls from #8340's coverage limitation. ### Options and tradeoffs Consider exact requested slot addresses in an existing native inspection, or an address/call-site investigation that composes provider read/call observations with separately identified file binding evidence. A whole-image binding inventory might be useful for other tasks but is not required by this example. The existing Objective-C class-binding input is scoped to class imports; a generic binding should not be mislabeled as an Objective-C class. Any approach needs bounded input/result handling and clear slice/target identity. ### Verification available so far I have observed results; there is no product prototype. Suggested checks: this chained-bind example, a distinct bind, rebased/raw/unbound slots, unsupported or malformed fixups, non-UTF-8 names, slice selection, and public CLI/MCP Evidence provenance. Legacy and arm64e cases, other providers and actual autonomous-agent behavior remain untested. ### Success criteria and open questions An analyst can distinguish a provider's computed-call candidate, the file's declared slot binding, and unknown runtime state without parsing another tool's display text. It remains open which existing contract owns that handoff and which inputs/formats deserve support. ### References Related: #8277, #8200, and the [shipped-library control on #8340](https://github.com/morluto/rea/pull/8340#issuecomment-6551463874). The reusable producer is [`AppleMachoFixups.ts`](https://github.com/morluto/rea/blob/6e9ee97a69cb16c7150d8b550e705cba055ee4f0/src/native/AppleMachoFixups.ts); Objective-C-only projection is [`AppleObjcClassImportBindings.ts`](https://github.com/morluto/rea/blob/6e9ee97a69cb16c7150d8b550e705cba055ee4f0/src/native/AppleObjcClassImportBindings.ts). Maintainers: @gensokyo-dev @kaoru0822-kitauji @N0zoM1z0 ### Kuna 1.988 import-slot path control On REA `bafe3784c` plus the version-only #8509 change and the previously described owned import-slot fixture, real Kuna 1.988 compact stdio MCP returned incoming slot references at `0x1360`, `0x1364`, and `0x1368`, where the direct Kuna JSON labels them data/address formation, read, and indirect call. `procedure_callees(_audit_import_call)` returned `0x4000` with partial indirect-call coverage; 1.986 returned no callee for the same fixture. Direct Kuna 1.988's graph labels the `0x4000` node `kind: import` named `malloc`. REA `trace_call_path(_audit_import_call, malloc)` returned `reached` on its observed edge to `0x4000`, with partial coverage and explicit residual unknowns. Its path nodes carry addresses rather than the graph's import classification; `inspect_address_context(0x4000)` reports the provider name `malloc` but no separately established file binding. The same path on #8443 head `b7160bd7` reports observed-graph reach with discovery unknown. The target was unchanged and not run. This is not a claim about the runtime pointer, runtime call, or exhaustive targets; the existing binding question remains distinct from an upstream provider's inferred call edge.
Author
Owner

@morluto commented on GitHub (Nov 1, 2026):

Same shipped Brotli 1.2.0 image and 0xc000 slot, now tested through real REA/Kuna 1.986 on main 6e9ee97a, macOS arm64, MCP SDK 2.3.1 with fetched compact schemas:

  • inspect_address_context(0xc000) and address_name returned Kuna's analyzed name __chkstk_darwin; inspect_macho separately returned the import ___chkstk_darwin from libSystem with address: null. These are useful existing clues, but the tested responses did not expose an explicit file-declared 0xc000 binding.
  • xrefs(0xc000) returned an empty list with the existing warning that emptiness is not proof of absence. Direct Kuna xrefs --to 0xc000 also returned no rows, whereas the independent disassembly and Ghidra's typed outgoing reference identify a read at 0x14d8. This is one provider observation, not a claim about all import slots or a reason to silently substitute Ghidra's answer.
  • Kuna did not declare resolve_native_call_targets; procedure_callees retained the four observed callees and reported partial callee coverage due to indirect calls. The known path from _BrotliDecoderDecompress to _BrotliDecoderDecompressStream was reached with that partial coverage; the path to _BrotliDecoderVersion was unknown. Both path records survived export/import into a fresh target-free server unchanged.

The image SHA-256 stayed d7ac1e69b6c443341fb4302de0169d187489d58d8a641c8eadd6db9ca2cd0cbf, both sessions closed, and no target code was executed. This adds a concrete Kuna handoff case; it does not turn the file binding into a runtime target or demonstrate coverage on other hosts or images.

A separate source-owned minimal _malloc GOT-load control reproduced the missing Kuna slot xref while Ghidra reported the load; upstream report: Noelo-Lab/kuna#1376.

<!-- gh-comment-id:6551524524 --> @morluto commented on GitHub (Nov 1, 2026): Same shipped Brotli 1.2.0 image and `0xc000` slot, now tested through real REA/Kuna 1.986 on main `6e9ee97a`, macOS arm64, MCP SDK 2.3.1 with fetched compact schemas: - `inspect_address_context(0xc000)` and `address_name` returned Kuna's analyzed name `__chkstk_darwin`; `inspect_macho` separately returned the import `___chkstk_darwin` from `libSystem` with `address: null`. These are useful existing clues, but the tested responses did not expose an explicit file-declared `0xc000` binding. - `xrefs(0xc000)` returned an empty list with the existing warning that emptiness is not proof of absence. Direct Kuna `xrefs --to 0xc000` also returned no rows, whereas the independent disassembly and Ghidra's typed outgoing reference identify a read at `0x14d8`. This is one provider observation, not a claim about all import slots or a reason to silently substitute Ghidra's answer. - Kuna did not declare `resolve_native_call_targets`; `procedure_callees` retained the four observed callees and reported partial callee coverage due to indirect calls. The known path from `_BrotliDecoderDecompress` to `_BrotliDecoderDecompressStream` was `reached` with that partial coverage; the path to `_BrotliDecoderVersion` was `unknown`. Both path records survived export/import into a fresh target-free server unchanged. The image SHA-256 stayed `d7ac1e69b6c443341fb4302de0169d187489d58d8a641c8eadd6db9ca2cd0cbf`, both sessions closed, and no target code was executed. This adds a concrete Kuna handoff case; it does not turn the file binding into a runtime target or demonstrate coverage on other hosts or images. A separate source-owned minimal `_malloc` GOT-load control reproduced the missing Kuna slot xref while Ghidra reported the load; upstream report: Noelo-Lab/kuna#1376.
Author
Owner

@morluto commented on GitHub (Nov 1, 2026):

A value-flow handoff control on the source-owned _malloc GOT fixture from Noelo-Lab/kuna#1376, using real Ghidra 12.1.4 and Kuna 1.986 through MCP on macOS arm64:

  • Ghidra trace_native_values for _audit_import_call with max_depth: 0 retained a CAST whose input location is 0x4000, a derived def-use edge to the CALLIND at 0x1368, and a separate resolved-indirect provider call edge. The AST operations carry is_dead: true and block_membership: member; these are the reported Ghidra model facts, not proof of live execution or runtime target identity. The trace explicitly reports the depth limit and unavailable/omitted parameter bindings.
  • The separate dyld_info -fixups observation for the same file declares 0x4000 bound to libSystem/_malloc. The tested REA value trace does not itself include that loader-binding record. Keeping these facts separately attributed would let a consumer assess the handoff without treating the file binding as an exhaustive runtime call target.
  • Kuna's same task reported Provider has no native def-use model; it did not invent a slot-to-call value-flow edge. This differs from its decompiled malloc() text and the empty slot xrefs in #1376.

Exporting and importing the trace Evidence preserved its normalized result and limitations for both providers. On current main, some source Evidence IDs cited by the traces were absent from the imported bundle; the candidate in #8384 retained them and their views resolved. The target SHA-256 stayed 62da83d290c7b276d8708937930ef12f469f41782710fed439001c01c2747c76, and it was not executed. This is one static fixture with a deliberately selected depth limit, not a coverage or performance estimate.

<!-- gh-comment-id:6551745679 --> @morluto commented on GitHub (Nov 1, 2026): A value-flow handoff control on the source-owned `_malloc` GOT fixture from Noelo-Lab/kuna#1376, using real Ghidra 12.1.4 and Kuna 1.986 through MCP on macOS arm64: - Ghidra `trace_native_values` for `_audit_import_call` with `max_depth: 0` retained a `CAST` whose input location is `0x4000`, a derived def-use edge to the `CALLIND` at `0x1368`, and a separate resolved-indirect provider call edge. The AST operations carry `is_dead: true` and `block_membership: member`; these are the reported Ghidra model facts, not proof of live execution or runtime target identity. The trace explicitly reports the depth limit and unavailable/omitted parameter bindings. - The separate `dyld_info -fixups` observation for the same file declares `0x4000` bound to `libSystem/_malloc`. The tested REA value trace does not itself include that loader-binding record. Keeping these facts separately attributed would let a consumer assess the handoff without treating the file binding as an exhaustive runtime call target. - Kuna's same task reported `Provider has no native def-use model`; it did not invent a slot-to-call value-flow edge. This differs from its decompiled `malloc()` text and the empty slot xrefs in #1376. Exporting and importing the trace Evidence preserved its normalized result and limitations for both providers. On current main, some source Evidence IDs cited by the traces were absent from the imported bundle; the candidate in #8384 retained them and their views resolved. The target SHA-256 stayed `62da83d290c7b276d8708937930ef12f469f41782710fed439001c01c2747c76`, and it was not executed. This is one static fixture with a deliberately selected depth limit, not a coverage or performance estimate.
Author
Owner

@morluto commented on GitHub (Nov 1, 2026):

A useful existing Ghidra-side coordinate and byte-provenance control on the same source-owned import-slot fixture:

  • resolve_native_call_targets(0x1368) reported the computed-call candidate 0xc000. inspect_address_context(0xc000) identified Ghidra's /usr/lib/libSystem.B.dylib::_malloc thunk, with thunk_target: "EXTERNAL:0x1". address_to_file_offset(0xc000) refused with an explicit uninitialized EXTERNAL block reason; read_bytes returned zero of four requested bytes and complete: false.
  • Separately, address_to_file_offset(0x4000) mapped the actual GOT slot to file offset 0x4000. Ghidra read_bytes returned analyzed memory 00c0000000000000 and explicitly stated that loader fixups are included. Independent direct file-byte inspection at that offset returned 0000000000000080.
  • inspect_native_load_image reported a source mapping for __got, an uninitialized EXTERNAL mapping, and an applied relocation at 0x4000 retaining exactly those original and memory byte strings. It reports independent verification unsupported for this Mach-O; these are Ghidra observations, not a verified reconstruction. Native-load-image mapping and relocation views matched exactly after Evidence export/import into a fresh target-free server.
  • On Kuna in the same target, address_name(0x4000) returned malloc, while the load-image, raw-byte and offset tools returned capability unavailability. No provider switch or implicit fallback was performed.

This shows an existing way to keep Ghidra's synthetic candidate, initialized memory and original file bytes distinct. It does not provide the explicit file-declared slot-to-library binding proposed here, establish runtime addresses, or justify comparing provider coordinates without provenance. All successful MCP responses used fetched compact schemas; the fixture SHA-256 stayed 62da83d290c7b276d8708937930ef12f469f41782710fed439001c01c2747c76 and it was not executed. The result is one macOS arm64 sample, not a general loader or format claim.

<!-- gh-comment-id:6551766849 --> @morluto commented on GitHub (Nov 1, 2026): A useful existing Ghidra-side coordinate and byte-provenance control on the same source-owned import-slot fixture: - `resolve_native_call_targets(0x1368)` reported the computed-call candidate `0xc000`. `inspect_address_context(0xc000)` identified Ghidra's `/usr/lib/libSystem.B.dylib::_malloc` thunk, with `thunk_target: "EXTERNAL:0x1"`. `address_to_file_offset(0xc000)` refused with an explicit uninitialized `EXTERNAL` block reason; `read_bytes` returned zero of four requested bytes and `complete: false`. - Separately, `address_to_file_offset(0x4000)` mapped the actual GOT slot to file offset `0x4000`. Ghidra `read_bytes` returned analyzed memory `00c0000000000000` and explicitly stated that loader fixups are included. Independent direct file-byte inspection at that offset returned `0000000000000080`. - `inspect_native_load_image` reported a source mapping for `__got`, an uninitialized `EXTERNAL` mapping, and an applied relocation at `0x4000` retaining exactly those original and memory byte strings. It reports independent verification **unsupported** for this Mach-O; these are Ghidra observations, not a verified reconstruction. Native-load-image mapping and relocation views matched exactly after Evidence export/import into a fresh target-free server. - On Kuna in the same target, `address_name(0x4000)` returned `malloc`, while the load-image, raw-byte and offset tools returned capability unavailability. No provider switch or implicit fallback was performed. This shows an existing way to keep Ghidra's synthetic candidate, initialized memory and original file bytes distinct. It does not provide the explicit file-declared slot-to-library binding proposed here, establish runtime addresses, or justify comparing provider coordinates without provenance. All successful MCP responses used fetched compact schemas; the fixture SHA-256 stayed `62da83d290c7b276d8708937930ef12f469f41782710fed439001c01c2747c76` and it was not executed. The result is one macOS arm64 sample, not a general loader or format claim.
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#6589
No description provided.