├── .github └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── build.sh └── src ├── main.cpp └── main.h /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build libndk_fixer 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | with: 9 | submodules: true 10 | - name: install cmake 11 | run: | 12 | sudo apt update -qq 13 | sudo apt install -y cmake 14 | - uses: nttld/setup-ndk@v1 15 | id: setup-ndk 16 | with: 17 | ndk-version: r25c 18 | add-to-path: false 19 | - run: ./build.sh 20 | env: 21 | ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} 22 | - uses: actions/upload-artifact@v4 23 | with: 24 | name: lib 25 | path: build/libndk_fixer.so -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */libs/ 2 | */obj/ 3 | .vscode/ 4 | build/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dobby"] 2 | path = dobby 3 | url = https://github.com/jmpews/dobby.git 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.22) 2 | 3 | if (NOT EXISTS ${CMAKE_BINARY_DIR}/CMakeCache.txt) 4 | if (NOT CMAKE_BUILD_TYPE) 5 | set(CMAKE_BUILD_TYPE "Release" CACHE STRING "" FORCE) 6 | endif() 7 | endif() 8 | 9 | set(ANDROID_PLATFORM 24) 10 | set(ANDROID_ABI x86_64) 11 | set(ANDROID_STL c++_static) 12 | set(ANDROID_CPP_FEATURES, "no-rtti no-exceptions") 13 | set(CMAKE_TOOLCHAIN_FILE $ENV{ANDROID_NDK_HOME}/build/cmake/android.toolchain.cmake) 14 | 15 | project(ndk_fixer) 16 | 17 | set(CMAKE_SYSTEM_NAME Android) 18 | set(CMAKE_SYSTEM_VERSION 24) 19 | set(CMAKE_ANDROID_ARCH_ABI x86_64) 20 | set(CMAKE_ANDROID_NDK $ENV{ANDROID_NDK_HOME}) 21 | set(CMAKE_ANDROID_NDK_TOOLCHAIN_VERSION clang) 22 | set(CMAKE_ANDROID_STL_TYPE c++_static) 23 | set(CMAKE_CXX_STANDARD 20) 24 | 25 | if(NOT TARGET dobby) 26 | set(DOBBY_DIR dobby) 27 | macro(SET_OPTION option value) 28 | set(${option} ${value} CACHE INTERNAL "" FORCE) 29 | endmacro() 30 | SET_OPTION(DOBBY_DEBUG OFF) 31 | SET_OPTION(DOBBY_GENERATE_SHARED OFF) 32 | add_subdirectory(${DOBBY_DIR} dobby) 33 | get_property(DOBBY_INCLUDE_DIRECTORIES 34 | TARGET dobby 35 | PROPERTY INCLUDE_DIRECTORIES) 36 | include_directories( 37 | . 38 | ${DOBBY_INCLUDE_DIRECTORIES} 39 | $ 40 | ) 41 | endif() 42 | 43 | add_library(ndk_fixer SHARED src/main.cpp) 44 | target_link_libraries(ndk_fixer log dobby_static) 45 | add_custom_command( 46 | TARGET "ndk_fixer" POST_BUILD 47 | DEPENDS "ndk_fixer" 48 | COMMAND $<$:${CMAKE_STRIP}> 49 | ARGS --strip-all $ 50 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libndk_fixer 2 | This fixes the roblox app crashing inside Waydroid when using libndk_translation. 3 | 4 | 5 | ## How to install 6 | - Build yourself or download a precompiled binary (click [here](https://nightly.link/Slappy826/libndk-fixer/workflows/build/master/lib.zip) to download the latest) from ci. 7 | - Ensure you have libndk installed, if not you can install it using [waydroid_script](https://github.com/casualsnek/waydroid_script) 8 | - Edit `/var/lib/waydroid/waydroid_base.prop` 9 | - Find the line that says `ro.dalvik.vm.native.bridge=libndk_translation.so` and replace the `translation` with `fixer` 10 | - Copy the `libndk_fixer.so` file to this directory `/var/lib/waydroid/overlay/system/lib64/` 11 | - Finished! The app should start normally now. 12 | 13 | 14 | #### Tested Distributions 15 | - Ubuntu 23.04 and later 16 | - Nobara 39 and later 17 | #### 18 | (the above list is not exhaustive and just something me and another person tested with personally) 19 | 20 | ## Building yourself 21 | - Ensure you have cmake and the [android ndk](https://developer.android.com/ndk/downloads) on your system and the path is stored in the `ANDROID_NDK_HOME` environment variable. 22 | - Clone the repository with `--recursive` to pull the dobby submodule. 23 | - Run build.sh 24 | - The resulting built file `libndk_fixer.so` will be in the build directory. 25 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | rm -rf build 4 | mkdir build 5 | cd build 6 | cmake .. 7 | make -j$(nproc) 8 | cd .. -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "main.h" 2 | 3 | std::once_flag copy_functions_jvm{}, 4 | copy_functions_jenv{}; 5 | std::vector known_envs{}; 6 | std::vector known_vms{}; 7 | unsigned long long jvm_vtable_copy[sizeof(JNIInvokeInterface) / 8]{}, 8 | jenv_vtable_copy[sizeof(JNINativeInterface) / 8]{}; 9 | std::unordered_map redirection_cache{}; 10 | std::atomic allow_call{}; 11 | bridge_class* orig_native_bridge = nullptr; 12 | struct sigaction previous_segv; 13 | 14 | // While probably not needed, ensures the symbol doesn't get mangled. 15 | extern "C" 16 | { 17 | bridge_class NativeBridgeItf 18 | { 19 | .version = 6, 20 | }; 21 | } 22 | 23 | void segv_handler(int signum, siginfo_t *p_act, void* context) 24 | { 25 | sigcontext* ctx = reinterpret_cast(&(((ucontext_t*)context)->uc_mcontext)); 26 | uintptr_t fault_address = ctx->rip; 27 | 28 | // This works by checking if the value held in RDI is a JavaVM or JNIEnv pointer 29 | // Which lets us know if the violation happened while attempting to call a jni function 30 | // And then can redirect it to the real function. 31 | // Redirection is needed as roblox overwrites the function pointers in the functions table 32 | // and ndk_translation does not copy the values when converting the pointer to a guest value 33 | // so it attempts to call into roblox's arm code, causing a crash. 34 | 35 | if (redirection_cache.count(fault_address)) 36 | { 37 | ctx->rip = redirection_cache[fault_address]; 38 | return; 39 | } 40 | else if (std::find(known_envs.begin(), known_envs.end(), reinterpret_cast(ctx->rdi)) != known_envs.end()) 41 | { 42 | auto functions = reinterpret_cast(reinterpret_cast(ctx->rdi)->functions); 43 | 44 | auto offset = 0; 45 | for (;; offset++) 46 | if (functions[offset] == ctx->rip) 47 | break; 48 | 49 | ctx->rip = jenv_vtable_copy[offset]; 50 | redirection_cache[fault_address] = jenv_vtable_copy[offset]; 51 | return; 52 | } 53 | else if (std::find(known_vms.begin(), known_vms.end(), reinterpret_cast(ctx->rdi)) != known_vms.end()) 54 | { 55 | auto functions = reinterpret_cast(reinterpret_cast(ctx->rdi)->functions); 56 | 57 | auto offset = 0; 58 | for (;; offset++) 59 | if (functions[offset] == ctx->rip) 60 | break; 61 | 62 | ctx->rip = jvm_vtable_copy[offset]; 63 | redirection_cache[fault_address] = jvm_vtable_copy[offset]; 64 | return; 65 | } 66 | else 67 | { 68 | dprint("failed to match env"); 69 | } 70 | 71 | // If we don't match anything continue to the original handler, therefore crashing the app. 72 | if (previous_segv.sa_flags & SA_SIGINFO) 73 | previous_segv.sa_sigaction(signum, p_act, context); 74 | else 75 | previous_segv.sa_handler(signum); 76 | } 77 | 78 | HOOK_DEF(int, sigaction, int __signal, const struct sigaction* __new_action, struct sigaction* __old_action) 79 | { 80 | if (__signal == SIGSEGV) 81 | { 82 | if (allow_call) 83 | { 84 | // If flag is set, then allow the call. 85 | allow_call = false; 86 | return orig_sigaction(__signal, __new_action, __old_action); 87 | } 88 | else 89 | { 90 | // This is where it prevents crashpad from overwriting the handler. 91 | dprint("something attempted to overwrite our handler"); 92 | return 0; 93 | } 94 | } 95 | 96 | return orig_sigaction(__signal, __new_action, __old_action); 97 | } 98 | 99 | HOOK_DEF(JavaVM*, to_guest_jvm, JavaVM* jvm) 100 | { 101 | std::call_once(copy_functions_jvm, [&]() 102 | { 103 | auto jvm_functions = reinterpret_cast(jvm->functions); 104 | for (auto i = 0; i < sizeof(JNIInvokeInterface) / 8; i++) 105 | jvm_vtable_copy[i] = jvm_functions[i]; 106 | 107 | dprint("copied jvm functions"); 108 | }); 109 | 110 | if (std::find(known_vms.begin(), known_vms.end(), jvm) == known_vms.end()) 111 | { 112 | // Found a new vm pointer, log it. 113 | known_vms.push_back(jvm); 114 | dprint("got jvm: %p", jvm); 115 | } 116 | 117 | return orig_to_guest_jvm(jvm); 118 | } 119 | 120 | HOOK_DEF(JNIEnv*, to_guest_jenv, JNIEnv* env) 121 | { 122 | std::call_once(copy_functions_jenv, [&]() 123 | { 124 | auto jenv_functions = reinterpret_cast(env->functions); 125 | for (auto i = 0; i < sizeof(JNINativeInterface) / 8; i++) 126 | jenv_vtable_copy[i] = jenv_functions[i]; 127 | 128 | dprint("copied jenv functions"); 129 | }); 130 | 131 | if (std::find(known_envs.begin(), known_envs.end(), env) == known_envs.end()) 132 | { 133 | // Found a new environment pointer, log it. 134 | known_envs.push_back(env); 135 | dprint("got env: %p", env); 136 | } 137 | 138 | return orig_to_guest_jenv(env); 139 | } 140 | 141 | void* hook_loadLibraryExt(const char* lib_path, int flag, void* ns) 142 | { 143 | if (strstr(lib_path, "libroblox.so")) 144 | { 145 | dprint("detected roblox"); 146 | 147 | auto to_guest_jvm = DobbySymbolResolver("libndk_translation.so", "_ZN15ndk_translation13ToGuestJavaVMEPv"); 148 | auto to_guest_jenv = DobbySymbolResolver("libndk_translation.so", "_ZN15ndk_translation13ToGuestJNIEnvEPv"); 149 | 150 | // Hook both of these functions to intercept every JNIEnv and JavaVM objects the roblox native library has access to 151 | DobbyHook(to_guest_jvm, (void*)&new_to_guest_jvm, (void**)&orig_to_guest_jvm); 152 | DobbyHook(to_guest_jenv, (void*)&new_to_guest_jenv, (void**)&orig_to_guest_jenv); 153 | 154 | void *libc_sigaction = DobbySymbolResolver("libc.so", "sigaction"); 155 | if (libc_sigaction) 156 | { 157 | // Hook sigaction to prevent crashpad in roblox from overwriting our handler 158 | // Don't stricly need to do this as I could just make it register after crashpad registers it's handler, but I am lazy. 159 | DobbyHook((void*)libc_sigaction, (void *) new_sigaction, (void **) &orig_sigaction); 160 | dprint("set sigaction hook"); 161 | } 162 | 163 | dprint("setting sigsegv signal"); 164 | 165 | // Register our segment violation handler 166 | struct sigaction sa; 167 | memset(&sa, 0, sizeof(sa)); 168 | sa.sa_flags = SA_SIGINFO; 169 | sigfillset(&sa.sa_mask); 170 | sa.sa_sigaction = segv_handler; 171 | 172 | allow_call = true; 173 | sigaction(SIGSEGV, &sa, &previous_segv); 174 | } 175 | 176 | // Return back to the original function 177 | return orig_native_bridge->loadLibraryExt(lib_path, flag, ns); 178 | } 179 | 180 | void __attribute__ ((constructor)) setup() 181 | { 182 | dprint("setup called"); 183 | 184 | // Loads original ndk_translation into memory 185 | void* handle = dlopen("libndk_translation.so", RTLD_LAZY); 186 | if (handle != nullptr) 187 | { 188 | orig_native_bridge = reinterpret_cast(dlsym(handle, "NativeBridgeItf")); 189 | 190 | // Copies all the original function pointers to our struct so everything will function normally 191 | memcpy(&NativeBridgeItf, orig_native_bridge, sizeof(bridge_class)); 192 | 193 | // We need to override the load library function to detect when roblox is loaded inside the app 194 | NativeBridgeItf.loadLibraryExt = &hook_loadLibraryExt; 195 | 196 | dprint("loaded libndk_translation and set functions"); 197 | } 198 | } -------------------------------------------------------------------------------- /src/main.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #define dprint(...) __android_log_print(ANDROID_LOG_DEBUG, "libndk-fix", __VA_ARGS__) 13 | #define HOOK_DEF(ret, func, ...) \ 14 | ret (*orig_##func)(__VA_ARGS__); \ 15 | ret new_##func(__VA_ARGS__) 16 | 17 | struct bridge_class 18 | { 19 | uint32_t version; 20 | uint8_t _pad1[104]; 21 | void* (*loadLibraryExt)(const char* lib_path, int flag, void* ns); 22 | uint8_t _pad2[28]; 23 | }; --------------------------------------------------------------------------------