From: Vsevolod Stakhov Date: Mon, 27 Jul 2026 15:07:48 +0000 (+0100) Subject: [Fix] regexp: stop matchn from looping on empty matches X-Git-Tag: 4.1.4~8 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=23e2ec9601c686c2c0cb14e9b4e771a8ab294586;p=thirdparty%2Frspamd.git [Fix] regexp: stop matchn from looping on empty matches rspamd_regexp_search() resumes an incremental search from *end, so a zero-width match leaves the cursor where it was. re:search(), re:split() and the re_cache loop all break out on `start >= end` for exactly this reason, but re:matchn() had no such guard: with a negative max_matches (unlimited) an empty-matchable pattern spins forever and hangs the worker. This is reachable from configuration: sa_regexp_match() in the spamassassin plugin passes -1 for any `multiple` rule that does not set `maxhits`, so an SA rule whose pattern can match an empty string wedges the process. Add the same `start >= end` break, after the max_matches check to match the ordering used in re_cache.c. --- diff --git a/src/lua/lua_regexp.c b/src/lua/lua_regexp.c index bff19714dd..233528a47e 100644 --- a/src/lua/lua_regexp.c +++ b/src/lua/lua_regexp.c @@ -679,6 +679,11 @@ lua_regexp_matchn(lua_State *L) if (max_matches >= 0 && matches >= max_matches) { break; } + + if (start >= end) { + /* We found all matches, so no more hits are possible (protect from empty patterns) */ + break; + } } } diff --git a/test/lua/unit/regxep.lua b/test/lua/unit/regxep.lua index cfd50d44c8..afbb81d15b 100644 --- a/test/lua/unit/regxep.lua +++ b/test/lua/unit/regxep.lua @@ -67,6 +67,28 @@ context("Regexp unit tests", function() end end) + test("Regexp matchn", function() + local cases = { + -- pattern, input, limit, expected + {'/a/', 'aaaa', -1, 4}, -- unlimited + {'/a/', 'aaaa', 2, 2}, -- limited + {'/a/', 'aaaa', 0, 1}, -- zero limit is a single match + -- Patterns that can match an empty string must not loop forever + -- when the limit is negative (i.e. unlimited) + {'/x*/', 'abc', -1, 1}, + {'/(?=b)/', 'abcb', -1, 1}, + {'/\\b/', 'ab cd', -1, 1}, + } + + for _,c in ipairs(cases) do + local r = re.create_cached(c[1]) + assert_not_nil(r, "cannot parse " .. c[1]) + + assert_equal(r:matchn(c[2], c[3]), c[4], + string.format("'%s' matchn '%s' with limit %d", c[1], c[2], c[3])) + end + end) + test("Regexp split", function() local cases = { {'\\s', 'one', {'one'}}, -- one arg