From 2fca8ccd0d7a0379e928a6124338b3e3e3102ab6 Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Fri, 20 Feb 2026 00:55:37 -0700 Subject: [PATCH 1/3] Use SearchValues for Setrep/Setloop interpreter opcodes Precompute SearchValues for character class strings at regex construction time. Use them in the Setrep and Setloop/Setloopatomic opcode handlers to replace per-character CharInClass loops with vectorized SIMD-accelerated span operations. Character classes that use Unicode categories, subtraction+negation, or have more than 128 characters fall back to the existing per-character path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../RegularExpressions/RegexInterpreter.cs | 53 +++++++++++++++---- .../RegexInterpreterCode.cs | 45 ++++++++++++++++ 2 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexInterpreter.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexInterpreter.cs index e477f7c742babc..c47abff0f27508 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexInterpreter.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexInterpreter.cs @@ -936,15 +936,29 @@ private bool TryMatchAtCurrentPosition(ReadOnlySpan inputSpan) } int operand0 = Operand(0); - string set = _code.Strings[operand0]; - ref uint[]? setLookup = ref _code.StringsAsciiLookup[operand0]; - while (c-- > 0) + if (!_rightToLeft && + _code.StringsSetSearchValues[operand0] is RegexInterpreterCode.SetSearchValues setSearchValues) { - if (!RegexCharClass.CharInClass(Forwardcharnext(inputSpan), set, ref setLookup)) + if (!setSearchValues.AllMatch(inputSpan.Slice(runtextpos, c))) { goto BreakBackward; } + + runtextpos += c; + } + else + { + string set = _code.Strings[operand0]; + ref uint[]? setLookup = ref _code.StringsAsciiLookup[operand0]; + + while (c-- > 0) + { + if (!RegexCharClass.CharInClass(Forwardcharnext(inputSpan), set, ref setLookup)) + { + goto BreakBackward; + } + } } } advance = 2; @@ -1022,16 +1036,35 @@ private bool TryMatchAtCurrentPosition(ReadOnlySpan inputSpan) { int len = Math.Min(Operand(1), Forwardchars()); int operand0 = Operand(0); - string set = _code.Strings[operand0]; - ref uint[]? setLookup = ref _code.StringsAsciiLookup[operand0]; int i; - for (i = len; i > 0; i--) + if (!_rightToLeft && + _code.StringsSetSearchValues[operand0] is RegexInterpreterCode.SetSearchValues setSearchValues) { - if (!RegexCharClass.CharInClass(Forwardcharnext(inputSpan), set, ref setLookup)) + int idx = setSearchValues.IndexOfAnyNonMatch(inputSpan.Slice(runtextpos, len)); + if (idx == -1) { - Backwardnext(); - break; + runtextpos += len; + i = 0; + } + else + { + runtextpos += idx; + i = len - idx; + } + } + else + { + string set = _code.Strings[operand0]; + ref uint[]? setLookup = ref _code.StringsAsciiLookup[operand0]; + + for (i = len; i > 0; i--) + { + if (!RegexCharClass.CharInClass(Forwardcharnext(inputSpan), set, ref setLookup)) + { + Backwardnext(); + break; + } } } diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexInterpreterCode.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexInterpreterCode.cs index 1fdf1112cd33a7..64207781a213b5 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexInterpreterCode.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexInterpreterCode.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -19,9 +20,53 @@ internal sealed class RegexInterpreterCode(RegexFindOptimizations findOptimizati public readonly string[] Strings = strings; /// ASCII lookup table optimization for sets in . public readonly uint[]?[] StringsAsciiLookup = new uint[strings.Length][]; + /// Precomputed set matchers for character class strings in , enabling vectorized scanning for Set opcodes. + public readonly SetSearchValues?[] StringsSetSearchValues = CreateSetSearchValues(strings); /// How many instructions in use backtracking. public readonly int TrackCount = trackcount; + /// Tries to create SetSearchValues for each character class string. + private static SetSearchValues?[] CreateSetSearchValues(string[] strings) + { + var result = new SetSearchValues?[strings.Length]; + Span chars = stackalloc char[128]; + + for (int i = 0; i < strings.Length; i++) + { + string set = strings[i]; + if (set.Length >= RegexCharClass.SetStartIndex) + { + // GetSetChars enumerates the characters described by the set (ignoring negation). + // If the set uses Unicode categories or has too many chars, it returns 0. + int count = RegexCharClass.GetSetChars(set, chars); + if (count > 0) + { + result[i] = new SetSearchValues( + SearchValues.Create(chars.Slice(0, count)), + RegexCharClass.IsNegated(set)); + } + } + } + + return result; + } + + /// Wraps a with the set's negation flag so the interpreter + /// can test "is character in class" without knowing whether the class was defined as negated. + internal readonly struct SetSearchValues(SearchValues values, bool negated) + { + private readonly SearchValues _values = values; + private readonly bool _negated = negated; + + /// Returns true if all characters in are in the character class. + public bool AllMatch(ReadOnlySpan span) => + _negated ? !span.ContainsAny(_values) : !span.ContainsAnyExcept(_values); + + /// Returns the index of the first character not in the character class, or -1 if all match. + public int IndexOfAnyNonMatch(ReadOnlySpan span) => + _negated ? span.IndexOfAny(_values) : span.IndexOfAnyExcept(_values); + } + /// Gets whether the specified opcode may incur backtracking. public static bool OpcodeBacktracks(RegexOpcode opcode) { From f53b79751a09d5aef0dbad229b98685b1a95bf00 Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Fri, 20 Feb 2026 01:28:23 -0700 Subject: [PATCH 2/3] Validate char-class encoding before calling GetSetChars The Strings table contains both character class strings and Multi literal strings. Add validation that the string has a well-formed char-class encoding (valid flags byte, consistent lengths) before calling GetSetChars, which assumes well-formed input. Also clarify the comment about GetSetChars behavior for negated sets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../RegularExpressions/RegexInterpreterCode.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexInterpreterCode.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexInterpreterCode.cs index 64207781a213b5..a33e778a28b6a3 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexInterpreterCode.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexInterpreterCode.cs @@ -34,10 +34,18 @@ internal sealed class RegexInterpreterCode(RegexFindOptimizations findOptimizati for (int i = 0; i < strings.Length; i++) { string set = strings[i]; - if (set.Length >= RegexCharClass.SetStartIndex) + + // The Strings table contains both character class strings and Multi literal strings. + // Character class strings have a flags byte of 0 (not negated) or 1 (negated) at index 0, + // followed by set length and category length. Validate the encoding before calling GetSetChars, + // which assumes a well-formed char-class string and could otherwise throw on arbitrary input. + if (set.Length >= RegexCharClass.SetStartIndex && + set[RegexCharClass.FlagsIndex] is '\0' or '\u0001' && + RegexCharClass.SetStartIndex + set[RegexCharClass.SetLengthIndex] + set[RegexCharClass.CategoryLengthIndex] <= set.Length) { - // GetSetChars enumerates the characters described by the set (ignoring negation). - // If the set uses Unicode categories or has too many chars, it returns 0. + // GetSetChars returns the characters that back the set. For a negated set, these are + // the characters excluded from the class; the separate IsNegated flag indicates how + // to interpret them. If the set uses Unicode categories or has too many chars, it returns 0. int count = RegexCharClass.GetSetChars(set, chars); if (count > 0) { From 195e844920eb6c85b9690c7dc1f75eaa2ba5ce9b Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Fri, 20 Feb 2026 01:41:01 -0700 Subject: [PATCH 3/3] Add regression test for Multi literal resembling char-class encoding A Multi literal starting with \0 and having an even-valued second byte and \0 at index 2 satisfies all CanEasilyEnumerateSetContents checks, causing GetSetChars to enumerate past the end of the string. This test verifies CreateSetSearchValues validates the encoding first. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/FunctionalTests/Regex.Match.Tests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Match.Tests.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Match.Tests.cs index 2ec270557c3c69..4e31ef38b16490 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Match.Tests.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Match.Tests.cs @@ -3231,5 +3231,20 @@ public static IEnumerable BalancingGroup_Various_MemberData() } } } + + [Fact] + public void Match_MultiLiteralResemblingCharClassEncoding() + { + // Regression test: a Multi literal string that happens to resemble a character class + // encoding could cause GetSetChars to throw IndexOutOfRangeException. + // The pattern below produces a Multi opcode whose literal string has: + // [0]='\0' (looks like non-negated flag), [1]='\u0002' (even "set length"), + // [2]='\0' (no categories), [3]='X' — but no [4], so the range enumeration + // in GetSetChars would access past the end of the string. + // CreateSetSearchValues must validate the char-class encoding before calling GetSetChars. + string input = "\x00\x02\x00X"; + var regex = new Regex(input); + Assert.True(regex.IsMatch(input)); + } } }