Skip to content

Fix crash on \c/\M- escapes with an invalid byte on a UTF-8 source#1092

Open
nabsei wants to merge 1 commit into
whitequark:masterfrom
nabsei:fix-invalid-byte-sequence-crash-in-control-meta-escapes
Open

Fix crash on \c/\M- escapes with an invalid byte on a UTF-8 source#1092
nabsei wants to merge 1 commit into
whitequark:masterfrom
nabsei:fix-invalid-byte-sequence-crash-in-control-meta-escapes

Conversation

@nabsei

@nabsei nabsei commented Jul 19, 2026

Copy link
Copy Markdown

What

Parsing a control/meta escape (\c, \M-) whose target byte isn't valid on its own in the source's encoding — eg. \c\xFF from a UTF-8 source — crashes 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.

Reported downstream at rubocop, where a Ruby source file simply containing /\c\xFF/ as a regex literal (part of MRI's own regexp test suite) crashes rubocop entirely: rubocop/rubocop#15457

Why

read_post_meta_or_ctrl_char, slash_c_char and slash_m_char all call .ord on @escape without controlling its encoding. @escape usually carries the source buffer's own encoding (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.

\c and \M- escapes operate on the raw byte value of their target, not on a Unicode codepoint, so this reads @escape's value via String#b instead, which never raises regardless of whether the byte happens to be valid in the source's encoding.

Verification

  • Full test suite: 1125 runs, 567214 assertions, 0 failures, 0 errors (was 1124 runs before this PR's added test).
  • Added a parser-level regression test (test_control_meta_escape_chars_in_regexp_from_utf8_source) covering all 7 \c/\M-/\C- + \xFF combinations from a UTF-8 source (the common case) — confirmed it reproduces the exact crash on unmodified code and passes with the fix.
  • The existing lexer-level test for these escapes (test_meta_control_hex_escaped_char, targeting an old Ruby version) and the existing parser-level test (test_control_meta_escape_chars_in_regexp__since_31) both still pass unmodified — the latter only exercises an ascii-8bit-forced source, which is why it never caught this in the first place.
  • Manually checked that already-working escapes (\ca, \x41, \x7f) and already-gracefully-erroring ones (\M-a, \M-\C-a) are unaffected.

I'm not marking this as closing rubocop/rubocop#15457 directly since that repo depends on a released version of this gem; leaving it linked for context instead.


Disclosure: this PR was prepared with the assistance of Claude Code (Anthropic). The investigation, root-cause diagnosis, fix, and verification steps described above were done and checked by me.

…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 rubocop/rubocop#15457

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant