From ed470686ecd0a18c3c95a53d6ee9635db01b79ab Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 19 Sep 2026 18:37:17 +0900 Subject: [PATCH 1/6] Diagnose inactive SHSTK with CPU and glibc details --- .github/workflows/shstk-amd64.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/shstk-amd64.yml b/.github/workflows/shstk-amd64.yml index a976c9bf8c4bc4..20f4edc0161e54 100644 --- a/.github/workflows/shstk-amd64.yml +++ b/.github/workflows/shstk-amd64.yml @@ -92,7 +92,14 @@ jobs: runuser -u ruby -- ./miniruby -e ' line = File.foreach("/proc/self/status").find { |entry| entry.start_with?("x86_Thread_features:") } features = line&.split(":", 2)&.last&.split || [] - abort "SHSTK is not active (x86_Thread_features: #{features.join(" ")})" unless features.include?("shstk") + unless features.include?("shstk") + warn "::notice::SHSTK is not active (x86_Thread_features: #{features.join(" ")})" + cpuinfo = File.read("/proc/cpuinfo") + model = cpuinfo[/^model name.*/] + flags = cpuinfo[/^flags.*/] + puts model, flags, ENV["GLIBC_TUNABLES"] + exit !/\buser_shstk\b/.match?(flags) + end ' - name: make check From 92cef2802ca115380e31bc0c88d25042d14d88f4 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 18 Sep 2026 22:11:24 +0900 Subject: [PATCH 2/6] Mark `rb_freeze_singleton_class` as internal `rb_freeze_singleton_class` is an implementation detail of `RB_OBJ_FREEZE`, so deprecate direct calls from extension libraries before removing its public declaration in Ruby 4.2. --- class.c | 4 ++++ include/ruby/internal/fl_type.h | 3 +++ internal/class.h | 1 + internal/error.h | 2 ++ 4 files changed, 10 insertions(+) diff --git a/class.c b/class.c index 7c6d9e3be49ea8..2f7f51b245357d 100644 --- a/class.c +++ b/class.c @@ -23,6 +23,7 @@ #include "internal.h" #include "internal/box.h" #include "internal/class.h" +#include "internal/error.h" #include "internal/eval.h" #include "internal/gc.h" #include "internal/hash.h" @@ -3000,6 +3001,9 @@ singleton_class_of(VALUE obj, bool ensure_eigenclass) return klass; } +#if RUBY_VERSION_SINCE(4, 2) +RBIMPL_TODO("make rb_freeze_singleton_class internal; remove from fl_type.h") +#endif void rb_freeze_singleton_class(VALUE attached_object) { diff --git a/include/ruby/internal/fl_type.h b/include/ruby/internal/fl_type.h index f7ec7424220fdd..20c782b4f712b1 100644 --- a/include/ruby/internal/fl_type.h +++ b/include/ruby/internal/fl_type.h @@ -355,6 +355,8 @@ ruby_fl_type { #undef RBIMPL_HAVE_ENUM_ATTRIBUTE RBIMPL_SYMBOL_EXPORT_BEGIN() + +RBIMPL_ATTR_DEPRECATED_EXT(("only for internal use")) /** * This is an implementation detail of #RB_OBJ_FREEZE(). People don't use it * directly. @@ -363,6 +365,7 @@ RBIMPL_SYMBOL_EXPORT_BEGIN() * @post `klass` gets frozen. */ void rb_freeze_singleton_class(VALUE klass); + RBIMPL_SYMBOL_EXPORT_END() RBIMPL_ATTR_PURE_UNLESS_DEBUG() diff --git a/internal/class.h b/internal/class.h index 1d608e38db2966..49a5d96521d10c 100644 --- a/internal/class.h +++ b/internal/class.h @@ -493,6 +493,7 @@ VALUE rb_keyword_error_new(const char *, VALUE); rb_classext_t *rb_class_unlink_classext(VALUE klass, const rb_box_t *box); void rb_class_classext_free(VALUE klass, rb_classext_t *ext, bool is_prime); void rb_iclass_classext_free(VALUE klass, rb_classext_t *ext, bool is_prime); +void rb_freeze_singleton_class(VALUE attached_object); RUBY_SYMBOL_EXPORT_BEGIN diff --git a/internal/error.h b/internal/error.h index 8cda52827360d6..cc065f1b5fa6db 100644 --- a/internal/error.h +++ b/internal/error.h @@ -75,6 +75,8 @@ PRINTF_ARGS(void rb_warn_deprecated_to_remove(const char *removal, const char *f PRINTF_ARGS(void rb_warn_reserved_name(const char *removal, const char *fmt, ...), 2, 3); #if RUBY_DEBUG # include "ruby/version.h" +#endif +#ifdef RUBY_API_VERSION_CODE # define RUBY_VERSION_SINCE(major, minor) (RUBY_API_VERSION_CODE >= (major) * 10000 + (minor) * 100) # define RUBY_VERSION_BEFORE(major, minor) (RUBY_API_VERSION_CODE < (major) * 10000 + (minor) * 100) # if defined(RBIMPL_WARNING_PRAGMA0) From a323b13af90ade4f890c9dd6d4ad84497d48b3e9 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 18 Sep 2026 23:04:42 +0900 Subject: [PATCH 3/6] Skip already frozen objects in `RB_OBJ_FREEZE` Avoid calling `rb_obj_freeze_inline` for already frozen objects, including immediate values, to match `rb_obj_freeze` semantics. --- include/ruby/internal/fl_type.h | 14 +++++++++++++- include/ruby/internal/intern/object.h | 6 ++---- object.c | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/include/ruby/internal/fl_type.h b/include/ruby/internal/fl_type.h index 20c782b4f712b1..2b780d018ec29a 100644 --- a/include/ruby/internal/fl_type.h +++ b/include/ruby/internal/fl_type.h @@ -88,7 +88,6 @@ #define FL_USER19 RBIMPL_CAST((VALUE)(unsigned int)RUBY_FL_USER19) /**< @old{RUBY_FL_USER19} */ #define ELTS_SHARED RUBY_ELTS_SHARED /**< @old{RUBY_ELTS_SHARED} */ -#define RB_OBJ_FREEZE rb_obj_freeze_inline /**< @alias{rb_obj_freeze_inline} */ /** @cond INTERNAL_MACRO */ #define RUBY_ELTS_SHARED RUBY_ELTS_SHARED @@ -105,6 +104,7 @@ #define RB_FL_TEST_RAW RB_FL_TEST_RAW #define RB_FL_UNSET RB_FL_UNSET #define RB_FL_UNSET_RAW RB_FL_UNSET_RAW +#define RB_OBJ_FREEZE RB_OBJ_FREEZE #define RB_OBJ_FREEZE_RAW RB_OBJ_FREEZE_RAW #define RB_OBJ_FROZEN RB_OBJ_FROZEN #define RB_OBJ_FROZEN_RAW RB_OBJ_FROZEN_RAW @@ -746,4 +746,16 @@ RB_OBJ_FREEZE_RAW(VALUE obj) rb_obj_freeze_inline(obj); } +RBIMPL_ATTR_ARTIFICIAL() +/** + * Freeze the given object if it is not frozen yet. + * + * @param[out] obj Object in question. + */ +static inline void +RB_OBJ_FREEZE(VALUE obj) +{ + if (!RB_OBJ_FROZEN(obj)) RB_OBJ_FREEZE_RAW(obj); +} + #endif /* RBIMPL_FL_TYPE_H */ diff --git a/include/ruby/internal/intern/object.h b/include/ruby/internal/intern/object.h index c008b058b63150..031230f809c93c 100644 --- a/include/ruby/internal/intern/object.h +++ b/include/ruby/internal/intern/object.h @@ -203,8 +203,7 @@ VALUE rb_obj_dup(VALUE obj); VALUE rb_obj_init_copy(VALUE src, VALUE dst); /** - * Just calls rb_obj_freeze_inline() inside. Does this make any sens to - * extension libraries? + * Same as RB_OBJ_FREEZE(), but returns the given object. * * @param[out] obj Object to freeze. * @return Verbatim `obj`. @@ -213,8 +212,7 @@ VALUE rb_obj_freeze(VALUE obj); RBIMPL_ATTR_PURE() /** - * Just calls RB_OBJ_FROZEN() inside. Does this make any sens to extension - * libraries? + * Same as RB_OBJ_FROZEN(), but returns #Qtrue/#Qfalse instead of #bool. * * @param[in] obj Object in question. * @retval RUBY_Qtrue Yes it is. diff --git a/object.c b/object.c index 11b10b169566c3..8170ac36ad696d 100644 --- a/object.c +++ b/object.c @@ -1371,7 +1371,7 @@ VALUE rb_obj_freeze(VALUE obj) { if (!OBJ_FROZEN(obj)) { - OBJ_FREEZE(obj); + OBJ_FREEZE_RAW(obj); if (SPECIAL_CONST_P(obj)) { rb_bug("special consts should be frozen."); } From f74cedf78f464e754e6dc6202d7c8dfef2553b5d Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 19 Sep 2026 00:10:43 +0900 Subject: [PATCH 4/6] Simplify `rb_obj_freeze` The frozen check in `OBJ_FREEZE` makes the separate check redundant, and immediate values are always frozen, so the old assertion is unreachable. --- object.c | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/object.c b/object.c index 8170ac36ad696d..69127b759d7417 100644 --- a/object.c +++ b/object.c @@ -1370,12 +1370,7 @@ rb_obj_dummy1(VALUE _x, VALUE _y) VALUE rb_obj_freeze(VALUE obj) { - if (!OBJ_FROZEN(obj)) { - OBJ_FREEZE_RAW(obj); - if (SPECIAL_CONST_P(obj)) { - rb_bug("special consts should be frozen."); - } - } + OBJ_FREEZE(obj); return obj; } From 79c60fe7eee4c8bc0a47f80ef4479f5c37635a8e Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Sat, 19 Sep 2026 06:12:51 -0500 Subject: [PATCH 5/6] [DOC] Fix broken link (#18917) --- doc/NEWS/NEWS-4.0.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/NEWS/NEWS-4.0.0.md b/doc/NEWS/NEWS-4.0.0.md index 077cd97ab07384..0796fa241ce0c7 100644 --- a/doc/NEWS/NEWS-4.0.0.md +++ b/doc/NEWS/NEWS-4.0.0.md @@ -240,7 +240,7 @@ Note: We're only listing outstanding class updates. * Ruby::Box * A new (experimental) feature to provide separation about definitions. - For the detail of "Ruby Box", see [doc/language/box.md](doc/language/box.md). + For the detail of "Ruby Box", see [box.md](rdoc-ref:language/box.md). [[Feature #21311]] [[Misc #21385]] * Set From 44874b7e7a6c364f5a78740624051d89465781c4 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 19 Sep 2026 09:20:52 +0900 Subject: [PATCH 6/6] Fix use-after-free in String#strip methods The selector conversion may run aribtrary Ruby code and modify the source string. This can cause a use-after-free if it resizes the source string. For example, the following script causes corruption: s = "-" * 1000 + "abc" + "-" * 1000 obj = Object.new obj.define_singleton_method(:to_str) do s.clear "-" end p s.strip(obj) --- string.c | 32 ++++++++++++++++++++++++++++++++ test/ruby/test_string.rb | 17 +++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/string.c b/string.c index d372f923b65add..2129f97595f697 100644 --- a/string.c +++ b/string.c @@ -11736,6 +11736,12 @@ rb_str_lstrip_bang(int argc, VALUE *argv, VALUE str) VALUE del = 0, nodel = 0; tr_setup_table_multi(table, &del, &nodel, str, argc, argv); + + /* the selector conversion may have modified str */ + str_modify_keep_cr(str); + enc = STR_ENC_GET(str); + RSTRING_GETMEM(str, start, olen); + loffset = lstrip_offset_table(str, start, start+olen, enc, table, del, nodel); } else { @@ -11792,6 +11798,10 @@ rb_str_lstrip(int argc, VALUE *argv, VALUE str) VALUE del = 0, nodel = 0; tr_setup_table_multi(table, &del, &nodel, str, argc, argv); + + /* the selector conversion may have modified str */ + RSTRING_GETMEM(str, start, len); + loffset = lstrip_offset_table(str, start, start+len, STR_ENC_GET(str), table, del, nodel); } else { @@ -11880,6 +11890,12 @@ rb_str_rstrip_bang(int argc, VALUE *argv, VALUE str) VALUE del = 0, nodel = 0; tr_setup_table_multi(table, &del, &nodel, str, argc, argv); + + /* the selector conversion may have modified str */ + str_modify_keep_cr(str); + enc = STR_ENC_GET(str); + RSTRING_GETMEM(str, start, olen); + roffset = rstrip_offset_table(str, start, start+olen, enc, table, del, nodel); } else { @@ -11936,6 +11952,11 @@ rb_str_rstrip(int argc, VALUE *argv, VALUE str) VALUE del = 0, nodel = 0; tr_setup_table_multi(table, &del, &nodel, str, argc, argv); + + /* the selector conversion may have modified str */ + enc = STR_ENC_GET(str); + + RSTRING_GETMEM(str, start, olen); roffset = rstrip_offset_table(str, start, start+olen, enc, table, del, nodel); } else { @@ -11974,6 +11995,12 @@ rb_str_strip_bang(int argc, VALUE *argv, VALUE str) VALUE del = 0, nodel = 0; tr_setup_table_multi(table, &del, &nodel, str, argc, argv); + + /* the selector conversion may have modified str */ + str_modify_keep_cr(str); + enc = STR_ENC_GET(str); + RSTRING_GETMEM(str, start, olen); + loffset = lstrip_offset_table(str, start, start+olen, enc, table, del, nodel); roffset = rstrip_offset_table(str, start+loffset, start+olen, enc, table, del, nodel); } @@ -12037,6 +12064,11 @@ rb_str_strip(int argc, VALUE *argv, VALUE str) VALUE del = 0, nodel = 0; tr_setup_table_multi(table, &del, &nodel, str, argc, argv); + + /* the selector conversion may have modified str */ + enc = STR_ENC_GET(str); + RSTRING_GETMEM(str, start, olen); + loffset = lstrip_offset_table(str, start, start+olen, enc, table, del, nodel); roffset = rstrip_offset_table(str, start+loffset, start+olen, enc, table, del, nodel); } diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb index 5fb76986cf5791..db92a2cac1da83 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -2635,6 +2635,23 @@ def test_rstrip_bang_with_chars assert_equal(S("abc"), a) end + def test_strip_selector_modifying_receiver + [:strip, :strip!, :lstrip, :lstrip!, :rstrip, :rstrip!].each do |m| + s = S("-" * 100 + "abc" + "-" * 100) + obj = Object.new + obj.define_singleton_method(:to_str) do + s.clear + "-" + end + + if m.end_with?("!") + assert_nil(s.public_send(m, obj), "String##{m}") + else + assert_equal("", s.public_send(m, obj), "String##{m}") + end + end + end + def test_sub assert_equal(S("h*llo"), S("hello").sub(/[aeiou]/, S('*'))) assert_equal(S("hllo"), S("hello").sub(/([aeiou])/, S('<\1>')))