regexutil¶
A tiny, framework-free helper that makes it safe to compile a regular expression whose pattern came from outside your binary — a config file, a CLI flag, a TUI input box, an HTTP payload — by capping its length and bounding how long compilation may run.
The problem: compilation is the DoS surface, not matching¶
Go's regexp package is built on RE2, which guarantees linear-time
matching — it does not suffer the classic catastrophic backtracking that
lets (a+)+b hang a Perl- or Java-style engine at match time. That is a real
and valuable property, and it is easy to assume it makes untrusted patterns
safe.
It does not. RE2 protects the match; it does not make compilation
guaranteed linear. A pathological pattern — nested unbounded repetition like
(a+)+, deeply nested alternation, or simply a very long chain of bounded
repetitions — can spend measurable wall-clock time inside regexp.Compile and
allocate an automaton of many thousands of states. That is enough to hang a
CLI's update flow or freeze an interactive TUI while a single compile churns.
So the moment a pattern originates from a user or a config file, the unbounded call is the risk:
// Risky: an attacker (or a fat-fingered config) controls how long this runs
// and how much it allocates.
re, err := regexp.Compile(userPattern)
The two defences¶
regexutil applies two bounds uniformly, so nothing slips through:
- A byte-length cap —
MaxPatternLength(1024). Oversize patterns are rejected before any compile work begins. Legitimate filename globs and search queries are short; 1 KiB is generous. - A wall-clock compile timeout —
DefaultCompileTimeout(100 ms). This bounds the worst case for anything that passes the length cap. A well-behaved 1 KiB pattern compiles in well under a millisecond, so 100 ms is two orders of magnitude of headroom and still imperceptible interactively.
Length alone cannot catch a short-but-pathological pattern; a timeout alone would still let a huge pattern allocate before the clock runs out. The two together are defence in depth — see the threat model.
Framework-free by design¶
- One dependency. The module graph declares exactly one external dependency,
cockroachdb/errors— no config framework, no TUI, no logging stack. Adepfootprint_test.goguard fails the build if anything else enters the graph. - Typed sentinel errors.
ErrPatternTooLong,ErrPatternCompileTimeout, andErrPatternInvalidlet callers branch precisely witherrors.Is. - Two entry points.
CompileBoundedfor a call site that already carries acontext.Context;CompileBoundedTimeoutfor one that does not.
Install¶
Quick start¶
package main
import (
"context"
"fmt"
"github.com/cockroachdb/errors"
"gitlab.com/phpboyscout/go/regexutil"
)
func main() {
userPattern := `^feature/.*$` // from a config file, flag, or prompt
re, err := regexutil.CompileBounded(context.Background(), userPattern)
switch {
case errors.Is(err, regexutil.ErrPatternTooLong):
fmt.Println("pattern is too long; shorten it")
return
case errors.Is(err, regexutil.ErrPatternCompileTimeout):
fmt.Println("pattern is too complex to compile safely")
return
case errors.Is(err, regexutil.ErrPatternInvalid):
fmt.Println("pattern is not valid regex syntax")
return
case err != nil:
fmt.Println("unexpected error:", err)
return
}
fmt.Println("compiled:", re.MatchString("feature/login"))
}
When not to reach for this¶
regexutil is for patterns you did not write. A pattern that is a literal
known at build time carries no DoS risk — you control it — so keep using
regexp.MustCompile directly:
Where to go next¶
The documentation follows the Diátaxis framework:
- Getting started — a learning-oriented walkthrough: compile a config-supplied pattern and handle each failure mode.
- Compile an untrusted pattern — a task-oriented recipe: choosing between the two entry points, picking a timeout, and the error-handling switch.
- Threat model & the two defences — why RE2's match-time safety does not extend to compilation, and why a length cap and a timeout.
- Reference — the full API, with runnable examples, lives on pkg.go.dev.