Version in base suite: 4.96-15+deb12u7 Base version: exim4_4.96-15+deb12u7 Target version: exim4_4.96-15+deb12u8 Base file: /srv/ftp-master.debian.org/ftp/pool/main/e/exim4/exim4_4.96-15+deb12u7.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/e/exim4/exim4_4.96-15+deb12u8.dsc changelog | 27 patches/82_01-GnuTLS-fix-hostname-verify-of-server-cert-for-empty-.patch | 84 ++ patches/82_02-Support-musl-libc-dn_expand-oddity.patch | 75 + patches/82_03-when-dewrap-only-skip-if-associated-char.patch | 58 + patches/82_04-Expansions-harden-for-malformed-UTF-8.patch | 59 + patches/82_05-Fix-SPA-authenticator.-Bug-3106.patch | 413 ++++++++++ patches/82_06-SPA-authenticator-harden-buffer-usage.patch | 258 ++++++ patches/series | 6 8 files changed, 980 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpkmqz5tmm/exim4_4.96-15+deb12u7.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpkmqz5tmm/exim4_4.96-15+deb12u8.dsc: no acceptable signature found diff -Nru exim4-4.96/debian/changelog exim4-4.96/debian/changelog --- exim4-4.96/debian/changelog 2025-03-22 10:25:14.000000000 +0000 +++ exim4-4.96/debian/changelog 2026-05-02 09:33:47.000000000 +0000 @@ -1,3 +1,30 @@ +exim4 (4.96-15+deb12u8) bookworm; urgency=medium + + * Fix GnuTLS hostname verify of a server certificate with a zero-length + Subject. Patch from upstream GIT master (Closes: #1134984) + * Pull CVE-fixes from 4.99.2 + +CVE-2026-40684 Possible crash with malicious DNS data when using musl + libc On systems using musl libc (not glibc) due to an oddity in octal + printing it is possible to crash the connection instance when malformed + DNS data is present in PTR records. + +CVE-2026-40685 Possible OOB read/write on corrupt JSON in header + configurations using json operators on invalid externally-provided input + could trigger heap corruption. + +CVE-2026-40686 Possible OOB read with large UTF8 trailing characters + configurations using utf8 operators on malformed utf8 in headers could + trigger OOB reads and might trigger some data leak if error messages are + required for subsequent emails in the current connection and similar + malformed headers are present. + +CVE-2026-40687 Possible OOB read/write with SPA authenticator in + configurations using the SPA authentication driver to a + hostile/compromised external SPA/NTLM connection it is possible to + trigger an OOB read/write and crash the connection instance or possibly + leak heap data to the instance. + +As a pre-dependeny to the patchset also add the fix for upstream Bug + 3106 from 4.99. + + -- Andreas Metzler Sat, 02 May 2026 11:33:47 +0200 + exim4 (4.96-15+deb12u7) bookworm-security; urgency=high * Fix use-after-free (requiring local command-line access) notified by diff -Nru exim4-4.96/debian/patches/82_01-GnuTLS-fix-hostname-verify-of-server-cert-for-empty-.patch exim4-4.96/debian/patches/82_01-GnuTLS-fix-hostname-verify-of-server-cert-for-empty-.patch --- exim4-4.96/debian/patches/82_01-GnuTLS-fix-hostname-verify-of-server-cert-for-empty-.patch 1970-01-01 00:00:00.000000000 +0000 +++ exim4-4.96/debian/patches/82_01-GnuTLS-fix-hostname-verify-of-server-cert-for-empty-.patch 2026-05-02 09:33:28.000000000 +0000 @@ -0,0 +1,84 @@ +From 371e5210218746e876fd71c888fdb666c85ceb56 Mon Sep 17 00:00:00 2001 +From: Jeremy Harris +Date: Sun, 19 Apr 2026 15:14:14 +0100 +Subject: [PATCH] GnuTLS: fix hostname verify of server cert for empty Subject. + Bug 3215 + +--- + doc/ChangeLog | 6 ++++++ + src/tls-gnu.c | 27 +++++++++++++++++---------- + 2 files changed, 23 insertions(+), 10 deletions(-) + +--- a/doc/ChangeLog ++++ b/doc/ChangeLog +@@ -110,10 +110,16 @@ JH/06 Bug 3054: Fix dnsdb lookup for a T + JH/s1 Refuse to accept a line "dot, LF" as end-of-DATA unless operating in + LF-only mode (as detected from the first header line). Previously we did + accept that in (normal) CRLF mode; this has been raised as a possible + attack scenario (under the name "smtp smuggling", CVE-2023-51766). + ++JH/33 Bug 3215: Fix GnuTLS hostname verify of a server certificate with a ++ zero-length Subject. These are now being handed out by LetsEncrypt; note ++ that this means they carry no DN (as well as no SN, that having decreed ++ deprecated in favour of SANs). The $tls_*peerdn variables relating to ++ these certificates will be empty strings. ++ + + Exim version 4.96 + ----------------- + + JH/01 Move the wait-for-next-tick (needed for unique message IDs) from +--- a/src/tls-gnu.c ++++ b/src/tls-gnu.c +@@ -2234,11 +2234,10 @@ gnutls_protocol_t protocol; + gnutls_cipher_algorithm_t cipher; + gnutls_kx_algorithm_t kx; + gnutls_mac_algorithm_t mac; + gnutls_certificate_type_t ct; + gnutls_x509_crt_t crt; +-uschar * dn_buf; + size_t sz; + + if (state->have_set_peerdn) + return OK; + state->have_set_peerdn = TRUE; +@@ -2356,22 +2355,30 @@ if ((ct = gnutls_certificate_type_get(se + rc = import_cert(&cert_list[0], &crt); + exim_gnutls_peer_err(US"cert 0"); + + state->tlsp->peercert = state->peercert = crt; + ++state->peerdn = US""; + sz = 0; +-rc = gnutls_x509_crt_get_dn(crt, NULL, &sz); +-if (rc != GNUTLS_E_SHORT_MEMORY_BUFFER) ++if (!(rc = gnutls_x509_crt_get_dn(crt, NULL, &sz))) ++ { DEBUG(D_tls) debug_printf_indent("TLS: zero-length DN\n"); } ++else if (rc == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) ++ { DEBUG(D_tls) debug_printf_indent("TLS: no DN\n"); } ++else + { +- exim_gnutls_peer_err(US"getting size for cert DN failed"); +- return FAIL; /* should not happen */ +- } +-dn_buf = store_get_perm(sz, GET_TAINTED); +-rc = gnutls_x509_crt_get_dn(crt, CS dn_buf, &sz); +-exim_gnutls_peer_err(US"failed to extract certificate DN [gnutls_x509_crt_get_dn(cert 0)]"); ++ uschar * dn_buf; ++ if (rc != GNUTLS_E_SHORT_MEMORY_BUFFER) ++ { ++ exim_gnutls_peer_err(US"getting size for cert DN failed"); ++ return FAIL; /* should not happen */ ++ } ++ dn_buf = store_get_perm(sz, GET_TAINTED); ++ rc = gnutls_x509_crt_get_dn(crt, CS dn_buf, &sz); ++ exim_gnutls_peer_err(US"failed to extract certificate DN [gnutls_x509_crt_get_dn(cert 0)]"); + +-state->peerdn = dn_buf; ++ state->peerdn = dn_buf; ++ } + + return OK; + #undef exim_gnutls_peer_err + } + diff -Nru exim4-4.96/debian/patches/82_02-Support-musl-libc-dn_expand-oddity.patch exim4-4.96/debian/patches/82_02-Support-musl-libc-dn_expand-oddity.patch --- exim4-4.96/debian/patches/82_02-Support-musl-libc-dn_expand-oddity.patch 1970-01-01 00:00:00.000000000 +0000 +++ exim4-4.96/debian/patches/82_02-Support-musl-libc-dn_expand-oddity.patch 2026-05-02 09:33:28.000000000 +0000 @@ -0,0 +1,75 @@ +From 628bbaca7672748d941a12e7cd5f0122a4e18c81 Mon Sep 17 00:00:00 2001 +From: Jeremy Harris +Date: Tue, 28 Apr 2026 14:47:32 +0100 +Subject: [PATCH 1/4] Support musl libc dn_expand oddity + +CVE-2026-40684 +--- + doc/ChangeLog | 16 ++++++++++++++++ + .../CVE2026-40684.assessment | 12 ++++++++++++ + src/string.c | 12 ++++++------ + 3 files changed, 34 insertions(+), 6 deletions(-) + create mode 100644 doc/doc-txt/exim-security-2026-04.1/CVE2026-40684.assessment + +--- a/doc/ChangeLog ++++ b/doc/ChangeLog +@@ -116,10 +116,14 @@ JH/33 Bug 3215: Fix GnuTLS hostname veri + zero-length Subject. These are now being handed out by LetsEncrypt; note + that this means they carry no DN (as well as no SN, that having decreed + deprecated in favour of SANs). The $tls_*peerdn variables relating to + these certificates will be empty strings. + ++JH/34 CVE-2026-40684: A crafted DNS record could cause a crash of the Exim ++ process acessing it, when operating with musl libc. This could be the ++ daemon. An Exim using Gnu libc is not affeected. ++ + + Exim version 4.96 + ----------------- + + JH/01 Move the wait-for-next-tick (needed for unique message IDs) from +--- /dev/null ++++ b/doc/doc-txt/exim-security-2026-04.1/CVE2026-40684.assessment +@@ -0,0 +1,12 @@ ++CVE2026-40684 ++ ++Vulnerability conditions ++------------------------ ++ ++- Exim build/run using musl libc (not gnulibc) ++- Deamon running, accepting connections ++ ++Impact ++------ ++ ++Remote-triggered crash, via crafted PTR record +--- a/src/string.c ++++ b/src/string.c +@@ -579,21 +579,21 @@ string_copy_dnsdomain(uschar * s) + { + uschar * yield; + uschar * ss = yield = store_get(Ustrlen(s) + 1, GET_TAINTED); /* always treat as tainted */ + + while (*s) +- { + if (*s != '\\') + *ss++ = *s++; +- else if (isdigit(s[1])) +- { +- *ss++ = (s[1] - '0')*100 + (s[2] - '0')*10 + s[3] - '0'; +- s += 4; ++ else if (isdigit(*++s)) /* Apparently, musl libc dn_expand seen doing \DD */ ++ { /* and \D also. We can only hope not when a real digit follows. */ ++ uschar c = *s++ - '0'; ++ if (isdigit(*s)) c = c * 10 + *s++ - '0'; ++ if (isdigit(*s)) c = c * 10 + *s++ - '0'; ++ *ss++ = c; + } + else if (*++s) + *ss++ = *s++; +- } + + *ss = 0; + return yield; + } + diff -Nru exim4-4.96/debian/patches/82_03-when-dewrap-only-skip-if-associated-char.patch exim4-4.96/debian/patches/82_03-when-dewrap-only-skip-if-associated-char.patch --- exim4-4.96/debian/patches/82_03-when-dewrap-only-skip-if-associated-char.patch 1970-01-01 00:00:00.000000000 +0000 +++ exim4-4.96/debian/patches/82_03-when-dewrap-only-skip-if-associated-char.patch 2026-05-02 09:33:28.000000000 +0000 @@ -0,0 +1,58 @@ +From 9fdc057e71b87c87a0d3d2288b2810a0efaaba57 Mon Sep 17 00:00:00 2001 +From: Bernard Quatermass +Date: Mon, 23 Mar 2026 16:43:51 +0000 +Subject: [PATCH 2/4] when dewrap, only skip \ if associated char + +CVE2026-40685 +--- + doc/ChangeLog | 5 ++++- + .../exim-security-2026-04.1/CVE2026-40685.assessment | 11 +++++++++++ + src/expand.c | 2 +- + 3 files changed, 16 insertions(+), 2 deletions(-) + create mode 100644 doc/doc-txt/exim-security-2026-04.1/CVE2026-40685.assessment + +--- a/doc/ChangeLog ++++ b/doc/ChangeLog +@@ -120,10 +120,13 @@ JH/33 Bug 3215: Fix GnuTLS hostname veri + + JH/34 CVE-2026-40684: A crafted DNS record could cause a crash of the Exim + process acessing it, when operating with musl libc. This could be the + daemon. An Exim using Gnu libc is not affeected. + ++BQ/02 CVE-2026-40685: JSON string expansions could, when fed crafted source ++ strings, corrupt the heap. ++ + + Exim version 4.96 + ----------------- + + JH/01 Move the wait-for-next-tick (needed for unique message IDs) from +--- /dev/null ++++ b/doc/doc-txt/exim-security-2026-04.1/CVE2026-40685.assessment +@@ -0,0 +1,11 @@ ++CVE2026-40685 ++ ++Vulnerability conditions ++------------------------ ++ ++- Config uses json operators on externally-provided input ++ ++Impact ++------ ++ ++- Remote-triggered heap corruption +--- a/src/expand.c ++++ b/src/expand.c +@@ -2278,11 +2278,11 @@ if (Uskip_whitespace(&p) == *wrap) + { + s = ++p; + wrap++; + while (*p) + { +- if (*p == '\\') p++; ++ if (*p == '\\' && *(p+1)) p++; + else if (!quotesmode && *p == wrap[-1]) depth++; + else if (*p == *wrap) + if (depth == 0) + { + *p = '\0'; diff -Nru exim4-4.96/debian/patches/82_04-Expansions-harden-for-malformed-UTF-8.patch exim4-4.96/debian/patches/82_04-Expansions-harden-for-malformed-UTF-8.patch --- exim4-4.96/debian/patches/82_04-Expansions-harden-for-malformed-UTF-8.patch 1970-01-01 00:00:00.000000000 +0000 +++ exim4-4.96/debian/patches/82_04-Expansions-harden-for-malformed-UTF-8.patch 2026-05-02 09:33:28.000000000 +0000 @@ -0,0 +1,59 @@ +From f2570bde16fb4d4a1242ff363a4c4eecf6372efc Mon Sep 17 00:00:00 2001 +From: Jeremy Harris +Date: Mon, 23 Mar 2026 15:10:28 +0000 +Subject: [PATCH 3/4] Expansions: harden for malformed UTF-8 + +CVE2026-40686 +--- + doc/ChangeLog | 4 ++++ + .../exim-security-2026-04.1/CVE2026-40686.assessment | 11 +++++++++++ + src/expand.c | 2 +- + 3 files changed, 16 insertions(+), 1 deletion(-) + create mode 100644 doc/doc-txt/exim-security-2026-04.1/CVE2026-40686.assessment + +--- a/doc/ChangeLog ++++ b/doc/ChangeLog +@@ -123,10 +123,14 @@ JH/34 CVE-2026-40684: A crafted DNS reco + daemon. An Exim using Gnu libc is not affeected. + + BQ/02 CVE-2026-40685: JSON string expansions could, when fed crafted source + strings, corrupt the heap. + ++JH/35 CVE-2026-40686: The ${from_utf8:} expansion operator, fed malformed input, ++ could read into the heap. If the result was used for an SMTP rejection ++ message, data exfiltration would be possible. ++ + + Exim version 4.96 + ----------------- + + JH/01 Move the wait-for-next-tick (needed for unique message IDs) from +--- /dev/null ++++ b/doc/doc-txt/exim-security-2026-04.1/CVE2026-40686.assessment +@@ -0,0 +1,11 @@ ++CVE2026-40686 ++ ++Vulnerability conditions ++------------------------ ++ ++- Config using UTF-8 operations on externally-provided input ++ ++Impact ++------ ++ ++- Heap data exfiltration +--- a/src/expand.c ++++ b/src/expand.c +@@ -884,11 +884,11 @@ static int utf8_table2[] = { 0xff, 0x1f, + if ((c & 0xc0) == 0xc0) \ + { \ + int a = utf8_table1[c & 0x3f]; /* Number of additional bytes */ \ + int s = 6*a; \ + c = (c & utf8_table2[a]) << s; \ +- while (a-- > 0) \ ++ while (a-- > 0 && *ptr) \ + { \ + s -= 6; \ + c |= (*ptr++ & 0x3f) << s; \ + } \ + } diff -Nru exim4-4.96/debian/patches/82_05-Fix-SPA-authenticator.-Bug-3106.patch exim4-4.96/debian/patches/82_05-Fix-SPA-authenticator.-Bug-3106.patch --- exim4-4.96/debian/patches/82_05-Fix-SPA-authenticator.-Bug-3106.patch 1970-01-01 00:00:00.000000000 +0000 +++ exim4-4.96/debian/patches/82_05-Fix-SPA-authenticator.-Bug-3106.patch 2026-05-02 09:33:28.000000000 +0000 @@ -0,0 +1,413 @@ +From a731c6050a1510734776851aaff5ad2f32fa3ae5 Mon Sep 17 00:00:00 2001 +From: Jeremy Harris +Date: Mon, 5 Aug 2024 12:51:12 +0100 +Subject: [PATCH] Fix SPA authenticator. Bug 3106 + +--- + doc/ChangeLog | 6 ++ + src/auths/auth-spa.c | 205 ++++++++++++++++----------------------- + src/auths/auth-spa.h | 17 ++-- + src/auths/spa.c | 2 +- + 4 files changed, 99 insertions(+), 131 deletions(-) + +--- a/doc/ChangeLog ++++ b/doc/ChangeLog +@@ -127,10 +127,16 @@ BQ/02 CVE-2026-40685: JSON string expans + + JH/35 CVE-2026-40686: The ${from_utf8:} expansion operator, fed malformed input, + could read into the heap. If the result was used for an SMTP rejection + message, data exfiltration would be possible. + ++JH/07 Bug 3106: Fix coding in SPA authenticator. A macro argument was not ++ properly parenthesized, resulting in a logic error. While the simple ++ fix was provided by Andrew Aitchison, the over-large code block resulting ++ from this macro made me want to replace it with a real function so more ++ extensive rework becamse needed. ++ + + Exim version 4.96 + ----------------- + + JH/01 Move the wait-for-next-tick (needed for unique message IDs) from +--- a/src/auths/auth-spa.c ++++ b/src/auths/auth-spa.c +@@ -1200,59 +1200,71 @@ A = B = C = D = 0; + char versionString[] = "libntlm version 0.21"; + + /* Utility routines that handle NTLM auth structures. */ + + /* The [IS]VAL macros are to take care of byte order for non-Intel +- * Machines -- I think this file is OK, but it hasn't been tested. +- * The other files (the ones stolen from Samba) should be OK. +- */ +- +- +-/* I am not crazy about these macros -- they seem to have gotten +- * a bit complex. A new scheme for handling string/buffer fields +- * in the structures probably needs to be designed +- */ +- +-#define spa_bytes_add(ptr, header, buf, count) \ +-{ \ +-if ( buf && (count) != 0 /* we hate -Wint-in-bool-contex */ \ +- && ptr->bufIndex + count < sizeof(ptr->buffer) \ +- ) \ +- { \ +- SSVAL(&ptr->header.len,0,count); \ +- SSVAL(&ptr->header.maxlen,0,count); \ +- SIVAL(&ptr->header.offset,0,((ptr->buffer - ((uint8x*)ptr)) + ptr->bufIndex)); \ +- memcpy(ptr->buffer+ptr->bufIndex, buf, count); \ +- ptr->bufIndex += count; \ +- } \ +-else \ +- { \ +- ptr->header.len = \ +- ptr->header.maxlen = 0; \ +- SIVAL(&ptr->header.offset,0,((ptr->buffer - ((uint8x*)ptr)) + ptr->bufIndex)); \ +- } \ +-} +- +-#define spa_string_add(ptr, header, string) \ +-{ \ +-uschar * p = string; \ +-int len = 0; \ +-if (p) len = Ustrlen(p); \ +-spa_bytes_add(ptr, header, p, len); \ +-} +- +-#define spa_unicode_add_string(ptr, header, string) \ +-{ \ +-uschar * p = string; \ +-uschar * b = NULL; \ +-int len = 0; \ +-if (p) \ +- { \ +- len = Ustrlen(p); \ +- b = US strToUnicode(CS p); \ +- } \ +-spa_bytes_add(ptr, header, b, len*2); \ ++Machines -- I think this file is OK, but it hasn't been tested. ++The other files (the ones stolen from Samba) should be OK. */ ++ ++ ++/* Append a string to the buffer and point the header struct at that. */ ++ ++static void ++spa_bytes_add(SPAbuf * buffer, size_t off, SPAStrHeader * header, ++ const uschar * src, int count) ++{ ++off += buffer->bufIndex; ++if ( src && count != 0 /* we hate -Wint-in-bool-contex */ ++ && buffer->bufIndex + count < sizeof(buffer->buffer) ++ ) ++ { ++ SSVAL(&header->len, 0, count); ++ SSVAL(&header->maxlen, 0, count); ++ SIVAL(&header->offset, 0, off); ++ memcpy(buffer->buffer + buffer->bufIndex, src, count); ++ buffer->bufIndex += count; ++ } ++else ++ { ++ header->len = header->maxlen = 0; ++ SIVAL(&header->offset, 0, off); ++ } ++} ++ ++static void ++spa_string_add(SPAbuf * buffer, size_t off, SPAStrHeader * header, ++ const uschar * string) ++{ ++int len = string ? Ustrlen(string) : 0; ++spa_bytes_add(buffer, off, header, string, len); ++} ++ ++static uschar * ++strToUnicode(const uschar * p) ++{ ++static uschar buf[1024]; ++size_t l = Ustrlen(p); ++ ++assert (l * 2 < sizeof buf); ++ ++for (int i = 0; l--; ) { buf[i++] = *p++; buf[i++] = 0; } ++return buf; ++} ++ ++static void ++spa_unicode_add_string(SPAbuf * buffer, size_t off, SPAStrHeader * header, ++ const uschar * string) ++{ ++const uschar * p = string; ++uschar * b = NULL; ++int len = 0; ++if (p) ++ { ++ len = Ustrlen(p); ++ b = US strToUnicode(p); ++ } ++spa_bytes_add(buffer, off, header, b, len*2); + } + + + #ifdef notdef + +@@ -1290,28 +1302,10 @@ for (i = 0; i < len; ++i) + buf[i] = '\0'; + return buf; + } + + static uschar * +-strToUnicode (char *p) +-{ +-static uschar buf[1024]; +-size_t l = strlen (p); +-int i = 0; +- +-assert (l * 2 < sizeof buf); +- +-while (l--) +- { +- buf[i++] = *p++; +- buf[i++] = 0; +- } +- +-return buf; +-} +- +-static uschar * + toString (char *p, size_t len) + { + static uschar buf[1024]; + + assert (len + 1 < sizeof buf); +@@ -1401,16 +1395,18 @@ if (p) + if (!domain) + domain = p + 1; + *p = '\0'; + } + +-request->bufIndex = 0; ++request->buf.bufIndex = 0; + memcpy (request->ident, "NTLMSSP\0\0\0", 8); + SIVAL (&request->msgType, 0, 1); + SIVAL (&request->flags, 0, 0x0000b207); /* have to figure out what these mean */ +-spa_string_add (request, user, u); +-spa_string_add (request, domain, domain); ++spa_string_add(&request->buf, offsetof(SPAAuthRequest, buf), &request->user, ++ u); ++spa_string_add(&request->buf, offsetof(SPAAuthRequest, buf), &request->domain, ++ domain); + } + + + + void +@@ -1424,11 +1420,11 @@ int random_seed = (int)time(NULL) ^ ((p + /* Ensure challenge data is cleared, in case it isn't all used. This + patch added by PH on suggestion of Russell King */ + + memset(challenge, 0, sizeof(SPAAuthChallenge)); + +-challenge->bufIndex = 0; ++challenge->buf.bufIndex = 0; + memcpy (challenge->ident, "NTLMSSP\0", 8); + SIVAL (&challenge->msgType, 0, 2); + SIVAL (&challenge->flags, 0, 0x00008201); + SIVAL (&challenge->uDomain.len, 0, 0x0000); + SIVAL (&challenge->uDomain.maxlen, 0, 0x0000); +@@ -1446,58 +1442,16 @@ memcpy(challenge->challengeData,chalstr, + } + + + + +-/* This is the original source of this function, preserved here for reference. ++/* The original version of this function is available in git. + The new version below was re-organized by PH following a patch and some further + suggestions from Mark Lyda to fix the problem that is described at the head of + this module. At the same time, I removed the untidiness in the code below that +-involves the "d" and "domain" variables. */ +- +-#ifdef NEVER +-void +-spa_build_auth_response (SPAAuthChallenge * challenge, +- SPAAuthResponse * response, char *user, +- char *password) +-{ +-uint8x lmRespData[24]; +-uint8x ntRespData[24]; +-char *d = strdup (GetUnicodeString (challenge, uDomain)); +-char *domain = d; +-char *u = strdup (user); +-char *p = strchr (u, '@'); +- +-if (p) +- { +- domain = p + 1; +- *p = '\0'; +- } +- +-spa_smb_encrypt (US password, challenge->challengeData, lmRespData); +-spa_smb_nt_encrypt (US password, challenge->challengeData, ntRespData); +- +-response->bufIndex = 0; +-memcpy (response->ident, "NTLMSSP\0\0\0", 8); +-SIVAL (&response->msgType, 0, 3); +- +-spa_bytes_add (response, lmResponse, lmRespData, 24); +-spa_bytes_add (response, ntResponse, ntRespData, 24); +-spa_unicode_add_string (response, uDomain, domain); +-spa_unicode_add_string (response, uUser, u); +-spa_unicode_add_string (response, uWks, u); +-spa_string_add (response, sessionKey, NULL); +- +-response->flags = challenge->flags; +- +-free (d); +-free (u); +-} +-#endif +- +- +-/* This is the re-organized version (see comments above) */ ++involves the "d" and "domain" variables. ++Further modified by JGH to replace complex macro "functions" with real ones. */ + + void + spa_build_auth_response (SPAAuthChallenge * challenge, + SPAAuthResponse * response, uschar * user, + uschar * password) +@@ -1507,10 +1461,12 @@ uint8x ntRespData[24]; + uint32x cf = IVAL(&challenge->flags, 0); + uschar * u = string_copy(user); + uschar * p = Ustrchr(u, '@'); + uschar * d = NULL; + uschar * domain; ++SPAbuf * buf = &response->buf; ++const size_t off = offsetof(SPAAuthResponse, buf); + + if (p) + { + domain = p + 1; + *p = '\0'; +@@ -1521,28 +1477,31 @@ else domain = d = string_copy(cf & 0x1 + : CUS get_challenge_str(challenge, &challenge->uDomain)); + + spa_smb_encrypt(password, challenge->challengeData, lmRespData); + spa_smb_nt_encrypt(password, challenge->challengeData, ntRespData); + +-response->bufIndex = 0; ++buf->bufIndex = 0; + memcpy (response->ident, "NTLMSSP\0\0\0", 8); + SIVAL (&response->msgType, 0, 3); + +-spa_bytes_add(response, lmResponse, lmRespData, cf & 0x200 ? 24 : 0); +-spa_bytes_add(response, ntResponse, ntRespData, cf & 0x8000 ? 24 : 0); ++spa_bytes_add(buf, off, &response->lmResponse, lmRespData, cf & 0x200 ? 24 : 0); ++spa_bytes_add(buf, off, &response->ntResponse, ntRespData, cf & 0x8000 ? 24 : 0); + +-if (cf & 0x1) { /* Unicode Text */ +- spa_unicode_add_string(response, uDomain, domain); +- spa_unicode_add_string(response, uUser, u); +- spa_unicode_add_string(response, uWks, u); +-} else { /* OEM Text */ +- spa_string_add(response, uDomain, domain); +- spa_string_add(response, uUser, u); +- spa_string_add(response, uWks, u); +-} ++if (cf & 0x1) /* Unicode Text */ ++ { ++ spa_unicode_add_string(buf, off, &response->uDomain, domain); ++ spa_unicode_add_string(buf, off, &response->uUser, u); ++ spa_unicode_add_string(buf, off, &response->uWks, u); ++ } ++else ++ { /* OEM Text */ ++ spa_string_add(buf, off, &response->uDomain, domain); ++ spa_string_add(buf, off, &response->uUser, u); ++ spa_string_add(buf, off, &response->uWks, u); ++ } + +-spa_string_add(response, sessionKey, NULL); ++spa_string_add(buf, off, &response->sessionKey, NULL); + response->flags = challenge->flags; + } + + + #endif /*!MACRO_PREDEF*/ +--- a/src/auths/auth-spa.h ++++ b/src/auths/auth-spa.h +@@ -35,31 +35,35 @@ typedef struct + uint32x offset; + } SPAStrHeader; + + typedef struct + { ++ uint8x buffer[1024]; ++ uint32x bufIndex; ++} SPAbuf; ++ ++typedef struct ++{ + char ident[8]; + uint32x msgType; + SPAStrHeader uDomain; + uint32x flags; + uint8x challengeData[8]; + uint8x reserved[8]; + SPAStrHeader emptyString; +- uint8x buffer[1024]; +- uint32x bufIndex; ++ SPAbuf buf; + } SPAAuthChallenge; + + + typedef struct + { + char ident[8]; + uint32x msgType; + uint32x flags; + SPAStrHeader user; + SPAStrHeader domain; +- uint8x buffer[1024]; +- uint32x bufIndex; ++ SPAbuf buf; + } SPAAuthRequest; + + typedef struct + { + char ident[8]; +@@ -69,15 +73,14 @@ typedef struct + SPAStrHeader uDomain; + SPAStrHeader uUser; + SPAStrHeader uWks; + SPAStrHeader sessionKey; + uint32x flags; +- uint8x buffer[1024]; +- uint32x bufIndex; ++ SPAbuf buf; + } SPAAuthResponse; + +-#define spa_request_length(ptr) (((ptr)->buffer - (uint8x*)(ptr)) + (ptr)->bufIndex) ++#define spa_request_length(ptr) (((uint8x*)&(ptr)->buf - (uint8x*)(ptr)) + (ptr)->buf.bufIndex) + + void spa_bits_to_base64 (unsigned char *, const unsigned char *, int); + int spa_base64_to_bits(char *, int, const char *); + void spa_build_auth_response (SPAAuthChallenge * challenge, + SPAAuthResponse * response, uschar * user, uschar * password); +--- a/src/auths/spa.c ++++ b/src/auths/spa.c +@@ -190,11 +190,11 @@ that causes failure if the size of msgbu + int i; + char * p; + int len = SVAL(&responseptr->uUser.len,0)/2; + + if ( (off = IVAL(&responseptr->uUser.offset,0)) >= sizeof(SPAAuthResponse) +- || len >= sizeof(responseptr->buffer)/2 ++ || len >= sizeof(responseptr->buf.buffer)/2 + || (p = (CS responseptr) + off) + len*2 >= CS (responseptr+1) + ) + { + DEBUG(D_auth) + debug_printf("auth_spa_server(): bad uUser spec in response\n"); diff -Nru exim4-4.96/debian/patches/82_06-SPA-authenticator-harden-buffer-usage.patch exim4-4.96/debian/patches/82_06-SPA-authenticator-harden-buffer-usage.patch --- exim4-4.96/debian/patches/82_06-SPA-authenticator-harden-buffer-usage.patch 1970-01-01 00:00:00.000000000 +0000 +++ exim4-4.96/debian/patches/82_06-SPA-authenticator-harden-buffer-usage.patch 2026-05-02 09:33:28.000000000 +0000 @@ -0,0 +1,258 @@ +From 68b963b9f75ca27b38e1c0f8c87037990199f505 Mon Sep 17 00:00:00 2001 +From: Jeremy Harris +Date: Tue, 10 Mar 2026 21:29:52 +0000 +Subject: [PATCH 4/4] SPA authenticator: harden buffer usage + +CVE-2026-40687 +--- + doc/ChangeLog | 4 + + .../CVE2026-40687.assessment | 12 ++ + src/auths/auth-spa.c | 113 +++--------------- + src/auths/auth-spa.h | 1 - + 4 files changed, 35 insertions(+), 95 deletions(-) + create mode 100644 doc/doc-txt/exim-security-2026-04.1/CVE2026-40687.assessment + +--- a/doc/ChangeLog ++++ b/doc/ChangeLog +@@ -133,10 +133,14 @@ JH/07 Bug 3106: Fix coding in SPA authen + properly parenthesized, resulting in a logic error. While the simple + fix was provided by Andrew Aitchison, the over-large code block resulting + from this macro made me want to replace it with a real function so more + extensive rework becamse needed. + ++JH/36 CVE-2026-40687: The spa authenticator used an unitialized buffer, which ++ could result in a leak of data. It also had potential for wrting past the ++ end of static buffers, by choice of data provided by the client. ++ + + Exim version 4.96 + ----------------- + + JH/01 Move the wait-for-next-tick (needed for unique message IDs) from +--- /dev/null ++++ b/doc/doc-txt/exim-security-2026-04.1/CVE2026-40687.assessment +@@ -0,0 +1,12 @@ ++CVE2026-40687 ++ ++Vulnerability conditions ++------------------------ ++ ++- Config uses the "spa" authenticator driver ++ ++Impact ++------ ++ ++- Remote-triggered crash (only of connection process, not daemon) ++- Infoleak +--- a/src/auths/auth-spa.c ++++ b/src/auths/auth-spa.c +@@ -162,11 +162,10 @@ int main (int argc, char ** argv) + + extern int DEBUGLEVEL; + + #include "../exim.h" + #include "auth-spa.h" +-#include + + + #ifndef _BYTEORDER_H + # define _BYTEORDER_H + +@@ -410,10 +409,12 @@ spa_base64_to_bits (char *out, int outle + /* base 64 to raw bytes in quasi-big-endian order, returning count of bytes */ + { + int len = 0; + uschar digit1, digit2, digit3, digit4; + ++memset(out, 0, outlength); ++ + if (in[0] == '+' && in[1] == ' ') + in += 2; + if (*in == '\r') + return (0); + +@@ -1237,63 +1238,37 @@ spa_string_add(SPAbuf * buffer, size_t o + int len = string ? Ustrlen(string) : 0; + spa_bytes_add(buffer, off, header, string, len); + } + + static uschar * +-strToUnicode(const uschar * p) ++strToUnicode(const uschar * p, int len) + { +-static uschar buf[1024]; +-size_t l = Ustrlen(p); +- +-assert (l * 2 < sizeof buf); +- +-for (int i = 0; l--; ) { buf[i++] = *p++; buf[i++] = 0; } ++uschar * buf = store_get(len * 2, p); ++for (int i = 0; len--; ) { buf[i++] = *p++; buf[i++] = 0; } + return buf; + } + + static void + spa_unicode_add_string(SPAbuf * buffer, size_t off, SPAStrHeader * header, + const uschar * string) + { +-const uschar * p = string; +-uschar * b = NULL; ++const uschar * p = string, * b = NULL; + int len = 0; + if (p) + { + len = Ustrlen(p); +- b = US strToUnicode(p); ++ b = strToUnicode(p, len); + } + spa_bytes_add(buffer, off, header, b, len*2); + } + + +-#ifdef notdef +- +-#define DumpBuffer(fp, structPtr, header) \ +- dumpRaw(fp,(US structPtr)+IVAL(&structPtr->header.offset,0),SVAL(&structPtr->header.len,0)) +- +- +-static void +-dumpRaw (FILE * fp, uschar *buf, size_t len) ++uschar * ++unicodeToString (char * p, size_t len) + { + int i; +- +-for (i = 0; i < len; ++i) +- fprintf (fp, "%02x ", buf[i]); +- +-fprintf (fp, "\n"); +-} +- +-#endif +- +-char * +-unicodeToString (char *p, size_t len) +-{ +-int i; +-static char buf[1024]; +- +-assert (len + 1 < sizeof buf); ++uschar * buf = store_get((int)len + 1, p); + + for (i = 0; i < len; ++i) + { + buf[i] = *p & 0x7f; + p += 2; +@@ -1302,89 +1277,37 @@ for (i = 0; i < len; ++i) + buf[i] = '\0'; + return buf; + } + + static uschar * +-toString (char *p, size_t len) ++toString (const char *p, size_t len) + { +-static uschar buf[1024]; +- +-assert (len + 1 < sizeof buf); ++uschar * buf = store_get((int)len + 1, p); + + memcpy (buf, p, len); +-buf[len] = 0; ++buf[len] = '\0'; + return buf; + } + + static inline uschar * + get_challenge_unistr(SPAAuthChallenge * challenge, SPAStrHeader * hdr) + { +-int off = IVAL(&hdr->offset, 0); +-int len = SVAL(&hdr->len, 0); +-return off + len < sizeof(SPAAuthChallenge) +- ? US unicodeToString(CS challenge + off, len/2) : US""; +-} ++int offset = IVAL(&hdr->offset, 0), len = SVAL(&hdr->len, 0); + +-static inline uschar * +-get_challenge_str(SPAAuthChallenge * challenge, SPAStrHeader * hdr) +-{ +-int off = IVAL(&hdr->offset, 0); +-int len = SVAL(&hdr->len, 0); +-return off + len < sizeof(SPAAuthChallenge) +- ? US toString(CS challenge + off, len) : US""; ++return offset + len < sizeof(SPAAuthChallenge) ++ ? unicodeToString(CS challenge + offset, len/2) : US""; + } + +-#ifdef notdef +- +-#define GetUnicodeString(structPtr, header) \ +- unicodeToString(((char*)structPtr) + IVAL(&structPtr->header.offset,0) , SVAL(&structPtr->header.len,0)/2) +- +-#define GetString(structPtr, header) \ +- toString(((CS structPtr) + IVAL(&structPtr->header.offset,0)), SVAL(&structPtr->header.len,0)) +- +- +-void +-dumpSmbNtlmAuthRequest (FILE * fp, SPAAuthRequest * request) ++static uschar * ++get_challenge_str(SPAAuthChallenge * challenge, SPAStrHeader * hdr) + { +-fprintf (fp, "NTLM Request:\n"); +-fprintf (fp, " Ident = %s\n", request->ident); +-fprintf (fp, " mType = %d\n", IVAL (&request->msgType, 0)); +-fprintf (fp, " Flags = %08x\n", IVAL (&request->flags, 0)); +-fprintf (fp, " User = %s\n", GetString (request, user)); +-fprintf (fp, " Domain = %s\n", GetString (request, domain)); +-} ++int offset = IVAL(&hdr->offset, 0), len = SVAL(&hdr->len, 0); + +-void +-dumpSmbNtlmAuthChallenge (FILE * fp, SPAAuthChallenge * challenge) +-{ +-fprintf (fp, "NTLM Challenge:\n"); +-fprintf (fp, " Ident = %s\n", challenge->ident); +-fprintf (fp, " mType = %d\n", IVAL (&challenge->msgType, 0)); +-fprintf (fp, " Domain = %s\n", GetUnicodeString (challenge, uDomain)); +-fprintf (fp, " Flags = %08x\n", IVAL (&challenge->flags, 0)); +-fprintf (fp, " Challenge = "); +-dumpRaw (fp, challenge->challengeData, 8); ++return offset + len < sizeof(SPAAuthChallenge) ++ ? toString(CS challenge + offset, len) : US""; + } + +-void +-dumpSmbNtlmAuthResponse (FILE * fp, SPAAuthResponse * response) +-{ +-fprintf (fp, "NTLM Response:\n"); +-fprintf (fp, " Ident = %s\n", response->ident); +-fprintf (fp, " mType = %d\n", IVAL (&response->msgType, 0)); +-fprintf (fp, " LmResp = "); +-DumpBuffer (fp, response, lmResponse); +-fprintf (fp, " NTResp = "); +-DumpBuffer (fp, response, ntResponse); +-fprintf (fp, " Domain = %s\n", GetUnicodeString (response, uDomain)); +-fprintf (fp, " User = %s\n", GetUnicodeString (response, uUser)); +-fprintf (fp, " Wks = %s\n", GetUnicodeString (response, uWks)); +-fprintf (fp, " sKey = "); +-DumpBuffer (fp, response, sessionKey); +-fprintf (fp, " Flags = %08x\n", IVAL (&response->flags, 0)); +-} +-#endif + + void + spa_build_auth_request (SPAAuthRequest * request, uschar * user, uschar * domain) + { + uschar * u = string_copy(user); +--- a/src/auths/auth-spa.h ++++ b/src/auths/auth-spa.h +@@ -88,8 +88,8 @@ void spa_build_auth_request (SPAAuthRequ + uschar * domain); + extern void spa_smb_encrypt (unsigned char * passwd, unsigned char * c8, + unsigned char * p24); + extern void spa_smb_nt_encrypt (unsigned char * passwd, unsigned char * c8, + unsigned char * p24); +-extern char *unicodeToString(char *p, size_t len); ++extern uschar *unicodeToString(char *p, size_t len); + extern void spa_build_auth_challenge(SPAAuthRequest *, SPAAuthChallenge *); + diff -Nru exim4-4.96/debian/patches/series exim4-4.96/debian/patches/series --- exim4-4.96/debian/patches/series 2025-03-21 14:35:40.000000000 +0000 +++ exim4-4.96/debian/patches/series 2026-05-02 09:33:28.000000000 +0000 @@ -55,4 +55,10 @@ 78_03-Compiler-quietening.patch 80_Lookups-fix-dbmnz-crash-on-zero-length-datum.-Bug-30.patch 81_CVE-2025-30232.patch +82_01-GnuTLS-fix-hostname-verify-of-server-cert-for-empty-.patch +82_02-Support-musl-libc-dn_expand-oddity.patch +82_03-when-dewrap-only-skip-if-associated-char.patch +82_04-Expansions-harden-for-malformed-UTF-8.patch +82_05-Fix-SPA-authenticator.-Bug-3106.patch +82_06-SPA-authenticator-harden-buffer-usage.patch 90_localscan_dlopen.dpatch