diff --git a/CHANGELOG.md b/CHANGELOG.md index 373e3695..69642cf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## (unreleased) +* Register the C globals that hold Ruby classes and modules with the GC, so raising `TinyTds::Error` after GC compaction no longer crashes with a segmentation fault. Fixes #608. + ## 3.4.0 * Add Ruby 4.0 to the cross compile list diff --git a/ext/tiny_tds/client.c b/ext/tiny_tds/client.c index 5f98dee4..4e84efc1 100644 --- a/ext/tiny_tds/client.c +++ b/ext/tiny_tds/client.c @@ -541,6 +541,7 @@ static VALUE rb_tinytds_connect(VALUE self, VALUE opts) void init_tinytds_client() { cTinyTdsClient = rb_define_class_under(mTinyTds, "Client", rb_cObject); + rb_global_variable(&cTinyTdsClient); rb_define_alloc_func(cTinyTdsClient, allocate); /* Define TinyTds::Client Public Methods */ rb_define_method(cTinyTdsClient, "tds_version", rb_tinytds_tds_version, 0); diff --git a/ext/tiny_tds/result.c b/ext/tiny_tds/result.c index 449f01c7..6323403e 100644 --- a/ext/tiny_tds/result.c +++ b/ext/tiny_tds/result.c @@ -702,9 +702,12 @@ void init_tinytds_result() { /* Data Classes */ cKernel = rb_const_get(rb_cObject, rb_intern("Kernel")); + rb_global_variable(&cKernel); cDate = rb_const_get(rb_cObject, rb_intern("Date")); + rb_global_variable(&cDate); /* Define TinyTds::Result */ cTinyTdsResult = rb_define_class_under(mTinyTds, "Result", rb_cObject); + rb_global_variable(&cTinyTdsResult); rb_undef_alloc_func(cTinyTdsResult); /* Define TinyTds::Result Public Methods */ rb_define_method(cTinyTdsResult, "fields", rb_tinytds_result_fields, 0); diff --git a/ext/tiny_tds/tiny_tds_ext.c b/ext/tiny_tds/tiny_tds_ext.c index 2ef4dd9a..1b01c83c 100644 --- a/ext/tiny_tds/tiny_tds_ext.c +++ b/ext/tiny_tds/tiny_tds_ext.c @@ -8,8 +8,10 @@ VALUE mTinyTds, cTinyTdsError; void Init_tiny_tds() { - mTinyTds = rb_define_module("TinyTds"); + mTinyTds = rb_define_module("TinyTds"); + rb_global_variable(&mTinyTds); cTinyTdsError = rb_const_get(mTinyTds, rb_intern("Error")); + rb_global_variable(&cTinyTdsError); init_tinytds_client(); init_tinytds_result(); } diff --git a/test/gc_compaction_test.rb b/test/gc_compaction_test.rb new file mode 100644 index 00000000..87ca1c84 --- /dev/null +++ b/test/gc_compaction_test.rb @@ -0,0 +1,37 @@ +require "test_helper" +require "open3" +require "rbconfig" + +class GcCompactionTest < Minitest::Spec + describe "after GC compaction" do + it "raises TinyTds::Error instead of crashing when a connection fails" do + skip "GC compaction is not supported on this platform" if !GC.respond_to?(:verify_compaction_references) + + # Ruby 3.2 renamed the double_heap: keyword to expand_heap: + heap_keyword = + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.2") + "expand_heap" + else + "double_heap" + end + + script = <<~RUBY + require "tiny_tds" + + GC.verify_compaction_references(#{heap_keyword}: true, toward: :empty) + + begin + TinyTds::Client.new(host: "127.0.0.1", port: 1, username: "unused", password: "unused", login_timeout: 1) + rescue TinyTds::Error + exit 0 + end + + exit 1 + RUBY + + output, status = Open3.capture2e(RbConfig.ruby, "-I", File.expand_path("../lib", __dir__), "-e", script) + + assert status.success?, "expected TinyTds::Error to be raised, got #{status.inspect}:\n#{output}" + end + end +end