From 87435fe232da029299d4b4f1fa9ca1ae7da5decc Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 20 Sep 2026 13:13:45 +0900 Subject: [PATCH 1/2] Enable the conforming MSVC preprocessor Avoid incorrect variadic argument forwarding in `RUBY_ASSERT_MESG_WHEN` by the traditional MSVC preprocessor, which breaks `VM_ASSERT` when `RUBY_DEBUG` is enabled. --- win32/Makefile.sub | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/win32/Makefile.sub b/win32/Makefile.sub index 897964ab02827d..4dc4d85369e997 100644 --- a/win32/Makefile.sub +++ b/win32/Makefile.sub @@ -357,6 +357,17 @@ CPPFLAGS = $(DEFS) $(ARCHDEFS) $(CPPFLAGS) CPPFLAGS = -DDISABLE_RUBYGEMS $(CPPFLAGS) !endif +# https://learn.microsoft.com/en-us/cpp/build/reference/zc-preprocessor?view=msvc-170 +!if $(MSC_VER) >= 1925 +# maybe >= 1926? +# https://devblogs.microsoft.com/cppblog/announcing-full-support-for-a-c-c-conformant-preprocessor-in-msvc/ +CPPFLAGS = -Zc:preprocessor $(CPPFLAGS) +!elseif $(MSC_VER) >= 1915 +CPPFLAGS = -experimental:preprocessor $(CPPFLAGS) +!else +! error C99 conformant preprocessor is required +!endif + POSTLINK = DLDFLAGS = $(LDFLAGS) -dll MAINLIBS = $(LIBS) @@ -1323,10 +1334,10 @@ modular-gc-precheck: {$(srcdir)/coroutine/win32}.asm{coroutine/win32}.obj: $(ECHO) assembling $(<:\=/) - $(Q) $(AS) $(ASFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(<:\=/) + $(Q) $(AS) $(ASFLAGS) $(COUTFLAG)$@ -c $(<:\=/) {$(srcdir)/coroutine/win64}.asm{coroutine/win64}.obj: $(ECHO) assembling $(<:\=/) - $(Q) $(AS) $(ASFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(<:\=/) + $(Q) $(AS) $(ASFLAGS) $(COUTFLAG)$@ -c $(<:\=/) {$(srcdir)/coroutine/arm64}.asm{coroutine/arm64}.obj: $(ECHO) assembling $(<:\=/) $(Q) $(AS) $(ASFLAGS) -o $@ $(<:\=/) From edce07a8c93895be8eeb2d27400aa138cc8a3cf9 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sun, 20 Sep 2026 11:11:44 +0900 Subject: [PATCH 2/2] Fix use-after-free in String#% when array modified If the array object passed into String#% is modified during conversion (such as via to_s, to_i, to_f, etc.) then it can be a use-after-free if the buffer of the array is freed. This commit changes it to detect if the buffer has been changed and raises an error when that happens. The following script reproduces the crash: count = 1_000 obj = Object.new ary = [obj, *Array.new(count - 1) { "x" }] obj.define_singleton_method(:to_s) do ary.replace([]) "X" end str = "%s" * count puts (str % ary).length --- internal/string.h | 1 + sprintf.c | 24 +++++++++++++++++++++++- string.c | 2 +- test/ruby/test_sprintf.rb | 14 ++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/internal/string.h b/internal/string.h index 67216f574879de..d407fa9c297017 100644 --- a/internal/string.h +++ b/internal/string.h @@ -126,6 +126,7 @@ VALUE rb_obj_as_string_result(VALUE str, VALUE obj); VALUE rb_str_opt_plus(VALUE x, VALUE y); VALUE rb_str_new_owned(char *ptr, long len, long capa, int encindex); VALUE rb_str_concat_literals(size_t num, const VALUE *strary); +VALUE rb_str_format_ary(int argc, const VALUE *argv, VALUE fmt, VALUE ary); VALUE rb_str_eql(VALUE str1, VALUE str2); VALUE rb_id_quote_unprintable(ID); VALUE rb_sym_proc_call(ID mid, int argc, const VALUE *argv, int kw_splat, VALUE passed_proc); diff --git a/sprintf.c b/sprintf.c index 9ff8498143127c..3adf55f0482f45 100644 --- a/sprintf.c +++ b/sprintf.c @@ -110,15 +110,26 @@ expand_result(VALUE result, long bsiz, long blen, long l) blen += (l);\ } while (0) +static void +format_args_check(VALUE ary, int argc, const VALUE *argv) +{ + if (!RTEST(ary)) return; + if (RARRAY_LEN(ary) != argc || RARRAY_CONST_PTR(ary) != argv) { + rb_raise(rb_eRuntimeError, "array modified during formatting"); + } +} + #define GETARG() (!UNDEF_P(nextvalue) ? nextvalue : \ GETNEXTARG()) #define GETNEXTARG() ( \ check_next_arg(posarg, nextarg), \ + (RTEST(ary) ? (format_args_check(ary, argc0, argv0), 0) : 0), \ (posarg = nextarg++, GETNTHARG(posarg))) #define GETPOSARG(n) ( \ check_pos_arg(posarg, (n)), \ + (RTEST(ary) ? (format_args_check(ary, argc0, argv0), 0) : 0), \ (posarg = -1, GETNTHARG(n))) #define GETNTHARG(nth) \ @@ -224,7 +235,7 @@ rb_f_sprintf(int argc, const VALUE *argv) } VALUE -rb_str_format(int argc, const VALUE *argv, VALUE fmt) +rb_str_format_ary(int argc, const VALUE *argv, VALUE fmt, VALUE ary) { enum {default_float_precision = 6}; rb_encoding *enc; @@ -268,6 +279,10 @@ rb_str_format(int argc, const VALUE *argv, VALUE fmt) ENC_CODERANGE_BROKEN : (coderange = cr))); \ } \ } while (0) + + const int argc0 = argc; + const VALUE *argv0 = argv; + ++argc; --argv; StringValue(fmt); @@ -945,6 +960,7 @@ rb_str_format(int argc, const VALUE *argv, VALUE fmt) rb_str_tmp_frozen_release(orig, fmt); /* XXX - We cannot validate the number of arguments if (digit)$ style used. */ + format_args_check(ary, argc0, argv0); if (posarg >= 0 && nextarg < argc && !(argc == 2 && RB_TYPE_P(argv[1], T_HASH))) { const char *mesg = "too many arguments for format string"; if (RTEST(ruby_verbose)) rb_warn("%s", mesg); @@ -954,6 +970,12 @@ rb_str_format(int argc, const VALUE *argv, VALUE fmt) return result; } +VALUE +rb_str_format(int argc, const VALUE *argv, VALUE fmt) +{ + return rb_str_format_ary(argc, argv, fmt, Qfalse); +} + static char * fmt_setup(char *buf, size_t size, int c, int flags, int width, int prec) { diff --git a/string.c b/string.c index 099933c5ddd859..7c8f7de12c3db9 100644 --- a/string.c +++ b/string.c @@ -2695,7 +2695,7 @@ rb_str_format_m(VALUE str, VALUE arg) VALUE tmp = rb_check_array_type(arg); if (!NIL_P(tmp)) { - VALUE result = rb_str_format(RARRAY_LENINT(tmp), RARRAY_CONST_PTR(tmp), str); + VALUE result = rb_str_format_ary(RARRAY_LENINT(tmp), RARRAY_CONST_PTR(tmp), str, tmp); RB_GC_GUARD(tmp); return result; } diff --git a/test/ruby/test_sprintf.rb b/test/ruby/test_sprintf.rb index bbbe6e7ec38589..a267249ae0c5d9 100644 --- a/test/ruby/test_sprintf.rb +++ b/test/ruby/test_sprintf.rb @@ -289,6 +289,20 @@ def test_invalid end end + def test_modify_argument_array + count = 1_000 + obj = Object.new + ary = [obj, *Array.new(count - 1) { "x" }] + obj.define_singleton_method(:to_s) do + ary.replace([]) + "X" + end + str = "%s" * count + assert_raise_with_message(RuntimeError, /array modified during formatting/) do + str % ary + end + end + def test_float assert_equal("36893488147419111424", sprintf("%20.0f", 36893488147419107329.0))