From: Vsevolod Stakhov Date: Mon, 27 Jul 2026 15:35:23 +0000 (+0100) Subject: [Fix] re_cache: propagate the regexp data limit to named scopes X-Git-Tag: 4.1.4~7 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5c6644a672fb50958ac685b1fcc8fcc033b3e106;p=thirdparty%2Frspamd.git [Fix] re_cache: propagate the regexp data limit to named scopes `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. --- diff --git a/src/libserver/re_cache.c b/src/libserver/re_cache.c index 9c8d1a0e88..4f4cc4abff 100644 --- a/src/libserver/re_cache.c +++ b/src/libserver/re_cache.c @@ -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) { diff --git a/src/libserver/re_cache.h b/src/libserver/re_cache.h index 9e0eb6b520..12c4fb0c2d 100644 --- a/src/libserver/re_cache.h +++ b/src/libserver/re_cache.h @@ -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) */ diff --git a/src/plugins/regexp.c b/src/plugins/regexp.c index df704b8ed7..66467157ad 100644 --- a/src/plugins/regexp.c +++ b/src/plugins/regexp.c @@ -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) { diff --git a/test/rspamd_cxx_unit.cxx b/test/rspamd_cxx_unit.cxx index 4867c90b86..6f3346057a 100644 --- a/test/rspamd_cxx_unit.cxx +++ b/test/rspamd_cxx_unit.cxx @@ -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 index 0000000000..8f6b6854e0 --- /dev/null +++ b/test/rspamd_cxx_unit_re_cache.hxx @@ -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