]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Fix] regexp: stop matchn from looping on empty matches
authorVsevolod Stakhov <vsevolod@rspamd.com>
Mon, 27 Jul 2026 15:07:48 +0000 (16:07 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Mon, 27 Jul 2026 15:07:48 +0000 (16:07 +0100)
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.

src/lua/lua_regexp.c
test/lua/unit/regxep.lua

index bff19714dd7bfbc42b8380ec5f865ba1c3b86655..233528a47ed0a4b4b49a4989844dbdc38448402c 100644 (file)
@@ -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;
+                               }
                        }
                }
 
index cfd50d44c826c2a12a1bdb47e58e11bba6c42fe7..afbb81d15b641251bb22ae5b05206d5273501fe0 100644 (file)
@@ -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