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.

Minimum Fields for a Native Dependency Graph
FieldWhat to RecordImpact on HardeningVerification Method
Library & SourceMain library, transitive dependencies, third-party, and system librariesDetermines modifiable scope and regression responsibilityUnpack and verify against each ABI in the final APK
Loading EntryStatic dependencies, System.loadLibrary, or runtime dlopenDetermines the earliest failure stage and log locationRecord application launch timeline and loading results
API FloorBuild APP_PLATFORM and used system symbolsMissing symbols may occur at load time if higher than device APIReal-device application launch on target systems and symbol verification
Initialization OrderJNI_OnLoad, constructors, and business initializationChanges in code layout or timing may amplify implicit dependenciesCompare timelines between unprotected and release candidate builds
Exceptions & ThreadsC++ exceptions, thread ownership, JNIEnv usageCross-library and cross-thread errors often cause immediate crashesCheckJNI, 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.

Key Checkpoints for Two JNI Registration Methods
Registration MethodDependenciesHardening Sensitivity PointsMinimum Verification
Static Name DiscoveryJava class name, method name, signature, and exported symbolsRenaming, hidden symbols, and signature mismatchesInvoke each critical entry point and check for linking errors
RegisterNativesClass lookup, method signatures, registration table, and initialization timingClass name processing, registration order, function address changesConfirm registration completion and execute real input/output scenarios
Third-Party WrappersInternal SDK rules and closed-source implementationsSelf-validation, implicit exports, and version differencesVerify 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.

Example ABI Acceptance Matrix
Check Itemarm64-v8aarmeabi-v7ax86_64Decision Rule
All Dependencies PresentVerify per libraryVerify per release scopeVerify only if requiredBlock if any required dependency is missing
Loadable on Minimum SystemTarget API real deviceTarget API real deviceEmulator or deviceDo not validate solely on the latest system
Critical JNI PathsReal input/outputReal input/outputCannot substitute ARM resultsRecord independently for each released ABI
Crash AttributabilityRetain matching symbolsRetain matching symbolsRetain matching symbolsSymbols 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.

Public Safe Record Format for Native Protection Groups
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-v0

Locate 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.

Common Symptoms and Next Evidence Steps
Earliest SymptomPriority ChecksRequired EvidenceWhat Not to Do First
dlopen FailureABI, DT_NEEDED, API floor, and symbolsLibrary list and loading errors matching the release candidateRepeatedly toggling unrelated protection switches
Native Method Not FoundRegistration method, class name, method signature, and timingRegistration table, retention rules, and invocation entry pointsLooking only at exported symbol counts
Crash on First InvocationParameters, references, threads, exceptions, and function processingSymbolicated stack, inputs, and baseline comparisonExplaining with symbol files from another version
Failure on Some DevicesSystem API, vendor differences, ABI, and third-party librariesDevice system matrix and earliest differencesSubstituting 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 judgmentFact or engineering basisApplicability 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