--- Begin Message ---
Package: release.debian.org
Severity: normal
Tags: bookworm
Control: affects -1 + src:exim4
User: [email protected]
Usertags: pu
Hello,
after discussion with the security team I would like to fix a couple
CVEs and unrelated to that a interoperability issue via
stable/oldstable uploads:
a) All the CVE fixes from the recent security update 4.99.2:
* CVE-2026-40684 Possible crash with malicious DNS data when using musl
libc ...
While we do not use musl libc, it is small contained patch, so I would
still prefer to inculde it.
* 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.
As far I understand this also does not hit our binaries, since we do
not build with JSON looks enabled. Howver users can build private
packages from our sources. One-line change.
* CVE-2026-40686 Possible OOB read with large UTF8 trailing character
... Another tiny change, applies to Debian.
* CVE-2026-40687 Possible OOB read/write with SPA authenticator.
This is client side and needs a hostile/compromised external
counterpart. This patch is rather big and required some handholding to
apply. For bookworm this required cherry-picking another upstream
change to let the patch apply.
b) 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).
This is also a small change and something our DSA would appreciate.
Upstream discussion starts here:
https://lists.exim.org/lurker/message/20260413.184322.ecbabb9e.en.html
TIA, cu Andreas
--
"You people are noisy," Nia said.
I made the gesture of agreement.
diff --git a/debian/changelog b/debian/changelog
index 5e267e24..3f4b9491 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -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 <[email protected]> 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 --git a/debian/patches/82_01-GnuTLS-fix-hostname-verify-of-server-cert-for-empty-.patch b/debian/patches/82_01-GnuTLS-fix-hostname-verify-of-server-cert-for-empty-.patch
new file mode 100644
index 00000000..c62e5496
--- /dev/null
+++ b/debian/patches/82_01-GnuTLS-fix-hostname-verify-of-server-cert-for-empty-.patch
@@ -0,0 +1,84 @@
+From 371e5210218746e876fd71c888fdb666c85ceb56 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <[email protected]>
+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 --git a/debian/patches/82_02-Support-musl-libc-dn_expand-oddity.patch b/debian/patches/82_02-Support-musl-libc-dn_expand-oddity.patch
new file mode 100644
index 00000000..99693f8a
--- /dev/null
+++ b/debian/patches/82_02-Support-musl-libc-dn_expand-oddity.patch
@@ -0,0 +1,75 @@
+From 628bbaca7672748d941a12e7cd5f0122a4e18c81 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <[email protected]>
+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 --git a/debian/patches/82_03-when-dewrap-only-skip-if-associated-char.patch b/debian/patches/82_03-when-dewrap-only-skip-if-associated-char.patch
new file mode 100644
index 00000000..8c06a59f
--- /dev/null
+++ b/debian/patches/82_03-when-dewrap-only-skip-if-associated-char.patch
@@ -0,0 +1,58 @@
+From 9fdc057e71b87c87a0d3d2288b2810a0efaaba57 Mon Sep 17 00:00:00 2001
+From: Bernard Quatermass <[email protected]>
+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 --git a/debian/patches/82_04-Expansions-harden-for-malformed-UTF-8.patch b/debian/patches/82_04-Expansions-harden-for-malformed-UTF-8.patch
new file mode 100644
index 00000000..46b7faa9
--- /dev/null
+++ b/debian/patches/82_04-Expansions-harden-for-malformed-UTF-8.patch
@@ -0,0 +1,59 @@
+From f2570bde16fb4d4a1242ff363a4c4eecf6372efc Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <[email protected]>
+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 --git a/debian/patches/82_05-Fix-SPA-authenticator.-Bug-3106.patch b/debian/patches/82_05-Fix-SPA-authenticator.-Bug-3106.patch
new file mode 100644
index 00000000..e4788e83
--- /dev/null
+++ b/debian/patches/82_05-Fix-SPA-authenticator.-Bug-3106.patch
@@ -0,0 +1,413 @@
+From a731c6050a1510734776851aaff5ad2f32fa3ae5 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <[email protected]>
+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 --git a/debian/patches/82_06-SPA-authenticator-harden-buffer-usage.patch b/debian/patches/82_06-SPA-authenticator-harden-buffer-usage.patch
new file mode 100644
index 00000000..10070488
--- /dev/null
+++ b/debian/patches/82_06-SPA-authenticator-harden-buffer-usage.patch
@@ -0,0 +1,258 @@
+From 68b963b9f75ca27b38e1c0f8c87037990199f505 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <[email protected]>
+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 <assert.h>
+
+
+ #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 --git a/debian/patches/series b/debian/patches/series
index 7fe80937..d9eb8562 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -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
signature.asc
Description: PGP signature
--- End Message ---