Skip to content

Commit d91b370

Browse files
mamebyroot
andcommitted
Fix JSON::ResumableParser stalling on a NUL byte
Co-Authored-By: Jean Boussier <jean.boussier@gmail.com>
1 parent 489b8c1 commit d91b370

2 files changed

Lines changed: 49 additions & 5 deletions

File tree

ext/json/ext/parser/parser.c

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,7 +1642,7 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
16421642
state->cursor++;
16431643
value = json_decode_array(state, config, 0);
16441644
break;
1645-
} else if (resumable && next == 0) {
1645+
} else if (resumable && eos(state)) {
16461646
state->cursor = value_start;
16471647
return false;
16481648
}
@@ -1691,8 +1691,14 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
16911691
}
16921692

16931693
case 0:
1694-
return false;
1695-
1694+
// peek() returns 0 both at end-of-stream and for a literal NUL byte in the
1695+
// buffer. Only a genuine EOS means "feed me more"; a NUL byte that is not at
1696+
// EOS is just an invalid character.
1697+
if (eos(state)) {
1698+
return false;
1699+
} else {
1700+
raise_syntax_error("unexpected NULL byte: %s", state);
1701+
}
16961702
default:
16971703
raise_syntax_error("unexpected character: %s", state);
16981704
}
@@ -1807,7 +1813,7 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
18071813
case JSON_PHASE_OBJECT_KEY: JSON_UNREACHABLE_RETURN(false);
18081814
case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON;
18091815
}
1810-
} else if (resumable && next_char == 0) {
1816+
} else if (resumable && eos(state)) {
18111817
return false;
18121818
} else {
18131819
raise_syntax_error("expected ',' or ']' after array value", state);
@@ -1858,7 +1864,7 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
18581864
case JSON_PHASE_OBJECT_KEY: JSON_UNREACHABLE_RETURN(false);
18591865
case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON;
18601866
}
1861-
} else if (resumable && next_char == 0) {
1867+
} else if (resumable && eos(state)) {
18621868
return false;
18631869
} else {
18641870
raise_syntax_error("expected ',' or '}' after object value, got: %s", state);

test/json/resumable_parser_test.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,30 @@ def test_parse_byte_by_byte_numbers
133133
assert_resumed_parsing('123 ')
134134
end
135135

136+
def test_nul_byte_is_a_syntax_error
137+
# A NUL byte in a structural position must raise, not stall forever waiting for more input
138+
# (peek() returns 0 both at EOS and for a literal NUL byte).
139+
assert_parse_error "\x00" # document value
140+
assert_parse_error "[\x00]" # first array element
141+
assert_parse_error "[1\x00]" # after an array element (',' or ']' expected)
142+
assert_parse_error "[1,\x00]" # array element after ','
143+
assert_parse_error "{\x00}" # object key
144+
assert_parse_error "{\"a\":1\x00}" # after an object value (',' or '}' expected)
145+
assert_parse_error "{\"a\":1,\x00}" # object key after ','
146+
end
147+
148+
def test_incomplete_input_at_structural_positions_resumes
149+
# Counterpart of test_nul_byte_is_a_syntax_error: a genuine EOS at the same positions must
150+
# stay incomplete (return false), not raise -- this is what distinguishes EOS from a NUL.
151+
assert_incomplete "["
152+
assert_incomplete "[1"
153+
assert_incomplete "[1,"
154+
assert_incomplete "{"
155+
assert_incomplete "{\"a\""
156+
assert_incomplete "{\"a\":1"
157+
assert_incomplete "{\"a\":1,"
158+
end
159+
136160
def test_rest
137161
@parser << '[1, 2, 3, "unterminated string'
138162
refute @parser.parse
@@ -316,6 +340,20 @@ def test_buffer_shrink
316340

317341
private
318342

343+
def assert_parse_error(json)
344+
parser = new_parser
345+
parser << json
346+
assert_raise(JSON::ParserError, "expected a parse error for #{json.inspect}") do
347+
parser.parse
348+
end
349+
end
350+
351+
def assert_incomplete(json)
352+
parser = new_parser
353+
parser << json
354+
refute(parser.parse, "expected #{json.inspect} not to produce a value")
355+
end
356+
319357
def assert_partial_value(expected, json)
320358
parser = new_parser
321359
parser << json

0 commit comments

Comments
 (0)