Skip to content

Compile an untrusted pattern

This guide covers the practical choices you make when compiling a user- or config-supplied regex with regexutil: which of the two entry points to call, how to think about the timeout, and how to branch on the failure modes.

If you are new to the package, work through Getting started first; this page assumes you know what the sentinels mean and focuses on the decisions.

Choose an entry point

regexutil exposes exactly two functions. Pick by whether your call site already carries a context.Context.

CompileBounded — you have a context

Use this from anywhere that already threads a context.Context (a request handler, a command's RunE, any function whose caller passes one down). It applies the MaxPatternLength cap and a DefaultCompileTimeout wall-clock bound, while still honouring any earlier deadline or cancellation on the context you pass.

func compileFilter(ctx context.Context, pattern string) (*regexp.Regexp, error) {
    return regexutil.CompileBounded(ctx, pattern)
}

If the context you pass is cancelled — or its own deadline fires — before the compile finishes, the call returns on the ErrPatternCompileTimeout path just as it would on the package's internal timeout, so a shutdown or a per-request deadline cleanly aborts the compile.

CompileBoundedTimeout — you have no context

Use this from a call site that does not naturally carry a context — a Bubble Tea TUI update loop, a small helper, an init-style path. It builds a context from context.Background() with the timeout you supply and delegates to CompileBounded:

re, err := regexutil.CompileBoundedTimeout(pattern, 50*time.Millisecond)

Choosing a timeout

You cannot widen the package's wall-clock bound. CompileBoundedTimeout applies the caller's timeout on top of CompileBounded, which itself layers DefaultCompileTimeout (100 ms) onto the context — so the effective bound is the minimum of your value and 100 ms.

  • Passing a value larger than 100 ms has no effect: DefaultCompileTimeout still wins. You cannot accidentally give a pathological pattern more room.
  • Passing a value smaller than 100 ms tightens the bound — useful for a latency-sensitive interactive loop where even 100 ms of freeze is too much.
// Tighten to 25 ms for a keystroke-driven TUI filter; the effective bound is
// min(25ms, 100ms) = 25ms.
re, err := regexutil.CompileBoundedTimeout(pattern, 25*time.Millisecond)

For most non-interactive call sites, prefer CompileBounded and let the 100 ms default stand — it is already two orders of magnitude above a normal compile.

Handle the failure modes

Both functions return an error that wraps one of three sentinels. Branch with errors.Is — never ==, because the returned value is a wrapped error carrying a user-facing hint, not the bare sentinel.

import "github.com/cockroachdb/errors"

re, err := regexutil.CompileBounded(ctx, pattern)
switch {
case errors.Is(err, regexutil.ErrPatternTooLong):
    // Rejected before compiling: pattern exceeded MaxPatternLength (1024 bytes).
    return fmt.Errorf("pattern too long (max %d bytes)", regexutil.MaxPatternLength)
case errors.Is(err, regexutil.ErrPatternCompileTimeout):
    // Compilation exceeded the wall-clock bound — treat as pathological.
    return errors.New("pattern too complex to compile safely; simplify it")
case errors.Is(err, regexutil.ErrPatternInvalid):
    // regexp.Compile rejected the syntax.
    return errors.Wrap(err, "invalid regex")
case err != nil:
    // No other kind is expected, but do not swallow surprises.
    return err
}

// err == nil: re is safe to use.
use(re)

A few points worth internalising:

  • ErrPatternTooLong costs nothing. The length check happens before any compile work, so an oversize pattern never allocates an automaton.
  • ErrPatternCompileTimeout is the pathological signal. A short pattern that still times out is exactly the case RE2's linear matching does not protect you from — see the threat model.
  • Rejection errors carry a hint, not your pattern. The too-long and timeout errors are annotated with the length and a remediation hint but never echo the offending pattern back, so an attacker cannot use them to amplify your logs. If you want to surface the pattern to an operator, do it from a trusted-source context where log amplification is not a concern.