Skip to content

Bounded regex compilation

Any regular expression whose pattern comes from outside the binary — a config file, a CLI flag, TUI input, an HTTP payload, a message queue — must be compiled through compile_bounded. Patterns that are build-time literals may use regex::Regex::new directly.

use rtb_app::regex_util::{compile_bounded, RegexCompileError};

compile_bounded

pub fn compile_bounded(pattern: &str) -> Result<regex::Regex, RegexCompileError>;
let re = compile_bounded(user_supplied)?;
if re.is_match(line) { /* … */ }

The length gate is checked before the pattern reaches the regex engine, so an over-long pattern costs a len() comparison rather than a compile attempt.

The limits

Constant Value What it caps
MAX_PATTERN_LEN 1024 (1 KiB) Pattern length in bytes, not characters.
SIZE_LIMIT 1 << 20 (1 MiB) RegexBuilder::size_limit — the compiled program.
DFA_SIZE_LIMIT 8 << 20 (8 MiB) RegexBuilder::dfa_size_limit — the lazy-DFA cache.

All three are pub const and fixed. There is no builder, no options struct and no way to raise or lower them for a particular call site. A tool that genuinely needs different bounds has to build its own RegexBuilder, and owns the consequences.

A pattern of exactly MAX_PATTERN_LEN bytes passes; MAX_PATTERN_LEN + 1 is rejected.

RegexCompileError

#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum RegexCompileError {
    TooLong { len: usize },
    Compile(#[source] regex::Error),
}
Variant Message Diagnostic code Cause
TooLong regex pattern is {len} bytes; limit is 1024 bytes rtb_app::regex::too_long The pattern exceeded MAX_PATTERN_LEN.
Compile regex failed to compile within memory bounds rtb_app::regex::compile Invalid syntax, or the compiled program would exceed SIZE_LIMIT / DFA_SIZE_LIMIT.

Compile carries the underlying regex::Error as its source, so the specific syntax complaint is available on the error chain — the top-level message does not include it. miette attaches the help text "simplify the pattern or reduce repetition counts" to this variant.

Note that a syntax error and a memory-bound breach are the same variant. If your tool needs to tell a user "your pattern is invalid" apart from "your pattern is too expensive", inspect the wrapped regex::Error — the enum discriminant will not tell you.

Neither variant panics and neither allocates unboundedly: compile_bounded(r"(?:a{10000}){10000}") returns Err(Compile(_)).

Why there is no match timeout

Rust's regex crate is a Thompson NFA with linear-time matching, so there is no catastrophic-backtracking class to defend against. That is the difference from the Go side of the toolkit, where regexutil also has to bound match time. Here the only remaining denial-of-service vector is compile-time memory, and that is what these three limits bound.