You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Segfault in connect under multithreaded use: cTinyTdsError/mTinyTds not registered with the Ruby GC #608
tiny_tds 3.4.0 (also reproducible on 3.2.1 and 3.3.0)
FreeTDS 1.5.19 (Homebrew)
Web server: Falcon / async-http, --threaded (12 native threads)
Summary
Under a multithreaded Ruby server, TinyTds::Client.new intermittently crashes
the whole process with a segmentation fault during connect. The crash happens
while FreeTDS delivers its normal info messages (e.g. "Changed language
setting"), inside tinytds_msg_handler. In a single-threaded context (plain ruby, or Falcon --forked) the same code runs fine.
Steps to reproduce
Run a multithreaded Ruby HTTP server (e.g. falcon serve --threaded) that
opens ESB/MS SQL connections per request via TinyTds::Client.new(host:, ...).
Make requests that trigger connect.
The process aborts with [BUG] Segmentation fault.
It does not reproduce in single-threaded execution — the same connect works
in isolated ruby -e 'TinyTds::Client.new(...)' and when the server runs with a
single thread/process.
Because cTinyTdsError/mTinyTds are not pinned via
rb_gc_register_address/rb_global_variable (unlike opt_escape_regex and
opt_escape_dblquote in client.c), the GC may move/reclaim the TinyTds
module and TinyTds::Error class while another native thread is mid-connect.
The C pointer then goes stale, and rb_exc_new2 crashes.
My example enters through tinytds_err_handler, while the original issue enters through tinytds_msg_handler. Both handlers call rb_tinytds_raise_error, which dereferences the same global cTinyTdsError value in rb_exc_new2.
An investigation found that there are other unregistered class/module globals used after extension initialization: cTinyTdsClient, cTinyTdsResult, cKernel, and cDate. They should be registered or otherwise made compaction-safe.
These addresses are not registered with Ruby's garbage collector but need to be. I agree with the proposed fix.
A Sidekiq worker process (24 Ruby threads, concurrency 10) crashed with [BUG] Segmentation fault while opening a new connection through Sequel.connect -> TinyTds::Client.new. The server was healthy: the same connection succeeded moments later from a console. It happened once in roughly two months across about a dozen worker processes that open connections daily, so it is rare but fatal — the whole process dies and the job running on it is lost.
Notably, nothing in our bundle calls GC.compact, GC.auto_compact= or GC.verify_compaction_references, so this was not induced by forced compaction as in the minimal repro above.
Same path as the original report: tinytds_msg_handler -> rb_tinytds_raise_error -> rb_exc_new2(cTinyTdsError, ...). The Check_Type -> unexpected_type frames show Ruby rejecting the class argument itself, i.e. cTinyTdsError no longer pointed to a valid class at the moment of the crash — the segfault then happens while Ruby tries to format that type-error message.
Additional context
In 3.1.0, cTinyTdsError is the only one of these globals obtained with rb_const_get from a class defined in Ruby (lib/tiny_tds/error.rb); mTinyTds, cTinyTdsClient, cTinyTdsResult, cKernel and cDate are also stored without rb_global_variable / rb_gc_register_address, while opt_escape_regex, opt_escape_dblquote and the opt_* values in result.c are registered.
The v4 branch (V4 #603) still has the same unregistered cTinyTdsError in tiny_tds_ext.c, so the rewrite does not fix it on its own.
+1 to the proposed fix of registering these globals with the GC. We are happy to open a PR with it (plus a regression test based on the compaction repro above) if that is welcome.
diff --git a/config/initializers/tiny_tds_gc_guard.rb b/config/initializers/tiny_tds_gc_guard.rb
new file mode 100644
index 000000000..a9f80b27f
--- /dev/null+++ b/config/initializers/tiny_tds_gc_guard.rb@@ -0,0 +1,25 @@+# frozen_string_literal: true++# TinyTDS 3.4.0 keeps these Ruby objects in C globals that are not registered+# with Ruby's GC. Pin their canonical values so compaction cannot leave the+# extension with stale pointers. Remove this after upstream issue #608 is fixed.+# https://github.com/rails-sqlserver/tiny_tds/issues/608+require "fiddle"+require "tiny_tds"++register_mark_object = Fiddle::Function.new(+ Fiddle::Handle::DEFAULT["rb_gc_register_mark_object"],+ [Fiddle::TYPE_VOIDP],+ Fiddle::TYPE_VOID,+)++[+ TinyTds,+ TinyTds::Error,+ TinyTds::Client,+ TinyTds::Result,+ Kernel,+ Date,+].each do |object|+ register_mark_object.call(Fiddle.dlwrap(object))+end
So far, the segfault has gone away. This is a workaround. And also, we don't know for 100% certainty that this fixes everything; since the crash was semi rare. That said, we were seeing it happen maybe 3 times a day. And it has not happened at all since this code was deployed. So presumably it worked.
And yes, the minimal repo I provided is not the way we get the crash in production. It was just my way of forcing the same type of crash in a super reliable way.
Activity
CoderJoshDK commentedon Sep 29, 2026
Confirming a similar experience. However, my minimal repo doesn't require multiple threads or really much at all:
Running this gives you a segfault
Details
My example enters through
tinytds_err_handler, while the original issue enters throughtinytds_msg_handler. Both handlers callrb_tinytds_raise_error, which dereferences the same globalcTinyTdsErrorvalue inrb_exc_new2.An investigation found that there are other unregistered class/module globals used after extension initialization:
cTinyTdsClient,cTinyTdsResult,cKernel, andcDate. They should be registered or otherwise made compaction-safe.These addresses are not registered with Ruby's garbage collector but need to be. I agree with the proposed fix.
plribeiro3000 commentedon Sep 30, 2026
We hit this in production as well, with a slightly different profile that may help narrow it down.
Environment
x86_64-linux, +YJIT +PRISM), officialruby:4.0.6Docker imagefreetds-devpackagetinytdsadapter), Sidekiq 8.1.7, Rails 8.1.3.1Microsoft SQL Azure (RTM) - 12.0.2000.8)Summary
A Sidekiq worker process (24 Ruby threads, concurrency 10) crashed with
[BUG] Segmentation faultwhile opening a new connection throughSequel.connect->TinyTds::Client.new. The server was healthy: the same connection succeeded moments later from a console. It happened once in roughly two months across about a dozen worker processes that open connections daily, so it is rare but fatal — the whole process dies and the job running on it is lost.Notably, nothing in our bundle calls
GC.compact,GC.auto_compact=orGC.verify_compaction_references, so this was not induced by forced compaction as in the minimal repro above.Key backtrace
Ruby level:
C level (top frames):
Same path as the original report:
tinytds_msg_handler->rb_tinytds_raise_error->rb_exc_new2(cTinyTdsError, ...). TheCheck_Type->unexpected_typeframes show Ruby rejecting the class argument itself, i.e.cTinyTdsErrorno longer pointed to a valid class at the moment of the crash — the segfault then happens while Ruby tries to format that type-error message.Additional context
cTinyTdsErroris the only one of these globals obtained withrb_const_getfrom a class defined in Ruby (lib/tiny_tds/error.rb);mTinyTds,cTinyTdsClient,cTinyTdsResult,cKernelandcDateare also stored withoutrb_global_variable/rb_gc_register_address, whileopt_escape_regex,opt_escape_dblquoteand theopt_*values inresult.care registered.v4branch (V4 #603) still has the same unregisteredcTinyTdsErrorintiny_tds_ext.c, so the rewrite does not fix it on its own.+1 to the proposed fix of registering these globals with the GC. We are happy to open a PR with it (plus a regression test based on the compaction repro above) if that is welcome.
CoderJoshDK commentedon Sep 30, 2026
In production, we have added
So far, the segfault has gone away. This is a workaround. And also, we don't know for 100% certainty that this fixes everything; since the crash was semi rare. That said, we were seeing it happen maybe 3 times a day. And it has not happened at all since this code was deployed. So presumably it worked.
And yes, the minimal repo I provided is not the way we get the crash in production. It was just my way of forcing the same type of crash in a super reliable way.
fix(ext): register globals holding ruby objects with the gc
fix(ext): register globals holding ruby objects with the gc
fix(ext): register globals holding ruby objects with the gc (rails-sq…