What regexutil does not do¶
regexutil bounds one thing: the cost of turning an untrusted pattern string
into a compiled regex. Everything below is deliberately outside that scope.
Knowing where the boundary sits is the difference between a defence and a false
sense of one.
If you are looking for what it does, start with Threat model & the two defences.
It does not bound matching¶
CompileBounded returns an ordinary *regexp.Regexp. Once you have it,
regexutil is out of the picture — it does not wrap the regex, does not
intercept MatchString, and applies no deadline to any match you subsequently
run.
Go's RE2 engine guarantees matching is linear in the length of the input, which is why this is usually fine. "Linear" is not "free", though: matching a large program against a large input still costs time proportional to both. If you match an untrusted pattern against untrusted input — a whole file, a request body, a log stream — bound that separately, with your own context deadline or a size cap on the input.
It does not bound memory¶
The two defences are a length cap and a wall-clock timeout. Neither limits how many states the compiled automaton has or how much it allocates. A pattern that compiles inside 100 ms has already been allowed to allocate whatever it needed to get there.
The length cap makes this survivable rather than solved: 1 KiB of pattern can
only describe so large a program, and Go's own repeat-count ceiling of 1000
(see Which patterns does Go itself reject?)
caps the expansion further. There is no configurable memory bound because Go's
regexp package does not expose one.
The bounds are not configurable¶
MaxPatternLength and DefaultCompileTimeout are const declarations. There
is no options struct, no functional option, no environment variable, and no
setter. You can read them; you cannot change them.
CompileBoundedTimeout looks like it lets you set the timeout, and it does —
downwards only. Your value is layered on top of the package's, so the effective
bound is min(yours, 100 ms) and a larger value is silently ignored. This is
intentional: a package whose whole purpose is a bound should not offer a way to
remove it. If you need a different bound, the honest answer is that this is not
the package for you — regexp.Compile under your own context is a few lines.
It does not stop a runaway compile¶
The timeout unblocks you; it does not stop the work. regexp.Compile is not
context-aware, so the compile runs in a goroutine that keeps going after the
call has returned your error, until it finishes on its own.
For a stream of distinct pathological patterns, that is a stream of
goroutines you cannot cancel — each holding one compile's working set. The
reasoning for accepting that, and why the alternative is worse, is in
The goroutine-leak tradeoff.
The practical consequence: regexutil protects your latency, not your CPU. A
determined attacker who can drive many distinct expensive compiles can still
cost you cycles.
It is not a rate limiter¶
Each call is bounded independently. Ten thousand concurrent calls are ten
thousand independent 100 ms budgets, not a shared one. If untrusted patterns
arrive over the network, put a rate limit or a concurrency bound in front of
the compile — regexutil has no notion of how many compiles are in flight.
Compiling the same pattern repeatedly is likewise never deduplicated: there is
no cache. A hot path that recompiles a config pattern per request should
compile it once at load time and keep the *regexp.Regexp, which is safe for
concurrent use.
It does not judge what a pattern means¶
The bounds are about cost, not about semantics. A pattern that compiles is returned, whatever it matches.
The case that catches people is the empty pattern: "" compiles happily
and matches every input, including the empty string. An unset configuration key
usually arrives as "", so "the operator left the filter blank" and "the
operator asked to match everything" are the same value by the time it reaches
CompileBounded. If those mean different things in your tool, decide which
before you call. The same applies to .*, to an over-broad character class, or
to a pattern that is simply wrong — none of that is a compile-time concern.
It does not keep the pattern out of every error¶
Rejections for length and for timeout are careful never to echo the pattern, because attacker-chosen text in your logs is its own small denial of service. The invalid-syntax path is different: it wraps Go's parse error, and that message quotes the expression that failed.
That is useful to the operator who mistyped a bracket and hazardous to a log
sink fed by strangers. regexutil does not resolve the tension for you — see
Does the error contain the pattern?
for what to do at the call site.
It does not cover every entry point of regexp¶
There is one wrapped operation: regexp.Compile. There is no bounded
equivalent of regexp.CompilePOSIX, no MustCompile-style panicking variant
(a bounded compile can fail on valid input, so panicking would be wrong), and
no bounded regexp.Match convenience helper.
Nor does it wrap the pattern in any way: no automatic anchoring, no implicit
case-insensitivity, no syntax subsetting. What you pass is what
regexp.Compile sees.
It does not tell caller cancellation apart from a slow compile¶
Both produce ErrPatternCompileTimeout, and neither carries the context's own
error, so errors.Is(err, context.Canceled) is false on a call you cancelled
yourself. During shutdown, every in-flight compile reports as though its
pattern were pathological.
If that distinction matters — say you count timeout rejections as a signal of
abuse — check ctx.Err() alongside the returned error rather than trusting the
sentinel alone.
Related¶
- Threat model & the two defences — what the package does defend against, and why.
- Limits and timeouts — the exact values and their edge cases.
- Errors — what each failure returns.