# LSPosed A16 Fix — JNINativeInterface Runtime Size Mismatch ## Summary LSPosed (JingMatrix fork v1.11.0 / Vector v2.0) crashes zygote64 on Android 16 with: ``` JNI DETECTED ERROR IN APPLICATION: negative buffer capacity in NewDirectByteBuffer from int com.android.internal.os.Zygote.nativeForkSystemServer ``` Magisk detects repeated zygote crashes and auto-rolls-back Zygisk. Modules never inject. ## Root Cause `zygisk/src/main/cpp/ipc_bridge.cpp` line 557: ```cpp memcpy(&native_interface_hook_, env->functions, sizeof(JNINativeInterface)); ``` **`sizeof(JNINativeInterface)` is a compile-time constant from the NDK used to build LSPosed (NDK 26 = ~2376 bytes).** Android 16 ART added new JNI functions to the `JNINativeInterface` structure (e.g. `NewDirectByteBuffer` overloads). The runtime structure is larger than the compile-time size. The `memcpy` truncates the table — function pointers for new JNI functions beyond the compile-time boundary become uninitialized garbage values. ART later calls them → crash. **Key detail:** `reserved0` in `JNINativeInterface` stores the actual runtime size (set by ART). The truncated copy preserves `reserved0` (it's at offset 0), so ART's `SetTableOverride` sees the correct size and treats garbage as valid function pointers. ## Fix Replace compile-time `sizeof(JNINativeInterface)` with runtime `reserved0`, and dynamically allocate the copy. ### File 1: `zygisk/src/main/cpp/include/ipc_bridge.h` ```diff - JNINativeInterface native_interface_hook_{}; + JNINativeInterface* native_interface_hook_ = nullptr; + size_t table_size_ = 0; ``` ### File 2: `zygisk/src/main/cpp/ipc_bridge.cpp` ```diff - memcpy(&native_interface_hook_, env->functions, sizeof(JNINativeInterface)); + table_size_ = (size_t)env->functions->reserved0; + if (table_size_ == 0 || table_size_ < sizeof(JNINativeInterface)) + table_size_ = sizeof(JNINativeInterface); + native_interface_hook_ = (JNINativeInterface*)malloc(table_size_); + memcpy(native_interface_hook_, env->functions, table_size_); - call_boolean_method_v_backup_ = env->functions->CallBooleanMethodV; + call_boolean_method_v_backup_ = native_interface_hook_->CallBooleanMethodV; - native_interface_hook_.CallBooleanMethodV = ... + native_interface_hook_->CallBooleanMethodV = ... - set_table_override_func(&native_interface_hook_); + set_table_override_func(native_interface_hook_); ``` ## How to Build ```bash # 1. Clone git clone --depth 1 --recursive https://github.com/JingMatrix/LSPosed.git cd LSPosed # 2. Apply fix bash apply_a16_fix.sh . # 3. Build (requires Android SDK + NDK) export ANDROID_HOME=/path/to/android-sdk ./gradlew :zygisk:assembleRelease # Output: zygisk/build/outputs/apk/release/ ``` ## How to Deploy on Device ```bash # Extract the fixed zygisk .so from the APK unzip -o zygisk-release.apk -d zygisk_out # Push to device and replace adb push zygisk_out/lib/arm64-v8a/libzygisk.so /data/local/tmp/ adb shell su -c 'cp /data/local/tmp/libzygisk.so /data/adb/modules/zygisk_vector/zygisk/arm64-v8a.so' # Ensure Zygisk is enabled adb shell su -c 'magisk --sqlite "INSERT OR REPLACE INTO settings (key,value) VALUES (\"zygisk\",1);"' # Reboot adb reboot ``` ## Verification (after reboot) ```bash # 1. Daemon running? adb shell su -c 'pidof lspd' # 2. Zygisk injected into zygote? adb shell su -c 'cat /proc/$(pidof zygote64 | awk "{print \$1}")/maps | grep zygisk' | head -3 # 3. Module injected into target app? adb shell su -c 'cat /proc/$(pidof com.xingin.xhs)/maps | grep xhshook' ``` ## Background Full reverse engineering session: Hermes Agent on Pixel 10 Pro Fold, Android 16, XHS 9.35.1. **Campaign goal:** MITM capture `xy-common-params` (ECFAAF/ECFAAF01 signatures) from edith.xiaohongshu.com. **Confirmed:** edith uses Google Mainline Cronet (`libmainlinecronet.141.0.7340.3.so`, 6MB) with static BoringSSL. Frida Interceptor cannot hook static-linked symbols. Only viable bypass is LSPosed CertificatePinner hook → MITM proxy capture. **Other achievements from the session:** - Java TrustManager bypass works for non-edith domains (crash/as/t2/mediacloud) - sentry abort Interceptor works (Frida survives 35+ seconds) - `--pause` mode confirms edith goes through TrustManager (timing race is the issue) - xhshook module pre-built at `/data/adb/modules/xhshook/xhshook.apk`