From 3c0a1a1b3989051d1e393322d61efc22dcd65764 Mon Sep 17 00:00:00 2001 From: AlekseyAzarov <63075467+AlekseyAzarov@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:43:42 +0400 Subject: [PATCH 1/3] Fix crash on failed init and class lookup with custom class loaders Fixes #1936. Database/Functions/Storage: DeleteInternal() no longer crashes when the instance failed to initialize. GetInstance() deletes such an instance, and on Android its internal object has a null App, so DeleteInternal() dereferenced a null App* (e.g. in MakeKey() via app->name()) instead of letting GetInstance() return nullptr with kInitResultFailedMissingDependency. An uninitialized instance was never registered with the App's CleanupNotifier or added to the instance cache, so DeleteInternal() now just deletes the internal object and returns. util_android: pass binary class names ("a.b.C") rather than JNI names ("a/b/C") to ClassLoader.findLoadedClass()/loadClass(). The Android branch of FindClassInFiles() and FindOrLoadClassFromLoaders() passed JNI names, which ART tolerates in a standalone app but which fail when the app runs under a custom class-loader setup (e.g. hosted in another app's process), so the embedded classes such as CppTransactionHandler could not be loaded. The conversion is now shared by both branches of FindClassInFiles() via JniClassNameToBinaryName(). --- app/src/util_android.cc | 27 +++++++++++++++++---------- database/src/common/database.cc | 17 +++++++++++------ functions/src/common/functions.cc | 8 ++++++++ storage/src/common/storage.cc | 8 ++++++++ 4 files changed, 44 insertions(+), 16 deletions(-) diff --git a/app/src/util_android.cc b/app/src/util_android.cc index 1ad15f0c4f..6f09fc5483 100644 --- a/app/src/util_android.cc +++ b/app/src/util_android.cc @@ -297,12 +297,22 @@ static jobject GetParentLoader() { return g_class_loaders->back(); } +// Convert a JNI class name (e.g. "java/lang/String"), as used by +// JNIEnv::FindClass(), to the binary name (e.g. "java.lang.String") expected by +// ClassLoader methods. +static std::string JniClassNameToBinaryName(const char* class_name) { + std::string binary_name(class_name); + std::replace(binary_name.begin(), binary_name.end(), '/', '.'); + return binary_name; +} + // Try to find a class in the list of class loaders. static jclass FindOrLoadClassFromLoaders(JNIEnv* env, const char* class_name) { assert(g_class_loaders); static const class_loader::Method kFindLoadClassMethods[] = { class_loader::kFindLoadedClass, class_loader::kLoadClass}; - jstring class_name_object = env->NewStringUTF(class_name); + jstring class_name_object = + env->NewStringUTF(JniClassNameToBinaryName(class_name).c_str()); jclass class_object = nullptr; for (size_t i = 0; i < FIREBASE_ARRAYSIZE(kFindLoadClassMethods); ++i) { for (auto it = g_class_loaders->begin(); @@ -1669,8 +1679,6 @@ jclass FindClassInFiles( firebase::util::JniStringToString(env, cache_dir_path_jstring); #if defined(FIREBASE_ANDROID_FOR_DESKTOP) - static const char kPathSeparator = '/'; - jobject cache_uri = env->CallObjectMethod(cache_dir, file::GetMethodId(file::kToUri)); CheckAndClearJniExceptions(env); @@ -1698,11 +1706,9 @@ jclass FindClassInFiles( url_path_array, GetParentLoader()); env->DeleteLocalRef(url_path_array); - std::string class_name_str(class_name); - std::replace(class_name_str.begin(), class_name_str.end(), kPathSeparator, - '.'); - LogDebug("Load class %s (a.k.a. %s)", class_name_str.c_str(), class_name); - jstring class_name_object = env->NewStringUTF(class_name_str.c_str()); + std::string binary_name = JniClassNameToBinaryName(class_name); + LogDebug("Load class %s (a.k.a. %s)", binary_name.c_str(), class_name); + jstring class_name_object = env->NewStringUTF(binary_name.c_str()); jclass loaded_class = static_cast(env->CallObjectMethod( class_loader_obj, url_class_loader::GetMethodId(url_class_loader::kLoadClass), @@ -1745,8 +1751,9 @@ jclass FindClassInFiles( env->DeleteLocalRef(code_cache_dir_path); env->DeleteLocalRef(dex_path_string); - LogDebug("Load class %s", class_name); - jstring class_name_object = env->NewStringUTF(class_name); + std::string binary_name = JniClassNameToBinaryName(class_name); + LogDebug("Load class %s (a.k.a. %s)", binary_name.c_str(), class_name); + jstring class_name_object = env->NewStringUTF(binary_name.c_str()); jclass loaded_class = static_cast(env->CallObjectMethod( class_loader_obj, dex_class_loader::GetMethodId(dex_class_loader::kLoadClass), diff --git a/database/src/common/database.cc b/database/src/common/database.cc index 3d262a9b64..ac17b9382c 100644 --- a/database/src/common/database.cc +++ b/database/src/common/database.cc @@ -122,15 +122,20 @@ void Database::DeleteInternal() { if (!internal_) return; + // An instance whose initialization failed was never registered with the + // App's CleanupNotifier or added to g_databases, and app() may be null. + if (!internal_->initialized()) { + delete internal_; + internal_ = nullptr; + return; + } + App* my_app = app(); std::string url = internal_->constructor_url(); - // Only need to unregister if internal_ is initialized - if (internal_->initialized()) { - CleanupNotifier* app_notifier = CleanupNotifier::FindByOwner(my_app); - assert(app_notifier); - app_notifier->UnregisterObject(this); - } + CleanupNotifier* app_notifier = CleanupNotifier::FindByOwner(my_app); + assert(app_notifier); + app_notifier->UnregisterObject(this); { MutexLock db_ref_lock(internal::g_database_reference_constructor_mutex); diff --git a/functions/src/common/functions.cc b/functions/src/common/functions.cc index ea278b3053..df262eeac5 100644 --- a/functions/src/common/functions.cc +++ b/functions/src/common/functions.cc @@ -122,6 +122,14 @@ void Functions::DeleteInternal() { if (!internal_) return; + // An instance whose initialization failed was never registered with the + // App's CleanupNotifier or added to g_functions, and app() may be null. + if (!internal_->initialized()) { + delete internal_; + internal_ = nullptr; + return; + } + CleanupNotifier* app_notifier = CleanupNotifier::FindByOwner(app()); assert(app_notifier); app_notifier->UnregisterObject(this); diff --git a/storage/src/common/storage.cc b/storage/src/common/storage.cc index 33786b4dda..3c995042b1 100644 --- a/storage/src/common/storage.cc +++ b/storage/src/common/storage.cc @@ -136,6 +136,14 @@ void Storage::DeleteInternal() { MutexLock lock(g_storages_lock); if (!internal_) return; + // An instance whose initialization failed was never registered with the + // App's CleanupNotifier or added to g_storages, and app() may be null. + if (!internal_->initialized()) { + delete internal_; + internal_ = nullptr; + return; + } + CleanupNotifier* app_notifier = CleanupNotifier::FindByOwner(app()); assert(app_notifier); app_notifier->UnregisterObject(this); From 3d5874a42130a84b587428ebedb7af33d8a1bed0 Mon Sep 17 00:00:00 2001 From: AlekseyAzarov <63075467+AlekseyAzarov@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:30:03 +0400 Subject: [PATCH 2/3] Add release notes for #1936 fixes --- release_build_files/readme.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/release_build_files/readme.md b/release_build_files/readme.md index e1b9847c6e..680f9137ea 100644 --- a/release_build_files/readme.md +++ b/release_build_files/readme.md @@ -613,6 +613,15 @@ workflow use only during the development of your app, not for publicly shipping code. ## Release Notes +### Upcoming +- Changes + - Database, Functions, Storage (Android): Fixed a crash in `GetInstance()` + when initialization fails. It now returns `nullptr` and reports + `kInitResultFailedMissingDependency`. + - General (Android): Fixed loading of embedded Java classes when the app + runs under a custom class loader setup, such as being hosted in another + app's process. (#1936) + ### 14.1.0 - Changes - General (Android): Update to Firebase Android BoM version 35.1.0. From 6bae1afbdda52f378956dd2f1f1fe25cb2bcfec9 Mon Sep 17 00:00:00 2001 From: AlekseyAzarov <63075467+AlekseyAzarov@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:12:35 +0400 Subject: [PATCH 3/3] Handle null class name in JniClassNameToBinaryName --- app/src/util_android.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/util_android.cc b/app/src/util_android.cc index 6f09fc5483..4eeda7eeb5 100644 --- a/app/src/util_android.cc +++ b/app/src/util_android.cc @@ -301,6 +301,7 @@ static jobject GetParentLoader() { // JNIEnv::FindClass(), to the binary name (e.g. "java.lang.String") expected by // ClassLoader methods. static std::string JniClassNameToBinaryName(const char* class_name) { + if (!class_name) return std::string(); std::string binary_name(class_name); std::replace(binary_name.begin(), binary_name.end(), '/', '.'); return binary_name;