tursodatabase / tursodatabase/libsql-client-ts
SIGSEGV in native binding's N-API finalizer (GC-triggered), Node 20.20.2 / musl / linux-x64
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 576
- Forks
- 69
- PR merge metrics
- No merged PRs in 30d
Description
Title
SIGSEGV in native binding's N-API finalizer (GC-triggered), Node 20.20.2 / musl / linux-x64
Environment
@libsql/client: 0.17.3@libsql/core: 0.17.3@libsql/linux-x64-musl: 0.5.29 (latest at time of writing)- Accessed via
@prisma/adapter-libsql7.8.0 /@prisma/client7.8.0 (Prisma ORM 7.9.1) - Node.js: v20.20.2
- OS: Alpine Linux 3.23.4 (musl libc), linux/amd64
- Runtime context: a long-running Next.js 16 server process (standalone build,
node server.js), single always-on process, SQLite database accessed over a local file path via the libsql adapter (not a remote Turso connection)
What happened
A production server process has segfaulted (SIGSEGV) six times over roughly 36 hours, with the gap between crashes trending shorter (4h41m → 26min → ... → ~12.5min most recently). The process is otherwise unremarkable: a Next.js app issuing normal Prisma queries through @prisma/adapter-libsql against a local SQLite file, plus a couple of setInterval-driven background polling loops that also query the same Prisma client. There is exactly one long-lived PrismaClient/PrismaLibSql adapter instance for the whole process lifetime — it is not recreated per request or per query.
Crashes are not correlated with any specific request pattern we've been able to identify — some happened mid-request, at least one happened after 2.5 hours of total log silence (no in-flight request at all).
Diagnosis
Enabled Linux core dumps (ulimit -c, custom core_pattern) and Node's --report-on-fatalerror flag on the container's startup command. The next crash produced a usable core dump (no diagnostic-report JSON was produced alongside it — this crash type apparently doesn't go through Node's own fatal-error reporting path).
Analyzed the dump with GDB inside a node:20.20.2-alpine3.23 Docker container (matching the exact Node/OS build that produced the crash), with the crashed process's own @libsql/linux-x64-musl/index.node bind-mounted at its original path so symbols would resolve:
gdb -batch -ex 'bt full' -ex 'info sharedlibrary' /usr/local/bin/node core.dump
Backtrace (crashing thread)
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x00007fb8a11b2bc4 in ?? () from /app/node_modules/@libsql/linux-x64-musl/index.node
#1 0x00007fb8a0e8d432 in ?? () from /app/node_modules/@libsql/linux-x64-musl/index.node
#2 0x00007fb8a0f64470 in ?? () from /app/node_modules/@libsql/linux-x64-musl/index.node
#3 0x00007fb8a0c4808e in ?? () from /app/node_modules/@libsql/linux-x64-musl/index.node
#4 0x00007fb8a0c23d93 in ?? () from /app/node_modules/@libsql/linux-x64-musl/index.node
#5 0x00007fb8a0c3e810 in ?? () from /app/node_modules/@libsql/linux-x64-musl/index.node
#6 0x00005569e5c5316e in void node_napi_env__::CallFinalizer<true>(void (*)(napi_env__*, void*, void*), void*, void*) ()
#7 0x00005569e5c24344 in v8impl::Reference::Finalize() ()
#8 0x00005569e5c4d275 in node_napi_env__::DrainFinalizerQueue() ()
#9 0x00005569e5bfb968 in node::Environment::RunAndClearNativeImmediates(bool) ()
#10 0x00005569e5bfc287 in node::Environment::CheckImmediate(uv_check_s*) ()
#11 0x00005569e695ee79 in uv__run_check (loop=loop@entry=0x5569ea7ecde0 <default_loop_struct>) at ../deps/uv/src/unix/loop-watcher.c:67
#12 0x00005569e695702c in uv_run (loop=0x5569ea7ecde0 <default_loop_struct>, mode=UV_RUN_DEFAULT) at ../deps/uv/src/unix/core.c:461
#13 0x00005569e5b94529 in node::SpinEventLoopInternal(node::Environment*) ()
#14 0x00005569e5cef3e4 in node::NodeMainInstance::Run() ()
#15 0x00005569e5c49a00 in node::Start(int, char**) ()
#16 0x00007fbda70e88d0 in libc_start_main_stage2 (main=0x5569e5b8c110 <main>, argc=4, argv=0x7ffdf5dd01f8) at src/env/__libc_start_main.c:95
#17 0x00005569e5b91a9e in _start ()
Crash instruction
=> 0x7fb8a11b2bc4: movzbl 0x71(%rdi),%eax
%rdi at the point of the fault does not point to valid memory for that access — the pointer is not null, but a read at a small, fixed struct-field offset (+0x71) faults, consistent with a use-after-free or double-free rather than a null-pointer dereference.
Interpretation
The crash happens entirely inside frames #0–#5 (all unresolved, @libsql/linux-x64-musl's own code — the shared library carries no debug symbols, (*) marked "missing debugging information" for the other system libs too), reached via Node's N-API garbage-collection finalizer path (CallFinalizer → Reference::Finalize → DrainFinalizerQueue). This strongly suggests: some JS-side object wrapping a native libSQL resource (most likely a prepared statement or a result-rows object, given the access pattern — the app makes frequent short-lived queries) was garbage-collected by V8 at an arbitrary point in time, and the native finalizer callback that runs its cleanup crashed accessing already-freed (or already-dropped, on the Rust side) memory.
This is not triggered by any specific SQL our application issues — it's triggered by V8's own GC finalizing a wrapper object, at a time entirely outside application control, which is why it doesn't correlate with any specific request pattern.
Why we think this isn't isolated
Two other open, unresolved segfault reports already exist against this native binding, neither an exact match for this signature but both suggesting unresolved memory-safety issues in the same area:
- #309 — segfault in the official
transactionsexample on Windows x64 - #313 — Bun segfault with looped awaited SQL updates inside a transaction (different crash address, different runtime)
What we've ruled out on our side
- Already on
@libsql/linux-x64-musl@0.5.29, npm's currentlatest— no newer stable release to try. - Not OOM (
oom_killed=falseper our hosting platform), not disk pressure, not a recent deploy regression (crashes recur across multiple unrelated deploys), not our own application code (single long-lived Prisma client, no manual native-resource handling). - Could not reproduce locally with a synthetic repeated-request load test — the crash appears tied to organic, longer-running production traffic patterns rather than any single request repeated many times.
Questions
- Is there a known workaround (e.g., disabling statement caching, forcing explicit
.finalize()/close calls instead of relying on GC, or a build/runtime flag) that avoids relying on the GC finalizer path for cleanup? - Is this a known class of issue being tracked internally, even if not yet reproduced in a minimal repro?
Happy to share the full core dump or run additional diagnostics against it if that would help — didn't want to attach a 200MB+ binary directly to the issue.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Begin with @libsql/linux-x64-musl/index.node and the Node N-API finalizer path shown in the backtrace; inspect the core dump in the matching node:20.20.2-alpine3.23 environment. Establish a minimal reproduction or confirmed workaround for GC-triggered cleanup, then document the verified fix and regression coverage.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nodejs, rust, sqlite, typescript
- Domain
- backend, databases
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100