Timo Sirainen [Thu, 11 Jun 2026 19:20:53 +0000 (19:20 +0000)]
lib-dcrypt: Add test for x9.62 fixed-width signature encoding
ECDSA signing uses a random nonce, so r and s vary in length each time.
Loop sign/verify 1000 times asserting the x9.62 signature is always the
fixed 2*32 bytes for P-256 and round-trips through verification. This
catches the case where r or s has leading zero bytes.
Timo Sirainen [Thu, 11 Jun 2026 19:20:41 +0000 (19:20 +0000)]
lib-dcrypt: Fix x9.62 ECDSA signature to use fixed-width encoding
The x9.62 / IEEE P1363 signature format is a fixed-width r||s, where each
value is padded to the curve order octet length. The openssl3 backend
instead derived the width from the DER encoding and only prepended a
single zero byte to whichever of r or s was shorter.
DER encodes minimal-length signed integers, so r and s vary in length
depending on the random ECDSA nonce: leading zero bytes are dropped and a
0x00 sign byte is added when the high bit is set. Whenever the two
lengths differed by more than one byte (one value having a zero top byte,
roughly 1 in 256 signatures), the output was either an odd total length
or had an incorrect split point. Verification then rejected it, causing
random failures.
Pad each value to the curve order width (EVP_PKEY_bits/8) after stripping
the DER sign byte, so the output is always a fixed 2*rs_len bytes.
Timo Sirainen [Thu, 11 Jun 2026 11:05:07 +0000 (11:05 +0000)]
auth: Add session_pid_trusted parameter to skip session_pid check
The master REQUEST verifies via UNIX credentials that the connecting
peer's PID matches the provided session_pid. This breaks auth proxies
(e.g. cluster) that connect to auth on behalf of the actual session
process: their peer PID never matches, so they were forced to drop
session_pid entirely. That in turn made auth skip auth_token
generation, which broke IMAP hibernation now that unhibernation
requires DOVECOT-TOKEN.
Let a trusted (userdb_restricted_uid == 0) master connection set
session_pid_trusted to skip the peer-credentials check, while still
binding auth_token to the real session_pid.
Timo Sirainen [Fri, 5 Jun 2026 14:29:14 +0000 (14:29 +0000)]
lib: istream-concat - Fail with EIO instead of asserting on truncated child
i_stream_concat_read() asserted cur_data_pos == data_size when it needed
to read more from the current child stream. This invariant can be broken
when a child istream reports (via stat()) a larger size than the amount
of data it can actually return, e.g. a corrupted or truncated stream. In
that case the child ends up with less data available than the
concat-istream had already provided from it, tripping the assert and
crashing the process.
Handle cur_data_pos > data_size by failing the stream with EIO instead.
The cur_data_pos < data_size direction stays a fatal assert, since that
would still indicate a genuine accounting bug.
Timo Sirainen [Fri, 5 Jun 2026 14:05:57 +0000 (14:05 +0000)]
lib-storage: Fix decoded size calculation for base64 attachments
The newline count was estimated by dividing the encoded size by the number
of base64 characters per line (base64_blocks_per_line * 4) instead of the
full physical line length, which also includes the newline bytes. This
overestimates the number of newlines, subtracts too many bytes and produces
a decoded size that is too small.
As a result a missing attachment was reconstructed too short, the message
became smaller than its cached size and was incorrectly reported as having
broken MIME parts / too little data, deleting the cache record.
Timo Sirainen [Fri, 5 Jun 2026 10:46:38 +0000 (10:46 +0000)]
config: Fix doveconf -f collapse of a filter that includes a @group
Including a @group inside a non-named filter (e.g. @testgroup=foo inside
local 1.2.3.4 { }) leaves the include_groups array populated on that
filter, since it is kept for config output. When doveconf -f matches such
a filter, config_parse_merge_filters() collapses it into its parent via
config_filters_merge(), which asserts the merged filter has no pending
include_groups:
Panic: file config-parser.c: line 2503 (config_filters_merge):
assertion failed: (array_is_empty(&src_filter->include_groups))
Named filters are skipped by this collapse, so only non-named filters
(local/remote/protocol) crashed.
The include groups have already been applied during parsing, so clear
them recursively on the matched filter subtree before merging it into the
parent. Because the include group reference is now gone, the group's
settings can no longer be re-expanded from it during config dumping;
promote them from CONFIG_PARSER_CHANGE_GROUP to explicit so they are
dumped directly. Otherwise a filter defined purely by the group (e.g. a
mailbox { } provided by the group) would be dropped by the
SET_AND_DEFAULT_OVERRIDES dump scope and fail to resolve with
"Filter mailbox=... unexpectedly not found".
Timo Sirainen [Fri, 5 Jun 2026 10:37:10 +0000 (10:37 +0000)]
config: Fix crash when including default @group into a single-tree filter
When a @group is defined in the built-in config, its child filters get
default_settings=TRUE. Including such a group into a non-default filter
(e.g. @fs_dictmap_defaults=cassandra inside dict_server { }) reached for
the parent's reverse_default_sibling to place the grafted filter into the
matching default/non-default tree. But a filter that exists in only one
tree has no reverse sibling, so the pointer was NULL and dereferencing it
crashed.
Only use the reverse sibling when it exists. Otherwise force the grafted
filter (and its children) into the parent's own tree by stamping its
default_settings to match the parent.
Timo Sirainen [Wed, 29 Apr 2026 21:07:23 +0000 (21:07 +0000)]
auth: Drop SIGUSR2 cache stats handler
The same counters are now available over the auth-master UNIX socket
via 'doveadm auth cache status', which doesn't require log access and
doesn't reset the counters as a side effect. Drop the SIGUSR2 handler.
Timo Sirainen [Wed, 29 Apr 2026 21:06:48 +0000 (21:06 +0000)]
auth: Drop SIGHUP cache flush handler
The same flush is already exposed as 'doveadm auth cache flush' over
the auth-master UNIX socket, so the SIGHUP handler is redundant.
Removing it also frees SIGHUP for other uses (e.g. anvil sending
SIGHUP to a service whose admin-socket CONFIG-RELOAD timed out).
Timo Sirainen [Sat, 2 May 2026 09:55:09 +0000 (09:55 +0000)]
doveadm: Add 'doveadm auth cache status' command
Accept --reset to clear the auth-side hit/miss/insert counters after
reading them. This gives a way to inspect the cache without needing
access to the auth process log, and replaces the SIGUSR2-only stats
path.
Timo Sirainen [Sat, 2 May 2026 09:54:05 +0000 (09:54 +0000)]
auth: Add CACHE-STATUS request to auth-master protocol
Expose the passdb cache counters (hits, misses, positive/negative
entry counts and sizes, used/max bytes) over the auth-master UNIX
socket via a new CACHE-STATUS request.
Add auth_cache_get_status() to snapshot the counters and
auth_cache_reset_counters() to clear the hit/miss/insert counters
without touching the cached entries themselves. The master connection
handler accepts an optional 'reset' argument that resets after
reporting.
global: Fix Lua linking issue by separating libs from dependencies
In src/auth/Makefile.am, LUA_LIBS was incorrectly added to auth_libs, which is used in auth_DEPENDENCIES. This caused make to fail when LUA_LIBS contained linker flags like -L (e.g. on Wolfi OS).
Also moved LUA_LIBS from LDFLAGS to LIBADD in mail-lua plugin.
Timo Sirainen [Wed, 3 Jun 2026 08:18:54 +0000 (11:18 +0300)]
master: Increase timeout after config reload before killing still running old processes
Previously after config reload master waited 6s before killing old
processes. However, lib-master could have waited for 30s before it
gracefully stopped the process. So increase the timeout to 35s.
Aki Tuomi [Sat, 16 May 2026 20:20:02 +0000 (23:20 +0300)]
lib-imap-client: Remove stale empty resp-text error check
imapc_connection_handle_imap_resp_text() rejected an empty text atom
with "Missing text in resp-text". RFC 9051 change note #23 explicitly
made text optional in resp-text, so this error is wrong.
The check was dead code in practice — the imap parser only enters
ARG_PARSE_TEXT mode after a resp-text-code ending in ']', so the text
argument at args[1] is never an empty atom — but leaving it in place
risks breaking connections if the parser behavior ever changes.
If the vsize-hdr in the mail index becomes corrupted (i.e., has a
size that is not zero but is incorrect, or a message-count that is
too low), the system fails to automatically correct it. The function
responsible for updating this header for new emails,
index_mailbox_vsize_update_appends(), misinterprets a corrupted header
as a non-existent one and intentionally avoids creating it. This
leads to a persistent state of corruption, causing incorrect reports
of mailbox size until a manual, full recalculation is triggered.
Add a hdr_corrupted field to the update struct, set when either
vsize_header_refresh() detects an invalid header size or
index_mailbox_vsize_check_rebuild() detects an invalid message
count.
The rebuild guards in index_mailbox_get_virtual_size() and
index_mailbox_vsize_update_appends() (added in the preceding
commit) ensure the rebuild path is always taken when the header
is in an invalid state, preventing stale data from being returned
or lock acquisition being skipped due to edge-case uidnext values.
lib-storage: Add rebuild guard in vsize early return and lock path
The vsize header rebuild path sets update->rebuild when the header
is missing or invalid. Two code paths had insufficient guards against
this state:
In index_mailbox_get_virtual_size(), the early-return for cached vsize
did not check rebuild. If rebuild is true, vsize_hdr has been zeroed
and should never be returned as valid cached data. Add the
!update->rebuild guard to prevent returning stale data in any
rebuild scenario (missing header, invalid count, invalid size).
In index_mailbox_vsize_update_appends(), the try_lock condition only
checked highest_uid + 1 != uidnext. When rebuild is true, highest_uid
is zeroed so this becomes 1 != uidnext. If uidnext == 1, the condition
evaluates false and the lock is skipped entirely, meaning the zeroed
header is never repaired. Add the update->rebuild guard to ensure the
full rebuild path is always taken when the header needs repair.
These guards ensure rebuild state is properly handled in all code paths,
independent of any specific corruption mechanism.
Aki Tuomi [Fri, 22 May 2026 17:43:02 +0000 (17:43 +0000)]
lib-mail: istream-binary-converter - Fix heap-use-after-free on bodyless MIME part
When end-of-headers fires as block.size==0 with hdr_buf still live (a
MIME part declaring Content-Transfer-Encoding: binary before any
Content-Type, followed directly by a boundary without an intervening
blank line), stream_add_data() was called with the buffer's own
storage as source while bstream->hdr_buf still pointed at it. The
self-append could trigger a realloc grow, freeing the source mid-copy.
Detach hdr_buf before the flush, matching the pattern already used in
stream_finish_convert_decision().
lib-fs: Fix assertion failure in fs_write_stream_abort
Fixes a panic "assertion failed: (output != &file->output)" in
fs_write_stream_abort().
This crash occurred when fs_write_stream_abort_error() was called with
&file->output, which happens in some error paths (e.g. during disk full
in FTS indexing).
The fix allows passing &file->output to fs_write_stream_abort() by carefully
handling the pointer nulling and stream abortion order.
rootvector2 [Thu, 21 May 2026 12:56:17 +0000 (18:26 +0530)]
lib-dict-backend: cdb - Fix use-after-realloc of returned key pointer
cdb_dict_next() reuses ctx->buffer for both the key and the value:
the key is placed at offset 0 of the buffer, and a pointer to it is
returned via *key_r. cdb_dict_iterate() then writes that pointer back
to *key_r in the caller's slot and only afterwards appends the value
into the same buffer with buffer_append_space_unsafe(). If the total
size (key_len + 1 + value_len + 1) exceeds the buffer's current
allocation (initially 256), the append triggers a realloc on the
default_pool, which moves the backing memory and frees the previous
allocation. The key pointer saved in *key_r is now dangling, and the
caller's next dereference is a heap-use-after-free.
The fix sets *key_r from ctx->buffer->data after the value has been
appended, so it always reflects the buffer's current (post-realloc)
base address. The key is at offset 0, so reading the buffer's data
pointer is sufficient.
Reproduced as a standalone case with AddressSanitizer, which reports
heap-use-after-free on the saved key pointer after the value append
forces the buffer to grow past its initial 256-byte allocation.
Paul Howarth [Thu, 26 Jun 2025 09:47:35 +0000 (10:47 +0100)]
m4: Fix for systems with signed 32-bit time_t
The code and tests have some hard-coded values based on the size of
time_t but they don't take into account whether or not time_t is a
signed value.
On systems such as i686 with time_t as a signed 32-bit integer, the
maximum value is the same as an unsigned 31-bit integer. Tweak the
configure test to treat 32-bit signed as 31 bits.
overflow the maximum value of 2147483647, causing the test to fail.
This can be fixed by using a "long long" (which is used for intmax_t)
instead, e.g.
Aki Tuomi [Wed, 20 May 2026 12:21:01 +0000 (15:21 +0300)]
lib: panic on nearest_power() overflow with the offending value
Replace the i_assert() with an i_panic() that reports the input
value, making overflow failures easier to diagnose. Add unit tests
for the boundary input and a fatal test for the overflow case.
Aki Tuomi [Wed, 13 May 2026 15:33:08 +0000 (15:33 +0000)]
quota: Fix maildir quota drop to zero after IMAP MOVE/REPLACE
When mailbox_move() or IMAP REPLACE is used with the maildir quota
driver, the source mail expunge is double-counted, causing the quota
to drop to zero.
quota_try_alloc() called quota_alloc_with_size(ctx, size, expunged_size),
which subtracted expunged_size from the destination transaction's
bytes_used. At the same time mail_expunge() on the source/expunged mail
fired quota_mail_expunge(), and the expunge was committed independently
via qbox->expunge_qt in sync_notify. Both deltas commit to the same
quota root, netting -size when size == expunged_size.
Co-Authored: Aleksei Fedorov <aleksei.fedorov@webpros.com>
Fixes issues with false positives on certain phrase searches, as searches
on tokenized pieces are really intended to be used as a limiter on the
full phrase searching as opposed to an indication of full phrase match
on those messages.
Example 1: Exact phrase no-match (the canonical case)
Message body: "abcde fghij"
Search: BODY "abcd fghi"
Before: MATCHES (false positive — tokenized "abcd*" AND "fghi*" found)
After: NO MATCH (correct — exact phrase "abcd fghi" not in body)
Example 2: Exact phrase that should match
Message body: "the quick brown fox"
Search: BODY "quick brown"
Before: MATCHES (worked, but for wrong reason — tokenized terms matched)
After: MATCHES (correct — exact phrase "quick brown" verified by core)
Example 3: Words exist but not as a phrase
Message body: "brown fox quick"
Search: BODY "quick brown"
Before: MATCHES (false positive — both words present, just not adjacent)
After: NO MATCH (correct — "quick brown" doesn't appear as a substring)
Example 4: Single-word split (e.g., dotted word)
Message body: "foo.bar value"
Search: TEXT "foo.bar"
Before: NO MATCH (broken — phrase logic incorrectly applied, original
string "foo.bar" was skipped during token expansion)
After: MATCHES (correct — single word without spaces is not treated
as a phrase; "foo.bar" is indexed and matched directly)
Example 5: Multi-word search with one non-phrase word
Message body: "hello world"
Search: BODY hello "world peace"
Before: "hello" matched; "world peace" could produce false positives
from tokenized "world*" AND "peace*"
After: "hello" matches; "world peace" correctly requires exact phrase
Phrase arguments, and the tokenized children are now labeled into
three categories:
- This is the full phrase
- This is a tokenized word of a phrase, and it is the first word
- This is a tokenized word of a phrase
Before recent changes, the mailbox_list_escape_name_params() leading-'~' rule
fired on every hierarchy part, so vname "parent/~child" was written to
disk as "parent/\7echild". The new code applies the rule only on the
first part, encoding the same vname as "parent/~child". On-disk
directories written by older Dovecot versions therefore become
inaccessible after upgrade: the lookup constructs the new-format
storage name and stat()s a path that no longer matches.
Timo Sirainen [Thu, 30 Apr 2026 14:41:33 +0000 (14:41 +0000)]
lib-storage: mailbox_name_hdr_encode/decode - Distinguish INBOX from <ns prefix>/INBOX
The box-name index header stored the storage name with per-part unescape,
which collapses <escape>49NBOX to "INBOX". On rebuild from box-name headers,
this made <ns prefix>/INBOX indistinguishable from the regular INBOX, so
list index rebuild restored <ns prefix>/INBOX as INBOX.
Append a backwards compatible flag byte to the box-name header after two
trailing NUL bytes, with MAILBOX_NAME_HDR_FLAG_INBOX_INBOX bit signaling
that the on-disk "INBOX" name actually refers to <ns prefix>/INBOX. The
flag byte is only emitted when at least one bit is set, so old headers
(no flag byte) round-trip unchanged. The decoder strips trailing NUL
padding added by lib-index, then detects the flag suffix unambiguously:
two consecutive NULs at the end never occur in the old format because
the encoder splits on the hierarchy separator and real storage names
contain no empty hierarchy parts.
Old Dovecot versions reading a new-format header will parse the flag
byte as a one-byte trailing hierarchy component. This is acceptable
because old Dovecot never wrote <ns prefix>/INBOX in the first place.
Timo Sirainen [Thu, 30 Apr 2026 09:22:10 +0000 (09:22 +0000)]
imapc: Document round-trip requirements for invalid mUTF-7 names
Note in the imapc-list.c top-of-file comment that the four name
forms (remote_name, storage_name, vname, fs_name) round-trip
losslessly even for invalid mUTF-7 remote names, but only when
mailbox_list_visible_escape_char is set. With it unset and
mailbox_list_utf8=no, vname falls through to the raw mUTF-7 form
and SELECT round-trips would re-encode literal '&' as "&-",
breaking access to those mailboxes.
This documents the existing behavior; no code change.
Timo Sirainen [Thu, 30 Apr 2026 09:06:07 +0000 (09:06 +0000)]
lib-storage: Add tests for mailbox_list_escape_name_params() first_part
Cover the leading-'~' rule (escaped only when first_part=TRUE), the
per-part dirstart escapes for "." and "..", separator conversion,
and a round-trip property test against
mailbox_list_unescape_name_params() for inputs that look like parts
vs full vnames.
The leading '~' check used to fire whenever the input started with '~'.
That assumed callers always passed the full vname. Since
mailbox_list_default_get_storage_name() and
mailbox_name_hdr_decode_storage_name() now split into hierarchy parts
before escaping, the '~' check would wrongly match the start of any
part - e.g. "foo/~bar" would have its second component escaped as
"+7ebar" even though '~' in the middle of a path has no special
meaning.
Add a first_part parameter. Only when first_part=TRUE do we apply the
leading-'~' escape. Callers operating on parts pass first_part=FALSE
for non-leading parts.
For mailbox_list_default_get_storage_name() also require prefix[0] ==
'\\0' so that when the INBOX/INBOX prefix has been prepended to
storage_name, the first split-part is no longer the very start of the
escaped output.
Timo Sirainen [Thu, 30 Apr 2026 09:03:08 +0000 (09:03 +0000)]
lib-storage: mailbox_list_unescape_name_params() - Drop unused ns_prefix parameter
Symmetric to the previous escape_name_params() cleanup. The three
callers (mailbox_list_default_get_vname_part(),
imapc_list_storage_to_remote_name(), imapc_list_fs_to_storage_name())
either pass an empty string or operate on a single hierarchy part
that won't start with the namespace prefix.
Timo Sirainen [Thu, 30 Apr 2026 09:01:58 +0000 (09:01 +0000)]
lib-storage: mailbox_list_escape_name_params() - Drop unused ns_prefix parameter
All callers either pass an empty string or pass list->ns->prefix
operating on input where the prefix has already been stripped earlier
(by mailbox_list_vname_prepare() or by splitting into hierarchy parts).
The str_begins(vname, ns_prefix, ...) match was effectively dead.
Drop the parameter. Callers updated: mailbox_list_default_get_storage_name(),
mailbox_name_hdr_decode_storage_name(), imapc_list_remote_to_storage_name(),
imapc_list_storage_to_fs_name().
Timo Sirainen [Mon, 26 Jan 2026 22:09:24 +0000 (00:09 +0200)]
lib-storage: Fix handling escaped mailbox names in mailbox list index
Code paths using mailbox_list_index_node_get_path() weren't escaping the
mailbox name, breaking some of the functionality for such mailboxes.
This includes:
* Auto-deleting \Noselect leaf mailboxes
* Notifying changes to mailbox (IMAP NOTIFY)
* Storing mailbox GUID in dovecot.list.index
Timo Sirainen [Tue, 5 May 2026 21:15:22 +0000 (21:15 +0000)]
login-common: Emit proxy_session_finished and new _started event when client is redirected
When LOGIN_PROXY_FAILURE_TYPE_AUTH_REDIRECT was handled, the disconnect
event chain never produced an error_code=proxy_dest_redirected entry.
Fire a new proxy_session_finished event with this error_code and start
a new proxy_session_started for the new destination.
Timo Sirainen [Tue, 5 May 2026 21:14:04 +0000 (21:14 +0000)]
login-proxy: Set destination event fields in login_proxy_set_destination()
Move dest_host, dest_ip and dest_port event_add_*() calls from
login_proxy_new() into login_proxy_set_destination(). This ensures the
fields stay in sync.
Timo Sirainen [Wed, 6 May 2026 15:19:16 +0000 (15:19 +0000)]
imap-login: Reject duplicate MULTIPLEX from backend
If a buggy backend IMAP server sends "* MULTIPLEX 0" twice, the second
parse call re-entered login_proxy_multiplex_input_start() and crashed
on the multiplex_orig_input==NULL assert. Detect the duplicate and
fail the proxy connection with a protocol error instead.
Returns whether login_proxy_multiplex_input_start() has already been
called on the proxy. Lets callers detect a duplicate MULTIPLEX
notification from a buggy backend instead of triggering the
multiplex_orig_input==NULL assert.
fts-flatcurve: Clean up FTS directory on mailbox deletion
This code is only needed for Maildir. dbox handles FTS cleanup
already because it can just recursively delete the entire index
directory natively. Maildir (for safety reasons) can only delete
directories that appear in the "is_internal_name" list.
Fred Morcos [Thu, 30 Apr 2026 14:44:27 +0000 (16:44 +0200)]
auth: Fix stale cache entry blocking auth after password change in worker path
When passdb cache verification runs on an auth worker, a successful
prior login plus a subsequent password change leaves a stale cached
password. The non-worker code path already handles this: if the
cached password mismatches and the entry's last_success flag (or
neg_expired) indicates the cache is likely stale, fall back to a
fresh passdb lookup. The worker callback was missing this handling,
so users would fail to authenticate until the cache entry expired.
After the worker returns a PASSWORD_MISMATCH, re-lookup the cache
node and apply the same passdb_cache_use_password_mismatch() rule
to decide whether to invoke the fallback. The node pointer cannot
be carried across the worker round-trip (see auth_cache_node
lifetime), so the lookup is redone here. orig_cache_result is
preserved so the re-lookup does not clobber the request's recorded
cache result.
If the re-lookup misses because the entry was evicted while the
worker was busy, fall back unconditionally instead of delivering
the mismatch as-is: a password change would otherwise be reported
as a mismatch instead of being retried, and the cached evidence
that would have driven the stale-cache rule no longer exists, so
an extra fresh lookup is the right behavior.
Fred Morcos [Thu, 30 Apr 2026 15:34:55 +0000 (17:34 +0200)]
auth: Convert passdb_cache_verify_plain() to continuation-passing style
Change passdb_cache_verify_plain() from a bool-returning function with
an out-param to one that always invokes a continuation: either it
finishes the verification itself (cache hit, negative entry, etc.)
or it invokes the supplied fallback callback (cache miss, password
mismatch with stale-cache eligibility, no cache configured).
Callers no longer need to handle the result themselves; the helper
decides and dispatches. This unifies how cache hits and misses are
handed back into the auth flow and is preparation for fixing the
worker path's stale-cache handling.
Add auth_request_verify_plain_internal_failure() as the fallback
callback for the INTERNAL_FAILURE/expired-cache path in
auth_request_verify_plain_passdb_callback(). The existing
auth_request_verify_plain_passdb() helper is reused as the fallback
for the initial verify path.
Move the post-cache-check passdb dispatch (state transition,
wanted_credentials_scheme reset, blocking/direct iface call) out of
auth_request_default_verify_plain_continue() into a static helper. Pure
refactoring with no behavior change.
Fred Morcos [Thu, 30 Apr 2026 15:31:47 +0000 (17:31 +0200)]
auth: Rename auth_request_verify_plain_callback() to auth_request_verify_plain_passdb_callback()
Pure rename. The callback is invoked from passdb verification paths
(both blocking-worker and direct), so the new name better describes
where it sits in the call chain and disambiguates it from
auth_request_verify_plain_callback_finish().