Skip to content

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.

import "gitlab.com/phpboyscout/go/regexutil"

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:

  1. A byte-length capMaxPatternLength (1024). Oversize patterns are rejected before any compile work begins. Legitimate filename globs and search queries are short; 1 KiB is generous.
  2. A wall-clock compile timeoutDefaultCompileTimeout (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. A depfootprint_test.go guard fails the build if anything else enters the graph.
  • Typed sentinel errors. ErrPatternTooLong, ErrPatternCompileTimeout, and ErrPatternInvalid let callers branch precisely with errors.Is.
  • Two entry points. CompileBounded for a call site that already carries a context.Context; CompileBoundedTimeout for one that does not.

Install

go get gitlab.com/phpboyscout/go/regexutil

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:

var branchRE = regexp.MustCompile(`^feature/.*$`) // build-time literal: fine

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.