Conclusions and decision conditions
- Record direct and transitive dependencies, loading entry points, initialization timing, minimum target APIs, and callers for every SO.
- Static JNI name discovery and RegisterNatives dynamic registration have different constraints; verify the actual binding method before processing names.
- arm64-v8a, armeabi-v7a, and x86_64 represent distinct artifacts and compatibility matrices; success on one architecture does not extrapolate to others.
- Protection strength acceptance and runtime stability acceptance must be bound to the same final APK, signature, and SO set.
Map the ELF Dependency Graph and Actual Loading Timeline First
An Android application may comprise main libraries, business logic libraries, algorithm libraries, third-party SDKs, and system libraries. Java or Kotlin code can directly invoke System.loadLibrary, while main libraries may further load other libraries via ELF dependencies or runtime loading. The ultimate crash point may lie outside the protected library, as symbol resolution and initialization order are determined by the entire dependency chain.
The dependency graph must record library files per ABI, DT_NEEDED relationships, minimum system API levels, loading entry points, initialization functions, exported interfaces, and callers. Additionally, mark whether libraries are self-developed, open-source dependencies, or closed-source SDKs, as this defines the boundaries for modification and regression testing responsibility.
Android NDK documentation states that native symbols are typically resolved during library loading. If code references an API absent on the target system, dlopen may fail immediately, even if runtime branching suggests the code path would not execute. Therefore, the NDK API floor and device system version must be included in the dependency graph.
| Field | What to Record | Impact on Hardening | Verification Method |
|---|---|---|---|
| Library & Source | Main library, transitive dependencies, third-party, and system libraries | Determines modifiable scope and regression responsibility | Unpack and verify against each ABI in the final APK |
| Loading Entry | Static dependencies, System.loadLibrary, or runtime dlopen | Determines the earliest failure stage and log location | Record application launch timeline and loading results |
| API Floor | Build APP_PLATFORM and used system symbols | Missing symbols may occur at load time if higher than device API | Real-device application launch on target systems and symbol verification |
| Initialization Order | JNI_OnLoad, constructors, and business initialization | Changes in code layout or timing may amplify implicit dependencies | Compare timelines between unprotected and release candidate builds |
| Exceptions & Threads | C++ exceptions, thread ownership, JNIEnv usage | Cross-library and cross-thread errors often cause immediate crashes | CheckJNI, symbolicated stacks, and business regression testing |
Static vs. Dynamic Registration Defines Symbol Processing Boundaries
JNI can discover native methods via naming conventions or establish mappings between Java methods and function addresses using RegisterNatives during initialization. Static registration relies on predictable exported names; dynamic registration typically requires exposing only a few initialization entry points but depends on class lookup, method signatures, registration timing, and the loading thread.
If hardening or symbol processing alters static registration names, the system may fail to locate the native implementation. Similarly, changes to method signatures, class name retention rules, or initialization order in the dynamic registration table can cause failures before the first invocation. Merely checking the count of exported symbols does not prove JNI bindings remain correct.
Official Android JNI guidelines also warn that JNIEnv has thread constraints; pending exceptions, invalid references, and cross-thread usage can lead to crashes. Post-hardening regression testing must cover real threads and exception paths, not just invoke a parameterless demo method.
| Registration Method | Dependencies | Hardening Sensitivity Points | Minimum Verification |
|---|---|---|---|
| Static Name Discovery | Java class name, method name, signature, and exported symbols | Renaming, hidden symbols, and signature mismatches | Invoke each critical entry point and check for linking errors |
| RegisterNatives | Class lookup, method signatures, registration table, and initialization timing | Class name processing, registration order, function address changes | Confirm registration completion and execute real input/output scenarios |
| Third-Party Wrappers | Internal SDK rules and closed-source implementations | Self-validation, implicit exports, and version differences | Verify against vendor support matrices and real-world scenarios |
- List key Java-to-Native mappings
- Confirm static or dynamic registration method
- Retain necessary class name and signature rules
- Verify exceptions, threads, and reference lifecycles
ABI Must Be Accepted as Independent Release Artifacts
ABI is more than a directory label. The official Android NDK documentation specifies that ABI defines the instruction set, endianness, invoking conventions, stack and register usage, executable file format, and C++ name mangling. arm64-v8a and armeabi-v7a differ in machine code and dependencies; passing on an x86_64 emulator does not represent success on ARM real devices.
For each ABI, confirm that all direct and transitive dependencies exist, that the library's API floor is compatible with the target system, and that packaging tools have not erroneously deleted files for a specific architecture. If only the main library includes arm64-v8a while a third-party dependency lacks the corresponding artifact, the application will still fail during loading.
If the product plans to support only a subset of ABIs, explicitly state this in release notes, app store configurations, and test scopes. Architectures that are unbuild, uninstalled, or where critical JNI paths were not executed must be marked as uncovered; compilation success cannot substitute for runtime evidence.
| Check Item | arm64-v8a | armeabi-v7a | x86_64 | Decision Rule |
|---|---|---|---|---|
| All Dependencies Present | Verify per library | Verify per release scope | Verify only if required | Block if any required dependency is missing |
| Loadable on Minimum System | Target API real device | Target API real device | Emulator or device | Do not validate solely on the latest system |
| Critical JNI Paths | Real input/output | Real input/output | Cannot substitute ARM results | Record independently for each released ABI |
| Crash Attributability | Retain matching symbols | Retain matching symbols | Retain matching symbols | Symbols must correspond to the same release candidate |
Which Native Runtime Conditions Does Protection Touch?
Symbol hiding reduces static clues, string processing lowers plaintext exposure, and control flow or function virtualization alters the execution form of selected code. These techniques impact exported symbols, function boundaries, exception unwinding, indirect invocations, alignment, and performance differently. Fewer symbols post-processing does not represent complete native protection, nor does the ability to read the library negate all protection effects.
Application launch initialization, signal handling, C++ exceptions, callbacks, thread-local state, reflective symbol lookup, and third-party SDK self-validation are high-sensitivity areas. When selecting scope, prioritize business functions with clear input/output, measurable function invocation frequency, and isolatable failures. Avoid processing the entire initialization chain in the first PoC.
Protection configurations should record the rationale, invocation phase, ABI, dependencies, performance budget, and rollback group for each function or function group. The following demonstrates only the format of a public, safe verification checklist, excluding internal symbols or product configuration syntax.
native_group: protocol-core-v1
abis: [arm64-v8a, armeabi-v7a]
load_phase: post-authentication
jni_registration: dynamic
execution:
frequency: bounded
main_thread: false
dependencies:
- business-runtime
- system-crypto
acceptance:
- dependency-resolution
- jni-registration
- exception-path
- output-parity
- latency-budget
rollback: native-group-v0Locate Crashes Post-SO Hardening Using the Earliest Difference
When troubleshooting, first fix the file identity, signature, device, system, installation method, and business input for both the unprotected baseline and the release candidate. Then, search the timeline for the earliest difference: process creation, Application start, library loading, JNI_OnLoad, registration completion, first native invocation, or key business return. The last crash log entry may merely be a chain reaction.
UnsatisfiedLinkError usually points to missing libraries, ABI mismatches, dependency issues, or symbol resolution problems; registration errors may manifest as missing native methods; incorrect JNIEnv, references, or pending exceptions may crash during business invocations. Retain native symbols matching the release candidate for internal attribution, but do not expose real symbols or addresses in public reports.
CheckJNI can help detect some JNI misuse, but it is a diagnostic tool, not a representation of production runtime state, and cannot cover all native memory and concurrency issues. After fixes, you must re-run critical paths under the target release configuration.
| Earliest Symptom | Priority Checks | Required Evidence | What Not to Do First |
|---|---|---|---|
| dlopen Failure | ABI, DT_NEEDED, API floor, and symbols | Library list and loading errors matching the release candidate | Repeatedly toggling unrelated protection switches |
| Native Method Not Found | Registration method, class name, method signature, and timing | Registration table, retention rules, and invocation entry points | Looking only at exported symbol counts |
| Crash on First Invocation | Parameters, references, threads, exceptions, and function processing | Symbolicated stack, inputs, and baseline comparison | Explaining with symbol files from another version |
| Failure on Some Devices | System API, vendor differences, ABI, and third-party libraries | Device system matrix and earliest differences | Substituting real-device conclusions with emulator success |
Release Conclusions Must Cover Strength, Compatibility, and Maintainability
Static strength observation can record changes in symbols, strings, code structure, and the exposure surface of key entry points; runtime acceptance verifies loading, JNI, critical business logic, exceptions, and target ABIs. These two types of evidence complement each other; neither can replace the other.
The final release candidate must also complete installation upgrades, signature verification, channel checks, application launch tests, crash monitoring, native symbol archiving, and rollback drills. Symbol archives must allow finding the matching version via file identity; otherwise, online native crashes cannot be correctly symbolicated.
This article provides an engineering inspection framework, not test conclusions for a specific SO or protection configuration. Without a target package, target ABI, and real invocation paths, one can only assess whether preparation work is complete, not promise compatibility or performance.
- Every released ABI has runtime evidence
- Critical JNI entry points cover normal and exception paths
- Native symbols match the final candidate identity
- Loading, business, and rollback results are reproducible
- Uncovered devices and systems are explicitly restricted
Evidence and applicability boundaries
This section separates documented platform facts, engineering judgment, and limits that cannot be generalized into unverified product claims.
| Article judgment | Fact or engineering basis | Applicability limit |
|---|---|---|
| ABI must be accepted as an independent artifact dimension. | Official Android NDK defines ABI covering instruction sets, invoking conventions, registers, stack, ELF format, and name mangling. | Documentation definitions do not prove the application includes complete dependencies or runs successfully on target devices. |
| Native API references higher than the device system may fail at load time. | NDK common problems documentation states symbols are typically resolved at library load time; referencing non-existent APIs cannot be bypassed by runtime branching. | Specific failures must still be confirmed via build parameters, dependencies, and device logs. |
| JNI errors require validation at the thread, reference, and exception levels. | Android JNI guidelines list issues such as invalid JNIEnv, references, pending exceptions, and method registration that can cause crashes. | CheckJNI only assists in detecting a subset of issues and cannot replace comprehensive business and memory safety testing. |
| Results from a single x86_64 emulator cannot be extrapolated to ARM real devices. | Different ABIs use different instruction sets and invoking conventions; final library and dependency combinations may also differ. | If the product explicitly does not release a certain ABI, it can be marked as not applicable rather than forcibly tested. |
| Protection strength and compatibility must be closed on the same release candidate. | Rebuilding or replacing SO files changes code, symbols, dependencies, and potential runtime behavior. | This is an artifact governance principle and does not imply any specific protection level has passed. |
Engineering questions
If there is only one main SO, is a dependency graph still needed?
Yes. The main SO may still depend on system libraries, C++ runtimes, or third-party libraries and interact with the Java layer via JNI. The dependency graph confirms the earliest loading point and responsibility boundaries.
If arm64-v8a passes, do I still need to test armeabi-v7a?
If the release package includes and supports armeabi-v7a, independent verification is required. The instructions, invoking conventions, and dependency artifacts for the two ABIs differ and cannot substitute for each other.
Is hiding all exported symbols safer?
Not necessarily. Necessary static JNI entry points, third-party invocations, and system conventions may rely on exported symbols. Confirm registration and invocation methods first, then minimize exposure.
Can I release if CheckJNI reports no errors?
No, this alone is insufficient for release. You must also verify the actual target configuration, business inputs, exception paths, ABI, system scope, installation upgrades, and rollback capabilities.
Should I disable all protection immediately after an SO hardening crash?
First fix the candidate identity and find the earliest difference. You can binary search protection groups or roll back, but change only one recordable variable at a time to avoid generating new unattributable artifacts.
Want to test this on your own app?
Submit the release candidate, target systems, and critical business paths for a Yudun PoC and compatibility assessment.
Continue with: SO hardening and native compatibility checklist