diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 18170e4a3..dcae97f02 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -39,7 +39,7 @@ jobs: - name: install packaging tools run: | apt-get update -q - apt-get install -y --no-install-recommends ca-certificates git git-buildpackage build-essential fakeroot rsync openssh-client + apt-get install -y --no-install-recommends ca-certificates git git-buildpackage devscripts build-essential fakeroot rsync openssh-client - uses: actions/checkout@v7 with: @@ -47,6 +47,31 @@ jobs: show-progress: false fetch-depth: 0 + # Add a distro-distinct version onto the changelog. The committed top + # entry is UNRELEASED with a distro-neutral base version (1:...+chatmail2); + # here dch finalizes it in place with a per-distro version and codename, + # so the bookworm and trixie build into separate versions and allow a + # combined release without a manual rename. Follow Debian's own security + # convention (+deb12uN / +deb13uN), which sorts above the neutral base + # and is necessary for dch to accept it as an upgrade. In the build step, + # the version extraction for the tarball is unaffected by this, and gbp + # picks up the version from the changelog for the build. + - name: finalize changelog for ${{ inputs.distro }} + working-directory: dovecot + env: + DEBFULLNAME: chatmail CI + DEBEMAIL: ci@chatmail.invalid + run: | + case "${{ inputs.distro }}" in + bookworm) suffix=deb12u1 ;; + trixie) suffix=deb13u1 ;; + *) echo "unknown distro '${{ inputs.distro }}'" >&2; exit 1 ;; + esac + base=$(dpkg-parsechangelog -SVersion) + dch -v "${base}+${suffix}" --distribution "${{ inputs.distro }}" \ + --force-distribution "Rebuild for ${{ inputs.distro }}." + dpkg-parsechangelog | sed -n '1,3p' + - name: install build dependencies run: apt-get build-dep -y --no-install-recommends ./dovecot diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a9aff8af0..636d99f90 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,32 +65,30 @@ jobs: apt-get update -q apt-get install -y --no-install-recommends gh version=$(dpkg-parsechangelog -l build-area/dovecot_*.dsc -Sversion | sed 's/^1://') - tag="upstream/${version}" - for f in build-area/dovecot-core_*.deb \ - build-area/dovecot-imapd_*.deb \ - build-area/dovecot-lmtpd_*.deb; do - mv "$f" "${f%.deb}_${{ matrix.distro }}.deb" - done - for f in build-area/dovecot_*.buildinfo; do - mv "$f" "${f%.buildinfo}_${{ matrix.distro }}.buildinfo" - done + # The build stamps a distro-distinct version onto the changelog + # (+deb12u1 / +deb13u1). The GitHub release itself + # stays combined across the matrix, keyed on the neutral base + # version with that stamp stripped back off. + base_version=$(echo "$version" | sed -E 's/\+deb[0-9]+u[0-9]+$//') + tag="upstream/${base_version}" gh release create "$tag" --title "$tag" || true gh release upload "$tag" --clobber \ - build-area/dovecot-core_*_${{ matrix.distro }}.deb \ - build-area/dovecot-imapd_*_${{ matrix.distro }}.deb \ - build-area/dovecot-lmtpd_*_${{ matrix.distro }}.deb \ - build-area/dovecot_*_${{ matrix.distro }}.buildinfo + build-area/dovecot-core_*.deb \ + build-area/dovecot-imapd_*.deb \ + build-area/dovecot-lmtpd_*.deb \ + build-area/dovecot_*.buildinfo env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Sources are byte-identical across the matrix (dpkg-source is - # deterministic on the same tag), so only one job uploads them to - # avoid the other three racing on the same object names. + # The changelog is stamped per-distro before the build so the source + # package differs and must be uploaded once per distro. Arch doesn't + # affect the source, so only one arch per distro uploads it. - name: upload source to github release - if: matrix.distro == 'trixie' && matrix.arch == 'amd64' + if: matrix.arch == 'amd64' run: | version=$(dpkg-parsechangelog -l build-area/dovecot_*.dsc -Sversion | sed 's/^1://') - tag="upstream/${version}" + base_version=$(echo "$version" | sed -E 's/\+deb[0-9]+u[0-9]+$//') + tag="upstream/${base_version}" gh release upload "$tag" --clobber \ build-area/dovecot_*.dsc \ build-area/dovecot_*.orig.tar.* \ diff --git a/debian/changelog b/debian/changelog index 6f02df43f..861260bfc 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,54 @@ +dovecot (1:2.3.21+dfsg1-3+chatmail2) UNRELEASED; urgency=medium + + * Backport 14 Debian security fixes from bookworm-security (2.3.19.1 + base, deb12u1-u6, tip b0beae072) onto our 2.3.21 rebuild, which shared no + history with the backport work and previously carried none of them. The + deb12u3/u4 regression fixes and deb12u5 corrections are already folded + into the copied files (single CVE-2025-59032.patch; CVE-2026-0394-2.patch + is the passwd-file regression fix; -27855-4 and -27857-5 are the u5 + corrections). All 26 patch files apply to 2.3.21 with offsets only, no + fuzz and no rejects. + - CVE-2024-23184: many address headers cause excessive CPU use during + message parsing. (deb12u1) + - CVE-2024-23185: oversized headers cause excessive memory/CPU use in the + header parser. (deb12u1) + - CVE-2025-59031: remove the FTS decode2text.sh helper; we shipped it only + as a doc example, and d/rules and d/dovecot-core.examples are updated to + drop the now-missing file. (deb12u2) + - CVE-2025-59032: managesieve-login crash on an unfinished command + pre-auth. (deb12u2) + - CVE-2026-0394: passwd-file passdb path normalization. (deb12u2) + - CVE-2026-27855: auth cache used the untranslated username when removing + an entry; align it with the lookup/insert paths, plus supporting passdb + event-lifecycle refactor and an empty-update_query guard. (deb12u2) + - CVE-2026-27856: doveadm HTTP/TCP API-key comparison timing side channel. + (deb12u2) + - CVE-2026-27857: unbounded IMAP parser list nesting is a pre-auth memory + DoS; add imap_parser_params.list_count_limit and cap the pre-login + parser to one open list. (deb12u2) + - CVE-2026-27858: managesieve-login accepted an unverified SASL initial + response. (deb12u2) + - CVE-2026-27859: RFC2231 continuation-parameter parsing DoS. (deb12u2) + - CVE-2026-33603: login-common accepted non-base64 bytes (e.g. TAB) in a + SASL continuation response, injecting fields into the auth protocol. + (deb12u6) + - CVE-2026-40016: Sieve :contains/:matches match could bypass the CPU + limit. (deb12u6) + - CVE-2026-40020: invalid ACL identifier handling in acl/imap-acl. + (deb12u6) + - CVE-2026-42006: the CVE-2026-27857 limit counted ')' not '('; move the + check into imap_parser_open_list() so it actually bounds nesting. + (deb12u6) + * d/tests: converge with bookworm-security, adding the regression tests that + ship with the fixes: usage/imap-cram-md5, usage/sieve, and the + debian_bug_1134464 passwd-file delivery test (guards CVE-2026-0394); + extend the usage Depends with dovecot-managesieved and sieve-connect. + * CI: emit a distro-distinct package version per stable target. The base + version here stays UNRELEASED; .github/workflows/pipeline.yml finalizes it + to +deb12u1 (bookworm) or +deb13u1 (trixie) at build time. + + -- j4n Tue, 21 Jul 2026 09:58:59 +0200 + dovecot (1:2.3.21+dfsg1-3+chatmail1) unstable; urgency=medium * Introduce subversion for chatmail relay downstream rebuild of Debian's diff --git a/debian/dovecot-core.examples b/debian/dovecot-core.examples deleted file mode 100644 index 80af4bc1b..000000000 --- a/debian/dovecot-core.examples +++ /dev/null @@ -1 +0,0 @@ -src/plugins/fts/decode2text.sh diff --git a/debian/patches/CVE-2024-23184.patch b/debian/patches/CVE-2024-23184.patch new file mode 100644 index 000000000..84f2b5252 --- /dev/null +++ b/debian/patches/CVE-2024-23184.patch @@ -0,0 +1,975 @@ +Author: Timo Sirainen +Description: Upstream fix for CVE-2024-23184 +Origin: https://github.com/dovecot/core/compare/8e4c42d%5E...1481c04.patch + +--- + src/lib/test-llist.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/lib/test-llist.c b/src/lib/test-llist.c +index d57006ce2aa..ed584318fa3 100644 +--- a/src/lib/test-llist.c ++++ b/src/lib/test-llist.c +@@ -71,7 +71,7 @@ static void test_dllist2(void) + l2 = t_new(struct dllist, 1); + l1 = t_new(struct dllist, 1); + +- test_begin("dllist"); ++ test_begin("dllist2"); + /* prepend to empty */ + DLLIST2_PREPEND(&head, &tail, l3); + test_assert(head == l3 && tail == l3); + +From cee08202c759a3bdf185d998dcf888ebd1bc6e36 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 9 Feb 2024 00:33:00 +0200 +Subject: [PATCH 2/6] lib: Add DLLIST2_JOIN() + +--- + src/lib/llist.h | 14 ++++++++++++++ + src/lib/test-llist.c | 39 +++++++++++++++++++++++++++++++++++++++ + 2 files changed, 53 insertions(+) + +diff --git a/src/lib/llist.h b/src/lib/llist.h +index 8a52e873352..5ad5d75c0df 100644 +--- a/src/lib/llist.h ++++ b/src/lib/llist.h +@@ -78,4 +78,18 @@ + #define DLLIST2_REMOVE(head, tail, item) \ + DLLIST2_REMOVE_FULL(head, tail, item, prev, next) + ++#define DLLIST2_JOIN_FULL(head1, tail1, head2, tail2, prev, next) STMT_START { \ ++ if (*(head1) == NULL) { \ ++ *(head1) = *(head2); \ ++ *(tail1) = *(tail2); \ ++ } else if (*(head2) != NULL) { \ ++ (*(tail1))->next = *(head2); \ ++ (*(head2))->prev = *(tail1); \ ++ (*tail1) = (*tail2); \ ++ } \ ++ } STMT_END ++ ++#define DLLIST2_JOIN(head1, tail1, head2, tail2) \ ++ DLLIST2_JOIN_FULL(head1, tail1, head2, tail2, prev, next) ++ + #endif +diff --git a/src/lib/test-llist.c b/src/lib/test-llist.c +index ed584318fa3..e293eb6a603 100644 +--- a/src/lib/test-llist.c ++++ b/src/lib/test-llist.c +@@ -131,8 +131,47 @@ static void test_dllist2(void) + test_end(); + } + ++static void test_dllist2_join(void) ++{ ++ struct dllist *head, *tail, *elem[4]; ++ struct dllist *head2, *tail2, *elem2[N_ELEMENTS(elem)]; ++ ++ test_begin("dllist2 join"); ++ for (unsigned int i = 0; i < N_ELEMENTS(elem); i++) { ++ elem[i] = t_new(struct dllist, 1); ++ elem2[i] = t_new(struct dllist, 1); ++ } ++ for (unsigned int i = 0; i < N_ELEMENTS(elem); i++) { ++ for (unsigned int j = 0; j < N_ELEMENTS(elem2); j++) { ++ head = tail = head2 = tail2 = NULL; ++ for (unsigned int n = 0; n < i; n++) ++ DLLIST2_APPEND(&head, &tail, elem[n]); ++ for (unsigned int n = 0; n < j; n++) ++ DLLIST2_APPEND(&head2, &tail2, elem2[n]); ++ DLLIST2_JOIN(&head, &tail, &head2, &tail2); ++ ++ /* verify */ ++ struct dllist *tmp = head, *last = NULL; ++ for (unsigned int n = 0; n < i; n++) { ++ test_assert(tmp == elem[n]); ++ last = tmp; ++ tmp = tmp->next; ++ } ++ for (unsigned int n = 0; n < j; n++) { ++ test_assert(tmp == elem2[n]); ++ last = tmp; ++ tmp = tmp->next; ++ } ++ test_assert(tmp == NULL); ++ test_assert(tail == last); ++ } ++ } ++ test_end(); ++} ++ + void test_llist(void) + { + test_dllist(); + test_dllist2(); ++ test_dllist2_join(); + } + +From 0bae091859c905dc335f21eed01347e6b8338672 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 30 Jan 2024 22:42:50 +0200 +Subject: [PATCH 3/6] lib-mail: test-imap-envelope - Use test_assert_idx() + where possible + +--- + src/lib-imap/test-imap-envelope.c | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/src/lib-imap/test-imap-envelope.c b/src/lib-imap/test-imap-envelope.c +index 1f295e58bab..c9b92b4be2b 100644 +--- a/src/lib-imap/test-imap-envelope.c ++++ b/src/lib-imap/test-imap-envelope.c +@@ -157,7 +157,7 @@ static void test_imap_envelope_write(void) + envlp = msg_parse(pool, test->message); + + imap_envelope_write(envlp, str); +- test_assert(strcmp(str_c(str), test->envelope) == 0); ++ test_assert_idx(strcmp(str_c(str), test->envelope) == 0, i); + + pool_unref(&pool); + test_end(); +@@ -179,12 +179,12 @@ static void test_imap_envelope_parse(void) + test_begin(t_strdup_printf("imap envelope parser [%u]", i)); + + ret = imap_envelope_parse(test->envelope, pool, &envlp, &error); +- test_assert(ret); ++ test_assert_idx(ret, i); + + if (ret) { + str_truncate(str, 0); + imap_envelope_write(envlp, str); +- test_assert(strcmp(str_c(str), test->envelope) == 0); ++ test_assert_idx(strcmp(str_c(str), test->envelope) == 0, i); + } else { + i_error("Invalid envelope: %s", error); + } + +From a1c9b0409454e45937bf7e9c3685f5e91d6a5a43 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Sun, 4 Feb 2024 00:26:57 +0200 +Subject: [PATCH 4/6] lib-mail: Change message_address to be doubly linked list + +--- + src/lib-imap/imap-envelope.c | 11 +- + src/lib-mail/message-address.c | 8 +- + src/lib-mail/message-address.h | 2 +- + src/lib-mail/test-message-address.c | 226 ++++++++++++++-------------- + 4 files changed, 121 insertions(+), 126 deletions(-) + +diff --git a/src/lib-imap/imap-envelope.c b/src/lib-imap/imap-envelope.c +index 87297f4f691..1312eae2ff3 100644 +--- a/src/lib-imap/imap-envelope.c ++++ b/src/lib-imap/imap-envelope.c +@@ -1,6 +1,7 @@ + /* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + + #include "lib.h" ++#include "llist.h" + #include "istream.h" + #include "str.h" + #include "message-address.h" +@@ -127,7 +128,7 @@ static bool + imap_envelope_parse_addresses(const struct imap_arg *arg, + pool_t pool, struct message_address **addrs_r) + { +- struct message_address *first, *addr, *prev; ++ struct message_address *first, *last, *addr; + const struct imap_arg *list_args; + + if (arg->type == IMAP_ARG_NIL) { +@@ -138,16 +139,12 @@ imap_envelope_parse_addresses(const struct imap_arg *arg, + if (!imap_arg_get_list(arg, &list_args)) + return FALSE; + +- first = addr = prev = NULL; ++ first = last = addr = NULL; + for (; !IMAP_ARG_IS_EOL(list_args); list_args++) { + if (!imap_envelope_parse_address + (list_args, pool, &addr)) + return FALSE; +- if (first == NULL) +- first = addr; +- if (prev != NULL) +- prev->next = addr; +- prev = addr; ++ DLLIST2_APPEND(&first, &last, addr); + } + + *addrs_r = first; +diff --git a/src/lib-mail/message-address.c b/src/lib-mail/message-address.c +index fb06afae7b7..9d192799468 100644 +--- a/src/lib-mail/message-address.c ++++ b/src/lib-mail/message-address.c +@@ -1,6 +1,7 @@ + /* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + + #include "lib.h" ++#include "llist.h" + #include "str.h" + #include "strescape.h" + #include "smtp-address.h" +@@ -27,11 +28,7 @@ static void add_address(struct message_address_parser_context *ctx) + memcpy(addr, &ctx->addr, sizeof(ctx->addr)); + i_zero(&ctx->addr); + +- if (ctx->first_addr == NULL) +- ctx->first_addr = addr; +- else +- ctx->last_addr->next = addr; +- ctx->last_addr = addr; ++ DLLIST2_APPEND(&ctx->first_addr, &ctx->last_addr, addr); + } + + /* quote with "" and escape all '\', '"' and "'" characters if need */ +@@ -631,6 +628,7 @@ const char *message_address_first_to_string(const struct message_address *addr) + struct message_address first_addr; + + first_addr = *addr; ++ first_addr.prev = NULL; + first_addr.next = NULL; + first_addr.route = NULL; + return message_address_to_string(&first_addr); +diff --git a/src/lib-mail/message-address.h b/src/lib-mail/message-address.h +index 8370397741c..85cff3dcc6f 100644 +--- a/src/lib-mail/message-address.h ++++ b/src/lib-mail/message-address.h +@@ -18,7 +18,7 @@ enum message_address_parse_flags { + {name = NULL, NULL, "group", NULL}, ..., {NULL, NULL, NULL, NULL} + */ + struct message_address { +- struct message_address *next; ++ struct message_address *prev, *next; + + /* display-name */ + const char *name; +diff --git a/src/lib-mail/test-message-address.c b/src/lib-mail/test-message-address.c +index e6204bb0588..261cbfba70a 100644 +--- a/src/lib-mail/test-message-address.c ++++ b/src/lib-mail/test-message-address.c +@@ -47,174 +47,174 @@ static void test_message_address(void) + } tests[] = { + /* user@domain -> */ + { "user@domain", "", NULL, +- { NULL, NULL, NULL, "user", "domain", FALSE }, +- { NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, + { "\"user\"@domain", "", NULL, +- { NULL, NULL, NULL, "user", "domain", FALSE }, +- { NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, + { "\"user name\"@domain", "<\"user name\"@domain>", NULL, +- { NULL, NULL, NULL, "user name", "domain", FALSE }, +- { NULL, NULL, NULL, "user name", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, NULL, "user name", "domain", FALSE }, ++ { NULL, NULL, NULL, NULL, "user name", "domain", FALSE }, 0 }, + { "\"user@na\\\\me\"@domain", "<\"user@na\\\\me\"@domain>", NULL, +- { NULL, NULL, NULL, "user@na\\me", "domain", FALSE }, +- { NULL, NULL, NULL, "user@na\\me", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, NULL, "user@na\\me", "domain", FALSE }, ++ { NULL, NULL, NULL, NULL, "user@na\\me", "domain", FALSE }, 0 }, + { "\"user\\\"name\"@domain", "<\"user\\\"name\"@domain>", NULL, +- { NULL, NULL, NULL, "user\"name", "domain", FALSE }, +- { NULL, NULL, NULL, "user\"name", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, NULL, "user\"name", "domain", FALSE }, ++ { NULL, NULL, NULL, NULL, "user\"name", "domain", FALSE }, 0 }, + { "\"\"@domain", "<\"\"@domain>", NULL, +- { NULL, NULL, NULL, "", "domain", FALSE }, +- { NULL, NULL, NULL, "", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, NULL, "", "domain", FALSE }, ++ { NULL, NULL, NULL, NULL, "", "domain", FALSE }, 0 }, + { "user", "", "", +- { NULL, NULL, NULL, "user", "", TRUE }, +- { NULL, NULL, NULL, "user", "MISSING_DOMAIN", TRUE }, 0 }, ++ { NULL, NULL, NULL, NULL, "user", "", TRUE }, ++ { NULL, NULL, NULL, NULL, "user", "MISSING_DOMAIN", TRUE }, 0 }, + { "@domain", "<\"\"@domain>", "", +- { NULL, NULL, NULL, "", "domain", TRUE }, +- { NULL, NULL, NULL, "MISSING_MAILBOX", "domain", TRUE }, 0 }, ++ { NULL, NULL, NULL, NULL, "", "domain", TRUE }, ++ { NULL, NULL, NULL, NULL, "MISSING_MAILBOX", "domain", TRUE }, 0 }, + + /* Display Name -> Display Name */ + { "Display Name", "\"Display Name\"", "\"Display Name\" ", +- { NULL, "Display Name", NULL, "", "", TRUE }, +- { NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, ++ { NULL, NULL, "Display Name", NULL, "", "", TRUE }, ++ { NULL, NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, + { "\"Display Name\"", "\"Display Name\"", "\"Display Name\" ", +- { NULL, "Display Name", NULL, "", "", TRUE }, +- { NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, ++ { NULL, NULL, "Display Name", NULL, "", "", TRUE }, ++ { NULL, NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, + { "Display \"Name\"", "\"Display Name\"", "\"Display Name\" ", +- { NULL, "Display Name", NULL, "", "", TRUE }, +- { NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, ++ { NULL, NULL, "Display Name", NULL, "", "", TRUE }, ++ { NULL, NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, + { "\"Display\" \"Name\"", "\"Display Name\"", "\"Display Name\" ", +- { NULL, "Display Name", NULL, "", "", TRUE }, +- { NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, ++ { NULL, NULL, "Display Name", NULL, "", "", TRUE }, ++ { NULL, NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, + { "\"\"", "", "", +- { NULL, "", NULL, "", "", TRUE }, +- { NULL, "", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, ++ { NULL, NULL, "", NULL, "", "", TRUE }, ++ { NULL, NULL, "", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, + + /* -> */ + { "", NULL, NULL, +- { NULL, NULL, NULL, "user", "domain", FALSE }, +- { NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, + { "<\"user\"@domain>", "", NULL, +- { NULL, NULL, NULL, "user", "domain", FALSE }, +- { NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, + { "<\"user name\"@domain>", NULL, NULL, +- { NULL, NULL, NULL, "user name", "domain", FALSE }, +- { NULL, NULL, NULL, "user name", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, NULL, "user name", "domain", FALSE }, ++ { NULL, NULL, NULL, NULL, "user name", "domain", FALSE }, 0 }, + { "<\"user@na\\\\me\"@domain>", NULL, NULL, +- { NULL, NULL, NULL, "user@na\\me", "domain", FALSE }, +- { NULL, NULL, NULL, "user@na\\me", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, NULL, "user@na\\me", "domain", FALSE }, ++ { NULL, NULL, NULL, NULL, "user@na\\me", "domain", FALSE }, 0 }, + { "<\"user\\\"name\"@domain>", NULL, NULL, +- { NULL, NULL, NULL, "user\"name", "domain", FALSE }, +- { NULL, NULL, NULL, "user\"name", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, NULL, "user\"name", "domain", FALSE }, ++ { NULL, NULL, NULL, NULL, "user\"name", "domain", FALSE }, 0 }, + { "<\"\"@domain>", NULL, NULL, +- { NULL, NULL, NULL, "", "domain", FALSE }, +- { NULL, NULL, NULL, "", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, NULL, "", "domain", FALSE }, ++ { NULL, NULL, NULL, NULL, "", "domain", FALSE }, 0 }, + { "", NULL, "", +- { NULL, NULL, NULL, "user", "", TRUE }, +- { NULL, NULL, NULL, "user", "MISSING_DOMAIN", TRUE }, 0 }, ++ { NULL, NULL, NULL, NULL, "user", "", TRUE }, ++ { NULL, NULL, NULL, NULL, "user", "MISSING_DOMAIN", TRUE }, 0 }, + { "<@route>", "<@route:\"\">", "", +- { NULL, NULL, "@route", "", "", TRUE }, +- { NULL, NULL, "INVALID_ROUTE", "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, ++ { NULL, NULL, NULL, "@route", "", "", TRUE }, ++ { NULL, NULL, NULL, "INVALID_ROUTE", "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, + + /* user@domain (Display Name) -> "Display Name" */ + { "user@domain (DisplayName)", "DisplayName ", NULL, +- { NULL, "DisplayName", NULL, "user", "domain", FALSE }, +- { NULL, "DisplayName", NULL, "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, "DisplayName", NULL, "user", "domain", FALSE }, ++ { NULL, NULL, "DisplayName", NULL, "user", "domain", FALSE }, 0 }, + { "user@domain (Display Name)", "\"Display Name\" ", NULL, +- { NULL, "Display Name", NULL, "user", "domain", FALSE }, +- { NULL, "Display Name", NULL, "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, "Display Name", NULL, "user", "domain", FALSE }, ++ { NULL, NULL, "Display Name", NULL, "user", "domain", FALSE }, 0 }, + { "user@domain (Display\"Name)", "\"Display\\\"Name\" ", NULL, +- { NULL, "Display\"Name", NULL, "user", "domain", FALSE }, +- { NULL, "Display\"Name", NULL, "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, "Display\"Name", NULL, "user", "domain", FALSE }, ++ { NULL, NULL, "Display\"Name", NULL, "user", "domain", FALSE }, 0 }, + { "user (Display Name)", "\"Display Name\" ", "\"Display Name\" ", +- { NULL, "Display Name", NULL, "user", "", TRUE }, +- { NULL, "Display Name", NULL, "user", "MISSING_DOMAIN", TRUE }, 0 }, ++ { NULL, NULL, "Display Name", NULL, "user", "", TRUE }, ++ { NULL, NULL, "Display Name", NULL, "user", "MISSING_DOMAIN", TRUE }, 0 }, + { "@domain (Display Name)", "\"Display Name\" <\"\"@domain>", "\"Display Name\" ", +- { NULL, "Display Name", NULL, "", "domain", TRUE }, +- { NULL, "Display Name", NULL, "MISSING_MAILBOX", "domain", TRUE }, 0 }, ++ { NULL, NULL, "Display Name", NULL, "", "domain", TRUE }, ++ { NULL, NULL, "Display Name", NULL, "MISSING_MAILBOX", "domain", TRUE }, 0 }, + { "user@domain ()", "", NULL, +- { NULL, NULL, NULL, "user", "domain", FALSE }, +- { NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, + + /* Display Name -> "Display Name" */ + { "DisplayName ", NULL, NULL, +- { NULL, "DisplayName", NULL, "user", "domain", FALSE }, +- { NULL, "DisplayName", NULL, "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, "DisplayName", NULL, "user", "domain", FALSE }, ++ { NULL, NULL, "DisplayName", NULL, "user", "domain", FALSE }, 0 }, + { "Display Name ", "\"Display Name\" ", NULL, +- { NULL, "Display Name", NULL, "user", "domain", FALSE }, +- { NULL, "Display Name", NULL, "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, "Display Name", NULL, "user", "domain", FALSE }, ++ { NULL, NULL, "Display Name", NULL, "user", "domain", FALSE }, 0 }, + { "\"Display Name\" ", NULL, NULL, +- { NULL, "Display Name", NULL, "user", "domain", FALSE }, +- { NULL, "Display Name", NULL, "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, "Display Name", NULL, "user", "domain", FALSE }, ++ { NULL, NULL, "Display Name", NULL, "user", "domain", FALSE }, 0 }, + { "\"Display\\\"Name\" ", NULL, NULL, +- { NULL, "Display\"Name", NULL, "user", "domain", FALSE }, +- { NULL, "Display\"Name", NULL, "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, "Display\"Name", NULL, "user", "domain", FALSE }, ++ { NULL, NULL, "Display\"Name", NULL, "user", "domain", FALSE }, 0 }, + { "Display Name ", "\"Display Name\" ", "\"Display Name\" ", +- { NULL, "Display Name", NULL, "user", "", TRUE }, +- { NULL, "Display Name", NULL, "user", "MISSING_DOMAIN", TRUE }, 0 }, ++ { NULL, NULL, "Display Name", NULL, "user", "", TRUE }, ++ { NULL, NULL, "Display Name", NULL, "user", "MISSING_DOMAIN", TRUE }, 0 }, + { "\"\" ", "", NULL, +- { NULL, NULL, NULL, "user", "domain", FALSE }, +- { NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE }, 0 }, + + /* <@route:user@domain> -> <@route:user@domain> */ + { "<@route:user@domain>", NULL, NULL, +- { NULL, NULL, "@route", "user", "domain", FALSE }, +- { NULL, NULL, "@route", "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, "@route", "user", "domain", FALSE }, ++ { NULL, NULL, NULL, "@route", "user", "domain", FALSE }, 0 }, + { "<@route,@route2:user@domain>", NULL, NULL, +- { NULL, NULL, "@route,@route2", "user", "domain", FALSE }, +- { NULL, NULL, "@route,@route2", "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, "@route,@route2", "user", "domain", FALSE }, ++ { NULL, NULL, NULL, "@route,@route2", "user", "domain", FALSE }, 0 }, + { "<@route@route2:user@domain>", "<@route,@route2:user@domain>", NULL, +- { NULL, NULL, "@route,@route2", "user", "domain", FALSE }, +- { NULL, NULL, "@route,@route2", "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, "@route,@route2", "user", "domain", FALSE }, ++ { NULL, NULL, NULL, "@route,@route2", "user", "domain", FALSE }, 0 }, + { "<@route@route2:user>", "<@route,@route2:user>", "<@route,@route2:user@MISSING_DOMAIN>", +- { NULL, NULL, "@route,@route2", "user", "", TRUE }, +- { NULL, NULL, "@route,@route2", "user", "MISSING_DOMAIN", TRUE }, 0 }, ++ { NULL, NULL, NULL, "@route,@route2", "user", "", TRUE }, ++ { NULL, NULL, NULL, "@route,@route2", "user", "MISSING_DOMAIN", TRUE }, 0 }, + { "<@route@route2:\"\"@domain>", "<@route,@route2:\"\"@domain>", NULL, +- { NULL, NULL, "@route,@route2", "", "domain", FALSE }, +- { NULL, NULL, "@route,@route2", "", "domain", FALSE }, 0 }, ++ { NULL, NULL, NULL, "@route,@route2", "", "domain", FALSE }, ++ { NULL, NULL, NULL, "@route,@route2", "", "domain", FALSE }, 0 }, + + /* Display Name <@route:user@domain> -> + "Display Name" <@route:user@domain> */ + { "Display Name <@route:user@domain>", "\"Display Name\" <@route:user@domain>", NULL, +- { NULL, "Display Name", "@route", "user", "domain", FALSE }, +- { NULL, "Display Name", "@route", "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, "Display Name", "@route", "user", "domain", FALSE }, ++ { NULL, NULL, "Display Name", "@route", "user", "domain", FALSE }, 0 }, + { "Display Name <@route,@route2:user@domain>", "\"Display Name\" <@route,@route2:user@domain>", NULL, +- { NULL, "Display Name", "@route,@route2", "user", "domain", FALSE }, +- { NULL, "Display Name", "@route,@route2", "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, "Display Name", "@route,@route2", "user", "domain", FALSE }, ++ { NULL, NULL, "Display Name", "@route,@route2", "user", "domain", FALSE }, 0 }, + { "Display Name <@route@route2:user@domain>", "\"Display Name\" <@route,@route2:user@domain>", NULL, +- { NULL, "Display Name", "@route,@route2", "user", "domain", FALSE }, +- { NULL, "Display Name", "@route,@route2", "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, "Display Name", "@route,@route2", "user", "domain", FALSE }, ++ { NULL, NULL, "Display Name", "@route,@route2", "user", "domain", FALSE }, 0 }, + { "Display Name <@route@route2:user>", "\"Display Name\" <@route,@route2:user>", "\"Display Name\" <@route,@route2:user@MISSING_DOMAIN>", +- { NULL, "Display Name", "@route,@route2", "user", "", TRUE }, +- { NULL, "Display Name", "@route,@route2", "user", "MISSING_DOMAIN", TRUE }, 0 }, ++ { NULL, NULL, "Display Name", "@route,@route2", "user", "", TRUE }, ++ { NULL, NULL, "Display Name", "@route,@route2", "user", "MISSING_DOMAIN", TRUE }, 0 }, + { "Display Name <@route@route2:\"\"@domain>", "\"Display Name\" <@route,@route2:\"\"@domain>", NULL, +- { NULL, "Display Name", "@route,@route2", "", "domain", FALSE }, +- { NULL, "Display Name", "@route,@route2", "", "domain", FALSE }, 0 }, ++ { NULL, NULL, "Display Name", "@route,@route2", "", "domain", FALSE }, ++ { NULL, NULL, "Display Name", "@route,@route2", "", "domain", FALSE }, 0 }, + + /* other tests: */ + { "\"foo: ;,\" ", NULL, NULL, +- { NULL, "foo: ;,", NULL, "user", "domain", FALSE }, +- { NULL, "foo: ;,", NULL, "user", "domain", FALSE }, 0 }, ++ { NULL, NULL, "foo: ;,", NULL, "user", "domain", FALSE }, ++ { NULL, NULL, "foo: ;,", NULL, "user", "domain", FALSE }, 0 }, + { "<>", "", "", +- { NULL, NULL, NULL, "", "", TRUE }, +- { NULL, NULL, NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, ++ { NULL, NULL, NULL, NULL, "", "", TRUE }, ++ { NULL, NULL, NULL, NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, + { "<@>", "", "", +- { NULL, NULL, NULL, "", "", TRUE }, +- { NULL, NULL, "INVALID_ROUTE", "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, ++ { NULL, NULL, NULL, NULL, "", "", TRUE }, ++ { NULL, NULL, NULL, "INVALID_ROUTE", "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 }, + + /* Test against a out-of-bounds read bug - keep these two tests + together in this same order: */ + { "aaaa@", "", "", +- { NULL, NULL, NULL, "aaaa", "", TRUE }, +- { NULL, NULL, NULL, "aaaa", "MISSING_DOMAIN", TRUE }, 0 }, ++ { NULL, NULL, NULL, NULL, "aaaa", "", TRUE }, ++ { NULL, NULL, NULL, NULL, "aaaa", "MISSING_DOMAIN", TRUE }, 0 }, + { "a(aa", "", "", +- { NULL, NULL, NULL, "", "", TRUE }, +- { NULL, NULL, NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, ++ { NULL, NULL, NULL, NULL, "", "", TRUE }, ++ { NULL, NULL, NULL, NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, + TEST_MESSAGE_ADDRESS_FLAG_SKIP_LIST }, + }; + static struct message_address group_prefix = { +- NULL, NULL, NULL, "group", NULL, FALSE ++ NULL, NULL, NULL, NULL, "group", NULL, FALSE + }; + static struct message_address group_suffix = { +- NULL, NULL, NULL, NULL, NULL, FALSE ++ NULL, NULL, NULL, NULL, NULL, NULL, FALSE + }; + const struct message_address *addr; + string_t *str, *group; +@@ -327,7 +327,7 @@ static void test_message_address_nuls(void) + const unsigned char input[] = + "\"user\0nuls\\\0-esc\"@[domain\0nuls\\\0-esc] (comment\0nuls\\\0-esc)"; + const struct message_address output = { +- NULL, "comment\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc", NULL, ++ NULL, NULL, "comment\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc", NULL, + "user\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc", + "[domain\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc]", FALSE + }; +@@ -345,7 +345,7 @@ static void test_message_address_nuls_display_name(void) + const unsigned char input[] = + "\"displayname\0nuls\\\0-esc\" <\"user\0nuls\\\0-esc\"@[domain\0nuls\\\0-esc]>"; + const struct message_address output = { +- NULL, "displayname\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc", NULL, ++ NULL, NULL, "displayname\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc", NULL, + "user\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc", + "[domain\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc]", FALSE + }; +@@ -369,7 +369,7 @@ static void test_message_address_non_strict_dots(void) + }; + const struct message_address *addr; + struct message_address output = { +- NULL, NULL, NULL, "local-part", ++ NULL, NULL, NULL, NULL, "local-part", + "example.com", FALSE + }; + +@@ -421,29 +421,29 @@ static void test_message_address_path(void) + struct message_address addr; + } tests[] = { + { "<>", NULL, +- { NULL, NULL, NULL, NULL, NULL, FALSE } }, ++ { NULL, NULL, NULL, NULL, NULL, NULL, FALSE } }, + { " < > ", "<>", +- { NULL, NULL, NULL, NULL, NULL, FALSE } }, ++ { NULL, NULL, NULL, NULL, NULL, NULL, FALSE } }, + { "", NULL, +- { NULL, NULL, NULL, "user", "domain", FALSE } }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE } }, + { " ", "", +- { NULL, NULL, NULL, "user", "domain", FALSE } }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE } }, + { "user@domain", "", +- { NULL, NULL, NULL, "user", "domain", FALSE } }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE } }, + { " user@domain ", "", +- { NULL, NULL, NULL, "user", "domain", FALSE } }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE } }, + { "<\"user\"@domain>", "", +- { NULL, NULL, NULL, "user", "domain", FALSE } }, ++ { NULL, NULL, NULL, NULL, "user", "domain", FALSE } }, + { "<\"user name\"@domain>", NULL, +- { NULL, NULL, NULL, "user name", "domain", FALSE } }, ++ { NULL, NULL, NULL, NULL, "user name", "domain", FALSE } }, + { "<\"user@na\\\\me\"@domain>", NULL, +- { NULL, NULL, NULL, "user@na\\me", "domain", FALSE } }, ++ { NULL, NULL, NULL, NULL, "user@na\\me", "domain", FALSE } }, + { "<\"user\\\"name\"@domain>", NULL, +- { NULL, NULL, NULL, "user\"name", "domain", FALSE } }, ++ { NULL, NULL, NULL, NULL, "user\"name", "domain", FALSE } }, + { "<\"\"@domain>", NULL, +- { NULL, NULL, NULL, "", "domain", FALSE } }, ++ { NULL, NULL, NULL, NULL, "", "domain", FALSE } }, + { "<@source", "<>", +- { NULL, NULL, NULL, NULL, NULL, TRUE } }, ++ { NULL, NULL, NULL, NULL, NULL, NULL, TRUE } }, + }; + const struct message_address *addr; + string_t *str; + +From da61d20311da34f22944c6111a0b97ea2a1f8a47 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 30 Jan 2024 22:17:38 +0200 +Subject: [PATCH 5/6] lib-mail: Add message_address_parse_full() and struct + message_address_list + +--- + src/lib-mail/message-address.c | 37 +++++++++++++--------- + src/lib-mail/message-address.h | 10 ++++++ + src/lib-mail/test-message-address.c | 48 +++++++++++++++++++++++++---- + 3 files changed, 75 insertions(+), 20 deletions(-) + +diff --git a/src/lib-mail/message-address.c b/src/lib-mail/message-address.c +index 9d192799468..ae37014079a 100644 +--- a/src/lib-mail/message-address.c ++++ b/src/lib-mail/message-address.c +@@ -13,7 +13,8 @@ struct message_address_parser_context { + pool_t pool; + struct rfc822_parser_context parser; + +- struct message_address *first_addr, *last_addr, addr; ++ struct message_address addr; ++ struct message_address_list addr_list; + string_t *str; + + bool fill_missing, non_strict_dots; +@@ -28,7 +29,7 @@ static void add_address(struct message_address_parser_context *ctx) + memcpy(addr, &ctx->addr, sizeof(ctx->addr)); + i_zero(&ctx->addr); + +- DLLIST2_APPEND(&ctx->first_addr, &ctx->last_addr, addr); ++ DLLIST2_APPEND(&ctx->addr_list.head, &ctx->addr_list.tail, addr); + } + + /* quote with "" and escape all '\', '"' and "'" characters if need */ +@@ -439,10 +440,11 @@ static int parse_path(struct message_address_parser_context *ctx) + return ret; + } + +-static struct message_address * ++static void + message_address_parse_real(pool_t pool, const unsigned char *data, size_t size, + unsigned int max_addresses, +- enum message_address_parse_flags flags) ++ enum message_address_parse_flags flags, ++ struct message_address_list *list_r) + { + struct message_address_parser_context ctx; + +@@ -461,7 +463,7 @@ message_address_parse_real(pool_t pool, const unsigned char *data, size_t size, + (void)parse_address_list(&ctx, max_addresses); + } + rfc822_parser_deinit(&ctx.parser); +- return ctx.first_addr; ++ *list_r = ctx.addr_list; + } + + static int +@@ -481,7 +483,7 @@ message_address_parse_path_real(pool_t pool, const unsigned char *data, + ret = parse_path(&ctx); + + rfc822_parser_deinit(&ctx.parser); +- *addr_r = ctx.first_addr; ++ *addr_r = ctx.addr_list.head; + return (ret < 0 ? -1 : 0); + } + +@@ -490,17 +492,24 @@ message_address_parse(pool_t pool, const unsigned char *data, size_t size, + unsigned int max_addresses, + enum message_address_parse_flags flags) + { +- struct message_address *addr; ++ struct message_address_list list; ++ message_address_parse_full(pool, data, size, max_addresses, flags, ++ &list); ++ return list.head; ++} + ++void message_address_parse_full(pool_t pool, const unsigned char *data, ++ size_t size, unsigned int max_addresses, ++ enum message_address_parse_flags flags, ++ struct message_address_list *list_r) ++{ + if (pool->datastack_pool) { +- return message_address_parse_real(pool, data, size, +- max_addresses, flags); +- } +- T_BEGIN { +- addr = message_address_parse_real(pool, data, size, +- max_addresses, flags); ++ message_address_parse_real(pool, data, size, ++ max_addresses, flags, list_r); ++ } else T_BEGIN { ++ message_address_parse_real(pool, data, size, ++ max_addresses, flags, list_r); + } T_END; +- return addr; + } + + int message_address_parse_path(pool_t pool, const unsigned char *data, +diff --git a/src/lib-mail/message-address.h b/src/lib-mail/message-address.h +index 85cff3dcc6f..224f7a75605 100644 +--- a/src/lib-mail/message-address.h ++++ b/src/lib-mail/message-address.h +@@ -31,12 +31,22 @@ struct message_address { + bool invalid_syntax; + }; + ++struct message_address_list { ++ struct message_address *head, *tail; ++}; ++ + /* Parse message addresses from given data. Note that giving an empty string + will return NULL since there are no addresses. */ + struct message_address * + message_address_parse(pool_t pool, const unsigned char *data, size_t size, + unsigned int max_addresses, + enum message_address_parse_flags flags); ++/* Same as message_address_parse(), but return message_address_list containing ++ both the first and the last address in the linked list. */ ++void message_address_parse_full(pool_t pool, const unsigned char *data, ++ size_t size, unsigned int max_addresses, ++ enum message_address_parse_flags flags, ++ struct message_address_list *list_r); + + /* Parse RFC 5322 "path" (Return-Path header) from given data. Returns -1 if + the path is invalid and 0 otherwise. +diff --git a/src/lib-mail/test-message-address.c b/src/lib-mail/test-message-address.c +index 261cbfba70a..54aa9a83101 100644 +--- a/src/lib-mail/test-message-address.c ++++ b/src/lib-mail/test-message-address.c +@@ -19,8 +19,9 @@ static bool cmp_addr(const struct message_address *a1, + a1->invalid_syntax == a2->invalid_syntax; + } + +-static const struct message_address * +-test_parse_address(const char *input, bool fill_missing) ++static void ++test_parse_address_full(const char *input, bool fill_missing, ++ struct message_address_list *list_r) + { + const enum message_address_parse_flags flags = + fill_missing ? MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING : 0; +@@ -28,11 +29,18 @@ test_parse_address(const char *input, bool fill_missing) + if there's any out-of-bounds access */ + size_t input_len = strlen(input); + unsigned char *input_dup = i_memdup(input, input_len); +- const struct message_address *addr = +- message_address_parse(pool_datastack_create(), +- input_dup, input_len, UINT_MAX, flags); ++ message_address_parse_full(pool_datastack_create(), ++ input_dup, input_len, UINT_MAX, flags, ++ list_r); + i_free(input_dup); +- return addr; ++} ++ ++static const struct message_address * ++test_parse_address(const char *input, bool fill_missing) ++{ ++ struct message_address_list list; ++ test_parse_address_full(input, fill_missing, &list); ++ return list.head; + } + + static void test_message_address(void) +@@ -322,6 +330,33 @@ static void test_message_address(void) + test_end(); + } + ++static void test_message_address_list(void) ++{ ++ test_begin("message address list"); ++ ++ const char *test_input = ++ "user1@example1.com, user2@example2.com, user3@example3.com"; ++ const struct message_address wanted_addrs[] = { ++ { NULL, NULL, NULL, NULL, "user1", "example1.com", FALSE }, ++ { NULL, NULL, NULL, NULL, "user2", "example2.com", FALSE }, ++ { NULL, NULL, NULL, NULL, "user3", "example3.com", FALSE }, ++ }; ++ ++ struct message_address_list list; ++ struct message_address *addr, *scanned_last_addr; ++ test_parse_address_full(test_input, FALSE, &list); ++ addr = list.head; ++ for (unsigned int i = 0; i < N_ELEMENTS(wanted_addrs); i++) { ++ test_assert_idx(cmp_addr(addr, &wanted_addrs[i]), i); ++ scanned_last_addr = addr; ++ addr = addr->next; ++ } ++ test_assert(list.tail == scanned_last_addr); ++ test_assert(addr == NULL); ++ ++ test_end(); ++} ++ + static void test_message_address_nuls(void) + { + const unsigned char input[] = +@@ -521,6 +556,7 @@ int main(void) + { + static void (*const test_functions[])(void) = { + test_message_address, ++ test_message_address_list, + test_message_address_nuls, + test_message_address_nuls_display_name, + test_message_address_non_strict_dots, + +From 1481c04f02df7647f520df65d63df7626bf0ee32 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 9 Feb 2024 00:57:12 +0200 +Subject: [PATCH 6/6] lib-mail, lib-imap: Optimize parsing large number of + address headers + +Every header was appended to a linked list by walking through the whole +list, causing excessive CPU usage when the list became large enough. +Fixed by changing struct message_part_envelope to use struct +message_address_list, which stores also linked list tail pointers. This +allows quickly appending to the end of the linked list. +--- + src/lib-imap/imap-envelope.c | 27 ++++++++++------------- + src/lib-mail/message-part-data.c | 17 +++++++------- + src/lib-mail/message-part-data.h | 6 +++-- + src/lib-storage/index/index-search-mime.c | 4 ++-- + 4 files changed, 27 insertions(+), 27 deletions(-) + +diff --git a/src/lib-imap/imap-envelope.c b/src/lib-imap/imap-envelope.c +index 1312eae2ff3..da3177025a5 100644 +--- a/src/lib-imap/imap-envelope.c ++++ b/src/lib-imap/imap-envelope.c +@@ -67,17 +67,17 @@ void imap_envelope_write(struct message_part_envelope *data, + } + + str_append_c(str, ' '); +- imap_write_address(str, data->from); ++ imap_write_address(str, data->from.head); + str_append_c(str, ' '); +- imap_write_address(str, NVL(data->sender, data->from)); ++ imap_write_address(str, NVL(data->sender.head, data->from.head)); + str_append_c(str, ' '); +- imap_write_address(str, NVL(data->reply_to, data->from)); ++ imap_write_address(str, NVL(data->reply_to.head, data->from.head)); + str_append_c(str, ' '); +- imap_write_address(str, data->to); ++ imap_write_address(str, data->to.head); + str_append_c(str, ' '); +- imap_write_address(str, data->cc); ++ imap_write_address(str, data->cc.head); + str_append_c(str, ' '); +- imap_write_address(str, data->bcc); ++ imap_write_address(str, data->bcc.head); + + str_append_c(str, ' '); + imap_append_nstring_nolf(str, data->in_reply_to); +@@ -126,28 +126,25 @@ imap_envelope_parse_address(const struct imap_arg *arg, + + static bool + imap_envelope_parse_addresses(const struct imap_arg *arg, +- pool_t pool, struct message_address **addrs_r) ++ pool_t pool, struct message_address_list *addrs_r) + { +- struct message_address *first, *last, *addr; ++ struct message_address *addr; + const struct imap_arg *list_args; + +- if (arg->type == IMAP_ARG_NIL) { +- *addrs_r = NULL; ++ i_zero(addrs_r); ++ if (arg->type == IMAP_ARG_NIL) + return TRUE; +- } + + if (!imap_arg_get_list(arg, &list_args)) + return FALSE; + +- first = last = addr = NULL; ++ addr = NULL; + for (; !IMAP_ARG_IS_EOL(list_args); list_args++) { + if (!imap_envelope_parse_address + (list_args, pool, &addr)) + return FALSE; +- DLLIST2_APPEND(&first, &last, addr); ++ DLLIST2_APPEND(&addrs_r->head, &addrs_r->tail, addr); + } +- +- *addrs_r = first; + return TRUE; + } + +diff --git a/src/lib-mail/message-part-data.c b/src/lib-mail/message-part-data.c +index a5771f87e2e..25019ab432d 100644 +--- a/src/lib-mail/message-part-data.c ++++ b/src/lib-mail/message-part-data.c +@@ -4,6 +4,7 @@ + #include "str.h" + #include "wildcard-match.h" + #include "array.h" ++#include "llist.h" + #include "rfc822-parser.h" + #include "rfc2231-parser.h" + #include "message-address.h" +@@ -176,7 +177,7 @@ void message_part_envelope_parse_from_header(pool_t pool, + { + struct message_part_envelope *d; + enum envelope_field field; +- struct message_address **addr_p, *addr; ++ struct message_address_list *addr_p, new_addr; + const char **str_p; + + if (*data == NULL) { +@@ -234,18 +235,18 @@ void message_part_envelope_parse_from_header(pool_t pool, + } + + if (addr_p != NULL) { +- addr = message_address_parse(pool, hdr->full_value, +- hdr->full_value_len, +- UINT_MAX, +- MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING); ++ message_address_parse_full(pool, hdr->full_value, ++ hdr->full_value_len, ++ UINT_MAX, ++ MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING, ++ &new_addr); + /* Merge multiple headers the same as if they were comma + separated in a single line. This is better from security + point of view, because attacker could intentionally write + addresses in a way that e.g. the first From header is + validated while MUA only shows the second From header. */ +- while (*addr_p != NULL) +- addr_p = &(*addr_p)->next; +- *addr_p = addr; ++ DLLIST2_JOIN(&addr_p->head, &addr_p->tail, ++ &new_addr.head, &new_addr.tail); + } else if (str_p != NULL) { + *str_p = message_header_strdup(pool, hdr->full_value, + hdr->full_value_len); +diff --git a/src/lib-mail/message-part-data.h b/src/lib-mail/message-part-data.h +index 5ff9ffe1bc6..7ec878de68e 100644 +--- a/src/lib-mail/message-part-data.h ++++ b/src/lib-mail/message-part-data.h +@@ -2,6 +2,7 @@ + #define MESSAGE_PART_DATA_H + + #include "message-part.h" ++#include "message-address.h" + + #define MESSAGE_PART_DEFAULT_CHARSET "us-ascii" + +@@ -14,8 +15,9 @@ struct message_part_param { + + struct message_part_envelope { + const char *date, *subject; +- struct message_address *from, *sender, *reply_to; +- struct message_address *to, *cc, *bcc; ++ ++ struct message_address_list from, sender, reply_to; ++ struct message_address_list to, cc, bcc; + + const char *in_reply_to, *message_id; + }; +diff --git a/src/lib-storage/index/index-search-mime.c b/src/lib-storage/index/index-search-mime.c +index da7e5e17092..3328ce98af1 100644 +--- a/src/lib-storage/index/index-search-mime.c ++++ b/src/lib-storage/index/index-search-mime.c +@@ -205,7 +205,7 @@ seach_arg_mime_envelope_address_match( + enum mail_search_mime_arg_type type, const char *key, + const struct message_part_envelope *envelope) + { +- const struct message_address *addrs; ++ struct message_address_list addrs; + string_t *addrs_enc; + + if (envelope == NULL) +@@ -239,7 +239,7 @@ seach_arg_mime_envelope_address_match( + probably be normalized directly in the struct message_address. */ + + addrs_enc = t_str_new(128); +- message_address_write(addrs_enc, addrs); ++ message_address_write(addrs_enc, addrs.head); + return (strstr(str_c(addrs_enc), key) != NULL ? 1 : 0); + } + diff --git a/debian/patches/CVE-2024-23185.patch b/debian/patches/CVE-2024-23185.patch new file mode 100644 index 000000000..2757e758e --- /dev/null +++ b/debian/patches/CVE-2024-23185.patch @@ -0,0 +1,491 @@ +Author: Marco Bettini +Date: Fri, 12 Apr 2024 15:06:43 +0000 +Description: Upstream fix for CVE-2024-23185 + +--- + src/lib-mail/message-header-parser.c | 48 ++++++++++++---- + src/lib-mail/message-header-parser.h | 10 ++++ + src/lib-mail/test-message-header-parser.c | 67 +++++++++++++++++++++++ + 3 files changed, 114 insertions(+), 11 deletions(-) + +diff --git a/src/lib-mail/message-header-parser.c b/src/lib-mail/message-header-parser.c +index c5026f1bb7..5e020bbeb3 100644 +--- a/src/lib-mail/message-header-parser.c ++++ b/src/lib-mail/message-header-parser.c +@@ -21,6 +21,9 @@ struct message_header_parser_ctx { + string_t *name; + buffer_t *value_buf; + ++ size_t header_block_max_size; ++ size_t header_block_total_size; ++ + enum message_header_parser_flags flags; + bool skip_line:1; + bool has_nuls:1; +@@ -38,6 +41,7 @@ message_parse_header_init(struct istream *input, struct message_size *hdr_size, + ctx->name = str_new(default_pool, 128); + ctx->flags = flags; + ctx->value_buf = buffer_create_dynamic(default_pool, 4096); ++ ctx->header_block_max_size = MESSAGE_HEADER_BLOCK_DEFAULT_MAX_SIZE; + i_stream_ref(input); + + if (hdr_size != NULL) +@@ -45,6 +49,21 @@ message_parse_header_init(struct istream *input, struct message_size *hdr_size, + return ctx; + } + ++void ++message_parse_header_set_limit(struct message_header_parser_ctx *parser, ++ size_t header_block_max_size) ++{ ++ parser->header_block_max_size = header_block_max_size; ++} ++ ++void ++message_parse_header_lower_limit(struct message_header_parser_ctx *parser, ++ size_t header_block_max_size) ++{ ++ if (header_block_max_size < parser->header_block_max_size) ++ message_parse_header_set_limit(parser, header_block_max_size); ++} ++ + void message_parse_header_deinit(struct message_header_parser_ctx **_ctx) + { + struct message_header_parser_ctx *ctx = *_ctx; +@@ -77,6 +96,7 @@ int message_parse_header_next(struct message_header_parser_ctx *ctx, + /* new header line */ + line->name_offset = ctx->input->v_offset; + colon_pos = UINT_MAX; ++ ctx->header_block_total_size += ctx->value_buf->used; + buffer_set_used_size(ctx->value_buf, 0); + } + +@@ -342,33 +362,39 @@ int message_parse_header_next(struct message_header_parser_ctx *ctx, + } + } + ++ line->value_len = I_MIN(line->value_len, ctx->header_block_max_size); ++ size_t line_value_size = line->value_len; ++ size_t header_total_used = ctx->header_block_total_size + ctx->value_buf->used; ++ size_t line_available = ctx->header_block_max_size <= header_total_used ? 0 : ++ ctx->header_block_max_size - header_total_used; ++ line_value_size = I_MIN(line_value_size, line_available); ++ + if (!line->continued) { + /* first header line. make a copy of the line since we can't + really trust input stream not to lose it. */ +- buffer_append(ctx->value_buf, line->value, line->value_len); ++ buffer_append(ctx->value_buf, line->value, line_value_size); + line->value = line->full_value = ctx->value_buf->data; +- line->full_value_len = line->value_len; ++ line->full_value_len = line->value_len = line_value_size; + } else if (line->use_full_value) { + /* continue saving the full value. */ + if (last_no_newline) { + /* line is longer than fit into our buffer, so we + were forced to break it into multiple + message_header_lines */ +- } else { +- if (last_crlf) ++ } else if (line_value_size > 1) { ++ if (last_crlf && line_value_size > 2) + buffer_append_c(ctx->value_buf, '\r'); + buffer_append_c(ctx->value_buf, '\n'); + } + if ((ctx->flags & MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE) != 0 && + line->value_len > 0 && line->value[0] != ' ' && +- IS_LWSP(line->value[0])) { ++ IS_LWSP(line->value[0]) && ++ line_value_size > 0) { + buffer_append_c(ctx->value_buf, ' '); +- buffer_append(ctx->value_buf, +- line->value + 1, line->value_len - 1); +- } else { +- buffer_append(ctx->value_buf, +- line->value, line->value_len); +- } ++ buffer_append(ctx->value_buf, line->value + 1, line_value_size - 1); ++ } else ++ buffer_append(ctx->value_buf, line->value, line_value_size); ++ + line->full_value = ctx->value_buf->data; + line->full_value_len = ctx->value_buf->used; + } else { +diff --git a/src/lib-mail/message-header-parser.h b/src/lib-mail/message-header-parser.h +index ce0825c8e5..43cf95e56a 100644 +--- a/src/lib-mail/message-header-parser.h ++++ b/src/lib-mail/message-header-parser.h +@@ -1,6 +1,9 @@ + #ifndef MESSAGE_HEADER_PARSER_H + #define MESSAGE_HEADER_PARSER_H + ++/* This can be overridden by message_parse_header_set_limit() */ ++#define MESSAGE_HEADER_BLOCK_DEFAULT_MAX_SIZE ((size_t) 10 * 1024*1024) ++ + #define IS_LWSP(c) \ + ((c) == ' ' || (c) == '\t') + +@@ -48,6 +51,13 @@ message_parse_header_init(struct istream *input, struct message_size *hdr_size, + enum message_header_parser_flags flags) ATTR_NULL(2); + void message_parse_header_deinit(struct message_header_parser_ctx **ctx); + ++void ++message_parse_header_set_limit(struct message_header_parser_ctx *parser, ++ size_t header_block_max_size); ++void ++message_parse_header_lower_limit(struct message_header_parser_ctx *parser, ++ size_t header_block_max_size); ++ + /* Read and return next header line. Returns 1 if header is returned, 0 if + input stream is non-blocking and more data needs to be read, -1 when all is + done or error occurred (see stream's error status). */ +diff --git a/src/lib-mail/test-message-header-parser.c b/src/lib-mail/test-message-header-parser.c +index 700d3413f1..93d8842002 100644 +--- a/src/lib-mail/test-message-header-parser.c ++++ b/src/lib-mail/test-message-header-parser.c +@@ -463,6 +463,71 @@ static void test_message_header_parser_extra_crlf_in_name(void) + test_end(); + } + ++#define assert_parsed_field(line, expected, actual, len) STMT_START { \ ++ test_assert_idx(memcmp(expected, actual, strlen(expected)) == 0, line); \ ++ test_assert_cmp_idx(strlen(expected), ==, len, line); \ ++} STMT_END ++ ++/* NOTE: implicit variables: (parser, hdr) */ ++#define assert_parse_line(line, exp_name, exp_value, exp_full) STMT_START { \ ++ test_assert_idx(message_parse_header_next(parser, &hdr) > 0, line); \ ++ assert_parsed_field(line, exp_name, hdr->name, hdr->name_len); \ ++ assert_parsed_field(line, exp_value, hdr->value, hdr->value_len); \ ++ assert_parsed_field(line, exp_full, hdr->full_value, hdr->full_value_len); \ ++ if (hdr->continues) hdr->use_full_value = TRUE; \ ++} STMT_END ++ ++static const unsigned char test_message_header_truncation_input[] = ++ /*01*/ "header1: this is short\n" ++ /*02*/ "header2: this is multiline\n" ++ /*03*/ " and long 343638404244464850525456586062\n" ++ /*04*/ " 64666870727476788082848688909294969800\n" ++ /*05*/ " 02040608101214161820222426283032343638\n" ++ /*06*/ "header3: I should not appear at all\n" ++ /*07*/ "\n"; ++ ++static void test_message_header_truncation_clean_oneline(void) ++{ ++ test_begin("message header parser truncate + CLEAN_ONELINE flag"); ++ struct message_header_line *hdr = NULL; ++ struct istream *input = test_istream_create_data(test_message_header_truncation_input, sizeof(test_message_header_truncation_input)); ++ struct message_header_parser_ctx *parser = message_parse_header_init(input, NULL, MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE); ++ message_parse_header_set_limit(parser, 96); ++ ++ assert_parse_line( 1, "header1", "this is short", "this is short"); ++ assert_parse_line( 2, "header2", "this is multiline", "this is multiline"); ++ assert_parse_line( 3, "header2", " and long 343638404244464850525456586062", "this is multiline and long 343638404244464850525456586062"); ++ assert_parse_line( 4, "header2", " 64666870727476788082848688909294969800", "this is multiline and long 343638404244464850525456586062 6466687072747678808284868"); ++ assert_parse_line( 5, "header2", " 02040608101214161820222426283032343638", "this is multiline and long 343638404244464850525456586062 6466687072747678808284868"); ++ assert_parse_line( 6, "header3", "", ""); ++ test_assert(message_parse_header_next(parser, &hdr) > 0 && hdr->eoh); ++ ++ message_parse_header_deinit(&parser); ++ i_stream_unref(&input); ++ test_end(); ++} ++ ++static void test_message_header_truncation_flag0(void) ++{ ++ test_begin("message header parser truncate + NO flags"); ++ struct message_header_line *hdr = NULL; ++ struct istream *input = test_istream_create_data(test_message_header_truncation_input, sizeof(test_message_header_truncation_input)); ++ struct message_header_parser_ctx *parser = message_parse_header_init(input, NULL, 0); ++ message_parse_header_set_limit(parser, 96); ++ ++ assert_parse_line( 1, "header1", "this is short", "this is short"); ++ assert_parse_line( 2, "header2", "this is multiline", "this is multiline"); ++ assert_parse_line( 3, "header2", " and long 343638404244464850525456586062", "this is multiline\n and long 343638404244464850525456586062"); ++ assert_parse_line( 4, "header2", " 64666870727476788082848688909294969800", "this is multiline\n and long 343638404244464850525456586062\n 646668707274767880828486"); ++ assert_parse_line( 5, "header2", " 02040608101214161820222426283032343638", "this is multiline\n and long 343638404244464850525456586062\n 646668707274767880828486"); ++ assert_parse_line( 6, "header3", "", ""); ++ test_assert(message_parse_header_next(parser, &hdr) > 0 && hdr->eoh); ++ ++ message_parse_header_deinit(&parser); ++ i_stream_unref(&input); ++ test_end(); ++} ++ + int main(void) + { + static void (*const test_functions[])(void) = { +@@ -473,6 +538,8 @@ int main(void) + test_message_header_parser_no_eoh, + test_message_header_parser_nul, + test_message_header_parser_extra_crlf_in_name, ++ test_message_header_truncation_flag0, ++ test_message_header_truncation_clean_oneline, + NULL + }; + return test_run(test_functions); + +From ce88c33abc37e408592eff70aeefa28f803effb9 Mon Sep 17 00:00:00 2001 +From: Marco Bettini +Date: Wed, 24 Apr 2024 10:45:46 +0000 +Subject: [PATCH 2/2] lib-mail: message-parser - Limit headers total count to + 50MB by default + +(including top headers and all mime-sections headers) +--- + src/lib-mail/message-parser-private.h | 2 + + src/lib-mail/message-parser.c | 15 +++ + src/lib-mail/message-parser.h | 6 + + src/lib-mail/test-message-parser.c | 154 ++++++++++++++++++++++++++ + 4 files changed, 177 insertions(+) + +diff --git a/src/lib-mail/message-parser-private.h b/src/lib-mail/message-parser-private.h +index 41c32daf3a..8b362a9e71 100644 +--- a/src/lib-mail/message-parser-private.h ++++ b/src/lib-mail/message-parser-private.h +@@ -30,6 +30,8 @@ struct message_parser_ctx { + enum message_parser_flags flags; + unsigned int max_nested_mime_parts; + unsigned int max_total_mime_parts; ++ size_t all_headers_max_size; ++ size_t all_headers_total_size; + + char *last_boundary; + struct message_boundary *boundaries; +diff --git a/src/lib-mail/message-parser.c b/src/lib-mail/message-parser.c +index 9a9c9a3515..c7e3b1e96a 100644 +--- a/src/lib-mail/message-parser.c ++++ b/src/lib-mail/message-parser.c +@@ -617,7 +617,18 @@ static int parse_next_header(struct message_parser_ctx *ctx, + } + if (ret < 0) { + /* no boundary */ ++ size_t headers_available = ++ ctx->all_headers_max_size > ctx->all_headers_total_size ? ++ ctx->all_headers_max_size - ctx->all_headers_total_size : 0; ++ message_parse_header_lower_limit(ctx->hdr_parser_ctx, headers_available); + ret = message_parse_header_next(ctx->hdr_parser_ctx, &hdr); ++ if (ret > 0) { ++ if (!hdr->continues) { ++ ctx->all_headers_total_size += hdr->name_len; ++ ctx->all_headers_total_size += hdr->middle_len; ++ } ++ ctx->all_headers_total_size += hdr->value_len; ++ } + if (ret == 0 || (ret < 0 && ctx->input->stream_errno != 0)) { + ctx->want_count = i_stream_get_data_size(ctx->input) + 1; + return ret; +@@ -762,6 +773,9 @@ message_parser_init_int(struct istream *input, + ctx->max_total_mime_parts = set->max_total_mime_parts != 0 ? + set->max_total_mime_parts : + MESSAGE_PARSER_DEFAULT_MAX_TOTAL_MIME_PARTS; ++ ctx->all_headers_max_size = set->all_headers_max_size != 0 ? ++ set->all_headers_max_size : ++ MESSAGE_PARSER_DEFAULT_ALL_HEADERS_MAX_SIZE; + ctx->input = input; + i_stream_ref(input); + return ctx; +@@ -779,6 +793,7 @@ message_parser_init(pool_t part_pool, struct istream *input, + ctx->next_part = &ctx->part->children; + ctx->parse_next_block = parse_next_header_init; + ctx->total_parts_count = 1; ++ ctx->all_headers_total_size = 0; + i_array_init(&ctx->next_part_stack, 4); + return ctx; + } +diff --git a/src/lib-mail/message-parser.h b/src/lib-mail/message-parser.h +index f19e526284..8d70d73f05 100644 +--- a/src/lib-mail/message-parser.h ++++ b/src/lib-mail/message-parser.h +@@ -19,6 +19,7 @@ enum message_parser_flags { + + #define MESSAGE_PARSER_DEFAULT_MAX_NESTED_MIME_PARTS 100 + #define MESSAGE_PARSER_DEFAULT_MAX_TOTAL_MIME_PARTS 10000 ++#define MESSAGE_PARSER_DEFAULT_ALL_HEADERS_MAX_SIZE ((size_t) 50 * 1024*1024) + + struct message_parser_settings { + enum message_header_parser_flags hdr_flags; +@@ -30,6 +31,11 @@ struct message_parser_settings { + /* Maximum MIME parts in total. + 0 = MESSAGE_PARSER_DEFAULT_MAX_TOTAL_MIME_PARTS. */ + unsigned int max_total_mime_parts; ++ ++ /* Maximum bytes fore headers in top header plus all ++ MIME sections headers ++ 0 = MESSAGE_PARSER_DEFAULT_ALL_HEADERS_MAX_SIZE */ ++ size_t all_headers_max_size; + }; + + struct message_parser_ctx; +diff --git a/src/lib-mail/test-message-parser.c b/src/lib-mail/test-message-parser.c +index 663bfe8c5a..b6bada2303 100644 +--- a/src/lib-mail/test-message-parser.c ++++ b/src/lib-mail/test-message-parser.c +@@ -1369,6 +1369,158 @@ static const char input_msg[] = + test_end(); + } + ++#define test_assert_virtual_size(part) \ ++ test_assert((part).virtual_size == (part).lines + (part).physical_size) ++ ++#define test_assert_part(part, flags_, children, h_lines, h_size, b_lines, b_size ) \ ++STMT_START { \ ++ test_assert((part)->flags == (flags_)); \ ++ test_assert((part)->children_count == children); \ ++ test_assert((part)->header_size.lines == h_lines); \ ++ test_assert((part)->header_size.physical_size == h_size); \ ++ test_assert((part)->body_size.lines == b_lines); \ ++ test_assert((part)->body_size.physical_size == b_size); \ ++ test_assert_virtual_size((part)->header_size); \ ++ test_assert_virtual_size((part)->body_size); \ ++} STMT_END ++ ++static const enum message_part_flags FLAGS_MULTIPART = ++ MESSAGE_PART_FLAG_IS_MIME | MESSAGE_PART_FLAG_MULTIPART; ++static const enum message_part_flags FLAGS_RFC822 = ++ MESSAGE_PART_FLAG_IS_MIME | MESSAGE_PART_FLAG_MESSAGE_RFC822; ++static const enum message_part_flags FLAGS_TEXT = ++ MESSAGE_PART_FLAG_IS_MIME | MESSAGE_PART_FLAG_TEXT; ++ ++static const char too_many_header_bytes_input_msg[] = ++ "Content-Type: multipart/mixed; boundary=\"1\"\n\n" ++ "--1\n" ++ "Content-Type: multipart/mixed; boundary=\"2\"\n\n" ++ "--2\n" ++ "Content-Type: message/rfc822\n\n" ++ "Content-Type: text/plain\n\n1-rfc822\n" ++ "--2\n" ++ "Content-Type: message/rfc822\n\n" ++ "Content-Type: text/plain\n\n2-rfc822\n" ++ "--1\n" ++ "Content-Type: message/rfc822\n\n" ++ "Content-Type: text/plain\n\n3-rfc822\n"; ++ ++static void test_message_parser_too_many_header_bytes_run( ++ const struct message_parser_settings *parser_set, ++ pool_t *pool_r, struct istream **input_r, ++ struct message_part **parts_r) ++{ ++ *pool_r = pool_alloconly_create("message parser", 10240); ++ *input_r = test_istream_create(too_many_header_bytes_input_msg); ++ struct message_parser_ctx *parser = message_parser_init(*pool_r, *input_r, parser_set); ++ ++ int ret; ++ struct message_block block ATTR_UNUSED; ++ while ((ret = message_parser_parse_next_block(parser, &block)) > 0); ++ test_assert(ret < 0); ++ ++ message_parser_deinit(&parser, parts_r); ++} ++ ++static void test_message_parser_too_many_header_bytes_default(void) ++{ ++ test_begin("message parser too many header bytes default"); ++ ++ pool_t pool; ++ struct istream *input; ++ struct message_part *part_root; ++ const struct message_parser_settings parser_set = { .all_headers_max_size = 0 }; ++ ++ test_message_parser_too_many_header_bytes_run(&parser_set, &pool, &input, &part_root); ++ ++ // test_assert_part(part, flags_, children, h_lines, h_size, b_lines, b_size ) ++ ++ test_assert_part(part_root, FLAGS_MULTIPART, 7, 2, 45, 21, 256); ++ test_assert(part_root->parent == NULL); ++ ++ struct message_part *part_1 = part_root->children; ++ test_assert_part(part_1, FLAGS_MULTIPART, 4, 2, 45, 11, 137); ++ ++ struct message_part *part_1_1 = part_1->children; ++ test_assert_part(part_1_1, FLAGS_RFC822, 1, 2, 30, 2, 34); ++ ++ struct message_part *part_1_1_1 = part_1_1->children; ++ test_assert_part(part_1_1_1, FLAGS_TEXT, 0, 2, 26, 0, 8); ++ ++ test_assert(part_1_1_1->next == NULL); ++ ++ struct message_part *part_1_2 = part_1_1->next; ++ test_assert_part(part_1_2, FLAGS_RFC822, 1, 2, 30, 2, 34); ++ ++ struct message_part *part_1_2_1 = part_1_2->children; ++ test_assert_part(part_1_2_1, FLAGS_TEXT, 0, 2, 26, 0, 8); ++ ++ test_assert(part_1_2_1->next == NULL); ++ ++ test_assert(part_1_2->next == NULL); ++ ++ struct message_part *part_2 = part_1->next; ++ test_assert_part(part_2, FLAGS_RFC822, 1, 2, 30, 3, 35); ++ ++ struct message_part *part_2_1 = part_2->children; ++ test_assert_part(part_2_1, FLAGS_TEXT, 0, 2, 26, 1, 9); ++ test_assert(part_2_1->next == NULL); ++ ++ test_assert(part_2->next == NULL); ++ ++ test_assert(part_root->next == NULL); ++ ++ test_parsed_parts(input, part_root); ++ i_stream_unref(&input); ++ pool_unref(&pool); ++ test_end(); ++} ++ ++static void test_message_parser_too_many_header_bytes_100(void) ++{ ++ test_begin("message parser too many header bytes 100"); ++ ++ pool_t pool; ++ struct istream *input; ++ struct message_part *part_root; ++ const struct message_parser_settings parser_set = { .all_headers_max_size = 100 }; ++ ++ test_message_parser_too_many_header_bytes_run(&parser_set, &pool, &input, &part_root); ++ ++ // test_assert_part(part, flags_, children, h_lines, h_size, b_lines, b_size ) ++ ++ test_assert_part(part_root, FLAGS_MULTIPART, 5, 2, 45, 21, 256); ++ test_assert(part_root->parent == NULL); ++ ++ struct message_part *part_1 = part_root->children; ++ test_assert_part(part_1, FLAGS_MULTIPART, 3, 2, 45, 11, 137); ++ ++ struct message_part *part_1_1 = part_1->children; ++ test_assert_part(part_1_1, FLAGS_RFC822, 1, 2, 30, 2, 34); ++ ++ struct message_part *part_1_1_1 = part_1_1->children; ++ test_assert_part(part_1_1_1, MESSAGE_PART_FLAG_IS_MIME, 0, 2, 26, 0, 8); ++ ++ test_assert(part_1_1_1->next == NULL); ++ ++ struct message_part *part_1_2 = part_1_1->next; ++ test_assert_part(part_1_2, MESSAGE_PART_FLAG_IS_MIME, 0, 2, 30, 2, 34); ++ ++ test_assert(part_1_2->next == NULL); ++ ++ struct message_part *part_2 = part_1->next; ++ test_assert_part(part_2, MESSAGE_PART_FLAG_IS_MIME, 0, 2, 30, 3, 35); ++ ++ test_assert(part_2->next == NULL); ++ ++ test_assert(part_root->next == NULL); ++ ++ test_parsed_parts(input, part_root); ++ i_stream_unref(&input); ++ pool_unref(&pool); ++ test_end(); ++} ++ + int main(void) + { + static void (*const test_functions[])(void) = { +@@ -1392,6 +1544,8 @@ int main(void) + test_message_parser_mime_part_limit_rfc822, + test_message_parser_mime_version, + test_message_parser_mime_version_missing, ++ test_message_parser_too_many_header_bytes_default, ++ test_message_parser_too_many_header_bytes_100, + NULL + }; + return test_run(test_functions); diff --git a/debian/patches/CVE-2025-59031.patch b/debian/patches/CVE-2025-59031.patch new file mode 100644 index 000000000..ec251e34c --- /dev/null +++ b/debian/patches/CVE-2025-59031.patch @@ -0,0 +1,133 @@ +From 089edc7750160bf224011c015347db1bdea435e3 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Thu, 8 Jan 2026 08:51:59 +0200 +Subject: [PATCH 02/24] fts: Remove decode2text.sh + +The script is flawed and not fit for production use, should +recommend writing your own script, or using Apache Tika. +--- + src/plugins/fts/Makefile.am | 3 - + src/plugins/fts/decode2text.sh | 105 --------------------------------- + 2 files changed, 108 deletions(-) + delete mode 100755 src/plugins/fts/decode2text.sh + +--- dovecot-2.3.21+dfsg1.orig/src/plugins/fts/Makefile.am ++++ dovecot-2.3.21+dfsg1/src/plugins/fts/Makefile.am +@@ -63,9 +63,6 @@ xml2text_CPPFLAGS = $(AM_CPPFLAGS) $(BIN + xml2text_LDADD = $(LIBDOVECOT) $(BINARY_LDFLAGS) + xml2text_DEPENDENCIES = $(module_LTLIBRARIES) $(LIBDOVECOT_DEPS) + +-pkglibexec_SCRIPTS = decode2text.sh +-EXTRA_DIST = $(pkglibexec_SCRIPTS) +- + doveadm_module_LTLIBRARIES = \ + lib20_doveadm_fts_plugin.la + +--- dovecot-2.3.21+dfsg1.orig/src/plugins/fts/decode2text.sh ++++ /dev/null +@@ -1,105 +0,0 @@ +-#!/bin/sh +- +-# Example attachment decoder script. The attachment comes from stdin, and +-# the script is expected to output UTF-8 data to stdout. (If the output isn't +-# UTF-8, everything except valid UTF-8 sequences are dropped from it.) +- +-# The attachment decoding is enabled by setting: +-# +-# plugin { +-# fts_decoder = decode2text +-# } +-# service decode2text { +-# executable = script /usr/local/libexec/dovecot/decode2text.sh +-# user = dovecot +-# unix_listener decode2text { +-# mode = 0666 +-# } +-# } +- +-libexec_dir=`dirname $0` +-content_type=$1 +- +-# The second parameter is the format's filename extension, which is used when +-# found from a filename of application/octet-stream. You can also add more +-# extensions by giving more parameters. +-formats='application/pdf pdf +-application/x-pdf pdf +-application/msword doc +-application/mspowerpoint ppt +-application/vnd.ms-powerpoint ppt +-application/ms-excel xls +-application/x-msexcel xls +-application/vnd.ms-excel xls +-application/vnd.openxmlformats-officedocument.wordprocessingml.document docx +-application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx +-application/vnd.openxmlformats-officedocument.presentationml.presentation pptx +-application/vnd.oasis.opendocument.text odt +-application/vnd.oasis.opendocument.spreadsheet ods +-application/vnd.oasis.opendocument.presentation odp +-' +- +-if [ "$content_type" = "" ]; then +- echo "$formats" +- exit 0 +-fi +- +-fmt=`echo "$formats" | grep -w "^$content_type" | cut -d ' ' -f 2` +-if [ "$fmt" = "" ]; then +- echo "Content-Type: $content_type not supported" >&2 +- exit 1 +-fi +- +-# most decoders can't handle stdin directly, so write the attachment +-# to a temp file +-path=`mktemp` +-trap "rm -f $path" 0 1 2 3 14 15 +-cat > $path +- +-xmlunzip() { +- name=$1 +- +- tempdir=`mktemp -d` +- if [ "$tempdir" = "" ]; then +- exit 1 +- fi +- trap "rm -rf $path $tempdir" 0 1 2 3 14 15 +- cd $tempdir || exit 1 +- unzip -q "$path" 2>/dev/null || exit 0 +- find . -name "$name" -print0 | xargs -0 cat | +- $libexec_dir/xml2text +-} +- +-wait_timeout() { +- childpid=$! +- trap "kill -9 $childpid; rm -f $path" 1 2 3 14 15 +- wait $childpid +-} +- +-LANG=en_US.UTF-8 +-export LANG +-if [ $fmt = "pdf" ]; then +- /usr/bin/pdftotext $path - 2>/dev/null& +- wait_timeout 2>/dev/null +-elif [ $fmt = "doc" ]; then +- (/usr/bin/catdoc $path; true) 2>/dev/null& +- wait_timeout 2>/dev/null +-elif [ $fmt = "ppt" ]; then +- (/usr/bin/catppt $path; true) 2>/dev/null& +- wait_timeout 2>/dev/null +-elif [ $fmt = "xls" ]; then +- (/usr/bin/xls2csv $path; true) 2>/dev/null& +- wait_timeout 2>/dev/null +-elif [ $fmt = "odt" -o $fmt = "ods" -o $fmt = "odp" ]; then +- xmlunzip "content.xml" +-elif [ $fmt = "docx" ]; then +- xmlunzip "document.xml" +-elif [ $fmt = "xlsx" ]; then +- xmlunzip "sharedStrings.xml" +-elif [ $fmt = "pptx" ]; then +- xmlunzip "slide*.xml" +-else +- echo "Buggy decoder script: $fmt not handled" >&2 +- exit 1 +-fi +-exit 0 diff --git a/debian/patches/CVE-2025-59032.patch b/debian/patches/CVE-2025-59032.patch new file mode 100644 index 000000000..71efa4ae5 --- /dev/null +++ b/debian/patches/CVE-2025-59032.patch @@ -0,0 +1,28 @@ +From efb68fac3a9d2d04d38c4ab14dd570cf0c23923c Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 22 Dec 2025 22:25:04 +0200 +Subject: [PATCH] managesieve-login: Fix crash when command didn't finish on + the first call + +--- + src/managesieve-login/client.c | 9 +++++---- + 1 file changed, 5 insertions(+), 4 deletions(-) + +--- dovecot-2.3.21+dfsg1.orig/pigeonhole/src/managesieve-login/client.c ++++ dovecot-2.3.21+dfsg1/pigeonhole/src/managesieve-login/client.c +@@ -330,10 +330,12 @@ static bool managesieve_client_input_nex + if (args[0].type != MANAGESIEVE_ARG_EOL) + ret = -1; + } +- } +- if (ret > 0) { ++ if (ret > 0) ++ ret = client->cmd->func(client, args); ++ } else { ++ /* Continue unfinished command */ + i_assert(client->cmd != NULL); +- ret = client->cmd->func(client, args); ++ ret = client->cmd->func(client, NULL); + } + + if (ret != 0) diff --git a/debian/patches/CVE-2026-0394-1.patch b/debian/patches/CVE-2026-0394-1.patch new file mode 100644 index 000000000..a21fd3aae --- /dev/null +++ b/debian/patches/CVE-2026-0394-1.patch @@ -0,0 +1,66 @@ +From c4fbf9a46ebabb7a580087033ee1b841e52d905e Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 13 Nov 2024 11:38:30 +0200 +Subject: [PATCH] auth: db-passwd-file - Add db_passwd_fix_path() + +--- + src/auth/db-passwd-file.c | 25 +++++++++++++++++++++++++ + src/auth/db-passwd-file.h | 2 ++ + 2 files changed, 27 insertions(+) + +Index: 2.3/src/auth/db-passwd-file.c +=================================================================== +--- 2.3.orig/src/auth/db-passwd-file.c ++++ 2.3/src/auth/db-passwd-file.c +@@ -14,6 +14,7 @@ + #include "str.h" + #include "eacces-error.h" + #include "ioloop.h" ++#include "path-util.h" + + #include + #include +@@ -420,6 +421,30 @@ void db_passwd_file_unref(struct db_pass + i_free(db); + } + ++int db_passwd_fix_path(const char *path, const char **path_r, ++ const char *orig_path, const char **error_r) ++{ ++ /* normalize path */ ++ const char *normalized; ++ if (t_normpath(path, &normalized, error_r) < 0) ++ return -1; ++ ++ /* check base path */ ++ const char *p; ++ if (*orig_path != '%' && ++ (p = strchr(orig_path, '%')) != NULL) { ++ ptrdiff_t len = p - orig_path; ++ if (strncmp(orig_path, normalized, len) != 0) { ++ *error_r = t_strdup_printf("Path is outside '%s'", ++ t_strdup_until(orig_path, p)); ++ return -1; ++ } ++ } ++ ++ *path_r = normalized; ++ return 0; ++} ++ + static const char * + path_fix(const char *path, + const struct auth_request *auth_request ATTR_UNUSED) +Index: 2.3/src/auth/db-passwd-file.h +=================================================================== +--- 2.3.orig/src/auth/db-passwd-file.h ++++ 2.3/src/auth/db-passwd-file.h +@@ -45,6 +45,8 @@ struct db_passwd_file { + bool userdb_warn_missing:1; + }; + ++int db_passwd_fix_path(const char *path, const char **path_r, ++ const char *orig_path, const char **error_r); + int db_passwd_file_lookup(struct db_passwd_file *db, + struct auth_request *request, + const char *username_format, diff --git a/debian/patches/CVE-2026-0394-2.patch b/debian/patches/CVE-2026-0394-2.patch new file mode 100644 index 000000000..3267af140 --- /dev/null +++ b/debian/patches/CVE-2026-0394-2.patch @@ -0,0 +1,59 @@ +From 7fb773cffa3d78b587c406ebfeaa5a1e911a1835 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 13 Nov 2024 11:42:05 +0200 +Subject: [PATCH] auth: db-passwd-file - Normalize path with + db_passwd_fix_path() + +Otherwise there is a chance that we leak one ../ +--- + src/auth/db-passwd-file.c | 22 ++++++---------------- + 1 file changed, 6 insertions(+), 16 deletions(-) + +Index: dovecot/src/auth/db-passwd-file.c +=================================================================== +--- dovecot.orig/src/auth/db-passwd-file.c ++++ dovecot/src/auth/db-passwd-file.c +@@ -445,21 +445,6 @@ int db_passwd_fix_path(const char *path, + return 0; + } + +-static const char * +-path_fix(const char *path, +- const struct auth_request *auth_request ATTR_UNUSED) +-{ +- const char *p; +- +- p = strchr(path, '/'); +- if (p == NULL) +- return path; +- +- /* most likely this is an invalid request. just cut off the '/' and +- everything after it. */ +- return t_strdup_until(path, p); +-} +- + int db_passwd_file_lookup(struct db_passwd_file *db, + struct auth_request *request, + const char *username_format, +@@ -474,14 +459,19 @@ int db_passwd_file_lookup(struct db_pass + pw = db->default_file; + else { + dest = t_str_new(256); +- if (auth_request_var_expand(dest, db->path, request, path_fix, ++ if (auth_request_var_expand(dest, db->path, request, NULL, + &error) <= 0) { + e_error(authdb_event(request), + "Failed to expand passwd-file path %s: %s", + db->path, error); + return -1; + } +- ++ const char *path; ++ if (db_passwd_fix_path(str_c(dest), &path, db->path, &error) < 0) { ++ e_info(authdb_event(request), "Failed to normalize path: %s", ++ error); ++ return 0; ++ } + pw = hash_table_lookup(db->files, str_c(dest)); + if (pw == NULL) { + /* doesn't exist yet. create lookup for it. */ diff --git a/debian/patches/CVE-2026-27855-1.patch b/debian/patches/CVE-2026-27855-1.patch new file mode 100644 index 000000000..0184bec37 --- /dev/null +++ b/debian/patches/CVE-2026-27855-1.patch @@ -0,0 +1,23 @@ +From f1bc3ea8ba747fcfe14ab56685d400e4e3cff130 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Mon, 9 Mar 2026 20:04:27 +0200 +Subject: [PATCH 21/24] auth: cache - Use translated username in + auth_cache_remove() + +--- + src/auth/auth-cache.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +Index: dovecot/src/auth/auth-cache.c +=================================================================== +--- dovecot.orig/src/auth/auth-cache.c ++++ dovecot/src/auth/auth-cache.c +@@ -472,7 +472,7 @@ void auth_cache_remove(struct auth_cache + { + struct auth_cache_node *node; + +- key = auth_request_expand_cache_key(request, key, request->fields.user); ++ key = auth_request_expand_cache_key(request, key, request->fields.translated_username); + node = hash_table_lookup(cache->hash, key); + if (node == NULL) + return; diff --git a/debian/patches/CVE-2026-27855-2.patch b/debian/patches/CVE-2026-27855-2.patch new file mode 100644 index 000000000..36cc08884 --- /dev/null +++ b/debian/patches/CVE-2026-27855-2.patch @@ -0,0 +1,91 @@ +From f2119a25a439f599b0a30fc674300b8f354e5b67 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 11 Mar 2026 12:30:32 +0200 +Subject: [PATCH 22/24] auth: Move passdb event lifecycle handling to + auth_request_passdb_event_(begin|end) + +--- + +From f2119a25a439f599b0a30fc674300b8f354e5b67 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 11 Mar 2026 12:30:32 +0200 +Subject: [PATCH 22/24] auth: Move passdb event lifecycle handling to + auth_request_passdb_event_(begin|end) + +--- + src/auth/auth-request.c | 39 +++++++++++++++++++++++++++------------ + src/auth/auth-request.h | 3 +++ + 2 files changed, 30 insertions(+), 12 deletions(-) + +Index: dovecot/src/auth/auth-request.c +=================================================================== +--- dovecot.orig/src/auth/auth-request.c ++++ dovecot/src/auth/auth-request.c +@@ -621,7 +621,7 @@ auth_request_cache_result_to_str(enum au + } + } + +-void auth_request_passdb_lookup_begin(struct auth_request *request) ++void auth_request_passdb_event_begin(struct auth_request *request) + { + struct event *event; + const char *name; +@@ -648,11 +648,33 @@ void auth_request_passdb_lookup_begin(st + else if (*request->passdb->set->auth_verbose == 'n') + event_set_min_log_level(event, LOG_TYPE_WARNING); + ++ array_push_back(&request->authdb_event, &event); ++} ++ ++void auth_request_passdb_event_end(struct auth_request *request) ++{ ++ struct event *event = authdb_event(request); ++ event_unref(&event); ++ array_pop_back(&request->authdb_event); ++ ++ /* restore protocol-specific settings */ ++ request->set = global_auth_settings; ++} ++ ++void auth_request_passdb_lookup_begin(struct auth_request *request) ++{ ++ struct event *event; ++ ++ i_assert(request->passdb != NULL); ++ i_assert(!request->userdb_lookup); ++ auth_request_passdb_event_begin(request); ++ ++ event = authdb_event(request); ++ + e_debug(event_create_passthrough(event)-> + set_name("auth_passdb_request_started")-> + event(), + "Performing passdb lookup"); +- array_push_back(&request->authdb_event, &event); + } + + void auth_request_passdb_lookup_end(struct auth_request *request, +@@ -668,8 +690,7 @@ void auth_request_passdb_lookup_end(stru + request->set->cache_ttl != 0 && request->set->cache_size != 0) + e->add_str("cache", auth_request_cache_result_to_str(request->passdb_cache_result)); + e_debug(e->event(), "Finished passdb lookup"); +- event_unref(&event); +- array_pop_back(&request->authdb_event); ++ auth_request_passdb_event_end(request); + } + + void auth_request_userdb_lookup_begin(struct auth_request *request) +Index: dovecot/src/auth/auth-request.h +=================================================================== +--- dovecot.orig/src/auth/auth-request.h ++++ dovecot/src/auth/auth-request.h +@@ -373,6 +373,9 @@ void auth_request_master_user_login_fini + const char *auth_request_get_log_prefix_db(struct auth_request *auth_request); + void auth_request_fields_init(struct auth_request *request); + ++void auth_request_passdb_event_begin(struct auth_request *request); ++void auth_request_passdb_event_end(struct auth_request *request); ++ + void auth_request_passdb_lookup_begin(struct auth_request *request); + void auth_request_passdb_lookup_end(struct auth_request *request, + enum passdb_result result); diff --git a/debian/patches/CVE-2026-27855-3.patch b/debian/patches/CVE-2026-27855-3.patch new file mode 100644 index 000000000..7326c7609 --- /dev/null +++ b/debian/patches/CVE-2026-27855-3.patch @@ -0,0 +1,56 @@ +From 8fd279db61be2054c8f3e7275766717318a2df91 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Mon, 9 Mar 2026 21:23:29 +0200 +Subject: [PATCH 23/24] auth: Initialize set_credentials event properly + +Fixes update_query +--- + +From 8fd279db61be2054c8f3e7275766717318a2df91 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Mon, 9 Mar 2026 21:23:29 +0200 +Subject: [PATCH 23/24] auth: Initialize set_credentials event properly + +Fixes update_query +--- + src/auth/auth-request.c | 4 ++++ + src/auth/auth-worker-server.c | 2 ++ + 2 files changed, 6 insertions(+) + +Index: 2.3/src/auth/auth-request.c +=================================================================== +--- 2.3.orig/src/auth/auth-request.c ++++ 2.3/src/auth/auth-request.c +@@ -1341,6 +1341,8 @@ void auth_request_set_credentials(struct + struct auth_passdb *passdb = request->passdb; + const char *cache_key, *new_credentials; + ++ auth_request_passdb_event_begin(request); ++ + cache_key = passdb_cache == NULL ? NULL : passdb->cache_key; + if (cache_key != NULL) + auth_cache_remove(passdb_cache, request, cache_key); +@@ -1357,6 +1359,8 @@ void auth_request_set_credentials(struct + /* this passdb doesn't support credentials update */ + callback(FALSE, request); + } ++ ++ auth_request_passdb_event_end(request); + } + + static void auth_request_userdb_save_cache(struct auth_request *request, +Index: 2.3/src/auth/auth-worker-client.c +=================================================================== +--- 2.3.orig/src/auth/auth-worker-client.c ++++ 2.3/src/auth/auth-worker-client.c +@@ -490,8 +490,10 @@ auth_worker_handle_setcred(struct auth_w + } + } + ++ auth_request_passdb_event_begin(auth_request); + auth_request->passdb->passdb->iface. + set_credentials(auth_request, creds, set_credentials_callback); ++ auth_request_passdb_event_end(auth_request); + return TRUE; + } + diff --git a/debian/patches/CVE-2026-27855-4.patch b/debian/patches/CVE-2026-27855-4.patch new file mode 100644 index 000000000..303b2615e --- /dev/null +++ b/debian/patches/CVE-2026-27855-4.patch @@ -0,0 +1,27 @@ +From 535a14209810e7b0c8f94479daf580ac4c637904 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 11 Mar 2026 12:46:53 +0200 +Subject: [PATCH 24/24] auth: passdb-sql - Require update_query to be set when + used + +--- + src/auth/passdb-sql.c | 6 ++++++ + 1 file changed, 6 insertions(+) + +Index: dovecot/src/auth/passdb-sql.c +=================================================================== +--- dovecot.orig/src/auth/passdb-sql.c ++++ dovecot/src/auth/passdb-sql.c +@@ -247,6 +247,12 @@ static void sql_set_credentials(struct a + return; + } + ++ if (*query == '\0') { ++ e_error(authdb_event(request), "update_query is empty"); ++ callback(FALSE, request); ++ return; ++ } ++ + sql_request = i_new(struct passdb_sql_request, 1); + sql_request->auth_request = request; + sql_request->callback.set_credentials = callback; diff --git a/debian/patches/CVE-2026-27856-1.patch b/debian/patches/CVE-2026-27856-1.patch new file mode 100644 index 000000000..9824ec34e --- /dev/null +++ b/debian/patches/CVE-2026-27856-1.patch @@ -0,0 +1,51 @@ +From 6ef282fab8af8faec75b0ff8cb87b11094642ba0 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 4 Mar 2026 08:05:13 +0200 +Subject: [PATCH 16/24] doveadm: client-connection - Use timing safe credential + check + +--- + src/doveadm/client-connection-http.c | 7 +++++-- + src/doveadm/client-connection-tcp.c | 4 +--- + 2 files changed, 6 insertions(+), 5 deletions(-) + +Index: dovecot/src/doveadm/client-connection-http.c +=================================================================== +--- dovecot.orig/src/doveadm/client-connection-http.c ++++ dovecot/src/doveadm/client-connection-http.c +@@ -984,7 +984,9 @@ doveadm_http_server_auth_basic(struct cl + value = p_strdup_printf(conn->conn.pool, + "doveadm:%s", set->doveadm_password); + base64_encode(value, strlen(value), b64_value); +- if (creds->data != NULL && strcmp(creds->data, str_c(b64_value)) == 0) ++ ++ if (creds->data != NULL && ++ str_equals_timing_almost_safe(value, creds->data)) + return TRUE; + + i_error("Invalid authentication attempt to HTTP API " +@@ -1009,7 +1011,8 @@ doveadm_http_server_auth_api_key(struct + b64_value = str_new(conn->conn.pool, 32); + base64_encode(set->doveadm_api_key, + strlen(set->doveadm_api_key), b64_value); +- if (creds->data != NULL && strcmp(creds->data, str_c(b64_value)) == 0) ++ if (creds->data != NULL && ++ str_equals_timing_almost_safe(creds->data, str_c(b64_value))) + return TRUE; + + i_error("Invalid authentication attempt to HTTP API " +Index: dovecot/src/doveadm/client-connection-tcp.c +=================================================================== +--- dovecot.orig/src/doveadm/client-connection-tcp.c ++++ dovecot/src/doveadm/client-connection-tcp.c +@@ -342,9 +342,7 @@ client_connection_tcp_authenticate(struc + return -1; + } + pass = t_strndup(data + 9, size - 9); +- if (strlen(pass) != strlen(set->doveadm_password) || +- !mem_equals_timing_safe(pass, set->doveadm_password, +- strlen(pass))) { ++ if (!str_equals_timing_almost_safe(pass, set->doveadm_password)) { + i_error("doveadm client authenticated with wrong password"); + return -1; + } diff --git a/debian/patches/CVE-2026-27856-2.patch b/debian/patches/CVE-2026-27856-2.patch new file mode 100644 index 000000000..50a874b3e --- /dev/null +++ b/debian/patches/CVE-2026-27856-2.patch @@ -0,0 +1,51 @@ +From 6d8c50154744284304ceeab69c3951e6d6852007 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 4 Mar 2026 09:28:18 +0200 +Subject: [PATCH 17/24] doveadm: Use datastack for temporary b64 value + +There is no need to allocate it from connection pool. +--- + src/doveadm/client-connection-http.c | 14 +++++--------- + 1 file changed, 5 insertions(+), 9 deletions(-) + +Index: dovecot/src/doveadm/client-connection-http.c +=================================================================== +--- dovecot.orig/src/doveadm/client-connection-http.c ++++ dovecot/src/doveadm/client-connection-http.c +@@ -972,7 +972,7 @@ doveadm_http_server_auth_basic(struct cl + struct client_connection_http *conn = req->conn; + const struct doveadm_settings *set = conn->conn.set; + string_t *b64_value; +- char *value; ++ const char *value; + + if (*set->doveadm_password == '\0') { + i_error("Invalid authentication attempt to HTTP API: " +@@ -980,13 +980,11 @@ doveadm_http_server_auth_basic(struct cl + return FALSE; + } + +- b64_value = str_new(conn->conn.pool, 32); +- value = p_strdup_printf(conn->conn.pool, +- "doveadm:%s", set->doveadm_password); +- base64_encode(value, strlen(value), b64_value); ++ value = t_strdup_printf("doveadm:%s", set->doveadm_password); ++ b64_value = t_base64_encode_str(0, UINT_MAX, value); + + if (creds->data != NULL && +- str_equals_timing_almost_safe(value, creds->data)) ++ str_equals_timing_almost_safe(str_c(b64_value), creds->data)) + return TRUE; + + i_error("Invalid authentication attempt to HTTP API " +@@ -1008,9 +1006,7 @@ doveadm_http_server_auth_api_key(struct + return FALSE; + } + +- b64_value = str_new(conn->conn.pool, 32); +- base64_encode(set->doveadm_api_key, +- strlen(set->doveadm_api_key), b64_value); ++ b64_value = t_base64_encode_str(0, UINT_MAX, set->doveadm_api_key); + if (creds->data != NULL && + str_equals_timing_almost_safe(creds->data, str_c(b64_value))) + return TRUE; diff --git a/debian/patches/CVE-2026-27856-3.patch b/debian/patches/CVE-2026-27856-3.patch new file mode 100644 index 000000000..0554559ec --- /dev/null +++ b/debian/patches/CVE-2026-27856-3.patch @@ -0,0 +1,23 @@ +From cc628976210de17bd26b4d9761ea879ce78b7980 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 4 Mar 2026 14:39:43 +0200 +Subject: [PATCH 18/24] doveadm: client-connection - Get API key from + per-connection settings + +--- + src/doveadm/client-connection-http.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +Index: dovecot/src/doveadm/client-connection-http.c +=================================================================== +--- dovecot.orig/src/doveadm/client-connection-http.c ++++ dovecot/src/doveadm/client-connection-http.c +@@ -997,7 +997,7 @@ doveadm_http_server_auth_api_key(struct + const struct http_auth_credentials *creds) + { + struct client_connection_http *conn = req->conn; +- const struct doveadm_settings *set = doveadm_settings; ++ const struct doveadm_settings *set = conn->conn.set; + string_t *b64_value; + + if (*set->doveadm_api_key == '\0') { diff --git a/debian/patches/CVE-2026-27857-1.patch b/debian/patches/CVE-2026-27857-1.patch new file mode 100644 index 000000000..44bdb2aa4 --- /dev/null +++ b/debian/patches/CVE-2026-27857-1.patch @@ -0,0 +1,24 @@ +From bc91df5f91e8b83c58f138799d39be687271106d Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 6 Mar 2026 17:06:45 +0200 +Subject: [PATCH 1/2] plugins: imap-filter-sieve: imap-filter-sieve - Adjust to + imap_parser_create() API change + +--- + src/plugins/imap-filter-sieve/cmd-filter-sieve.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +Index: dovecot/pigeonhole/src/plugins/imap-filter-sieve/cmd-filter-sieve.c +=================================================================== +--- dovecot.orig/pigeonhole/src/plugins/imap-filter-sieve/cmd-filter-sieve.c ++++ dovecot/pigeonhole/src/plugins/imap-filter-sieve/cmd-filter-sieve.c +@@ -379,7 +379,8 @@ bool cmd_filter_sieve(struct client_comm + asynchronously the same way as APPEND does. */ + client->input_lock = cmd; + ctx->parser = imap_parser_create(client->input, client->output, +- client->set->imap_max_line_length); ++ client->set->imap_max_line_length, ++ NULL); + if (client->set->imap_literal_minus) + imap_parser_enable_literal_minus(ctx->parser); + o_stream_unset_flush_callback(client->output); diff --git a/debian/patches/CVE-2026-27857-2.patch b/debian/patches/CVE-2026-27857-2.patch new file mode 100644 index 000000000..712b27003 --- /dev/null +++ b/debian/patches/CVE-2026-27857-2.patch @@ -0,0 +1,279 @@ +From ad8e6542518acee4e1a803001b232c03732cdaff Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 6 Mar 2026 15:25:14 +0200 +Subject: [PATCH 12/24] lib-imap, global: Add params parameter to + imap_parser_create() + +--- + src/imap-login/imap-login-client.c | 4 ++-- + src/imap-login/imap-login-cmd-id.c | 4 +++- + src/imap/cmd-append.c | 3 ++- + src/imap/cmd-setmetadata.c | 3 ++- + src/imap/imap-client.c | 3 ++- + src/lib-imap-client/imapc-connection.c | 3 ++- + src/lib-imap-storage/imap-msgpart.c | 2 +- + src/lib-imap/imap-bodystructure.c | 4 ++-- + src/lib-imap/imap-envelope.c | 2 +- + src/lib-imap/imap-parser.c | 3 ++- + src/lib-imap/imap-parser.h | 6 +++++- + src/lib-imap/test-imap-parser.c | 10 +++++----- + src/plugins/virtual/virtual-config.c | 2 +- + 13 files changed, 30 insertions(+), 19 deletions(-) + +Index: 2.3/src/imap-login/imap-login-client.c +=================================================================== +--- 2.3.orig/src/imap-login/imap-login-client.c ++++ 2.3/src/imap-login/imap-login-client.c +@@ -379,7 +379,7 @@ static void imap_client_create(struct cl + imap_client->parser = + imap_parser_create(imap_client->common.input, + imap_client->common.output, +- IMAP_LOGIN_MAX_LINE_LENGTH); ++ IMAP_LOGIN_MAX_LINE_LENGTH, NULL); + if (imap_client->set->imap_literal_minus) + imap_parser_enable_literal_minus(imap_client->parser); + client->io = io_add_istream(client->input, client_input, client); +@@ -416,7 +416,7 @@ static void imap_client_starttls(struct + imap_client->parser = + imap_parser_create(imap_client->common.input, + imap_client->common.output, +- IMAP_LOGIN_MAX_LINE_LENGTH); ++ IMAP_LOGIN_MAX_LINE_LENGTH, NULL); + + /* CRLF is lost from buffer when streams are reopened. */ + imap_client->skip_line = FALSE; +Index: 2.3/src/imap-login/imap-login-cmd-id.c +=================================================================== +--- 2.3.orig/src/imap-login/imap-login-cmd-id.c ++++ 2.3/src/imap-login/imap-login-cmd-id.c +@@ -237,7 +237,8 @@ int cmd_id(struct imap_client *client) + client->cmd_id = id = i_new(struct imap_client_cmd_id, 1); + id->parser = imap_parser_create(client->common.input, + client->common.output, +- IMAP_LOGIN_MAX_LINE_LENGTH); ++ IMAP_LOGIN_MAX_LINE_LENGTH, ++ NULL); + if (client->set->imap_literal_minus) + imap_parser_enable_literal_minus(id->parser); + parser_flags = IMAP_PARSE_FLAG_STOP_AT_LIST; +Index: 2.3/src/imap/cmd-append.c +=================================================================== +--- 2.3.orig/src/imap/cmd-append.c ++++ 2.3/src/imap/cmd-append.c +@@ -947,7 +947,8 @@ bool cmd_append(struct client_command_co + o_stream_unset_flush_callback(client->output); + + ctx->save_parser = imap_parser_create(client->input, client->output, +- client->set->imap_max_line_length); ++ client->set->imap_max_line_length, ++ NULL); + if (client->set->imap_literal_minus) + imap_parser_enable_literal_minus(ctx->save_parser); + +Index: 2.3/src/imap/cmd-setmetadata.c +=================================================================== +--- 2.3.orig/src/imap/cmd-setmetadata.c ++++ 2.3/src/imap/cmd-setmetadata.c +@@ -289,7 +289,8 @@ cmd_setmetadata_start(struct imap_setmet + asynchronously the same way as APPEND does. */ + client->input_lock = cmd; + ctx->parser = imap_parser_create(client->input, client->output, +- client->set->imap_max_line_length); ++ client->set->imap_max_line_length, ++ NULL); + if (client->set->imap_literal_minus) + imap_parser_enable_literal_minus(ctx->parser); + o_stream_unset_flush_callback(client->output); +Index: 2.3/src/imap/imap-client.c +=================================================================== +--- 2.3.orig/src/imap/imap-client.c ++++ 2.3/src/imap/imap-client.c +@@ -953,7 +953,8 @@ client_command_new(struct client *client + } else { + cmd->parser = + imap_parser_create(client->input, client->output, +- client->set->imap_max_line_length); ++ client->set->imap_max_line_length, ++ NULL); + if (client->set->imap_literal_minus) + imap_parser_enable_literal_minus(cmd->parser); + } +Index: 2.3/src/lib-imap-client/imapc-connection.c +=================================================================== +--- 2.3.orig/src/lib-imap-client/imapc-connection.c ++++ 2.3/src/lib-imap-client/imapc-connection.c +@@ -1822,7 +1822,8 @@ static void imapc_connection_connect_nex + o_stream_set_flush_callback(conn->output, imapc_connection_connected, + conn); + conn->parser = imap_parser_create(conn->input, NULL, +- conn->client->set.max_line_length); ++ conn->client->set.max_line_length, ++ NULL); + conn->to = timeout_add(conn->client->set.connect_timeout_msecs, + imapc_connection_timeout, conn); + conn->to_output = timeout_add(conn->client->set.max_idle_time*1000, +Index: 2.3/src/lib-imap-storage/imap-msgpart.c +=================================================================== +--- 2.3.orig/src/lib-imap-storage/imap-msgpart.c ++++ 2.3/src/lib-imap-storage/imap-msgpart.c +@@ -148,7 +148,7 @@ imap_msgpart_get_header_fields(pool_t po + int result = 0; + + input = i_stream_create_from_data(header_list, strlen(header_list)); +- parser = imap_parser_create(input, NULL, SIZE_MAX); ++ parser = imap_parser_create(input, NULL, SIZE_MAX, NULL); + + if (imap_parser_finish_line(parser, 0, 0, &args) > 0 && + imap_arg_get_list_full(args, &hdr_list, &list_count) && +Index: 2.3/src/lib-imap/imap-bodystructure.c +=================================================================== +--- 2.3.orig/src/lib-imap/imap-bodystructure.c ++++ 2.3/src/lib-imap/imap-bodystructure.c +@@ -694,7 +694,7 @@ int imap_bodystructure_parse_full(const + input = i_stream_create_from_data(bodystructure, strlen(bodystructure)); + (void)i_stream_read(input); + +- parser = imap_parser_create(input, NULL, SIZE_MAX); ++ parser = imap_parser_create(input, NULL, SIZE_MAX, NULL); + ret = imap_parser_finish_line(parser, 0, + IMAP_PARSE_FLAG_LITERAL_TYPE, &args); + if (ret < 0) { +@@ -937,7 +937,7 @@ int imap_body_parse_from_bodystructure(c + input = i_stream_create_from_data(bodystructure, strlen(bodystructure)); + (void)i_stream_read(input); + +- parser = imap_parser_create(input, NULL, SIZE_MAX); ++ parser = imap_parser_create(input, NULL, SIZE_MAX, NULL); + ret = imap_parser_finish_line(parser, 0, IMAP_PARSE_FLAG_NO_UNESCAPE | + IMAP_PARSE_FLAG_LITERAL_TYPE, &args); + if (ret < 0) { +Index: 2.3/src/lib-imap/imap-envelope.c +=================================================================== +--- 2.3.orig/src/lib-imap/imap-envelope.c ++++ 2.3/src/lib-imap/imap-envelope.c +@@ -222,7 +222,7 @@ bool imap_envelope_parse(const char *env + input = i_stream_create_from_data(envelope, strlen(envelope)); + (void)i_stream_read(input); + +- parser = imap_parser_create(input, NULL, SIZE_MAX); ++ parser = imap_parser_create(input, NULL, SIZE_MAX, NULL); + ret = imap_parser_finish_line(parser, 0, + IMAP_PARSE_FLAG_LITERAL_TYPE, &args); + if (ret < 0) { +Index: 2.3/src/lib-imap/imap-parser.c +=================================================================== +--- 2.3.orig/src/lib-imap/imap-parser.c ++++ 2.3/src/lib-imap/imap-parser.c +@@ -69,7 +69,8 @@ struct imap_parser { + + struct imap_parser * + imap_parser_create(struct istream *input, struct ostream *output, +- size_t max_line_size) ++ size_t max_line_size, ++ const struct imap_parser_params *params ATTR_UNUSED) + { + struct imap_parser *parser; + +Index: 2.3/src/lib-imap/imap-parser.h +=================================================================== +--- 2.3.orig/src/lib-imap/imap-parser.h ++++ 2.3/src/lib-imap/imap-parser.h +@@ -38,6 +38,9 @@ enum imap_parser_error { + IMAP_PARSE_ERROR_LITERAL_TOO_BIG + }; + ++struct imap_parser_params { ++}; ++ + struct imap_parser; + + /* Create new IMAP argument parser. output is used for sending command +@@ -53,7 +56,8 @@ struct imap_parser; + 2 * max_line_size. */ + struct imap_parser * + imap_parser_create(struct istream *input, struct ostream *output, +- size_t max_line_size) ATTR_NULL(2); ++ size_t max_line_size, ++ const struct imap_parser_params *params); + void imap_parser_ref(struct imap_parser *parser); + void imap_parser_unref(struct imap_parser **parser); + +Index: 2.3/src/lib-imap/test-imap-parser.c +=================================================================== +--- 2.3.orig/src/lib-imap/test-imap-parser.c ++++ 2.3/src/lib-imap/test-imap-parser.c +@@ -16,7 +16,7 @@ static void test_imap_parser_crlf(void) + + test_begin("imap parser crlf handling"); + input = test_istream_create(test_input); +- parser = imap_parser_create(input, NULL, 1024); ++ parser = imap_parser_create(input, NULL, 1024, NULL); + + /* must return -2 until LF is read */ + for (i = 0; test_input[i] != '\n'; i++) { +@@ -60,7 +60,7 @@ static void test_imap_parser_partial_lis + + test_begin("imap parser partial list"); + input = test_istream_create(test_input); +- parser = imap_parser_create(input, NULL, 1024); ++ parser = imap_parser_create(input, NULL, 1024, NULL); + + (void)i_stream_read(input); + test_assert(imap_parser_read_args(parser, 0, +@@ -123,7 +123,7 @@ static void test_imap_parser_read_tag_cm + if (tests[i].type != COMMAND) { + input = test_istream_create(tests[i].input); + test_assert(i_stream_read(input) > 0); +- parser = imap_parser_create(input, NULL, 1024); ++ parser = imap_parser_create(input, NULL, 1024, NULL); + ret = imap_parser_read_tag(parser, &atom); + test_assert_idx(ret == tests[i].ret, i); + test_assert_idx(ret <= 0 || strcmp(tests[i].tag, atom) == 0, i); +@@ -134,7 +134,7 @@ static void test_imap_parser_read_tag_cm + if (tests[i].type != TAG) { + input = test_istream_create(tests[i].input); + test_assert(i_stream_read(input) > 0); +- parser = imap_parser_create(input, NULL, 1024); ++ parser = imap_parser_create(input, NULL, 1024, NULL); + ret = imap_parser_read_command_name(parser, &atom); + test_assert_idx(ret == tests[i].ret, i); + test_assert_idx(ret <= 0 || strcmp(tests[i].tag, atom) == 0, i); +Index: 2.3/src/plugins/virtual/virtual-config.c +=================================================================== +--- 2.3.orig/src/plugins/virtual/virtual-config.c ++++ 2.3/src/plugins/virtual/virtual-config.c +@@ -54,7 +54,7 @@ virtual_search_args_parse(const string_t + input = i_stream_create_from_data(str_data(rule), str_len(rule)); + (void)i_stream_read(input); + +- imap_parser = imap_parser_create(input, NULL, SIZE_MAX); ++ imap_parser = imap_parser_create(input, NULL, SIZE_MAX, NULL); + ret = imap_parser_finish_line(imap_parser, 0, 0, &args); + if (ret < 0) { + sargs = NULL; +Index: 2.3/src/lib-imap/imap-id.c +=================================================================== +--- 2.3.orig/src/lib-imap/imap-id.c ++++ 2.3/src/lib-imap/imap-id.c +@@ -111,7 +111,7 @@ const char *imap_id_reply_generate(const + input = i_stream_create_from_data(settings, strlen(settings)); + (void)i_stream_read(input); + +- parser = imap_parser_create(input, NULL, SIZE_MAX); ++ parser = imap_parser_create(input, NULL, SIZE_MAX, NULL); + if (imap_parser_finish_line(parser, 0, 0, &args) <= 0) + ret = "NIL"; + else +Index: 2.3/src/director/director-test.c +=================================================================== +--- 2.3.orig/src/director/director-test.c ++++ 2.3/src/director/director-test.c +@@ -250,7 +250,7 @@ static void imap_client_create(int fd) + o_stream_set_no_error_handling(client->output, TRUE); + client->io = io_add(fd, IO_READ, imap_client_input, client); + client->parser = +- imap_parser_create(client->input, client->output, 4096); ++ imap_parser_create(client->input, client->output, 4096, NULL); + o_stream_nsend_str(client->output, + "* OK [CAPABILITY IMAP4rev1] director-test ready.\r\n"); + DLLIST_PREPEND(&imap_clients, client); diff --git a/debian/patches/CVE-2026-27857-3.patch b/debian/patches/CVE-2026-27857-3.patch new file mode 100644 index 000000000..925e8481f --- /dev/null +++ b/debian/patches/CVE-2026-27857-3.patch @@ -0,0 +1,160 @@ +From bb845a61f81be978f641b79058d4bae06f27ee39 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 6 Mar 2026 15:32:29 +0200 +Subject: [PATCH 13/24] lib-imap: Add imap_parser_params.list_count_limit + +--- + src/lib-imap/imap-parser.c | 15 ++++++++++- + src/lib-imap/imap-parser.h | 6 +++++ + src/lib-imap/test-imap-parser.c | 46 +++++++++++++++++++++++++++++++++ + 3 files changed, 66 insertions(+), 1 deletion(-) + +Index: dovecot/src/lib-imap/imap-parser.c +=================================================================== +--- dovecot.orig/src/lib-imap/imap-parser.c ++++ dovecot/src/lib-imap/imap-parser.c +@@ -39,6 +39,7 @@ struct imap_parser { + struct istream *input; + struct ostream *output; + size_t max_line_size; ++ unsigned int list_count_limit; + enum imap_parser_flags flags; + + /* reset by imap_parser_reset(): */ +@@ -46,6 +47,7 @@ struct imap_parser { + ARRAY_TYPE(imap_arg_list) root_list; + ARRAY_TYPE(imap_arg_list) *cur_list; + struct imap_arg *list_arg; ++ unsigned int list_count; + + enum arg_parse_type cur_type; + size_t cur_pos; /* parser position in input buffer */ +@@ -70,7 +72,7 @@ struct imap_parser { + struct imap_parser * + imap_parser_create(struct istream *input, struct ostream *output, + size_t max_line_size, +- const struct imap_parser_params *params ATTR_UNUSED) ++ const struct imap_parser_params *params) + { + struct imap_parser *parser; + +@@ -81,6 +83,10 @@ imap_parser_create(struct istream *input + parser->input = input; + parser->output = output; + parser->max_line_size = max_line_size; ++ if (params != NULL && params->list_count_limit > 0) ++ parser->list_count_limit = params->list_count_limit; ++ else ++ parser->list_count_limit = UINT_MAX; + + p_array_init(&parser->root_list, parser->pool, LIST_INIT_COUNT); + parser->cur_list = &parser->root_list; +@@ -122,6 +128,7 @@ void imap_parser_reset(struct imap_parse + p_array_init(&parser->root_list, parser->pool, LIST_INIT_COUNT); + parser->cur_list = &parser->root_list; + parser->list_arg = NULL; ++ parser->list_count = 0; + + parser->cur_type = ARG_PARSE_NONE; + parser->cur_pos = 0; +@@ -210,6 +217,12 @@ static bool imap_parser_close_list(struc + parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX; + return FALSE; + } ++ if (parser->list_count >= parser->list_count_limit) { ++ parser->error_msg = "Too many '('"; ++ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX; ++ return FALSE; ++ } ++ parser->list_count++; + + arg = imap_arg_create(parser); + arg->type = IMAP_ARG_EOL; +Index: dovecot/src/lib-imap/imap-parser.h +=================================================================== +--- dovecot.orig/src/lib-imap/imap-parser.h ++++ dovecot/src/lib-imap/imap-parser.h +@@ -39,6 +39,12 @@ enum imap_parser_error { + }; + + struct imap_parser_params { ++ /* How many open lists ('(' chars) to allow before faililng the parsing. ++ 0 means unlimited. This is mainly used to prevent excessive memory ++ usage in imap-login process. In imap process there are many other ++ ways to increase memory usage, so we let the max_line_size be the ++ only limit. */ ++ unsigned int list_count_limit; + }; + + struct imap_parser; +Index: dovecot/src/lib-imap/test-imap-parser.c +=================================================================== +--- dovecot.orig/src/lib-imap/test-imap-parser.c ++++ dovecot/src/lib-imap/test-imap-parser.c +@@ -2,6 +2,7 @@ + + #include "lib.h" + #include "istream.h" ++#include "istream-chain.h" + #include "imap-parser.h" + #include "test-common.h" + +@@ -79,6 +80,50 @@ static void test_imap_parser_partial_lis + test_end(); + } + ++static void test_imap_parser_list_limit(void) ++{ ++ struct { ++ const char *input; ++ int ret; ++ } tests[] = { ++ { "(())\r\n", 1 }, ++ { "((()))\r\n", -1 }, ++ }; ++ struct istream_chain *chain; ++ struct istream *chain_input; ++ struct imap_parser *parser; ++ const struct imap_arg *args; ++ ++ test_begin("imap parser list limit"); ++ struct imap_parser_params params = { ++ .list_count_limit = 2, ++ }; ++ ++ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) { ++ chain_input = i_stream_create_chain(&chain, IO_BLOCK_SIZE); ++ parser = imap_parser_create(chain_input, NULL, 1024, ¶ms); ++ ++ for (unsigned int j = 0; j < 2; j++) { ++ struct istream *input = ++ test_istream_create(tests[i].input); ++ i_stream_chain_append(chain, input); ++ i_stream_unref(&input); ++ ++ (void)i_stream_read(chain_input); ++ ++ test_assert_cmp(imap_parser_read_args(parser, 0, 0, &args), ==, tests[i].ret); ++ /* skip over CRLF */ ++ i_stream_skip(chain_input, i_stream_get_data_size(chain_input)); ++ ++ /* make sure parser reset works */ ++ imap_parser_reset(parser); ++ } ++ imap_parser_unref(&parser); ++ i_stream_destroy(&chain_input); ++ } ++ test_end(); ++} ++ + static void test_imap_parser_read_tag_cmd(void) + { + enum read_type { +@@ -150,6 +195,7 @@ int main(void) + static void (*const test_functions[])(void) = { + test_imap_parser_crlf, + test_imap_parser_partial_list, ++ test_imap_parser_list_limit, + test_imap_parser_read_tag_cmd, + NULL + }; diff --git a/debian/patches/CVE-2026-27857-4.patch b/debian/patches/CVE-2026-27857-4.patch new file mode 100644 index 000000000..93c4f038c --- /dev/null +++ b/debian/patches/CVE-2026-27857-4.patch @@ -0,0 +1,84 @@ +From ab084588f615b7f2bb6e18c8ee9176511b647aca Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Fri, 6 Mar 2026 15:35:12 +0200 +Subject: [PATCH 14/24] imap-login: Limit the number of open IMAP parser lists + +This prevents attackers from using a large number of '(' in a command to +grow memory usage excessively. +--- + src/imap-login/imap-login-client.c | 10 ++++++++-- + src/imap-login/imap-login-client.h | 4 ++++ + src/imap-login/imap-login-cmd-id.c | 6 +++++- + 3 files changed, 17 insertions(+), 3 deletions(-) + +Index: 2.3/src/imap-login/imap-login-client.c +=================================================================== +--- 2.3.orig/src/imap-login/imap-login-client.c ++++ 2.3/src/imap-login/imap-login-client.c +@@ -375,11 +375,14 @@ static void imap_client_create(struct cl + { + struct imap_client *imap_client = (struct imap_client *)client; + ++ struct imap_parser_params params = { ++ .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT, ++ }; + imap_client->set = other_sets[0]; + imap_client->parser = + imap_parser_create(imap_client->common.input, + imap_client->common.output, +- IMAP_LOGIN_MAX_LINE_LENGTH, NULL); ++ IMAP_LOGIN_MAX_LINE_LENGTH, ¶ms); + if (imap_client->set->imap_literal_minus) + imap_parser_enable_literal_minus(imap_client->parser); + client->io = io_add_istream(client->input, client_input, client); +@@ -412,11 +415,14 @@ static void imap_client_starttls(struct + { + struct imap_client *imap_client = (struct imap_client *)client; + ++ struct imap_parser_params params = { ++ .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT, ++ }; + imap_parser_unref(&imap_client->parser); + imap_client->parser = + imap_parser_create(imap_client->common.input, + imap_client->common.output, +- IMAP_LOGIN_MAX_LINE_LENGTH, NULL); ++ IMAP_LOGIN_MAX_LINE_LENGTH, ¶ms); + + /* CRLF is lost from buffer when streams are reopened. */ + imap_client->skip_line = FALSE; +Index: 2.3/src/imap-login/imap-login-client.h +=================================================================== +--- 2.3.orig/src/imap-login/imap-login-client.h ++++ 2.3/src/imap-login/imap-login-client.h +@@ -11,6 +11,10 @@ + /* maximum length for IMAP command line. */ + #define IMAP_LOGIN_MAX_LINE_LENGTH 8192 + ++/* Maximum number of '(' allowed in an IMAP command. Pre-login only uses ++ lists in the ID command. */ ++#define IMAP_LOGIN_LIST_COUNT_LIMIT 1 ++ + enum imap_client_id_state { + IMAP_CLIENT_ID_STATE_LIST = 0, + IMAP_CLIENT_ID_STATE_KEY, +Index: 2.3/src/imap-login/imap-login-cmd-id.c +=================================================================== +--- 2.3.orig/src/imap-login/imap-login-cmd-id.c ++++ 2.3/src/imap-login/imap-login-cmd-id.c +@@ -235,10 +235,14 @@ int cmd_id(struct imap_client *client) + + if (client->cmd_id == NULL) { + client->cmd_id = id = i_new(struct imap_client_cmd_id, 1); ++ ++ struct imap_parser_params params = { ++ .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT, ++ }; + id->parser = imap_parser_create(client->common.input, + client->common.output, + IMAP_LOGIN_MAX_LINE_LENGTH, +- NULL); ++ ¶ms); + if (client->set->imap_literal_minus) + imap_parser_enable_literal_minus(id->parser); + parser_flags = IMAP_PARSE_FLAG_STOP_AT_LIST; diff --git a/debian/patches/CVE-2026-27857-5.patch b/debian/patches/CVE-2026-27857-5.patch new file mode 100644 index 000000000..036c604d9 --- /dev/null +++ b/debian/patches/CVE-2026-27857-5.patch @@ -0,0 +1,59 @@ +From e0b953af76d754c2984c2867b71fccfef5231bf5 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 2 Mar 2026 13:50:24 +0200 +Subject: [PATCH 15/24] global: Use const for struct imap_parser_params params + +--- + src/imap-login/imap-login-client.c | 4 ++-- + src/imap-login/imap-login-cmd-id.c | 2 +- + src/lib-imap/test-imap-parser.c | 2 +- + 3 files changed, 4 insertions(+), 4 deletions(-) + +Index: 2.3/src/imap-login/imap-login-client.c +=================================================================== +--- 2.3.orig/src/imap-login/imap-login-client.c ++++ 2.3/src/imap-login/imap-login-client.c +@@ -375,7 +375,7 @@ static void imap_client_create(struct cl + { + struct imap_client *imap_client = (struct imap_client *)client; + +- struct imap_parser_params params = { ++ const struct imap_parser_params params = { + .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT, + }; + imap_client->set = other_sets[0]; +@@ -415,7 +415,7 @@ static void imap_client_starttls(struct + { + struct imap_client *imap_client = (struct imap_client *)client; + +- struct imap_parser_params params = { ++ const struct imap_parser_params params = { + .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT, + }; + imap_parser_unref(&imap_client->parser); +Index: 2.3/src/imap-login/imap-login-cmd-id.c +=================================================================== +--- 2.3.orig/src/imap-login/imap-login-cmd-id.c ++++ 2.3/src/imap-login/imap-login-cmd-id.c +@@ -236,7 +236,7 @@ int cmd_id(struct imap_client *client) + if (client->cmd_id == NULL) { + client->cmd_id = id = i_new(struct imap_client_cmd_id, 1); + +- struct imap_parser_params params = { ++ const struct imap_parser_params params = { + .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT, + }; + id->parser = imap_parser_create(client->common.input, +Index: 2.3/src/lib-imap/test-imap-parser.c +=================================================================== +--- 2.3.orig/src/lib-imap/test-imap-parser.c ++++ 2.3/src/lib-imap/test-imap-parser.c +@@ -95,7 +95,7 @@ static void test_imap_parser_list_limit( + const struct imap_arg *args; + + test_begin("imap parser list limit"); +- struct imap_parser_params params = { ++ const struct imap_parser_params params = { + .list_count_limit = 2, + }; + diff --git a/debian/patches/CVE-2026-27858.patch b/debian/patches/CVE-2026-27858.patch new file mode 100644 index 000000000..1843cb2ad --- /dev/null +++ b/debian/patches/CVE-2026-27858.patch @@ -0,0 +1,28 @@ +From fcb339d39bfb19abf1bb88711112f49c1c589b1a Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 2 Mar 2026 14:40:57 +0200 +Subject: [PATCH 2/2] managesieve-login: Verify AUTHENTICATE initial response + size isn't too large + +This prevents DoSing the managesieve-login by sending an excessively large +initial response size, which causes a huge memory allocation. +--- + src/managesieve-login/client-authenticate.c | 5 +++++ + 1 file changed, 5 insertions(+) + +Index: dovecot/pigeonhole/src/managesieve-login/client-authenticate.c +=================================================================== +--- dovecot.orig/pigeonhole/src/managesieve-login/client-authenticate.c ++++ dovecot/pigeonhole/src/managesieve-login/client-authenticate.c +@@ -189,6 +189,11 @@ static int managesieve_client_auth_read_ + if ( i_stream_get_size + (msieve_client->auth_response_input, FALSE, &resp_size) <= 0 ) + resp_size = 0; ++ else if (resp_size > LOGIN_MAX_AUTH_BUF_SIZE) { ++ client_destroy(client, ++ "Authentication response too large"); ++ return -1; ++ } + + if (client->auth_response == NULL) + client->auth_response = str_new(default_pool, I_MAX(resp_size+1, 256)); diff --git a/debian/patches/CVE-2026-27859.patch b/debian/patches/CVE-2026-27859.patch new file mode 100644 index 000000000..e3816e080 --- /dev/null +++ b/debian/patches/CVE-2026-27859.patch @@ -0,0 +1,103 @@ +From 4041a0cb7bbaa6faf38670ab92afc1039dd0d0d5 Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Tue, 24 Feb 2026 13:11:14 +0200 +Subject: [PATCH 03/24] lib-mail: Limit the number of RFC2231 parameters that + can be parsed + +This avoids excessive CPU usage especially in result_append(). +--- + src/lib-mail/rfc2231-parser.c | 4 +++- + src/lib-mail/rfc822-parser.h | 5 +++++ + src/lib-mail/test-rfc2231-parser.c | 30 ++++++++++++++++++++++++++++++ + 3 files changed, 38 insertions(+), 1 deletion(-) + +Index: dovecot/src/lib-mail/rfc2231-parser.c +=================================================================== +--- dovecot.orig/src/lib-mail/rfc2231-parser.c ++++ dovecot/src/lib-mail/rfc2231-parser.c +@@ -45,7 +45,7 @@ int rfc2231_parse(struct rfc822_parser_c + const struct rfc2231_parameter *rfc2231_params; + const char *key, *p, *p2; + string_t *str; +- unsigned int i, j, count, next, next_idx; ++ unsigned int i, j, count, next, next_idx, params_count = 0; + bool ok, have_extended, broken = FALSE; + const char *prev_replacement_str; + int ret; +@@ -63,6 +63,8 @@ int rfc2231_parse(struct rfc822_parser_c + t_array_init(&rfc2231_params_arr, 8); + str = t_str_new(64); + while ((ret = rfc822_parse_content_param(ctx, &key, str)) != 0) { ++ if (++params_count > RFC2231_MAX_PARAMS) ++ break; + if (ret < 0) { + /* try to continue anyway.. */ + broken = TRUE; +Index: dovecot/src/lib-mail/rfc822-parser.h +=================================================================== +--- dovecot.orig/src/lib-mail/rfc822-parser.h ++++ dovecot/src/lib-mail/rfc822-parser.h +@@ -3,6 +3,11 @@ + + #include "unichar.h" + ++/* Maximum number of parameters to parse. After this the rest of the parameters ++ are skipped. This is to avoid excessive CPU usage that can be caused by ++ merging of these parameters. */ ++#define RFC2231_MAX_PARAMS 128 ++ + /* This can be used as a common NUL replacement character */ + #define RFC822_NUL_REPLACEMENT_STR UNICODE_REPLACEMENT_CHAR_UTF8 + +Index: dovecot/src/lib-mail/test-rfc2231-parser.c +=================================================================== +--- dovecot.orig/src/lib-mail/test-rfc2231-parser.c ++++ dovecot/src/lib-mail/test-rfc2231-parser.c +@@ -1,6 +1,7 @@ + /* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */ + + #include "lib.h" ++#include "str.h" + #include "rfc822-parser.h" + #include "rfc2231-parser.h" + #include "test-common.h" +@@ -41,10 +42,39 @@ static void test_rfc2231_parser(void) + test_end(); + } + ++static void test_rfc2231_parser_limits(void) ++{ ++ string_t *input = t_str_new(1024); ++ ++ test_begin("rfc2231 parser limits"); ++ str_append(input, "; "); ++ for (unsigned int i = 0; i < 1100; i++) ++ str_printfa(input, "a%u=b%u; ", i, i); ++ struct rfc822_parser_context parser; ++ const char *const *result; ++ rfc822_parser_init(&parser, str_data(input), str_len(input), NULL); ++ test_assert(rfc2231_parse(&parser, &result) == 0); ++ ++ unsigned int count = str_array_length(result); ++ test_assert(count == RFC2231_MAX_PARAMS * 2); ++ for (unsigned int i = 0; i < count; i += 2) { ++ str_truncate(input, 0); ++ str_printfa(input, "a%u", i / 2); ++ test_assert_strcmp_idx(result[i], str_c(input), i); ++ ++ str_truncate(input, 0); ++ str_printfa(input, "b%u", i / 2); ++ test_assert_strcmp_idx(result[i + 1], str_c(input), i); ++ } ++ rfc822_parser_deinit(&parser); ++ test_end(); ++} ++ + int main(void) + { + static void (*const test_functions[])(void) = { + test_rfc2231_parser, ++ test_rfc2231_parser_limits, + NULL + }; + return test_run(test_functions); diff --git a/debian/patches/CVE-2026-33603.patch b/debian/patches/CVE-2026-33603.patch new file mode 100644 index 000000000..2ef0a94c3 --- /dev/null +++ b/debian/patches/CVE-2026-33603.patch @@ -0,0 +1,36 @@ +From c1c53885bda550632b944dd305013cd010e0e058 Mon Sep 17 00:00:00 2001 +From: Aki Tuomi +Date: Wed, 8 Apr 2026 11:33:11 +0300 +Subject: [PATCH] login-common: Only accept base64 in sasl + +--- + src/login-common/client-common-auth.c | 9 +++++++++ + 1 file changed, 9 insertions(+) + +Index: 2.3/src/login-common/client-common-auth.c +=================================================================== +--- 2.3.orig/src/login-common/client-common-auth.c ++++ 2.3/src/login-common/client-common-auth.c +@@ -3,6 +3,7 @@ + #include "hostpid.h" + #include "login-common.h" + #include "array.h" ++#include "base64.h" + #include "iostream.h" + #include "istream.h" + #include "ostream.h" +@@ -649,6 +650,14 @@ client_auth_handle_reply(struct client * + + void client_auth_respond(struct client *client, const char *response) + { ++ /* Only accept base64 */ ++ for (size_t i = 0; response[i] != '\0'; i++) { ++ if (!base64_is_valid_char(response[i]) && response[i] != '=') { ++ client_auth_fail(client, "Invalid base64 in response"); ++ return; ++ } ++ } ++ + client->auth_waiting = FALSE; + client_set_auth_waiting(client); + auth_client_request_continue(client->auth_request, response); diff --git a/debian/patches/CVE-2026-40016.patch b/debian/patches/CVE-2026-40016.patch new file mode 100644 index 000000000..157e16f45 --- /dev/null +++ b/debian/patches/CVE-2026-40016.patch @@ -0,0 +1,255 @@ +From 5b0ed9d1034c023d3daf218b6b8656f0cdd383dc Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Sun, 19 Apr 2026 23:10:29 +0000 +Subject: [PATCH] lib-sieve: Enforce CPU time limit within :contains and + :matches matcher loops + +The naive O(N*M) substring search in mcht-contains.c and the naive find loop +in mcht-matches.c can run for hours on a large value (e.g. a message body), +completely bypassing sieve_max_cpu_time because that limit was only checked +between bytecode operations. + +Expose the active CPU limit via sieve_runtime_cpu_limit_exceeded() and poll +it every 4096 inner iterations. When the limit is hit the match returns +SIEVE_EXEC_RESOURCE_LIMIT, matching the existing behavior at the bytecode +boundary. This is a minimal safety net ahead of switching the matchers to +algorithms that do not require it. +--- + src/lib-sieve/mcht-contains.c | 21 ++++++++++++ + src/lib-sieve/mcht-matches.c | 57 +++++++++++++++++++++++++++---- + src/lib-sieve/sieve-interpreter.c | 19 +++++++++++ + src/lib-sieve/sieve-interpreter.h | 12 +++++++ + 4 files changed, 103 insertions(+), 6 deletions(-) + +Index: 2.3/pigeonhole/src/lib-sieve/mcht-contains.c +=================================================================== +--- 2.3.orig/pigeonhole/src/lib-sieve/mcht-contains.c ++++ 2.3/pigeonhole/src/lib-sieve/mcht-contains.c +@@ -8,6 +8,7 @@ + + #include "sieve-match-types.h" + #include "sieve-comparators.h" ++#include "sieve-interpreter.h" + #include "sieve-match.h" + + #include +@@ -38,7 +39,14 @@ const struct sieve_match_type_def contai + + /* FIXME: Naive substring match implementation. Should switch to more + * efficient algorithm if large values need to be searched (e.g. message body). ++ * ++ * The inner loop polls the interpreter CPU time limit periodically so that a ++ * single O(N*M) match on a large value cannot run for many times the ++ * configured sieve_max_cpu_time (which is otherwise only checked between ++ * bytecode operations). + */ ++#define SIEVE_CONTAINS_CPU_CHECK_INTERVAL 4096 ++ + static int mcht_contains_match_key + (struct sieve_match_context *mctx, const char *val, size_t val_size, + const char *key, size_t key_size) +@@ -48,6 +56,7 @@ static int mcht_contains_match_key + const char *kend = (const char *) key + key_size; + const char *vp = val; + const char *kp = key; ++ unsigned int counter = 0; + + if ( val_size == 0 ) + return ( key_size == 0 ? 1 : 0 ); +@@ -58,6 +67,18 @@ static int mcht_contains_match_key + while ( (vp < vend) && (kp < kend) ) { + if ( !cmp->def->char_match(cmp, &vp, vend, &kp, kend) ) + vp++; ++ ++ if ( ++counter >= SIEVE_CONTAINS_CPU_CHECK_INTERVAL ) { ++ counter = 0; ++ if ( sieve_runtime_cpu_limit_exceeded(mctx->runenv) ) { ++ sieve_runtime_error( ++ mctx->runenv, NULL, ++ "execution exceeded CPU time limit"); ++ mctx->exec_status = ++ SIEVE_EXEC_RESOURCE_LIMIT; ++ return -1; ++ } ++ } + } + + return ( kp == kend ? 1 : 0 ); +Index: 2.3/pigeonhole/src/lib-sieve/mcht-matches.c +=================================================================== +--- 2.3.orig/pigeonhole/src/lib-sieve/mcht-matches.c ++++ 2.3/pigeonhole/src/lib-sieve/mcht-matches.c +@@ -9,6 +9,7 @@ + + #include "sieve-match-types.h" + #include "sieve-comparators.h" ++#include "sieve-interpreter.h" + #include "sieve-match.h" + + #include +@@ -46,16 +47,38 @@ const struct sieve_match_type_def matche + #endif + + /* FIXME: Naive implementation, substitute this with dovecot src/lib/str-find.c ++ * ++ * The inner loop polls the interpreter CPU time limit periodically so that a ++ * single O(N*M) search on a large value cannot run for many times the ++ * configured sieve_max_cpu_time. Returns 1 on match, 0 on exhaustion, or -1 ++ * when the CPU time limit was exceeded (mctx->exec_status is set). + */ +-static inline bool _string_find(const struct sieve_comparator *cmp, +- const char **valp, const char *vend, const char **keyp, const char *kend) ++#define SIEVE_MATCHES_CPU_CHECK_INTERVAL 4096 ++ ++static int ++_string_find(struct sieve_match_context *mctx, ++ const struct sieve_comparator *cmp, ++ const char **valp, const char *vend, ++ const char **keyp, const char *kend, ++ unsigned int *counter) + { + while ( (*valp < vend) && (*keyp < kend) ) { + if ( !cmp->def->char_match(cmp, valp, vend, keyp, kend) ) + (*valp)++; ++ if (++(*counter) >= SIEVE_MATCHES_CPU_CHECK_INTERVAL) { ++ *counter = 0; ++ if (sieve_runtime_cpu_limit_exceeded(mctx->runenv)) { ++ sieve_runtime_error( ++ mctx->runenv, NULL, ++ "execution exceeded CPU time limit"); ++ mctx->exec_status = ++ SIEVE_EXEC_RESOURCE_LIMIT; ++ return -1; ++ } ++ } + } + +- return (*keyp == kend); ++ return (*keyp == kend ? 1 : 0); + } + + static char _scan_key_section +@@ -93,6 +116,7 @@ static int mcht_matches_match_key + char wcard = '\0'; /* Current wildcard */ + char next_wcard = '\0'; /* Next widlcard */ + unsigned int key_offset = 0; ++ unsigned int counter = 0; + + if ( cmp->def == NULL || cmp->def->char_match == NULL ) + return 0; +@@ -134,6 +158,19 @@ static int mcht_matches_match_key + while (kp < kend && vp < vend ) { + const char *needle, *nend; + ++ if (++counter >= SIEVE_MATCHES_CPU_CHECK_INTERVAL) { ++ counter = 0; ++ if (sieve_runtime_cpu_limit_exceeded(mctx->runenv)) { ++ sieve_runtime_error( ++ mctx->runenv, NULL, ++ "execution exceeded CPU time limit"); ++ mctx->exec_status = ++ SIEVE_EXEC_RESOURCE_LIMIT; ++ sieve_match_values_abort(&mvalues); ++ return -1; ++ } ++ } ++ + if ( !backtrack ) { + /* Search the next '*' wildcard in the key string */ + +@@ -268,10 +305,20 @@ static int mcht_matches_match_key + + /* Match may happen at any offset (>= key offset): find substring */ + vp += key_offset; +- if ( (vp >= vend) || !_string_find(cmp, &vp, vend, &needle, nend) ) { ++ if (vp >= vend) { + debug_printf(" failed to find needle at an offset\n"); + break; + } ++ int fres = _string_find(mctx, cmp, &vp, vend, ++ &needle, nend, &counter); ++ if (fres < 0) { ++ sieve_match_values_abort(&mvalues); ++ return -1; ++ } ++ if (fres == 0) { ++ debug_printf(" failed to find needle at an offset\n"); ++ break; ++ } + + prv = vp - str_len(section); + prk = kp; +Index: 2.3/pigeonhole/src/lib-sieve/sieve-interpreter.c +=================================================================== +--- 2.3.orig/pigeonhole/src/lib-sieve/sieve-interpreter.c ++++ 2.3/pigeonhole/src/lib-sieve/sieve-interpreter.c +@@ -85,6 +85,13 @@ struct sieve_interpreter { + struct sieve_runtime_trace trace; + struct sieve_resource_usage rusage; + ++ /* CPU time limit for the current sieve_interpreter_continue() call; ++ NULL when no limit is configured or not currently executing. Exposed ++ via sieve_runtime_cpu_limit_exceeded() so long-running runtime code ++ can enforce the limit without waiting for the next bytecode ++ boundary. */ ++ struct cpu_limit *climit; ++ + /* Current operation */ + struct sieve_operation oprtn; + +@@ -362,6 +369,15 @@ sieve_interpreter_svinst(struct sieve_in + return interp->runenv.exec_env->svinst; + } + ++bool sieve_runtime_cpu_limit_exceeded(const struct sieve_runtime_env *renv) ++{ ++ struct sieve_interpreter *interp = renv->interp; ++ ++ if (interp->climit == NULL) ++ return FALSE; ++ return cpu_limit_exceeded(interp->climit); ++} ++ + /* Do not use this function for normal sieve extensions. This is intended for + * the testsuite only. + */ +@@ -939,6 +955,7 @@ int sieve_interpreter_continue(struct si + climit = cpu_limit_init(svinst->max_cpu_time_secs, + CPU_LIMIT_TYPE_USER); + } ++ interp->climit = climit; + + while (ret == SIEVE_EXEC_OK && !interp->interrupted && + *address < sieve_binary_block_get_size(renv->sblock)) { +@@ -959,6 +976,8 @@ int sieve_interpreter_continue(struct si + ret = sieve_interpreter_operation_execute(interp); + } + ++ interp->climit = NULL; ++ + if (climit != NULL) { + sieve_resource_usage_init(&rusage); + rusage.cpu_time_msecs = +Index: 2.3/pigeonhole/src/lib-sieve/sieve-interpreter.h +=================================================================== +--- 2.3.orig/pigeonhole/src/lib-sieve/sieve-interpreter.h ++++ 2.3/pigeonhole/src/lib-sieve/sieve-interpreter.h +@@ -165,6 +165,18 @@ int sieve_interpreter_run(struct sieve_i + struct sieve_result *result); + + /* ++ * CPU limit ++ */ ++ ++/* Returns TRUE if the current interpreter execution has exceeded its CPU ++ time limit (sieve_max_cpu_time). Callable from within long-running runtime ++ code (e.g. matcher inner loops) so that limit enforcement is not deferred ++ until the next bytecode boundary. Returns FALSE if no limit is active or ++ no execution is currently in progress. Cheap: does not call getrusage() ++ on each invocation. */ ++bool sieve_runtime_cpu_limit_exceeded(const struct sieve_runtime_env *renv); ++ ++/* + * Error handling + */ + diff --git a/debian/patches/CVE-2026-40020-1.patch b/debian/patches/CVE-2026-40020-1.patch new file mode 100644 index 000000000..e32e076d0 --- /dev/null +++ b/debian/patches/CVE-2026-40020-1.patch @@ -0,0 +1,75 @@ +From b7daa4104ff064c1fb549540cc9d96c2d9e2509c Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 22 Apr 2026 15:43:58 +0300 +Subject: [PATCH 1/3] acl: Add acl_id_is_valid() + +Returns TRUE if the ACL identifier string is at most ACL_ID_MAX_LEN +(1024) bytes long, contains no control characters and is valid UTF-8. +--- + src/plugins/acl/acl-rights.c | 16 ++++++++++++++++ + src/plugins/acl/acl-rights.h | 6 ++++++ + 2 files changed, 22 insertions(+) + +Index: 2.3/src/plugins/acl/acl-api.c +=================================================================== +--- 2.3.orig/src/plugins/acl/acl-api.c ++++ 2.3/src/plugins/acl/acl-api.c +@@ -3,6 +3,7 @@ + #include "lib.h" + #include "array.h" + #include "str.h" ++#include "unichar.h" + #include "strescape.h" + #include "hash.h" + #include "mail-user.h" +@@ -11,6 +12,8 @@ + #include "acl-cache.h" + #include "acl-api-private.h" + ++#include ++ + struct acl_letter_map { + char letter; + const char *name; +@@ -269,6 +272,19 @@ int acl_backend_nonowner_lookups_rebuild + return backend->v.nonowner_lookups_rebuild(backend); + } + ++bool acl_id_is_valid(const char *id) ++{ ++ size_t len = strlen(id); ++ ++ if (len > ACL_ID_MAX_LEN) ++ return FALSE; ++ for (size_t i = 0; i < len; i++) { ++ if (i_iscntrl(id[i])) ++ return FALSE; ++ } ++ return uni_utf8_data_is_valid((const unsigned char *)id, len); ++} ++ + void acl_rights_write_id(string_t *dest, const struct acl_rights *right) + { + switch (right->id_type) { +Index: 2.3/src/plugins/acl/acl-api.h +=================================================================== +--- 2.3.orig/src/plugins/acl/acl-api.h ++++ 2.3/src/plugins/acl/acl-api.h +@@ -35,6 +35,8 @@ struct acl_object; + #define MAILBOX_ATTRIBUTE_PREFIX_ACL \ + MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT"acl/" + ++#define ACL_ID_MAX_LEN 1024 ++ + /* ACL identifiers in override order */ + enum acl_id_type { + /* Anyone's rights, including anonymous's. +@@ -164,4 +166,8 @@ int acl_object_list_deinit(struct acl_ob + /* Returns the canonical ID for the right. */ + const char *acl_rights_get_id(const struct acl_rights *right); + ++/* Returns TRUE if the ACL identifier string is valid: no longer than ++ ACL_ID_MAX_LEN bytes, no control characters and valid UTF-8. */ ++bool acl_id_is_valid(const char *id); ++ + #endif diff --git a/debian/patches/CVE-2026-40020-2.patch b/debian/patches/CVE-2026-40020-2.patch new file mode 100644 index 000000000..d4c15902b --- /dev/null +++ b/debian/patches/CVE-2026-40020-2.patch @@ -0,0 +1,28 @@ +From 20b48c3db5fed7ccaa8e0a4c10ca54f6dc36a63d Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 22 Apr 2026 15:44:24 +0300 +Subject: [PATCH 2/3] imap-acl: Fail if ACL identifier is invalid + +Reject invalid identifiers early in imap_acl_identifier_parse() using +acl_id_is_valid(). This prevents CR/LF injection and rejects identifiers +that are too long, contain control characters or are not valid UTF-8. +--- + src/plugins/imap-acl/imap-acl-plugin.c | 5 +++++ + 1 file changed, 5 insertions(+) + +Index: 2.3/src/plugins/imap-acl/imap-acl-plugin.c +=================================================================== +--- 2.3.orig/src/plugins/imap-acl/imap-acl-plugin.c ++++ 2.3/src/plugins/imap-acl/imap-acl-plugin.c +@@ -802,6 +802,11 @@ imap_acl_identifier_parse(struct client_ + { + struct mail_user *user = cmd->client->user; + ++ if (!acl_id_is_valid(id)) { ++ *client_error_r = "Invalid identifier"; ++ return -1; ++ } ++ + if (str_begins(id, IMAP_ACL_GLOBAL_PREFIX)) { + *client_error_r = t_strdup_printf("Global ACLs can't be modified: %s", + id); diff --git a/debian/patches/CVE-2026-40020-3.patch b/debian/patches/CVE-2026-40020-3.patch new file mode 100644 index 000000000..523bc18a3 --- /dev/null +++ b/debian/patches/CVE-2026-40020-3.patch @@ -0,0 +1,23 @@ +From 1cf6ad1a119e5dace816e401e73ba6cc11d1472e Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Wed, 22 Apr 2026 15:45:00 +0300 +Subject: [PATCH 3/3] acl: Assert-crash if ACL identifier is invalid before + writing it + +It should have been checked earlier already. +--- + src/plugins/acl/acl-backend-vfile-update.c | 1 + + 1 file changed, 1 insertion(+) + +Index: 2.3/src/plugins/acl/acl-backend-vfile-update.c +=================================================================== +--- 2.3.orig/src/plugins/acl/acl-backend-vfile-update.c ++++ 2.3/src/plugins/acl/acl-backend-vfile-update.c +@@ -116,6 +116,7 @@ vfile_write_right(string_t *dest, const + if (neg) str_append_c(dest,'-'); + acl_rights_write_id(dest, right); + ++ i_assert(acl_id_is_valid(str_c(dest))); + if (strchr(str_c(dest), ' ') != NULL) T_BEGIN { + /* need to escape it */ + const char *escaped = t_strdup(str_escape(str_c(dest))); diff --git a/debian/patches/CVE-2026-42006.patch b/debian/patches/CVE-2026-42006.patch new file mode 100644 index 000000000..9d9c4e0f2 --- /dev/null +++ b/debian/patches/CVE-2026-42006.patch @@ -0,0 +1,101 @@ +From da1438c76b797f055d4ad7f0eaa17e5e29ca31ee Mon Sep 17 00:00:00 2001 +From: Timo Sirainen +Date: Mon, 27 Apr 2026 17:40:46 +0300 +Subject: [PATCH] lib-imap: Fix imap_parser_params.list_count_limit to actually + work + +The previous fix in d0f67b52914565a35f3817335ab9633cb291513c was +accidentally limiting the number of ')', not the number of '('. +--- + src/lib-imap/imap-parser.c | 19 +++++++++++-------- + src/lib-imap/test-imap-parser.c | 14 ++++++++++++-- + 2 files changed, 23 insertions(+), 10 deletions(-) + +diff --git a/src/lib-imap/imap-parser.c b/src/lib-imap/imap-parser.c +index 6212aed33d..b1df3d7b78 100644 +--- a/src/lib-imap/imap-parser.c ++++ b/src/lib-imap/imap-parser.c +@@ -191,8 +191,15 @@ static struct imap_arg *imap_arg_create(struct imap_parser *parser) + return arg; + } + +-static void imap_parser_open_list(struct imap_parser *parser) ++static bool imap_parser_open_list(struct imap_parser *parser) + { ++ if (parser->list_count >= parser->list_count_limit) { ++ parser->error_msg = "Too many '('"; ++ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX; ++ return FALSE; ++ } ++ parser->list_count++; ++ + parser->list_arg = imap_arg_create(parser); + parser->list_arg->type = IMAP_ARG_LIST; + p_array_init(&parser->list_arg->_data.list, parser->pool, +@@ -200,6 +207,7 @@ static void imap_parser_open_list(struct imap_parser *parser) + parser->cur_list = &parser->list_arg->_data.list; + + parser->cur_type = ARG_PARSE_NONE; ++ return TRUE; + } + + static bool imap_parser_close_list(struct imap_parser *parser) +@@ -217,12 +225,6 @@ static bool imap_parser_close_list(struct imap_parser *parser) + parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX; + return FALSE; + } +- if (parser->list_count >= parser->list_count_limit) { +- parser->error_msg = "Too many '('"; +- parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX; +- return FALSE; +- } +- parser->list_count++; + + arg = imap_arg_create(parser); + arg->type = IMAP_ARG_EOL; +@@ -673,7 +675,8 @@ static bool imap_parser_read_arg(struct imap_parser *parser) + parser->literal8 = FALSE; + break; + case '(': +- imap_parser_open_list(parser); ++ if (!imap_parser_open_list(parser)) ++ return FALSE; + if ((parser->flags & IMAP_PARSE_FLAG_STOP_AT_LIST) != 0) { + i_stream_skip(parser->input, 1); + return FALSE; +diff --git a/src/lib-imap/test-imap-parser.c b/src/lib-imap/test-imap-parser.c +index fb7c308a23..d509c8176d 100644 +--- a/src/lib-imap/test-imap-parser.c ++++ b/src/lib-imap/test-imap-parser.c +@@ -85,9 +85,15 @@ static void test_imap_parser_list_limit(void) + struct { + const char *input; + int ret; ++ const char *error; + } tests[] = { +- { "(())\r\n", 1 }, +- { "((()))\r\n", -1 }, ++ { "(())\r\n", 1, NULL }, ++ { "((\r\n", -1, "Missing ')'" }, ++ { "(()\r\n", -1, "Missing ')'" }, ++ { "(()))\r\n", -1, "Unexpected ')'" }, ++ { "((()))\r\n", -1, "Too many '('" }, ++ { "(({10}\r\n", -2, NULL }, ++ { "((({10}\r\n", -1, "Too many '('" }, + }; + struct istream_chain *chain; + struct istream *chain_input; +@@ -112,6 +118,10 @@ static void test_imap_parser_list_limit(void) + (void)i_stream_read(chain_input); + + test_assert_cmp(imap_parser_read_args(parser, 0, 0, &args), ==, tests[i].ret); ++ if (tests[i].ret == -1) { ++ enum imap_parser_error err; ++ test_assert_strcmp_idx(imap_parser_get_error(parser, &err), tests[i].error, i); ++ } + /* skip over CRLF */ + i_stream_skip(chain_input, i_stream_get_data_size(chain_input)); + +-- +2.47.3 + diff --git a/debian/patches/series b/debian/patches/series index b32ab1d85..96704a8e1 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -25,3 +25,29 @@ Cleanup-temporary-build-files.patch Silence-prototype-conflicts.patch remove-500ms-idle-debounce.patch Skip-flaky-cpu-limit-tests.patch +CVE-2024-23184.patch +CVE-2024-23185.patch +CVE-2025-59031.patch +CVE-2025-59032.patch +CVE-2026-0394-1.patch +CVE-2026-0394-2.patch +CVE-2026-27856-1.patch +CVE-2026-27856-2.patch +CVE-2026-27856-3.patch +CVE-2026-27858.patch +CVE-2026-27859.patch +CVE-2026-40016.patch +CVE-2026-40020-1.patch +CVE-2026-40020-2.patch +CVE-2026-40020-3.patch +CVE-2026-27855-1.patch +CVE-2026-27855-2.patch +CVE-2026-27855-3.patch +CVE-2026-27855-4.patch +CVE-2026-33603.patch +CVE-2026-27857-1.patch +CVE-2026-27857-2.patch +CVE-2026-27857-3.patch +CVE-2026-27857-4.patch +CVE-2026-27857-5.patch +CVE-2026-42006.patch diff --git a/debian/rules b/debian/rules index bbefa51f6..8264271c0 100755 --- a/debian/rules +++ b/debian/rules @@ -163,7 +163,6 @@ override_dh_auto_install: $(MAKE) -C $(PIGEONHOLE_DIR) install DESTDIR=$(CORE_DIR) rm `find $(CURDIR)/debian -name '*.la'` rm $(CORE_DIR)/etc/dovecot/README - rm $(CORE_DIR)/usr/lib/dovecot/decode2text.sh override_dh_install: # dh_auto_install has installed everything in the dovecot-core package. diff --git a/debian/tests/control b/debian/tests/control index 496cfcc0b..1bd689025 100644 --- a/debian/tests/control +++ b/debian/tests/control @@ -6,9 +6,13 @@ Tests: systemd Depends: dovecot-core, systemd-sysv Test-Command: run-parts --report --exit-on-error debian/tests/usage -Depends: dovecot-imapd, dovecot-pop3d, python3 +Depends: dovecot-imapd, dovecot-pop3d, dovecot-managesieved, sieve-connect, python3 Restrictions: needs-root, breaks-testbed, allow-stderr Tests: testmails Restrictions: needs-root, breaks-testbed Depends: dovecot-imapd, dovecot-pop3d, lsb-release, python3, python3-passlib + +Tests: debian_bug_1134464 +Restrictions: needs-root, breaks-testbed, allow-stderr +Depends: dovecot-imapd diff --git a/debian/tests/debian_bug_1134464 b/debian/tests/debian_bug_1134464 new file mode 100755 index 000000000..9ab182a55 --- /dev/null +++ b/debian/tests/debian_bug_1134464 @@ -0,0 +1,54 @@ +#!/bin/sh + +set -e + +echo "Setting up dovecot for the test" +# Move aside 10-auth.conf to disable passwd-based auth +if [ -f /etc/dovecot/conf.d/10-auth.conf ]; then + mv /etc/dovecot/conf.d/10-auth.conf /etc/dovecot/conf.d/10-auth.conf.bak +fi + +cat >/etc/dovecot/local.conf <<-EOF + auth_mechanisms = plain + mail_location = maildir:~/Maildir + auth_verbose = yes + + passdb { + driver = passwd-file + args = username_format=%Ln /srv/dovecot/etc/vdomains/%Ld/passwd.%{if;%Ls;eq;smtp;%Ls;real} + } + + userdb { + driver = static + args = uid=nobody gid=nogroup home=/srv/dovecot-dep8/%u + } +EOF + +mkdir -p /srv/dovecot/etc/vdomains/example.com +echo 'dep8:$5$g5AtG9Qqne/tKFN9$dUEO/AaHIk8hvxZySialx3P7B0SlC1TF026IUIPQBY1:65534:65534:dovecot test user,,,:/srv/dovecot-dep8:/usr/sbin/nologin' > /srv/dovecot/etc/vdomains/example.com/passwd.real + +mkdir -p /srv/dovecot-dep8 +chown nobody:nogroup /srv/dovecot-dep8 + +echo "Restarting the service" +systemctl restart dovecot + +echo "Sending a test message via the LDA" +/usr/lib/dovecot/dovecot-lda -f "test@example.com" -d dep8@example.com < +Message-Id: +From: Test User +To: dep8 +Subject: DEP-8 test + +This is just a test +EOF + +echo "Verifying that the email was correctly delivered" +if [ -z "$(doveadm search -u dep8@example.com header message-id dep8-test-1@debian.org)" ]; then + echo "Message not found" + exit 1 +fi + +echo "Done" +echo diff --git a/debian/tests/usage/00_setup b/debian/tests/usage/00_setup index 2eeeb2f73..c5a6481fa 100755 --- a/debian/tests/usage/00_setup +++ b/debian/tests/usage/00_setup @@ -9,7 +9,7 @@ if [ -f /etc/dovecot/conf.d/10-auth.conf ]; then fi cat >/etc/dovecot/local.conf <<-EOF - auth_mechanisms = plain + auth_mechanisms = plain cram-md5 mail_location = maildir:~/Maildir passdb { diff --git a/debian/tests/usage/imap-cram-md5 b/debian/tests/usage/imap-cram-md5 new file mode 100755 index 000000000..a888e93ea --- /dev/null +++ b/debian/tests/usage/imap-cram-md5 @@ -0,0 +1,38 @@ +#!/usr/bin/python3 +import imaplib + +imaplib.Debug = 4 + +print("Testing IMAP") +print("Connecting") +client = imaplib.IMAP4('localhost') + +print("Checking for STARTTLS capability") +assert 'STARTTLS' in client.capabilities + +client.starttls() + +print("Logging in with CRAM-MD5") +client.login_cram_md5('dep8', 'test') + +print("Selecting INBOX") +client.select() + +print("Looking for the test message") +res, uids = client.search(None, 'HEADER', 'MESSAGE-ID', '""') + +assert res == 'OK' +assert len(uids[0]) > 0 + +uid = uids[0].split()[0] + +print("Fetching and verifying test message") +res, data = client.fetch(uid, '(RFC822)') + +assert res == 'OK' + +lines = data[0][1].splitlines() + +assert b'Subject: DEP-8 test' in lines + +print("Done") diff --git a/debian/tests/usage/sieve b/debian/tests/usage/sieve new file mode 100755 index 000000000..9ef68dd9e --- /dev/null +++ b/debian/tests/usage/sieve @@ -0,0 +1,8 @@ +#!/usr/bin/bash +# Validate managesieved authentication + +user=dep8 +pass=test + +sieve-connect --notlsverify --debug -s localhost \ + -p 4190 -u "$user" --list --passwordfd 3 3< <(echo "$pass")