]> git.ipfire.org Git - thirdparty/rspamd.git/log
thirdparty/rspamd.git
2 days ago[Test] Add unit tests for the MIME address list parser 6143/head
Vsevolod Stakhov [Thu, 16 Jul 2026 11:28:26 +0000 (12:28 +0100)] 
[Test] Add unit tests for the MIME address list parser

Covers single addresses, display names (plain, quoted, UTF-8),
comments, comma and semicolon lists, separators inside quoted strings
and unquoted display names, group constructs and no-address inputs.

Issue: #6139

2 days ago[Fix] Split MIME address lists on semicolons as well as commas
Vsevolod Stakhov [Thu, 16 Jul 2026 11:28:23 +0000 (12:28 +0100)] 
[Fix] Split MIME address lists on semicolons as well as commas

A semicolon is not an RFC 5322 address-list separator (it only
terminates a group construct), but Outlook-style ;-separated recipient
lists are pervasive in real mail and most MTAs (e.g. Postfix) split
them. Previously such a list was parsed as a single malformed address
whose local part swallowed the semicolon and the next address, breaking
recipient counts and producing false invalid-address signals.

Treat ';' exactly like ',' in the list parser. This mirrors the
existing comma behaviour in every edge case: semicolons inside quoted
strings are preserved, an unquoted semicolon in a display name drops
the preceding phrase just like a comma does, an empty group
(undisclosed-recipients:;) now yields no addresses like the comma
variant, and the trailing ';' of a group construct no longer leaks
into the last member's domain.

Issue: #6139

7 days ago[Fix] Lua 5.5 compatibility: do not assign to for-loop variables
Vsevolod Stakhov [Sat, 11 Jul 2026 09:30:34 +0000 (10:30 +0100)] 
[Fix] Lua 5.5 compatibility: do not assign to for-loop variables

Lua 5.5 makes for-loop control variables read-only, so assigning to
them is now a compile-time error. Rebind the value to a local (or pass
it as a closure parameter) in all remaining places; found by
parse-checking the whole tree with luac 5.5.

7 days ago[Test] Fix race in history reset e2e test
Vsevolod Stakhov [Sat, 11 Jul 2026 09:19:38 +0000 (10:19 +0100)] 
[Test] Fix race in history reset e2e test

Register waitForResponse listeners before clicking the reset button:
on localhost the historyreset response arrives before a listener
registered after the click can see it, which made the legacy
(Playwright 1.45.3) CI leg fail deterministically.

7 days agoMerge pull request #6130 from AmateurECE/bugfix/lua-lupa-5.5-compatibility-word-loop...
Vsevolod Stakhov [Sat, 11 Jul 2026 08:39:22 +0000 (09:39 +0100)] 
Merge pull request #6130 from AmateurECE/bugfix/lua-lupa-5.5-compatibility-word-loop-variable

fix(lua-lupa): Use local loop variable

7 days agoMerge pull request #6123 from moisseev/test
Vsevolod Stakhov [Sat, 11 Jul 2026 07:39:11 +0000 (08:39 +0100)] 
Merge pull request #6123 from moisseev/test

[Test] Update dev dependencies

7 days agoMerge pull request #6126 from rspamd/vstakhov-composite-conditions
Vsevolod Stakhov [Sat, 11 Jul 2026 07:38:56 +0000 (08:38 +0100)] 
Merge pull request #6126 from rspamd/vstakhov-composite-conditions

[Feature] composites: per-symbol Lua conditions and explicit dependencies

7 days agoMerge pull request #6127 from rspamd/vstakhov-selectors-anchor-parse
Vsevolod Stakhov [Sat, 11 Jul 2026 07:38:43 +0000 (08:38 +0100)] 
Merge pull request #6127 from rspamd/vstakhov-selectors-anchor-parse

[Fix] lua_selectors: reject selectors that parse only as a prefix

7 days agofix(lua-lupa): Use local loop variable 6130/head
Ethan D. Twardy [Fri, 10 Jul 2026 23:03:58 +0000 (18:03 -0500)] 
fix(lua-lupa): Use local loop variable

Lua 5.5 makes `for` expression variables constant. The recent upgrade to
lua 5.5 broke lua-lupa, which previously attempted to modify the loop
variable. This caused an unrecoverable error on startup:

rspamd_lua_require_function: require of lua_util.jinja_template failed: error loading module 'lupa' from file '/usr/share/rspamd/lualib/lupa.lua':\x0A\x09/usr/share/rspamd/lualib/lupa.lua:1858: attempt to assign to const variable 'word'

8 days ago[Test] selectors: cases for partially-parsed selectors 6127/head
Vsevolod Stakhov [Fri, 10 Jul 2026 13:16:22 +0000 (14:16 +0100)] 
[Test] selectors: cases for partially-parsed selectors

Negative cases: a second ':' method, a stray closing paren and junk
after a ';' list element must be rejected. Control cases: valid
method/transform chains, selector lists and trailing whitespace must
keep parsing.

8 days ago[Fix] lua_selectors: reject selectors that parse only as a prefix
Vsevolod Stakhov [Fri, 10 Jul 2026 13:16:11 +0000 (14:16 +0100)] 
[Fix] lua_selectors: reject selectors that parse only as a prefix

The selector grammar was not anchored to the end of input, so lpeg
matched the longest valid prefix and silently dropped the rest of the
string. E.g. 'from("smtp"):domain:lower' (a second ':' cannot parse)
was accepted and evaluated as 'from("smtp"):domain', and any trailing
garbage after a selector or a ';' list element was ignored. All
consumers (multimap, rbl, ratelimit, settings, reputation, the
controller selector check used by the WebUI) reported such selectors
as valid while evaluating only the prefix.

Append an lpeg.Cp() capture after the grammar (plus optional trailing
whitespace) and make parse_selector require the whole input to be
consumed, logging the position and the unparsed tail otherwise.

User-visible change: selectors that previously loaded thanks to the
silent truncation now fail configuration load with an error pointing
at the offending token. Such selectors were never evaluating as
written, so failing loudly is the correct behaviour.

8 days ago[Fix] rspamd_symcache.h: keep C linkage of lua.h for C++ includers 6126/head
Vsevolod Stakhov [Fri, 10 Jul 2026 07:38:03 +0000 (08:38 +0100)] 
[Fix] rspamd_symcache.h: keep C linkage of lua.h for C++ includers

The raw lua.h include sat outside extern "C", so any C++ unit that
pulled rspamd_symcache.h before lua/lua_common.h got C++-mangled
declarations of the Lua C API and failed to link. Including
lua_common.h itself is not an option due to the include cycle via
rspamd.h -> cfg_file.h -> rspamd_symcache.h, so wrap lua.h the same
way lua_common.h does. This also lets composites sources include
lua_common.h in the natural position instead of first.

8 days ago[Test] composites: functional cases for Lua conditions
Vsevolod Stakhov [Fri, 10 Jul 2026 07:32:18 +0000 (08:32 +0100)] 
[Test] composites: functional cases for Lua conditions

Cover: option-inspecting condition, cross-symbol join on a shared
option value, numeric return as atom weight combined with an
expression limit, false condition, and depends_on deferring a
composite whose condition consults a postfilter symbol.

8 days ago[Feature] composites: per-symbol Lua conditions and explicit dependencies
Vsevolod Stakhov [Fri, 10 Jul 2026 07:32:07 +0000 (08:32 +0100)] 
[Feature] composites: per-symbol Lua conditions and explicit dependencies

Allow gating composite atoms with synchronous Lua functions, by analogy
with re_conditions of mime expressions:

  rspamd_config:add_composite('C', {
    expression = 'SYM_A & SYM_B',
    conditions = {
      SYM_A = function(task, symbol) ... end,
    },
    depends_on = { 'SOME_POSTFILTER_SYM' },
  })

A condition is called as f(task, symbol) where symbol is a table in the
task:get_symbol() layout; it may return true/false or a number used as
the atom weight. The condition is ANDed with option filters of the atom
and a failed condition is treated exactly as a missing symbol, so
removal policies are not applied. Since a condition can consult symbols
invisible to the expression, the optional depends_on list feeds the
first/second pass placement as if those symbols were expression atoms.

rspamd_config:add_composite() now also accepts a full definition table,
and rspamd_lua_push_symbol_result() is exported for reuse.

10 days ago[Test] phishing: no FP for same label under different suffix
Vsevolod Stakhov [Wed, 8 Jul 2026 12:59:27 +0000 (13:59 +0100)] 
[Test] phishing: no FP for same label under different suffix

