From e7f9abdc910bbb37d04e8e65e9bad5f36fa074e1 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Fri, 5 Dec 2025 22:09:00 -0500 Subject: [PATCH 01/16] Sync doc/stringio in sync_default_gems.rb --- tool/sync_default_gems.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/tool/sync_default_gems.rb b/tool/sync_default_gems.rb index c2f352d797ffc8..811e72fc008eff 100755 --- a/tool/sync_default_gems.rb +++ b/tool/sync_default_gems.rb @@ -266,6 +266,7 @@ def lib((upstream, branch), gemspec_in_subdir: false) ["ext/stringio", "ext/stringio"], ["test/stringio", "test/stringio"], ["stringio.gemspec", "ext/stringio/stringio.gemspec"], + ["doc/stringio", "doc/stringio"], ], exclude: [ "ext/stringio/README.md", ]), From 240afe50a1d3790d1570ad7bbc16f03bc8512b47 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 7 Dec 2025 00:38:25 +0900 Subject: [PATCH 02/16] Suppress noisy outputs Fix up ruby/ruby#14700. --- test/-ext-/scheduler/test_interrupt_with_scheduler.rb | 4 +++- tool/sync_default_gems.rb | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/-ext-/scheduler/test_interrupt_with_scheduler.rb b/test/-ext-/scheduler/test_interrupt_with_scheduler.rb index 3f9a7f55a07d11..42a109b98b51a6 100644 --- a/test/-ext-/scheduler/test_interrupt_with_scheduler.rb +++ b/test/-ext-/scheduler/test_interrupt_with_scheduler.rb @@ -13,6 +13,8 @@ def setup def test_without_handle_interrupt_signal_works IO.pipe do |input, output| pid = fork do + STDERR.reopen(output) + scheduler = Scheduler.new Fiber.set_scheduler scheduler @@ -33,7 +35,7 @@ def test_without_handle_interrupt_signal_works assert_equal "ready\n", input.gets sleep 0.1 # Ensure the child is in the blocking loop - $stderr.puts "Sending interrupt" + # $stderr.puts "Sending interrupt" Process.kill(:INT, pid) reaper = Thread.new do diff --git a/tool/sync_default_gems.rb b/tool/sync_default_gems.rb index 811e72fc008eff..c2f352d797ffc8 100755 --- a/tool/sync_default_gems.rb +++ b/tool/sync_default_gems.rb @@ -266,7 +266,6 @@ def lib((upstream, branch), gemspec_in_subdir: false) ["ext/stringio", "ext/stringio"], ["test/stringio", "test/stringio"], ["stringio.gemspec", "ext/stringio/stringio.gemspec"], - ["doc/stringio", "doc/stringio"], ], exclude: [ "ext/stringio/README.md", ]), From 7319db44fc835d04616369b7b61c33e57960dcde Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 6 Dec 2025 17:53:54 +0900 Subject: [PATCH 03/16] [ruby/pathname] Define `to_path` alias directly The constant `TO_PATH` was defined for 1.9 compatibility but the code for it was dropped 10 years ago. https://github.com/ruby/pathname/commit/95ad4ceb19 --- pathname_builtin.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 1dedf5e08df546..d1b2947107a837 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -195,9 +195,6 @@ class Pathname # :stopdoc: - # to_path is implemented so Pathname objects are usable with File.open, etc. - TO_PATH = :to_path - SAME_PATHS = if File::FNM_SYSCASE.nonzero? # Avoid #zero? here because #casecmp can return nil. proc {|a, b| a.casecmp(b) == 0} @@ -264,7 +261,7 @@ def to_s end # to_path is implemented so Pathname objects are usable with File.open, etc. - alias_method TO_PATH, :to_s + alias to_path to_s def inspect # :nodoc: "#<#{self.class}:#{@path}>" From b675deeeb175b6434bf6abaa747ca3a471eb18ad Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 6 Dec 2025 18:10:44 +0900 Subject: [PATCH 04/16] [ruby/pathname] Use `File.path` for conversion to path name This method has been defined since 1.9, as the standard conversion procedure. https://github.com/ruby/pathname/commit/8f582dc65d --- pathname_builtin.rb | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index d1b2947107a837..0dee1446c07746 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -212,17 +212,7 @@ class Pathname # If +path+ contains a NUL character (\0), an ArgumentError is raised. # def initialize(path) - unless String === path - path = path.to_path if path.respond_to? :to_path - path = path.to_str if path.respond_to? :to_str - raise TypeError, "Pathname.new requires a String, #to_path or #to_str" unless String === path - end - - if path.include?("\0") - raise ArgumentError, "pathname contains \\0: #{path.inspect}" - end - - @path = path.dup + @path = File.path(path).dup end def freeze From da3b7d5ee3afb5ae8e97a906d114b56242a5ff27 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 6 Dec 2025 18:13:58 +0900 Subject: [PATCH 05/16] [ruby/pathname] Freeze and hide internal constants https://github.com/ruby/pathname/commit/60f5d58d73 --- pathname_builtin.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 0dee1446c07746..3d78619c9e4041 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -201,6 +201,8 @@ class Pathname else proc {|a, b| a == b} end + SAME_PATHS.freeze + private_constant :SAME_PATHS attr_reader :path protected :path @@ -307,6 +309,9 @@ def sub_ext(repl) SEPARATOR_LIST = Regexp.quote File::SEPARATOR SEPARATOR_PAT = /#{SEPARATOR_LIST}/ end + SEPARATOR_LIST.freeze + SEPARATOR_PAT.freeze + private_constant :SEPARATOR_LIST, :SEPARATOR_LIST if File.dirname('A:') == 'A:.' # DOSish drive letter # Regexp that matches an absolute path. @@ -314,6 +319,7 @@ def sub_ext(repl) else ABSOLUTE_PATH = /\A#{SEPARATOR_PAT}/ end + ABSOLUTE_PATH.freeze private_constant :ABSOLUTE_PATH # :startdoc: From 2e828dd98feeec5bab5ea85f0661638524004a01 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 6 Dec 2025 10:37:56 -0500 Subject: [PATCH 06/16] Fix strict aliasing warning in ruby_swap128_int The following warnings are emitted. We can use type punning to prevent strict aliasing violations. io_buffer.c:1935:23: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing] 1935 | rb_uint128_t u = *(rb_uint128_t*)&x; | ^~~~~~~~~~~~~~~~~ io_buffer.c:1937:13: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing] 1937 | return *(rb_int128_t*)&swapped; | --- io_buffer.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/io_buffer.c b/io_buffer.c index 0d2cbdb4b7e704..b81527ff71147a 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -1928,13 +1928,19 @@ ruby_swap128_uint(rb_uint128_t x) return result; } +union uint128_int128_conversion { + rb_uint128_t uint128; + rb_int128_t int128; +}; + static inline rb_int128_t ruby_swap128_int(rb_int128_t x) { - // Cast to unsigned, swap, then cast back - rb_uint128_t u = *(rb_uint128_t*)&x; - rb_uint128_t swapped = ruby_swap128_uint(u); - return *(rb_int128_t*)&swapped; + union uint128_int128_conversion conversion = { + .int128 = x + }; + conversion.uint128 = ruby_swap128_uint(conversion.uint128); + return conversion.int128; } #define IO_BUFFER_DECLARE_TYPE(name, type, endian, wrap, unwrap, swap) \ From 9dfb7bd7d44ac99bc4d7233cef00e0e6e0743905 Mon Sep 17 00:00:00 2001 From: Theo Buehler Date: Fri, 5 Dec 2025 13:14:45 +0100 Subject: [PATCH 07/16] [ruby/openssl] const correct ossl_bin2hex() This helper only reads from its in parameter. Making that const avoids a couple of casts in an upcoming change. https://github.com/ruby/openssl/commit/970d5764e3 --- ext/openssl/ossl.c | 2 +- ext/openssl/ossl.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/openssl/ossl.c b/ext/openssl/ossl.c index a19ff23b107c88..5fd6bff98b7bbe 100644 --- a/ext/openssl/ossl.c +++ b/ext/openssl/ossl.c @@ -122,7 +122,7 @@ ossl_buf2str(char *buf, int len) } void -ossl_bin2hex(unsigned char *in, char *out, size_t inlen) +ossl_bin2hex(const unsigned char *in, char *out, size_t inlen) { const char *hex = "0123456789abcdef"; size_t i; diff --git a/ext/openssl/ossl.h b/ext/openssl/ossl.h index 93deafb4b68363..0b479a7200a1f4 100644 --- a/ext/openssl/ossl.h +++ b/ext/openssl/ossl.h @@ -131,7 +131,7 @@ do{\ * Convert binary string to hex string. The caller is responsible for * ensuring out has (2 * len) bytes of capacity. */ -void ossl_bin2hex(unsigned char *in, char *out, size_t len); +void ossl_bin2hex(const unsigned char *in, char *out, size_t len); /* * Our default PEM callback From d3aa7b889f572237467156f3c6bc3c68ef45e9c4 Mon Sep 17 00:00:00 2001 From: Theo Buehler Date: Fri, 5 Dec 2025 13:29:59 +0100 Subject: [PATCH 08/16] [ruby/openssl] Convert ossl_ocsp.c to opaque ASN1_STRING OpenSSL plans to make asn1_string_st opaque, the struct underlying most ASN.1 types such as ASN1_*STRING, ASN1_ENUMERATED, ASN1_INTEGER, etc. Most of ruby/openssl's C code can be straigtforwardly converted to use accessors available since OpenSS https://github.com/ruby/openssl/commit/374262435a --- ext/openssl/ossl_ocsp.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ext/openssl/ossl_ocsp.c b/ext/openssl/ossl_ocsp.c index 97ab24c347b556..93d8bc85671eac 100644 --- a/ext/openssl/ossl_ocsp.c +++ b/ext/openssl/ossl_ocsp.c @@ -1550,8 +1550,9 @@ ossl_ocspcid_get_issuer_name_hash(VALUE self) GetOCSPCertId(self, id); OCSP_id_get0_info(&name_hash, NULL, NULL, NULL, id); - ret = rb_str_new(NULL, name_hash->length * 2); - ossl_bin2hex(name_hash->data, RSTRING_PTR(ret), name_hash->length); + ret = rb_str_new(NULL, ASN1_STRING_length(name_hash) * 2); + ossl_bin2hex(ASN1_STRING_get0_data(name_hash), RSTRING_PTR(ret), + ASN1_STRING_length(name_hash)); return ret; } @@ -1573,8 +1574,9 @@ ossl_ocspcid_get_issuer_key_hash(VALUE self) GetOCSPCertId(self, id); OCSP_id_get0_info(NULL, NULL, &key_hash, NULL, id); - ret = rb_str_new(NULL, key_hash->length * 2); - ossl_bin2hex(key_hash->data, RSTRING_PTR(ret), key_hash->length); + ret = rb_str_new(NULL, ASN1_STRING_length(key_hash) * 2); + ossl_bin2hex(ASN1_STRING_get0_data(key_hash), RSTRING_PTR(ret), + ASN1_STRING_length(key_hash)); return ret; } From 98c151b0e55e25217334a94c17102ea8382027f2 Mon Sep 17 00:00:00 2001 From: Theo Buehler Date: Fri, 5 Dec 2025 13:44:18 +0100 Subject: [PATCH 09/16] [ruby/openssl] Convert some of ossl_asn1.c to opaque ASN1_STRING This uses the normal accessors but leaves out BIT STRINGS, which will need compat implementations for ASN1_BIT_STRING_get_length() and ASN1_BIT_STRING_set1() for older libcryptos. https://github.com/openssl/openssl/issues/29184 https://github.com/openssl/openssl/issues/29185 https://github.com/ruby/openssl/commit/ba3d1cc5c2 --- ext/openssl/ossl_asn1.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ext/openssl/ossl_asn1.c b/ext/openssl/ossl_asn1.c index b21a79484ccc4a..dc72df5a63fe3e 100644 --- a/ext/openssl/ossl_asn1.c +++ b/ext/openssl/ossl_asn1.c @@ -114,7 +114,8 @@ ossl_time_split(VALUE time, time_t *sec, int *days) VALUE asn1str_to_str(const ASN1_STRING *str) { - return rb_str_new((const char *)str->data, str->length); + return rb_str_new((const char *)ASN1_STRING_get0_data(str), + ASN1_STRING_length(str)); } /* @@ -129,7 +130,7 @@ asn1integer_to_num(const ASN1_INTEGER *ai) if (!ai) { ossl_raise(rb_eTypeError, "ASN1_INTEGER is NULL!"); } - if (ai->type == V_ASN1_ENUMERATED) + if (ASN1_STRING_type(ai) == V_ASN1_ENUMERATED) /* const_cast: workaround for old OpenSSL */ bn = ASN1_ENUMERATED_to_BN((ASN1_ENUMERATED *)ai, NULL); else From a07997bf8124b8aac516f8f70388e86fd24f4a2b Mon Sep 17 00:00:00 2001 From: Theo Buehler Date: Fri, 5 Dec 2025 13:52:04 +0100 Subject: [PATCH 10/16] [ruby/openssl] Convert ossl_ns_spki.c to opaque ASN1_STRING https://github.com/ruby/openssl/commit/0941ebbda5 --- ext/openssl/ossl_ns_spki.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ext/openssl/ossl_ns_spki.c b/ext/openssl/ossl_ns_spki.c index 1d1498824620a9..8440c2ee82b909 100644 --- a/ext/openssl/ossl_ns_spki.c +++ b/ext/openssl/ossl_ns_spki.c @@ -230,13 +230,12 @@ ossl_spki_get_challenge(VALUE self) NETSCAPE_SPKI *spki; GetSPKI(self, spki); - if (spki->spkac->challenge->length <= 0) { + if (ASN1_STRING_length(spki->spkac->challenge) <= 0) { OSSL_Debug("Challenge.length <= 0?"); return rb_str_new(0, 0); } - return rb_str_new((const char *)spki->spkac->challenge->data, - spki->spkac->challenge->length); + return asn1str_to_str(spki->spkac->challenge); } /* From 38ad0806d7270926ff6fc5c23092aa3e822f386b Mon Sep 17 00:00:00 2001 From: Theo Buehler Date: Fri, 5 Dec 2025 13:54:49 +0100 Subject: [PATCH 11/16] [ruby/openssl] Convert ossl_ts.c to opaque ASN1_STRING https://github.com/ruby/openssl/commit/8945f379b3 --- ext/openssl/ossl_ts.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ext/openssl/ossl_ts.c b/ext/openssl/ossl_ts.c index 615eedc016a0f4..b31a854a63e9ac 100644 --- a/ext/openssl/ossl_ts.c +++ b/ext/openssl/ossl_ts.c @@ -259,7 +259,7 @@ ossl_ts_req_get_msg_imprint(VALUE self) mi = TS_REQ_get_msg_imprint(req); hashed_msg = TS_MSG_IMPRINT_get_msg(mi); - ret = rb_str_new((const char *)hashed_msg->data, hashed_msg->length); + ret = asn1str_to_str(hashed_msg); return ret; } @@ -470,7 +470,7 @@ ossl_ts_req_to_der(VALUE self) ossl_raise(eTimestampError, "Message imprint missing algorithm"); hashed_msg = TS_MSG_IMPRINT_get_msg(mi); - if (!hashed_msg->length) + if (!ASN1_STRING_length(hashed_msg)) ossl_raise(eTimestampError, "Message imprint missing hashed message"); return asn1_to_der((void *)req, (int (*)(void *, unsigned char **))i2d_TS_REQ); @@ -981,7 +981,7 @@ ossl_ts_token_info_get_msg_imprint(VALUE self) GetTSTokenInfo(self, info); mi = TS_TST_INFO_get_msg_imprint(info); hashed_msg = TS_MSG_IMPRINT_get_msg(mi); - ret = rb_str_new((const char *)hashed_msg->data, hashed_msg->length); + ret = asn1str_to_str(hashed_msg); return ret; } From 8d3da814c03e06ce331e7022b87500943b57fa4e Mon Sep 17 00:00:00 2001 From: Theo Buehler Date: Fri, 5 Dec 2025 14:15:08 +0100 Subject: [PATCH 12/16] [ruby/openssl] Convert ossl_x509ext.c to opaque ASN1_STRING https://github.com/ruby/openssl/commit/a41cf28bab --- ext/openssl/ossl_x509ext.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/openssl/ossl_x509ext.c b/ext/openssl/ossl_x509ext.c index 85e2eff7a275c6..ef66ecc3fe8390 100644 --- a/ext/openssl/ossl_x509ext.c +++ b/ext/openssl/ossl_x509ext.c @@ -402,7 +402,7 @@ ossl_x509ext_get_value_der(VALUE obj) if ((value = X509_EXTENSION_get_data(ext)) == NULL) ossl_raise(eX509ExtError, NULL); - return rb_str_new((const char *)value->data, value->length); + return asn1str_to_str(value); } static VALUE From 1f0d41aa4d1a3b36e9640e5e8e64c030c71ed613 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 6 Dec 2025 12:07:08 -0500 Subject: [PATCH 13/16] [ruby/date] Call rb_gc_register_mark_object after object allocation It's possible that both half_days_in_day and day_in_nanoseconds are Ruby objects, which means that creating day_in_nanoseconds may trigger GC. Since half_days_in_day is not registered as a mark object until after day_in_nanoseconds is allocated, the GC may reclaim half_days_in_day. We can see this crash: ruby(rb_print_backtrace+0xb) [0x63a373c0] vm_dump.c:1105 ruby(rb_vm_bugreport) vm_dump.c:1450 ruby(rb_assert_failure_detail+0xdb) [0x6371d3a2] error.c:1216 ruby(RB_FL_TEST_RAW+0x0) [0x6371d3d5] error.c:1192 ruby(rb_assert_failure) (null):0 ruby(rb_gc_impl_writebarrier+0xb4) [0x636f01e4] gc/default/default.c:6103 ruby(pin_array_list_append+0x72) [0x638f9787] include/ruby/internal/gc.h:788 ruby(rb_vm_register_global_object) vm.c:4713 ruby(rb_gc_register_mark_object+0x3a) [0x6374144a] gc.c:3449 .ext/i686-linux-gnu/date_core.so(Init_date_core+0x204) [0xdbec86c4] ext/date/date_core.c:9511 .ext/i686-linux-gnu/date_core.so(Init_date_core) (null):0 ruby(dln_load_and_init+0x71) [0x6392c541] dln.c:521 ruby(dln_load_feature+0xd2) [0x6392c7d2] dln.c:566 ruby(load_ext+0xc3) [0x637931b3] load.c:1210 ruby(rb_vm_pop_frame+0x0) [0x638f80cd] vm.c:3120 ruby(rb_vm_call_cfunc_in_box) vm.c:3122 ruby(rb_long2num_inline+0x0) [0x637956f8] load.c:1353 ruby(require_internal) load.c:1354 ruby(rb_require_string_internal+0x60) [0x63795fa1] load.c:1457 ruby(rb_require_string) load.c:1443 https://github.com/ruby/date/commit/cbec5948e0 --- ext/date/date_core.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ext/date/date_core.c b/ext/date/date_core.c index cf8ea3c0a44990..6bcf272b62d8b0 100644 --- a/ext/date/date_core.c +++ b/ext/date/date_core.c @@ -9496,6 +9496,7 @@ Init_date_core(void) sym_zone = ID2SYM(rb_intern_const("zone")); half_days_in_day = rb_rational_new2(INT2FIX(1), INT2FIX(2)); + rb_gc_register_mark_object(half_days_in_day); #if (LONG_MAX / DAY_IN_SECONDS) > SECOND_IN_NANOSECONDS day_in_nanoseconds = LONG2NUM((long)DAY_IN_SECONDS * @@ -9507,8 +9508,6 @@ Init_date_core(void) day_in_nanoseconds = f_mul(INT2FIX(DAY_IN_SECONDS), INT2FIX(SECOND_IN_NANOSECONDS)); #endif - - rb_gc_register_mark_object(half_days_in_day); rb_gc_register_mark_object(day_in_nanoseconds); positive_inf = +INFINITY; From 47c0dae188162798c50fdb2580d852ceec89f2ec Mon Sep 17 00:00:00 2001 From: Theo Buehler Date: Sat, 6 Dec 2025 17:58:56 +0100 Subject: [PATCH 14/16] [ruby/openssl] asn1integer_to_num: don't cast away const ASN1_ENUMERATED_to_BN() has been const-correct for a long time in all supported libcrytos, so we can remove this workaround. https://github.com/ruby/openssl/commit/d0f36a7c65 --- ext/openssl/ossl_asn1.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ext/openssl/ossl_asn1.c b/ext/openssl/ossl_asn1.c index dc72df5a63fe3e..628140a75e3ea4 100644 --- a/ext/openssl/ossl_asn1.c +++ b/ext/openssl/ossl_asn1.c @@ -131,8 +131,7 @@ asn1integer_to_num(const ASN1_INTEGER *ai) ossl_raise(rb_eTypeError, "ASN1_INTEGER is NULL!"); } if (ASN1_STRING_type(ai) == V_ASN1_ENUMERATED) - /* const_cast: workaround for old OpenSSL */ - bn = ASN1_ENUMERATED_to_BN((ASN1_ENUMERATED *)ai, NULL); + bn = ASN1_ENUMERATED_to_BN(ai, NULL); else bn = ASN1_INTEGER_to_BN(ai, NULL); From 87bc106b8790d5beaccc46555538424fccb1f50f Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Sat, 6 Dec 2025 17:37:40 +0000 Subject: [PATCH 15/16] [ruby/stringio] [DOC] Change link to on-page https://github.com/ruby/stringio/commit/a7c118d786 --- ext/stringio/stringio.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/stringio/stringio.c b/ext/stringio/stringio.c index 5556426fdc387a..9240929646d5ea 100644 --- a/ext/stringio/stringio.c +++ b/ext/stringio/stringio.c @@ -1888,7 +1888,7 @@ strio_truncate(VALUE self, VALUE len) * external_encoding -> encoding or nil * * Returns an Encoding object that represents the encoding of the string; - * see {Encoding}[rdoc-ref:Encoding]: + * see {Encodings}[rdoc-ref:StringIO@Encodings]: * * strio = StringIO.new('foo') * strio.external_encoding # => # From 588347a088625b5c16eedbc5f3a7a1189a427e25 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 6 Dec 2025 11:47:34 -0500 Subject: [PATCH 16/16] Fix id2ref for multi-Ractor The id2ref table needs to be under a VM lock to ensure there are no race conditions. The following script crashes: o = Object.new ObjectSpace._id2ref(o.object_id) 10.times.map do Ractor.new do 10_000.times do a = Object.new a.object_id end end end.map(&:value) With: [BUG] Object ID seen, but not in _id2ref table: object_id=2800 object=T_OBJECT ruby 4.0.0dev (2025-12-06T15:15:43Z ractor-id2ref-fix e7f9abdc91) +PRISM [x86_64-linux] -- Control frame information ----------------------------------------------- c:0001 p:---- s:0003 e:000002 l:y b:---- DUMMY [FINISH] -- Threading information --------------------------------------------------- Total ractor count: 5 Ruby thread count for this ractor: 1 -- C level backtrace information ------------------------------------------- miniruby(rb_print_backtrace+0x14) [0x6047d09b2dff] vm_dump.c:1105 miniruby(rb_vm_bugreport) vm_dump.c:1450 miniruby(rb_bug_without_die_internal+0x5f) [0x6047d066bf57] error.c:1098 miniruby(rb_bug) error.c:1116 miniruby(rb_gc_get_ractor_newobj_cache+0x0) [0x6047d066c8dd] gc.c:2052 miniruby(gc_sweep_plane+0xad) [0x6047d079276d] gc/default/default.c:3513 miniruby(gc_sweep_page) gc/default/default.c:3605 miniruby(gc_sweep_step) gc/default/default.c:3886 miniruby(gc_sweep+0x1ba) [0x6047d0794cfa] gc/default/default.c:4154 miniruby(gc_start+0xbf2) [0x6047d0796742] gc/default/default.c:6519 miniruby(heap_prepare+0xcc) [0x6047d079748c] gc/default/default.c:2090 miniruby(heap_next_free_page) gc/default/default.c:2305 miniruby(newobj_cache_miss) gc/default/default.c:2412 miniruby(newobj_alloc+0xd) [0x6047d0798ff5] gc/default/default.c:2436 miniruby(rb_gc_impl_new_obj) gc/default/default.c:2515 miniruby(newobj_of) gc.c:996 miniruby(rb_wb_protected_newobj_of) gc.c:1046 miniruby(str_alloc_embed+0x28) [0x6047d08fda18] string.c:1019 miniruby(str_enc_new) string.c:1069 miniruby(prep_io+0x5) [0x6047d07cda14] io.c:9305 miniruby(prep_stdio) io.c:9347 miniruby(rb_io_prep_stdin) io.c:9365 miniruby(thread_start_func_2+0x77c) [0x6047d093a55c] thread.c:679 miniruby(thread_sched_lock_+0x0) [0x6047d093aacd] thread_pthread.c:2241 miniruby(co_start) thread_pthread_mn.c:469 --- bootstraptest/test_ractor.rb | 17 +++++++++++++++++ gc.c | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index bec742da4be0d5..af739ba4bc7fd7 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -1116,6 +1116,23 @@ class C end.value RUBY +# Inserting into the id2ref table should be Ractor-safe +assert_equal 'ok', <<~'RUBY' + # Force all calls to Kernel#object_id to insert into the id2ref table + ObjectSpace._id2ref(Object.new.object_id) + + 10.times.map do + Ractor.new do + 10_000.times do + a = Object.new + a.object_id + end + end + end.map(&:value) + + :ok +RUBY + # Ractor.make_shareable(obj) assert_equal 'true', <<~'RUBY', frozen_string_literal: false class C diff --git a/gc.c b/gc.c index 4f5f041fb348fb..c660d1491f3a6d 100644 --- a/gc.c +++ b/gc.c @@ -1905,7 +1905,9 @@ object_id0(VALUE obj) RUBY_ASSERT(rb_shape_obj_has_id(obj)); if (RB_UNLIKELY(id2ref_tbl)) { - st_insert(id2ref_tbl, (st_data_t)id, (st_data_t)obj); + RB_VM_LOCKING() { + st_insert(id2ref_tbl, (st_data_t)id, (st_data_t)obj); + } } return id; }