Regex: negated character class loop followed by \b incorrectly made atomic

Aperta
#136,601 3 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
54/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
csharp
Ambito
testing-qa

Direzione di ricerca

Start by reading RegexCharClass.IsKnownWordClassSubset in src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCharClass.cs and the fix in PR 125636. Add regression coverage for the listed negated character class patterns, including the source generator if the existing test structure supports it. Confirm the tests fail on .NET 10 and pass on the fixed code; the expected matches are given in the issue.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

area-System.Text.RegularExpressions untriaged
Description

On .NET 10, a + loop over a negated character class followed by \b is incorrectly made atomic,
so the backtracking engines and the source generator fail to find matches that exist. The
NonBacktracking engine is correct. .NET 9 was correct, and .NET 11 is already correct.

The pattern ^[^a-z]+\b(?:\W+|$) matches nothing at all on .NET 10.

PR 125636 fixed this in .net 11 but doesn't have a dedicated test.

This is not the same as https://github.com/dotnet/runtime/issues/135108

Reproduction Steps
using System.Text.RegularExpressions;
Console.WriteLine(new Regex(@"^[^a-z]+\b\W").IsMatch("A ghi"));  // False on .NET 10; True on 9 and 11

Full three-runtime Docker repro (one image per SDK, identical source):
docker build --build-arg SDK_TAG=10.0 --build-arg TFM=net10.0 -t r . && docker run --rm r

Pattern Input .NET 9.0.19 .NET 10.0.11 .NET 11.0.0-p7
^[^a-z]+\b A ghi "A " "A " "A "
^[^a-z]+\b\W A ghi "A " no match "A "
^[^a-z]+\b(?:\W+|$) A ghi "A " no match "A "
^[^a-z]*\b(?:\W+|$) A ghi "A " "A " "A "
^[^a-z]{1,}\b\W A ghi "A " no match "A "
^[^a-z]{1,9}\b\W A ghi "A " no match "A "
^[A-Z ]+\b(?:\W+|$) A ghi "A " "A " "A "
^[^0-9]+\b\W A ghi "A " no match "A "
^\W+\b\W -- ghi no match no match no match

Trigger requires both a loop minimum ≥ 1 (the node.M > 0 guard) and a negated set whose stated
characters are all word characters. RegexOptions.None, Compiled and [GeneratedRegex] are all
affected; NonBacktracking is not.

Expected behavior

Expected true

Actual behavior

.NET 10 returns false.

Regression?

Yes, .NET 9 → .NET 10. Introduced by #117892; incidentally fixed on main by #125636, which replaced
IsKnownWordClassSubset with IsSubsetOf guarded by !IsNegated(subset). Root-cause detail and the
proposed one-line patch for release/10.0 are above.

Known Workarounds
  • RegexOptions.NonBacktracking — correct on all versions, no pattern change.
  • Move the boundary into the alternation: ^[^a-z]+(?:\b\W+|$). Note ^[^a-z]+(?:\b\W+|\b$) still reproduces, so it only helps when not every branch begins with the boundary.
  • Avoid the negated class, or use * where semantics permit.
Configuration

.NET 10.0.14 - incorrect
.NET 10.0.11 (SDK 10.0.400) - incorrect
.NET 9.0.19 (SDK 9.0.317) - correct
.NET 11.0.0-preview.7.26381.103 - correct
Reproduced in the official mcr.microsoft.com/dotnet/sdk images on both linux/arm64 and linux/amd64, and on macOS 15 arm64 directly.

Other information

One guard, mirroring what the category path above it already does:

--- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCharClass.cs
+++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCharClass.cs
@@ public static bool IsKnownWordClassSubset(string set)
         // If we can enumerate every character in the set quickly, do so, checking to see whether they're all in \w.
-        if (CanEasilyEnumerateSetContents(set))
+        // Note: for a negated set this would enumerate the characters that are EXCLUDED, so bail.
+        if (!IsNegated(set) && CanEasilyEnumerateSetContents(set))
         {

Plus a regression test, which is missing everywhere today:

// [^a-z] is not a subset of \w - it contains spaces, punctuation, digits and uppercase -
// so [^a-z]+ must not be made atomic ahead of the \b.
yield return (@"^[^a-z]+\b(?:\W+|$)", "CA Actress", RegexOptions.None, 0, 10, true, "CA ");
yield return (@"^[^a-z]+\b\W",        "A ghi",      RegexOptions.None, 0, 5,  true, "A ");
yield return (@"^[^0-9]+\b\W",        "A ghi",      RegexOptions.None, 0, 5,  true, "A ");

Worth landing that test on main too, so .NET 11's currently-incidental correctness is actually pinned.

Lingua principale
C#
Stelle
18.4k
Fork
5.8k
Merge medio
2g 17h
PR unite (30g)
581

Preparare l'ambiente

Apri in Codespaces

Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di dotnet/runtime

Tutte le issue di dotnet/runtime

Issue simili

Altre issue su C#

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.