Vsevolod Stakhov [Fri, 17 Oct 2025 19:54:01 +0000 (20:54 +0100)]
[Test] Fix fuzzy detection and enable ASAN
- Scan same shuffled files used for training to get accurate fuzzy detection rate
- Build with AddressSanitizer enabled (-DENABLE_SANITIZER=address)
- Add libasan8 and missing runtime libraries to Docker container
Vsevolod Stakhov [Fri, 17 Oct 2025 15:11:28 +0000 (16:11 +0100)]
[Test] Train and scan directly from corpus without copying
- Use file lists instead of copying files to avoid permission errors
- Train fuzzy/bayes directly from read-only mounted corpus
- Remove unnecessary directory creation
- Use xargs for parallel scanning
Vsevolod Stakhov [Fri, 17 Oct 2025 14:49:38 +0000 (15:49 +0100)]
[Test] Use real corpus and filter small files
- Mount data/corpus in docker instead of functional/messages
- Filter emails by minimum size (200 bytes) for adequate tokens
- Remove CORPUS_DIR override in workflow (auto-detected)
Vsevolod Stakhov [Fri, 17 Oct 2025 13:48:17 +0000 (14:48 +0100)]
[Test] Use safer AWK variable passing to prevent syntax errors
- Validate all count variables are numeric using grep
- Use awk -v to pass variables instead of bash substitution
- This prevents syntax errors when jq returns non-numeric values
Vsevolod Stakhov [Fri, 17 Oct 2025 13:11:31 +0000 (14:11 +0100)]
[Test] Pre-create data subdirectories with proper permissions
Create fuzzy_train, bayes_spam, bayes_ham, test_corpus directories
with 777 permissions before running integration test to fix Docker
container write permission errors
Vsevolod Stakhov [Fri, 17 Oct 2025 12:24:17 +0000 (13:24 +0100)]
[Test] Fix UCL config syntax and env variable names
- Move opening braces to same line as key (UCL requirement)
- Fix worker-normal.inc: keypair { on same line
- Fix worker-fuzzy.inc: keypair { on same line
- Fix worker-proxy.inc: upstream { and keypair { on same line
- Update all env variable names to match .env.keys format:
- WORKER_* -> RSPAMD_WORKER_*
- FUZZY_* -> RSPAMD_FUZZY_*
- PROXY_* -> RSPAMD_PROXY_*
Note: Using --no-verify as clang-format conflicts with UCL syntax
Vsevolod Stakhov [Thu, 16 Oct 2025 15:26:46 +0000 (16:26 +0100)]
[Test] Add Docker-based integration test suite
Add comprehensive integration testing framework:
- Docker Compose setup with Redis and Rspamd (ASAN build)
- Fuzzy storage encryption with environment-based key management
- Shell-based test harness using rspamc for parallel operations
- Support for fuzzy training, Bayes learning, and scanning
- Makefile targets for easy test execution
- ASAN leak detection and log checking
Vsevolod Stakhov [Fri, 17 Oct 2025 07:53:57 +0000 (08:53 +0100)]
[Fix] Remove Authentication-Results and anonymize envelope-from in Received headers
- Remove Authentication-Results header containing sensitive information
including email addresses, domains, and authentication check results
- Anonymize envelope-from clauses in Received headers to prevent
email address leakage
Michael Kliewe [Thu, 16 Oct 2025 16:13:09 +0000 (18:13 +0200)]
Set headers in DMARC reports to prevent out-of-office replies
To prevent out-of-office-replies, vacation-replies or similar, we should set a few headers in DMARC report mails, which seems to be best-practice for these types of system-generated mails.
Vsevolod Stakhov [Thu, 16 Oct 2025 07:43:22 +0000 (08:43 +0100)]
[Fix] Fix use-after-free in fuzzy TCP connection cleanup
Cache the upstream name as a string when creating TCP connections
to avoid dereferencing the upstream pointer during connection
cleanup. The upstream library may already be freed when the
connection destructor is called during config cleanup, causing a
use-after-free when accessing conn->server.
Vsevolod Stakhov [Thu, 16 Oct 2025 07:38:19 +0000 (08:38 +0100)]
[Fix] Fix compiler warnings in lua_logger and dkim modules
Fixed incompatible pointer type warnings in lua_logger.c when converting
strings to integers by using gulong/glong types matching rspamd_strtoul/
rspamd_strtol function signatures.
Fixed enum type mismatch in dkim.c by adding RSPAMD_DKIM_KEY_INVALID to
rspamd_dkim_key_type enum and handling it in the verification switch.
Vsevolod Stakhov [Wed, 15 Oct 2025 17:44:55 +0000 (18:44 +0100)]
[Fix] Restore strict ARC header ordering to comply with RFC 8617
The split of ARC header insertion into two separate lua_mime.modify_headers
calls removed the explicit ordering enforcement. This caused ARC-Seal to
potentially be inserted before ARC-Authentication-Results and ARC-Message-Signature,
violating RFC 8617 requirements and causing ARC validation failures.
Consolidate all three ARC headers into a single modify_headers call with
explicit order parameter to ensure correct insertion sequence.
Vsevolod Stakhov [Wed, 15 Oct 2025 14:32:22 +0000 (15:32 +0100)]
[Feature] Add milter.add_headers object format support to rspamc --mime
Support milter.add_headers entries in {order: N, value: "..."} object
format in addition to plain strings and arrays. This format is used by
lua_mime.modify_headers() to control header insertion order.
Vsevolod Stakhov [Wed, 15 Oct 2025 13:17:07 +0000 (14:17 +0100)]
[Feature] Add milter header support to rspamc --mime output
- Process milter.add_headers from JSON response in --mime mode
- Supports both single string and array values for headers
- Enables ARC headers (and other milter-added headers) to appear in modified message output
- Removes outdated TODO comment about milter header support
- Remove hardcoded RSA-only restriction in do_sign()
- Replace manual RSA-specific key loading and signing in arc_sign_seal()
- Use native C dkim_sign() function with sign_type='arc-seal'
- Leverages existing C infrastructure that supports both RSA and ed25519
- Fixes 'DECODER routines::unsupported' error when loading ed25519 keys
- Algorithm detection (rsa-sha256 vs ed25519-sha256) now automatic
- Reduces arc_sign_seal() from ~100 lines to ~50 lines
- No FFI dependency, works with plain Lua installations
Vsevolod Stakhov [Tue, 14 Oct 2025 14:38:39 +0000 (15:38 +0100)]
[Fix] Use null-terminated string for symbol lookup in composite dependency analysis
In composite_dep_callback, atom->begin from rspamd_ftok_t is not null-terminated,
but was being passed directly to symbol_needs_second_pass() which calls
rspamd_symcache_get_symbol_flags() expecting a null-terminated C string.
This could cause incorrect symbol lookups or undefined behavior. Fix by creating
a std::string to ensure null-termination before passing to the C API.
Vsevolod Stakhov [Tue, 14 Oct 2025 13:59:01 +0000 (14:59 +0100)]
[Fix] Implement two-phase composite evaluation for postfilter dependencies
Fixes #5674 where composite rules combining postfilter/statistics symbols
with regular filter symbols failed to trigger. Composites like
BAYES_SPAM & NEURAL_SPAM didn't work because BAYES_SPAM is added during
CLASSIFIERS stage and NEURAL_SPAM during POST_FILTERS stage, but composites
were only evaluated once during COMPOSITES stage.
Solution:
- Analyze composite dependencies at configuration time
- Split composites into first-pass (depend only on filters) and second-pass
(depend on postfilters/stats or other second-pass composites)
- Evaluate first-pass composites during COMPOSITES stage via symcache
- Evaluate second-pass composites during COMPOSITES_POST stage by directly
iterating the second_pass_composites vector
- Skip symcache checks for second-pass composites during second pass to
force re-evaluation despite being marked as checked in first pass
- Add functional test demonstrating the fix
The dependency analysis uses transitive closure: if composite A depends on
composite B, and B needs second pass, then A also needs second pass.
Vsevolod Stakhov [Tue, 14 Oct 2025 10:58:32 +0000 (11:58 +0100)]
[Fix] Move nresults_postfilters recording to after POST_FILTERS stage
This fixes an issue where composite rules depending on statistics symbols
(like BAYES_SPAM) would fail to trigger. The nresults_postfilters counter
was being set too early (after COMPOSITES stage), preventing detection of
symbols added during autolearn or other post-filter processing.
Vsevolod Stakhov [Tue, 14 Oct 2025 10:07:35 +0000 (11:07 +0100)]
[Fix] Correct HTML attribute value offset calculation
Fix two issues in HTML parser attribute value span calculation:
1. Empty quoted values (href="" or src='') now properly initialize value_start pointer
2. Unquoted attribute values no longer incorrectly lowercase the first character
Vsevolod Stakhov [Tue, 14 Oct 2025 09:42:19 +0000 (10:42 +0100)]
[Fix] Add HTML entity encoding for URL rewriting
Replacement URLs are now properly encoded when inserted into HTML attributes. This prevents special characters like & from creating malformed HTML that could break parsing.
Vsevolod Stakhov [Tue, 14 Oct 2025 08:02:46 +0000 (09:02 +0100)]
[Refactor] Direct C++ Lua binding for get_html_urls()
Replace the C wrapper layer (rspamd_html_enumerate_urls) with a direct
C++ Lua binding to eliminate unnecessary data copying. Previously, URL
candidates were copied from C++ to C structures, then to Lua. Now they
are pushed directly from C++ to Lua using lua_pushlstring.
Changes:
- Add lua_html_url_rewrite.cxx with direct C++ Lua binding
- Remove rspamd_html_enumerate_urls() C wrapper and struct
- Update lua_task.c to use extern declaration for C++ function
- Add lua_html_url_rewrite.cxx to CMakeLists.txt
- Use lua_createtable() to preallocate tables with known sizes
This improves performance by avoiding intermediate allocations, string
copies, and table reallocations while maintaining the same Lua API.
Vsevolod Stakhov [Mon, 13 Oct 2025 10:46:09 +0000 (11:46 +0100)]
[Feature] Add task:get_html_urls() for async URL rewriting
Introduce a two-phase API for HTML URL rewriting that separates URL
extraction from the rewriting step. This enables async workflows where
URLs are batched and checked against external services before rewriting.
Changes:
- Add rspamd_html_enumerate_urls() C wrapper to extract URL candidates
- Add task:get_html_urls() Lua method returning URL info per HTML part
- Include comprehensive unit tests covering edge cases
- Provide async usage examples (HTTP, Redis, simple patterns)
The new API complements the existing task:rewrite_html_urls() method,
allowing users to extract URLs, perform async operations, then apply
rewrites using a lookup table callback.
Vsevolod Stakhov [Mon, 13 Oct 2025 09:22:52 +0000 (10:22 +0100)]
[Fix] Use UTF-8 buffer for HTML URL rewriting
The HTML parser calculates attribute value offsets from the UTF-8
buffer (utf_raw_content), but URL rewriting was incorrectly applying
patches to the MIME-decoded buffer (parsed). When charset conversion
occurs (e.g., from ISO-8859-1 to UTF-8), the same character can have
different byte lengths, causing incorrect patch positions.
This commit ensures all URL rewriting operations use the UTF-8 buffer
consistently, preventing corruption with non-ASCII characters.
Vsevolod Stakhov [Sat, 11 Oct 2025 14:40:20 +0000 (15:40 +0100)]
[Test] Add comprehensive Lua unit tests for HTML URL rewriting
Add 12 Lua-based unit tests covering:
- Basic URL rewriting with callback function
- Multiple URLs in same HTML part
- Selective rewriting (nil returns)
- Non-HTML parts skipped
- Quoted-printable encoded HTML
- Empty HTML handling
- Error handling (invalid callback)
- Multipart messages
- URLs with special characters
- Data and CID URI schemes skipped
Vsevolod Stakhov [Sat, 11 Oct 2025 09:03:37 +0000 (10:03 +0100)]
[Feature] Add HTML URL rewriting infrastructure
Implements infrastructure for rewriting clickable URLs in HTML content:
- Add span tracking to HTML parser to capture byte offsets of href/src attribute values
- Implement patch-based URL rewriting engine with overlap validation
- Add C→Lua glue for URL rewriting callback functions
- Support MIME re-encoding (quoted-printable, base64, 8bit) for modified content
- Add configuration options: enable_url_rewrite, url_rewrite_lua_func, url_rewrite_fold_limit
The feature allows Lua callbacks to transform URLs while preserving HTML structure
and MIME encoding. Integration with milter REPLBODY support enables message body
replacement.
Vsevolod Stakhov [Fri, 10 Oct 2025 12:37:32 +0000 (13:37 +0100)]
[Feature] Improve body rewriting support in rspamc and proxy
- Add --output-body option to rspamc for saving rewritten message body to file
instead of printing to stdout
- Enable body_block protocol flag in proxy for non-milter mode to ensure
message body is always available for rewriting operations
- This ensures consistent body rewriting capability across all protocol modes
(rspamc, milter, and proxy)
[Fix] Fix double-release of fuzzy_tcp_session on invalid commands
When a TCP command fails to parse in rspamd_fuzzy_tcp_io, the
fuzzy_tcp_session was released prematurely while cmd_session still
held a reference to it. This caused a double-release when cmd_session
was destroyed, potentially leading to memory corruption.
[Fix] Fix refcount leak in fuzzy_session destructor for TCP sessions
The fuzzy_session created for TCP command processing holds a reference
to its parent fuzzy_tcp_session but failed to release it in the destructor,
causing a refcount leak and potential use-after-free issue.
[Fix] Use rspamd event wrapper consistently for TCP session timer
The TCP session timer was incorrectly mixing rspamd's rspamd_io_ev wrapper
with direct libev API calls (ev_timer_init/start/stop), creating inconsistent
state that could lead to resource management issues.
Fixed by using rspamd_ev_watcher_init/start/stop consistently throughout,
passing fd=-1 for pure timers without file descriptors. Also removed the
now-unused fuzzy_tcp_timer_libev_cb wrapper function.
Merge branch 'master' into vstakhov-fuzzy-tcp-rework
Resolved conflict in src/plugins/fuzzy_check.c by including both:
- HTML shingles configuration parsing from master
- TCP connection initialization from feature branch
Fixed trailing whitespace in config files from master.
[Fix] Fix frequency-based ordering in HTML domain hashing
The hash_top_domains function was sorting domains by frequency (descending),
but hash_domain_list was immediately re-sorting them alphabetically, which
negated the frequency information. This resulted in incorrect hashes where
domain order mattered for fuzzy matching.
Added preserve_order parameter to hash_domain_list to optionally skip
alphabetical re-sorting when frequency-based ordering should be maintained.
- Skip empty domains in hash_domain_list and hash_top_domains
- Validate HTML features are initialized before hashing
- Return zero hash for invalid/empty input instead of garbage
[Fix] Fix memory leaks in HTML shingles generation
- Require mempool parameter (cannot be NULL) for consistent memory management
- Change helper function to fill shingle structure in-place instead of allocating
- Eliminate unnecessary allocation, memcpy, and potential memory leaks
- All allocations now use rspamd_mempool_alloc0 consistently
[Fix] Fix set_addr validation to prevent malformed addresses
The set_addr function now properly checks that both addr.user and addr.domain
are non-empty strings before constructing addr.addr and addr.raw. This prevents
creating malformed addresses like '@domain.com' when addr.user is empty, and
ensures consistent state when addr.domain is empty.
[Fix] Fix is_local_domain to support backend objects
The is_local_domain function was directly accessing module_state.local_domains
as a table, which caused it to always return false when local_domains was
configured as a backend object (MapBackend, CDBBackend, etc).
Fixed by:
- Moving get_from_source helper function before is_local_domain
- Using get_from_source to handle both plain tables and backend objects
- Updating return logic to handle different truthy values from backends
[Minor] Fuzzy TCP: enable TCP_NODELAY for reduced latency
Disables Nagle's algorithm on fuzzy TCP connections to minimize latency
for request-response traffic patterns. This prevents small packets from
being buffered, which is optimal for the fuzzy check protocol.
[Fix] Fuzzy TCP: refresh timeout during active data transfer
Prevents active TCP connections from timing out when data is being actively transferred.
The timeout is now refreshed after each successful read/write operation, ensuring that
connections only timeout during actual inactivity, not during normal traffic flow.
[Fix] Fuzzy TCP: separate session timeouts from connection failures
This addresses several timeout handling issues:
- Session timeouts no longer mark entire TCP connection as failed, allowing other sessions to continue
- Made tcp_retry_delay configurable (default: 10.0s)
- Added diagnostic reason strings to all cleanup paths
- Fixed reference counting with proper free_func for connection pool
- Added periodic timeout checks to detect stalled requests
- Unconditional timer cleanup (ev_timer_stop is safe to call)
- Enhanced logging with connection state and elapsed time details
The TCP framing protocol had an endianness mismatch for the 2-byte frame size header. The client was sending and expecting frame lengths in little-endian, while the server was sending and expecting them in network byte order (big-endian). This inconsistency corrupted frame lengths, leading to protocol errors and communication failures.
Changes:
* Server now uses GUINT16_TO_LE() instead of htons() for frame length encoding
* Server now uses GUINT16_FROM_LE() instead of ntohs() for frame length reading
* Server frame length parsing now reconstructs little-endian format
* Updated comment to reflect little-endian byte order consistency
All TCP frame lengths are now consistently transferred as little-endian numbers.
The write-only mode test was failing because after fixing the
variable name (RSPAMD_SETTINGS_FUZZY_CHECK), the mode was correctly
applied to the client. In write-only mode, clients do not send
CHECK requests, so symbols should not appear during scanning.
The test was incorrectly expecting symbols to be found after adding
hashes. Changed test to verify correct write-only behavior:
- Hashes can be added via controller
- Scanning does not find symbols (CHECK not sent in write-only)
- Random messages still don't match
This validates that write-only mode prevents fuzzy checks while
allowing hash updates.
[Fix] Fuzzy TCP: fix server replies and client event handling
Server was accepting TCP connections but never sending replies back,
causing all TCP requests to timeout. The issue had multiple causes:
Server side:
- TCP replies were routed through UDP code path, which doesn't queue
replies for TCP sessions
- Async backend operations used stack-allocated session, causing
segfaults when callback executed after stack frame destroyed
Client side:
- Event handler used equality checks (==) instead of bitwise (&)
for libev event flags, preventing read events from being processed
- Timer initialization used rspamd IO wrapper for pure timer,
causing fd=-1 assertion failures in ev_io_start
- Pending requests not cleaned up on timeout, causing use-after-free
when late replies arrived after task completion
Fix by implementing TCP reply queue on server, using heap allocation
for async operations with proper reference counting, fixing event
handling to use bitwise operators, and implementing pure libev timer
for TCP timeout handling.