What's wrong
Commit b249339 bounded regex evaluation with a 1-second RegexMatchTimeout (TextFilter/TextFilter.cs:121-124). The comment says this is "short enough that a pathological one cannot wedge a UI", and the DoesMatchRegex remarks say "a caller-supplied pattern cannot hang the calling thread".
The timeout limits each Regex.IsMatch call, and nothing remembers that a pattern timed out. DoesMatchRegex catches the RegexMatchTimeoutException and returns false for that one token (TextFilter.cs:494-506). The next word, and the next item in Filter, run the same pathological pattern again and wait the full second each time. Worst-case time is therefore:
items × distinct words per item × 1 s
The cache (AddBounded(RegexCache, ...), line 477) stores the compiled Regex, so the pattern stays expensive on every keystroke and every item for the lifetime of the process.
Reproduction
using System.Diagnostics;
using ktsu.TextFilter;
string bad = new string('a', 40) + "X";
var items = Enumerable.Range(0, 10).Select(i => bad + i).ToList();
var sw = Stopwatch.StartNew();
var res = TextFilter.Filter(items, "(a+)+$", TextFilterType.Regex, TextFilterMatchOptions.ByWholeString).ToList();
Console.WriteLine($"Filter 10 items: {res.Count} results in {sw.ElapsedMilliseconds} ms");
sw.Restart();
bool r = TextFilter.IsMatch(string.Join(' ', Enumerable.Range(0, 5).Select(i => new string('a', 40) + "X" + i)),
"(a+)+$", TextFilterType.Regex, TextFilterMatchOptions.ByWordAny);
Console.WriteLine($"IsMatch, one text of 5 words: {r} in {sw.ElapsedMilliseconds} ms");
Observed (net10.0, main 1a5f996):
Filter 10 items: 0 results in 10005 ms
IsMatch, one text of 5 words: False in 5028 ms
Expected: once the pattern has timed out, the rest of the evaluation should not pay the timeout again. The whole Filter call should take about 1 s, not 1 s per item and per word.
In a keystroke-driven filter box over 1,000 entries (the use case the cache comments describe), a pattern like (a+)+$ or (\w+\s?)+$ blocks the UI thread for about 17 minutes, which is the hang the timeout was meant to prevent. The existing test ACatastrophicallyBacktrackingPatternTimesOutInsteadOfHanging checks only a single IsMatch on a single token, so it does not catch this.
Suggested fix / acceptance criteria
- When a pattern times out, record that under its cache key (for example, replace the cached
Regex with a "timed out" sentinel or track it in a bounded set). Later tokens and calls should then return the degraded answer (false) immediately instead of running the regex again.
- Optionally, treat the timeout as a budget for the whole
DoesMatchRegex call rather than for each token.
- Add tests:
Filter over at least 10 pathological items with (a+)+$ completes in well under 10 × the timeout (for example < 3 s).
IsMatch with ByWordAny/ByWordAll on text with several distinct pathological words completes in about one timeout.
- Ordinary patterns are unaffected, and a pattern that timed out under one case sensitivity does not poison the other (the keys already differ by the
i:/s: prefix).
What's wrong
Commit b249339 bounded regex evaluation with a 1-second
RegexMatchTimeout(TextFilter/TextFilter.cs:121-124). The comment says this is "short enough that a pathological one cannot wedge a UI", and theDoesMatchRegexremarks say "a caller-supplied pattern cannot hang the calling thread".The timeout limits each
Regex.IsMatchcall, and nothing remembers that a pattern timed out.DoesMatchRegexcatches theRegexMatchTimeoutExceptionand returnsfalsefor that one token (TextFilter.cs:494-506). The next word, and the next item inFilter, run the same pathological pattern again and wait the full second each time. Worst-case time is therefore:items × distinct words per item × 1 sThe cache (
AddBounded(RegexCache, ...), line 477) stores the compiledRegex, so the pattern stays expensive on every keystroke and every item for the lifetime of the process.Reproduction
Observed (net10.0, main 1a5f996):
Expected: once the pattern has timed out, the rest of the evaluation should not pay the timeout again. The whole
Filtercall should take about 1 s, not 1 s per item and per word.In a keystroke-driven filter box over 1,000 entries (the use case the cache comments describe), a pattern like
(a+)+$or(\w+\s?)+$blocks the UI thread for about 17 minutes, which is the hang the timeout was meant to prevent. The existing testACatastrophicallyBacktrackingPatternTimesOutInsteadOfHangingchecks only a singleIsMatchon a single token, so it does not catch this.Suggested fix / acceptance criteria
Regexwith a "timed out" sentinel or track it in a bounded set). Later tokens and calls should then return the degraded answer (false) immediately instead of running the regex again.DoesMatchRegexcall rather than for each token.Filterover at least 10 pathological items with(a+)+$completes in well under 10 × the timeout (for example < 3 s).IsMatchwithByWordAny/ByWordAllon text with several distinct pathological words completes in about one timeout.i:/s:prefix).