Environment
- Ruby 3.4.10 (
arm64-darwin, Apple Silicon)
- 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.
Key backtrace (from the crash log)
libsybdb.5.dylib(_dblib_handle_info_message)
libsybdb.5.dylib(tds_connect)
libsybdb.5.dylib(tds_connect_and_login)
libsybdb.5.dylib(tdsdbopen)
tiny_tds.bundle(rb_tinytds_connect)
...
tiny_tds.bundle(tinytds_msg_handler)
tiny_tds.bundle(rb_tinytds_raise_error)
libruby(rb_exc_new_cstr)
libruby(rb_class_superclass -> rb_unexpected_type -> unexpected_type)
libruby(rb_obj_as_string -> rb_funcall -> callable_method_entry_or_negative) ← SIGSEGV
The value in x0 at the crash is a fragment of an ASCII string (e.g.
"read \"i\""), i.e. the C code is dereferencing a stale/freed pointer.
Root cause
ext/tiny_tds/tiny_tds_ext.c keeps global VALUEs that are not registered
with the GC:
VALUE mTinyTds, cTinyTdsError;
void Init_tiny_tds() {
mTinyTds = rb_define_module("TinyTds");
cTinyTdsError = rb_const_get(mTinyTds, rb_intern("Error"));
init_tinytds_client();
init_tinytds_result();
}
rb_tinytds_raise_error builds an exception on every FreeTDS message:
e = rb_exc_new2(cTinyTdsError, error.error);
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.
Proposed fix
void Init_tiny_tds() {
mTinyTds = rb_define_module("TinyTds");
cTinyTdsError = rb_const_get(mTinyTds, rb_intern("Error"));
rb_gc_register_address(&mTinyTds);
rb_gc_register_address(&cTinyTdsError);
init_tinytds_client();
init_tinytds_result();
}
Related
Possibly the same family of multithreading crashes as #539 (also unregistered
GC globals / native code + concurrent threads).
Environment
arm64-darwin, Apple Silicon)--threaded(12 native threads)Summary
Under a multithreaded Ruby server,
TinyTds::Client.newintermittently crashesthe whole process with a segmentation fault during
connect. The crash happenswhile FreeTDS delivers its normal info messages (e.g. "Changed language
setting"), inside
tinytds_msg_handler. In a single-threaded context (plainruby, or Falcon--forked) the same code runs fine.Steps to reproduce
falcon serve --threaded) thatopens ESB/MS SQL connections per request via
TinyTds::Client.new(host:, ...).connect.[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 asingle thread/process.
Key backtrace (from the crash log)
libsybdb.5.dylib(_dblib_handle_info_message)
libsybdb.5.dylib(tds_connect)
libsybdb.5.dylib(tds_connect_and_login)
libsybdb.5.dylib(tdsdbopen)
tiny_tds.bundle(rb_tinytds_connect)
...
tiny_tds.bundle(tinytds_msg_handler)
tiny_tds.bundle(rb_tinytds_raise_error)
libruby(rb_exc_new_cstr)
libruby(rb_class_superclass -> rb_unexpected_type -> unexpected_type)
libruby(rb_obj_as_string -> rb_funcall -> callable_method_entry_or_negative) ← SIGSEGV
The value in
x0at the crash is a fragment of an ASCII string (e.g."read \"i\""), i.e. the C code is dereferencing a stale/freed pointer.Root cause
ext/tiny_tds/tiny_tds_ext.ckeeps globalVALUEs that are not registeredwith the GC:
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.
Proposed fix
Related
Possibly the same family of multithreading crashes as #539 (also unregistered
GC globals / native code + concurrent threads).