From 99d06012e41ae320c9b1276d27a7ac09a3e059ec Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 25 Mar 2026 14:06:14 +0900 Subject: [PATCH 1/7] sprintf.c: Refactor CHECK macro --- sprintf.c | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/sprintf.c b/sprintf.c index 234aff76f5d3a1..4788e1872889e3 100644 --- a/sprintf.c +++ b/sprintf.c @@ -65,15 +65,22 @@ sign_bits(int base, const char *p) #define FPREC 64 #define FPREC0 128 +static long +expand_result(VALUE result, long bsiz, long blen, long l) +{ + int cr = ENC_CODERANGE(result); + RUBY_ASSERT(bsiz >= blen); + while (l > bsiz - blen) { + bsiz *= 2; + if (bsiz < 0) rb_raise(rb_eArgError, "too big specifier"); + } + rb_str_resize(result, bsiz); + ENC_CODERANGE_SET(result, cr); + return bsiz; +} + #define CHECK(l) do {\ - int cr = ENC_CODERANGE(result);\ - RUBY_ASSERT(bsiz >= blen); \ - while ((l) > bsiz - blen) {\ - bsiz*=2;\ - if (bsiz<0) rb_raise(rb_eArgError, "too big specifier");\ - }\ - rb_str_resize(result, bsiz);\ - ENC_CODERANGE_SET(result, cr);\ + bsiz = expand_result(result, bsiz, blen, l);\ buf = RSTRING_PTR(result);\ } while (0) From e39475a544f08ce5f140213c740f2464933a2bae Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 25 Mar 2026 14:00:02 +0900 Subject: [PATCH 2/7] sprintf.c: Fix width overflow --- sprintf.c | 21 ++++++++++----------- test/ruby/test_sprintf.rb | 6 ++++++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/sprintf.c b/sprintf.c index 4788e1872889e3..b91d7589572969 100644 --- a/sprintf.c +++ b/sprintf.c @@ -84,6 +84,11 @@ expand_result(VALUE result, long bsiz, long blen, long l) buf = RSTRING_PTR(result);\ } while (0) +#define CHECK_WIDTH(l, w) do { \ + if ((l) > INT_MAX - (w)) rb_raise(rb_eArgError, "width too big");\ + CHECK((l)+(w));\ +} while (0) + #define PUSH(s, l) do { \ CHECK(l);\ PUSH_(s, l);\ @@ -476,19 +481,13 @@ rb_str_format(int argc, const VALUE *argv, VALUE fmt) rb_enc_mbcput(c, &buf[blen], enc); blen += n; } - else if ((flags & FMINUS)) { - --width; - CHECK(n + (width > 0 ? width : 0)); - rb_enc_mbcput(c, &buf[blen], enc); - blen += n; - if (width > 0) FILL_(' ', width); - } else { --width; - CHECK(n + (width > 0 ? width : 0)); - if (width > 0) FILL_(' ', width); + CHECK_WIDTH(n, (width > 0 ? width : 0)); + if (!(flags & FMINUS) && (width > 0)) FILL_(' ', width); rb_enc_mbcput(c, &buf[blen], enc); blen += n; + if ((flags & FMINUS) && (width > 0)) FILL_(' ', width); } } break; @@ -525,7 +524,7 @@ rb_str_format(int argc, const VALUE *argv, VALUE fmt) /* need to adjust multi-byte string pos */ if ((flags&FWIDTH) && (width > slen)) { width -= (int)slen; - CHECK(len + width); + CHECK_WIDTH(len, width); if (!(flags&FMINUS)) { FILL_(' ', width); width = 0; @@ -839,7 +838,7 @@ rb_str_format(int argc, const VALUE *argv, VALUE fmt) if (sign || (flags&FSPACE)) ++len; if (prec > 0) ++len; /* period */ fill = width > len ? width - len : 0; - CHECK(fill + len); + CHECK(fill + len); /* max(width, len) */ if (fill && !(flags&(FMINUS|FZERO))) { FILL_(' ', fill); } diff --git a/test/ruby/test_sprintf.rb b/test/ruby/test_sprintf.rb index 1c7e89c2651a9d..bbbe6e7ec38589 100644 --- a/test/ruby/test_sprintf.rb +++ b/test/ruby/test_sprintf.rb @@ -1,5 +1,6 @@ # frozen_string_literal: false require 'test/unit' +require 'rbconfig/sizeof' class TestSprintf < Test::Unit::TestCase def test_positional @@ -539,6 +540,11 @@ def test_named_with_nil def test_width_underflow bug = 'https://github.com/mruby/mruby/issues/3347' assert_equal("!", sprintf("%*c", 0, ?!.ord), bug) + + int_max = RbConfig::LIMITS["INT_MAX"] + assert_raise_with_message(ArgumentError, /width too big/) { + sprintf "%*c", int_max, 0x80 + } end def test_negative_width_overflow From 2c72ee12e4707da53b78c1a0c2555b07fa812258 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 7 Jun 2026 16:37:10 +0900 Subject: [PATCH 3/7] Skip the hang-up test in mmtk [ci skip] --- test/.excludes-mmtk/TestObjSpace.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/.excludes-mmtk/TestObjSpace.rb b/test/.excludes-mmtk/TestObjSpace.rb index 94eb2c436d4435..feb05063df63bf 100644 --- a/test/.excludes-mmtk/TestObjSpace.rb +++ b/test/.excludes-mmtk/TestObjSpace.rb @@ -2,3 +2,4 @@ exclude(:test_dump_flag_age, "testing behaviour specific to default GC") exclude(:test_dump_flags, "testing behaviour specific to default GC") exclude(:test_dump_objects_dumps_page_slot_sizes, "testing behaviour specific to default GC") +exclude(:test_trace_object_allocations_does_not_reuse_freed_allocation_info, "hang up") From c022eccde0a62929ecaf66368205ec4b1392669e Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sun, 7 Jun 2026 10:11:23 +0200 Subject: [PATCH 4/7] [ruby/json] Mark JSON_Parser_frame_stack_type as WB protected It already implictly is on recent rubies because it has no mark function, but might as well make it explicit. https://github.com/ruby/json/commit/8d7f975b01 --- ext/json/parser/parser.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index c0631728c38c80..524b80200fcf77 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -488,7 +488,7 @@ static const rb_data_type_t JSON_Parser_frame_stack_type = { .dfree = json_frame_stack_free, .dsize = json_frame_stack_memsize, }, - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_EMBEDDABLE, + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE, }; static json_frame_stack *json_frame_stack_spill(json_frame_stack *old_stack, VALUE *handle, json_frame_stack **stack_ref) From c332b80bfe30565654de71111c821ed3338a203f Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sun, 7 Jun 2026 10:07:27 +0200 Subject: [PATCH 5/7] [ruby/json] Compile UNREACHABLE_RETURN into `rb_bug` when in debug mode This makes it much easier to debug. https://github.com/ruby/json/commit/bbcf0a3254 --- ext/json/json.h | 8 ++++++++ ext/json/parser/parser.c | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/ext/json/json.h b/ext/json/json.h index cf9420d4dd2456..78b0eee0a4d608 100644 --- a/ext/json/json.h +++ b/ext/json/json.h @@ -11,6 +11,9 @@ #if defined(RUBY_DEBUG) && RUBY_DEBUG # define JSON_ASSERT RUBY_ASSERT +# ifndef JSON_DEBUG +# define JSON_DEBUG 1 +# endif #else # ifdef JSON_DEBUG # include @@ -20,6 +23,11 @@ # endif #endif +#ifdef JSON_DEBUG +# define JSON_UNREACHABLE_RETURN(val) rb_bug("Unreachable") +#else +# define JSON_UNREACHABLE_RETURN UNREACHABLE_RETURN +#endif /* shims */ #if SIZEOF_UINT64_T == SIZEOF_LONG_LONG diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 524b80200fcf77..6b8164c062daca 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -1489,7 +1489,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) case JSON_PHASE_OBJECT_KEY: goto JSON_PHASE_OBJECT_KEY; case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON; } - UNREACHABLE_RETURN(Qundef); + JSON_UNREACHABLE_RETURN(Qundef); JSON_PHASE_DONE: { // The root document value is parsed; it is the lone survivor on @@ -1623,10 +1623,10 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) case JSON_PHASE_ARRAY_COMMA: goto JSON_PHASE_ARRAY_COMMA; case JSON_PHASE_OBJECT_COMMA: goto JSON_PHASE_OBJECT_COMMA; case JSON_PHASE_VALUE: goto JSON_PHASE_VALUE; - case JSON_PHASE_OBJECT_KEY: UNREACHABLE_RETURN(Qundef); + case JSON_PHASE_OBJECT_KEY: JSON_UNREACHABLE_RETURN(Qundef); case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON; } - UNREACHABLE_RETURN(Qundef); + JSON_UNREACHABLE_RETURN(Qundef); } JSON_PHASE_OBJECT_KEY: { @@ -1648,7 +1648,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) raise_parse_error("expected object key, got: %s", state); } } - UNREACHABLE_RETURN(Qundef); + JSON_UNREACHABLE_RETURN(Qundef); } JSON_PHASE_OBJECT_COLON: { @@ -1669,7 +1669,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) raise_parse_error("expected ':' after object key, got: %s", state); } } - UNREACHABLE_RETURN(Qundef); + JSON_UNREACHABLE_RETURN(Qundef); } JSON_PHASE_ARRAY_COMMA: { @@ -1705,13 +1705,13 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) case JSON_PHASE_ARRAY_COMMA: goto JSON_PHASE_ARRAY_COMMA; case JSON_PHASE_OBJECT_COMMA: goto JSON_PHASE_OBJECT_COMMA; case JSON_PHASE_VALUE: goto JSON_PHASE_VALUE; - case JSON_PHASE_OBJECT_KEY: UNREACHABLE_RETURN(Qundef); + case JSON_PHASE_OBJECT_KEY: JSON_UNREACHABLE_RETURN(Qundef); case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON; } } else { raise_parse_error("expected ',' or ']' after array value", state); } - UNREACHABLE_RETURN(Qundef); + JSON_UNREACHABLE_RETURN(Qundef); } JSON_PHASE_OBJECT_COMMA: { @@ -1754,16 +1754,16 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) case JSON_PHASE_ARRAY_COMMA: goto JSON_PHASE_ARRAY_COMMA; case JSON_PHASE_OBJECT_COMMA: goto JSON_PHASE_OBJECT_COMMA; case JSON_PHASE_VALUE: goto JSON_PHASE_VALUE; - case JSON_PHASE_OBJECT_KEY: UNREACHABLE_RETURN(Qundef); + case JSON_PHASE_OBJECT_KEY: JSON_UNREACHABLE_RETURN(Qundef); case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON; } } else { raise_parse_error("expected ',' or '}' after object value, got: %s", state); } - UNREACHABLE_RETURN(Qundef); + JSON_UNREACHABLE_RETURN(Qundef); } - UNREACHABLE_RETURN(Qundef); + JSON_UNREACHABLE_RETURN(Qundef); } static void json_ensure_eof(JSON_ParserState *state) From 4bd3e14fc2623414680008c7b1d38f1a3df2877e Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 7 Jun 2026 16:40:52 +0900 Subject: [PATCH 6/7] IO::Buffer: Validate the mask argument of bit operations --- io_buffer.c | 12 ++++++++++++ test/ruby/test_io_buffer.rb | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/io_buffer.c b/io_buffer.c index faa53042481144..d9f50fc234c016 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -3625,6 +3625,10 @@ io_buffer_and_inplace(VALUE self, VALUE mask) size_t size; io_buffer_get_bytes_for_writing(buffer, &base, &size); + const void *mask_base; + size_t mask_size; + io_buffer_get_bytes_for_reading(mask_buffer, &mask_base, &mask_size); + memory_and_inplace(base, size, mask_buffer->base, mask_buffer->size); return self; @@ -3671,6 +3675,10 @@ io_buffer_or_inplace(VALUE self, VALUE mask) size_t size; io_buffer_get_bytes_for_writing(buffer, &base, &size); + const void *mask_base; + size_t mask_size; + io_buffer_get_bytes_for_reading(mask_buffer, &mask_base, &mask_size); + memory_or_inplace(base, size, mask_buffer->base, mask_buffer->size); return self; @@ -3717,6 +3725,10 @@ io_buffer_xor_inplace(VALUE self, VALUE mask) size_t size; io_buffer_get_bytes_for_writing(buffer, &base, &size); + const void *mask_base; + size_t mask_size; + io_buffer_get_bytes_for_reading(mask_buffer, &mask_base, &mask_size); + memory_xor_inplace(base, size, mask_buffer->base, mask_buffer->size); return self; diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index b6372f25b88ef6..327a3ece9c3398 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -712,6 +712,10 @@ def test_operators_raise_on_freed_self assert_raise(IO::Buffer::InvalidatedError) { slice | mask } assert_raise(IO::Buffer::InvalidatedError) { slice ^ mask } assert_raise(IO::Buffer::InvalidatedError) { ~slice } + + assert_raise(IO::Buffer::InvalidatedError) { slice.and!(mask) } + assert_raise(IO::Buffer::InvalidatedError) { slice.or!(mask) } + assert_raise(IO::Buffer::InvalidatedError) { slice.xor!(mask) } end def test_operators_raise_on_freed_mask @@ -723,6 +727,11 @@ def test_operators_raise_on_freed_mask assert_raise(IO::Buffer::InvalidatedError) { source & mask_slice } assert_raise(IO::Buffer::InvalidatedError) { source | mask_slice } assert_raise(IO::Buffer::InvalidatedError) { source ^ mask_slice } + + source = source.dup + assert_raise(IO::Buffer::InvalidatedError) { source.and!(mask_slice) } + assert_raise(IO::Buffer::InvalidatedError) { source.or!(mask_slice) } + assert_raise(IO::Buffer::InvalidatedError) { source.xor!(mask_slice) } end def test_bit_count From be9725c383f7e554180df7c77a201df1f3ae7f15 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sun, 7 Jun 2026 21:17:10 +0900 Subject: [PATCH 7/7] [DOC] Fix missing parentheses in Kernel#print --- io.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/io.c b/io.c index effcb349c3c47b..f121167102b2ef 100644 --- a/io.c +++ b/io.c @@ -8798,14 +8798,14 @@ rb_io_print(int argc, const VALUE *argv, VALUE out) * * Writes the given objects to $stdout; returns +nil+. * Appends the output record separator $OUTPUT_RECORD_SEPARATOR - * $\\), if it is not +nil+. + * ($\\), if it is not +nil+. * * With argument +objects+ given, for each object: * * - Converts via its method +to_s+ if not a string. * - Writes to stdout. * - If not the last object, writes the output field separator - * $OUTPUT_FIELD_SEPARATOR ($, if it is not +nil+. + * $OUTPUT_FIELD_SEPARATOR ($,) if it is not +nil+. * * With default separators: *