Skip to content

fix: stop bundling libuv into the extension (#106) - #140

Merged
CodeLieutenant merged 1 commit into
trunkfrom
fix/106-libuv-symbol-interposition
Aug 4, 2026
Merged

fix: stop bundling libuv into the extension (#106)#140
CodeLieutenant merged 1 commit into
trunkfrom
fix/106-libuv-symbol-interposition

Conversation

@CodeLieutenant

@CodeLieutenant CodeLieutenant commented Jul 28, 2026

Copy link
Copy Markdown
Member

Fixes #106.

The bug

Every CMake preset set LINK_LIBUV_STATIC=ON, so cassandra.so carries its own copy of libuv. When that module is linked against a shared libcassandra.so.2 / libscylla-cpp-driver.so, ELF's flat namespace makes the driver's uv_* references bind to the copy inside cassandra.so rather than the libuv.so.1 it was compiled against. The driver then runs a libuv build that doesn't match its headers, which corrupts memory around its event loop.

In #106 that showed up as a garbage refcount and an abort inside the driver:

php: ref_counted.hpp:43: ... RetryPolicy::dec_ref(): Assertion `new_ref_count >= 1' failed.
#30 uv__finish_close ... at out/ReleaseLibCassandra/_deps/libuv-src/src/unix/core.c:351
#31 uv__run_closing_handles ...
#32 uv_run ...
#33 datastax::internal::core::EventLoop::handle_run()

Frames #30–32 are the php-driver's libuv running inside libcassandra's event-loop thread — the interposition, right there in the reporter's own backtrace.

Two things this explains:

  • v1.3.8 works, v1.3.9+ doesn't. 1.3.8 built via config.m4 and linked libuv dynamically (PHP_ADD_LIBRARY(uv,...)); the CMake build introduced in 1.3.9 links it statically.
  • Not reproducible on macOS. Mach-O two-level namespaces bind each dylib to its own dependencies at link time, so no interposition happens.

Reduced repro of the mechanism (gcc:13, uv_version standing in for libuv):

--- module statically links its own copy (current preset behaviour) ---
libcassandra sees uv_version() = 148     # wrong copy wins
--- same, plus -Wl,--exclude-libs,ALL ---
libcassandra sees uv_version() = 144     # correct

The fix

The extension used libuv for exactly one thing — an rwlock guarding the cassandra.log INI value — which does not justify the dependency.

  • src/php_scylladb.c: uv_rwlock_tpthread_rwlock_t, statically initialised. That also removes an ordering hazard: the log callback can fire from driver threads before php_scylladb_log_initialize() ran uv_rwlock_init(). Cleanup now takes the write lock instead of destroying the lock.
  • libuv dropped from the extension build: find_package(Libuv), Libuv::Libuv, cmake/FindLibuv.cmake, the LINK_LIBUV_STATIC / BUILD_LIBUV_FROM_SRC options, the --enable-libuv-static config.m4/PIE flag, and the preset entries. libuv remains a dependency of the C/C++ driver, which is where it belongs — scripts/compile-libuv.sh and the CI action stay.
  • -Wl,--exclude-libs,ALL on non-Apple platforms, so no future statically linked third-party archive (OpenSSL, GMP, a static cpp-driver) can interpose on the driver's dependencies. -fvisibility=hidden was already set, but it only covers our own objects, never archives compiled elsewhere.

Reviewer notes

@CodeLieutenant CodeLieutenant self-assigned this Jul 28, 2026
@mergify

mergify Bot commented Jul 28, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

Every CMake preset built the extension with LINK_LIBUV_STATIC=ON, which
puts a private copy of libuv inside cassandra.so. On Linux, ELF's flat
namespace then makes the shared libcassandra/libscylla-cpp-driver resolve
its own uv_* references against that copy instead of the libuv.so.1 it
was compiled against. The driver ends up running a different libuv build
than its headers described, which corrupts memory around its event loop —
observed as a garbage RefCounted ref_count and an abort in
RetryPolicy::dec_ref during session connect.

The extension used libuv for exactly one thing: an rwlock guarding the
cassandra.log INI value. Swap it for pthread_rwlock_t (statically
initialised, which also removes the ordering hazard between the log
callback and INI registration) and drop libuv from the build entirely.
libuv stays a dependency of the C/C++ driver, where it belongs.

Also link with -Wl,--exclude-libs,ALL on non-Apple platforms so no future
statically linked third-party archive can interpose on the driver's own
dependencies. -fvisibility=hidden was already set but only covers our own
objects, never archives compiled elsewhere. macOS is unaffected either
way: two-level namespaces bind each dylib to its own dependencies.

Drive-by: config.m4 passed the stale -DPHP_DRIVER_STATIC, so
--enable-driver-static was a no-op; the regenerated CMakePresets.json
picks up the same PHP_DRIVER_* -> PHP_SCYLLADB_* rename.
@CodeLieutenant
CodeLieutenant force-pushed the fix/106-libuv-symbol-interposition branch from 2ba5b5c to 5142933 Compare August 4, 2026 09:51
@CodeLieutenant
CodeLieutenant merged commit 07a3e6e into trunk Aug 4, 2026
55 of 56 checks passed
@CodeLieutenant
CodeLieutenant deleted the fix/106-libuv-symbol-interposition branch August 4, 2026 12:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

undefined symbol: cass_session_new when installing with libcassandra

1 participant