Contents
The pattern (?:){50000000} is 14 bytes. I matched it against an empty string with github.com/dlclark/regexp2 v1.12.0 and the process allocated 2,560 MB over 11.2 seconds before returning true. The same call on github.com/dlclark/regexp2/v2 v2.3.0 returned in 73 microseconds with no measurable allocation. That gap is CVE-2026-77299, and the part worth your attention is the module path: the fix lives under /v2.
The mechanism
regexp2 is a backtracking engine modelled on .NET’s, which is why people reach for it when Go’s RE2-based regexp package lacks lookarounds or backreferences. Per the advisory, a counted quantifier on an empty subexpression runs the empty loop body m-1 times and pushes values onto the backtracking stack each pass, with no limit. The parser accepts counts up to math.MaxInt32. One public summary of the CVE (strix.ai) says each iteration pushes two integers.
The attacker needs control of the pattern, not the input. That matters for any service that lets users supply a regex: search filters, log-routing rules, validation rules stored per tenant, WAF-style rule editors. Trusted input does not help.
What the fix does
The fix commit makes two separate changes.
First, reduceRep in syntax/tree.go now drops a repeat whose only child is a literal empty node, since repeating nothing has no observable effect. That is what removes (?:){50000000} entirely and explains my 73 microseconds.
Second, and more general, the runner’s backtrack stack is capped. growTrack() doubles the slice as before but clamps to MaxBacktrackingStackSize, and ensureStorage(), goTo() and backtrack() now return an error instead of growing past it. The default is 100,000 integer slots, a negative value disables the cap, and the failure surfaces as ErrBacktrackingStackLimit. The option is OptionMaxBacktrackingStackSize(n).
The second change is needed because the first only special-cases the literal empty case. A pattern like (?:^){50000000} is zero-width but not an empty node, so it still pushes frames. On v2.3.0 I got regexp2: maximum backtracking stack size exceeded after 1.8 ms and about 1 MB of allocation.
The default is a behavior change
The cap also rejects patterns that matched before. (?:^){60000} returned true on v2.2.2 in my run and returns the limit error on v2.3.0. The commit’s own tests check exactly this: the default rejects (?:^){60000}, and an override of 200,000 or -1 lets it match. Real patterns rarely need tens of thousands of zero-width iterations, but a pattern with a large count over something that backtracks heavily can also hit 100,000 slots. If you upgrade, treat ErrBacktrackingStackLimit like you already treat MatchTimeout errors: a result of “did not match in budget”, not a crash.
Why the timeout is not the control
The advisory says MatchTimeout reduces but does not eliminate the exposure, because memory is allocated faster than the timer fires. My measurement is more mixed. With a 100 ms MatchTimeout on v1.12.0, a 50-million count returned a timeout error after 519 ms having allocated 160 MB, so the timer did fire, at roughly five times its budget. Without a timeout, the same pattern allocated 2.5 GB. On a container with a 512 MB limit, the difference between those two numbers is the difference between an error and an OOM kill, and timer overshoot depends on how busy the scheduler is. I only ran this on one machine, so read it as “the timeout helps but gives you no bound on memory”, which is the weaker claim and the one I can support.
The v1 problem
Go treats github.com/dlclark/regexp2 and github.com/dlclark/regexp2/v2 as different modules. Querying the module proxy with go list -m -versions, the original path ends at v1.12.0 (tagged 2026-04-18) and v2.3.0 was tagged 2026-07-10. The advisory says the reporter’s proof of concept names v1.12.0 as the latest affected release and lists 2.3.0 as the patched version. I found no v1 release containing the fix. I cannot tell from here whether a v1 backport is planned.
So “upgrade to 2.3.0” is not a version bump for most projects. It is an import path change, and the API is not identical: calling regexp2.MustCompile(pattern, 0) against v2 failed to compile in my test, because options are no longer plain integers. When I added the v2 module, my Go 1.24.7 toolchain also tried to download Go 1.25.0, so the module appears to require a newer toolchain. I did not read the v2 go.mod directly, so verify that on your side.
Dates do not line up cleanly
The GitHub advisory page shows publication on July 11, 2026, a day after the v2.3.0 tag. The aggregator I found it through lists the CVE record as published November 6. I did not reconcile the two, so I cannot say which date reflects NVD. If your scanner only started flagging this week, a late CVE assignment for a fix that has been public since July is a plausible explanation, though that is my inference.
Checking your own services
go list -m all | grep dlclark/regexp2
go mod why -m github.com/dlclark/regexp2
A line ending in regexp2 v1.x means you are on the unfixed path. go mod why shows whether it is you or a dependency importing it, which decides who has to move. If a dependency owns the import and compiles user-supplied patterns, you cannot fix it with a version bump, so cap pattern length and repeat counts before compilation. A reject rule such as “no counted quantifier above 1,000” is cheap and works on either module line.
Sources
- GHSA-wq9v-j77v-qr26 / CVE-2026-77299. Primary. Affected and patched versions, severity, workarounds.
- Fix commit 6e446cb. Primary. The
tree.goandrunner.gohunks I describe. - regexp2 v2.3.0 release. Primary. It has no itemized notes.
- strix.ai CVE-2026-77299. Secondary. Source for the two-integers-per-iteration detail and the November 6 date.
- Local measurements: Go 1.24.7, linux/amd64,
runtime.MemStats.TotalAllocbefore and after a singleMatchString("")call, run during this session. Versions and tag times from the Go module proxy viago list -m.