[GH-ISSUE #2379] [Design] Correlate Android Java/JNI and native debugger observations with verified module identities #791

Open
opened 2026-10-15 18:28:23 +00:00 by bsakaguchi · 2 comments
Owner

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

Type: Architecture / RFC Status: Proposed Ecosystem: REA

Analyst question

When my APK crosses JNI, which observed managed method and loaded native location participate in the call, and what is unknown about that relationship?

Android's Java and native debuggers observe different execution domains. Static JNI naming or same-process timestamps alone do not establish a dynamic call edge.

Proposed outcome

  • Compose the selected Android managed session (#2378, if admitted) with a separately admitted native debugger lane under #1267.
  • Bind both captures to the same verified device/boot/process generation. Preserve independent thread IDs, stop identities, clocks and interruption effects.
  • Identify native modules by observed ABI, load mapping and build ID when available; caller-selected .so/debug files must have explicit association evidence.
  • Preserve JNI exports and observed dynamic registrations separately. A registration fact is a binding, not proof of invocation.
  • Label cross-domain edges observed, derived, inferred or unresolved. Do not automatically merge native and managed frames into a claimed complete stack.
  • Keep software-breakpoint writes, attach/suspend/step effects and cleanup responsibility explicit for both engines.

Acceptance

  • An own APK/NDK fixture uses both exported JNI naming and RegisterNatives; evidence distinguishes bindings from actual observed calls.
  • A wrong-ABI or wrong-build .so, unloaded module and process restart prevent verified location joins.
  • Partial capture from either engine remains usable with explicit coverage.
  • Concurrent stops, resume and cancellation do not deadlock or strand the selected process.
  • CLI/MCP expose contributing evidence and unresolved links in the same domain contract.
  • One JNI correlation spec/plan follows approved managed/native lanes; it does not duplicate their provider implementations.

Scope and references

This composition adds runtime behavior beyond existing static Android analysis. CONTRIBUTING.md requires a maintainer decision covering the final dual-debugger effects before substantial implementation for integration. No implicit installation, rooting or device security changes.

Originally created by @matbrgz on GitHub (Oct 12, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2379 [![Type: Architecture / RFC](https://img.shields.io/badge/type-architecture_%2F_RFC-darkred.svg?style=flat-square)](https://github.com/morluto/rea) [![Status: Proposed](https://img.shields.io/badge/status-proposed-blue.svg?style=flat-square)](https://github.com/morluto/rea) [![Ecosystem: REA](https://img.shields.io/badge/ecosystem-morluto%2Frea-8A2BE2.svg?style=flat-square)](https://github.com/morluto/rea) ## Analyst question When my APK crosses JNI, which observed managed method and loaded native location participate in the call, and what is unknown about that relationship? Android's Java and native debuggers observe different execution domains. Static JNI naming or same-process timestamps alone do not establish a dynamic call edge. ## Proposed outcome - Compose the selected Android managed session (#2378, if admitted) with a separately admitted native debugger lane under #1267. - Bind both captures to the same verified device/boot/process generation. Preserve independent thread IDs, stop identities, clocks and interruption effects. - Identify native modules by observed ABI, load mapping and build ID when available; caller-selected .so/debug files must have explicit association evidence. - Preserve JNI exports and observed dynamic registrations separately. A registration fact is a binding, not proof of invocation. - Label cross-domain edges observed, derived, inferred or unresolved. Do not automatically merge native and managed frames into a claimed complete stack. - Keep software-breakpoint writes, attach/suspend/step effects and cleanup responsibility explicit for both engines. ## Acceptance - [ ] An own APK/NDK fixture uses both exported JNI naming and RegisterNatives; evidence distinguishes bindings from actual observed calls. - [ ] A wrong-ABI or wrong-build .so, unloaded module and process restart prevent verified location joins. - [ ] Partial capture from either engine remains usable with explicit coverage. - [ ] Concurrent stops, resume and cancellation do not deadlock or strand the selected process. - [ ] CLI/MCP expose contributing evidence and unresolved links in the same domain contract. - [ ] One JNI correlation spec/plan follows approved managed/native lanes; it does not duplicate their provider implementations. ## Scope and references This composition adds runtime behavior beyond existing static Android analysis. CONTRIBUTING.md requires a maintainer decision covering the final dual-debugger effects before substantial implementation for integration. No implicit installation, rooting or device security changes. - [Android Java-only, native-only and dual debugger modes](https://developer.android.com/studio/debug) - [JNI functions and registration](https://docs.oracle.com/en/java/javase/25/docs/specs/jni/functions.html) - Related: #717, #968, #1267, #2376.
Author
Owner

@matbrgz commented on GitHub (Oct 12, 2026):

Design & Correlation Notes

Objective (ELI5):
Bridge Java/ART runtime stacks and native C/C++ (JNI .so libraries) thread frames into a single correlated call trace.

Correlation Requirements:

  • Track Java_ native bridge transitions: correlate the Java method call to the native ELF symbol offset.
  • Report exact module provenance for native libraries loaded from /data/app/.../lib/.
  • Handle cases where native frames lack DWARF unwind information gracefully by marking native caller frames as unwound_via_frame_pointer or unwind_truncated.
<!-- gh-comment-id:6133914810 --> @matbrgz commented on GitHub (Oct 12, 2026): ### Design & Correlation Notes **Objective (ELI5):** Bridge Java/ART runtime stacks and native C/C++ (JNI `.so` libraries) thread frames into a single correlated call trace. **Correlation Requirements:** - Track `Java_` native bridge transitions: correlate the Java method call to the native ELF symbol offset. - Report exact module provenance for native libraries loaded from `/data/app/.../lib/`. - Handle cases where native frames lack DWARF unwind information gracefully by marking native caller frames as `unwound_via_frame_pointer` or `unwind_truncated`.
Author
Owner

@Rinkia commented on GitHub (Oct 14, 2026):

A data point on the static side of this, from a real APK, which might belong here as an input or in a separate issue (maintainers' call).

Target: F-Droid de.markusfisch.android.binaryeye vc 180 (sha256 568cec65…44f1), ARM64 lib/arm64-v8a/libzxingcpp_android.so (sha256 b143dc73…b030). rea-agents@6.4.0, Ghidra 12.1.4.

What I did by hand. inspect-android-class lists de.markusfisch.android.zxingcpp.ZxingCpp. rea search … Java_ --kind procedures --provider ghidra finds 7 exports in the library:

0x197e14 Java_de_markusfisch_android_zxingcpp_ZxingCpp_readYBuffer
0x199df8 Java_de_markusfisch_android_zxingcpp_ZxingCpp_readByteArray
0x199f48 Java_de_markusfisch_android_zxingcpp_ZxingCpp_readBitmap
0x19a204 Java_de_markusfisch_android_zxingcpp_ZxingCpp_encodeString
0x19aaac Java_de_markusfisch_android_zxingcpp_ZxingCpp_encodeByteArray
0x19ac94 Java_de_markusfisch_android_zxingcpp_ZxingCpp_encodeStringAsSvg
0x19af98 Java_de_markusfisch_android_zxingcpp_ZxingCpp_encodeByteArrayAsSvg

Matching them to the DEX native declarations is a deterministic, static step: JNI short names here, long __<sig> names when a native method is overloaded. Today REA doesn't do it. project_android_application_graph only forms path-based DEX × library hypotheses ("does not identify a specific JNI binding"), and the proposal above starts from runtime observations.

Decompiled export. Ghidra's pseudocode for readYBuffer keeps JNIEnv* untyped:

lVar2 = (**(code **)(*param_1 + 0x730))(param_1,param_3);   // JNIEnv fn #230 = GetDirectBufferAddress
FUN_00197f3c(auStack_a8, lVar2 + param_6 * param_4 + (long)param_5, param_7, param_8, 0x1000000, param_4, 0);

The DEX signature (Ljava/nio/ByteBuffer;IIIIIILde/markusfisch/android/zxingcpp/ZxingCpp$ReaderOptions;)Ljava/util/List; gives the parameters exactly (JNIEnv*, jobject thiz, jobject yBuffer, jint rowStride, left, top, width, height, rotation, jobject options). With those applied, the line above reads as pixels + top*rowStride + left and ImageFormat::Lum, matching the wrapper source (markusfisch/zxing-cpp v3.1.1.3, wrappers/android/zxingcpp/src/main/cpp/ZXingCpp.cpp:260).

Possible static piece, kept separate from runtime evidence as this design asks:

  • For each DEX native method, the expected short/long JNI names, then the matched exports with addresses.
  • Native declarations with no export reported as unresolved (possible RegisterNatives), and Java_* exports with no native declaration reported separately.
  • Labelled as a static name binding, explicitly not proof of invocation.
  • Optionally, JNI parameter types applied in the provider's decompile of matched exports.

Related: method access flags aren't exposed in inspect_android_class today, which is the first input this needs. I've filed that as #3489.

<!-- gh-comment-id:6168882904 --> @Rinkia commented on GitHub (Oct 14, 2026): A data point on the static side of this, from a real APK, which might belong here as an input or in a separate issue (maintainers' call). **Target:** F-Droid `de.markusfisch.android.binaryeye` vc 180 (sha256 `568cec65…44f1`), ARM64 `lib/arm64-v8a/libzxingcpp_android.so` (sha256 `b143dc73…b030`). `rea-agents@6.4.0`, Ghidra 12.1.4. **What I did by hand.** `inspect-android-class` lists `de.markusfisch.android.zxingcpp.ZxingCpp`. `rea search … Java_ --kind procedures --provider ghidra` finds 7 exports in the library: ```text 0x197e14 Java_de_markusfisch_android_zxingcpp_ZxingCpp_readYBuffer 0x199df8 Java_de_markusfisch_android_zxingcpp_ZxingCpp_readByteArray 0x199f48 Java_de_markusfisch_android_zxingcpp_ZxingCpp_readBitmap 0x19a204 Java_de_markusfisch_android_zxingcpp_ZxingCpp_encodeString 0x19aaac Java_de_markusfisch_android_zxingcpp_ZxingCpp_encodeByteArray 0x19ac94 Java_de_markusfisch_android_zxingcpp_ZxingCpp_encodeStringAsSvg 0x19af98 Java_de_markusfisch_android_zxingcpp_ZxingCpp_encodeByteArrayAsSvg ``` Matching them to the DEX `native` declarations is a deterministic, static step: JNI short names here, long `__<sig>` names when a native method is overloaded. Today REA doesn't do it. `project_android_application_graph` only forms path-based DEX × library hypotheses ("does not identify a specific JNI binding"), and the proposal above starts from runtime observations. **Decompiled export.** Ghidra's pseudocode for `readYBuffer` keeps `JNIEnv*` untyped: ```c lVar2 = (**(code **)(*param_1 + 0x730))(param_1,param_3); // JNIEnv fn #230 = GetDirectBufferAddress FUN_00197f3c(auStack_a8, lVar2 + param_6 * param_4 + (long)param_5, param_7, param_8, 0x1000000, param_4, 0); ``` The DEX signature `(Ljava/nio/ByteBuffer;IIIIIILde/markusfisch/android/zxingcpp/ZxingCpp$ReaderOptions;)Ljava/util/List;` gives the parameters exactly (`JNIEnv*`, `jobject thiz`, `jobject yBuffer`, `jint rowStride, left, top, width, height, rotation`, `jobject options`). With those applied, the line above reads as `pixels + top*rowStride + left` and `ImageFormat::Lum`, matching the wrapper source (`markusfisch/zxing-cpp` `v3.1.1.3`, `wrappers/android/zxingcpp/src/main/cpp/ZXingCpp.cpp:260`). **Possible static piece**, kept separate from runtime evidence as this design asks: - For each DEX native method, the expected short/long JNI names, then the matched exports with addresses. - Native declarations with no export reported as `unresolved` (possible `RegisterNatives`), and `Java_*` exports with no native declaration reported separately. - Labelled as a static name binding, explicitly not proof of invocation. - Optionally, JNI parameter types applied in the provider's decompile of matched exports. Related: method access flags aren't exposed in `inspect_android_class` today, which is the first input this needs. I've filed that as #3489.
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#791
No description provided.