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.
  • What regexutil does not do — the bounds it deliberately does not apply: matching, memory, concurrency, configurability.
  • Reference — the exported surface, the exact limits and their edge cases, and the errors. Godoc lives on pkg.go.dev.

Further reading

The blog carries a curated route through this subject: Building a command-line tool in Go collects everything written about it, ordered so you can start at the beginning rather than newest-first.

Ask phpbotscout

phpbotscout

He answers questions about the projects over on the Discord, citing the docs where they already cover it, and offering to raise an issue where they don't. Bring a bug, an idea, or a questionable engineering decision.

Join the Discord