diff --git a/mrbgems/mruby-sprintf/src/sprintf.c b/mrbgems/mruby-sprintf/src/sprintf.c index 66f960d779..c7fbf4397e 100644 --- a/mrbgems/mruby-sprintf/src/sprintf.c +++ b/mrbgems/mruby-sprintf/src/sprintf.c @@ -380,6 +380,13 @@ mrb_str_format(mrb_state *mrb, mrb_int argc, const mrb_value *argv, mrb_value fm argc++; argv--; mrb_ensure_string_type(mrb, fmt); + /* Duplicate the format string so that to_s/inspect callbacks invoked + during the loop cannot invalidate p/end by mutating the original + via String#replace or similar. mrb_str_dup shares the underlying + buffer, so this is O(1); String#replace on the original goes + through str_replace which decrements the shared refcount, leaving + our copy's buffer intact. */ + fmt = mrb_str_dup(mrb, fmt); p = RSTRING_PTR(fmt); end = p + RSTRING_LEN(fmt); blen = 0; diff --git a/mrbgems/mruby-sprintf/test/sprintf.rb b/mrbgems/mruby-sprintf/test/sprintf.rb index 1b1fe95f10..80220137a8 100644 --- a/mrbgems/mruby-sprintf/test/sprintf.rb +++ b/mrbgems/mruby-sprintf/test/sprintf.rb @@ -90,3 +90,19 @@ "%?" % "" end end + +assert("sprintf with to_s mutating format string") do + # The to_s callback must not be able to invalidate sprintf's internal + # iteration pointers by mutating the format string. + fmt = "%s" + "B" * 200 + mutator = Object.new + $sprintf_test_fmt = fmt + def mutator.to_s + $sprintf_test_fmt.replace("Z") + "ok" + end + result = sprintf(fmt, mutator) + assert_equal 202, result.length + assert_equal "ok", result[0, 2] + assert_equal "B" * 200, result[2..] +end