From fafb55877aaf34592278eb3ef9ba3f61473d0a56 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Sun, 3 May 2026 21:29:09 -0700 Subject: [PATCH 1/5] Avoid issues with refinement zsuper lookup Without this patch, there are multiple issues when defining a zsuper method via a refinement. These issues are due to the orig_me entry for the zsuper method not being updated when the method it refers to is modified via: * Method being redefined (in same or closer ancestor) * Method being removed * Method being undef-ed (in same or closer ancestor) * Method being overridden by method in included module * Method being overridden by method in prepended module * Method being defined in an included/prepended module at time of refinement This includes a comprehensive test suite for these cases, using both zsuper methods and iseq methods that call super. Without the changes to refinement method lookup, the following types of errors occur for refinement zsuper methods (all of the iseq methods that call super work correctly with or without these changes): * Incorrect result (not returning result of expected method, not raising NoMethodError if the method was removed or has been undef-ed) * SystemStackError: stack level too deep * NotImplementedError: false() function is unimplemented on this machine This avoids the issues with refinement zsuper lookup by turning the zsuper into a cfunc that uses rb_call_super_kw. This is not a perfect solution, for two reasons: * cfuncs are slower than zsuper * arity/parameters for the method not as helpful (you It may possible to avoid these issues by clearing method caches in more cases. However, I think that would require a lot of extra work, since you cannot just clear the method cache for the current class. You would need to clear it for all subclasses that are refined, and if this is a module, do the same for all classes that include/prepend the module, as well as any subclasses of those classes. Considering the need for refinement zsuper methods is very rare, the performance and arity/parameters issues seem acceptable (to me). Fixes [Bug #22022] --- test/ruby/test_refinement.rb | 580 +++++++++++++++++++++++++++++++++++ vm_method.c | 21 +- 2 files changed, 596 insertions(+), 5 deletions(-) diff --git a/test/ruby/test_refinement.rb b/test/ruby/test_refinement.rb index f4fe2fc44b5b61..daff07ecb0abff 100644 --- a/test/ruby/test_refinement.rb +++ b/test/ruby/test_refinement.rb @@ -1058,6 +1058,586 @@ def chop! end; end + { + zsuper: "public :a", + super: "def a = super" + }.each do |desc, method_def| + define_method :"test_modify_#{desc}_refinement_method_in_superclass" do + assert_separately([], <<-"end;") + class A + private def a = :a + alias a a + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:a, B.new.a) + + class A + def a = :b + end + assert_equal(:b, B.new.a) + end; + end + + define_method :"test_modify_#{desc}_refinement_method_in_module_prepended_to_superclass" do + assert_separately([], <<-"end;") + module M + private def a = :a + alias a a + end + + class A + prepend M + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:a, B.new.a) + + module M + def a = :b + end + assert_equal(:b, B.new.a) + end; + end + + define_method :"test_modify_#{desc}_refinement_method_in_module_included_in_superclass" do + assert_separately([], <<-"end;") + module M + private def a = :a + alias a a + end + + class A + include M + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:a, B.new.a) + + module M + def a = :b + end + assert_equal(:b, B.new.a) + end; + end + + define_method :"test_remove_#{desc}_refinement_method_from_superclass" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + class B < A + private def a = :b + end + + class C < B + end + + module R + refine C do + #{method_def} + end + end + using R + assert_equal(:b, C.new.a) + + class B + remove_method(:a) + end + assert_equal(:a, C.new.a) + end; + end + + define_method :"test_remove_#{desc}_refinement_method_from_module_prepended_to_superclass" do + assert_separately([], <<-"end;") + module M + private def a = :b + end + + class A + prepend M + private def a = :a + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:b, B.new.a) + + module M + remove_method(:a) + end + assert_equal(:a, B.new.a) + end; + end + + define_method :"test_remove_#{desc}_refinement_method_from_module_prepended_to_class" do + assert_separately([], <<-"end;") + module M + private def a = :b + end + + class A + prepend M + private def a = :a + end + + module R + refine A do + #{method_def} + end + end + using R + assert_equal(:b, A.new.a) + + module M + remove_method(:a) + end + assert_equal(:a, A.new.a) + end; + end + + define_method :"test_remove_#{desc}_refinement_method_from_module_included_in_superclass" do + assert_separately([], <<-"end;") + module M + private def a = :b + end + + class A + private def a = :a + end + + class B < A + include M + end + + class C < B + end + + module R + refine C do + #{method_def} + end + end + using R + assert_equal(:b, C.new.a) + + module M + remove_method(:a) + end + assert_equal(:a, C.new.a) + end; + end + + define_method :"test_remove_#{desc}_refinement_method_from_module_included_in_class" do + assert_separately([], <<-"end;") + module M + private def a = :b + end + + class A + private def a = :a + end + + class B < A + include M + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:b, B.new.a) + + module M + remove_method(:a) + end + assert_equal(:a, B.new.a) + end; + end + + define_method :"test_undef_#{desc}_refinement_method_in_superclass" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + class B < A + private def a = :b + end + + class C < B + end + + module R + refine C do + #{method_def} + end + end + using R + assert_equal(:b, C.new.a) + + class B + undef_method(:a) + end + assert_raise(NoMethodError) { C.new.a } + end; + end + + define_method :"test_undef_#{desc}_refinement_method_in_module_prepended_to_superclass" do + assert_separately([], <<-"end;") + module M + private def a = :b + end + + class A + prepend M + private def a = :a + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:b, B.new.a) + + module M + undef_method(:a) + end + assert_raise(NoMethodError) { B.new.a } + end; + end + + define_method :"test_undef_#{desc}_refinement_method_in_module_prepended_to_class" do + assert_separately([], <<-"end;") + module M + private def a = :b + end + + class A + prepend M + private def a = :a + end + + module R + refine A do + #{method_def} + end + end + using R + assert_equal(:b, A.new.a) + + module M + undef_method(:a) + end + assert_raise(NoMethodError) { A.new.a } + end; + end + + define_method :"test_undef_#{desc}_refinement_method_in_module_included_in_superclass" do + assert_separately([], <<-"end;") + module M + private def a = :b + end + + class A + private def a = :a + end + + class B < A + include M + end + + class C < B + end + + module R + refine C do + #{method_def} + end + end + using R + assert_equal(:b, C.new.a) + + module M + undef_method(:a) + end + assert_raise(NoMethodError) { C.new.a } + end; + end + + define_method :"test_undef_#{desc}_refinement_method_in_module_included_in_class" do + assert_separately([], <<-"end;") + module M + private def a = :b + end + + class A + private def a = :a + end + + class B < A + include M + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:b, B.new.a) + + module M + undef_method(:a) + end + assert_raise(NoMethodError) { B.new.a } + end; + end + + define_method :"test_override_#{desc}_refinement_method_by_prepending_to_class" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + module R + refine A do + #{method_def} + end + end + using R + assert_equal(:a, A.new.a) + + module M + def a = :b + end + A.prepend M + assert_equal(:b, A.new.a) + end; + end + + define_method :"test_override_#{desc}_refinement_method_by_prepending_to_superclass" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:a, B.new.a) + + module M + def a = :b + end + A.prepend M + assert_equal(:b, B.new.a) + end; + end + + define_method :"test_override_#{desc}_refinement_method_by_including_in_class" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:a, B.new.a) + + module M + def a = :b + end + B.include M + assert_equal(:b, B.new.a) + end; + end + + define_method :"test_override_#{desc}_refinement_method_by_including_in_superclass" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + class B < A + end + + class C < B + end + + module R + refine C do + #{method_def} + end + end + using R + assert_equal(:a, C.new.a) + + module M + def a = :b + end + B.include M + assert_equal(:b, C.new.a) + end; + end + + define_method :"test_override_#{desc}_refinement_method_by_prepending_undef_to_class" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + module R + refine A do + #{method_def} + end + end + using R + assert_equal(:a, A.new.a) + + module M + def a = :b + undef_method :a + end + A.prepend M + assert_raise(NoMethodError) { A.new.a } + end; + end + + define_method :"test_override_#{desc}_refinement_method_by_prepending_undef_to_superclass" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:a, B.new.a) + + module M + def a = :b + undef_method :a + end + A.prepend M + assert_raise(NoMethodError) { B.new.a } + end; + end + + define_method :"test_override_#{desc}_refinement_method_by_including_undef_in_class" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + class B < A + end + + module R + refine B do + #{method_def} + end + end + using R + assert_equal(:a, B.new.a) + + module M + def a = :b + undef_method :a + end + B.include M + assert_raise(NoMethodError) { B.new.a } + end; + end + + define_method :"test_override_#{desc}_refinement_method_by_including_undef_in_superclass" do + assert_separately([], <<-"end;") + class A + private def a = :a + end + + class B < A + end + + class C < B + end + + module R + refine C do + #{method_def} + end + end + using R + assert_equal(:a, C.new.a) + + module M + def a = :b + undef_method :a + end + B.include M + assert_raise(NoMethodError) { C.new.a } + end; + end + end + def test_instance_methods bug8881 = '[ruby-core:57080] [Bug #8881]' assert_not_include(Foo.instance_methods(false), :z, bug8881) diff --git a/vm_method.c b/vm_method.c index 9a090d1eb7eaed..e9f0d85cae4c0c 100644 --- a/vm_method.c +++ b/vm_method.c @@ -1370,6 +1370,12 @@ check_override_opt_method(VALUE klass, VALUE mid) } } +static VALUE +zsuper_to_super(int argc, VALUE *argv, VALUE self) +{ + return rb_call_super_kw(argc, argv, RB_PASS_CALLED_KEYWORDS); +} + static inline rb_method_entry_t* search_method0(VALUE klass, ID id, VALUE *defined_class_ptr, bool skip_refined); /* * klass->method_table[mid] = method_entry(defined_class, visi, def) @@ -1386,6 +1392,7 @@ rb_method_entry_make(VALUE klass, ID mid, VALUE defined_class, rb_method_visibil st_data_t data; int make_refined = 0; VALUE orig_klass; + bool turn_zsuper_to_super = false; if (NIL_P(klass)) { klass = rb_cObject; @@ -1411,12 +1418,10 @@ rb_method_entry_make(VALUE klass, ID mid, VALUE defined_class, rb_method_visibil if (RB_TYPE_P(klass, T_MODULE) && FL_TEST(klass, RMODULE_IS_REFINEMENT)) { VALUE refined_class = rb_refinement_module_get_refined_class(klass); - bool search_superclass = type == VM_METHOD_TYPE_ZSUPER && !lookup_method_table(refined_class, mid); - rb_add_refined_method_entry(refined_class, mid); - if (search_superclass) { - rb_method_entry_t *me = lookup_method_table(refined_class, mid); - RB_OBJ_WRITE(me, &me->def->body.refined.orig_me, search_method0(refined_class, mid, NULL, true)); + if (type == VM_METHOD_TYPE_ZSUPER) { + turn_zsuper_to_super = true; } + rb_add_refined_method_entry(refined_class, mid); } if (type == VM_METHOD_TYPE_REFINED) { rb_method_entry_t *old_me = lookup_method_table(RCLASS_ORIGIN(klass), mid); @@ -1479,6 +1484,12 @@ rb_method_entry_make(VALUE klass, ID mid, VALUE defined_class, rb_method_visibil me = rb_method_entry_create(mid, defined_class, visi, NULL); if (def == NULL) { def = rb_method_definition_create(type, original_id); + if (turn_zsuper_to_super) { + def->type = VM_METHOD_TYPE_CFUNC; + def->body.cfunc.func = (rb_cfunc_t)zsuper_to_super; + def->body.cfunc.invoker = ractor_safe_call_cfunc_m1; + def->body.cfunc.argc = -1; + } } rb_method_definition_set(me, def, opts); From 4f28236d5115aef71573693bef85c3bee7cef619 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 6 May 2026 21:21:20 -0400 Subject: [PATCH 2/5] [DOC] Fix hash style in ObjectSpace.count_objects_size --- ext/objspace/objspace.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/objspace/objspace.c b/ext/objspace/objspace.c index 8a4128e1b4e11b..ffe184573592f3 100644 --- a/ext/objspace/objspace.c +++ b/ext/objspace/objspace.c @@ -230,7 +230,7 @@ type2sym(enum ruby_value_type i) * T_DATA may be wrong. * * It returns a hash as: - * {:TOTAL=>1461154, :T_CLASS=>158280, :T_MODULE=>20672, :T_STRING=>527249, ...} + * {TOTAL: 1461154, T_CLASS: 158280, T_MODULE: 20672, T_STRING: 527249, ...} * * If the optional argument, result_hash, is given, * it is overwritten and returned. From c4df92d6dbda68937eb033b17403a738cc551999 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 02:11:13 +0000 Subject: [PATCH 3/5] Bump actions/labeler in the github-actions group across 1 directory Bumps the github-actions group with 1 update in the / directory: [actions/labeler](https://github.com/actions/labeler). Updates `actions/labeler` from 6.0.1 to 6.1.0 - [Release notes](https://github.com/actions/labeler/releases) - [Commits](https://github.com/actions/labeler/compare/634933edcd8ababfe52f92936142cc22ac488b1b...f27b608878404679385c85cfa523b85ccb86e213) --- updated-dependencies: - dependency-name: actions/labeler dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/labeler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index c7eb2865f11485..d0a8024b053b0d 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -12,4 +12,4 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 + - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0 From 66b1559d39aac8dec41a8ab4d207be9375ecf6d1 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 7 May 2026 13:54:05 +0900 Subject: [PATCH 4/5] [ruby/stringio] Check arguments in pread https://github.com/ruby/stringio/commit/efe8351676 --- ext/stringio/stringio.c | 30 +++++++++++++++++++----------- test/stringio/test_stringio.rb | 4 +++- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/ext/stringio/stringio.c b/ext/stringio/stringio.c index 09757a283eaf7c..e9ca42e3255da6 100644 --- a/ext/stringio/stringio.c +++ b/ext/stringio/stringio.c @@ -161,6 +161,16 @@ strio_substr(struct StringIO *ptr, long pos, long len, rb_encoding *enc) return enc_subseq(str, pos, len, enc); } +static VALUE +strio_readbuf(struct StringIO *ptr, VALUE str) +{ + if (!NIL_P(str)) { + StringValue(str); + rb_str_modify(str); + } + return str; +} + #define StringIO(obj) get_strio(obj) #define StringIOForRead(obj) get_strio_for_read(obj) @@ -1684,11 +1694,7 @@ strio_read(int argc, VALUE *argv, VALUE self) switch (argc) { case 2: - str = argv[1]; - if (!NIL_P(str)) { - StringValue(str); - rb_str_modify(str); - } + str = strio_readbuf(ptr, argv[1]); /* fall through */ case 1: if (!NIL_P(argv[0])) { @@ -1753,6 +1759,8 @@ static VALUE strio_pread(int argc, VALUE *argv, VALUE self) { VALUE rb_len, rb_offset, rb_buf; + struct StringIO *ptr = readable(self); + rb_scan_args(argc, argv, "21", &rb_len, &rb_offset, &rb_buf); long len = NUM2LONG(rb_len); long offset = NUM2LONG(rb_offset); @@ -1761,6 +1769,12 @@ strio_pread(int argc, VALUE *argv, VALUE self) rb_raise(rb_eArgError, "negative string size (or size too big): %" PRIsVALUE, rb_len); } + if (offset < 0) { + rb_syserr_fail_str(EINVAL, rb_sprintf("pread: Invalid offset argument: %" PRIsVALUE, rb_offset)); + } + + rb_buf = strio_readbuf(ptr, rb_buf); + if (len == 0) { if (NIL_P(rb_buf)) { return rb_str_new("", 0); @@ -1768,12 +1782,6 @@ strio_pread(int argc, VALUE *argv, VALUE self) return rb_buf; } - if (offset < 0) { - rb_syserr_fail_str(EINVAL, rb_sprintf("pread: Invalid offset argument: %" PRIsVALUE, rb_offset)); - } - - struct StringIO *ptr = readable(self); - if (outside_p(ptr, offset)) { rb_eof_error(); } diff --git a/test/stringio/test_stringio.rb b/test/stringio/test_stringio.rb index 656c0bb91d25c5..aca250a85a21db 100644 --- a/test/stringio/test_stringio.rb +++ b/test/stringio/test_stringio.rb @@ -831,9 +831,11 @@ def test_pread assert_raise(EOFError) { f.pread(1, 5) } assert_raise(ArgumentError) { f.pread(-1, 0) } assert_raise(Errno::EINVAL) { f.pread(3, -1) } + assert_raise(Errno::EINVAL) { f.pread(0, -1) } + assert_raise(IOError) { StringIO.new(nil, "w").pread(3, 0) } + assert_raise(TypeError) { f.pread(3, 0, []) } assert_equal "".b, StringIO.new("").pread(0, 0) - assert_equal "".b, StringIO.new("").pread(0, -10) buf = "stale".b assert_equal "stale".b, StringIO.new("").pread(0, 0, buf) From 1d2d4ac734ad7c14722cca63f841bcb1faa22800 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 7 May 2026 14:31:30 +0900 Subject: [PATCH 5/5] [ruby/stringio] Cannot read into the underlying string Fix https://hackerone.com/reports/3716987 https://github.com/ruby/stringio/commit/4ed1d6b7cf --- ext/stringio/stringio.c | 3 +++ test/stringio/test_stringio.rb | 3 +++ 2 files changed, 6 insertions(+) diff --git a/ext/stringio/stringio.c b/ext/stringio/stringio.c index e9ca42e3255da6..41aa71f893afe7 100644 --- a/ext/stringio/stringio.c +++ b/ext/stringio/stringio.c @@ -167,6 +167,9 @@ strio_readbuf(struct StringIO *ptr, VALUE str) if (!NIL_P(str)) { StringValue(str); rb_str_modify(str); + if (str == ptr->string) { + rb_raise(rb_eArgError, "cannot read into the underlying string"); + } } return str; } diff --git a/test/stringio/test_stringio.rb b/test/stringio/test_stringio.rb index aca250a85a21db..0f61245a8a4686 100644 --- a/test/stringio/test_stringio.rb +++ b/test/stringio/test_stringio.rb @@ -765,6 +765,8 @@ def test_read s = "" f.read(nil, s) assert_equal(Encoding::ASCII_8BIT, s.encoding, bug20418) + + assert_raise(ArgumentError) {f.read(1, f.string)} end def test_readpartial @@ -830,6 +832,7 @@ def test_pread assert_raise(EOFError) { f.pread(1, 5) } assert_raise(ArgumentError) { f.pread(-1, 0) } + assert_raise(ArgumentError) { f.pread(0, 0, f.string) } assert_raise(Errno::EINVAL) { f.pread(3, -1) } assert_raise(Errno::EINVAL) { f.pread(0, -1) } assert_raise(IOError) { StringIO.new(nil, "w").pread(3, 0) }