From 945394bb97d7e1aed639c3c978b7474b61e472ab Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 17 Jun 2026 08:26:47 +0200 Subject: [PATCH] ResumableParser#<<: call rb_str_modify before shrinking the buffer Fix: https://github.com/ruby/json/issues/1013 The string may be shared. --- ext/json/ext/parser/parser.c | 9 ++++++--- test/json/resumable_parser_test.rb | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/ext/json/ext/parser/parser.c b/ext/json/ext/parser/parser.c index c216faf4..136aab6a 100644 --- a/ext/json/ext/parser/parser.c +++ b/ext/json/ext/parser/parser.c @@ -2300,16 +2300,19 @@ static VALUE cResumableParser_feed(VALUE self, VALUE str) const size_t size = parser->state.end - parser->state.start; const size_t consumed = size - remaining; - char *old_ptr = RSTRING_PTR(parser->buffer); if (RB_OBJ_FROZEN_RAW(parser->buffer)) { VALUE new_buffer = rb_obj_hide(rb_str_buf_new(remaining + RSTRING_LEN(str))); - MEMCPY(RSTRING_PTR(new_buffer), old_ptr + consumed, char, remaining); + + char *old_ptr = RSTRING_PTR(parser->buffer); + memcpy(RSTRING_PTR(new_buffer), old_ptr + consumed, remaining); rb_str_set_len(new_buffer, remaining); offset = 0; parser->buffer = new_buffer; } else if (consumed > (size / 2) && size >= 512) { - MEMMOVE(old_ptr, old_ptr + consumed, char, remaining); + rb_str_modify(parser->buffer); + char *old_ptr = RSTRING_PTR(parser->buffer); + memmove(old_ptr, old_ptr + consumed, remaining); rb_str_set_len(parser->buffer, remaining); offset = 0; } diff --git a/test/json/resumable_parser_test.rb b/test/json/resumable_parser_test.rb index 600cd33e..52f1356a 100644 --- a/test/json/resumable_parser_test.rb +++ b/test/json/resumable_parser_test.rb @@ -244,6 +244,21 @@ def test_spill_frames_stack assert_equal expected, @parser.value end + def test_buffer_shrink + doc1 = '{"a":"' + ("x" * 800) + '"} {' # >= 512 bytes + doc2 = '"b":1} ' + + parser = JSON::ResumableParser.new({}) + + parser << doc1 # internal buffer becomes a *shared* string here + parser.parse # consume doc1 -> >50% of a >=512B buffer is now consumed + parser.value + + parser << doc2 # buffer is shrinked + parser.parse + parser.value + end + private def assert_partial_value(expected, json)