Part (1) of #1591 landed in ce603e68e: InsnCountHook now checks
alarm_clock. It cannot cover this case. lua_sethook(LUA_MASKCOUNT) counts
VM instructions and does not fire inside a C function, and a pathological
pattern spends all of its time inside one call to str_find_aux.
lstrlib.c's own MAXCCALLS guard bounds recursion DEPTH, which is what
stops a C stack overflow. It does not bound running time: 'a-a-a-...-b'
against a string of 'a's backtracks exponentially in BREADTH, so depth
stays under 200 while the number of match() calls goes to 2^n.
Measured on macOS arm64, max_cmdsecs 1, each probe on its own fresh server
behind a trivial control that had to answer first:
before nothing in 45s, server pinned at 100% CPU
after #-1 CPU LIMITED at t=1.00s, CPU back to 2.6%
Adds a step counter to MatchState alongside matchdepth, checked in the
same l_unlikely branch in match(). Every 65536 steps it calls
lua_match_interrupt if installed; the module installs it around the same
pcall that arms the count hook, and returns non-zero once alarm_clock has
fired. That routes through the existing m_bCpuLimited path, so the caller
sees "#-1 CPU LIMITED" -- the same answer the AST evaluator and the JIT
give. One budget, one message, all four routes.
The counter resets in prepstate but deliberately NOT in reprepstate: a
scan that retries from every position in a long subject is slow in
aggregate even when each attempt is cheap, and the budget bounds the whole
call.
mux/lua54 is the only vendored file touched, and the pointer defaults to
NULL, so that tree still builds standalone as stock Lua 5.4.
Ordinary patterns are unaffected -- find, match, gsub, gmatch, anchors, a
20000-character scan and a self-terminating backtracker all return in
0.00s with the budget active. An ordinary pattern never reaches 65536
steps, so the common path costs one increment and one predictable branch.
make test green: 1561/1561 on both smoke routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>