Hi! Whilst working with dbt's ADBC integration, me and Claude bumped into this issue. Below report is written by Claude, but I have verified it seems to be a real issue.
What happened?
The Rust driver manager opens driver libraries with RTLD_LAZY, so a driver whose own dependency symbols cannot be resolved loads successfully. The unresolved symbols are bound at first call instead, and for a driver linked with -undefined dynamic_lookup (flat namespace) they bind to NULL — so the process jumps to address 0 and dies with SIGSEGV, arbitrarily far from the load site and with no diagnostic.
DriverLibrary::load_library passes the flags here:
libloading::os::unix::Library::open(
Some(filename.as_ref()),
libloading::os::unix::RTLD_LAZY | libloading::os::unix::RTLD_LOCAL | RTLD_NODELETE,
)
The error handling here is fine — the dlopen result is checked. The problem is purely that RTLD_LAZY defers the failure past the point where it could be reported. The same dlopen with RTLD_NOW returns a precise, actionable error naming the missing symbol.
The two differ only in that one flag, on the identical library:
| flag |
dlopen result |
outcome |
RTLD_NOW |
fails |
symbol not found in flat namespace '_OPENSSL_sk_num' |
RTLD_LAZY |
succeeds |
SIGSEGV at 0x0 on first call |
This matters more for a driver manager than for a typical dlopen caller: the whole point is loading third-party plugins whose link-time hygiene you do not control, and RTLD_LAZY converts "this driver is broken" into "your process crashed somewhere unrelated".
Real-world instance
A PostgreSQL driver built with -undefined dynamic_lookup on macOS ARM64 leaves the OpenSSL symbols libpq needs unresolved. ManagedDriver::load_dynamic_from_filename returns Ok, AdbcDriverInit is resolved and called, and the process dies at the first OPENSSL_* call inside libpq. lldb shows EXC_BAD_ACCESS (code=1, address=0x0) with frame #0: 0x0000000000000000 — no symbol, no backtrace, nothing pointing at the driver.
Because the crash surfaces at the first connection attempt rather than at load, this reads as a bug in whatever was executing at the time. It took a dlopen probe outside the process to identify the real cause.
Stack Trace
* thread #2, stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
frame #0: 0x0000000000000000
error: memory read failed for 0x0
How can we reproduce the bug?
Self-contained; no ADBC build required. It isolates the flag choice, which is the whole of the bug.
fake_driver.c — stands in for any driver linked with -undefined dynamic_lookup that left a dependency symbol unresolved:
extern int OPENSSL_sk_num(const void *);
int AdbcDriverInit(int version, void *raw_driver, void *error) {
(void)version; (void)raw_driver; (void)error;
return OPENSSL_sk_num(0); /* unresolved at load time */
}
loader.c — the driver manager's flags vs. strict ones:
#include <dlfcn.h>
#include <stdio.h>
int main(int argc, char **argv) {
int lazy = (argc > 2 && argv[2][0] == 'l');
int flags = (lazy ? RTLD_LAZY : RTLD_NOW) | RTLD_LOCAL;
printf("dlopen(%s)\n", lazy ? "RTLD_LAZY" : "RTLD_NOW");
void *h = dlopen(argv[1], flags);
if (!h) { printf(" -> load error (clean): %s\n", dlerror()); return 0; }
printf(" -> load reported SUCCESS\n");
int (*init)(int, void *, void *) = (int (*)(int, void *, void *))dlsym(h, "AdbcDriverInit");
printf(" -> calling AdbcDriverInit ...\n"); fflush(stdout);
init(1000000, 0, 0);
return 0;
}
cc -dynamiclib -o libfake_driver.dylib fake_driver.c \
-Wl,-undefined,dynamic_lookup -Wl,-flat_namespace
cc -o loader loader.c
# what a strict loader would do
./loader ./libfake_driver.dylib now; echo "exit=$?"
# what the driver manager does
./loader ./libfake_driver.dylib lazy; echo "exit=$?"
Output:
dlopen(RTLD_NOW)
-> load error (clean): dlopen(./libfake_driver.dylib, 0x0006): symbol not found in flat namespace '_OPENSSL_sk_num'
exit=0
dlopen(RTLD_LAZY)
-> load reported SUCCESS
-> calling AdbcDriverInit ...
exit=139 # SIGSEGV
Suggested fix
Use RTLD_NOW in load_library, so an unloadable driver is reported as a load error rather than becoming a latent crash. The existing map_err(libloading_error_to_adbc_error) then surfaces the linker's own message, which already names the offending symbol.
RTLD_NODELETE and RTLD_LOCAL are unaffected — the reasons documented in the comment above those lines (the Go runtime not honouring -z nodelete on macOS) are orthogonal to eager vs. lazy binding.
If eager binding is deliberate for startup cost, an alternative is to keep RTLD_LAZY but retry with RTLD_NOW when a load succeeds, so the diagnostic is at least available — though that pays the cost twice and still leaves the default path crashing.
Environment/Setup
- macOS 15 (Darwin 25.6.0), ARM64
adbc_driver_manager (Rust) 0.22.0
- Also present on
apache/arrow-adbc main at 00dfb8b9c0f776940781ea42c5b32b6bf3618652 — the flags are unchanged there.
The -undefined dynamic_lookup linkage that triggers it is a property of how a given driver binary was built, not of ADBC itself; the report here is about the driver manager turning that into an undiagnosable crash rather than a load error.
Hi! Whilst working with dbt's ADBC integration, me and Claude bumped into this issue. Below report is written by Claude, but I have verified it seems to be a real issue.
What happened?
The Rust driver manager opens driver libraries with
RTLD_LAZY, so a driver whose own dependency symbols cannot be resolved loads successfully. The unresolved symbols are bound at first call instead, and for a driver linked with-undefined dynamic_lookup(flat namespace) they bind toNULL— so the process jumps to address0and dies withSIGSEGV, arbitrarily far from the load site and with no diagnostic.DriverLibrary::load_librarypasses the flags here:The error handling here is fine — the
dlopenresult is checked. The problem is purely thatRTLD_LAZYdefers the failure past the point where it could be reported. The samedlopenwithRTLD_NOWreturns a precise, actionable error naming the missing symbol.The two differ only in that one flag, on the identical library:
dlopenresultRTLD_NOWsymbol not found in flat namespace '_OPENSSL_sk_num'RTLD_LAZYSIGSEGVat0x0on first callThis matters more for a driver manager than for a typical
dlopencaller: the whole point is loading third-party plugins whose link-time hygiene you do not control, andRTLD_LAZYconverts "this driver is broken" into "your process crashed somewhere unrelated".Real-world instance
A PostgreSQL driver built with
-undefined dynamic_lookupon macOS ARM64 leaves the OpenSSL symbols libpq needs unresolved.ManagedDriver::load_dynamic_from_filenamereturnsOk,AdbcDriverInitis resolved and called, and the process dies at the firstOPENSSL_*call inside libpq.lldbshowsEXC_BAD_ACCESS (code=1, address=0x0)withframe #0: 0x0000000000000000— no symbol, no backtrace, nothing pointing at the driver.Because the crash surfaces at the first connection attempt rather than at load, this reads as a bug in whatever was executing at the time. It took a
dlopenprobe outside the process to identify the real cause.Stack Trace
How can we reproduce the bug?
Self-contained; no ADBC build required. It isolates the flag choice, which is the whole of the bug.
fake_driver.c— stands in for any driver linked with-undefined dynamic_lookupthat left a dependency symbol unresolved:loader.c— the driver manager's flags vs. strict ones:Output:
Suggested fix
Use
RTLD_NOWinload_library, so an unloadable driver is reported as a load error rather than becoming a latent crash. The existingmap_err(libloading_error_to_adbc_error)then surfaces the linker's own message, which already names the offending symbol.RTLD_NODELETEandRTLD_LOCALare unaffected — the reasons documented in the comment above those lines (the Go runtime not honouring-z nodeleteon macOS) are orthogonal to eager vs. lazy binding.If eager binding is deliberate for startup cost, an alternative is to keep
RTLD_LAZYbut retry withRTLD_NOWwhen a load succeeds, so the diagnostic is at least available — though that pays the cost twice and still leaves the default path crashing.Environment/Setup
adbc_driver_manager(Rust) 0.22.0apache/arrow-adbcmainat00dfb8b9c0f776940781ea42c5b32b6bf3618652— the flags are unchanged there.The
-undefined dynamic_lookuplinkage that triggers it is a property of how a given driver binary was built, not of ADBC itself; the report here is about the driver manager turning that into an undiagnosable crash rather than a load error.