]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Fix] re_cache: propagate the regexp data limit to named scopes
authorVsevolod Stakhov <vsevolod@rspamd.com>
Mon, 27 Jul 2026 15:35:23 +0000 (16:35 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Mon, 27 Jul 2026 15:35:23 +0000 (16:35 +0100)
`regexp.max_size` was applied to the list head only, so regexps registered
in a named scope (e.g. multimap `regexp_rules`) were matched against
unbounded input whilst the default scope was capped at 1 MiB.

Named scope caches are now created inheriting the default scope limit, and
setting the limit updates every scope registered so far, which covers both
orderings: Lua scopes registered before the regexp module is configured and
plugins that register their scopes during filters init.

src/libserver/re_cache.c
src/libserver/re_cache.h
src/plugins/regexp.c
test/rspamd_cxx_unit.cxx
test/rspamd_cxx_unit_re_cache.hxx [new file with mode: 0644]

index 9c8d1a0e88ab627bd874dd6786ee4ec0fc14f6d9..4f4cc4abff4f188ff0478e6ccbfde6002c4ba9f0 100644 (file)
@@ -239,6 +239,12 @@ rspamd_re_cache_add_to_scope_list(struct rspamd_re_cache **cache_head, const cha
 
        /* Add to linked list */
        if (*cache_head) {
+               /*
+                * Inherit the max data limit from the default scope (the list head),
+                * otherwise scoped regexps would be matched against unbounded input
+                * whilst the default scope honours `regexp.max_size`
+                */
+               new_cache->max_re_data = (*cache_head)->max_re_data;
                DL_APPEND(*cache_head, new_cache);
        }
        else {
@@ -2059,6 +2065,26 @@ unsigned int rspamd_re_cache_set_limit_scoped(struct rspamd_re_cache *cache_head
        return old;
 }
 
+unsigned int rspamd_re_cache_set_limit_all_scopes(struct rspamd_re_cache *cache_head, unsigned int limit)
+{
+       struct rspamd_re_cache *cur;
+       unsigned int old = 0;
+
+       if (!cache_head) {
+               return old;
+       }
+
+       /* The list head is the default scope, report its previous limit */
+       old = cache_head->max_re_data;
+
+       DL_FOREACH(cache_head, cur)
+       {
+               cur->max_re_data = limit;
+       }
+
+       return old;
+}
+
 const char *
 rspamd_re_cache_type_to_string(enum rspamd_re_type type)
 {
index 9e0eb6b52020eacf01dfbbf2c6b8ed4f673d9d7b..12c4fb0c2dbda1c1d7b25470646c96b074b09efe 100644 (file)
@@ -218,6 +218,13 @@ unsigned int rspamd_re_cache_set_limit(struct rspamd_re_cache *cache, unsigned i
  */
 unsigned int rspamd_re_cache_set_limit_scoped(struct rspamd_re_cache *cache_head, const char *scope, unsigned int limit);
 
+/**
+ * Set limit for all regular expressions in all scopes of the cache list, returns
+ * the previous limit of the default scope. Scopes created afterwards inherit this
+ * limit from the default scope.
+ */
+unsigned int rspamd_re_cache_set_limit_all_scopes(struct rspamd_re_cache *cache_head, unsigned int limit);
+
 /**
  * Convert re type to a human readable string (constant one)
  */
index df704b8ed760fa33b2b60c6c226d4bf66c85bfc8..66467157ad5cf69e9bb51f50e095b6168c23121c 100644 (file)
@@ -199,7 +199,10 @@ int regexp_module_config(struct rspamd_config *cfg, bool validate)
                if (g_ascii_strncasecmp(ucl_object_key(value), "max_size",
                                                                sizeof("max_size") - 1) == 0) {
                        regexp_module_ctx->max_size = ucl_obj_toint(value);
-                       rspamd_re_cache_set_limit(cfg->re_cache, regexp_module_ctx->max_size);
+                       /* Apply to all scopes registered so far, the ones added later
+                        * inherit this limit from the default scope */
+                       rspamd_re_cache_set_limit_all_scopes(cfg->re_cache,
+                                                                                                regexp_module_ctx->max_size);
                }
                else if (g_ascii_strncasecmp(ucl_object_key(value), "max_threads",
                                                                         sizeof("max_threads") - 1) == 0) {
index 4867c90b86f10423e13ff7c073d90076fedda20c..6f3346057a6dd8fe2030920d2686a363938683c7 100644 (file)
@@ -46,6 +46,7 @@
 #include "rspamd_cxx_unit_text_stats.hxx"
 #include "rspamd_cxx_unit_multipattern.hxx"
 #include "rspamd_cxx_unit_regexp.hxx"
+#include "rspamd_cxx_unit_re_cache.hxx"
 #include "rspamd_cxx_unit_compression.hxx"
 #include "rspamd_cxx_unit_tokenizer.hxx"
 #include "rspamd_cxx_unit_http_timeout.hxx"
diff --git a/test/rspamd_cxx_unit_re_cache.hxx b/test/rspamd_cxx_unit_re_cache.hxx
new file mode 100644 (file)
index 0000000..8f6b685
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2026 Vsevolod Stakhov
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef RSPAMD_RSPAMD_CXX_UNIT_RE_CACHE_HXX
+#define RSPAMD_RSPAMD_CXX_UNIT_RE_CACHE_HXX
+
+#define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
+#include "doctest/doctest.h"
+
+#include "libserver/re_cache.h"
+#include "libutil/regexp.h"
+
+/*
+ * `regexp.max_size` bounds the amount of data any regexp in the cache is
+ * matched against. Named scopes live in their own cache structures, so the
+ * limit must be propagated to the scopes that already exist and inherited by
+ * the ones registered later, otherwise scoped regexps would scan unbounded
+ * input. The limit is only observable through the previous value returned by
+ * the setters, which is what these tests assert on.
+ */
+TEST_SUITE("re cache limits")
+{
+       /* Registers a regexp in `scope`, creating the scope cache as a side effect */
+       static void re_cache_add_scope(struct rspamd_re_cache * *head, const char *scope,
+                                                                  const char *pattern)
+       {
+               auto *re = rspamd_regexp_new(pattern, nullptr, nullptr);
+               REQUIRE(re != nullptr);
+               REQUIRE(rspamd_re_cache_add_scoped(head, scope, re, RSPAMD_RE_BODY,
+                                                                                  nullptr, 0, -1) != nullptr);
+               rspamd_regexp_unref(re);
+       }
+
+       TEST_CASE("scope created after the limit inherits it")
+       {
+               struct rspamd_re_cache *head = rspamd_re_cache_new();
+
+               rspamd_re_cache_set_limit_all_scopes(head, 1024);
+               re_cache_add_scope(&head, "late_scope", "/late/");
+
+               /* Setting the limit anew reports the inherited one */
+               CHECK(rspamd_re_cache_set_limit_scoped(head, "late_scope", 0) == 1024);
+
+               rspamd_re_cache_unref_scoped(head);
+       }
+
+       TEST_CASE("scope created before the limit gets it too")
+       {
+               struct rspamd_re_cache *head = rspamd_re_cache_new();
+
+               re_cache_add_scope(&head, "early_scope", "/early/");
+               CHECK(rspamd_re_cache_set_limit_all_scopes(head, 2048) == 0);
+
+               CHECK(rspamd_re_cache_set_limit_scoped(head, "early_scope", 0) == 2048);
+               /* The default scope (the list head) is set as well */
+               CHECK(rspamd_re_cache_set_limit(head, 0) == 2048);
+
+               rspamd_re_cache_unref_scoped(head);
+       }
+
+       TEST_CASE("all scopes are covered")
+       {
+               struct rspamd_re_cache *head = rspamd_re_cache_new();
+
+               re_cache_add_scope(&head, "scope1", "/one/");
+               re_cache_add_scope(&head, "scope2", "/two/");
+               re_cache_add_scope(&head, "scope3", "/three/");
+
+               rspamd_re_cache_set_limit_all_scopes(head, 4096);
+
+               CHECK(rspamd_re_cache_set_limit_scoped(head, "scope1", 0) == 4096);
+               CHECK(rspamd_re_cache_set_limit_scoped(head, "scope2", 0) == 4096);
+               CHECK(rspamd_re_cache_set_limit_scoped(head, "scope3", 0) == 4096);
+
+               rspamd_re_cache_unref_scoped(head);
+       }
+}
+
+#endif