Repository navigation
database: replace uWebSockets and libuv with libcurl WebSockets - #1959
Conversation
There was a problem hiding this comment.
Code Review
This pull request replaces the legacy uWebSockets and libuv WebSocket transport in the desktop Realtime Database SDK with a native libcurl-based implementation. It removes the libuv and uWebSockets dependencies entirely, centralizes the TLS policy across REST and WebSocket connections, and introduces a robust set of unit and integration tests. The reviewer identified a potential memory leak in web_socket_client_impl.cc where request_headers_.release() is passed directly to curl_slist_append; if the append fails, the original list is leaked. A safer helper lambda was suggested to handle appending safely.
ebbb972 to
beef0b1
Compare
2a9856c to
2323985
Compare
2323985 to
94d75ff
Compare
- Drop unmaintained external dependencies uWebSockets (v0.14) and libuv
(v1.40) from CMake, external rules, patches, and release NOTICES.
- Migrate Realtime Database WebSocket client (WebSocketClientImpl) to
libcurl's native WebSocket support (curl_ws_send, curl_ws_recv,
CURLOPT_CONNECT_ONLY = 2L).
- Give each WebSocket connection its own lightweight event thread and private
CURLM* handle using curl_multi_poll() and curl_multi_wakeup().
- Add stalled handshake deadline watchdog (5000 ms default).
- Centralize SDK transport security in app/rest/curl/tls_policy.{h,cc}
(TLS 1.2+ minimum, peer/hostname verification, and native OS certificate
trust store via Schannel on Windows, Keychain on macOS, and runtime CA bundle
detection on Linux).
- Add thread-safe reference-counted AcquireCurlGlobal() / ReleaseCurlGlobal()
and RAII handle deleters in app/rest/curl/curl_handles.{h,cc}.
- Initialize DatabaseInternal::persistence_enabled_ to false to prevent
reading uninitialized member variables on desktop platforms.
- Guard Logger::LogMessageImplV when parent_logger_ is null.
- Fix package.sh symbol hiding script generation for libfirebase_database.a.
- Add local loopback RFC 6455 WebSocket unit tests (8/8 passing).
94d75ff to
32dac60
Compare
❌ Integration test FAILEDRequested by @AustinBenoit on commit 5090147
Add flaky tests to go/fpl-cpp-flake-tracker |
|
Triggered Firebase Unity SDK build & integration tests against
|
…estroy the Connection Connection::OnError() now calls Close() synchronously. Close() invokes ConnectionEventHandler::OnDisconnect(), and PersistentConnection destroys the Connection from inside that callback (realtime_.reset()). The callback locked the SafeReference through the member, so ~Connection() freed the ReferenceData while its mutex was still held and ~Mutex() ran pthread_mutex_destroy() on a locked mutex: FIREBASE_ASSERT -> abort() on Linux/macOS, use-after-free on Windows. Any failed connect or reconnect attempt (DNS failure, refused connection, handshake timeout) triggered it because no keep-alive callback exists before OnOpen() to hold a second reference. Lock a local copy of the SafeReference in OnOpen/OnMessage/OnClose/OnError so the reference data outlives the lock even when the handler destroys this. Add regression tests that own the Connection the way PersistentConnection does and destroy it from OnDisconnect() after a refused loopback connect and after a DNS failure. Both abort without the fix.
- Check every curl_easy_setopt() in ConnectOnLoop() via a shared ApplyOpt() helper (moved from tls_policy.cc into curl_handles.h) and fail the connect with OnError() if any option is rejected. - Raise kPollTimeoutMs from 1 s to 60 s: curl_multi_poll() honors libcurl's own timers and the handshake deadline is applied separately, so the timeout is only a fallback for a failed curl_multi_wakeup(). Log when that call fails since the fallback is then the only thing that wakes the loop. - Leak the curl global-init mutex (as util.cc does) so ReleaseCurlGlobal() is safe during static destruction. - Log curl_ws_send() failures at Debug like curl_ws_recv() failures; both are ordinary network drops. - Drop the duplicate persistence_enabled_ initializer, clarify the thread_ and PING-validation comments. - Restore TestEdgeCase2 to a time-boxed loop (>= 500 iterations, <= 10 s or 10000 iterations) instead of a fixed 50, and add DestroyClientFromOwnErrorCallback, which destroys the client from inside its own scheduled OnError() the way the Realtime Database does.
…_test LinuxCaBundlePath()/LinuxCaDirectoryPath() ran getenv()/stat()/access() on every REST request and WebSocket connect and returned a pointer into the environment block. Resolve once per process into a leaked std::string and return that; the uncached probes are exposed as internal::Resolve*() so the precedence (CURL_CA_BUNDLE, SSL_CERT_FILE, distro paths; SSL_CERT_DIR, FIREBASE_SSL_CAPATH) can be unit-tested. Trim the CMake and tls_policy.h comments to what the configuration actually guarantees. libcurl 8.23.0 uses SCH_CRED_REVOCATION_CHECK_CHAIN (not _EXCLUDE_ROOT) with Schannel, Apple SecTrust revocation is best-effort (kSecRevocationUseAnyAvailableMethod), and the OpenSSL backend performs no revocation checks, so the previous claims about enforced OCSP/CRL were inaccurate. Document that the environment variables are a deliberate choice (libcurl itself does not read them) and must be set before first use. Add app/rest/tests/tls_policy_test.cc covering ApplyTlsPolicy() success and error reporting, cache stability, and the Linux env-var precedence rules.
…ersarial review) WebSocketClientImpl: - Set CURLMOPT_QUICK_EXIT so curl_multi_cleanup() does not join libcurl's resolver thread pool; ~WebSocketClientImpl runs on the Database scheduler thread and could otherwise stall for a full DNS timeout when a connection is torn down mid-resolve. - curl_multi_init() failure (e.g. fd exhaustion) no longer aborts the process: the client stays inert and Connect() reports OnError(). - Cap ReceiveFrames() at 256 frames per pass and re-poll without blocking (receive_backlog_) so a peer streaming frames continuously cannot starve posted work (Close, Send, destructor teardown). - Honor CURLM return codes: a failing curl_multi_perform() during the handshake reports OnError(); a failing curl_multi_poll() backs off 100 ms instead of spinning. - Once curl_multi_wakeup() has failed, poll with a 50 ms timeout so Post() latency stays bounded without a working wakeup. Connection::Send: announce ceil(len / kMaxFrameSize) frames. The old len / kMaxFrameSize + 1 announced one frame too many for payloads that are an exact multiple of 16384 bytes, so the server waited for a frame that never came and glued the next message onto the incomplete one. tls_policy: apply the same verification, TLS 1.2 floor and CA location to the HTTPS-proxy leg (CURLOPT_PROXY_*), and read the CA environment variables with secure_getenv() so AT_SECURE (setuid/setgid) processes ignore them. connection.h: correct the threading comment; the WebSocket callbacks do run on the scheduler thread and may destroy the Connection.
- connection_test target: define WIN32_LEAN_AND_MEAN and link ws2_32 on MSVC; the new GetClosedLoopbackPort() includes <winsock2.h> after SDK headers that include <windows.h>. - Add MessageOfExactlyMaxFrameSizeMultiple (live): fails without the Connection::Send frame-count fix. - DestroyFromOnDisconnect* tests: own the Logger in the fixture and never reset the Connection from the main thread, so a timeout cannot race the scheduler. - RejectsNonWebSocketUriSchemes: point http:// at a listening echo server and assert it accepted zero connections (plus a ws:// control), so the test no longer passes merely because the port was refused. - TestEdgeCase2: drop the tautological EXPECT_GE(count, kMinIterations). - tls_policy_test: replace the test that could never reach the null-bundle path with one that pins the libcurl contract it relied on (null CAINFO / CAPATH are accepted).
…ndling Covers the user-visible changes in the two adversarial-review commits; drop this commit together with them if they move to a follow-up PR.
8fd3f94 set CURLMOPT_QUICK_EXIT so ~WebSocketClientImpl would not wait for an in-flight DNS lookup. With it, curl_multi_cleanup() detaches running resolver threads (Curl_thrdpool_destroy, join == false) instead of joining them. In Database-only processes the libcurl global refcount drops to zero between connections, so those detached threads kept running across curl_global_cleanup() and the next curl_global_init(). ThreadSanitizer reported the resulting races on libcurl's allocator globals (Curl_getaddrinfo_ex and thrdslot_destroy vs. global_init) in DestroyFromOnDisconnectAfterDnsFailure and TestCreateDestroyRace. Go back to joining. The destructor can again wait for getaddrinfo() to return when a connection is torn down mid-resolve; that is the behaviour of the original change and is preferable to libcurl threads outliving curl_global_cleanup().
Summary
Replaces the Realtime Database desktop WebSocket transport (
uWebSocketsv0.14 andlibuvv1.33.1) with libcurl's WebSocket API (CURLOPT_CONNECT_ONLY = 2L,curl_ws_recv(),curl_ws_send()), removing two unmaintained third-party dependencies.1. Dependency removal
uWebSocketsandlibuvfromCMakeLists.txt,cmake/external/*,cmake/external_rules.cmake,README.md,scripts/gha/print_matrix_configuration.py,release_build_files/NOTICES, and the uWebSockets patch underscripts/git/patches/.psapioruserenvforfirebase_database.static_assertinapp/rest/curl/curl_handles.h).2.
WebSocketClientImpl(database/src/desktop/connection/)CURLM*withcurl_multi_poll()andcurl_multi_wakeup(). Handler callbacks are marshalled onto the Database scheduler through aSafeReference.101upgrade with an explicit handshake deadline, because libcurl's connect timeout does not cover the Upgrade exchange.CURLWS_NOAUTOPONG) that never splices a control frame into a partially sent message, a 125-byte control-frame cap and a 256 MiB message cap.curl_easy_setopt()is checked; a rejected option fails the connect throughOnError().curl_multi_wakeup()is logged and switches the loop to a 50 ms poll.curl_multi_init()failure leaves the client inert andConnect()reportsOnError()instead of aborting.ReceiveFrames()handles at most 256 frames per pass so a peer that streams continuously cannot starveClose(),Send()or teardown.CURLMerror codes are honoured: a failingcurl_multi_perform()during the handshake reportsOnError(), and a failingcurl_multi_poll()backs off 100 ms instead of spinning.CURLMOPT_QUICK_EXITis deliberately not set. It detaches in-flight resolver threads, which then outlivecurl_global_cleanup()in Database-only processes. The destructor can therefore wait for an in-flightgetaddrinfo().3.
Connection::OnErrorcloses synchronously, and the crash that exposedConnection::OnError()used to scheduleClose()through a second scheduler hop, so mid-session errors were reported askDisconnectReasonConnectionLost. It now callsClose()directly and reportskDisconnectReasonWebsocketError.That exposed a latent bug.
Close()callsConnectionEventHandler::OnDisconnect(), andPersistentConnectiondestroys theConnectionthere (realtime_.reset()) while the handler still holds aSafeReferenceLocktaken through the membersafe_this_.~Mutex()then ranpthread_mutex_destroy()on a locked mutex, which asserts and aborts on Linux and macOS and is a use-after-free on Windows. BeforeOnOpen()nothing else holds a reference, so every failed connect or reconnect attempt aborted.OnOpen,OnMessage,OnCloseandOnErrornow lock a local copy of theSafeReference, so the reference data outlives the lock even when the handler destroysthis.Regression tests in
connection_test.cc:DestroyFromOnDisconnectAfterConnectRefused(loopback port with no listener) andDestroyFromOnDisconnectAfterDnsFailure(.invalidTLD). The first aborts the test binary without the fix.4. Shared libcurl primitives (
app/rest/curl/)curl_handles.{h,cc}: RAII deleters,ApplyOpt()for checking batches ofcurl_easy_setopt(), MSVC#pragma comment(lib)directives, and a refcountedAcquireCurlGlobal()/ReleaseCurlGlobal()shared bytransport_curl.cc,util.ccand the WebSocket client. The refcount mutex is leaked on purpose so release during static destruction is safe.tls_policy.{h,cc}: one TLS policy for REST and WebSockets.VERIFYPEER=1,VERIFYHOST=2, TLS 1.2 minimum, and OS trust stores (Schannel on Windows; OpenSSL/BoringSSL withUSE_APPLE_SECTRUSTon macOS; the distribution CA store on Linux). The SDK does not configure revocation checking.CURL_CA_BUNDLE, so the packaged Linux SDK failed withCURLE_SSL_CACERT_BADFILEon RHEL, Fedora and openSUSE. The bundle and directory are now resolved once per process at runtime:CURL_CA_BUNDLE,SSL_CERT_FILE, then the standard distribution paths;SSL_CERT_DIR, then/etc/ssl/certs. Honouring the environment variables is a deliberate choice; libcurl itself does not read them.CURLOPT_PROXY_*).secure_getenv()on glibc, so setuid/setgid processes ignore them.app/rest/tests/tls_policy_test.cc.5. Supporting fixes
Logger::LogMessageImplVtolerates a nullparent_logger_.DatabaseInternal::persistence_enabled_is default-initialized.build_scripts/desktop/package.sh: in sequential mode, allmerge_<product>.shscripts are generated before any runs. Previouslymerge_app.shwith-Ddeletedlibcurl.abeforemerge_database.shresolved its--hide_c_symbolslist, which left thecurl_*references inlibfirebase_database.aun-renamed. This path is reached wheneverparallelis missing and-jfalls back to sequential.6.
Connection::Sendframe count (bug that predates this PR)Connection::Sendannouncedlen / 16384 + 1frames but sentceil(len / 16384). For a serialized request that is an exact multiple of 16384 bytes the server waited for a frame that never came and attached the next request to the incomplete one. The count is now a ceiling division.ConnectionTest.MessageOfExactlyMaxFrameSizeMultiplefails on the old formula.Behaviour changes
kDisconnectReasonWebsocketErrorrather thankDisconnectReasonConnectionLost. The reason is only logged.Verification
Linux x86_64, GCC 15.2, system OpenSSL 3.6:
firebase_rtdb_desktop_connection_web_socket_client_impl_test: 9/9.firebase_rtdb_desktop_connection_connection_test: 12/12 against the live backend.firebase_app_rest_tls_policy_test: 10/10.Appteardown against the heartbeat thread (app/src/app_common.cc,app/src/heartbeat/), which this PR does not touch.package.sh -Dsequential (withparallelhidden) and-D -jboth leave no un-renamedcurl_*symbols inlibfirebase_database.a. Thepackage.shonmainleaves 18 in sequential mode.ubuntu-22.04x64/x86 Debug & Release +local-integration-tests-linux-openssl), macOS (macos-26arm64 Debug & Release), and Windows (windows-2022x64 Release static — 105/105 CTest suites passed — and x86 Debug dynamic): all 10 jobs green.tests: succeeded): Desktop (Linux & Windows OpenSSL), Android (FTL real & emulator), iOS (device & simulator), and tvOS (simulator) all green.Notes on manual checks:
ConnectionTest.DestroyFromOnDisconnectAfterConnectRefusedandConnectionTest.DestroyFromOnDisconnectAfterDnsFailureacross Linux, macOS, and Windows CTest runs.package.shpicks up leveldb.Follow-ups
SafeReferenceLockhold ashared_ptrto the reference data, and addsafe_reference_test.cc. The same member-lock pattern remains inPersistentConnection,Repo,DatabaseInternal,RemoteConfigandUserSecureManager.Connection::OnConnectionShutdown()readsclient_afterOnKill()may have destroyed theConnection.PersistentConnection::TryScheduleReconnect()has no backoff.Repo::~Repo()decrements the shared scheduler refcount even when the constructor returned early on a bad URL.FIREBASE_ASSERTor a null dereference instead ofkDisconnectReasonProtocolError.Close(), no progress timeout on a stalled send queue, and no test of the partial-write path.curl_global_cleanup().