[REQUIRED] Please fill in the following fields:
- Pre-built SDK from the website or open-source from this repo: Pre-built (shipped inside the Firebase Unity SDK)
- Firebase C++ SDK version: 12.5.0 (
FirebaseCppApp-12_5_0.dll); the affected code is unchanged on main today
- Problematic Firebase Component: Database (desktop implementation)
- Other Firebase Components in use: Auth
- Platform you are using the C++ SDK on: Windows
- Platform you are targeting: desktop (Windows, via Unity editor and standalone)
[REQUIRED] Please describe the issue here:
The desktop Realtime Database implementation crashes with a use-after-free when a listener is removed while a listen response for the same query is still in flight. I hit this repeatedly in a Unity project on Windows (8 crash dumps over one week, all at the identical RVA), and I traced it to a dangling const View* in WebSocketListenResponse.
Root cause
database/src/desktop/core/web_socket_listen_provider.cc stores a raw, non-owning View* on the response object:
class WebSocketListenResponse : public connection::Response {
...
QuerySpec query_spec_; // stored by value
const View* view_; // raw pointer, not lifetime-guarded
};
The completion callback carefully guards the Repo lifetime with Repo::ThisRefLock, but then dereferences the unguarded view_ two lines later:
Repo::ThisRefLock lock(&response->repo_ref());
Repo* repo = lock.GetReference();
if (repo == nullptr) { return; } // Repo lifetime IS guarded
std::vector<Event> events;
if (!response->HasError()) {
const QuerySpec& query_spec = response->view()->query_spec(); // view_ is NOT guarded
View objects are owned by the SyncTree. SyncTree::RemoveEventRegistration destroys them (subtree->value().reset(), sync_tree.cc:623) as soon as a query's registration count hits zero. Nothing removes or invalidates the in-flight WebSocketListenResponse when that happens, so the callback reads freed memory.
The race window is widest around connection churn. A dropped connection synthesizes error responses for every outstanding listen, and a reconnect re-sends all listens and creates a fresh in-flight response for each one. Any listener detached in that window (screen teardown, GoOffline() during a network flap) leaves a doomed response behind.
Crash signature
All 8 dumps fault at the same instruction, FirebaseCppApp-12_5_0.dll+0x12D18B (movups xmm0, [rdi], an inlined MSVC small-string copy helper reading the freed View's query_spec_.path):
Crash_2026-07-03 ACCESS_VIOLATION FirebaseCppApp-12_5_0.dll+0x12d18b READ 0xffff1000
Crash_2026-07-04 ACCESS_VIOLATION FirebaseCppApp-12_5_0.dll+0x12d18b READ 0x0
Crash_2026-07-05 ACCESS_VIOLATION FirebaseCppApp-12_5_0.dll+0x12d18b READ 0x0
Crash_2026-07-08 ACCESS_VIOLATION FirebaseCppApp-12_5_0.dll+0x12d18b READ 0xffffffffffffffff
Crash_2026-07-09 ACCESS_VIOLATION FirebaseCppApp-12_5_0.dll+0x12d18b READ 0x0
One note on symbolication, because it has sent existing reports down the wrong path: the Windows crash handler labels this stack uS::Socket::write / uS::Socket::freeMessage. That's an artifact. The DLL exports almost none of the RTDB desktop code, so the reporter snaps unexported addresses to the nearest named uS:: template export. I disassembled the DLL and matched the faulting function's call sites back to source using the Log* format strings embedded in .rdata; one call site sits inside the WebSocketListenResponse callback (its "Listen at %s failed: %s" literal is on line 91 of web_socket_listen_provider.cc). The bug is in the listener lifecycle code, not in uWebSockets. firebase/firebase-unity-sdk#1391 and firebase/firebase-unity-sdk#540 show the same misleading uS:: stacks, and #1391 reproduces on Unity SDK 13.7.0, so current releases still carry it.
Steps to reproduce:
Repro rate is intermittent by nature (it's a thread race). In my project it fired roughly once a day during normal development. The recipe:
- Windows desktop app with one or more active RTDB listeners (
ValueChanged etc.).
- Induce connection churn: drop DNS/network, or call
GoOffline()/GoOnline(), or let a flaky network do it for you.
- Detach a listener (remove the last event registration for a query) while a listen response is pending, i.e. shortly after attaching, or during the churn.
- The pending response completes and dereferences the freed
View.
I have not built a quickstart-cpp repro; the analysis above is from disassembly of the shipped DLL cross-checked against the v12.5.0 source tree, plus the 8 minidumps.
Relevant Code:
The fix looks like a one-liner. The response already stores its QuerySpec by value, the error branch already uses it (response->query_spec()), and QuerySpecForListening preserves the path, which is the only field the success branch reads. So the success branch can use the stored copy too:
// web_socket_listen_provider.cc, in the WebSocketListenResponse callback:
- const QuerySpec& query_spec = response->view()->query_spec();
+ const QuerySpec& query_spec = response->query_spec();
With that change nothing reads view_ anymore, so the member and its constructor argument can be deleted outright and the dangling pointer is gone. Happy to send a PR if this looks right to you.
[REQUIRED] Please fill in the following fields:
FirebaseCppApp-12_5_0.dll); the affected code is unchanged onmaintoday[REQUIRED] Please describe the issue here:
The desktop Realtime Database implementation crashes with a use-after-free when a listener is removed while a listen response for the same query is still in flight. I hit this repeatedly in a Unity project on Windows (8 crash dumps over one week, all at the identical RVA), and I traced it to a dangling
const View*inWebSocketListenResponse.Root cause
database/src/desktop/core/web_socket_listen_provider.ccstores a raw, non-owningView*on the response object:The completion callback carefully guards the
Repolifetime withRepo::ThisRefLock, but then dereferences the unguardedview_two lines later:Viewobjects are owned by theSyncTree.SyncTree::RemoveEventRegistrationdestroys them (subtree->value().reset(), sync_tree.cc:623) as soon as a query's registration count hits zero. Nothing removes or invalidates the in-flightWebSocketListenResponsewhen that happens, so the callback reads freed memory.The race window is widest around connection churn. A dropped connection synthesizes error responses for every outstanding listen, and a reconnect re-sends all listens and creates a fresh in-flight response for each one. Any listener detached in that window (screen teardown,
GoOffline()during a network flap) leaves a doomed response behind.Crash signature
All 8 dumps fault at the same instruction,
FirebaseCppApp-12_5_0.dll+0x12D18B(movups xmm0, [rdi], an inlined MSVC small-string copy helper reading the freedView'squery_spec_.path):One note on symbolication, because it has sent existing reports down the wrong path: the Windows crash handler labels this stack
uS::Socket::write/uS::Socket::freeMessage. That's an artifact. The DLL exports almost none of the RTDB desktop code, so the reporter snaps unexported addresses to the nearest nameduS::template export. I disassembled the DLL and matched the faulting function's call sites back to source using theLog*format strings embedded in.rdata; one call site sits inside theWebSocketListenResponsecallback (its"Listen at %s failed: %s"literal is on line 91 of web_socket_listen_provider.cc). The bug is in the listener lifecycle code, not in uWebSockets. firebase/firebase-unity-sdk#1391 and firebase/firebase-unity-sdk#540 show the same misleadinguS::stacks, and #1391 reproduces on Unity SDK 13.7.0, so current releases still carry it.Steps to reproduce:
Repro rate is intermittent by nature (it's a thread race). In my project it fired roughly once a day during normal development. The recipe:
ValueChangedetc.).GoOffline()/GoOnline(), or let a flaky network do it for you.View.I have not built a quickstart-cpp repro; the analysis above is from disassembly of the shipped DLL cross-checked against the v12.5.0 source tree, plus the 8 minidumps.
Relevant Code:
The fix looks like a one-liner. The response already stores its
QuerySpecby value, the error branch already uses it (response->query_spec()), andQuerySpecForListeningpreserves the path, which is the only field the success branch reads. So the success branch can use the stored copy too:With that change nothing reads
view_anymore, so the member and its constructor argument can be deleted outright and the dangling pointer is gone. Happy to send a PR if this looks right to you.