Aki Tuomi [Tue, 14 Jul 2026 05:55:27 +0000 (05:55 +0000)]
lib-smtp: address - Fix assert on empty broken localpart
smtp_address_parse_path_full() with an end pointer and
ALLOW_BAD_LOCALPART could leave the separator search pointer one before
the parse position for an empty address (e.g. a leading space),
underflowing into p_strdup_until() and aborting with an assertion. Clamp
the pointer to the parse position. Add a test covering this case.
Aki Tuomi [Tue, 14 Jul 2026 05:52:10 +0000 (05:52 +0000)]
submission-login: proxy - Fail cleanly on malformed AUTH reply
When the backend answered the proxied AUTH command with a malformed
status line, the AUTHENTICATE state broke out into the shared failure
block, which assumes proxy_reply was already created. That assumption
does not hold for an invalid line, so the login process aborted with an
assertion. Handle the invalid line as a protocol failure and return
immediately instead.
Aki Tuomi [Tue, 14 Jul 2026 05:47:11 +0000 (05:47 +0000)]
submission-login: proxy - Cap AUTH reply continuation lines
A malicious or compromised backend could answer the proxied AUTH command
with an endless multi-line reply, growing proxy_reply without bound until
the login process ran out of memory (only loosely bounded by the login
timeout). Limit the number of continuation lines and fail the proxy
connection with a protocol error when the cap is exceeded.
Markus Valentin [Mon, 13 Jul 2026 13:46:32 +0000 (15:46 +0200)]
lib: ostream-multiplex - Fix panic when destroying corked channel 0 after parent failed
If the parent ostream fails (e.g. EPIPE when the client disconnects)
while the multiplex has corked it for sending, the uncork at the end of
o_stream_multiplex_sendv() is a no-op on the failed stream, so
parent_corked stays TRUE. Destroying the still-corked channel 0
afterwards tried to transfer the cork back to the parent and panicked.
Fixes:
Panic: file ostream-multiplex.c: line 518 (o_stream_multiplex_try_destroy): assertion failed: (!mstream->parent_corked)
Timo Sirainen [Fri, 10 Jul 2026 07:08:48 +0000 (07:08 +0000)]
doveadm: fs delete - Fix crash with concurrent async deletes
When deleting multiple objects asynchronously (doveadm fs delete -n), an
error on one file could leave another file's async lookup still running
when it was deinitialized. The still-pending EAGAIN was masked by the
errored file's ret=-1, and the drain loop in
doveadm_fs_delete_async_finish() was gated on doveadm_exit_code==0, so it
skipped draining once any file failed. Deinitializing a file mid-lookup
tripped the object_id_lookup_state != RUNNING assert in fs-dictmap.
Track pending separately from ret so it's no longer masked, and always
drain pending async operations before deinit, regardless of errors. Also
deinit files that hit a terminal error immediately, so they aren't
retried - otherwise the retry restarts their async lookup and keeps the
drain loop pending forever.
rootvector2 [Thu, 28 May 2026 06:34:25 +0000 (12:04 +0530)]
lib-oauth2: jwt - Avoid use-after-realloc of jwt_node front pointer
oauth2_jwt_copy_fields() captures subroot from array_front(&nodes)
then calls array_append_space(&nodes) inside the inner loop. When the
append cannot extend the buffer in place it relocates it, so subroot
aliases the previous slot and later subroot->prefix / subroot->array
reads operate on the abandoned location. Refresh subroot after the
append.
Aki Tuomi [Mon, 13 Jul 2026 05:27:23 +0000 (05:27 +0000)]
config: Delay module dlclose() until after lib_deinit()
module_dir_unload() ran deinit() and dlclose()d config modules before
master_service_deinit()/lib_deinit(). If a module (e.g. the
managesieve settings plugin, which pulls in libdovecot-sieve.so and
registers an event category) got dlclose()d, its memory was unmapped
before lib_event_deinit()'s fuzzing-build category cleanup ran,
causing a crash on process exit.
Split module_dir_unload() into module_dir_deinit() (while lib is
still alive) followed by master_service_deinit(), and only then
dlclose() the modules via module_dir_unload().
Hector Cao [Sat, 11 Jul 2026 08:04:51 +0000 (10:04 +0200)]
lib-ssl-iostream: Don't call OPENSSL_cleanup() for openssl 4.0
With OpenSSL 4.0 the lib-dcrypt unit tests (test-crypto, test-stream)
segfault at process exit.
dovecot_openssl_common_global_ref() installs custom OpenSSL memory
functions via CRYPTO_set_mem_functions(); their code lives inside the
dynamically loaded OpenSSL/dcrypt module. It also passes
OPENSSL_INIT_NO_ATEXIT so that libcrypto does not register its own
atexit(OPENSSL_cleanup) handler.
dovecot_openssl_common_global_unref() then called OPENSSL_cleanup()
explicitly. On OpenSSL 4.0 this explicit cleanup re-arms an internal
exit-time cleanup that runs after main() returns [1], i.e. after the module
(and therefore the custom memory functions) has been dlclose()d, so
libcrypto calls the now-unmapped free() function and the process crashes
with SIGSEGV.
Aki Tuomi [Fri, 10 Jul 2026 07:49:32 +0000 (07:49 +0000)]
lib-http: chunked - Fix UB on chunk-size overflow
Overflow check ran after the shift, so chunk_size <<= 4 itself
overflowed once the top 4 bits were set, triggering UBSan. Check
before shifting instead, same as str_parse_uintmax_hex() in strnum.c.
Add a test case with an oversized chunk-size to catch this.
Timo Sirainen [Thu, 9 Jul 2026 20:29:29 +0000 (20:29 +0000)]
lib: ostream-multiplex - Fix busy-loop on a single sendv larger than IO_BLOCK_SIZE
o_stream_multiplex_ochannel_sendv() grows the effective avail to
IO_BLOCK_SIZE when the channel buffer is small and the parent has no
un-flushed data, so a max_buffer_size==0 parent (which always reports
avail==0) can still be written to. But the following "avail < total"
block then re-read avail from the real buffer (0 again) and returned 0,
discarding the grown value. When a single sendv was larger than
IO_BLOCK_SIZE this buffered nothing and returned 0, so the caller made
no forward progress.
With o_stream_send_istream() of an in-memory istream - which returns all
of its data in one read - a field larger than IO_BLOCK_SIZE is sent as
one oversized sendv. The channel kept accepting nothing while the parent
socket stayed writable, so the flush was rescheduled immediately and the
process busy-looped at ~100% CPU.
Timo Sirainen [Thu, 9 Jul 2026 11:51:02 +0000 (11:51 +0000)]
anvil: admin-client - Fix busy loop on disconnected admin socket
The connection destroy callback failed pending commands but never removed
the connection's io. The connection framework's connection_closed() calls
v.destroy() expecting the connection to be torn down, but the actual
io_remove() happens only in connection_deinit() via admin_client_unref()
when refcount drops to 0. Since the pool or kick target still holds a
reference, connection_deinit() was never reached.
The io therefore stayed registered on the dead fd and kept firing, calling
destroy repeatedly for the same connection and spinning anvil at ~90% CPU.
Disconnect the connection in the destroy callback so the io is removed and
conn.disconnected is set, matching admin_client_send_cmd()'s reconnect check.
Timo Sirainen [Thu, 2 Jul 2026 12:01:49 +0000 (12:01 +0000)]
imap: imap-state - Add unit tests for parser helpers
Test the seq-range/string import helpers (round-trip, truncation,
overflow rejection) and the compress/enabled-feature/searchres
state-block import handlers.
Timo Sirainen [Thu, 2 Jul 2026 12:01:37 +0000 (12:01 +0000)]
imap: imap-state - Expose parser helpers via imap-state-private.h
Add imap-state-private.h that declares the seq-range, string and
state-block import/export helpers, and make them non-static so they can
be unit tested. Rename all exposed functions to use the imap_state_
prefix for consistency.
Timo Sirainen [Wed, 8 Jul 2026 07:12:39 +0000 (07:12 +0000)]
doveadm: Use STREAM multiplex format at protocol v1.4
The doveadm server->client output used the deprecated PACKET multiplex
format. Bump the protocol to v1.4 and use the STREAM format when the client
is new enough to understand it (minor version >=
DOVEADM_PROTOCOL_MIN_VERSION_MULTIPLEX_STREAM); older clients still get
PACKET. The client istream side auto-detects the format, so no client change
is needed beyond advertising the newer version.
Timo Sirainen [Wed, 8 Jul 2026 07:12:39 +0000 (07:12 +0000)]
anvil: Use STREAM multiplex format instead of the deprecated PACKET
Both ends of an anvil connection are internal Dovecot processes from the
same build, so the format can be switched without protocol negotiation (the
istream side auto-detects the format anyway). Move off the deprecated PACKET
format to STREAM.
Timo Sirainen [Wed, 8 Jul 2026 07:12:39 +0000 (07:12 +0000)]
lib: ostream-multiplex - Mark the PACKET format deprecated
The original packet-based framing requires the parent ostream to buffer a
whole packet and cannot stream to a parent with max_buffer_size==0, so it
does not work with senders that stream directly to the socket. New code
should use the STREAM format; PACKET is kept only for backwards
compatibility and is being phased out.
Timo Sirainen [Wed, 8 Jul 2026 09:53:37 +0000 (09:53 +0000)]
lib: ostream-multiplex - Apply backpressure in the STREAM format
o_stream_multiplex_ochannel_sendv() always bumped the available space up
to IO_BLOCK_SIZE when the channel buffer was small, to batch tiny writes.
This ignored whether the parent could actually accept the data. A parent
with max_buffer_size==0 (as imap-fetch sets while streaming a body
straight to the socket) always reports avail==0, so the bump made the
channel keep accepting data unconditionally. Since o_stream_multiplex_sendv()
no longer gates the STREAM format on parent space, the data was buffered
into the parent without bound and writev() was retried on the full socket
in a tight loop - o_stream_send_istream() never got a 0 return to make it
stop and wait for the socket to become writable.
Only apply the IO_BLOCK_SIZE floor while the parent has no un-flushed
backlog, i.e. the socket is keeping up. Once the parent has buffered data
the socket is congested and the real avail==0 is honored, so the channel
sendv returns 0 and backpressure propagates to the caller.
Timo Sirainen [Wed, 8 Jul 2026 06:37:03 +0000 (06:37 +0000)]
lib: ostream-multiplex - Don't gate STREAM flushing on a max_buffer_size=0 parent
o_stream_multiplex_sendv() skipped sending a channel's buffered data unless
the parent stream reported at least 6 bytes of available space (room for a
frame header). A parent with max_buffer_size==0 always reports
get_buffer_avail_size()==0, so this gate blocks every send to such a parent.
The gate is only correct for the PACKET format, which writes a whole header
and its data with a single sendv and can't cope with a partial parent write.
The STREAM format sends incrementally through pending_buf and already handles
partial parent writes, so it must attempt the send regardless; a full or
streaming parent then applies backpressure naturally through short writes.
imap-fetch sets the output's max_buffer_size to 0 around the send_istream()
that streams a body, so that it goes straight to the socket instead of being
buffered. When the output is an ostream-multiplex channel - as with the
imap-login proxy that multiplexes the backend connection - that 0 reaches the
multiplex parent and the gate stops STREAM from writing through it while it is
set. Under the login-proxy relay this wedged the connection (observed as
either a CPU busy-loop or an idle stall) and the client timed out.
Apply the available-space gate only for the PACKET format. Also assert that a
gated PACKET parent can actually buffer a header, so an unsupported
max_buffer_size==0 PACKET parent fails loudly instead of hanging.
Timo Sirainen [Wed, 10 Jun 2026 22:27:09 +0000 (22:27 +0000)]
lib-auth-client: Use master_service_ioloop_run_interruptible()
auth_master_request_wait() and auth_master_wait() both hand-rolled the same
kill-poll pattern: a 100ms timeout whose handler checked
master_service_is_killed() and stopped the ioloop. Replace both with
master_service_ioloop_run_interruptible().
Marco Bettini [Tue, 7 Jul 2026 10:24:30 +0000 (10:24 +0000)]
auth: Pass resolved credentials scheme via lookup_credentials callback
passdb_get_credentials() mutated request->wanted_credentials_scheme to
report the resolved scheme, letting it leak into the next passdb
during a multi-passdb continue lookup and wrongly reject a different
scheme there as SCHEME_NOT_AVAILABLE. Pass the resolved scheme as a
callback parameter instead of mutating shared request state.
wanted_credentials_scheme stays immutable.
Timo Sirainen [Sun, 28 Jun 2026 11:56:08 +0000 (11:56 +0000)]
lib-var-expand: Add iso8601 filter
Add an iso8601 filter that formats a UNIX timestamp as an ISO 8601 /
RFC 3339 date-time string, e.g. 2025-06-08T10:40:00Z. The tz parameter
selects the timezone: utc (alias gmt, the default), which uses the 'Z'
suffix, or local, which uses a +HH:MM / -HH:MM offset. This produces
RFC 3339 compliant output, which the strftime-based date filter cannot
(strftime's %z lacks the colon and cannot emit 'Z').
Reuses iso8601_date_create_tm() and utc_offset() from lib.
Timo Sirainen [Sun, 28 Jun 2026 08:24:49 +0000 (08:24 +0000)]
lib-var-expand: Add date filter
Add a date filter that formats a UNIX timestamp using strftime(3). The
input may be a plain seconds value or the "<seconds>.<nanoseconds>" form
from the time:unix provider; any fractional part is ignored. The tz
parameter selects the timezone used to break the timestamp into calendar
fields: utc (alias gmt, the default) or local.
Timo Sirainen [Sun, 28 Jun 2026 08:23:45 +0000 (08:23 +0000)]
lib-var-expand: Add from_epoch filter
Add a from_epoch filter, the inverse of epoch: it converts an integer
UNIX timestamp expressed in s/ms/us/ns into the canonical
"<seconds>.<nanoseconds>" form. This is useful for feeding millisecond
or nanosecond timestamps from external input into the date filter.
Timo Sirainen [Sun, 28 Jun 2026 08:22:56 +0000 (08:22 +0000)]
lib-var-expand: Add epoch filter
Add an epoch filter that converts a UNIX timestamp into an integer in
the requested unit (s, ms, us or ns). The input may be a plain seconds
value or the "<seconds>.<nanoseconds>" form returned by the time:unix
provider. The output never contains a decimal point.
Timo Sirainen [Sun, 28 Jun 2026 08:21:40 +0000 (08:21 +0000)]
lib-var-expand: Add time:unix provider
Add a new %{time:unix} provider that returns the current time as a
"<seconds>.<nanoseconds>" UNIX timestamp. This is intended as the input
for the epoch and date filters.
last-login: Add support for updating multiple dictionary fields
Extend the last-login plugin to support a new 'last_login_dict_fields'
setting. This allows users to specify a list of key/value pairs (with
variable expansion support) to be updated during the login process.
Timo Sirainen [Mon, 6 Jul 2026 21:14:06 +0000 (21:14 +0000)]
lib-dict-extra: Increase dict client request timeout to 65s
The dict client request timeout was 30 seconds, which is lower than the
backend SQL/Cassandra request timeout default of 60 seconds
(SQL_QUERY_TIMEOUT_SECS). A slow backend query would therefore hit the
dict client timeout first, logging a "Dict server timeout" warning and
forcing a reconnect, instead of letting the backend abort the query with
a proper error.
Raise the timeout to 65 seconds so it sits above the backend timeouts.
Timo Sirainen [Fri, 3 Jul 2026 13:04:53 +0000 (13:04 +0000)]
lib-storage: Fix crash when reverting a failed mailbox deletion
If a backend's delete_box fails after the mailbox was marked deleted,
mailbox_delete_real() reverts the mark by calling mailbox_mark_index_deleted()
which reopens the mailbox. That open can fail before the index is opened,
leaving box->index NULL, and mailbox_try_undelete() then dereferenced it in
mail_index_get_modification_time(), crashing.
Return FALSE from mailbox_try_undelete() when box->index is NULL - there is
nothing to undelete.
Timo Sirainen [Fri, 3 Jul 2026 11:33:31 +0000 (11:33 +0000)]
fts: Don't run FTS optimize on forced resync
This was an obsolete design decision, which is only making force-resyncs
slower and can cause them to fail. FTS optimization has nothing to do with
index rebuilds.
Timo Sirainen [Mon, 6 Jul 2026 16:19:16 +0000 (16:19 +0000)]
lib-storage: Fix NFC subscription rewrite for mUTF7 storage names
mailbox_list_subscriptions_refresh() normalized the raw subscription file
entry, which is a storage name (e.g. mUTF7). With mUTF7 the non-NFC
characters are hidden inside the encoding and the storage name looks like
plain ASCII, so uni_utf8_to_nfc() was a no-op and the subscription was
never rewritten to NFC form.
The mailbox directory itself does get renamed to NFC during listing (that
code normalizes the decoded vname), leaving the subscription pointing at a
non-existent non-NFC name. This triggered the NFC rename fallback on every
iteration: repeated 'Failed to rename mailbox for NFC normalization:
Mailbox doesn't exist' errors and a bogus GUID-suffixed subscription entry.
Normalize the decoded vname instead and convert the result back to a
storage name, matching what mailbox listing does.
Timo Sirainen [Fri, 3 Jul 2026 16:01:08 +0000 (16:01 +0000)]
imapc: Fix COPY checking expunged state of wrong mailbox
imapc_copy() checked imapc_is_mail_expunged() against the destination
mailbox (ctx->mbox) while passing a source UID. The check looks up the
UID in the given mailbox's delayed_expunged_uids / delayed_sync_trans,
i.e. in that mailbox's own UID space, so it must be given the source
mailbox (ctx->src_mbox).
Because of this, a source mail whose EXPUNGE had already been received
but not yet applied to the msgmap was not detected. The message map
still contained the UID, so the copy proceeded and a UID COPY was sent
for an already-expunged mail. The remote server then silently skipped
it (UID COPY ignores non-existent UIDs rather than failing), returning
fewer COPYUIDs than mails requested. That mismatch tripped the
all-or-nothing saved_uids assertion in
mailbox_transaction_commit_get_changes() and crashed imap. This showed
up as random failures in the obox-tests/imapc-shared-folder test.
Check the source mailbox so such mails are detected up front and the
copy is aborted with MAIL_ERROR_EXPUNGED before any UID COPY is sent.
Timo Sirainen [Thu, 2 Jul 2026 08:24:51 +0000 (08:24 +0000)]
lib-sql: Make sqlite busy_timeout configurable
Add sqlite_busy_timeout setting (default 1s, unchanged from the previous
hardcoded value). Under WAL only one writer runs at a time, so concurrent
writers can still hit SQLITE_BUSY; a configurable timeout lets deployments
with slow storage widen the retry window.
lib-imap: imap-bodystructure - Don't read past args in message/rfc822 parsing
The message/rfc822 branch of imap_bodystructure_parse_args_int()
dereferences the child bodystructure at args[1] before it validates the
envelope at args[0]. A BODYSTRUCTURE truncated right after the size field,
e.g. ("message" "rfc822" NIL NIL NIL "7bit" 0), leaves args pointing at
the argument list's IMAP_ARG_EOL terminator, so reading args[1] indexes one
struct imap_arg past the end of the EOL-terminated array.
Bail out when args[0] is IMAP_ARG_EOL before indexing args[1], matching the
ordering already used by the sibling imap_parse_bodystructure_args(). This is
reachable via imapc from an upstream server's FETCH BODYSTRUCTURE reply.