Describe the bug
OpenApiDocument.LoadAsync can fail on a valid document with RegexMatchTimeoutException when the machine is under load, even though the regex involved does only linear work.
Loading runs the default rule set. One rule in it, OpenApiComponentsRules.KeyMustBeRegularExpression, matches every components key against:
internal static readonly Regex KeyRegex = new(@"^[a-zA-Z0-9\.\-_]+$", RegexOptions.None, TimeSpan.FromMilliseconds(100));
(source at v3.9.0; unchanged at v3.10.2)
This is an interpreted Regex. The interpreter's scan calls CheckTimeout() before every match attempt and inside its loops. .NET regex timeouts are measured on the wall clock from the start of the match, so the check can fire for reasons unrelated to the regex. If the matching thread is descheduled, or paused by a garbage collection, for more than 100 ms while it matches a key, the match throws, even though matching that key takes microseconds. The exception escapes LoadAsync, so the document fails to load.
The pattern is a single anchored character-class loop. Its work is linear in the key length, and it cannot backtrack catastrophically. The timeout therefore protects nothing here and only makes loading nondeterministic.
(OpenApiResponsesRules.StatusCodeRegex also declares a 100 ms timeout. On net8.0 and later it is source-generated with no loop, and the generated code never checks the timeout, so it is not the cause. Downlevel targets build it as an interpreted Regex, where the same exposure applies.)
OpenApi File To Reproduce
Any valid document. The failure depends on machine load, not on the document's content.
We hit it in CI. Our test suite parses a 140-operation OpenAPI 3.1 document, with 380 components keys, more than a dozen times per run. On a busy Windows GitHub-hosted runner, that parse failed twice in about 200 runs with:
RegexMatchTimeoutException: The Regex engine has timed out while trying to match a pattern to an input string. ...
It never failed on Linux or macOS runners, or locally.
Expected behavior
Whether a key is valid should not depend on how busy the machine is. Any of these would remove the wall-clock dependency:
Regex.InfiniteMatchTimeout for these fixed, library-owned patterns, since they are not user input;
RegexOptions.NonBacktracking;
- a source-generated regex (
[GeneratedRegex]), as StatusCodeRegex already uses on net8.0 and later.
Screenshots/Code Snippets
To keep the default validation unchanged, we replaced this one rule in our reader's ValidationRuleSet with an equivalent that has no timeout. It accepts and refuses exactly the same keys and reports the same error. We will remove it once the library drops the timeout.
Additional context
Microsoft.OpenApi 3.9.0 (checked unchanged in 3.10.2), .NET 10, Windows Server GitHub-hosted runner. We found this while building a .NET SDK that generates from an OpenAPI 3.1 document (opencode-dotnet/opencode-sdk-dotnet).
Describe the bug
OpenApiDocument.LoadAsynccan fail on a valid document withRegexMatchTimeoutExceptionwhen the machine is under load, even though the regex involved does only linear work.Loading runs the default rule set. One rule in it,
OpenApiComponentsRules.KeyMustBeRegularExpression, matches every components key against:(source at v3.9.0; unchanged at v3.10.2)
This is an interpreted
Regex. The interpreter's scan callsCheckTimeout()before every match attempt and inside its loops. .NET regex timeouts are measured on the wall clock from the start of the match, so the check can fire for reasons unrelated to the regex. If the matching thread is descheduled, or paused by a garbage collection, for more than 100 ms while it matches a key, the match throws, even though matching that key takes microseconds. The exception escapesLoadAsync, so the document fails to load.The pattern is a single anchored character-class loop. Its work is linear in the key length, and it cannot backtrack catastrophically. The timeout therefore protects nothing here and only makes loading nondeterministic.
(
OpenApiResponsesRules.StatusCodeRegexalso declares a 100 ms timeout. On net8.0 and later it is source-generated with no loop, and the generated code never checks the timeout, so it is not the cause. Downlevel targets build it as an interpretedRegex, where the same exposure applies.)OpenApi File To Reproduce
Any valid document. The failure depends on machine load, not on the document's content.
We hit it in CI. Our test suite parses a 140-operation OpenAPI 3.1 document, with 380 components keys, more than a dozen times per run. On a busy Windows GitHub-hosted runner, that parse failed twice in about 200 runs with:
It never failed on Linux or macOS runners, or locally.
Expected behavior
Whether a key is valid should not depend on how busy the machine is. Any of these would remove the wall-clock dependency:
Regex.InfiniteMatchTimeoutfor these fixed, library-owned patterns, since they are not user input;RegexOptions.NonBacktracking;[GeneratedRegex]), asStatusCodeRegexalready uses on net8.0 and later.Screenshots/Code Snippets
To keep the default validation unchanged, we replaced this one rule in our reader's
ValidationRuleSetwith an equivalent that has no timeout. It accepts and refuses exactly the same keys and reports the same error. We will remove it once the library drops the timeout.Additional context
Microsoft.OpenApi 3.9.0 (checked unchanged in 3.10.2), .NET 10, Windows Server GitHub-hosted runner. We found this while building a .NET SDK that generates from an OpenAPI 3.1 document (opencode-dotnet/opencode-sdk-dotnet).