Skip to content

Commit 59552ec

Browse files
matzclaude
andcommitted
mruby-sprintf: protect format string from mutation during callbacks
mrb_str_format captured raw C pointers (p, end) into the format string's buffer before the main loop. The %s and %p specifiers call to_s and inspect, which can invoke Ruby code that mutates the format string via String#replace, freeing or reallocating its buffer. The loop then continued iterating with dangling pointers, reading freed memory and potentially leaking adjacent heap contents into the result. Duplicate the format string with mrb_str_dup() before the loop. This is O(1) because mrb_str_dup shares the underlying buffer; if the original is later mutated via String#replace, str_replace decrements the shared refcount, leaving our duplicate's buffer intact. Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4eb4884 commit 59552ec

File tree

2 files changed

+23
-0
lines changed

2 files changed

+23
-0
lines changed

mrbgems/mruby-sprintf/src/sprintf.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,13 @@ mrb_str_format(mrb_state *mrb, mrb_int argc, const mrb_value *argv, mrb_value fm
380380
argc++;
381381
argv--;
382382
mrb_ensure_string_type(mrb, fmt);
383+
/* Duplicate the format string so that to_s/inspect callbacks invoked
384+
during the loop cannot invalidate p/end by mutating the original
385+
via String#replace or similar. mrb_str_dup shares the underlying
386+
buffer, so this is O(1); String#replace on the original goes
387+
through str_replace which decrements the shared refcount, leaving
388+
our copy's buffer intact. */
389+
fmt = mrb_str_dup(mrb, fmt);
383390
p = RSTRING_PTR(fmt);
384391
end = p + RSTRING_LEN(fmt);
385392
blen = 0;

mrbgems/mruby-sprintf/test/sprintf.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,19 @@
9090
"%?" % ""
9191
end
9292
end
93+
94+
assert("sprintf with to_s mutating format string") do
95+
# The to_s callback must not be able to invalidate sprintf's internal
96+
# iteration pointers by mutating the format string.
97+
fmt = "%s" + "B" * 200
98+
mutator = Object.new
99+
$sprintf_test_fmt = fmt
100+
def mutator.to_s
101+
$sprintf_test_fmt.replace("Z")
102+
"ok"
103+
end
104+
result = sprintf(fmt, mutator)
105+
assert_equal 202, result.length
106+
assert_equal "ok", result[0, 2]
107+
assert_equal "B" * 200, result[2..]
108+
end

0 commit comments

Comments
 (0)