Skip to content

Commit 5ca8a67

Browse files
committed
parser.c: Ensure the user provided string can't be mutated
Reported-By: Yuhang Wu <yuhang@depthfirst.com>
1 parent dba1d88 commit 5ca8a67

2 files changed

Lines changed: 39 additions & 15 deletions

File tree

ext/json/ext/parser/parser.c

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,18 +1457,21 @@ static void json_ensure_eof(JSON_ParserState *state)
14571457

14581458
static VALUE convert_encoding(VALUE source)
14591459
{
1460-
int encindex = RB_ENCODING_GET(source);
1460+
StringValue(source);
1461+
int encindex = RB_ENCODING_GET(source);
14611462

1462-
if (RB_LIKELY(encindex == utf8_encindex)) {
1463-
return source;
1464-
}
1463+
if (RB_LIKELY(encindex == utf8_encindex)) {
1464+
return source;
1465+
}
14651466

1466-
if (encindex == binary_encindex) {
1467-
// For historical reason, we silently reinterpret binary strings as UTF-8
1468-
return rb_enc_associate_index(rb_str_dup(source), utf8_encindex);
1469-
}
1467+
if (encindex == binary_encindex) {
1468+
// For historical reason, we silently reinterpret binary strings as UTF-8
1469+
return rb_enc_associate_index(rb_str_dup(source), utf8_encindex);
1470+
}
14701471

1471-
return rb_funcall(source, i_encode, 1, Encoding_UTF_8);
1472+
source = rb_funcall(source, i_encode, 1, Encoding_UTF_8);
1473+
StringValue(source);
1474+
return source;
14721475
}
14731476

14741477
struct parser_config_init_args {
@@ -1583,10 +1586,16 @@ static VALUE cParserConfig_initialize(VALUE self, VALUE opts)
15831586
return self;
15841587
}
15851588

1586-
static VALUE cParser_parse(JSON_ParserConfig *config, VALUE Vsource)
1589+
static VALUE cParser_parse(JSON_ParserConfig *config, VALUE src)
15871590
{
1588-
Vsource = convert_encoding(StringValue(Vsource));
1589-
StringValue(Vsource);
1591+
VALUE Vsource = convert_encoding(src);
1592+
1593+
// Ensure the string isn't mutated under us.
1594+
// The classic API to use is `rb_str_locktmp`, but then we'd
1595+
// need to use `rb_protect` to make sure we always unlock.
1596+
if (Vsource == src) {
1597+
Vsource = rb_str_new_frozen(Vsource);
1598+
}
15901599

15911600
VALUE rvalue_stack_buffer[RVALUE_STACK_INITIAL_CAPA];
15921601
rvalue_stack stack = {
@@ -1597,6 +1606,7 @@ static VALUE cParser_parse(JSON_ParserConfig *config, VALUE Vsource)
15971606

15981607
long len;
15991608
const char *start;
1609+
16001610
RSTRING_GETMEM(Vsource, start, len);
16011611

16021612
VALUE stack_handle = 0;
@@ -1615,6 +1625,7 @@ static VALUE cParser_parse(JSON_ParserConfig *config, VALUE Vsource)
16151625
// it won't cause a leak.
16161626
rvalue_stack_eagerly_release(stack_handle);
16171627
RB_GC_GUARD(stack_handle);
1628+
RB_GC_GUARD(Vsource);
16181629
json_ensure_eof(state);
16191630

16201631
return result;
@@ -1635,9 +1646,6 @@ static VALUE cParserConfig_parse(VALUE self, VALUE Vsource)
16351646

16361647
static VALUE cParser_m_parse(VALUE klass, VALUE Vsource, VALUE opts)
16371648
{
1638-
Vsource = convert_encoding(StringValue(Vsource));
1639-
StringValue(Vsource);
1640-
16411649
JSON_ParserConfig _config = {0};
16421650
JSON_ParserConfig *config = &_config;
16431651
parser_config_init(config, opts, false);

test/json/json_parser_test.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,22 @@ def test_frozen
878878
end
879879
end
880880

881+
def test_mutating_source_string_during_parsing
882+
expected = ([1] * 100) + [2.3] + ([1] * 100)
883+
source = JSON.generate(expected)
884+
expected.delete_at(100)
885+
886+
fake_decimal_class = Class.new
887+
fake_decimal_class.define_method(:initialize) do |number|
888+
source.tr!('1', '0')
889+
number.to_f
890+
end
891+
892+
actual = JSON.parse(source, decimal_class: fake_decimal_class)
893+
actual.delete_at(100)
894+
assert_equal expected, actual
895+
end
896+
881897
private
882898

883899
def assert_equal_float(expected, actual, delta = 1e-2)

0 commit comments

Comments
 (0)