Repository navigation
Fix use-after-free when Firebase instances are destroyed during static destruction - #1949
AustinBenoit wants to merge 7 commits into
Conversation
Each product keeps a process-wide map of App -> instance plus a mutex, and both are touched from the instance's destructor via DeleteInternal(). When a user holds an instance in a static (e.g. a static std::unique_ptr<Auth>) that is destroyed after the SDK's own globals, the destructor accesses an already-destroyed map or mutex. Auth and Remote Config stored the map as a static std::map, which produced a heap-use-after-free in ~Auth()/~RemoteConfig(). Database, Storage, Functions, Installations and App Check already heap-allocated the map but still used a static Mutex, which is destroyed at exit before the lock in DeleteInternal(). Bring every product in line with the layout App and Firestore already use: the map is lazily allocated and freed when the last instance goes away, and the mutex is allocated once and never freed. Fixes #110
There was a problem hiding this comment.
Code Review
This pull request addresses a potential crash at process exit by ensuring that global mutexes and maps for various Firebase SDK components (Auth, RemoteConfig, Database, Storage, Functions, Installations, and AppCheck) are handled safely. The changes involve dynamically allocating these objects to prevent destruction order issues during C++ runtime shutdown. The reviewer correctly identified that the current implementation of global mutex initialization is susceptible to the Static Initialization Order Fiasco (SIOF) and suggested using a function-local static pointer ('construct on first use' idiom) to mitigate this risk.
Replace the namespace-scope `Mutex* g_x = new Mutex();` globals with accessor functions that own a function-local static. Dynamic initialization of a namespace-scope pointer is subject to static initialization order, so a static initializer in another translation unit that called GetInstance() would have found a null mutex. A function-local static is initialized on first use, thread-safely, regardless of translation unit order, and the pointer it holds is still never freed. Auth and Remote Config now also keep their map behind the same kind of accessor instead of lazily allocating and freeing a raw pointer, which removes the null checks and manual lifetime management from the GetInstance/Find/DeleteInternal paths. Callers only ever see references.
Introduce firebase::NoDestructor<T>, a small wrapper (modelled on absl::NoDestructor) that constructs a T in place and never runs its destructor. Use it for the registry mutexes of every product and the registry maps of Auth and Remote Config in place of `static T* const t = new T;`. The objects are now stored inline in the function-local static rather than on the heap, there is no `new` or raw pointer anywhere in the registry code, and leak checkers have nothing to report since nothing is allocated. The semantics are otherwise unchanged: constructed on first use, thread-safely, regardless of translation unit order, and never destroyed. Includes a unit test for NoDestructor.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a NoDestructor utility class to wrap function-local statics, such as global maps and mutexes, ensuring they are constructed on first use and never destroyed. This change prevents crashes at process exit when Firebase SDK instances (such as Auth, RemoteConfig, Database, Storage, Functions, Installations, or AppCheck) are owned by static objects and destroyed after the SDK's globals. The review feedback highlights several critical issues regarding redundant recursive locking or potential deadlocks in Auth, RemoteConfig, and Installations where locked functions call other functions that attempt to acquire the same mutex. Additionally, minor improvements are suggested for NoDestructor to use decayed array pointers instead of pointer-to-array types for cleaner and more portable casts.
- In NoDestructor, cast the decayed array pointer `storage_` rather than `&storage_` in placement new and `get()`. - In Auth, RemoteConfig, and Installations, factor out `Find*Locked(App*)` helpers (and inline the internal cleanup in `~Installations()`) so callers that already hold the registry mutex do not recursively re-lock it.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces the NoDestructor template class to wrap function-local statics, preventing process-exit crashes when Firebase SDK instances (such as Auth, RemoteConfig, Database, Storage, Functions, Installations, and AppCheck) are owned by static objects and destroyed after the SDK's globals. The feedback suggests optimizing the Auth map deletion by performing a direct key lookup instead of iterating through the map, and refactoring the Installations cleanup logic into a helper function to eliminate code duplication.
…odules - Harden NoDestructor<T> with SFINAE forwarding-ctor guard, brace-init overloads, and std::launder access - Migrate firebase_app, app/rest, and all product libraries (Analytics, AppCheck, Auth, Database, Firestore, Functions, Installations, Messaging, RemoteConfig, Storage, UMP) across Desktop, Android, and iOS onto function-local NoDestructor accessors - Fix Installations out-of-order App deletion map retention, ABA safety, failed-init cleanup in Installations/RemoteConfig, and O(log N) Auth map removal - Enable -Werror=exit-time-destructors on Clang/AppleClang across all 12 static library targets - Add firebase_app_static_destruction_test, expanded NoDestructor unit tests, and Installations out-of-order teardown regression test
| if (!cleaned_up_) { | ||
| while (callbacks_.begin() != callbacks_.end()) { | ||
| std::pair<void *, CleanupCallback> object_and_callback = | ||
| std::pair<void*, CleanupCallback> object_and_callback = |
There was a problem hiding this comment.
Add #include <utility> for pair<>
| @@ -47,9 +48,10 @@ static_assert(sizeof(FutureBase) == sizeof(Future<int>), | |||
| typedef void DataDeleteFn(void* data_to_delete); | |||
| typedef std::pair<FutureHandleId, FutureBackingData*> BackingPair; | |||
There was a problem hiding this comment.
Add #include <utility> for pair<>
|
|
||
| const std::pair<Variant, Variant>& QueryParamsComparator::MaxNode() { | ||
| static const NoDestructor<std::pair<Variant, Variant>> max_node( | ||
| std::make_pair(QueryParamsComparator::kMaxKey, MaxVariant())); |
There was a problem hiding this comment.
Add #include <utility> for make_pair
| namespace internal { | ||
| extern ::firebase::Mutex g_cached_ump_embedded_files_mutex; | ||
| ::firebase::Mutex& CachedUmpEmbeddedFilesMutex(); | ||
| extern std::vector<::firebase::internal::EmbeddedFile>* |
There was a problem hiding this comment.
Add #include <vector> for vector<>
Description
Fixes #110.
Destroying a static
firebase::Appor product wrapper (e.g.static std::unique_ptr<Auth>) during process exit can run after the C++ runtime has already destroyed the SDK's own static maps, mutexes, or loggers, causing use-after-free or destroyed-mutex UB.firebase::NoDestructor<T>(app/src/no_destructor.h) for inline, trivially destructible function-local statics that initialize thread-safely on first use and never run destructors at exit.firebase_app,app/rest, and all product libraries (Analytics,AppCheck,Auth,Database,Firestore,Functions,Installations,Messaging,RemoteConfig,Storage,UMP) ontoNoDestructor<T>.Installationsout-of-orderAppteardown map retention, failed-init leaks inInstallationsandRemoteConfig, andO(log N)map removal inAuth::DeleteInternal().-Werror=exit-time-destructorson Clang/AppleClang across all 12 static library targets.Testing
0__cxa_atexitreferences across all 12 built desktop static archives (208object files) and cleanclang++ -Werror=exit-time-destructorscompilation.firebase_app_static_destruction_test(post-main()static teardown and repeatedAppreinitialization), expandedfirebase_app_no_destructor_test, and addedInstallationsTest.DestroyAppBeforeInstallationsClearsCachedInstance.App/Auth/RemoteConfigteardown under AddressSanitizer (exit 0, no UAF).Type of Change
Place an
xthe applicable box:Fixes #110
Notes
Release Notessection ofrelease_build_files/readme.md.