Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ext/json/ext/fbuffer/fbuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 8 additions & 2 deletions ext/json/ext/generator/generator.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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)) {
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions java/src/json/ext/Generator.java
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,7 @@ static RubyString generateFragmentNew(ThreadContext context, Session session, IR

static void generateFragment(ThreadContext context, Session session, IRubyObject object, OutputStream buffer) throws IOException {
RubyString result = generateFragmentNew(context, session, object);
if (buffer instanceof BufferedOutputStream) result = result.newFrozen();
ByteList bytes = result.getByteList();
buffer.write(bytes.unsafeBytes(), bytes.begin(), bytes.length());
}
Expand Down Expand Up @@ -814,6 +815,7 @@ static RubyString generateGenericNew(ThreadContext context, Session session, IRu

static void generateGeneric(ThreadContext context, Session session, IRubyObject object, OutputStream buffer) throws IOException {
RubyString result = generateGenericNew(context, session, object);
if (buffer instanceof BufferedOutputStream) result = result.newFrozen();
ByteList bytes = result.getByteList();
buffer.write(bytes.unsafeBytes(), bytes.begin(), bytes.length());
}
Expand Down
7 changes: 5 additions & 2 deletions java/src/json/ext/StringEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
package json.ext;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
Expand Down Expand Up @@ -192,11 +193,13 @@ static StringEncoder createBasicEncoder() {
void generate(ThreadContext context, RubyString object, OutputStream buffer) throws IOException {
object = ensureValidEncoding(context, object);

ByteList byteList = object.getByteList();
// IO writes can mutate the original string while we are reading it.
RubyString source = buffer instanceof BufferedOutputStream ? object.newFrozen() : object;
ByteList byteList = source.getByteList();
init(byteList);
out = buffer;
append('"');
switch (object.scanForCodeRange()) {
switch (source.scanForCodeRange()) {
case StringSupport.CR_7BIT:
case StringSupport.CR_VALID:
encode(byteList);
Expand Down
39 changes: 39 additions & 0 deletions test/json/json_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,45 @@ def test_dump_deprecated_limit
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
[
[[], '[]'],
Expand Down
Loading