Files
reverse-engineering-anything/tests/fixtures/android/jni-bindings
gensokyo-dev 570abe95f2 feat(android): link native declarations to static JNI candidates (#7472)
Link original DEX native declarations to per-ABI function exports and relocated static JNI table candidates through shared CLI/MCP workflows. Preserve global attribution ambiguity, exact provenance, partial coverage and retained Evidence; default MCP delivery to bounded summaries with filtered/batch pages.

Use one immutable ELF relocation decoder with streaming IL2CPP materialization, cooperative bounded export/version parsing and bounded JNI state. Reuse validated session inventories without loading target libraries or executing JNI code.

Validation: relevant type/lint/build and parser/domain/public CLI/MCP regressions; genuine four-ABI NDK fixtures and independent library/slot oracles; actual JDK/JADX Conscrypt workflows with retained-source removal. Full measurements and limitations are recorded in docs/verification/android-jni-bindings-2026-10-27.md.

Closes #6403.
2026-10-27 18:53:22 +08:00
..

Static JNI fixture built with the Android NDK

This fixture contains genuine Java native declarations, javac-generated JNI headers, Java_* exports and actual JNI_OnLoad/RegisterNatives source. Its DEX-only APK containers have no Android manifest, resources or signature. They are never installed, loaded or executed; static matches do not prove runtime registration, loading or invocation.

source-expectations.json is handwritten from these sources. There are twelve native declarations, including overloaded and supplementary-Unicode names, two classes with the same (ambiguous, (I)I) pair, one export in two libraries per ABI, and an intentionally unmatched declaration. data_methods has three entries in .data; relro_methods has one in .data.rel.ro. A Modified UTF-8 table name encodes U+10400 as the two UTF-16 surrogate units, unlike ordinary four-byte UTF-8.

Pinned existing tools

  • NDK r27d / 27.3.13750724, official Linux archive: https://dl.google.com/android/repository/android-ndk-r27d-linux.zip. Size 663956036 bytes; official SHA-1 22105e410cf29afcf163760cc95522b9fb981121, from Google's repository manifest. Observed archive SHA-256: 601246087a682d1944e1e16dd85bc6e49560fe8b6d61255be2829178c8ed15d9. The latter is an acquired-byte identity, not an independently published checksum.
  • Observed bundled Android clang 18.0.4, build 13691557, LLVM source d8003a456d14a3deb8054cdaa529ffbf02d9b262; genuine LLD/readelf 18.0.4.
  • Existing Temurin JDK 21.0.12.1+1, Android 36 platform JAR, and D8 8.0.40 from the checksum-pinned R8 JAR described in the neighboring mapping fixture.

Private acquisition streams the archive to disk and checks its byte count/SHA-1 before extraction. The executed selection retained about 305 MiB, admitted below 512 MiB: clang/LLD/readelf and their archive-owned relative links, host shared dependencies, Clang headers, four target builtins/libunwind.a, NDK sysroot headers, and selected target API 21/23 libraries. It omitted unrelated debuggers, samples and sanitizer runtimes. Preserve actual archive paths/modes/link targets; the host JDK's jni.h or a host ld.lld wrapper cannot substitute for the NDK. No global toolchain or configuration is changed.

Generate with explicit inputs

Run serially with two CPUs and a process-family memory supervisor. Java commands also cap their heap and active processors; LLD uses two threads. Select an output directory that does not exist. build.py does not download or install tools.

python3 tests/fixtures/android/jni-bindings/build.py \
  --ndk /absolute/private/android-ndk-r27d \
  --jdk /absolute/existing/jdk-21 \
  --r8-jar /absolute/verified/r8-8.0.40.jar \
  --android-jar /absolute/sdk/platforms/android-36/android.jar \
  --output /absolute/fresh-output-directory

The generator compiles three Java classes with javac -h, checks every expected export against those generated declarations, runs D8, and independently checks the resulting DEX ACC_NATIVE flags and exact names/prototypes. It then compiles two libraries for each of arm64-v8a, armeabi-v7a, x86_64 and x86, with three packing variants. All subprocess arguments and tool versions are recorded in the output.

APK variant Observed table relocation encoding Required interpretation
jni-ordinary-static.apk AArch64/x86-64 RELA; ARM/i686 REL implicit addends Every source-known candidate and exact slot value survives
jni-relr-static.apk Standard DT_RELR with encoded bitmaps on all four ABIs Same facts, with RELR source evidence
jni-aps2-static.apk Android packed APS2 REL/RELA on all four ABIs Explicit unsupported/partial table coverage; preserve independently readable exports

Ordinary and RELR source expectations have, across four ABIs, 32 matched, 12 ambiguous and four unmatched bindings. These are source-known outcomes. The report's matched status establishes observed candidate presence; separate partial coverage does not promise complete uniqueness. A source-known unmatched method can remain unknown when GLOB_DAT or NOBITS handling leaves incomplete coverage. APS2's expected_methods retains source-known export facts and marks unrecovered table declarations unknown. The report must retain readable export candidates and must not claim that an undecoded table is absent.

Independent evidence and limits

oracle.py imports no REA parser. For these small generated artifacts it reads known ELF symbols and source-specified table slots, applies standard relative REL/RELA/RELR records, checks the exact Modified UTF-8 bytes, and requires each function pointer to equal the compiled function symbol inside executable PT_LOAD. It is a known-slot oracle, not a general JNI table discovery implementation. It deliberately does not decode APS2. The separate official llvm-readelf transcripts establish actual sections, symbols, relocation tags and GNU_RELRO.

Generated full-expectations.json contains every ABI/library digest, export and expected declaration binding, including the independently recovered slot addresses, file offsets, raw words, relocated words and function addresses. oracle/dex.json, headers/, javap/, readelf/, commands.json and compiler logs retain the independent proof. Generated APK hashes are per-run identities; ZIP metadata can vary between runs. Keep all generated binaries, caches and receipts outside Git.

The static library checker reads the Conscrypt oracle only when the generated expectations declare that optional case. A declared but missing oracle fails verification; NDK-only expectations require only their 24 generated libraries.

Optional --conscrypt-aar and --conscrypt-original-dex-zip package the already acquired Conscrypt Android 2.8.0 original-name D8 together with the exact four native libraries from the public Apache-2.0 AAR. The source AAR SHA-256 is 752044c289bc98e2dcabc00a1f88ca158b3aa3f505b38f124552bf4b2a0902b1. The independent arm64 oracle pins its library SHA-256 and validates slots 0x2ada68, 0x2ada70, 0x2ada78, resolving EVP_PKEY_new_RSA with descriptor ([B[B[B[B[B[B[B[B)J to 0xfa11c. Its existing D8 warning about the missing platform adapter OpenSSLSocketImpl remains a limitation. This is a static container, and pairing an R8-minified DEX with unchanged original JNI strings would be a different negative case.

Public workflow checker

After building REA and selecting the existing JADX MCP JAR, the maintained public MCP checker accepts the generated full-expectations.json. Run it under the same external resource supervisor; it does not rebuild fixtures or run target code.

node tests/support/analysis-efficiency/android-jni.mjs \
  --repository /absolute/rea-checkout \
  --target /absolute/fixture-output/jni-ordinary-static.apk \
  --expectations /absolute/fixture-output/full-expectations.json \
  --variant ordinary --detail summary --proof --pages --cli \
  --java-home /absolute/existing/jdk-21 --jar /absolute/existing/jadx-mcp.jar \
  --output /absolute/fresh-receipts-directory

Use relr, aps2 or conscrypt with its corresponding APK for the other cases. Two passes distinguish a cold provider from warm reuse. Add --omit-detail with --detail summary to verify the MCP default through an actually omitted argument. --proof counts and checks complete Evidence export; --pages checks all four collections, selected fields, empty filters, native source ordinals, coverage and parent row indices; --cli checks one CLI result after MCP cleanup. Optional --retained-removal runs a separate qualification after the investigation receipts: it copies the APK exclusively into the fresh output directory, analyzes that copy, removes only that owned copy, then reads retained summary/slot/pages and exports unchanged parent Evidence. It verifies the original APK digest remains unchanged. These calls are recorded and costed separately from cold/warm investigation tasks. The optional cost reader counts catalogs, all request/reply content and proof/page calls with an existing tiktoken installation. These are estimates, not model billing. Earlier main has no equivalent static RegisterNatives workflow, so it is not an equal-coverage performance baseline.