From eb37283f71fbbb7a75e8ec475ed6d878779e335f Mon Sep 17 00:00:00 2001 From: nabsei Date: Sun, 19 Jul 2026 14:59:55 +0200 Subject: [PATCH] - lexer-strings.rl: fix crash on \c/\M- escapes with an invalid byte on a UTF-8 source read_post_meta_or_ctrl_char, slash_c_char and slash_m_char all called .ord on @escape without controlling its encoding. @escape usually carries the source buffer's own encoding (eg. UTF-8 by default), and either that byte directly, or the result of encode_escape() applying the \c/\M- bit operations to it and force_encoding-ing the result back to the source's encoding, can be invalid on its own in that encoding (eg. \c\xFF, whose \x escape decodes to byte 0xFF). Parsing such a literal from a UTF-8 source then crashed with an unhandled "ArgumentError: invalid byte sequence in UTF-8" instead of the existing, already-handled :invalid_encoding diagnostic that similar escapes like \M-a already raise gracefully. Read @escape's byte value with String#b instead, since a raw byte is what \c/\M- escapes actually operate on. The existing lexer-level test for these escapes only exercised an ascii-8bit source, which never hit this; added a parser-level test using the common, UTF-8-by-default case that reproduces the crash. Relates to https://github.com/rubocop/rubocop/issues/15457 Co-authored-by: Claude --- lib/parser/lexer-strings.rl | 15 ++++++++--- test/test_parser.rb | 50 +++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/lib/parser/lexer-strings.rl b/lib/parser/lexer-strings.rl index 23e206d28..764940c41 100644 --- a/lib/parser/lexer-strings.rl +++ b/lib/parser/lexer-strings.rl @@ -445,7 +445,11 @@ class Parser::LexerStrings end def read_post_meta_or_ctrl_char(p) - @escape = source_buffer.slice(p - 1, 1).chr + # \c and \M- escapes operate on the raw byte value of their target, not on + # a Unicode codepoint, so read it as a raw byte here: the source buffer's + # own encoding (eg. UTF-8) would otherwise make `.ord` below raise on a + # byte that isn't valid on its own in that encoding (eg. \c\xFF). + @escape = source_buffer.slice(p - 1, 1).chr.b if @version >= 27 && ((0..8).include?(@escape.ord) || (14..31).include?(@escape.ord)) diagnostic :fatal, :invalid_escape @@ -477,11 +481,16 @@ class Parser::LexerStrings end def slash_c_char - @escape = encode_escape(@escape[0].ord & 0x9f) + # @escape may already carry the source encoding (eg. via encode_escape, + # which force_encodes a raw byte without validating it), so read its + # value as a raw byte here rather than as a character in that encoding: + # a \c/\M control/meta escape operates on bytes, and .ord on a string + # that isn't valid in its tagged encoding raises ArgumentError. + @escape = encode_escape(@escape.b[0].ord & 0x9f) end def slash_m_char - @escape = encode_escape(@escape[0].ord | 0x80) + @escape = encode_escape(@escape.b[0].ord | 0x80) end def emit_character_constant diff --git a/test/test_parser.rb b/test/test_parser.rb index e9d95ef8b..9c898471d 100644 --- a/test/test_parser.rb +++ b/test/test_parser.rb @@ -11070,6 +11070,56 @@ def test_control_meta_escape_chars_in_regexp__since_31 SINCE_3_1) end + # Same escapes as test_control_meta_escape_chars_in_regexp__since_31, but + # from a UTF-8 source (the common case, and what these literals actually + # have by default unless the source is forced to ascii-8bit as above). + # \x9F on its own isn't valid UTF-8, so this can't successfully parse to a + # :str node the way the ascii-8bit case does; it must instead raise a + # graceful diagnostic rather than crash with an unhandled ArgumentError. + def test_control_meta_escape_chars_in_regexp_from_utf8_source + assert_diagnoses( + [:error, :invalid_encoding], + %q{/\c\xFF/}, + %q{}, + SINCE_3_1) + + assert_diagnoses( + [:error, :invalid_encoding], + %q{/\c\M-\xFF/}, + %q{}, + SINCE_3_1) + + assert_diagnoses( + [:error, :invalid_encoding], + %q{/\C-\xFF/}, + %q{}, + SINCE_3_1) + + assert_diagnoses( + [:error, :invalid_encoding], + %q{/\C-\M-\xFF/}, + %q{}, + SINCE_3_1) + + assert_diagnoses( + [:error, :invalid_encoding], + %q{/\M-\xFF/}, + %q{}, + SINCE_3_1) + + assert_diagnoses( + [:error, :invalid_encoding], + %q{/\M-\C-\xFF/}, + %q{}, + SINCE_3_1) + + assert_diagnoses( + [:error, :invalid_encoding], + %q{/\M-\c\xFF/}, + %q{}, + SINCE_3_1) + end + def test_forward_arg_with_open_args assert_diagnoses_many( [