From 5b06142fb6d55df8517ee26f4dc154f47d41e6ec Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Mon, 23 Mar 2026 20:31:56 -0600 Subject: [PATCH 1/4] Fix regex test OOM on x86 checked coreclr Chunk SourceGenRegexAsync batches to 200 patterns on 32-bit processes to avoid OutOfMemoryException from Roslyn compiling thousands of patterns at once in the ~2GB address space. Skip CharClassSubtraction_DeepNesting_DoesNotStackOverflow on 32-bit as the 1000-depth nested BDD exhausts address space. Fixes #126003 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/FunctionalTests/Regex.Match.Tests.cs | 5 +++++ .../RegexGeneratorHelper.netcoreapp.cs | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) 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 cd5eb03827ffbd..eb6d7fe98bb8b9 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 @@ -2723,6 +2723,11 @@ public async Task CharClassSubtraction_DeepNesting_DoesNotStackOverflow(RegexEng throw new SkipTestException("Deep nesting with NonBacktracking hits threading APIs not supported on single-threaded WASM."); } + if (!Environment.Is64BitProcess) + { + throw new SkipTestException("Deep nesting exhausts address space on 32-bit processes."); + } + // Build a pattern with deeply nested character class subtractions: [a-[a-[a-[...[a]...]]]] // This previously caused a StackOverflowException due to unbounded recursion in the parser. // Use a reduced depth for SourceGenerated to avoid overwhelming Roslyn compilation. diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs index 946b1d2f1be6e9..5343915788cb4c 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs @@ -146,7 +146,7 @@ internal static async Task SourceGenRegexAsync( (string pattern, CultureInfo? culture, RegexOptions? options, TimeSpan? matchTimeout)[] regexes, CancellationToken cancellationToken = default) { // Un-ifdef to compile each regex individually, which can be useful if one regex among thousands is causing a failure. - // We compile them all en mass for test efficiency, but it can make it harder to debug a compilation failure in one of them. + // We compile them all en masse for test efficiency, but it can make it harder to debug a compilation failure in one of them. #if false if (regexes.Length > 1) { @@ -159,6 +159,21 @@ internal static async Task SourceGenRegexAsync( } #endif + // On 32-bit processes the ~2GB address space limit can cause OutOfMemoryException + // when Roslyn compiles thousands of patterns at once. Chunk into smaller batches + // on 32-bit to avoid OOM while keeping full batching on 64-bit. + const int MaxBatchSize = 200; + if (!Environment.Is64BitProcess && regexes.Length > MaxBatchSize) + { + var results = new List(); + for (int i = 0; i < regexes.Length; i += MaxBatchSize) + { + int end = Math.Min(i + MaxBatchSize, regexes.Length); + results.AddRange(await SourceGenRegexAsync(regexes[i..end], cancellationToken)); + } + return results.ToArray(); + } + Debug.Assert(regexes.Length > 0); var code = new StringBuilder(); From ac0eb69c60c230b049c3c98cfcb41847725c9fa3 Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Mon, 23 Mar 2026 20:59:52 -0600 Subject: [PATCH 2/4] Address review feedback: use ConditionalTheory attribute and pre-size list - Use [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.Is64BitProcess))] instead of runtime check, matching the pattern used by StressTestDeepNestingOfLoops - Pre-allocate List with regexes.Length capacity to avoid resizing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/FunctionalTests/Regex.Match.Tests.cs | 7 +------ .../FunctionalTests/RegexGeneratorHelper.netcoreapp.cs | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) 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 eb6d7fe98bb8b9..dc92e4bd893f62 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 @@ -2713,7 +2713,7 @@ public async Task StressTestDeepNestingOfLoops(RegexEngine engine, string begin, } } - [ConditionalTheory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.Is64BitProcess))] // deep nesting exhausts address space on 32-bit [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "Fix is not available on .NET Framework")] [MemberData(nameof(RegexHelpers.AvailableEngines_MemberData), MemberType = typeof(RegexHelpers))] public async Task CharClassSubtraction_DeepNesting_DoesNotStackOverflow(RegexEngine engine) @@ -2723,11 +2723,6 @@ public async Task CharClassSubtraction_DeepNesting_DoesNotStackOverflow(RegexEng throw new SkipTestException("Deep nesting with NonBacktracking hits threading APIs not supported on single-threaded WASM."); } - if (!Environment.Is64BitProcess) - { - throw new SkipTestException("Deep nesting exhausts address space on 32-bit processes."); - } - // Build a pattern with deeply nested character class subtractions: [a-[a-[a-[...[a]...]]]] // This previously caused a StackOverflowException due to unbounded recursion in the parser. // Use a reduced depth for SourceGenerated to avoid overwhelming Roslyn compilation. diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs index 5343915788cb4c..791b7e56685e77 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs @@ -165,7 +165,7 @@ internal static async Task SourceGenRegexAsync( const int MaxBatchSize = 200; if (!Environment.Is64BitProcess && regexes.Length > MaxBatchSize) { - var results = new List(); + var results = new List(regexes.Length); for (int i = 0; i < regexes.Length; i += MaxBatchSize) { int end = Math.Min(i + MaxBatchSize, regexes.Length); From fb1b5e72b14d0203898b436b3083b0cab3aad639 Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Wed, 25 Mar 2026 21:54:31 -0600 Subject: [PATCH 3/4] Fix CS0136: rename 'results' to 'batchResults' to avoid conflict with enclosing scope Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../FunctionalTests/RegexGeneratorHelper.netcoreapp.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs index 791b7e56685e77..a1167107203a1b 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs @@ -165,13 +165,13 @@ internal static async Task SourceGenRegexAsync( const int MaxBatchSize = 200; if (!Environment.Is64BitProcess && regexes.Length > MaxBatchSize) { - var results = new List(regexes.Length); + var batchResults = new List(regexes.Length); for (int i = 0; i < regexes.Length; i += MaxBatchSize) { int end = Math.Min(i + MaxBatchSize, regexes.Length); - results.AddRange(await SourceGenRegexAsync(regexes[i..end], cancellationToken)); + batchResults.AddRange(await SourceGenRegexAsync(regexes[i..end], cancellationToken)); } - return results.ToArray(); + return batchResults.ToArray(); } Debug.Assert(regexes.Length > 0); From ff59a7ba78b43ebf4e68a8ac54dfeac444533599 Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Fri, 27 Mar 2026 18:03:46 -0600 Subject: [PATCH 4/4] Simplify 32-bit chunking loop with Enumerable.Chunk Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs index a1167107203a1b..6644119e4016d8 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs @@ -166,10 +166,9 @@ internal static async Task SourceGenRegexAsync( if (!Environment.Is64BitProcess && regexes.Length > MaxBatchSize) { var batchResults = new List(regexes.Length); - for (int i = 0; i < regexes.Length; i += MaxBatchSize) + foreach (var chunk in regexes.Chunk(MaxBatchSize)) { - int end = Math.Min(i + MaxBatchSize, regexes.Length); - batchResults.AddRange(await SourceGenRegexAsync(regexes[i..end], cancellationToken)); + batchResults.AddRange(await SourceGenRegexAsync(chunk, cancellationToken)); } return batchResults.ToArray(); }