Skip to content

Database, Functions, Storage (Android): Fix crash on failed init and embedded class lookup - #1947

Open
AlekseyAzarov wants to merge 3 commits into
firebase:mainfrom
AlekseyAzarov:my-fix-branch
Open

AlekseyAzarov wants to merge 3 commits into
firebase:mainfrom
AlekseyAzarov:my-fix-branch

Conversation

@AlekseyAzarov

Copy link
Copy Markdown

Description

Provide details of the change, and generalize the change in the PR title above.

Fixes #1936. Two related Android defects: a failed embedded class lookup made
Database::GetInstance() fail, and the failure path then crashed with SIGSEGV
instead of returning an error.

1. Crash in DeleteInternal() after a failed initialization (Database, Functions, Storage)

When *Internal initialization fails, GetInstance() deletes the new instance
and should return nullptr with kInitResultFailedMissingDependency. On
Android the internal object of a failed instance has a null App, but
DeleteInternal() still used it: Database called MakeKey() →
app->name(), and Functions/Storage called
CleanupNotifier::FindByOwner(app()) and app()->options(). A failed
instance is never registered with the App's CleanupNotifier or added to the
instance cache, so DeleteInternal() now just deletes the internal object
and returns in that case. Desktop behavior is unchanged: there initialized()
is always true.

2. JNI class names passed to ClassLoader (app/src/util_android.cc)

ClassLoader.loadClass() and findLoadedClass() expect binary names
(a.b.C), but the Android branch of FindClassInFiles() and
FindOrLoadClassFromLoaders() passed JNI names (a/b/C). ART accepts these
in a standalone app, but the lookup fails under a custom class-loader setup,
as in the reporter's case where the app is hosted in another app's process.
The desktop branch of FindClassInFiles() already converted the name. The
conversion now lives in a shared helper, JniClassNameToBinaryName(), which
both branches of FindClassInFiles() and FindOrLoadClassFromLoaders() use.
The first lookup through JNIEnv::FindClass() still uses the JNI name, so
there is no change where lookup already succeeds.


Testing

Describe how you've tested these changes. Link any manually triggered Integration tests or CPP binary SDK Packaging Github Action workflows, if applicable.

  • Ran the build workflows on my fork; both succeeded:
    • Android Builds
      on Ubuntu, Windows and macOS (run on 79b9e21, the code commit; the later
      commit only adds release notes).
    • Desktop Builds
      on Ubuntu, macOS and Windows, including the unit tests where the
      workflow runs them and local-integration-tests-linux-openssl.
  • Compiled the changed files for Android arm64 with NDK r29 clang
    (-fsyntax-only -Wall), with no errors or warnings.
  • Checked formatting with clang-format 14.0.6, the version the Checks
    workflow uses on ubuntu-22.04. It reported no changes.
  • I could not run the integration tests: they need the project's secrets. Could
    a maintainer please add tests-requested: quick?
  • An integration test can't easily reach the failed-initialization path,
    because embedded class loading would have to fail. The reporter of [Database][Android] SIGSEGV in Database::DeleteInternal when embedded bridge-class init fails; FindClassInFiles passes JNI-form names to loadClass on Android #1936
    offered to test a patched build in their hosted-process setup.

Type of Change

Place an x the applicable box:

  • Bug fix. Add the issue # below if applicable.
  • New feature. A non-breaking change which adds functionality.
  • Other, such as a build process or documentation change.

Fixes #1936


Notes

  • Bug fixes and feature changes require an update to the Release Notes section of release_build_files/readme.md.
  • Read the contribution guidelines CONTRIBUTING.md.
  • Changes to the public API require an internal API review. If you'd like to help us make Firebase APIs better, please propose your change in a feature request so that we can discuss it together.

🤖 Generated with Claude Code

@google-cla

google-cla Bot commented Oct 24, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses initialization and class loading issues on Android. Specifically, it prevents crashes in Database, Functions, and Storage when initialization fails by ensuring uninitialized instances are safely deleted without accessing potentially null app pointers. Additionally, it resolves class loading issues under custom class loaders by introducing a helper function to convert JNI class names to binary names. The review feedback recommends adding a null check to this new helper function to avoid undefined behavior and potential crashes when handling null class name pointers.

Comment thread app/src/util_android.cc
Comment on lines +303 to +307
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Constructing a std::string from a nullptr const char* is undefined behavior in C++ and will cause a crash. To ensure robust defensive programming, we should add a null check for class_name before initializing the std::string.

static std::string JniClassNameToBinaryName(const char* class_name) {
  if (!class_name) {
    return "";
  }
  std::string binary_name(class_name);
  std::replace(binary_name.begin(), binary_name.end(), '/', '.');
  return binary_name;
}

Fixes firebase#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().
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant