commit 4fd268c4dc9131d83648701d9a524a604f666c53
parent 5563418d79596e7185a47fb51f65f14f9828d8d2
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Thu, 25 Jan 2024 23:26:54 +0100
feat(native): database lock
- refactor native
Diffstat:
11 files changed, 279 insertions(+), 160 deletions(-)
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt
@@ -183,7 +183,7 @@ class SnapEnhance {
val libName = param.arg<String>(2)
if (libName != "client") return@hook
unhook()
- appContext.native.initOnce(appContext.androidContext.classLoader)
+ appContext.native.initOnce()
appContext.native.nativeUnaryCallCallback = { request ->
appContext.event.post(NativeUnaryCallEvent(request.uri, request.buffer)) {
request.buffer = buffer
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt
@@ -17,6 +17,7 @@ import me.rhunk.snapenhance.common.util.ktx.getStringOrNull
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
import me.rhunk.snapenhance.core.ModContext
import me.rhunk.snapenhance.core.manager.Manager
+import me.rhunk.snapenhance.nativelib.NativeLib
enum class DatabaseType(
@@ -59,9 +60,15 @@ class DatabaseAccess(
}
- private inline fun <T> SQLiteDatabase.performOperation(crossinline query: SQLiteDatabase.() -> T?): T? {
+ private fun <T> SQLiteDatabase.performOperation(query: SQLiteDatabase.() -> T?): T? {
return runCatching {
- synchronized(this) {
+ if (NativeLib.initialized && openedDatabases[DatabaseType.ARROYO] == this) {
+ var result: T? = null
+ context.native.lockNativeDatabase(DatabaseType.ARROYO.fileName) {
+ result = query()
+ }
+ result
+ } else synchronized(this) {
query()
}
}.onFailure {
diff --git a/native/jni/src/common.h b/native/jni/src/common.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <stdint.h>
+#include "util.h"
+
+#ifdef __aarch64__
+#define ARM64 true
+#else
+#define ARM64 false
+#endif
+
+typedef struct {
+ bool disable_bitmoji;
+ bool disable_metrics;
+ bool hook_asset_open;
+} native_config_t;
+
+namespace common {
+ static JavaVM *java_vm;
+ static jobject native_lib_object;
+
+ static util::module_info_t client_module;
+ static native_config_t *native_config = new native_config_t;
+}
diff --git a/native/jni/src/config.h b/native/jni/src/config.h
@@ -1,7 +0,0 @@
-#pragma once
-
-typedef struct {
- bool disable_bitmoji;
- bool disable_metrics;
- bool hook_asset_open;
-} native_config_t;-
\ No newline at end of file
diff --git a/native/jni/src/grpc.h b/native/jni/src/grpc.h
@@ -1,16 +0,0 @@
-#pragma once
-
-namespace grpc {
- typedef struct {
- void* ref_counter;
- size_t length;
- uint8_t* data;
- } ref_counted_slice_byte_buffer;
-
- typedef struct {
- void* reserved;
- void* type;
- void* compression;
- ref_counted_slice_byte_buffer *slice_buffer;
- } grpc_byte_buffer;
-}
diff --git a/native/jni/src/hooks/asset_hook.h b/native/jni/src/hooks/asset_hook.h
@@ -0,0 +1,25 @@
+#pragma once
+
+
+namespace AssetHook {
+ jmethodID native_lib_on_asset_load;
+ AAsset* (*AAssetManager_open_original)(AAssetManager*, const char*, int) = nullptr;
+
+ AAsset* AAssetManager_open_hook(AAssetManager* mgr, const char* filename, int mode) {
+ if (common::native_config->hook_asset_open) {
+ JNIEnv *env = nullptr;
+ common::java_vm->GetEnv((void **)&env, JNI_VERSION_1_6);
+
+ if (!env->CallBooleanMethod(common::native_lib_object, native_lib_on_asset_load, env->NewStringUTF(filename))) {
+ return nullptr;
+ }
+ }
+
+ return AAssetManager_open_original(mgr, filename, mode);
+ }
+
+ void init(JNIEnv *env) {
+ native_lib_on_asset_load = env->GetMethodID(env->GetObjectClass(common::native_lib_object), "shouldLoadAsset", "(Ljava/lang/String;)Z");
+ DobbyHook((void *) AAssetManager_open, (void *) AAssetManager_open_hook, (void **) &AAssetManager_open_original);
+ }
+}+
\ No newline at end of file
diff --git a/native/jni/src/hooks/fstat_hook.h b/native/jni/src/hooks/fstat_hook.h
@@ -0,0 +1,29 @@
+#pragma once
+
+namespace FstatHook {
+ auto fstat_original = (int (*)(int, struct stat *)) nullptr;
+
+ int fstat_hook(int fd, struct stat *buf) {
+ char name[256];
+ memset(name, 0, sizeof(name));
+ snprintf(name, sizeof(name), "/proc/self/fd/%d", fd);
+ readlink(name, name, sizeof(name));
+
+ std::string fileName(name);
+
+ if (common::native_config->disable_metrics && fileName.find("files/blizzardv2/queues") != std::string::npos) {
+ unlink(name);
+ return -1;
+ }
+
+ if (common::native_config->disable_bitmoji && fileName.find("com.snap.file_manager_4_SCContent") != std::string::npos) {
+ return -1;
+ }
+
+ return fstat_original(fd, buf);
+ }
+
+ void init() {
+ DobbyHook((void *)DobbySymbolResolver("libc.so", "fstat"), (void *)fstat_hook, (void **)&fstat_original);
+ }
+}+
\ No newline at end of file
diff --git a/native/jni/src/hooks/sqlite_mutex.h b/native/jni/src/hooks/sqlite_mutex.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include <map>
+
+namespace SqliteMutexHook {
+ typedef struct {
+ pthread_mutex_t mutex;
+ } sqlite3_mutex;
+
+ typedef struct {
+ void* pVfs;
+ void* pVdbe;
+ void* pDfltColl;
+ sqlite3_mutex* mutex;
+ } sqlite3;
+
+ static std::map<std::string, sqlite3_mutex *> mutex_map = {};
+ static int (*sqlite3_open_original)(const char *, sqlite3 **, unsigned int, const char *) = nullptr;
+
+ int sqlite3_open_hook(const char *filename, sqlite3 **ppDb, unsigned int flags, const char *zVfs) {
+ auto result = sqlite3_open_original(filename, ppDb, flags, zVfs);
+ if (result == 0) {
+ auto mutex = (*ppDb)->mutex;
+ if (mutex == nullptr) return result;
+
+ auto last_slash = strrchr(filename, '/') + 1;
+ if (last_slash > filename) {
+ LOGD("sqlite3_open_hook: %s", last_slash);
+ mutex_map[last_slash] = mutex;
+ }
+ }
+ return result;
+ }
+
+ void init() {
+ auto open_database_sig = util::find_signature(
+ common::client_module.base, common::client_module.size,
+ ARM64 ? "FF FF 00 A9 3F 00 00 F9" : "8D B0 06 46 E7 48",
+ ARM64 ? -0x3C : -0x7
+ );
+ if (open_database_sig == 0) {
+ LOGE("sqlite3 openDatabase sig not found");
+ return;
+ }
+ DobbyHook((void *) open_database_sig, (void *) sqlite3_open_hook, (void **) &sqlite3_open_original);
+ }
+}+
\ No newline at end of file
diff --git a/native/jni/src/hooks/unary_call.h b/native/jni/src/hooks/unary_call.h
@@ -0,0 +1,88 @@
+#pragma once
+
+#include "../common.h"
+#include "../util.h"
+
+namespace UnaryCallHook {
+ namespace grpc {
+ typedef struct {
+ void* ref_counter;
+ size_t length;
+ uint8_t* data;
+ } ref_counted_slice_byte_buffer;
+
+ typedef struct {
+ void* reserved;
+ void* type;
+ void* compression;
+ ref_counted_slice_byte_buffer *slice_buffer;
+ } grpc_byte_buffer;
+ }
+
+ static void *(*unaryCall_original)(void *, const char *, grpc::grpc_byte_buffer **, void *, void *, void *);
+ static jmethodID native_lib_on_unary_call_method;
+
+ void *unaryCall_hook(void *unk1, const char *uri, grpc::grpc_byte_buffer **buffer_ptr, void *unk4, void *unk5, void *unk6) {
+ // request without reference counter can be hooked using xposed ig
+ auto slice_buffer = (*buffer_ptr)->slice_buffer;
+ if (slice_buffer->ref_counter == 0) {
+ return unaryCall_original(unk1, uri, buffer_ptr, unk4, unk5, unk6);
+ }
+
+ JNIEnv *env = nullptr;
+ common::java_vm->GetEnv((void **)&env, JNI_VERSION_1_6);
+
+ auto jni_buffer_array = env->NewByteArray(slice_buffer->length);
+ env->SetByteArrayRegion(jni_buffer_array, 0, slice_buffer->length, (jbyte *)slice_buffer->data);
+
+ auto native_request_data_object = env->CallObjectMethod(common::native_lib_object, native_lib_on_unary_call_method, env->NewStringUTF(uri), jni_buffer_array);
+
+ if (native_request_data_object != nullptr) {
+ auto native_request_data_class = env->GetObjectClass(native_request_data_object);
+ auto is_canceled = env->GetBooleanField(native_request_data_object, env->GetFieldID(native_request_data_class, "canceled", "Z"));
+
+ if (is_canceled) {
+ LOGD("canceled request for %s", uri);
+ return nullptr;
+ }
+
+ auto new_buffer = (jbyteArray)env->GetObjectField(native_request_data_object, env->GetFieldID(native_request_data_class, "buffer", "[B"));
+ auto new_buffer_length = env->GetArrayLength(new_buffer);
+ auto new_buffer_data = env->GetByteArrayElements(new_buffer, nullptr);
+
+ //we need to allocate a new ref_counter struct and copy the old ref_counter and the new_buffer to it
+ const static auto ref_counter_struct_size = (uintptr_t)slice_buffer->data - (uintptr_t)slice_buffer->ref_counter;
+
+ auto new_ref_counter = malloc(ref_counter_struct_size + new_buffer_length);
+ //copy the old ref_counter and the native_request_data_object
+ memcpy(new_ref_counter, slice_buffer->ref_counter, ref_counter_struct_size);
+ memcpy((void *)((uintptr_t)new_ref_counter + ref_counter_struct_size), new_buffer_data, new_buffer_length);
+
+ //free the old ref_counter
+ free(slice_buffer->ref_counter);
+
+ //update the slice_buffer
+ slice_buffer->ref_counter = new_ref_counter;
+ slice_buffer->length = new_buffer_length;
+ slice_buffer->data = (uint8_t *)((uintptr_t)new_ref_counter + ref_counter_struct_size);
+ }
+
+ return unaryCall_original(unk1, uri, buffer_ptr, unk4, unk5, unk6);
+ }
+
+ void init(JNIEnv *env) {
+ auto unaryCall_func = util::find_signature(
+ common::client_module.base, common::client_module.size,
+ ARM64 ? "A8 03 1F F8 C2 00 00 94" : "0A 90 00 F0 3F F9",
+ ARM64 ? -0x48 : -0x37
+ );
+
+ native_lib_on_unary_call_method = env->GetMethodID(env->GetObjectClass(common::native_lib_object), "onNativeUnaryCall", "(Ljava/lang/String;[B)L" BUILD_NAMESPACE "/NativeRequestData;");
+
+ if (unaryCall_func != 0) {
+ DobbyHook((void *)unaryCall_func, (void *)unaryCall_hook, (void **)&unaryCall_original);
+ } else {
+ LOGE("can't find unaryCall signature");
+ }
+ }
+}+
\ No newline at end of file
diff --git a/native/jni/src/library.cpp b/native/jni/src/library.cpp
@@ -5,120 +5,19 @@
#include <android/asset_manager.h>
#include "logger.h"
-#include "config.h"
-#include "util.h"
-#include "grpc.h"
-
-#ifdef __aarch64__
-#define ARM64 true
-#else
-#define ARM64 false
-#endif
-
-static native_config_t *native_config;
-static JavaVM *java_vm;
-static jobject native_lib_object;
-static jmethodID native_lib_on_unary_call_method;
-static jmethodID native_lib_on_asset_load;
-
-// original functions
-static void *(*unaryCall_original)(void *, const char *, grpc::grpc_byte_buffer **, void *, void *, void *);
-static auto fstat_original = (int (*)(int, struct stat *)) nullptr;
-static AAsset* (*AAssetManager_open_original)(AAssetManager*, const char*, int) = nullptr;
-
-static int fstat_hook(int fd, struct stat *buf) {
- char name[256];
- memset(name, 0, sizeof(name));
- snprintf(name, sizeof(name), "/proc/self/fd/%d", fd);
- readlink(name, name, sizeof(name));
-
- std::string fileName(name);
-
- if (native_config->disable_metrics && fileName.find("files/blizzardv2/queues") != std::string::npos) {
- unlink(name);
- return -1;
- }
-
- if (native_config->disable_bitmoji && fileName.find("com.snap.file_manager_4_SCContent") != std::string::npos) {
- return -1;
- }
-
- return fstat_original(fd, buf);
-}
-
-
-static void *unaryCall_hook(void *unk1, const char *uri, grpc::grpc_byte_buffer **buffer_ptr, void *unk4, void *unk5, void *unk6) {
- // request without reference counter can be hooked using xposed ig
- auto slice_buffer = (*buffer_ptr)->slice_buffer;
- if (slice_buffer->ref_counter == 0) {
- return unaryCall_original(unk1, uri, buffer_ptr, unk4, unk5, unk6);
- }
-
- JNIEnv *env = nullptr;
- java_vm->GetEnv((void **)&env, JNI_VERSION_1_6);
-
- auto jni_buffer_array = env->NewByteArray(slice_buffer->length);
- env->SetByteArrayRegion(jni_buffer_array, 0, slice_buffer->length, (jbyte *)slice_buffer->data);
+#include "common.h"
+#include "hooks/asset_hook.h"
+#include "hooks/unary_call.h"
+#include "hooks/fstat_hook.h"
+#include "hooks/sqlite_mutex.h"
- auto native_request_data_object = env->CallObjectMethod(native_lib_object, native_lib_on_unary_call_method, env->NewStringUTF(uri), jni_buffer_array);
-
- if (native_request_data_object != nullptr) {
- auto native_request_data_class = env->GetObjectClass(native_request_data_object);
- auto is_canceled = env->GetBooleanField(native_request_data_object, env->GetFieldID(native_request_data_class, "canceled", "Z"));
-
- if (is_canceled) {
- LOGD("canceled request for %s", uri);
- return nullptr;
- }
-
- auto new_buffer = (jbyteArray)env->GetObjectField(native_request_data_object, env->GetFieldID(native_request_data_class, "buffer", "[B"));
- auto new_buffer_length = env->GetArrayLength(new_buffer);
- auto new_buffer_data = env->GetByteArrayElements(new_buffer, nullptr);
-
- //we need to allocate a new ref_counter struct and copy the old ref_counter and the new_buffer to it
- const static auto ref_counter_struct_size = (uintptr_t)slice_buffer->data - (uintptr_t)slice_buffer->ref_counter;
-
- auto new_ref_counter = malloc(ref_counter_struct_size + new_buffer_length);
- //copy the old ref_counter and the native_request_data_object
- memcpy(new_ref_counter, slice_buffer->ref_counter, ref_counter_struct_size);
- memcpy((void *)((uintptr_t)new_ref_counter + ref_counter_struct_size), new_buffer_data, new_buffer_length);
-
- //free the old ref_counter
- free(slice_buffer->ref_counter);
-
- //update the slice_buffer
- slice_buffer->ref_counter = new_ref_counter;
- slice_buffer->length = new_buffer_length;
- slice_buffer->data = (uint8_t *)((uintptr_t)new_ref_counter + ref_counter_struct_size);
- }
-
- return unaryCall_original(unk1, uri, buffer_ptr, unk4, unk5, unk6);
-}
-
-static AAsset* AAssetManager_open_hook(AAssetManager* mgr, const char* filename, int mode) {
- if (native_config->hook_asset_open) {
- JNIEnv *env = nullptr;
- java_vm->GetEnv((void **)&env, JNI_VERSION_1_6);
-
- if (!env->CallBooleanMethod(native_lib_object, native_lib_on_asset_load, env->NewStringUTF(filename))) {
- return nullptr;
- }
- }
-
- return AAssetManager_open_original(mgr, filename, mode);
-}
-
-void JNICALL init(JNIEnv *env, jobject clazz, jobject classloader) {
+void JNICALL init(JNIEnv *env, jobject clazz) {
LOGD("Initializing native");
- // config
- native_config = new native_config_t;
+ using namespace common;
- // native lib object
+ native_config = new native_config_t;
native_lib_object = env->NewGlobalRef(clazz);
- native_lib_on_unary_call_method = env->GetMethodID(env->GetObjectClass(clazz), "onNativeUnaryCall", "(Ljava/lang/String;[B)L" BUILD_NAMESPACE "/NativeRequestData;");
- native_lib_on_asset_load = env->GetMethodID(env->GetObjectClass(clazz), "shouldLoadAsset", "(Ljava/lang/String;)Z");
-
- auto client_module = util::get_module("libclient.so");
+ client_module = util::get_module("libclient.so");
if (client_module.base == 0) {
LOGE("libclient not loaded!");
@@ -127,44 +26,53 @@ void JNICALL init(JNIEnv *env, jobject clazz, jobject classloader) {
LOGD("libclient.so base=0x%0lx, size=0x%0lx", client_module.base, client_module.size);
- // hooks
- DobbyHook((void *)DobbySymbolResolver("libc.so", "fstat"), (void *)fstat_hook, (void **)&fstat_original);
-
- auto unaryCall_func = util::find_signature(
- client_module.base, client_module.size,
- ARM64 ? "A8 03 1F F8 C2 00 00 94" : "0A 90 00 F0 3F F9",
- ARM64 ? -0x48 : -0x37
- );
+ AssetHook::init(env);
+ UnaryCallHook::init(env);
+ FstatHook::init();
+ SqliteMutexHook::init();
- if (unaryCall_func != 0) {
- LOGD("found unaryCall at 0x%0lx", unaryCall_func);
- DobbyHook((void *)unaryCall_func, (void *)unaryCall_hook, (void **)&unaryCall_original);
- } else {
- LOGE("can't find unaryCall signature");
- }
-
- DobbyHook((void *) AAssetManager_open, (void *) AAssetManager_open_hook, (void **) &AAssetManager_open_original);
LOGD("Native initialized");
}
void JNICALL load_config(JNIEnv *env, jobject _, jobject config_object) {
auto native_config_clazz = env->GetObjectClass(config_object);
#define GET_CONFIG_BOOL(name) env->GetBooleanField(config_object, env->GetFieldID(native_config_clazz, name, "Z"))
+ auto native_config = common::native_config;
native_config->disable_bitmoji = GET_CONFIG_BOOL("disableBitmoji");
native_config->disable_metrics = GET_CONFIG_BOOL("disableMetrics");
native_config->hook_asset_open = GET_CONFIG_BOOL("hookAssetOpen");
}
+void JNICALL lock_database(JNIEnv *env, jobject _, jstring database_name, jobject runnable) {
+ auto database_name_str = env->GetStringUTFChars(database_name, nullptr);
+ auto mutex = SqliteMutexHook::mutex_map[database_name_str];
+ env->ReleaseStringUTFChars(database_name, database_name_str);
+
+ if (mutex != nullptr) {
+ auto lock_result = pthread_mutex_lock(&mutex->mutex);
+ if (lock_result != 0) {
+ LOGE("pthread_mutex_lock failed: %d", lock_result);
+ return;
+ }
+ }
+
+ env->CallVoidMethod(runnable, env->GetMethodID(env->GetObjectClass(runnable), "run", "()V"));
+
+ if (mutex != nullptr) {
+ pthread_mutex_unlock(&mutex->mutex);
+ }
+}
+
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *_) {
- // register native methods
- java_vm = vm;
+ common::java_vm = vm;
JNIEnv *env = nullptr;
vm->GetEnv((void **)&env, JNI_VERSION_1_6);
auto methods = std::vector<JNINativeMethod>();
- methods.push_back({"init", "(Ljava/lang/ClassLoader;)V", (void *)init});
+ methods.push_back({"init", "()V", (void *)init});
methods.push_back({"loadConfig", "(L" BUILD_NAMESPACE "/NativeConfig;)V", (void *)load_config});
+ methods.push_back({"lockDatabase", "(Ljava/lang/String;Ljava/lang/Runnable;)V", (void *)lock_database});
env->RegisterNatives(env->FindClass(std::string(BUILD_NAMESPACE "/NativeLib").c_str()), methods.data(), methods.size());
return JNI_VERSION_1_6;
diff --git a/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt b/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt
@@ -11,11 +11,11 @@ class NativeLib {
private set
}
- fun initOnce(classloader: ClassLoader) {
+ fun initOnce() {
if (initialized) throw IllegalStateException("NativeLib already initialized")
runCatching {
System.loadLibrary(BuildConfig.NATIVE_NAME)
- init(classloader)
+ init()
initialized = true
}.onFailure {
Log.e("SnapEnhance", "NativeLib init failed")
@@ -24,7 +24,6 @@ class NativeLib {
@Suppress("unused")
private fun onNativeUnaryCall(uri: String, buffer: ByteArray): NativeRequestData? {
- // Log.d("SnapEnhance", "onNativeUnaryCall: uri=$uri, bufferSize=${buffer.size}, buffer=${buffer.contentToString()}")
val nativeRequestData = NativeRequestData(uri, buffer)
runCatching {
nativeUnaryCallCallback(nativeRequestData)
@@ -45,6 +44,18 @@ class NativeLib {
loadConfig(config)
}
- private external fun init(classLoader: ClassLoader)
+ fun lockNativeDatabase(name: String, callback: () -> Unit) {
+ if (!initialized) return
+ lockDatabase(name) {
+ runCatching {
+ callback()
+ }.onFailure {
+ Log.e("SnapEnhance", "lockNativeDatabase callback failed", it)
+ }
+ }
+ }
+
+ private external fun init()
private external fun loadConfig(config: NativeConfig)
+ private external fun lockDatabase(name: String, callback: Runnable)
}
\ No newline at end of file