From 36ff3552e7af73fa9485c3c12452a9daa198192c Mon Sep 17 00:00:00 2001 From: j4n Date: Tue, 21 Jul 2026 10:00:12 +0200 Subject: [PATCH 01/16] ci: emit a distro-distinct package version per stable target We build the same source for two stable releases (bookworm/Debian 12 and trixie/Debian 13), but both derived the same package version string from debian/changelog. Downstream that collision was worked around by separate upload directories (download.delta.chat/dovecot//) and a manual `_${distro}` suffix rename for the GitHub release. Fix it right: make each stable 'stamp' its own version. - debian/changelog: make the current/next entry (1:2.3.21+dfsg1-3+chatmail2) UNRELEASED with a neutral base version only. This lets dch amend the entry in place at build time. Previously, we used 'unstable', which was never correct for our builds and inherited from the original sid backport. - .github/workflows/pipeline.yml: after checkout, before the build, add a "finalize changelog" step that maps inputs.distro to a Debian-security-style suffix (bookworm -> +deb12u1, trixie -> +deb13u1) and runs `dch -v + --distribution --force-distribution` which rewrites the version in place. Explicity pull in 'devscripts' is added to the tools install. - .github/workflows/pipeline.yml: delete _distro suffix rename hack and derive base version for combined GitHub release; upload both source variants (changelog differs per-distro). --- .github/workflows/pipeline.yml | 27 ++++++++++++++++++++++++++- .github/workflows/release.yml | 34 ++++++++++++++++------------------ debian/changelog | 10 ++++++++++ 3 files changed, 52 insertions(+), 19 deletions(-) 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..9f8e3287b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +dovecot (1:2.3.21+dfsg1-3+chatmail2) UNRELEASED; urgency=medium + + * Backport Debian security fixes from bookworm-security (2.3.19.1 base, + deb12u1-u6) onto our 2.3.21 rebuild. + * 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 From 6a39cd592f0092f505b24f70c000b31592b61567 Mon Sep 17 00:00:00 2001 From: j4n Date: Tue, 21 Jul 2026 10:02:38 +0200 Subject: [PATCH 02/16] d/patches: CVE-2024-23184: fix message-address parsing DoS Backport from Debian bookworm-security (deb12u1). Bounded recursion / allocation in the RFC822 address and MIME header parsers (lib-mail/message-address, message-header-parser, lib-imap/imap-envelope, lib-storage index-search-mime) that a crafted message could drive into excessive CPU/memory use. Squashed upstream series, applies clean onto our 2.3.21 tree (byte-identical to upstream). --- debian/patches/CVE-2024-23184.patch | 975 ++++++++++++++++++++++++++++ debian/patches/series | 1 + 2 files changed, 976 insertions(+) create mode 100644 debian/patches/CVE-2024-23184.patch 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/series b/debian/patches/series index b32ab1d85..625075663 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -25,3 +25,4 @@ Cleanup-temporary-build-files.patch Silence-prototype-conflicts.patch remove-500ms-idle-debounce.patch Skip-flaky-cpu-limit-tests.patch +CVE-2024-23184.patch From b4e0b2130a7ce06d6a17d13d97383164783f0448 Mon Sep 17 00:00:00 2001 From: j4n Date: Tue, 21 Jul 2026 10:02:46 +0200 Subject: [PATCH 03/16] d/patches: CVE-2024-23185: fix message header parsing DoS Backport from Debian bookworm-security (deb12u1). Limits on header count and size in lib-mail/message-header-parser and message-parser to stop a crafted message with a huge number of headers from exhausting CPU/memory. Applies clean onto our 2.3.21 tree (byte-identical to upstream). --- debian/patches/CVE-2024-23185.patch | 491 ++++++++++++++++++++++++++++ debian/patches/series | 1 + 2 files changed, 492 insertions(+) create mode 100644 debian/patches/CVE-2024-23185.patch 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/series b/debian/patches/series index 625075663..244be4ad8 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -26,3 +26,4 @@ Silence-prototype-conflicts.patch remove-500ms-idle-debounce.patch Skip-flaky-cpu-limit-tests.patch CVE-2024-23184.patch +CVE-2024-23185.patch From 7689f614e0c9e09b963688c3d59ad7a0871b8509 Mon Sep 17 00:00:00 2001 From: j4n Date: Tue, 21 Jul 2026 10:03:47 +0200 Subject: [PATCH 04/16] d/patches: CVE-2025-59031: remove flawed fts decode2text.sh Backport from Debian bookworm-security (deb12u2). Upstream removed the example FTS attachment-decoder script src/plugins/fts/decode2text.sh: it shells out with insufficiently sanitized input and is not fit for production, upstream recommends writing your own or using Apache Tika. The patch (written against 2.3.21+dfsg1.orig, applies cleanly) drops the script and its Makefile.am install lines. Folded companion packaging edits so our build still works once the file is gone (both would otherwise reference a missing path): - debian/rules: drop the `rm .../usr/lib/dovecot/decode2text.sh` line in override_dh_auto_install (make install no longer ships it). - debian/dovecot-core.examples: deleted (its sole entry was the script). The script was only shipped as doc example, never under /usr/lib/dovecot, we mirror Debian's full removal. --- debian/dovecot-core.examples | 1 - debian/patches/CVE-2025-59031.patch | 133 ++++++++++++++++++++++++++++ debian/patches/series | 1 + debian/rules | 1 - 4 files changed, 134 insertions(+), 2 deletions(-) delete mode 100644 debian/dovecot-core.examples create mode 100644 debian/patches/CVE-2025-59031.patch 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-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/series b/debian/patches/series index 244be4ad8..b3ff8c249 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -27,3 +27,4 @@ remove-500ms-idle-debounce.patch Skip-flaky-cpu-limit-tests.patch CVE-2024-23184.patch CVE-2024-23185.patch +CVE-2025-59031.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. From da8054d96a76635c6d8bfb267b59d35ad654918b Mon Sep 17 00:00:00 2001 From: j4n Date: Tue, 21 Jul 2026 10:04:16 +0200 Subject: [PATCH 05/16] d/patches: CVE-2025-59032: fix managesieve-login crash Backport from Debian bookworm-security (deb12u2). A crash in pigeonhole managesieve-login when a command did not finish on the initial line (NULL-deref / assert reachable pre-auth). Applies cleanly onto 2.3.21. --- debian/patches/CVE-2025-59032.patch | 28 ++++++++++++++++++++++++++++ debian/patches/series | 1 + 2 files changed, 29 insertions(+) create mode 100644 debian/patches/CVE-2025-59032.patch 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/series b/debian/patches/series index b3ff8c249..f96a17f4b 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -28,3 +28,4 @@ Skip-flaky-cpu-limit-tests.patch CVE-2024-23184.patch CVE-2024-23185.patch CVE-2025-59031.patch +CVE-2025-59032.patch From 4f59896d95605a817e7a30055a0c4a820c5a6b80 Mon Sep 17 00:00:00 2001 From: j4n Date: Tue, 21 Jul 2026 10:04:35 +0200 Subject: [PATCH 06/16] d/patches: CVE-2026-0394: normalize passwd-file paths Backport from Debian bookworm-security (deb12u2, with the deb12u4 regression correction folded in). auth db-passwd-file could be tricked via unnormalized %-variable paths; -1 adds db_passwd_fix_path(), -2 normalizes the path with it (the -2 commit is Debian's passwd-file regression fix from u4, so copying the deb12u6 versions carries the corrected behavior). Both apply cleanly onto 2.3.21. --- debian/patches/CVE-2026-0394-1.patch | 66 ++++++++++++++++++++++++++++ debian/patches/CVE-2026-0394-2.patch | 59 +++++++++++++++++++++++++ debian/patches/series | 2 + 3 files changed, 127 insertions(+) create mode 100644 debian/patches/CVE-2026-0394-1.patch create mode 100644 debian/patches/CVE-2026-0394-2.patch 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/series b/debian/patches/series index f96a17f4b..c0793268f 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -29,3 +29,5 @@ 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 From fe5869873b32aa2154c6bb585029edd845fb4e13 Mon Sep 17 00:00:00 2001 From: j4n Date: Tue, 21 Jul 2026 10:05:43 +0200 Subject: [PATCH 07/16] d/patches: CVE-2026-27856: timing-safe doveadm HTTP credential check Backport from Debian bookworm-security (deb12u2). The doveadm HTTP/TCP client-connection compared the API key / credentials with a non-constant -time compare, leaking key material via a timing side channel; -1 switches to a timing-safe compare, -2 moves the temporary base64 value to the datastack, -3 fetches the API key from the correct source. Apply cleanly onto 2.3.21. --- debian/patches/CVE-2026-27856-1.patch | 51 +++++++++++++++++++++++++++ debian/patches/CVE-2026-27856-2.patch | 51 +++++++++++++++++++++++++++ debian/patches/CVE-2026-27856-3.patch | 23 ++++++++++++ debian/patches/series | 3 ++ 4 files changed, 128 insertions(+) create mode 100644 debian/patches/CVE-2026-27856-1.patch create mode 100644 debian/patches/CVE-2026-27856-2.patch create mode 100644 debian/patches/CVE-2026-27856-3.patch 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/series b/debian/patches/series index c0793268f..99700b1cb 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -31,3 +31,6 @@ 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 From 1516e57733dc1dd72ae64d2da79bb01c241c9042 Mon Sep 17 00:00:00 2001 From: j4n Date: Tue, 21 Jul 2026 10:05:52 +0200 Subject: [PATCH 08/16] d/patches: CVE-2026-27858: verify managesieve AUTHENTICATE initial response Backport from Debian bookworm-security (deb12u2). pigeonhole managesieve-login did not properly verify the AUTHENTICATE initial response, allowing a malformed pre-auth SASL initial response to be mishandled. Applies cleanly onto 2.3.21. --- debian/patches/CVE-2026-27858.patch | 28 ++++++++++++++++++++++++++++ debian/patches/series | 1 + 2 files changed, 29 insertions(+) create mode 100644 debian/patches/CVE-2026-27858.patch 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/series b/debian/patches/series index 99700b1cb..819c4bfa1 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -34,3 +34,4 @@ CVE-2026-0394-2.patch CVE-2026-27856-1.patch CVE-2026-27856-2.patch CVE-2026-27856-3.patch +CVE-2026-27858.patch From 6f86f32271448cda107c4ffe8747a9227b752b34 Mon Sep 17 00:00:00 2001 From: j4n Date: Tue, 21 Jul 2026 10:06:00 +0200 Subject: [PATCH 09/16] d/patches: CVE-2026-27859: limit RFC2231 parameters Backport from Debian bookworm-security (deb12u2). lib-mail rfc2231-parser did not bound the number of RFC2231 continuation parameters, letting a crafted MIME header drive excessive work (DoS). Adds a parameter-count limit. Applies cleanly onto 2.3.21. --- debian/patches/CVE-2026-27859.patch | 103 ++++++++++++++++++++++++++++ debian/patches/series | 1 + 2 files changed, 104 insertions(+) create mode 100644 debian/patches/CVE-2026-27859.patch 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/series b/debian/patches/series index 819c4bfa1..f4f4c9b9c 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -35,3 +35,4 @@ CVE-2026-27856-1.patch CVE-2026-27856-2.patch CVE-2026-27856-3.patch CVE-2026-27858.patch +CVE-2026-27859.patch From 57733fcea6b059db4cd0cf28c2b5ab5e66c364a4 Mon Sep 17 00:00:00 2001 From: j4n Date: Tue, 21 Jul 2026 10:06:17 +0200 Subject: [PATCH 10/16] d/patches: CVE-2026-40016: enforce sieve CPU limit in :contains/:matches Backport from Debian bookworm-security (deb12u6). pigeonhole lib-sieve did not enforce the CPU time limit inside the :contains and :matches match types, so a crafted Sieve script / message could burn unbounded CPU (DoS). Applies cleanly onto 2.3.21. --- debian/patches/CVE-2026-40016.patch | 255 ++++++++++++++++++++++++++++ debian/patches/series | 1 + 2 files changed, 256 insertions(+) create mode 100644 debian/patches/CVE-2026-40016.patch 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/series b/debian/patches/series index f4f4c9b9c..44be49df8 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -36,3 +36,4 @@ CVE-2026-27856-2.patch CVE-2026-27856-3.patch CVE-2026-27858.patch CVE-2026-27859.patch +CVE-2026-40016.patch From 7936847136e8685c4c49f612c6bdc05d16506e39 Mon Sep 17 00:00:00 2001 From: j4n Date: Tue, 21 Jul 2026 10:06:26 +0200 Subject: [PATCH 11/16] d/patches: CVE-2026-40020: reject invalid ACL identifiers Backport from Debian bookworm-security (deb12u6). imap-acl / acl plugins did not validate ACL identifiers: -1 adds acl_id_is_valid(), -2 makes imap-acl fail on an invalid identifier, -3 asserts before writing an invalid identifier to the vfile backend (prevents ACL corruption / mis-scoped rights). Apply cleanly onto 2.3.21. --- debian/patches/CVE-2026-40020-1.patch | 75 +++++++++++++++++++++++++++ debian/patches/CVE-2026-40020-2.patch | 28 ++++++++++ debian/patches/CVE-2026-40020-3.patch | 23 ++++++++ debian/patches/series | 3 ++ 4 files changed, 129 insertions(+) create mode 100644 debian/patches/CVE-2026-40020-1.patch create mode 100644 debian/patches/CVE-2026-40020-2.patch create mode 100644 debian/patches/CVE-2026-40020-3.patch 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/series b/debian/patches/series index 44be49df8..9ec8d9e0a 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -37,3 +37,6 @@ 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 From a8052e20e5c12ced62caab61500d9273f5b63091 Mon Sep 17 00:00:00 2001 From: j4n Date: Tue, 21 Jul 2026 10:28:01 +0200 Subject: [PATCH 12/16] d/patches: CVE-2026-27855: fix auth-cache key + passdb event lifecycle Backport Debian's four-part fix for CVE-2026-27855 from upstream/stable/bookworm-security (deb12u2 base, deb12u5-corrected, copied from the deb12u6 tip). Applies to our 2.3.21 tree with line offsets only. The security-relevant defect (patch -1) is that auth_cache_remove() expanded the passdb cache key from the raw, untranslated request->fields.user instead of the post-userdb-translation request->fields.translated_username. Every other cache-key expansion in auth-cache.c (lookup, insert) already uses translated_username, so a credentials update could compute a key that does not match the entry that lookups use. The stale/mismatched entry is left behind, letting a subsequent auth reuse cached data keyed under the wrong (pre-translation) identity. Aligning the removal path with the lookup/insert paths closes that inconsistency. The remaining three parts are the supporting refactor and hardening Debian shipped in the same CVE set: -2 extracts auth_request_passdb_event_(begin|end) out of auth_request_passdb_lookup_(begin|end) so the passdb event/settings lifecycle can be driven around any passdb call, not just lookups (src/auth/auth-request.c, .h). -3 wraps the credentials-update path with that begin/end pair, in both auth_request_set_credentials() and the auth worker's setcred handler, so update_query runs with the correct per-passdb event + settings context (src/auth/auth-request.c, src/auth/auth-worker-client.c). Note Debian's diffstat names auth-worker-server.c but the hunk targets auth-worker-client.c; both files exist in 2.3.21 and the hunk lands on the correct one. -4 rejects an empty update_query with an explicit error instead of issuing a malformed SQL credentials update (src/auth/passdb-sql.c). --- debian/patches/CVE-2026-27855-1.patch | 23 +++++++ debian/patches/CVE-2026-27855-2.patch | 91 +++++++++++++++++++++++++++ debian/patches/CVE-2026-27855-3.patch | 56 +++++++++++++++++ debian/patches/CVE-2026-27855-4.patch | 27 ++++++++ debian/patches/series | 4 ++ 5 files changed, 201 insertions(+) create mode 100644 debian/patches/CVE-2026-27855-1.patch create mode 100644 debian/patches/CVE-2026-27855-2.patch create mode 100644 debian/patches/CVE-2026-27855-3.patch create mode 100644 debian/patches/CVE-2026-27855-4.patch 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/series b/debian/patches/series index 9ec8d9e0a..453cb059f 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -40,3 +40,7 @@ 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 From b0b83a6d07213c9d177559e4abe5f50896a45e15 Mon Sep 17 00:00:00 2001 From: j4n Date: Tue, 21 Jul 2026 10:28:54 +0200 Subject: [PATCH 13/16] d/patches: CVE-2026-33603: reject non-base64 in SASL continuation Backport Debian's CVE-2026-33603 fix (deb12u6) from bookworm-security. Applies to our 2.3.21 tree with a line offset only. Defect: client_auth_respond() forwarded the client's SASL continuation response verbatim to auth_client_request_continue(), which hands it to the auth server over the tab-delimited auth protocol. The response is attacker-controlled and arrives pre-authentication. A response containing a TAB (or other non-base64 byte) is not valid SASL base64 but was passed through unchecked, letting the client inject extra fields into the auth-protocol request line. The fix validates every byte against base64_is_valid_char() (plus '=') before forwarding and fails the auth attempt on the first invalid character, so only well-formed base64 ever reaches the auth server. --- debian/patches/CVE-2026-33603.patch | 36 +++++++++++++++++++++++++++++ debian/patches/series | 1 + 2 files changed, 37 insertions(+) create mode 100644 debian/patches/CVE-2026-33603.patch 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/series b/debian/patches/series index 453cb059f..091cc5d9a 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -44,3 +44,4 @@ CVE-2026-27855-1.patch CVE-2026-27855-2.patch CVE-2026-27855-3.patch CVE-2026-27855-4.patch +CVE-2026-33603.patch From c8bf26095ea282084939f172d292901ffe6f95de Mon Sep 17 00:00:00 2001 From: j4n Date: Tue, 21 Jul 2026 10:31:43 +0200 Subject: [PATCH 14/16] d/patches: CVE-2026-27857: bound IMAP parser open-list nesting Backport Debian's five-part CVE-2026-27857 fix (deb12u2 base, deb12u5-corrected, copied from the deb12u6 tip) from bookworm-security. Applies to our 2.3.21 tree with no fuzz and no rejects; our imap_parser_create() call-site set is identical to Debian's. Defect: the pre-authentication IMAP parser imposed no limit on the depth of parenthesised lists. Each '(' opens a new list arg allocated from the parser pool, so a client could send a command with a huge run of '(' and drive the imap-login process's memory usage up without ever completing a line, a pre-auth memory-exhaustion DoS. The fix threads an optional parameters struct into the parser and lets a caller cap the open-list count: -1 adjusts the pigeonhole imap-filter-sieve call site for the new imap_parser_create() arity (pass NULL) -2 adds the trailing `const struct imap_parser_params *params` argument to imap_parser_create() and passes NULL at every existing call site. params is unused in this step. -3 gives imap_parser_params a list_count_limit field and enforces it in the parser: exceeding the limit fails the line with an error, adds a unit test. -4 sets list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT (1) for the two imap-login parsers and the pre-login ID-command parser, since pre-login only ever uses a list in the ID command. -5 makes the on-stack params const (u5 correction). The list-limit test relies on test_assert_strcmp_idx / test_assert_cmp (src/lib-test/test-common.h) and istream-chain (src/lib/istream-chain.h), both present in 2.3.21, so it builds. Note CVE-2026-42006 (next commit) corrects a counting bug in the -3 enforcement. --- debian/patches/CVE-2026-27857-1.patch | 24 +++ debian/patches/CVE-2026-27857-2.patch | 279 ++++++++++++++++++++++++++ debian/patches/CVE-2026-27857-3.patch | 160 +++++++++++++++ debian/patches/CVE-2026-27857-4.patch | 84 ++++++++ debian/patches/CVE-2026-27857-5.patch | 59 ++++++ debian/patches/series | 5 + 6 files changed, 611 insertions(+) create mode 100644 debian/patches/CVE-2026-27857-1.patch create mode 100644 debian/patches/CVE-2026-27857-2.patch create mode 100644 debian/patches/CVE-2026-27857-3.patch create mode 100644 debian/patches/CVE-2026-27857-4.patch create mode 100644 debian/patches/CVE-2026-27857-5.patch 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/series b/debian/patches/series index 091cc5d9a..d6d63757a 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -45,3 +45,8 @@ 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 From 5e0db677b1ebde02fe28fe390f8f5c14f406d637 Mon Sep 17 00:00:00 2001 From: j4n Date: Tue, 21 Jul 2026 10:32:05 +0200 Subject: [PATCH 15/16] d/patches: CVE-2026-42006: fix list_count_limit to bound '(' not ')' Backport Debian's CVE-2026-42006 fix (deb12u6) from bookworm-security. Applies to our 2.3.21 tree with no fuzz and no rejects. Must layer on top of CVE-2026-27857 (previous commit), which introduced the code this corrects. Defect: the nesting limit added in CVE-2026-27857-3 was placed in imap_parser_close_list(), i.e. it counted and capped the ')' that close lists rather than the '(' that open (and allocate) them. Because the allocation happens on open, an attacker could still send an unbounded run of '(' without any ')' and exhaust imap-login memory pre-auth; the original limit never engaged on the byte that actually grows the pool. The fix moves the counter+limit check into imap_parser_open_list(), makes that function return bool, and fails the parse at the offending '(' from imap_parser_read_arg(). The unit test is extended to assert the exact error string across the missing-')', unexpected-')', too-many-'(' and pending-literal cases. --- debian/patches/CVE-2026-42006.patch | 101 ++++++++++++++++++++++++++++ debian/patches/series | 1 + 2 files changed, 102 insertions(+) create mode 100644 debian/patches/CVE-2026-42006.patch 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 d6d63757a..96704a8e1 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -50,3 +50,4 @@ CVE-2026-27857-2.patch CVE-2026-27857-3.patch CVE-2026-27857-4.patch CVE-2026-27857-5.patch +CVE-2026-42006.patch From 333a985427805fd8993d5c08f27ec1278a5b49ac Mon Sep 17 00:00:00 2001 From: j4n Date: Tue, 21 Jul 2026 10:34:24 +0200 Subject: [PATCH 16/16] d/tests: converge regression tests with bookworm-security Bring debian/tests up to the version Debian ships alongside the security fixes. Our tests were an older fork of Debian's own (the run-parts debian/tests/usage layout came from Debian via merge 7c68cb026), so this is convergence, not divergence, and it lands the regression coverage the CVE work added. Changes: * debian/changelog: add lists of patches * debian/tests/control: now byte-identical to bookworm-security. The usage (run-parts) stanza gains dovecot-managesieved and sieve-connect in Depends (both satisfiable: we build dovecot-managesieved per debian/control, and sieve-connect is in bookworm and trixie main). A new debian_bug_1134464 stanza is added (Depends: dovecot-imapd; Restrictions: needs-root, breaks-testbed, allow-stderr). * debian/tests/usage/00_setup: enable cram-md5 alongside plain in auth_mechanisms. The static passdb already stores the password as plaintext (password=test), which CRAM-MD5 can use, so the new imap-cram-md5 test authenticates without further setup. * debian/tests/usage/imap-cram-md5 (new): STARTTLS + CRAM-MD5 login, then fetch and verify the seeded DEP-8 message. * debian/tests/usage/sieve (new): authenticate to managesieved on 4190 via sieve-connect and LIST scripts. * debian/tests/debian_bug_1134464 (new): self-contained passwd-file + LDA delivery test that guards the CVE-2026-0394 passwd-file path regression (Debian bug 1134464). Writes its own local.conf, so it does not depend on the usage/ setup. --- debian/changelog | 45 ++++++++++++++++++++++++-- debian/tests/control | 6 +++- debian/tests/debian_bug_1134464 | 54 ++++++++++++++++++++++++++++++++ debian/tests/usage/00_setup | 2 +- debian/tests/usage/imap-cram-md5 | 38 ++++++++++++++++++++++ debian/tests/usage/sieve | 8 +++++ 6 files changed, 149 insertions(+), 4 deletions(-) create mode 100755 debian/tests/debian_bug_1134464 create mode 100755 debian/tests/usage/imap-cram-md5 create mode 100755 debian/tests/usage/sieve diff --git a/debian/changelog b/debian/changelog index 9f8e3287b..861260bfc 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,7 +1,48 @@ dovecot (1:2.3.21+dfsg1-3+chatmail2) UNRELEASED; urgency=medium - * Backport Debian security fixes from bookworm-security (2.3.19.1 base, - deb12u1-u6) onto our 2.3.21 rebuild. + * 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. 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")