[ruby/json] Prevent use-after-free when strings change during IO generation

https://github.com/ruby/json/commit/f720614728
This commit is contained in:
ydah authored and git committed 2026-10-01 06:31:16 +00:00
1 parent f452e5403e
commit ad5851e797
3 files changed
+51 -2

No files matched your search

+4
View File
@@ -169,6 +169,10 @@ static inline void fbuffer_append_reserved_char(FBuffer *fb, char chr)
static void fbuffer_append_str(FBuffer *fb, VALUE str)
{
if (RB_UNLIKELY(fb->io)) {
// Growing the buffer may flush it and invoke arbitrary Ruby code.
str = rb_str_new_frozen(str);
}
const char *ptr;
size_t len;
RSTRING_GETMEM(str, ptr, len);
+8 -2
View File
@@ -892,12 +892,17 @@ ALWAYS_INLINE(static) VALUE ensure_valid_encoding(struct generate_json_data *dat
static void raw_generate_json_string(FBuffer *buffer, struct generate_json_data *data, VALUE obj)
{
VALUE str = obj;
if (RB_UNLIKELY(buffer->io)) {
// IO writes can mutate the original string while we are reading it.
str = rb_str_new_frozen(str);
}
fbuffer_append_char(buffer, '"');
long len;
search_state search;
search.buffer = buffer;
RSTRING_GETMEM(obj, search.ptr, len);
RSTRING_GETMEM(str, search.ptr, len);
search.cursor = search.ptr;
search.end = search.ptr + len;
@@ -908,7 +913,7 @@ static void raw_generate_json_string(FBuffer *buffer, struct generate_json_data
search.chunk_end = NULL;
#endif /* HAVE_SIMD */
switch (json_str_coderange(obj)) {
switch (json_str_coderange(str)) {
case ENC_CODERANGE_7BIT:
case ENC_CODERANGE_VALID:
if (RB_UNLIKELY(data->state->ascii_only)) {
@@ -924,6 +929,7 @@ static void raw_generate_json_string(FBuffer *buffer, struct generate_json_data
break;
}
fbuffer_append_char(buffer, '"');
RB_GC_GUARD(str);
}
static void generate_json_string(FBuffer *buffer, struct generate_json_data *data, VALUE obj)
+39
View File
@@ -91,6 +91,45 @@ class JSONGeneratorTest < Test::Unit::TestCase
assert_equal '[1]', io.string
end
def test_dump_string_mutated_during_io_write
[{}, {ascii_only: true}, {script_safe: true}, {buffer_initial_length: 1}].each do |options|
['a', "a\né/"].each do |pattern|
string = pattern * 100_000
assert_dump_preserves_string_during_io_write(string, string, options)
end
end
end
def test_dump_fragment_mutated_during_io_write
string = JSON.generate('a' * 100_000)
assert_dump_preserves_string_during_io_write(JSON::Fragment.new(string), string)
end
def test_dump_to_json_result_mutated_during_io_write
string = JSON.generate('a' * 100_000)
object = Object.new
object.define_singleton_method(:to_json) { |*| string }
assert_dump_preserves_string_during_io_write(object, string)
end
def assert_dump_preserves_string_during_io_write(object, string, options = {})
expected = JSON.dump([object], options)
io = StringIO.new
mutated = false
io.define_singleton_method(:write) do |chunk|
unless mutated
string.setbyte(0, 'b'.ord)
string.replace('changed')
mutated = true
GC.start
end
super(chunk)
end
assert_same io, JSON.dump([object], io, options)
assert_equal 'changed', string
assert_equal true, expected == io.string, 'IO output must preserve the original string'
end
def test_not_frozen
[
[[], '[]'],