Regression test: brand.co.za displayed over a brand.com href must not
fire PHISHING (uses co.za since the functional test tld file has no
co.uk)

10 days ago[Fix] phishing: don't penalise same label under another TLD
Vsevolod Stakhov [Wed, 8 Jul 2026 12:59:15 +0000 (13:59 +0100)] 
[Fix] phishing: don't penalise same label under another TLD

Three fixes for cross-TLD brand false positives
(e.g. brand.co.uk displayed over a brand.com href):

- Normalise the DMARC_POLICY_ALLOW domain to eSLD before comparing it
  with the link target tld: DMARC reports the raw From domain (possibly
  a subdomain), so the existing exclusion for authenticated mail never
  matched
- Compare registrable labels instead of stripping the last dot
  component: the old code only handled single-label suffixes, so
  brand.co.uk vs brand.com was levenshtein-compared as full strings and
  scored full weight; now the identical label yields weight 0
- Check strict_domains regardless of the computed weight (gated on
  tld ~= ptld, equivalent to the old reachable behaviour), so displaying
  a strictly protected domain over a same-label different-suffix target
  still fires at full weight

10 days ago[Test] lua_cryptobox: unit tests for secretbox nonce padding
Vsevolod Stakhov [Wed, 8 Jul 2026 11:55:20 +0000 (12:55 +0100)] 
[Test] lua_cryptobox: unit tests for secretbox nonce padding

Guard against the regression fixed in #6122: a short nonce must produce
the same ciphertext as the explicitly zero-padded 24-byte nonce, and
short/padded forms must decrypt each other. Both assertions fail on the
unpatched code as they read past the nonce buffer.

10 days ago[Fix] lua_cryptobox: pass padded nonce to secretbox encrypt/decrypt
Jamon Camisso [Wed, 8 Jul 2026 11:54:31 +0000 (07:54 -0400)] 
[Fix] lua_cryptobox: pass padded nonce to secretbox encrypt/decrypt

Both secretbox encrypt and decrypt built a zero-padded 24-byte real_nonce
but passed the original short nonce pointer to libsodium, reading up to
23 bytes out of bounds and producing ciphertexts that depend on adjacent
memory contents.

Fixes #6121

10 days agoMerge pull request #6119 from rspamd/dependabot/pip/contrib/neural-embedding-service...
Vsevolod Stakhov [Wed, 8 Jul 2026 11:38:22 +0000 (12:38 +0100)] 
Merge pull request #6119 from rspamd/dependabot/pip/contrib/neural-embedding-service/transformers-5.3.0

Bump transformers from 5.0.0rc3 to 5.3.0 in /contrib/neural-embedding-service

10 days ago[Minor] maps: move glob pattern anchoring into rspamd_str_regexp_escape
Vsevolod Stakhov [Wed, 8 Jul 2026 09:09:41 +0000 (10:09 +0100)] 
[Minor] maps: move glob pattern anchoring into rspamd_str_regexp_escape

Add RSPAMD_REGEXP_ESCAPE_ANCHOR flag that wraps the escaped pattern
into ^(?:...)$ within the same allocation instead of a second
g_strdup_printf in the glob map insertion path.

10 days ago[Fix] maps: anchor glob map patterns to match the whole subject
Vsevolod Stakhov [Wed, 8 Jul 2026 08:52:53 +0000 (09:52 +0100)] 
[Fix] maps: anchor glob map patterns to match the whole subject

Glob map entries were compiled into unanchored regexps and matched
with substring search semantics, so a `t.co` entry matched
`walmart.com` and `*.bit.ly` matched `foo.bit.ly.evil.com`.
Wrap the translated pattern into `^(?:...)$` at map load time so
glob entries match the subject as a whole: bare names match exactly,
wildcards match only what they say.

This affects all glob maps: multimap glob/glob_multi, url_redirector
redirector_hosts_map, dkim_signing/arc signing_table and key_table,
mx_check exclusions and the rbl glob returncodes matcher. Maps that
relied on the accidental substring behaviour must now use explicit
wildcards.

Fixes #6125

13 days agoMerge pull request #6120 from moisseev/tabulator
Vsevolod Stakhov [Sun, 5 Jul 2026 17:47:12 +0000 (18:47 +0100)] 
Merge pull request #6120 from moisseev/tabulator

WebUI: migrate from FooTable to Tabulator

13 days ago[Fix] WebUI: replace BS3 form-group with mb-3 6120/head
Alexander Moisseev [Sun, 5 Jul 2026 17:08:02 +0000 (20:08 +0300)] 
[Fix] WebUI: replace BS3 form-group with mb-3

Removing footable.standalone.min.css dropped its .form-group
{margin-bottom:15px} rule, collapsing the gap above the buttons on
the selectors tab (and the action-score rows on the configuration
tab). .form-group is a Bootstrap 3 class with no meaning in BS5;
replace it with the mb-3 spacing utility that provides the same gap.

13 days ago[Fix] lua_selectors: don't crash on a missing method call
Vsevolod Stakhov [Sun, 5 Jul 2026 11:43:13 +0000 (12:43 +0100)] 
[Fix] lua_selectors: don't crash on a missing method call

Selectors like `time:digest` (method syntax used where a transform
was meant) aborted the whole selector at scan time with an uncaught
Lua error:

  Cannot run callback: .../lua_selectors/init.lua: attempt to call
  a nil value

The `:` separator always compiles to a method call on the extracted
value; when the value has no such method, the lookup yields nil and
the unguarded call crashed the callback. Guard the lookup (via pcall,
since indexing some types raises) and yield no value instead, logging
an error that hints at the `.name` transform syntax when the name
matches a known transform. Valid method calls (e.g. `:gsub`,
`:lower`) and table field access are unaffected.

2 weeks ago[Test] Update dev dependencies 6123/head
Alexander Moisseev [Fri, 3 Jul 2026 14:59:00 +0000 (17:59 +0300)] 
[Test] Update dev dependencies

- Update ESLint to 10.6.0, stylelint to 17.14.0, and related packages

2 weeks ago[Fix] WebUI: release scroll-preservation listeners
Alexander Moisseev [Thu, 2 Jul 2026 15:03:04 +0000 (18:03 +0300)] 
[Fix] WebUI: release scroll-preservation listeners

installScrollPreservation bound listeners on the mount element and
window without removing the previous set. Each column-options rebuild
(destroy + re-init on the persistent mount element) stacked a new set
that was never released — a listener/memory leak on the history and
scan tables. Mirror bindRowClickToggle's WeakMap guard: key the set by
mount element and tear it down before re-binding.

2 weeks agoMerge pull request #6118 from rspamd/vstakhov-static-embed
Vsevolod Stakhov [Thu, 2 Jul 2026 14:16:38 +0000 (15:16 +0100)] 
Merge pull request #6118 from rspamd/vstakhov-static-embed

[Feature] neural: static_embed provider (WordPiece + static matrix)

2 weeks ago[Minor] WebUI: update Tabulator to 6.5.2
Alexander Moisseev [Thu, 2 Jul 2026 13:08:38 +0000 (16:08 +0300)] 
[Minor] WebUI: update Tabulator to 6.5.2

2 weeks ago[Minor] WebUI: compact header-filter inputs
Alexander Moisseev [Thu, 2 Jul 2026 12:25:34 +0000 (15:25 +0300)] 
[Minor] WebUI: compact header-filter inputs

2 weeks ago[Minor] WebUI: per-column filter on recipients
Alexander Moisseev [Thu, 2 Jul 2026 12:08:03 +0000 (15:08 +0300)] 
[Minor] WebUI: per-column filter on recipients

2 weeks agoBump transformers in /contrib/neural-embedding-service 6119/head
dependabot[bot] [Thu, 2 Jul 2026 11:40:57 +0000 (11:40 +0000)] 
Bump transformers in /contrib/neural-embedding-service

