[GH-ISSUE #3488] [Bug] inspect_android_method returns neighbouring members as the "complete" body of a native method #1300

Closed
opened 2026-10-16 21:03:56 +00:00 by bsakaguchi · 2 comments
Owner

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

Before submitting

  • I searched the open and closed issues for a related report. Related but different: #3443 (an inlined method reported as "native/abstract" with no body) and #1846 (<clinit>). In this report a native method is reported as having a body that isn't its own.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

For a native method, inspect_android_method returns body_status: available, representation: java, fell_back: false, markers: [], source.status: complete. The source text starts with the selected native declaration and then continues to the end of the class, including other members:

    public final native List<Result> readYBuffer(ByteBuffer byteBuffer, int i, int i2, int i3, int i4, int i5, int i6, ReaderOptions readerOptions);

    public final List<Result> readYBuffer(ByteBuffer byteBuffer, int i, Rect rect, int i2, ReaderOptions readerOptions) {
        ...
    }

    public final Bitmap toBitmap(BitMatrix bitMatrix, int i, int i2) {
    ...
    public final String toSvg(BitMatrix bitMatrix) {
    ...
    public final String toText(BitMatrix bitMatrix, boolean z) {
    ...
    public static final class ReaderOptions {
        ... (entire nested class) ...
    }

    private static final int toText$toInt(boolean z) {
        return z ? 1 : 0;
    }
}

That's about 400 lines for a method that has no Java body at all. Every included member after the first line belongs to something other than the selected method: the other readYBuffer overload, toBitmap, toSvg, toText, the nested ReaderOptions class, and toText$toInt.

Reproduction

Public APK: F-Droid de.markusfisch.android.binaryeye versionCode 180 (https://f-droid.org/repo/de.markusfisch.android.binaryeye_180.apk, sha256 568cec6598dbf42fbde9071af8747071ffea798473016a3d77b6ddb3bff944f1).

rea inspect-android-method /targets/binaryeye.apk de.markusfisch.android.zxingcpp.ZxingCpp readYBuffer --overload-index 0 --format json

overload_count is 2. Overload 0 is readYBuffer(java.nio.ByteBuffer, int, int, int, int, int, int, ReaderOptions), the native one. Overload 1 is the Rect convenience wrapper.

Same class, other native methods, same command shape:

Method Result
readYBuffer --overload-index 0 available, about 400 lines including other members (this bug)
readByteArray --overload-index 0 not_available, // method exists but has no decompiled body (native/abstract)
readBitmap --overload-index 0 not_available, same placeholder
encodeByteArray --overload-index 0 not_available, same placeholder
encodeString --overload-index 0 not_available, same placeholder

So most native methods are handled, and this one isn't. I haven't isolated what distinguishes it. In the decompiled class it is the last native declaration before the run of non-native members (toBitmap, toSvg, toText, nested class), which may matter for where the body slice ends.

Expected behavior

  • A native method returns body_status: not_available, ideally stating native explicitly rather than "native/abstract" (see #3443).
  • inspect_android_method never returns text belonging to other members as the selected method's source. If the engine's slice can't be bounded to one member, the result should be marked or rejected, not reported as complete.

Impact and scope

  • Silent misattribution: an agent receives unrelated code labelled as the selected method's complete Java source, with no marker. In my session it would have attributed toBitmap/toSvg/ReaderOptions logic to the camera-frame JNI entry point.
  • Observed on 1 of 5 native methods in this class, deterministic across runs.
  • Source-derived, not runtime-verified on main: current main still sends every non-<clinit> method through getMethodBodySmart in bridge/android/ReaJadxBridge.java:143-144. Android changes merged after 6.4.0 (#2439, fe8d0bdd3, 067746887) don't appear to touch body slicing, but I've only run 6.4.0.
  • The slice most likely comes from jadx-headless-mcp's getMethodBodySmart. Since REA presents the result as Evidence, a guard in REA (for example, rejecting or marking text that contains more than one member declaration) would protect callers regardless of the engine.

REA version

rea-agents@6.4.0 (npm)

Runtime and platform

Linux x64 Docker container (Ubuntu 24.04) on Windows 11 / Docker Desktop. Node 24.21.0, JDK 21.0.12, jadx-headless-mcp 0.7.1 (sha256 6e5eacf5…).

Target and artifact context

Public F-Droid APK listed above. No private samples.

Interface

CLI (the MCP tool inspect_android_method shares the workflow; MCP not run separately for this case).

Relevant output or evidence

{"class_name":"de.markusfisch.android.zxingcpp.ZxingCpp","method":{"name":"readYBuffer","overload_index":0},"overload_count":2,"representation":"java","body_status":"available","fell_back":false,"markers":[],"source":{"status":"complete","text":"    public final native List<Result> readYBuffer(...);\n\n    public final List<Result> readYBuffer(ByteBuffer byteBuffer, int i, Rect rect, ...) {\n ... (continues to end of class)"}}
Originally created by @Rinkia on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3488 ### Before submitting - [x] I searched the open and closed issues for a related report. Related but different: #3443 (an inlined method reported as "native/abstract" with no body) and #1846 (`<clinit>`). In this report a native method is reported as having a body that isn't its own. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior For a `native` method, `inspect_android_method` returns `body_status: available`, `representation: java`, `fell_back: false`, `markers: []`, `source.status: complete`. The source text starts with the selected native declaration and then continues to the end of the class, including other members: ```java public final native List<Result> readYBuffer(ByteBuffer byteBuffer, int i, int i2, int i3, int i4, int i5, int i6, ReaderOptions readerOptions); public final List<Result> readYBuffer(ByteBuffer byteBuffer, int i, Rect rect, int i2, ReaderOptions readerOptions) { ... } public final Bitmap toBitmap(BitMatrix bitMatrix, int i, int i2) { ... public final String toSvg(BitMatrix bitMatrix) { ... public final String toText(BitMatrix bitMatrix, boolean z) { ... public static final class ReaderOptions { ... (entire nested class) ... } private static final int toText$toInt(boolean z) { return z ? 1 : 0; } } ``` That's about 400 lines for a method that has no Java body at all. Every included member after the first line belongs to something other than the selected method: the other `readYBuffer` overload, `toBitmap`, `toSvg`, `toText`, the nested `ReaderOptions` class, and `toText$toInt`. ### Reproduction Public APK: F-Droid `de.markusfisch.android.binaryeye` versionCode 180 (`https://f-droid.org/repo/de.markusfisch.android.binaryeye_180.apk`, sha256 `568cec6598dbf42fbde9071af8747071ffea798473016a3d77b6ddb3bff944f1`). ```bash rea inspect-android-method /targets/binaryeye.apk de.markusfisch.android.zxingcpp.ZxingCpp readYBuffer --overload-index 0 --format json ``` `overload_count` is 2. Overload 0 is `readYBuffer(java.nio.ByteBuffer, int, int, int, int, int, int, ReaderOptions)`, the native one. Overload 1 is the `Rect` convenience wrapper. Same class, other native methods, same command shape: | Method | Result | |---|---| | `readYBuffer --overload-index 0` | `available`, about 400 lines including other members (this bug) | | `readByteArray --overload-index 0` | `not_available`, `// method exists but has no decompiled body (native/abstract)` | | `readBitmap --overload-index 0` | `not_available`, same placeholder | | `encodeByteArray --overload-index 0` | `not_available`, same placeholder | | `encodeString --overload-index 0` | `not_available`, same placeholder | So most native methods are handled, and this one isn't. I haven't isolated what distinguishes it. In the decompiled class it is the last native declaration before the run of non-native members (`toBitmap`, `toSvg`, `toText`, nested class), which may matter for where the body slice ends. ### Expected behavior - A native method returns `body_status: not_available`, ideally stating native explicitly rather than "native/abstract" (see #3443). - `inspect_android_method` never returns text belonging to other members as the selected method's source. If the engine's slice can't be bounded to one member, the result should be marked or rejected, not reported as `complete`. ### Impact and scope - Silent misattribution: an agent receives unrelated code labelled as the selected method's complete Java source, with no marker. In my session it would have attributed `toBitmap`/`toSvg`/`ReaderOptions` logic to the camera-frame JNI entry point. - Observed on 1 of 5 native methods in this class, deterministic across runs. - Source-derived, not runtime-verified on `main`: current `main` still sends every non-`<clinit>` method through `getMethodBodySmart` in `bridge/android/ReaJadxBridge.java:143-144`. Android changes merged after 6.4.0 (#2439, `fe8d0bdd3`, `067746887`) don't appear to touch body slicing, but I've only run 6.4.0. - The slice most likely comes from jadx-headless-mcp's `getMethodBodySmart`. Since REA presents the result as Evidence, a guard in REA (for example, rejecting or marking text that contains more than one member declaration) would protect callers regardless of the engine. ### REA version `rea-agents@6.4.0` (npm) ### Runtime and platform Linux x64 Docker container (Ubuntu 24.04) on Windows 11 / Docker Desktop. Node 24.21.0, JDK 21.0.12, `jadx-headless-mcp` 0.7.1 (sha256 `6e5eacf5…`). ### Target and artifact context Public F-Droid APK listed above. No private samples. ### Interface CLI (the MCP tool `inspect_android_method` shares the workflow; MCP not run separately for this case). ### Relevant output or evidence ```json {"class_name":"de.markusfisch.android.zxingcpp.ZxingCpp","method":{"name":"readYBuffer","overload_index":0},"overload_count":2,"representation":"java","body_status":"available","fell_back":false,"markers":[],"source":{"status":"complete","text":" public final native List<Result> readYBuffer(...);\n\n public final List<Result> readYBuffer(ByteBuffer byteBuffer, int i, Rect rect, ...) {\n ... (continues to end of class)"}} ```
Author
Owner

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

Cross-referencing with sibling issue #3443 ([Bug] inspect_android_method reports an inlined Java method as "native/abstract" with no body). Both stem from JADX method boundary and body resolution inaccuracies in inspect_android_method.

Consolidated under Cluster P (Android Method Boundary Analysis & Body Extraction) in consolidation tracker #4330.

<!-- gh-comment-id:6216047978 --> @matbrgz commented on GitHub (Oct 16, 2026): Cross-referencing with sibling issue #3443 (`[Bug] inspect_android_method reports an inlined Java method as "native/abstract" with no body`). Both stem from JADX method boundary and body resolution inaccuracies in `inspect_android_method`. Consolidated under **Cluster P (Android Method Boundary Analysis & Body Extraction)** in consolidation tracker #4330.
Author
Owner

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

Fixed in 742b6f587, adapting the guard and two tiny Smali fixtures from #3527 with balkan16 credited as coauthor.

Observed baseline: the real CLI and pinned 0.7.1 JAR returned the entire tiny NativeOverload class as the selected native method’s complete Java body. The same completed MCP baseline workflow also returned neighbouring code for an abstract method.

Structural fix: capture native/abstract body-absence facts with the existing immutable method snapshot before code generation. Those facts bypass the source slicer and produce body_status=not_available, empty source, and native_method/abstract_method markers. Concrete source and overload selection retain their path. No new protocol/schema or execution authority. The expanded real verifier is now method-bodies.mjs; its old entry point was removed and sole consumer migrated.

Validation: 72 focused tests across 3 files pass before and after rebase; static/docs checks pass. New real-JAR regression fails on b27e1a74c and passes on the candidate, including no source-generation transition for absent bodies and stable results after generating neighbouring concrete methods. Six CLI and six stdio MCP calls verify folded/mixed initializers, native, static native, abstract and concrete overloads; text/structured parity, digest, selected identity and unchanged APK bytes are asserted. A post-rebase actual CLI control passes. Two source reviews found no blockers.

Six completed real MCP cost workflows include full discovery and native/concrete/abstract inspections of a four-class authored APK. Two alternating warm repeats/build: elapsed median 3.886→3.724 s; reaped-family CPU 5.655→5.643 s; sampled process-family RSS 1.376→1.383 GB; cl100k output tokens 812278→811813.5. First-process results separate; no speedup established. Transport JSON ordering, owned snapshot paths, bridge hash and Evidence IDs vary; comparison retains normalized facts and outer Evidence, excluding raw transport/IDs and intended no-body differences. Measured bridge hash exactly matches the final rebased commit; unrelated incoming Hexagon admission was not part of the measured runtime. Costs exclude whole-agent/input tokens and true cold caches; sampled memory can miss transient/reparented processes.

Real verification used the pinned JAR SHA-256 6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75 and existing JDK 21.0.12.1 on macOS arm64. JVM heap 256 MiB, processors 2, analysis thread 1. The reported BinaryEye APK, Linux and Windows were not run; no APK code was executed. Heavy profiling/stress remained stopped.

#3443 remains separate: unavailable concrete Java must not be described as native/abstract merely from an upstream placeholder. No inlined-method fix or holder attribution is claimed here.

<!-- gh-comment-id:6236173022 --> @morluto commented on GitHub (Oct 17, 2026): Fixed in 742b6f587, adapting the guard and two tiny Smali fixtures from #3527 with balkan16 credited as coauthor. Observed baseline: the real CLI and pinned 0.7.1 JAR returned the entire tiny NativeOverload class as the selected native method’s complete Java body. The same completed MCP baseline workflow also returned neighbouring code for an abstract method. Structural fix: capture native/abstract body-absence facts with the existing immutable method snapshot before code generation. Those facts bypass the source slicer and produce body_status=not_available, empty source, and native_method/abstract_method markers. Concrete source and overload selection retain their path. No new protocol/schema or execution authority. The expanded real verifier is now method-bodies.mjs; its old entry point was removed and sole consumer migrated. Validation: 72 focused tests across 3 files pass before and after rebase; static/docs checks pass. New real-JAR regression fails on b27e1a74c and passes on the candidate, including no source-generation transition for absent bodies and stable results after generating neighbouring concrete methods. Six CLI and six stdio MCP calls verify folded/mixed initializers, native, static native, abstract and concrete overloads; text/structured parity, digest, selected identity and unchanged APK bytes are asserted. A post-rebase actual CLI control passes. Two source reviews found no blockers. Six completed real MCP cost workflows include full discovery and native/concrete/abstract inspections of a four-class authored APK. Two alternating warm repeats/build: elapsed median 3.886→3.724 s; reaped-family CPU 5.655→5.643 s; sampled process-family RSS 1.376→1.383 GB; cl100k output tokens 812278→811813.5. First-process results separate; no speedup established. Transport JSON ordering, owned snapshot paths, bridge hash and Evidence IDs vary; comparison retains normalized facts and outer Evidence, excluding raw transport/IDs and intended no-body differences. Measured bridge hash exactly matches the final rebased commit; unrelated incoming Hexagon admission was not part of the measured runtime. Costs exclude whole-agent/input tokens and true cold caches; sampled memory can miss transient/reparented processes. Real verification used the pinned JAR SHA-256 6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75 and existing JDK 21.0.12.1 on macOS arm64. JVM heap 256 MiB, processors 2, analysis thread 1. The reported BinaryEye APK, Linux and Windows were not run; no APK code was executed. Heavy profiling/stress remained stopped. #3443 remains separate: unavailable concrete Java must not be described as native/abstract merely from an upstream placeholder. No inlined-method fix or holder attribution is claimed here.
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#1300
No description provided.