[GH-ISSUE #8197] [Design] Trace UI actions through separately evidenced tail transfers and thunks #6479

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

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

Before submitting

  • I searched open and closed issues and pull requests for a related discussion. #5438 addresses a Hopper reverse-caller defect; #8190 discusses Kuna outgoing data/reference semantics.
  • 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

How should REA help an analyst trace a compiled UI action through a direct tail transfer or thunk to a separate function, while keeping observed jumps, provider-classified calls, inferences, and ordinary intra-function branches distinct?

Context and evidence

I built an unexecuted arm64 AppKit app with an authored NIB action runAction:. Its source handler calls a non-inlined audit_tail_sink; with clang -O2, otool showed the handler at 0x10000077c moves its argument and uses a direct b at 0x100000780 to _audit_tail_sink at 0x100000778. Executable SHA-256 c2bf21802d31ce75925deef6cfc84c29fb768d68eb9f31a233652617957ddd79.

  • Real REA MCP trace_native_ui_action({"action":"runAction:","max_depth":3,"max_nodes":40,"max_edges":80}) with both providers resolved the authored action and Objective-C selector to the handler. With Ghidra 12.1.4, the trace also contained the sink and an observed direct_call edge; Ghidra's procedure_references classified the branch site as UNCONDITIONAL_CALL.
  • With Kuna 1.986, the trace stopped at the handler and correctly reported static native-call coverage as partial. Its handler pseudocode named _audit_tail_sink with a tail-call comment. Direct Kuna xrefs --from 0x10000077c --json independently reported the branch as a jump from 0x100000780 to the sink; REA/Kuna's existing call graph excludes jump edges.
  • trace_call_path reached the sink with Ghidra but returned not_reached with Kuna, with Evidence limitations stating that Kuna's graph excludes jumps. This is consistent with that tool's direct-call scope, not proof that the target is unreachable at runtime.

A separate x86-64 fixture with a jump over an instruction-decoding decoy was classified by Ghidra as an entry thunk to another function; both providers' direct-call path returned not_reached there, while Ghidra's outgoing references retained the jump. These two fixtures are not general coverage or runtime measurements.

Expected interaction with targets

Read selected files and existing static provider observations. No target execution or binary mutation is proposed.

Boundary details (optional)

Do not silently turn arbitrary jumps into calls. Distinguish a provider's call classification, an observed branch target, a proven or inferred thunk/tail-transfer relation, unresolved targets, and intra-function control flow. Keep existing direct-call contracts, source coordinates, target identity, and partial coverage. Static results do not establish runtime reachability.

Options and tradeoffs

Explore whether trace_native_ui_action can surface separately typed tail/thunk candidates, or whether a bounded control-flow investigation should connect to it. Compare Ghidra's function classifications and references with Kuna's jump observations rather than forwarding every provider edge. Decide whether trace_call_path should remain call-only and link to a complementary workflow.

Verification available so far

I have a prototype or observed results.

Success criteria and open questions

Exercise direct tail transfers, non-tail inter-function jumps, internal jumps, stubs/thunks, indirect and unresolved flows, ambiguous ownership, and coverage bounds on both providers. Preserve action/handler evidence and do not assert that a candidate is executed.

References

Related: #8190 (Kuna outgoing reference semantics) and #8184 (structured coverage). REA d48e472fa plus the unrelated Kuna baseline update in #8187; macOS arm64.

Originally created by @morluto on GitHub (Oct 30, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8197 ### Before submitting - [x] I searched open and closed issues and pull requests for a related discussion. #5438 addresses a Hopper reverse-caller defect; #8190 discusses Kuna outgoing data/reference semantics. - [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 How should REA help an analyst trace a compiled UI action through a direct tail transfer or thunk to a separate function, while keeping observed jumps, provider-classified calls, inferences, and ordinary intra-function branches distinct? ### Context and evidence I built an unexecuted arm64 AppKit app with an authored NIB action `runAction:`. Its source handler calls a non-inlined `audit_tail_sink`; with `clang -O2`, `otool` showed the handler at `0x10000077c` moves its argument and uses a direct `b` at `0x100000780` to `_audit_tail_sink` at `0x100000778`. Executable SHA-256 `c2bf21802d31ce75925deef6cfc84c29fb768d68eb9f31a233652617957ddd79`. - Real REA MCP `trace_native_ui_action({"action":"runAction:","max_depth":3,"max_nodes":40,"max_edges":80})` with both providers resolved the authored action and Objective-C selector to the handler. With Ghidra 12.1.4, the trace also contained the sink and an observed `direct_call` edge; Ghidra's `procedure_references` classified the branch site as `UNCONDITIONAL_CALL`. - With Kuna 1.986, the trace stopped at the handler and correctly reported static native-call coverage as partial. Its handler pseudocode named `_audit_tail_sink` with a `tail-call` comment. Direct Kuna `xrefs --from 0x10000077c --json` independently reported the branch as a `jump` from `0x100000780` to the sink; REA/Kuna's existing call graph excludes jump edges. - `trace_call_path` reached the sink with Ghidra but returned `not_reached` with Kuna, with Evidence limitations stating that Kuna's graph excludes jumps. This is consistent with that tool's direct-call scope, not proof that the target is unreachable at runtime. A separate x86-64 fixture with a jump over an instruction-decoding decoy was classified by Ghidra as an entry thunk to another function; both providers' direct-call path returned `not_reached` there, while Ghidra's outgoing references retained the jump. These two fixtures are not general coverage or runtime measurements. ### Expected interaction with targets Read selected files and existing static provider observations. No target execution or binary mutation is proposed. ### Boundary details (optional) Do not silently turn arbitrary jumps into calls. Distinguish a provider's call classification, an observed branch target, a proven or inferred thunk/tail-transfer relation, unresolved targets, and intra-function control flow. Keep existing direct-call contracts, source coordinates, target identity, and partial coverage. Static results do not establish runtime reachability. ### Options and tradeoffs Explore whether `trace_native_ui_action` can surface separately typed tail/thunk candidates, or whether a bounded control-flow investigation should connect to it. Compare Ghidra's function classifications and references with Kuna's jump observations rather than forwarding every provider edge. Decide whether `trace_call_path` should remain call-only and link to a complementary workflow. ### Verification available so far I have a prototype or observed results. ### Success criteria and open questions Exercise direct tail transfers, non-tail inter-function jumps, internal jumps, stubs/thunks, indirect and unresolved flows, ambiguous ownership, and coverage bounds on both providers. Preserve action/handler evidence and do not assert that a candidate is executed. ### References Related: #8190 (Kuna outgoing reference semantics) and #8184 (structured coverage). REA `d48e472fa` plus the unrelated Kuna baseline update in #8187; macOS arm64.
Author
Owner

@morluto commented on GitHub (Oct 30, 2026):

A real REA/Kuna 1.986 recovery check on the original UI fixture narrows the gap. procedure_assembly for the resolved handler shows b 0x100000778 at 0x100000780. Once the destination is known, xrefs at 0x100000778 returns 0x100000780, and procedure_address resolves the sink name to that destination. These are useful separate observations, not a structured tail-transfer relationship: xrefs returns untyped incoming addresses, procedure_references and inspect_native_instruction report capability unavailability with Kuna, and the call graph still excludes jumps. An agent would need to interpret assembly and decide whether the destination belongs to a different function; this scripted check does not establish autonomous agent success.

I also built an unexecuted arm64 control containing an internal unconditional branch and a separate one-instruction branch to another function. REA/Ghidra 12.1.4 procedure_references classified both as unconditional jumps, but attached the internal branch's source and destination to the same function and classified the separate entry as a thunk to its target. Its trace_call_path did not follow that thunk, consistent with the call-only scope. REA/Kuna's assembly exposed both branch targets, and exact incoming xrefs found each source, but did not encode that ownership distinction. Direct Kuna xrefs --to for the internal target included the same source and destination function, while xrefs --from the enclosing function returned no rows on this fixture; direct --from on the separate branch returned a jump to the other function. This is an observed asymmetry, not a claim about every branch or provider.

These controls support preserving source/target ownership and provider classification alongside a candidate transfer: treating every jump as a function call would misclassify the internal branch, and a separate thunk can also be excluded by the call-only graph. The selected binaries' SHA-256 values were unchanged through REA, and no targets were executed.

<!-- gh-comment-id:6534337154 --> @morluto commented on GitHub (Oct 30, 2026): A real REA/Kuna 1.986 recovery check on the original UI fixture narrows the gap. `procedure_assembly` for the resolved handler shows `b 0x100000778` at `0x100000780`. Once the destination is known, `xrefs` at `0x100000778` returns `0x100000780`, and `procedure_address` resolves the sink name to that destination. These are useful separate observations, not a structured tail-transfer relationship: `xrefs` returns untyped incoming addresses, `procedure_references` and `inspect_native_instruction` report capability unavailability with Kuna, and the call graph still excludes jumps. An agent would need to interpret assembly and decide whether the destination belongs to a different function; this scripted check does not establish autonomous agent success. I also built an unexecuted arm64 control containing an internal unconditional branch and a separate one-instruction branch to another function. REA/Ghidra 12.1.4 `procedure_references` classified both as unconditional jumps, but attached the internal branch's source and destination to the same function and classified the separate entry as a thunk to its target. Its `trace_call_path` did not follow that thunk, consistent with the call-only scope. REA/Kuna's assembly exposed both branch targets, and exact incoming `xrefs` found each source, but did not encode that ownership distinction. Direct Kuna `xrefs --to` for the internal target included the same source and destination function, while `xrefs --from` the enclosing function returned no rows on this fixture; direct `--from` on the separate branch returned a jump to the other function. This is an observed asymmetry, not a claim about every branch or provider. These controls support preserving source/target ownership and provider classification alongside a candidate transfer: treating every jump as a function call would misclassify the internal branch, and a separate thunk can also be excluded by the call-only graph. The selected binaries' SHA-256 values were unchanged through REA, and no targets were executed.
Author
Owner

@morluto commented on GitHub (Oct 31, 2026):

Looping in @gensokyo-dev @kaoru0822-kitauji @N0zoM1z0 for visibility on the Ghidra, Kuna, and REA tool-design investigation.

<!-- gh-comment-id:6538793743 --> @morluto commented on GitHub (Oct 31, 2026): Looping in @gensokyo-dev @kaoru0822-kitauji @N0zoM1z0 for visibility on the Ghidra, Kuna, and REA tool-design investigation.
Author
Owner

@morluto commented on GitHub (Oct 31, 2026):

ELF shared-object PLT control outside the UI workflow

An additional source-owned x86-64 ELF shared object calls its own exported overload through a PLT stub. On real REA/Ghidra 12.1.4 MCP, trace_call_path reached the stub 0x1014a0 but returned not_reached for the implementation 0x101430; procedure_info identified the stub as a thunk to that implementation, and outgoing references included a computed jump. Kuna 1.986 likewise reached its stub and stopped before the implementation; its stub pseudocode described a jump-as-call. Both call-path outputs retain provider scope and limitations. Independent LLVM disassembly and the R_X86_64_JUMP_SLOT relocation support the static stub/slot description, not a runtime binding or reachability claim.

This is one non-UI ELF control for the existing question of exposing typed thunk/tail-transfer candidates alongside a call-only graph. It does not imply that an arbitrary jump is a call or that an export will be selected by the loader. Full coordinate and exact-symbol handoff details are in https://github.com/morluto/rea/issues/8183#issuecomment-6541542609. Target bytes were unchanged and the binary was not executed.

<!-- gh-comment-id:6541544146 --> @morluto commented on GitHub (Oct 31, 2026): ### ELF shared-object PLT control outside the UI workflow An additional source-owned x86-64 ELF shared object calls its own exported overload through a PLT stub. On real REA/Ghidra 12.1.4 MCP, `trace_call_path` reached the stub `0x1014a0` but returned `not_reached` for the implementation `0x101430`; `procedure_info` identified the stub as a thunk to that implementation, and outgoing references included a computed jump. Kuna 1.986 likewise reached its stub and stopped before the implementation; its stub pseudocode described a `jump-as-call`. Both call-path outputs retain provider scope and limitations. Independent LLVM disassembly and the `R_X86_64_JUMP_SLOT` relocation support the static stub/slot description, not a runtime binding or reachability claim. This is one non-UI ELF control for the existing question of exposing typed thunk/tail-transfer candidates alongside a call-only graph. It does not imply that an arbitrary jump is a call or that an export will be selected by the loader. Full coordinate and exact-symbol handoff details are in https://github.com/morluto/rea/issues/8183#issuecomment-6541542609. Target bytes were unchanged and the binary was not executed.
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#6479
No description provided.