Summary
@json.parse crashes the process (an abort from a guard!, not a catchable ParseError) when a JSON string contains a raw lone trailing surrogate together with any escape sequence.
Minimal repro
A 5-code-unit JSON text: " U+DC00 \n "
test {
let lone_low = String::from_array([(0xDC00).unsafe_to_char()])
// aborts in StringView::sub (builtin/stringview.mbt guard!) instead of parsing:
ignore(@json.parse("\"" + lone_low + "\\n\""))
}
Escape-free strings with the same lone surrogate parse fine (the fast path accepts them), so this is an internal inconsistency as well as a totality violation: a parser should either succeed or raise its documented ParseError, never abort.
Root cause
json/lex_string.mbt, ParseContext::lex_string_slow — the local flush helper slices the pending run with the checked ctx.input[start:end] (StringView::sub), which guard!-aborts when the code unit at a slice boundary is a trailing surrogate. The fast path in lex_string uses the bounds-check-only view(start_offset~, end_offset~) and therefore tolerates raw lone surrogates; only the slow path (taken whenever the string contains a backslash escape) crashes.
Affected targets
All backends — wasm-gc, js, and native (the abort is in shared library code; reproduced on all three).
Severity
High for robustness: any consumer parsing untrusted JSON text can be crashed (denial of service) by a short crafted input. Found by adversarial QuickCheck testing (the default Arbitrary String never generates lone surrogates, which is why existing property tests missed it).
Summary
@json.parsecrashes the process (an abort from aguard!, not a catchableParseError) when a JSON string contains a raw lone trailing surrogate together with any escape sequence.Minimal repro
A 5-code-unit JSON text:
"U+DC00\n"Escape-free strings with the same lone surrogate parse fine (the fast path accepts them), so this is an internal inconsistency as well as a totality violation: a parser should either succeed or raise its documented
ParseError, never abort.Root cause
json/lex_string.mbt,ParseContext::lex_string_slow— the localflushhelper slices the pending run with the checkedctx.input[start:end](StringView::sub), whichguard!-aborts when the code unit at a slice boundary is a trailing surrogate. The fast path inlex_stringuses the bounds-check-onlyview(start_offset~, end_offset~)and therefore tolerates raw lone surrogates; only the slow path (taken whenever the string contains a backslash escape) crashes.Affected targets
All backends — wasm-gc, js, and native (the abort is in shared library code; reproduced on all three).
Severity
High for robustness: any consumer parsing untrusted JSON text can be crashed (denial of service) by a short crafted input. Found by adversarial QuickCheck testing (the default
Arbitrary Stringnever generates lone surrogates, which is why existing property tests missed it).