Bumps [transformers](https://github.com/huggingface/transformers) from 5.0.0rc3 to 5.3.0.
- [Release notes](https://github.com/huggingface/transformers/releases)
- [Commits](https://github.com/huggingface/transformers/compare/v5.0.0rc3...v5.3.0)

---
updated-dependencies:
- dependency-name: transformers
  dependency-version: 5.3.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2 weeks ago[Minor] WebUI: add tab-utils.js to ARCHITECTURE.md
Alexander Moisseev [Thu, 2 Jul 2026 11:27:22 +0000 (14:27 +0300)] 
[Minor] WebUI: add tab-utils.js to ARCHITECTURE.md

2 weeks ago[Rework] WebUI: remove FooTable
Alexander Moisseev [Thu, 2 Jul 2026 10:35:46 +0000 (13:35 +0300)] 
[Rework] WebUI: remove FooTable

FooTable was superseded by Tabulator across all tables (Phases 0-3).
Remove the vendored library/CSS, the footable-fontawesome icon module,
and every remaining reference:

- index.html: drop the two <link> tags
- main.js: drop the requirejs path and shim
- rspamd.js: drop the app/footable-fontawesome require; stickyTabs
  runs directly (declared module dependency)
- common.js: delete dead appendButtonsToFtFilterDropdown (no callers)
- rspamd.css: drop all .footable rules and the dead .footable-details /
  .footable-filtering-search selectors

Rename column-options dropdown hooks ft-columns-* -> tab-columns-*
(Tabulator; consistent with tab-utils.js). Legacy history path kept.

2 weeks ago[Test] static_embed: pack fixture floats via rspamd_util.pack 6118/head
Vsevolod Stakhov [Thu, 2 Jul 2026 08:37:33 +0000 (09:37 +0100)] 
[Test] static_embed: pack fixture floats via rspamd_util.pack

math.frexp was removed in Lua 5.4, so the hand-rolled float32 packer
broke the test run on non-LuaJIT builds; rspamd_util.pack provides
string.pack semantics on every supported Lua version.

2 weeks ago[Feature] static_embed: per-token sequence access
Vsevolod Stakhov [Thu, 2 Jul 2026 08:12:44 +0000 (09:12 +0100)] 
[Feature] static_embed: per-token sequence access

Add model:get_token_vectors(input[, opts]) for offline consumers
(external trainers exporting order-aware text features): the token
embedding sequence in token order instead of only the pooled mean.

- Accepts exactly what get_sentence_vector accepts (word list or whole
  text) and tokenizes through the same shared code path; unk rows are
  included the same way the pooled path includes them.
- opts.max_tokens truncates after tokenization to the first N tokens
  (the returned count is post-truncation); opts.raw returns an
  rspamd_text with ntokens*dim little-endian float32s packed row-major
  instead of a table of tables. Invalid opts raise errors, no silent
  coercions. Empty input yields an empty table/text and 0, never nil.
- The provider path is unchanged: fusion vectors stay fixed-dim, so the
  neural provider keeps using only the pooled get_sentence_vector.
- Tests: id/row alignment, pooled-mean consistency (incl. unk-heavy
  input), word-list/text equivalence, max_tokens, raw packing and
  strict opts validation.

2 weeks ago[Feature] neural: static_embed provider (WordPiece + static matrix)
Vsevolod Stakhov [Wed, 1 Jul 2026 20:24:09 +0000 (21:24 +0100)] 
[Feature] neural: static_embed provider (WordPiece + static matrix)

Add a static token-embedding provider (Model2Vec style), the cheap
multilingual successor to fasttext_embed: words from rspamd's regular
tokenization pipeline are re-tokenized into WordPiece subword tokens and
embedded by mean-pooling rows of a precomputed float32 matrix, with no
neural forward pass and no new dependencies.

- rspamd_static_embed: a Lua-C module combining a WordPiece tokenizer
  (BertNormalizer via ICU + Bert pre-tokenizer + greedy WordPiece) with
  an mmap-ed embedding matrix shared between workers. The model spec is
  read from the model directory (config.json + vocab.txt + matrix +
  optional HF tokenizer.json) and validated strictly, fail-fast: any
  unsupported normalizer/pre-tokenizer/model type, pooling other than
  mean, non-float32 matrix or size mismatch fails the load instead of
  degrading silently. get_sentence_vector() accepts a word list (the
  provider path) or a whole text; both produce identical vectors.
- The WordPiece tokenizer is internal to the vectorizer: the global
  word-breaking / statistics tokenization path is untouched, so Bayes
  tokens and fuzzy hashes are unaffected.
- static_embed provider: extracts words like fasttext_embed and feeds
  them to the model; the Lua side holds no matrix data and uses no FFI.
- unit tests with a generated fixture covering normalization, subword
  splitting, greedy matching, CJK padding, unk handling, mean pooling,
  word-list/text equivalence and strict rejection of unsupported
  configs (BPE model type, vocab/matrix size mismatches, pooling).

Verified against the reference tokenizer oracle: token ids match
exactly, pooled vectors match within 2.4e-05 max abs diff, and the
word-list path is bit-identical to raw-text tokenization on the corpus.

2 weeks ago[Rework] WebUI: global search for history/scan
Alexander Moisseev [Wed, 1 Jul 2026 15:16:46 +0000 (18:16 +0300)] 
[Rework] WebUI: global search for history/scan

Add a per-table boolean search box (history/scan) with a query
language ported from FooTable's built-in filtering: whitespace =
AND, OR, - (exclude), "exact phrase". The predicate ANDs with the
per-column header filters.

Also add headerFilter:"input" to the text columns (matching the
errors-table convention) and align all header-filter inputs on a
shared bottom baseline (title-holder flex-grow) regardless of how
many lines a wrapped title occupies.

2 weeks ago[Rework] WebUI: migrate history/scan to Tabulator
Alexander Moisseev [Wed, 1 Jul 2026 11:06:40 +0000 (14:06 +0300)] 
[Rework] WebUI: migrate history/scan to Tabulator

Migrate the last two FooTable tables, #historyTable_history and
#historyTable_scan, to Tabulator. Both share rendering code in libft.js
(process_history_v2/columns_v2, initHistoryTable, the column-options
dropdown and the symbol-order toggle).

- Action column uses a custom headerFilter (select + "not" checkbox)
  with headerFilterFunc for exact matching.
- Column-options dropdown keeps Visible/Hidden plus "Row", emulated
  via responsive:100 and persisted to localStorage.
- Symbol-order "Sort by:" buttons are embedded in the symbols column
  title, so they are recreated on every render; the active state is
  reapplied on tableBuilt/renderComplete.
- columnDefaults uses the "html" formatter so upstream-escaped values
  decode instead of being re-escaped by "plaintext".
- Shared helpers (scroll preservation, etc.) live in tab-utils.js.

Update the scan/symbols Playwright specs for the Tabulator DOM and
replace the racy "disabled during reload" check with response waits.

2 weeks ago[Rework] WebUI: migrate symbols table to Tabulator
Alexander Moisseev [Thu, 25 Jun 2026 15:26:57 +0000 (18:26 +0300)] 
[Rework] WebUI: migrate symbols table to Tabulator

Phase 2 of the FooTable→Tabulator migration:

- Replace FooTable.init with Tabulator in symbols.js: columns, local
  pagination (25/page), per-column header filters, responsive collapse
- Custom group dropdown filter: native <select> with "Any group" clear
  option, auto-sorted, refreshes on Update
- Score column: formatter:"html" preserves inline editing; row-click
  toggle skips form elements (tab-utils updated)
- Frequency/stddev: raw number storage + closure formatter for
  exponential display (e.g. "1.23e-5"), sorter:"number"
- Events: ready→tableBuilt (read-only), postdraw→renderComplete
- Update: rows.load→setData + dropdown refresh
- rspamd.css: scorebar specificity fix for dark mode, combined hover
  selector for rrd-table + symbolsTable, form-select for group dropdown
- Playwright: update symbols test selector for Tabulator DOM

2 weeks ago[Rework] WebUI: migrate rrd-table to Tabulator
Alexander Moisseev [Tue, 23 Jun 2026 16:06:46 +0000 (19:06 +0300)] 
[Rework] WebUI: migrate rrd-table to Tabulator

Phase 1 of the FooTable→Tabulator migration:

- Extract reusable Tabulator helpers (scroll-prevention, tabindex
  removal, footer-hide, row-click-toggle, scrollIntoView guard) from
  history.js into a new tab-utils.js module — no footable dependency,
  so graph.js no longer loads footable.min.js for the throughput tab
- Refactor history.js initErrorsTable to use tab-utils helpers
- Migrate the rrd summary table (graph.js) from FooTable to Tabulator:
  row coloring via CSS variable, in-place updates via updateData,
  dynamic column titles via updateColumnDefinition, header sorting
- Update rspamd.css: rrd-table selectors for Tabulator DOM, row color
  via --rrd-row-color (visible in dark mode), row hover, layout tweaks

2 weeks ago[Rework] WebUI: migrate errors log to Tabulator
Alexander Moisseev [Mon, 22 Jun 2026 10:52:11 +0000 (13:52 +0300)] 
[Rework] WebUI: migrate errors log to Tabulator

Replace FooTable with Tabulator (v6.4.0) for the errors log table — the
first step of the FooTable→Tabulator migration. Tabulator is vanilla-JS and
jQuery-free, so it survives the planned jQuery removal; FooTable is
unmaintained (GPLv3, last release 2017).

- Vendored tabulator.min.js + tabulator_bs5.min.css; wired via RequireJS
- Rewrote initErrorsTable on the Tabulator API: columns, local pagination,
  responsive collapse, per-column header filters
- rspamd.css: map the bs5 theme to Bootstrap CSS variables so light/dark
  flip automatically with data-bs-theme
- Row click toggles responsive collapse (only when collapse is active);
  pagination footer hidden on a single page (FooTable parity)
- Scroll prevention: Tabulator scrolls the page internally on interactions;
  the call is elusive, so window.scrollY is preserved/restored around
  clicks/Update. Row hover backgrounds disabled to avoid the resulting
  expand-scroll flicker (documented inline for future maintainers).

2 weeks ago[Feature]: src/rspamadm: allow reading password from env (#6114)
Stefan Benten [Tue, 30 Jun 2026 20:02:26 +0000 (22:02 +0200)] 
[Feature]: src/rspamadm: allow reading password from env (#6114)

* src/rspamadm: allow reading password from stdin

Currently the cli only allows reading the password from tty.
This makes it challenging to check the password via automation,
ie. used by continous deployment to ensure a clean state.

This change adds the functionality to read from stdin, so that
passwords can be piped in.

* test/functional/cases: test for stdin password checks

* src/rspamadm/: switch to ENV instead of stdin

2 weeks agoMerge pull request #6117 from HarukaMa/HarukaMa-patch-1
Vsevolod Stakhov [Tue, 30 Jun 2026 19:41:58 +0000 (20:41 +0100)] 
Merge pull request #6117 from HarukaMa/HarukaMa-patch-1

Fix insertion of HTTP header in get_http_headers

2 weeks agoFix insertion of HTTP header in get_http_headers 6117/head
Haruka [Tue, 30 Jun 2026 06:32:25 +0000 (15:32 +0900)] 
Fix insertion of HTTP header in get_http_headers

3 weeks ago[Feature] rspamadm: discover command modules from external directories
Vsevolod Stakhov [Tue, 23 Jun 2026 07:58:48 +0000 (08:58 +0100)] 
[Feature] rspamadm: discover command modules from external directories

rspamadm discovered Lua sub-commands by globbing only the built-in
LUALIBDIR/rspamadm directory, so a third-party or premium package could
not ship a rspamadm command without writing into the OSS-owned lualib
tree.

Scan two additional sources, in order, after the built-in directory:
- $CONFDIR/rspamadm.d/*.lua, a drop-in dir consistent with local.d
- every directory in the colon-separated RSPAMADM_COMMAND_PATH env var
  (mirrors how PATH works)

Duplicate command names are skipped (first wins), so built-in commands
can never be shadowed. Externally loaded modules run with the same
globals and lua_path as built-in ones, so they can require premium
lualibs and use lua_redis. No behavior change when the env var and the
.d directory are absent.

Factor the per-file loader and per-directory scan into helpers, and fix
a latent out-of-bounds in the basename fallback (it searched .lua in the
full path but indexed the basename buffer). Document discovery order and
the new env var in the man page.

3 weeks ago[Fix] lua_kann: correct load arg index and save return value
Vsevolod Stakhov [Tue, 23 Jun 2026 07:57:22 +0000 (08:57 +0100)] 
[Fix] lua_kann: correct load arg index and save return value

rspamd_kann.load is a module function, not a method, so the options
table is at arg 1. The lua_istable(L, 1) branch incorrectly read the
filename field from arg 2 (a copy-paste from lua_kann_save where arg 1
is self), so the documented load({filename = ...}) form always failed
with 'missing filename'.

Also fix lua_kann_save: the trailing lua_pop popped the boolean result
it had just pushed, so save({filename = ...}) returned the filename
string instead of true. Pop the filename field before pushing the
result.

Add a regression test covering the file round-trip via the single-table
form.

3 weeks agoMerge pull request #6111 from moisseev/push
Vsevolod Stakhov [Mon, 22 Jun 2026 13:22:49 +0000 (14:22 +0100)] 
Merge pull request #6111 from moisseev/push

[Test] Run push-triggered CI on all branches in forks

3 weeks ago[Test] Run push-triggered CI on all branches in forks 6111/head
Alexander Moisseev [Mon, 22 Jun 2026 12:16:16 +0000 (15:16 +0300)] 
[Test] Run push-triggered CI on all branches in forks

rspamd/rspamd restricts push to master (71605c25) to avoid the duplicate
push+pull_request run on feature branches that leaves a spurious *cancelled*
status. This trade-off is acceptable for the main repo where contributors
open PRs, but inconvenient in forks where CI is needed on every push without
opening a PR.

4 weeks ago[Feature] neural: forced-learn fast path and first-class freeze
Vsevolod Stakhov [Sat, 20 Jun 2026 19:20:19 +0000 (20:20 +0100)] 
[Feature] neural: forced-learn fast path and first-class freeze

Two training controls plus a supporting task primitive:

* train.forced_learn_minimal_scan (default on when disable_symbols_input):
  a high-priority neural prefilter disables every non-neural symbol on an
  ANN-Train scan, so a symbols-independent training vector is built without
  issuing RBL/DNS, fuzzy, bayes, ClickHouse or capture/cluster work. The
  stored vector and the profile key are byte-for-byte identical to the live
  full-scan path (asserted in tests). For symbol-dependent rules it stays off,
  and if any applicable neural rule needs symbols the whole task falls back to
  a full scan.

* train.frozen: stops automatic training and auto-storing of live vectors so a
  frozen model's pools cannot drift into an imbalanced live set, while
  inference keeps serving the current ANN unchanged. An explicit ANN-Train
  still stores and retrains on demand (gated by a per-profile retrain marker).
  Supersedes the auto-learn side of store_set_only/store_pool_only; both keep
  working when frozen is unset.

* task:disable_all_symbols([skip_mask]): Lua binding over the existing
  rspamd_symcache_disable_all_symbols "process only these" primitive (defaults
  to keeping explicit_disable symbols), used by the prefilter.

Functional coverage in test/functional/cases/330_neural/006_forced_learn_minimal
and 007_frozen.

4 weeks ago[Minor] Update version to 4.1.2
Vsevolod Stakhov [Fri, 19 Jun 2026 14:58:25 +0000 (15:58 +0100)] 
[Minor] Update version to 4.1.2

4 weeks agoRelease 4.1.1 4.1.1
Vsevolod Stakhov [Fri, 19 Jun 2026 14:57:05 +0000 (15:57 +0100)] 
Release 4.1.1

4 weeks ago[Minor] lua_text stats: use 1-based byte indexing
Vsevolod Stakhov [Fri, 19 Jun 2026 11:31:37 +0000 (12:31 +0100)] 
[Minor] lua_text stats: use 1-based byte indexing

The byte-distribution statistics methods (entropy, byte_mean,
byte_deviation, serial_correlation, monte_carlo_pi) took a 0-based
offset, which was inconsistent with the rest of the rspamd_text API
(span/sub/at) and with text:crc32. Switch the optional range argument
to a 1-based start index so all rspamd_text slicing uses the same
convention; len semantics and the lenient out-of-range-yields-0
behaviour are unchanged. Docs and tests updated accordingly.

4 weeks ago[Feature] lua: zlib/YARA-compatible crc32
Vsevolod Stakhov [Fri, 19 Jun 2026 11:28:15 +0000 (12:28 +0100)] 
[Feature] lua: zlib/YARA-compatible crc32

Expose standard CRC-32 (poly 0xEDB88320, init/final 0xFFFFFFFF XOR),
computed via zlib crc32() so it is bit-exact with YARA hash.crc32:

  - rspamd_cryptobox_hash.create_specific("crc32"): streaming
    update + final, big-endian out so :hex() yields e.g. cbf43926
  - rspamd_text:crc32([start[, len]]) and
    rspamd_util.crc32(input[, start, len]): return the checksum as a
    Lua integer over a zero-copy 1-based slice (no buffer copy)

Tests cover golden values ("" -> 0, "123456789" -> 0xCBF43926),
1-based slicing, streaming-vs-one-shot, reset, and a real
attachment-like buffer cross-checked against zlib/YARA crc32.

4 weeks ago[Feature] lua_archive: extraction limits vs zip bombs
Vsevolod Stakhov [Fri, 19 Jun 2026 09:37:44 +0000 (10:37 +0100)] 
[Feature] lua_archive: extraction limits vs zip bombs

archive.unpack/unzip/untar read whole members into memory via
libarchive, so a tiny hostile archive could expand to gigabytes
(OOM). Add an optional opts table that caps extraction, enforced
while reading so memory stays bounded:

  - max_output     total uncompressed bytes across all members
  - max_file_size  per-member uncompressed cap (truncated at cap)
  - max_files      member count cap
  - max_ratio      per-member uncompressed/compressed ratio cap

Ratio uses real compressed bytes consumed via
archive_filter_bytes(a, -1), checked per chunk above a 64 KiB
floor, so a bomb is stopped early even with no size cap set. All
limits are opt-in (0/absent = unlimited), preserving the previous
behaviour. A second boolean return flags truncation so a capped
extraction is never mistaken for a complete one.

4 weeks agoMerge pull request #6106 from rspamd/vstakhov-text-stats
Vsevolod Stakhov [Fri, 19 Jun 2026 08:50:15 +0000 (09:50 +0100)] 
Merge pull request #6106 from rspamd/vstakhov-text-stats

[Feature] lua_text: byte-distribution statistics methods

4 weeks agoMerge branch 'master' into vstakhov-text-stats 6106/head
Vsevolod Stakhov [Thu, 18 Jun 2026 19:32:17 +0000 (20:32 +0100)] 
Merge branch 'master' into vstakhov-text-stats

4 weeks ago[Minor] CI: trigger push only on master to dedup PR runs
Vsevolod Stakhov [Thu, 18 Jun 2026 18:16:54 +0000 (19:16 +0100)] 
[Minor] CI: trigger push only on master to dedup PR runs

A commit on a branch with an open PR fired both a push and a
pull_request run; concurrency cancel-in-progress reaped one, leaving a
spurious *cancelled* run that reads like a CI failure. Restrict push to
master so feature branches run a single pull_request workflow; key the
concurrency group on PR number / ref to still cancel superseded runs.

4 weeks agoMerge pull request #6105 from rspamd/vstakhov-multipattern-som
Vsevolod Stakhov [Thu, 18 Jun 2026 17:56:29 +0000 (18:56 +0100)] 
Merge pull request #6105 from rspamd/vstakhov-multipattern-som

[Feature] multipattern: explicit SOM flag and offset docs

4 weeks ago[Feature] lua_text: byte-distribution statistics methods
Vsevolod Stakhov [Thu, 18 Jun 2026 14:12:09 +0000 (15:12 +0100)] 
[Feature] lua_text: byte-distribution statistics methods

Add byte-distribution statistics as methods on the rspamd_text class,
implemented in C++20 under src/lua (lua_text_stats.{hxx,cxx}); lua_text.c
is left untouched and the rspamd{text} metatable is augmented at load.

Methods (each takes an optional 0-based (off, len) range, defaulting to
the whole buffer):
  - text:entropy([off[, len]])              Shannon entropy, bits/byte
  - text:byte_mean([off[, len]])            mean of unsigned byte values
  - text:byte_deviation(mean[, off[, len]]) mean abs deviation from mean
  - text:serial_correlation([off[, len]])   ENT serial correlation
  - text:monte_carlo_pi([off[, len]])       ENT Monte-Carlo Pi deviation

The core is header-only, allocation-free and O(n) (a single histogram
pass shared by entropy/mean/deviation) and produces deterministic,
bit-reproducible results. Offsets are byte offsets, 0-based; the range is
clamped to the buffer and an out-of-range or empty range yields 0.

Add C++ doctest golden-vector tests (analytically-derived exact values)
and Lua unit tests covering empty/single-byte/uniform/two-symbol buffers,
overlapping groups, slicing and edge cases.

4 weeks ago[Feature] multipattern: explicit SOM flag and offset docs 6105/head
Vsevolod Stakhov [Thu, 18 Jun 2026 13:22:25 +0000 (14:22 +0100)] 
[Feature] multipattern: explicit SOM flag and offset docs

SOM (start-of-match) reporting already exists on master as the default
(hyperscan compiles every pattern with HS_FLAG_SOM_LEFTMOST), but there
was no explicit way to request it and the offset convention was
undocumented.

- Add RSPAMD_MULTIPATTERN_SOM (rspamd_trie.flags.som): an explicit
  opt-in for start offsets that also overrides no_start/single_match
  (forces SOM and drops the incompatible SINGLEMATCH).
- Document the offset convention: pattern id is 1-based; match start
  and end are byte offsets, 0-based, start inclusive and end exclusive
  (one past the last matched byte), so end - start is the match length.
- Fix the regex (flags.re) fallback used when hyperscan is unavailable:
  it discarded the real PCRE start and reported end - strlen(pattern),
  which is bogus for variable-length matches. It now reports the true
  start/end from rspamd_regexp_search.

Add C++ (rspamd_cxx_unit_multipattern.hxx) and Lua (trie.lua) unit
tests asserting (id, start, end) against hand-computed positions:
multiple/overlapping occurrences, icase, literal vs regex, no-match,
SOM-overrides-single_match and a large buffer. Existing rspamd_trie
behaviour and its callers (url.c, lang_detection, lua plugins) are
unchanged.

4 weeks ago[Fix] monitored: alphanumeric-only random DNS prefixes
Vsevolod Stakhov [Wed, 17 Jun 2026 17:52:16 +0000 (18:52 +0100)] 
[Fix] monitored: alphanumeric-only random DNS prefixes

random_monitored RBL checks built random labels from an alphabet that
included '-' and '_'. That produced names like '_Q8...0-' (leading
underscore, trailing hyphen) or '-7d0...' (leading hyphen) which are
not valid DNS labels (RFC 952/1123: no leading/trailing hyphen, no
underscore in hostnames). Authoritative DNSBL servers such as
spfbl.net reject these with SERVFAIL.

Restrict the alphabet to alphanumerics, which always forms a valid
label regardless of position while keeping ample entropy.

Fixes #6103

4 weeks agoMerge pull request #6104 from rspamd/vstakhov-coroutines
Vsevolod Stakhov [Wed, 17 Jun 2026 17:49:15 +0000 (18:49 +0100)] 
Merge pull request #6104 from rspamd/vstakhov-coroutines

[Fix] lua: state management and reuse safety for coroutine thread pool

4 weeks ago[Fix] lua: refcount coroutine thread entries 6104/head
Vsevolod Stakhov [Tue, 16 Jun 2026 19:05:26 +0000 (20:05 +0100)] 
[Fix] lua: refcount coroutine thread entries

The state/generation guards from the previous commit read the entry on
resume, so they only help while the entry is still allocated. If the
owning task is torn down while an async request is in flight, the entry
could be freed before the late completion fires, turning the guard into
a use-after-free.

Make thread_entry refcounted (ref.h, non-atomic - workers are single
threaded). The pool holds the initial reference; terminate_thread() and
the pool-full path now drop it via REF_RELEASE instead of freeing
directly, so the struct is destroyed only once the last reference goes
away. Every async library that stashes an entry for a later completion
now takes its own reference and drops it when done:

  - dns/util: retain at the request, release in the one-shot callback.
  - http/redis: retain at yield, release in the cbdata/ctx destructor
    (and, for redis, at each point that consumes ctx->thread).
  - tcp: retain at each yield, release at the matching resume, since the
    tcp cbdata has several direct-free error paths that bypass its
    destructor; pairing with yield/resume keeps the balance exact and
    leaves bad-argument paths (which return before yielding) untouched.

Combined with the generation guard, a completion that races task
teardown now finds the entry alive but DEAD/recycled and refuses the
resume, instead of dereferencing freed memory.

4 weeks ago[Fix] lua: state management for coroutine thread pool
Vsevolod Stakhov [Tue, 16 Jun 2026 15:35:34 +0000 (16:35 +0100)] 
[Fix] lua: state management for coroutine thread pool

Async libraries (dns/redis/tcp/http/util) capture the "currently
running" coroutine at a yield point and resume it later from a C
completion callback. Nothing previously guaranteed the entry resumed
was still the one captured: a double-fired event, a completion racing
task teardown, or an entry recycled into another task would resume the
wrong (or freed) coroutine and corrupt memory. These failures are
interleaving-dependent and invisible in isolation.

Give each pooled thread an explicit lifecycle (FREE/RUNNING/YIELDED/
DEAD) plus a generation counter, both carried in the existing
thread_entry and per-request cbdata structs - no new allocations on any
hot path:

  - get/return/terminate/yield/resume enforce legal state transitions,
    so returning a suspended thread or resuming a non-suspended one now
    aborts at the exact violation instead of corrupting a core later.
  - lua_thread_resume_checked() refuses to resume unless the thread is
    still YIELDED and its generation matches the value snapshotted at
    the yield point; a stale/duplicate completion becomes a logged
    no-op rather than a wrong-coroutine resume. All five async libs are
    migrated to it.
  - lua_tcp keeps cbd->thread pointing at the coroutine actually
    yielded by sync read/write, so the resume always targets it.

generation is bumped on every acquire and release, so an entry that
goes back to the pool and is handed out again no longer matches a
completion that was already in flight.

4 weeks ago[Fix] dns: do not defer resolver nameservers (fixes #6096)
Vsevolod Stakhov [Tue, 16 Jun 2026 07:54:45 +0000 (08:54 +0100)] 
[Fix] dns: do not defer resolver nameservers (fixes #6096)

A nameserver that failed to resolve at config time was turned into a
zero-address PENDING_RESOLVE upstream by 904fd6218, then dereferenced as
NULL in rspamd_dns_server_init -> SIGSEGV at worker startup (regression
in 4.1.0). DNS resolver nameservers are consumed synchronously by
rdns_resolver_add_server and can never be promoted async, so never defer
them; also NULL-guard rspamd_dns_server_init as defense in depth.

4 weeks ago[Fix] mx_check: loopback-only MX is LOCAL, not bogon
Vsevolod Stakhov [Mon, 15 Jun 2026 08:59:47 +0000 (09:59 +0100)] 
[Fix] mx_check: loopback-only MX is LOCAL, not bogon

A domain whose MX resolves only to loopback is hosted on the scanning
host itself -- a self-MX, typically the host's own FQDN mapped to
127.0.0.1 in /etc/hosts, which rspamd's resolver honours as a fake reply
that shadows public DNS. That made fully DMARC-aligned self-hosted mail
score MX_BOGON_ONLY (+8.0): the strongest "not spam infrastructure"
signal treated as the strongest spam signal.

Move 127.0.0.0/8 and ::1/128 from BOGON_CIDRS to LOCAL_CIDRS so a
loopback-only MX emits MX_LOCAL_ONLY (3.0) instead. test_mode now lifts
loopback out of the LOCAL set (was: bogon) so the probe path stays
exercisable against local listeners.

Add a regression test (170_mx_check_selfmx.robot, test_mode = false):
the existing suites run test_mode = true and cannot cover the production
loopback-classification path.

Closes #6101

5 weeks agoMerge pull request #6102 from moisseev/devdeps
Vsevolod Stakhov [Sat, 13 Jun 2026 20:32:40 +0000 (21:32 +0100)] 
Merge pull request #6102 from moisseev/devdeps

[Test] Update dev dependencies

5 weeks ago[Feature] neural: sequence output mode and SIF word selection in fasttext_embed
Vsevolod Stakhov [Sat, 13 Jun 2026 12:18:18 +0000 (13:18 +0100)] 
[Feature] neural: sequence output mode and SIF word selection in fasttext_embed

Add a generic word-vector sequence output to the fasttext_embed provider
so that custom ANN architectures (e.g. attention pooling) can learn their
own pooling instead of receiving a pre-pooled vector:

* output_mode = "sequence": emits the first max_words word vectors
  flattened word-major and zero-padded to max_words * channels.
* word_selection = "sif": since order-invariant poolers do not need a
  prefix, optionally fill the sequence with the max_words most
  distinctive (highest SIF weight) words from anywhere in the message
  instead of the leading ones. Default stays "prefix".

This is the data half only; how the sequence is consumed is left to the
ANN architecture. Both modes are bounded by max_words.

5 weeks ago[Feature] neural: pluggable feature-provider and ANN-architecture registries
Vsevolod Stakhov [Sat, 13 Jun 2026 12:18:01 +0000 (13:18 +0100)] 
[Feature] neural: pluggable feature-provider and ANN-architecture registries

Turn the neural plugin into an extension point so third-party (including
closed-source) modules can add feature providers and network topologies
without patching the core.

* register_architecture(name, builder) / get_architecture(name): a
  registry of ANN builders, function(n_inputs, rule) -> kann network.
  The built-in 'symbol', 'embedding' and 'conv1d' architectures are now
  registered through it; create_ann() dispatches on rule.architecture
  and falls back to the historical auto-selection when it is unset, so
  existing configs are unaffected.
* register_provider (already present) and register_architecture are
  exported from the neural module, so a module that does
  require 'plugins/neural' can register a custom provider or
  architecture and select it with provider type / rule.architecture.

An unknown rule.architecture now fails loudly with a hint that the
providing module may not be loaded, instead of silently falling back.

5 weeks ago[Feature] lua_kann: expose slice and concat transforms
Vsevolod Stakhov [Sat, 13 Jun 2026 12:17:42 +0000 (13:17 +0100)] 
[Feature] lua_kann: expose slice and concat transforms

kad_slice and kad_concat_array were already implemented and serialized
in kautodiff but not reachable from Lua. Exposed as
rspamd_kann.transform.slice(node, axis, start, end) (0-based, end
exclusive, batch is axis 0) and rspamd_kann.transform.concat(axis,
node1, node2, ...). These make split/bypass/merge graph topologies
buildable from Lua, e.g. routing different parts of a fused input
vector through different sub-networks (needed by custom ANN
architectures such as attention pooling with late fusion).

5 weeks ago[Test] Update dev dependencies 6102/head
Alexander Moisseev [Sat, 13 Jun 2026 07:16:53 +0000 (10:16 +0300)] 
[Test] Update dev dependencies

Update ESLint 10.3.0 → 10.5.0, stylelint 17.11.0 → 17.13.0, and related packages

5 weeks ago[Feature] kann: add multi-head attention pooling operator
Vsevolod Stakhov [Fri, 12 Jun 2026 15:38:19 +0000 (16:38 +0100)] 
[Feature] kann: add multi-head attention pooling operator

New kad operator attn_pool (op 38, appended to preserve model
serialization compatibility): multi-head dot-product attention pooling
over a zero-padded sequence of word vectors with learned query vectors.
All-zero positions are treated as padding and masked out of the
softmax; attention weights are stashed in gtmp between the forward and
backward passes. Exposed as kann_layer_attn_pool() and
rspamd_kann.layer.attn_pool(node, n_words[, n_heads]).

Verified: converges on a needle-in-haystack task unsolvable by a flat
dense net (0.985 vs 0.715 accuracy), exact word-order invariance of the
pooled output, padding determinism and save/load roundtrip.

5 weeks ago[Fix] neural: stabilize training on dense embedding inputs
Vsevolod Stakhov [Fri, 12 Jun 2026 14:35:59 +0000 (15:35 +0100)] 
[Fix] neural: stabilize training on dense embedding inputs

Training an ANN on dense provider features (fasttext_embed, text_hash)
could silently produce a degenerate model: with the historical
learning_rate=0.01 default, RMSprop drives the net into tanh saturation
depending on weight init luck - the loss freezes, yet the constant
all-one-class model is saved and classifies every message as spam (or
ham) until the next retrain. On a real corpus this happened in roughly
one of three weight inits.

Fixes:

* use the embedding (funnel) architecture for any rule with dense
  feature providers, not only LLM ones: the simple symbol architecture
  applies ReLU directly to the input, clipping the negative half of the
  embedding space, and is the least stable option on such vectors
  (it is also less accurate; layernorm in the funnel fixes the
  conditioning)
* resolve the learning_rate default by input type: 0.01 for symbol
  vectors as before, 0.001 for dense embeddings, which converges
  reliably with equal accuracy; an explicit config value still wins
* add a quality gate to the training child: a model with constant or
  single-class output on its own training set is rejected instead of
  saved; the lock is released and training retries on the next cycle
  with a different weight init, which converges in practice
* return an explicit msgpack rejection marker from the training child
  instead of nil on the gate/NaN paths: a nil return used to deadlock
  the controller against the training subprocess (see the lua_worker
  fix) and stalled training forever

5 weeks ago[Fix] lua_worker: do not deadlock when subprocess returns an invalid value
Vsevolod Stakhov [Fri, 12 Jun 2026 14:35:41 +0000 (15:35 +0100)] 
[Fix] lua_worker: do not deadlock when subprocess returns an invalid value

When a function run via worker:spawn_process returned nil (or any
non-string value), the child logged an error but wrote nothing to the
result pipe. The parent then kept waiting for a reply while the child
blocked forever on the post-reply ack read, deadlocking both processes
and anything serialised behind them (e.g. the neural training lock,
which got extended indefinitely so training never retried until a full
restart).

Report invalid return values to the parent as a regular error reply so
on_complete fires and the caller can recover.

5 weeks agoMerge pull request #6065 from dragoangel/fix/url-suspect-oneshot
Vsevolod Stakhov [Fri, 12 Jun 2026 11:03:34 +0000 (12:03 +0100)] 
Merge pull request #6065 from dragoangel/fix/url-suspect-oneshot

[Fix] Do not multiply URL multiple AT signs and backslash in URLs

5 weeks ago[Fix] protocol: use case preserving boundary for HTTP multipart parsing
Vsevolod Stakhov [Fri, 12 Jun 2026 10:46:34 +0000 (11:46 +0100)] 
[Fix] protocol: use case preserving boundary for HTTP multipart parsing

RFC 2046 boundaries are case sensitive, but the v3 HTTP multipart
callers used ct->boundary, which is lowercased for MIME clients quirks,
so requests with uppercase characters in the boundary failed to parse.
Use ct->orig_boundary in protocol.c, rspamd_proxy.c and rspamdclient.c.

5 weeks ago[Feature] css: detect more text hiding tricks
Vsevolod Stakhov [Fri, 12 Jun 2026 00:15:50 +0000 (01:15 +0100)] 
[Feature] css: detect more text hiding tricks

Extend the invisible-text detection with several common CSS hiding
techniques used to dilute visible content with hidden ham text:

- off-screen positioning: position:absolute|fixed with a large negative
  left/top
- image-replacement text-indent: a large negative text-indent
- clip / clip-path collapsing the element to a zero area, e.g.
  rect(0,0,0,0), inset(100%), circle(0)
- visibility:collapse (treated as hidden)
- tiny font sizes (<= 3px), not only font-size:0

These are modelled as a hidden display in compile_to_block so the
hiding correctly propagates to descendants. The previous off-screen
heuristic was a fragile substring match on the raw style that only
incremented a feature counter and never actually hid the text; it is
replaced by structured parsing of the position/left/top/text-indent/
clip properties, with the offscreen feature counter now driven by a
flag set on the compiled block.

5 weeks ago[Fix] css: detect text hidden via overflow clipping, opacity and max-* sizes
Vsevolod Stakhov [Thu, 11 Jun 2026 22:02:56 +0000 (23:02 +0100)] 
[Fix] css: detect text hidden via overflow clipping, opacity and max-* sizes

Phishing messages dilute the visible content with hidden ham text using
CSS hiding techniques that the parser did not understand:

- 'max-width:0; max-height:0; overflow:hidden' was fully ignored as
  max-width/max-height/overflow were not parsed at all
- 'opacity:0' was parsed but the value was silently discarded in
  compile_to_block
- 'height:0' was applied to the block width due to a copy-paste bug,
  and zero dimensions were never considered by compute_visibility

Fixes:

- parse max-width/max-height (clamping width/height) and overflow
- treat a block with zero height or width and overflow:hidden as
  invisible, propagating it to descendants via the display value
- treat opacity < 0.1 as a hidden display, as descendants cannot reset
  the ancestor opacity
- do not allow a child display value to resurrect content of a hidden
  ancestor in propagate_block (display:none is not resettable in CSS)
- fix the height->width copy-paste bug and a missing break that made
  the font-size case fall through into the opacity case

5 weeks agoMerge pull request #6100 from a16bitsysop/s390x-test
Vsevolod Stakhov [Thu, 11 Jun 2026 19:50:57 +0000 (20:50 +0100)] 
Merge pull request #6100 from a16bitsysop/s390x-test

[Fix] unit test checks upstream rate limit state using custom fake_clock

5 weeks ago[Fix] unit test checks upstream rate limit state using a custom fake_clock 6100/head
Duncan Bellamy [Thu, 11 Jun 2026 18:37:18 +0000 (19:37 +0100)] 
[Fix] unit test checks upstream rate limit state using a custom fake_clock

under a mock environment where the event loop (ev_run()) is never actually executed.

5 weeks ago[Fix] mime: fix build with OpenSSL 4.0 opaque ASN1_STRING
Vsevolod Stakhov [Thu, 11 Jun 2026 17:20:02 +0000 (18:20 +0100)] 
[Fix] mime: fix build with OpenSSL 4.0 opaque ASN1_STRING

OpenSSL 4.0 made ASN1_STRING (and thus ASN1_OCTET_STRING) opaque, so
direct access to its length/data fields no longer compiles. Use
ASN1_STRING_length()/ASN1_STRING_get0_data() which are available since
OpenSSL 1.1.0 and LibreSSL 2.7.

Also move the legacy OpenSSL init calls (ERR_load_crypto_strings,
SSL_load_error_strings, OpenSSL_add_all_*) under the pre-1.1.0 guard:
they are redundant on modern OpenSSL and break no-deprecated builds.

Fixes: #6087
5 weeks ago[Fix] milter: send QUARANTINE even with a custom reply
Vsevolod Stakhov [Thu, 11 Jun 2026 14:05:13 +0000 (15:05 +0100)] 
[Fix] milter: send QUARANTINE even with a custom reply

The QUARANTINE command was sent inside the `if (!reply)` guard that
synthesises a default quarantine reason, so a caller-supplied SMTP
message (e.g. task:set_pre_result('quarantine', 'reason')) suppressed
the command entirely and the message was accepted instead of
quarantined. Affects both METRIC_ACTION_QUARANTINE and reject converted
via quarantine_on_reject. Regression from fbc6e35db (3.10.0).

The reply now becomes the quarantine reason, matching the reject and
tempfail branches where a caller-supplied message takes precedence over
the configured default.

Reported by @johnmosli, who also attached the fix.

Issue: #6088

5 weeks agoMerge pull request #6094 from moisseev/symcache
Vsevolod Stakhov [Wed, 10 Jun 2026 19:03:33 +0000 (20:03 +0100)] 
Merge pull request #6094 from moisseev/symcache

[Fix] symcache: fix timeout inflation in pre_postfilter_iter grouping

5 weeks ago[Fix] url: scan bare query parameters containing '=' as a whole
Vsevolod Stakhov [Wed, 10 Jun 2026 19:02:34 +0000 (20:02 +0100)] 
[Fix] url: scan bare query parameters containing '=' as a whole

A bare embedded URL is its own query parameter and can contain '='
itself (e.g. base64 padding in the path). The query-embedded scan
treated everything before the first '=' as a parameter key, so such
URLs were discarded. Treat the prefix as a key only when it has no
URL structure characters (':' or '/'); otherwise scan the whole
parameter.

5 weeks ago[Fix] symcache: fix timeout inflation in pre_postfilter_iter grouping 6094/head
Alexander Moisseev [Wed, 10 Jun 2026 07:33:15 +0000 (10:33 +0300)] 
[Fix] symcache: fix timeout inflation in pre_postfilter_iter grouping

The `saved_priority` initialization to -1 caused the first item in each
phase vector (prefilters/postfilters/idempotent) to be split off from
its priority group and counted individually, inflating the computed
maximum symbols cache timeout. Initialize to the first item's priority
instead so items at the same priority are correctly grouped.

Issue: #6092

5 weeks ago[Test] checkv3: drop requests_toolbelt from form-data parse
Vsevolod Stakhov [Tue, 9 Jun 2026 13:13:33 +0000 (14:13 +0100)] 
[Test] checkv3: drop requests_toolbelt from form-data parse

The /checkv3 content negotiation tests parsed the multipart/form-data
reply with requests_toolbelt, a third-party module not present in every
test pipeline (e.g. the rspamd-docker functional run installs
python3-msgpack but not requests-toolbelt), so they failed with
ModuleNotFoundError.

Replace it with a self-contained stdlib HTTP-multipart splitter: split
on the boundary delimiter (HTTP-multipart style, deliberately not the
email/MIME parser used for the message/rfc822 case) and trim only the
single CRLF framing each part so binary (zstd) payloads stay byte-exact.
Drop the now-unneeded requests-toolbelt from the CI pip install.

5 weeks agoMerge pull request #6089 from rspamd/vstakhov-neural-stale-fix
Vsevolod Stakhov [Tue, 9 Jun 2026 12:24:03 +0000 (13:24 +0100)] 
Merge pull request #6089 from rspamd/vstakhov-neural-stale-fix

[Fix] neural: don't strand trained ANNs behind tombstones

5 weeks ago[Fix] neural: don't strand trained ANNs behind tombstones 6089/head
Vsevolod Stakhov [Tue, 9 Jun 2026 11:43:40 +0000 (12:43 +0100)] 
[Fix] neural: don't strand trained ANNs behind tombstones

A trained ANN could become unreachable to workers even though
training succeeded: NEURAL_SPAM/NEURAL_HAM stopped firing while the
controller logged "ann ... is changed, our version = N, remote
version = M" forever.

Root cause is a version regression, not a missing zset registration.
The new version was seeded from the in-memory set.ann, and
fill_set_ann resets set.ann.version to 0 whenever a worker never
loaded an ANN (restart, or the selected profile's blob was missing).
A worker that trained from the _4 profile then saved version 1.
process_existing_ann selects the highest version among compatible
profiles, so the live version-1 blob was shadowed by the stale
version-4 zset entry whose key was empty. The profile zset has no
TTL, so the dead high-version tombstone was immortal and the
condition self-perpetuated (the _4 blob was never rewritten).

Three fixes:

1. Version monotonicity (lualib/plugins/neural.lua): seed the new
   version from the profile actually trained from (the trained-from
   key encodes it as the trailing _<n>), max'd with
   training_profile/set.ann, so the new entry always outranks the
   profile it supersedes.

2. Liveness-aware selection (src/plugins/lua/neural.lua,
   neural_maybe_invalidate.lua): when the selected profile's blob is
   missing, fall back to the next compatible profile with a live blob
   instead of going dark, and emit a throttled warning (was a silent
   debug line). The invalidate script also GCs profile entries that
   have no blob and no training data and are older than a grace
   window.

3. Lifetime coupling (neural_save_unlock.lua,
   src/plugins/lua/neural.lua): give the profile zset a TTL refreshed
   each check_anns cycle, and refresh the blob TTL on every reload,
   so an actively used ANN never expires out from under its entry.

Adds 330_neural/005_stale_version.robot, which injects a
higher-version tombstone and asserts inference recovers.

5 weeks agoMerge pull request #6072 from xandris/bugfix/mime_string_const_iterator
Vsevolod Stakhov [Mon, 8 Jun 2026 08:51:58 +0000 (09:51 +0100)] 
Merge pull request #6072 from xandris/bugfix/mime_string_const_iterator

fix: mime_string const iterator

5 weeks agoMerge pull request #6083 from rspamd/vstakhov-controller-checkv3
Vsevolod Stakhov [Sun, 7 Jun 2026 16:27:38 +0000 (17:27 +0100)] 
Merge pull request #6083 from rspamd/vstakhov-controller-checkv3

[Feature] checkv3: controller endpoint + Accept/Accept-Encoding negotiation

5 weeks agoMerge pull request #6084 from moisseev/redirector
Vsevolod Stakhov [Sun, 7 Jun 2026 16:27:24 +0000 (17:27 +0100)] 
Merge pull request #6084 from moisseev/redirector

[Minor] url_redirector: distinguish direct URL errors from redirect errors

5 weeks ago[Minor] url_redirector: distinguish direct URL errors from redirect errors 6084/head
Alexander Moisseev [Sun, 7 Jun 2026 08:53:45 +0000 (11:53 +0300)] 
[Minor] url_redirector: distinguish direct URL errors from redirect errors

When http_callback reports an error on the first hop (orig_url == url),
no redirect has occurred yet, but the old message "found redirect error
from X to X" implied one.  Split the message: "error checking URL" for
direct failures and "redirect error: A -> B" for mid-chain failures.

6 weeks agoMerge pull request #6082 from moisseev/simdutf
Vsevolod Stakhov [Sat, 6 Jun 2026 19:21:58 +0000 (20:21 +0100)] 
Merge pull request #6082 from moisseev/simdutf

[Fix] Prioritise bundled simdutf headers over system ones

6 weeks ago[Minor] checkv3: trim verbose comments to house style 6083/head
Vsevolod Stakhov [Sat, 6 Jun 2026 17:00:24 +0000 (18:00 +0100)] 
[Minor] checkv3: trim verbose comments to house style

Condense the multi-line explanatory blocks added with the negotiation
reply to single-line notes, and drop a dangling 'see contract above'
reference that pointed to nothing in the source.

6 weeks ago[Feature] checkv3: Accept/Accept-Encoding negotiation
Vsevolod Stakhov [Sat, 6 Jun 2026 14:33:58 +0000 (15:33 +0100)] 
[Feature] checkv3: Accept/Accept-Encoding negotiation

The /checkv3 reply ignored Accept beyond a json-vs-msgpack toggle and
always emitted a hard-coded multipart/mixed body (with form-data part
headers). There was no way to ask for a plain v2-style json/msgpack
body, no true multipart/form-data reply for HTTP multipart parsers,
and Accept-Encoding had no defined default.

Negotiate the representation solely from Accept and compression solely
from Accept-Encoding on the single chokepoint all three workers (normal
scan worker, rspamd_proxy, controller) share, the reply_v3 helper:

  application/json | application/msgpack -> single-body v2 reply
  message/rfc822                         -> multipart/mixed envelope
  multipart/form-data                    -> multipart/form-data envelope
  absent / wildcard                      -> multipart/form-data default
  only unsupported types (e.g. xml)      -> 406 Not Acceptable

Inside the multipart envelopes the result-part serialization mirrors
the input metadata serialization (json or msgpack); the two envelopes
differ only in the top-level Content-Type. Compression honours
Accept-Encoding: zstd and defaults to identity. Vary: Accept,
Accept-Encoding is always advertised. Negotiation reuses the existing
http_content_negotiation parser (q-values + wildcards), extended with
two media types; the input metadata serialization is recorded on the
task via a new protocol flag.

rspamc previously sent Accept: application/json|msgpack for v3, which
now selects a single-body reply it does not expect; it now requests
multipart/form-data and accepts any multipart/ subtype, with the result
serialization carried by the metadata Content-Type.

Tested by a new C++ content_negotiation suite, multipart envelope-mode
unit tests, and a functional negotiation suite run against both the
normal worker and the controller (json/msgpack/email-MIME/HTTP-multipart
parsers). Adds msgpack/requests/requests-toolbelt to functional CI deps.

6 weeks ago[Fix] controller: support the /checkv3 endpoint
Vsevolod Stakhov [Sat, 6 Jun 2026 12:45:18 +0000 (13:45 +0100)] 
[Fix] controller: support the /checkv3 endpoint

The scan worker and rspamd_proxy both handle /checkv3 (multipart
metadata + message in, multipart/mixed results out), but the
controller's HTTP path router never registered it. Since rspamc
defaults to the controller port (11334) for localhost, a plain
`rspamc --protocol-v3` returned 404 with
"rspamd_http_router_finish_handler: path: /checkv3 not found".

Register /checkv3 on the controller (routed to the existing scan
handler) and branch on CMD_CHECK_V3 in both directions:
parse the body via rspamd_protocol_handle_v3_request() on input and
emit the multipart reply via rspamd_protocol_http_reply_v3() on
output, mirroring the proxy. Auth posture matches /check and
/checkv2 (read command, no enable password).

6 weeks ago[Fix] Prioritise bundled simdutf headers over system ones 6082/head
Alexander Moisseev [Sat, 6 Jun 2026 06:36:24 +0000 (09:36 +0300)] 
[Fix] Prioritise bundled simdutf headers over system ones

6 weeks ago[Minor] Update version to 4.1.1
Vsevolod Stakhov [Fri, 5 Jun 2026 10:37:50 +0000 (11:37 +0100)] 
[Minor] Update version to 4.1.1

6 weeks agoRelease 4.1.0 4.1.0
Vsevolod Stakhov [Fri, 5 Jun 2026 10:36:33 +0000 (11:36 +0100)] 
Release 4.1.0

6 weeks agoMerge pull request #6080 from moisseev/redirector-log
Vsevolod Stakhov [Fri, 5 Jun 2026 09:51:58 +0000 (10:51 +0100)] 
Merge pull request #6080 from moisseev/redirector-log

[Minor] url_redirector: clarify log messages for successful HTTP responses