return false;
}
- auto flags = rspamd_symcache_get_symbol_flags(cfg->cache, symbol_name);
+ /*
+ * Only postfilter-stage symbols are unavailable during the first composites
+ * pass (filters and classifiers already ran). get_symbol_stage() resolves
+ * virtuals to their parent, so e.g. NEURAL_SPAM reports its NEURAL_CHECK
+ * postfilter stage. Do NOT key off SYMBOL_TYPE_NOSTAT: it is set on nearly
+ * every virtual/callback symbol and wrongly deferred most composites.
+ */
+ auto stage = rspamd_symcache_get_symbol_stage(cfg->cache, symbol_name);
- /* Postfilters and classifiers/statistics symbols require second pass */
- return (flags & (SYMBOL_TYPE_POSTFILTER | SYMBOL_TYPE_CLASSIFIER | SYMBOL_TYPE_NOSTAT)) != 0;
+ return stage == SYMBOL_TYPE_POSTFILTER;
}
/* Callback data for walking expression atoms to find symbol dependencies */
unsigned int rspamd_symcache_get_symbol_flags(struct rspamd_symcache *cache,
const char *symbol);
+/**
+ * Returns the processing stage of a symbol as a single SYMBOL_TYPE_* bit
+ * (SYMBOL_TYPE_NORMAL for a plain filter). Virtual symbols are resolved to
+ * their parent so the returned stage reflects when the symbol is actually
+ * produced. Returns 0 if the symbol is unknown or its parent cannot be
+ * resolved. Used by the composites two-phase analysis to detect dependencies
+ * on symbols computed after the first composites pass (postfilters).
+ */
+unsigned int rspamd_symcache_get_symbol_stage(struct rspamd_symcache *cache,
+ const char *symbol);
+
void rspamd_symcache_get_symbol_details(struct rspamd_symcache *cache,
const char *symbol,
ucl_object_t *this_sym_ucl);
return 0;
}
+unsigned int rspamd_symcache_get_symbol_stage(struct rspamd_symcache *cache,
+ const char *symbol)
+{
+ auto *real_cache = C_API_SYMCACHE(cache);
+
+ /* Resolve virtual symbols to their parent so the stage reflects where the
+ * symbol is actually produced (e.g. NEURAL_SPAM -> NEURAL_CHECK postfilter). */
+ auto *sym = real_cache->get_item_by_name(symbol, true);
+
+ if (sym == nullptr) {
+ return 0;
+ }
+
+ switch (sym->get_type()) {
+ case rspamd::symcache::symcache_item_type::CONNFILTER:
+ return SYMBOL_TYPE_CONNFILTER;
+ case rspamd::symcache::symcache_item_type::PREFILTER:
+ return SYMBOL_TYPE_PREFILTER;
+ case rspamd::symcache::symcache_item_type::FILTER:
+ return SYMBOL_TYPE_NORMAL;
+ case rspamd::symcache::symcache_item_type::POSTFILTER:
+ return SYMBOL_TYPE_POSTFILTER;
+ case rspamd::symcache::symcache_item_type::IDEMPOTENT:
+ return SYMBOL_TYPE_IDEMPOTENT;
+ case rspamd::symcache::symcache_item_type::CLASSIFIER:
+ return SYMBOL_TYPE_CLASSIFIER;
+ case rspamd::symcache::symcache_item_type::COMPOSITE:
+ return SYMBOL_TYPE_COMPOSITE;
+ case rspamd::symcache::symcache_item_type::VIRTUAL:
+ /* Unresolved virtual (parent missing); treat stage as unknown */
+ return SYMBOL_TYPE_VIRTUAL;
+ }
+
+ return 0;
+}
+
const struct rspamd_symcache_item_stat *
rspamd_symcache_item_stat(struct rspamd_symcache_item *item)
{
${MESSAGE} ${RSPAMD_TESTDIR}/messages/spam_message.eml
${RSPAMD_LUA_SCRIPT} ${RSPAMD_TESTDIR}/lua/composites_postfilter.lua
${SETTINGS_COMPOSITES_POSTFILTER} symbols_enabled [TEST_FILTER_SYM, TEST_POSTFILTER_COMPOSITE, TEST_POSTFILTER_SYM]
+${SETTINGS_COMPOSITES_FIRSTPASS} symbols_enabled [TEST_NOSTAT_FILTER_SYM, TEST_FIRSTPASS_COMPOSITE, TEST_OBSERVE_FIRSTPASS_COMPOSITE, TEST_COMPOSITE_SEEN_IN_POSTFILTER]
*** Test Cases ***
Composite With Postfilter And Filter
Expect Symbol With Score TEST_POSTFILTER_COMPOSITE 10.0
Do Not Expect Symbol TEST_FILTER_SYM
Do Not Expect Symbol TEST_POSTFILTER_SYM
+
+Composite Of Filter Symbol Is Visible In Postfilter
+ [Documentation] Composite depending only on a filter-stage (NOSTAT) symbol must be
+ ... evaluated in the first pass and thus visible to postfilters
+ Scan File ${MESSAGE}
+ ... Settings=${SETTINGS_COMPOSITES_FIRSTPASS}
+ Expect Symbol TEST_FIRSTPASS_COMPOSITE
+ Expect Symbol TEST_COMPOSITE_SEEN_IN_POSTFILTER
expression = "TEST_FILTER_SYM & TEST_POSTFILTER_SYM";
score = 10.0;
}
+ # Depends only on a filter-stage symbol (carrying NOSTAT) -> must be
+ # evaluated in the first composites pass, before post-filters run.
+ TEST_FIRSTPASS_COMPOSITE {
+ expression = "TEST_NOSTAT_FILTER_SYM";
+ score = 5.0;
+ }
}
worker "controller" {
-- This should match when both symbols are present
-- BUG: Currently fails because composite is evaluated before postfilter runs
-- and is not re-evaluated in the second pass
+
+-- Regression for the inverse of #5674: a composite that depends ONLY on
+-- filter-stage symbols must be evaluated in the FIRST composites pass so that
+-- it (and its groups) are visible from postfilters via task:get_symbols() /
+-- task:get_groups(). NOSTAT is set on essentially every virtual/callback rule
+-- (regexp, multimap, rbl, ...); a filter-stage symbol carrying NOSTAT must not
+-- cause the composite to be wrongly deferred to the second (post-filters) pass.
+rspamd_config:register_symbol({
+ type = 'normal',
+ flags = 'nostat',
+ name = 'TEST_NOSTAT_FILTER_SYM',
+ callback = function(task)
+ task:insert_result('TEST_NOSTAT_FILTER_SYM', 1.0)
+ end
+})
+rspamd_config:set_metric_symbol({
+ name = 'TEST_NOSTAT_FILTER_SYM',
+ score = 1.0
+})
+
+-- Postfilter that observes whether the first-pass composite has already fired.
+-- It only inserts its marker if TEST_FIRSTPASS_COMPOSITE is already present,
+-- which can only happen if the composite was evaluated during the first pass.
+rspamd_config:register_symbol({
+ type = 'postfilter',
+ name = 'TEST_OBSERVE_FIRSTPASS_COMPOSITE',
+ callback = function(task)
+ if task:has_symbol('TEST_FIRSTPASS_COMPOSITE') then
+ task:insert_result('TEST_COMPOSITE_SEEN_IN_POSTFILTER', 1.0)
+ end
+ end
+})
+rspamd_config:set_metric_symbol({
+ name = 'TEST_COMPOSITE_SEEN_IN_POSTFILTER',
+ score = 1.0
+})
+
+-- Composite TEST_FIRSTPASS_COMPOSITE = TEST_NOSTAT_FILTER_SYM is defined in
+-- merged-local.conf