Skip to content

Commit 82bf6be

Browse files
elang2byroot
authored andcommitted
Fix parser silently accepting a lone trailing surrogate in string values
The C parser guards leading-first surrogate pairs but silently accepts a lone trailing surrogate, encoding U+DC00..U+DFFF as three UTF-8 bytes into the output buffer. The returned Ruby String is tagged UTF-8 but fails valid_encoding? and raises misleading errors from downstream String operations (upcase, split, regex, encode, JSON.generate). Add a symmetric branch alongside the leading-surrogate check to raise JSON::ParserError at the parse boundary. Extend test_invalid_surogates with the new cases and move the three JSONTestSuite fixtures that this fix newly rejects from INVALID_ENCODING_TESTS into UNDEFINED_FAILING, closing the CRuby/JRuby parser parity gap documented in the file.
1 parent 1d37403 commit 82bf6be

4 files changed

Lines changed: 16 additions & 3 deletions

File tree

‎CHANGES.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### Unreleased
44

55
* Add `JSON::ParserError#json_path` to locate parse errors in the document as a JSONPath-style string (e.g. `$.foo[0].bar`). For duplicate key errors it points at the duplicated key itself.
6+
* Fix the parser to also reject lone trailing UTF-16 surrogates (`\uDCxx` with no leading partner), symmetric to the leading-surrogate case. The Java parser already rejected these; this closes the CRuby/JRuby parity gap.
67

78
### 2026-08-11 (3.0.0.rc1)
89

‎ext/json/ext/parser/parser.c‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,9 @@ NOINLINE(static) VALUE json_string_unescape(JSON_ParserState *state, JSON_Parser
10411041
raise_syntax_error_at("incomplete surrogate pair at %s", state, p);
10421042
break;
10431043
}
1044+
} else if ((ch & 0xFC00) == 0xDC00) {
1045+
raise_syntax_error_at("unpaired trailing surrogate at %s", state, p);
1046+
break;
10441047
}
10451048

10461049
int unescape_len = convert_UTF32_to_UTF8(buffer, ch);

‎test/json/json_minefield_parser_test.rb‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,9 @@ def define_test(name, &block)
2222
i_string_overlong_sequence_2_bytes
2323
i_string_not_in_unicode_range
2424
i_string_lone_utf8_continuation_byte
25-
i_string_lone_second_surrogate
2625
i_string_iso_latin_1
2726
i_string_invalid_utf-8
28-
i_string_incomplete_surrogate_pair
2927
i_string_UTF-8_invalid_sequence
30-
i_object_key_lone_2nd_surrogate
3128
)
3229

3330
COMMENT_TESTS = %w(
@@ -56,6 +53,9 @@ def define_test(name, &block)
5653
i_string_utf16BE_no_BOM
5754
i_string_utf16LE_no_BOM
5855
i_structure_UTF-8_BOM_empty_object
56+
i_string_lone_second_surrogate
57+
i_string_incomplete_surrogate_pair
58+
i_object_key_lone_2nd_surrogate
5959
)
6060

6161
if RUBY_ENGINE == 'jruby'

‎test/json/json_parser_test.rb‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,15 @@ def test_invalid_surogates
403403
assert_raise(JSON::ParserError) { parse('"\\uD800_________________"') }
404404
assert_raise(JSON::ParserError) { parse('"\\uD800\\u0041"') }
405405
assert_raise(JSON::ParserError) { parse('"\\uD800\\u004') }
406+
# Lone trailing surrogate (issue #1069): parser previously returned an
407+
# invalid-UTF-8 String instead of raising. Symmetric to the leading cases
408+
# above.
409+
assert_raise(JSON::ParserError) { parse('"\\uDC00"') }
410+
assert_raise(JSON::ParserError) { parse('"\\uDC00_________________"') }
411+
assert_raise(JSON::ParserError) { parse('"\\uDC00\\uD800"') }
412+
# Valid pair still parses to the astral codepoint U+10000.
413+
assert_predicate JSON.parse('"\\uD800\\uDC00"'), :valid_encoding?
414+
assert_equal "\u{10000}", JSON.parse('"\\uD800\\uDC00"')
406415
end
407416

408417
def test_parse_big_integers

0 commit comments

Comments
 (0)