Issue Description
Two unprivileged local accounts with reciprocal user Sieve redirect rules (A forwards to B, B forwards to A — a typical vacation-forwarding setup created via a webmail rule builder) create an infinite internal mail loop. No existing loop-control mechanism catches it: the Sieve engine’s redirect loop avoidance and MAXREDIRECTS operate per script execution, so each hop is a single legal redirect, and the cycle only exists across the two accounts. RFC 5228 §4.2 requires loop control measures; per-script control exists, cross-account control does not.
The consequence is unbounded database growth: every loop iteration writes the full message blob again (twice, due to “keep”), and since v0.16.10 each iteration additionally re-triggers RocksDB blob garbage collection, whose compactions rewrite ~270 MB blob files continuously — the loop and the GC feed each other. In our incident the store grew until the disk was full. Effectively an unprivileged DoS: two ordinary user accounts can take down the entire server. On our HDD-backed storage the episode ended in RocksDB corruption after an unclean stop and required a restore from backup.
Expected Behavior
The server detects that a message is circulating between local accounts via redirect and terminates the loop after a small number of hops, delivering locally instead of redirecting again — analogous to Exchange’s auto-forward limit. The existing duplicate (RFC 7352) tracking infrastructure could serve this: if the same Message-ID re-enters an account via redirect within a short window, deliver locally.
Actual Behavior
The message circulates indefinitely; the database grows without bound (~400 writes/s sustained, ~70 MB flushes every ~4 s on an otherwise idle server) until the disk is full and the server fails. “keep” additionally deposits a copy in both mailboxes on every iteration. Client-side mitigations (guard headers in generated scripts) cannot be enforced, since any ManageSieve/JMAP client or hand-written script bypasses them.
Reproduction Steps
-
Create two local accounts A and B, no special permissions.
-
Upload this user Sieve script to A (and the mirror image to B, redirecting to A):
if header :contains “From” “@” {
redirect “[email protected]”;
keep;
} -
Send one external email (ideally with a large attachment) to either account.
-
Observe the message looping between A and B; database size and write load grow unbounded until the disk is full.
Relevant Log Output
Loop signature — identical ~70 MB memtable flushes every ~4 s on an otherwise idle server (sequence numbers grew from 144,001,813 to 144,091,184 in ~3.5 min, ~400 writes/s, no client traffic):
2026/07/03-23:58:22.968203 EVENT_LOG_v1 {"job": 40, "event": "flush_started", "num_memtables": 1, "total_num_input_entries": 14, "num_deletes": 0, "total_data_size": 70504793, "flush_reason": "Write Buffer Full"}
2026/07/03-23:59:17.282108 EVENT_LOG_v1 {"job": 59, "event": "flush_started", "num_memtables": 1, "total_num_input_entries": 14, "num_deletes": 0, "total_data_size": 70790381, "flush_reason": "Write Buffer Full"}
14 entries producing a ~70 MB write buffer means these are almost pure message blobs (large mails/attachments), the same handful of messages re-written on every loop iteration.
Each batch of L0 flushes crosses the threshold and triggers a compaction that rewrites ~270 MB blob files — this is the RocksDB blob GC introduced in v0.16.10, so the loop and the GC continuously feed each other:
2026/07/03-23:59:13.237058 [t] Level summary: files[4 0 0 0 0 2 19] max score 1.00, estimated pending compaction bytes 67265454
2026/07/03-23:59:13.237213 EVENT_LOG_v1 {"job": 58, "event": "compaction_started", "cf_name": "t", "compaction_reason": "LevelL0FilesNum", "files_L0": [25590, 25587, 25581, 25574], "score": 1}
2026/07/03-23:59:13.993786 EVENT_LOG_v1 {"job": 58, "event": "blob_file_creation", "file_number": 25592, "total_blob_count": 54, "total_blob_bytes": 271340696, "status": "OK"}
2026/07/03-23:59:14.775375 EVENT_LOG_v1 {"job": 58, "event": "blob_file_creation", "file_number": 25594, "total_blob_count": 54, "total_blob_bytes": 271465528, "status": "OK"}
For scale, blob files are ~270 MB each and were being produced roughly one per ~0.8 s during the sustained loop (file numbers climbed 25318 → 25596+ in minutes), i.e. hundreds of MB/s of blob rewrites driven entirely by the two-message loop.
Stalwart Version
v0.16.x
Installation Method
Binary (Linux)
Database Backend
RocksDB
Blob Storage
RocksDB
Search Engine
Internal
Directory Backend
Internal
Additional Context
Single node, LXC container on ZFS (HDD pool at the time of the incident — which made the sustained compaction pressure worse, but the unbounded growth itself is storage-independent). The rules were created via a JMAP webmail client’s visual rule builder; the generated catch-all condition (From contains “@”) matches every message, including forwarded ones. The issue is client-independent: any ManageSieve/JMAP client or hand-written script produces the same result.
Happy to provide full RocksDB LOG files, the second account’s script, or test a patched build against the reproduction.
I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.
on
I have searched this support forum (open and closed topics) and confirm this is not a duplicate.
on
I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.
on