summaryrefslogtreecommitdiffstats
path: root/debian/patches
diff options
context:
space:
mode:
Diffstat (limited to 'debian/patches')
-rwxr-xr-xdebian/patches/31_eximmanpage.dpatch250
-rw-r--r--debian/patches/32_exim4.dpatch106
-rwxr-xr-xdebian/patches/33_eximon.binary.dpatch17
-rwxr-xr-xdebian/patches/34_eximstatsmanpage.dpatch20
-rwxr-xr-xdebian/patches/35_install.dpatch49
-rwxr-xr-xdebian/patches/60_convert4r4.dpatch41
-rw-r--r--debian/patches/67_unnecessaryCopt.diff69
-rwxr-xr-xdebian/patches/70_remove_exim-users_references.dpatch38
-rw-r--r--debian/patches/75_01-Fix-exit-on-attempt-to-rewrite-a-malformed-address.-.patch57
-rw-r--r--debian/patches/75_05-SPF-fix-memory-accounting-for-error-case.patch25
-rw-r--r--debian/patches/75_08-Fix-regex-n-use-after-free.-Bug-2915.patch196
-rw-r--r--debian/patches/75_09-Fix-non-WITH_CONTENT_SCAN-build.patch58
-rw-r--r--debian/patches/75_10-Fix-non-WITH_CONTENT_SCAN-build-2.patch135
-rw-r--r--debian/patches/75_11-Fix-non-WITH_CONTENT_SCAN-build-3.patch45
-rw-r--r--debian/patches/75_16-GnuTLS-fix-for-clients-offering-no-TLS-extensions.patch114
-rw-r--r--debian/patches/75_18-Fix-Build-with-libopendmarc-1.4.x-fixes-2728.patch88
-rw-r--r--debian/patches/75_19-DMARC-fix-use-after-free-in-dmarc_dns_lookup.patch39
-rw-r--r--debian/patches/75_22-Fix-daemon-startup.-Bug-2930.patch68
-rw-r--r--debian/patches/75_23-Fix-reccipients-after-run.-.-Bug-2929.patch45
-rw-r--r--debian/patches/75_31-Fix-regext-substring-capture-variables-for-null-matc.patch79
-rw-r--r--debian/patches/75_32-Fix-regex-substring-capture-variables-for-null-match.patch94
-rw-r--r--debian/patches/75_34-Fix-regex-substring-capture-commentary.-Bug-2933.patch48
-rw-r--r--debian/patches/75_37-OpenSSL-when-preloading-creds-do-the-server-certs-be.patch232
-rw-r--r--debian/patches/75_38-OpenSSL-fix-double-expansion-of-tls_verify_certifica.patch217
-rw-r--r--debian/patches/75_42-Fix-run-arg-parsing.patch100
-rw-r--r--debian/patches/75_50-Fix-logging-of-max-size-log-line.patch82
-rw-r--r--debian/patches/75_55-Fix-recursion-on-dns_again_means_nonexist.-Bug-2911.patch76
-rw-r--r--debian/patches/75_58-Close-server-smtp-socket-explicitly-on-connect-ACL-d.patch50
-rw-r--r--debian/patches/75_60-OpenSSL-fix-tls_eccurve-setting-explicit-curve-group.patch184
-rw-r--r--debian/patches/75_62-OpenSSL-Fix-tls_eccurve-on-earlier-versions-than-3.0.patch42
-rw-r--r--debian/patches/75_63-OpenSSL-log-conns-rejected-for-bad-ALPN-with-the-off.patch99
-rw-r--r--debian/patches/75_64-DANE-do-not-check-dns_again_means_nonexist-for-TLSA-.patch96
-rw-r--r--debian/patches/75_66-Fix-crash-in-expansions.patch84
-rw-r--r--debian/patches/75_68-Fix-srs_encode-.-for-mod-1024-day-zero.patch62
-rw-r--r--debian/patches/75_70-Fix-variable-initialisation-in-smtp-transport.-Bug-2.patch46
-rw-r--r--debian/patches/75_71-Auths-fix-possible-OOB-write-in-external-authenticat.patch22
-rw-r--r--debian/patches/75_72-Auths-use-uschar-more-in-spa-authenticator.patch226
-rw-r--r--debian/patches/75_73-Auths-fix-possible-OOB-write-in-SPA-authenticator.-B.patch24
-rw-r--r--debian/patches/75_74-Auths-fix-possible-OOB-read-in-SPA-authenticator.-Bu.patch75
-rw-r--r--debian/patches/75_74-Cancel-early-pipe-on-an-observed-advertising-change.patch35
-rw-r--r--debian/patches/75_76-Expansions-disallow-UTF-16-surrogates-from-utf8clean.patch99
-rw-r--r--debian/patches/75_77-GnuTLS-fix-crash-with-tls_dhparam-none.patch92
-rw-r--r--debian/patches/75_79-Fix-recipients-expansion-when-used-within-run.-.-Bug.patch294
-rw-r--r--debian/patches/75_82-GnuTLS-fix-autogen-cert-expiry-date.-Bug-3014.patch42
-rw-r--r--debian/patches/75_83-Re-fix-live-variable-value-free.-The-inital-fix-resu.patch77
-rw-r--r--debian/patches/76-01-fix-string_is_ip_address-CVE-2023-42117-Bug-3031.patch309
-rw-r--r--debian/patches/76-02-SPF-harden-against-crafted-DNS-responses.patch64
-rw-r--r--debian/patches/76-03-Harden-dnsdb-against-crafted-DNS-responses.-Bug-3033.patch244
-rw-r--r--debian/patches/76-10-Fix-tr.-and-empty-strings.-Bug-3023.patch56
-rw-r--r--debian/patches/76-12-DNS-more-hardening-against-crafted-responses.patch219
-rw-r--r--debian/patches/76-14-Lookups-Fix-dnsdb-lookup-of-multi-chunk-TXT.-Bug-305.patch118
-rw-r--r--debian/patches/77_CVE-2023-51766_4.97.1-release.diff440
-rw-r--r--debian/patches/90_localscan_dlopen.dpatch347
-rw-r--r--debian/patches/series53
54 files changed, 5887 insertions, 0 deletions
diff --git a/debian/patches/31_eximmanpage.dpatch b/debian/patches/31_eximmanpage.dpatch
new file mode 100755
index 0000000..cfbfc26
--- /dev/null
+++ b/debian/patches/31_eximmanpage.dpatch
@@ -0,0 +1,250 @@
+Description: We ship the binary as exim4 instead of exim, fix manpage
+ accordingly.
+Author: Marc Haber <mh+debian-packages@zugschlus.de>,
+ Andreas Metzler <ametzler@bebt.de>
+Last-Update: 2019-10-17
+Forwarded: not-needed (upstream uses the "exim" name)
+
+--- a/doc/exim.8
++++ b/doc/exim.8
+@@ -1,9 +1,9 @@
+-.TH EXIM 8
++.TH EXIM4 8
+ .SH NAME
+-exim \- a Mail Transfer Agent
++exim4 \- a Mail Transfer Agent
+ .SH SYNOPSIS
+ .nf
+-.B exim [options] arguments ...
++.B exim4 [options] arguments ...
+ .B mailq [options] arguments ...
+ .B rsmtp [options] arguments ...
+ .B rmail [options] arguments ...
+@@ -40,7 +40,7 @@ local message on the standard input, wit
+ recipients) is assumed. Thus, for example, if Exim is installed in
+ \fI/usr/sbin\fP, you can send a message from the command line like this:
+ .sp
+- /usr/sbin/exim -i <recipient-address(es)>
++ /usr/sbin/exim4 -i <recipient-address(es)>
+ <message content, including all the header lines>
+ CTRL-D
+ .sp
+@@ -125,8 +125,8 @@ ports, on multiple ports, and only on sp
+ .sp
+ When a listening daemon
+ is started without the use of \fB\-oX\fP (that is, without overriding the normal
+-configuration), it writes its process id to a file called exim\-daemon.pid
+-in Exim's spool directory. This location can be overridden by setting
++configuration), it writes its process id to a file called
++/run/exim4/exim.pid. This location can be overridden by setting
+ PID_FILE_PATH in Local/Makefile. The file is written while Exim is still
+ running as root.
+ .sp
+@@ -180,7 +180,7 @@ available to admin users.
+ This option operates like \fB\-be\fP except that it must be followed by the name
+ of a file. For example:
+ .sp
+- exim \-bem /tmp/testmessage
++ exim4 \-bem /tmp/testmessage
+ .sp
+ The file is read as a message (as if receiving a locally\-submitted non\-SMTP
+ message) before any of the test expansions are done. Thus, message\-specific
+@@ -206,7 +206,7 @@ If you want to test a system filter file
+ can use both \fB\-bF\fP and \fB\-bf\fP on the same command, in order to test a system
+ filter and a user filter in the same run. For example:
+ .sp
+- exim \-bF /system/filter \-bf /user/filter </test/message
++ exim4 \-bF /system/filter \-bf /user/filter </test/message
+ .sp
+ This is helpful when the system filter adds header lines or sets filter
+ variables that are used by the user filter.
+@@ -258,8 +258,8 @@ This option runs a fake SMTP session as
+ standard input and output. The IP address may include a port number at the end,
+ after a full stop. For example:
+ .sp
+- exim \-bh 10.9.8.7.1234
+- exim \-bh fe80::a00:20ff:fe86:a061.5678
++ exim4 \-bh 10.9.8.7.1234
++ exim4 \-bh fe80::a00:20ff:fe86:a061.5678
+ .sp
+ When an IPv6 address is given, it is converted into canonical form. In the case
+ of the second example above, the value of \fI$sender_host_address\fP after
+@@ -417,7 +417,7 @@ main configuration options to be written
+ of one or more specific options can be requested by giving their names as
+ arguments, for example:
+ .sp
+- exim \-bP qualify_domain hold_domains
++ exim4 \-bP qualify_domain hold_domains
+ .sp
+ However, any option setting that is preceded by the word "hide" in the
+ configuration file is not shown in full, except to an admin user. For other
+@@ -445,7 +445,7 @@ written directly into the spool director
+ .sp
+ If \fB\-bP\fP is followed by a name preceded by +, for example,
+ .sp
+- exim \-bP +local_domains
++ exim4 \-bP +local_domains
+ .sp
+ it searches for a matching named list of any type (domain, host, address, or
+ local part) and outputs what it finds.
+@@ -454,7 +454,7 @@ If one of the words \fBrouter\fP, \fBtra
+ followed by the name of an appropriate driver instance, the option settings for
+ that driver are output. For example:
+ .sp
+- exim \-bP transport local_delivery
++ exim4 \-bP transport local_delivery
+ .sp
+ The generic driver options are output first, followed by the driver's private
+ options. A list of the names of drivers of a particular type can be obtained by
+@@ -539,7 +539,7 @@ This option is for testing retry rules,
+ arguments. It causes Exim to look for a retry rule that matches the values
+ and to write it to the standard output. For example:
+ .sp
+- exim \-brt bach.comp.mus.example
++ exim4 \-brt bach.comp.mus.example
+ Retry rule: *.comp.mus.example F,2h,15m; F,4d,30m;
+ .sp
+ The first
+@@ -552,7 +552,7 @@ rule is found that matches the host, one
+ sought. Finally, an argument that is the name of a specific delivery error, as
+ used in setting up retry rules, can be given. For example:
+ .sp
+- exim \-brt haydn.comp.mus.example quota_3d
++ exim4 \-brt haydn.comp.mus.example quota_3d
+ Retry rule: *@haydn.comp.mus.example quota_3d F,1h,15m
+ .TP 10
+ \fB\-brw\fP
+@@ -655,7 +655,7 @@ doing such tests.
+ .TP 10
+ \fB\-bV\fP
+ This option causes Exim to write the current version number, compilation
+-number, and compilation date of the \fIexim\fP binary to the standard output.
++number, and compilation date of the \fIexim4\fP binary to the standard output.
+ It also lists the DBM library that is being used, the optional modules (such as
+ specific lookup types), the drivers that are included in the binary, and the
+ name of the runtime configuration file that is in use.
+@@ -683,7 +683,7 @@ If no arguments are given, Exim runs in
+ right angle bracket for addresses to be verified.
+ .sp
+ Unlike the \fB\-be\fP test option, you cannot arrange for Exim to use the
+-readline() function, because it is running as \fIexim\fP and there are
++readline() function, because it is running as \fIexim4\fP and there are
+ security issues.
+ .sp
+ Verification differs from address testing (the \fB\-bt\fP option) in that routers
+@@ -796,14 +796,14 @@ command line item. \fB\-D\fP can be used
+ string, in which case the equals sign is optional. These two commands are
+ synonymous:
+ .sp
+- exim \-DABC ...
+- exim \-DABC= ...
++ exim4 \-DABC ...
++ exim4 \-DABC= ...
+ .sp
+ To include spaces in a macro definition item, quotes must be used. If you use
+ quotes, spaces are permitted around the macro name and the equals sign. For
+ example:
+ .sp
+- exim '\-D ABC = something' ...
++ exim4 '\-D ABC = something' ...
+ .sp
+ \fB\-D\fP may be repeated up to 10 times on a command line.
+ Only macro names up to 22 letters long can be set.
+@@ -938,8 +938,8 @@ never provoke a bounce. An empty sender
+ string, or as a pair of angle brackets with nothing between them, as in these
+ examples of shell commands:
+ .sp
+- exim \-f '<>' user@domain
+- exim \-f "" user@domain
++ exim4 \-f '<>' user@domain
++ exim4 \-f "" user@domain
+ .sp
+ In addition, the use of \fB\-f\fP is not restricted when testing a filter file
+ with \fB\-bf\fP or when testing or verifying addresses using the \fB\-bt\fP or
+@@ -1315,12 +1315,12 @@ other circumstances, they are ignored un
+ The \fB\-oMa\fP option sets the sender host address. This may include a port
+ number at the end, after a full stop (period). For example:
+ .sp
+- exim \-bs \-oMa 10.9.8.7.1234
++ exim4 \-bs \-oMa 10.9.8.7.1234
+ .sp
+ An alternative syntax is to enclose the IP address in square brackets,
+ followed by a colon and the port number:
+ .sp
+- exim \-bs \-oMa [10.9.8.7]:1234
++ exim4 \-bs \-oMa [10.9.8.7]:1234
+ .sp
+ The IP address is placed in the \fI$sender_host_address\fP variable, and the
+ port, if present, in \fI$sender_host_port\fP. If both \fB\-oMa\fP and \fB\-bh\fP
+@@ -1526,22 +1526,22 @@ If other commandline options specify an
+ will specify a queue to operate on.
+ For example:
+ .sp
+- exim \-bp \-qGquarantine
++ exim4 \-bp \-qGquarantine
+ mailq \-qGquarantine
+- exim \-qGoffpeak \-Rf @special.domain.example
++ exim4 \-qGoffpeak \-Rf @special.domain.example
+ .TP 10
+ \fB\-q\fP<\fIqflags\fP> <\fIstart id\fP> <\fIend id\fP>
+ When scanning the queue, Exim can be made to skip over messages whose ids are
+ lexically less than a given value by following the \fB\-q\fP option with a
+ starting message id. For example:
+ .sp
+- exim \-q 0t5C6f\-0000c8\-00
++ exim4 \-q 0t5C6f\-0000c8\-00
+ .sp
+ Messages that arrived earlier than 0t5C6f\-0000c8\-00 are not inspected. If a
+ second message id is given, messages whose ids are lexically greater than it
+ are also skipped. If the same id is given twice, for example,
+ .sp
+- exim \-q 0t5C6f\-0000c8\-00 0t5C6f\-0000c8\-00
++ exim4 \-q 0t5C6f\-0000c8\-00 0t5C6f\-0000c8\-00
+ .sp
+ just one delivery process is started, for that message. This differs from
+ \fB\-M\fP in that retry data is respected, and it also differs from \fB\-Mc\fP in
+@@ -1557,7 +1557,7 @@ starting a queue runner process at inter
+ single daemon process handles both functions. A common way of starting up a
+ combined daemon at system boot time is to use a command such as
+ .sp
+- /usr/exim/bin/exim \-bd \-q30m
++ /usr/sbin/exim4 \-bd \-q30m
+ .sp
+ Such a daemon listens for incoming SMTP calls, and also starts a queue runner
+ process every 30 minutes.
+@@ -1588,7 +1588,7 @@ regular expression; otherwise it is a li
+ If you want to do periodic queue runs for messages with specific recipients,
+ you can combine \fB\-R\fP with \fB\-q\fP and a time value. For example:
+ .sp
+- exim \-q25m \-R @special.domain.example
++ exim4 \-q25m \-R @special.domain.example
+ .sp
+ This example does a queue run for messages with recipients in the given domain
+ every 25 minutes. Any additional flags that are specified with \fB\-q\fP are
+@@ -1704,6 +1704,26 @@ under most shells.
+ .sp
+ .
+ .SH "SEE ALSO"
++.BR exicyclog (8),
++.BR exigrep (8),
++.BR exim_checkaccess (8),
++.BR exim_convert4r4 (8),
++.BR exim_db (8),
++.BR exim_dbmbuild (8),
++.BR exim_lock (8),
++.BR eximon (8),
++.BR exinext (8),
++.BR exiqgrep (8),
++.BR exiqsumm (8),
++.BR exiwhat (8),
++.BR update\-exim4.conf (8),
++.BR update\-exim4defaults (8),
++/usr/share/doc/exim4\-base/,
++/usr/share/doc/exim4\-base/README.Debian.[gz|html].
+ .rs
+ .sp
+ The full Exim specification, the Exim book, and the Exim wiki.
++
++.SH AUTHOR
++This manual page was provided with the upstream Exim source package.
++It was enhanced for the Debian GNU/Linux system.
diff --git a/debian/patches/32_exim4.dpatch b/debian/patches/32_exim4.dpatch
new file mode 100644
index 0000000..967869d
--- /dev/null
+++ b/debian/patches/32_exim4.dpatch
@@ -0,0 +1,106 @@
+Description: Accommodate source for installing exim as exim4.
+Author: Andreas Metzler <ametzler@debian.org>
+Origin: vendor
+Forwarded: not-needed
+Last-Update: 2018-12-12
+
+--- a/OS/Makefile-Linux
++++ b/OS/Makefile-Linux
+@@ -28,9 +28,9 @@ XLFLAGS=-L$(X11)/lib
+ X11_LD_LIB=$(X11)/lib
+
+ EXIWHAT_PS_ARG=ax
+-EXIWHAT_EGREP_ARG='/exim( |$$)'
++EXIWHAT_EGREP_ARG='/exim4( |$$)'
+ EXIWHAT_MULTIKILL_CMD=killall
+-EXIWHAT_MULTIKILL_ARG=exim
++EXIWHAT_MULTIKILL_ARG=exim4
+ EXIWHAT_KILL_SIGNAL=-USR1
+
+ # End
+--- a/src/exicyclog.src
++++ b/src/exicyclog.src
+@@ -149,7 +149,7 @@ done
+
+ st=' '
+ exim_path=`grep "^[$st]*exim_path" $config | sed "s/.*=[$st]*//"`
+-if test "$exim_path" = ""; then exim_path=BIN_DIRECTORY/exim; fi
++if test "$exim_path" = ""; then exim_path=BIN_DIRECTORY/exim4; fi
+
+ spool_directory=`$exim_path -C $config -bP spool_directory | sed 's/.*=[ ]*//'`
+
+--- a/src/exim_checkaccess.src
++++ b/src/exim_checkaccess.src
+@@ -52,7 +52,7 @@ done
+ # a tab to keep the tab in one place.
+
+ exim_path=`perl -ne 'chop;if (/^\s*exim_path\s*=\s*(.*)/){print "$1\n";last;}' $config`
+-if test "$exim_path" = ""; then exim_path=BIN_DIRECTORY/exim; fi
++if test "$exim_path" = ""; then exim_path=BIN_DIRECTORY/exim4; fi
+
+
+ #########################################################################
+--- a/src/eximon.src
++++ b/src/eximon.src
+@@ -79,7 +79,7 @@ config=${EXIMON_EXIM_CONFIG-$config}
+
+ st=' '
+ EXIM_PATH=`grep "^[$st]*exim_path" $config | sed "s/.*=[$st]*//"`
+-if test "$EXIM_PATH" = ""; then EXIM_PATH=BIN_DIRECTORY/exim; fi
++if test "$EXIM_PATH" = ""; then EXIM_PATH=BIN_DIRECTORY/exim4; fi
+
+ SPOOL_DIRECTORY=`$EXIM_PATH -C $config -bP spool_directory | sed 's/.*=[ ]*//'`
+ LOG_FILE_PATH=`$EXIM_PATH -C $config -bP log_file_path | sed 's/.*=[ ]*//'`
+--- a/src/exinext.src
++++ b/src/exinext.src
+@@ -97,7 +97,7 @@ if [ "$exim_path" = "" ]; then
+ exim_path=`grep "^[$st]*exim_path" $config | sed "s/.*=[$st]*//"`
+ fi
+
+-if test "$exim_path" = ""; then exim_path=BIN_DIRECTORY/exim; fi
++if test "$exim_path" = ""; then exim_path=BIN_DIRECTORY/exim4; fi
+ spool_directory=`$exim_path $eximmacdef -C $config -bP spool_directory | sed 's/.*=[ ]*//'`
+ qualify_domain=`$exim_path $eximmacdef -C $config -bP qualify_domain | sed 's/.*=[ ]*//'`
+
+@@ -181,7 +181,7 @@ perl - $exim_path "$eximmacdef" $argone
+
+ # Run exim_dumpdb to get out the retry data and pick off what we want
+
+- open(DATA, "${exim}_dumpdb $spool retry |") ||
++ open(DATA, "/usr/sbin/exim_dumpdb $spool retry |") ||
+ die "can't run exim_dumpdb";
+
+ while (<DATA>)
+--- a/src/exiqgrep.src
++++ b/src/exiqgrep.src
+@@ -24,7 +24,7 @@ use Getopt::Std;
+ use File::Basename;
+
+ # Have this variable point to your exim binary.
+-my $exim = 'BIN_DIRECTORY/exim';
++my $exim = 'BIN_DIRECTORY/exim4';
+ my $eargs = '-bpu';
+ my %id;
+ my %opt;
+--- a/src/exiwhat.src
++++ b/src/exiwhat.src
+@@ -98,7 +98,7 @@ fi
+
+ st=' '
+ exim_path=`grep "^[$st]*exim_path" $config | sed "s/.*=[$st]*//"`
+-if test "$exim_path" = ""; then exim_path=BIN_DIRECTORY/exim; fi
++if test "$exim_path" = ""; then exim_path=BIN_DIRECTORY/exim4; fi
+ spool_directory=`$exim_path -C $config -bP spool_directory | sed "s/.*=[ ]*//"`
+ process_log_path=`$exim_path -C $config -bP process_log_path | sed "s/.*=[ ]*//"`
+
+--- a/src/globals.c
++++ b/src/globals.c
+@@ -906,7 +906,7 @@ const uschar *event_name = NULL;
+
+
+ gid_t exim_gid = EXIM_GID;
+-uschar *exim_path = US BIN_DIRECTORY "/exim"
++uschar *exim_path = US BIN_DIRECTORY "/exim4"
+ "\0<---------------Space to patch exim_path->";
+ uid_t exim_uid = EXIM_UID;
+ int expand_level = 0; /* Nesting depth, indent for debug */
diff --git a/debian/patches/33_eximon.binary.dpatch b/debian/patches/33_eximon.binary.dpatch
new file mode 100755
index 0000000..e0f5593
--- /dev/null
+++ b/debian/patches/33_eximon.binary.dpatch
@@ -0,0 +1,17 @@
+Description: We move eximon.bin out of $PATH to /usr/libexec/. Let it be
+ found there.
+Author: Andreas Piesk
+Last-Update: 2020-03-21
+Forwarded: not-needed
+
+--- a/OS/eximon.conf-Default
++++ b/OS/eximon.conf-Default
+@@ -5,7 +5,7 @@
+ # The name of the eximon binary, usually the same as the eximon script,
+ # with .bin stuck on the end.
+
+-EXIMON_BINARY="${EXIMON_BINARY-$0.bin}"
++EXIMON_BINARY="/usr/libexec/exim4/${EXIMON_BINARY-${0##*/}.bin}"
+
+ # The remaining parameters are values likely to be changed to suit the
+ # user's taste. They are documented in the EDITME file.
diff --git a/debian/patches/34_eximstatsmanpage.dpatch b/debian/patches/34_eximstatsmanpage.dpatch
new file mode 100755
index 0000000..3245965
--- /dev/null
+++ b/debian/patches/34_eximstatsmanpage.dpatch
@@ -0,0 +1,20 @@
+Description: Add note about installing perl-modules on Debian to
+ generated manpage
+Author: Andreas Metzler <ametzler@debian.org>
+Origin: vendor
+Forwarded: not-needed
+Last-Update: 2013-09-28
+
+--- exim4-4.82~rc1.orig/src/eximstats.src
++++ exim4-4.82~rc1/src/eximstats.src
+@@ -501,6 +501,10 @@ To install these, download and unpack th
+ make test
+ make install
+
++On B<Debian GNU/Linux> you can use
++C<apt-get install libgd-perl libgd-text-perl libgd-graph-perl>
++instead.
++
+ =item B<-chartdir>I <dir>
+
+ Create the charts in the directory <dir>
diff --git a/debian/patches/35_install.dpatch b/debian/patches/35_install.dpatch
new file mode 100755
index 0000000..d0ab602
--- /dev/null
+++ b/debian/patches/35_install.dpatch
@@ -0,0 +1,49 @@
+Description: Exim's installation scripts install the binary as
+ exim-<version> - disable this feature.
+Author: Andreas Metzler <ametzler@debian.org>
+Origin: vendor
+Forwarded: not-needed
+Last-Update: 2016-09-25
+
+--- a/scripts/exim_install
++++ b/scripts/exim_install
+@@ -221,6 +221,8 @@ while [ $# -gt 0 ]; do
+ version=exim-`$exim 2>/dev/null | \
+ awk '/Exim version/ { OFS=""; print $3,"-",substr($4,2,length($4)-1) }'`${EXE}
+
++ version=exim
++
+ if [ "${version}" = "exim-${EXE}" ]; then
+ echo $com ""
+ echo $com "*** Could not run $exim to find version number ***"
+@@ -370,10 +372,8 @@ done
+
+
+
+-# If there is no configuration file, install the default, modifying it to refer
+-# to the configured system aliases file. If there is no setting for
+-# SYSTEM_ALIASES_FILE, use the traditional /etc/aliases. If the file does not
+-# exist, install a default (dummy) for that too.
++# Install default configuration file
++# This is a local Debian modification.
+
+ # However, if CONFIGURE_FILE specifies a list of files, skip this code.
+
+@@ -396,7 +396,7 @@ elif [ ! -f ${CONFIGURE_FILE} ]; then
+ ${real} ${MKDIR} -p `${DIRNAME} ${CONFIGURE_FILE}`
+
+ echo sed -e '\\'
+- echo " \"/SYSTEM_ALIASES_FILE/ s'SYSTEM_ALIASES_FILE'${ACTUAL_SYSTEM_ALIASES_FILE}'\"" '\\'
++ echo " \"/SYSTEM_ALIASES_FILE/ s'SYSTEM_ALIASES_FILE'/etc/aliases'\"" '\\'
+ echo " ../src/configure.default > \${CONFIGURE_FILE}"
+
+ # I can't find a way of writing this using the ${real} feature because
+@@ -405,7 +405,7 @@ elif [ ! -f ${CONFIGURE_FILE} ]; then
+
+ if [ "$real" = "" ] ; then
+ sed -e \
+- "/SYSTEM_ALIASES_FILE/ s'SYSTEM_ALIASES_FILE'${ACTUAL_SYSTEM_ALIASES_FILE}'" \
++ "/SYSTEM_ALIASES_FILE/ s'SYSTEM_ALIASES_FILE'/etc/aliases'" \
+ ../src/configure.default > ${CONFIGURE_FILE}
+ else
+ true
diff --git a/debian/patches/60_convert4r4.dpatch b/debian/patches/60_convert4r4.dpatch
new file mode 100755
index 0000000..23fb0ae
--- /dev/null
+++ b/debian/patches/60_convert4r4.dpatch
@@ -0,0 +1,41 @@
+Description: Add a warning message to convert4r4
+Author: Marc Haber <mh+debian-packages@zugschlus.de>
+Origin: vendor
+Forwarded: not-needed
+Last-Update: 2013-09-28
+
+--- a/src/convert4r4.src
++++ b/src/convert4r4.src
+@@ -666,6 +666,32 @@ return defined $main{$_[0]} && $main{$_[
+
+ print STDERR "Runtime configuration file converter for Exim release 4.\n";
+
++if( !defined $ENV{"CONVERT4R4"} || $ENV{"CONVERT4R4"} ne "I understand this is an unsupported tool" ) {
++
++ print STDERR <<EOF;
++convert4r4 on Debian GNU/Linux deprecated
++
++This tool is unsupported by upstream and discouraged by the Debian Exim 4
++maintainers. It has multiple known bugs, and you need to manually
++review its output after using it anyway. Please seriously consider complete
++manual regeneration of the Exim 4 configuration, preferably by using the new
++Debconf interface to Exim 4.
++
++If you decide to ignore this advice and to use this script anyway,
++setting the environment variable CONVERT4R4 to the value
++\"I understand this is an unsupported tool\"
++will allow you to run the script. If you find bugs, you get to keep
++the pieces. Please do not file bugs against this script in the Debian
++BTS without providing a patch fixing the bugs, and please do not
++expect the upstream exim-users mailing list to answer questions.
++
++Kind regards
++the Debian Exim4 Maintainers
++EOF
++
++ exit 1;
++}
++
+ $transport_start = $director_start = $router_start = $retry_start
+ = $rewrite_start = $auth_start = 999999;
+
diff --git a/debian/patches/67_unnecessaryCopt.diff b/debian/patches/67_unnecessaryCopt.diff
new file mode 100644
index 0000000..daae1e4
--- /dev/null
+++ b/debian/patches/67_unnecessaryCopt.diff
@@ -0,0 +1,69 @@
+Description: Stop using exim's -C option in utility scripts (exiwhat
+ et al.) since this breaks with ALT_CONFIG_PREFIX.
+Author: Andreas Metzler <ametzler@bebt.de>
+Forwarded: http://bugs.exim.org/show_bug.cgi?id=1045
+Last-Update: 2018-12-31
+
+--- a/src/exicyclog.src
++++ b/src/exicyclog.src
+@@ -151,10 +151,10 @@ st=' '
+ exim_path=`grep "^[$st]*exim_path" $config | sed "s/.*=[$st]*//"`
+ if test "$exim_path" = ""; then exim_path=BIN_DIRECTORY/exim4; fi
+
+-spool_directory=`$exim_path -C $config -bP spool_directory | sed 's/.*=[ ]*//'`
++spool_directory=`$exim_path -bP spool_directory | sed 's/.*=[ ]*//'`
+
+ if [ "$log_file_path" = "" ] ; then
+- log_file_path=`$exim_path -C $config -bP log_file_path | sed 's/.*=[ ]*//'`
++ log_file_path=`$exim_path -bP log_file_path | sed 's/.*=[ ]*//'`
+ fi
+
+ # If log_file_path contains only "syslog" then no Exim log files are in use.
+--- a/src/eximon.src
++++ b/src/eximon.src
+@@ -81,8 +81,8 @@ st=' '
+ EXIM_PATH=`grep "^[$st]*exim_path" $config | sed "s/.*=[$st]*//"`
+ if test "$EXIM_PATH" = ""; then EXIM_PATH=BIN_DIRECTORY/exim4; fi
+
+-SPOOL_DIRECTORY=`$EXIM_PATH -C $config -bP spool_directory | sed 's/.*=[ ]*//'`
+-LOG_FILE_PATH=`$EXIM_PATH -C $config -bP log_file_path | sed 's/.*=[ ]*//'`
++SPOOL_DIRECTORY=`$EXIM_PATH -bP spool_directory | sed 's/.*=[ ]*//'`
++LOG_FILE_PATH=`$EXIM_PATH -bP log_file_path | sed 's/.*=[ ]*//'`
+
+ # If log_file_path is "syslog" then logging is only to syslog, and the monitor
+ # is unable to display a log tail unless EXIMON_LOG_FILE_PATH is set to tell
+--- a/src/exinext.src
++++ b/src/exinext.src
+@@ -98,8 +98,8 @@ if [ "$exim_path" = "" ]; then
+ fi
+
+ if test "$exim_path" = ""; then exim_path=BIN_DIRECTORY/exim4; fi
+-spool_directory=`$exim_path $eximmacdef -C $config -bP spool_directory | sed 's/.*=[ ]*//'`
+-qualify_domain=`$exim_path $eximmacdef -C $config -bP qualify_domain | sed 's/.*=[ ]*//'`
++spool_directory=`$exim_path $eximmacdef -bP spool_directory | sed 's/.*=[ ]*//'`
++qualify_domain=`$exim_path $eximmacdef -bP qualify_domain | sed 's/.*=[ ]*//'`
+
+ # Now do the job. Perl uses $ so frequently that we don't want to have to
+ # escape them all from the shell, so pass in shell variable values as
+@@ -144,7 +144,7 @@ perl - $exim_path "$eximmacdef" $argone
+ # Run Exim to get a list of hosts for the given domain; for
+ # each one construct the appropriate retry key.
+
+- open(LIST, "$exim -C $config -v -bt $address |") ||
++ open(LIST, "$exim -v -bt $address |") ||
+ die "can't run exim to route $address";
+
+ while (<LIST>)
+--- a/src/exiwhat.src
++++ b/src/exiwhat.src
+@@ -99,8 +99,8 @@ fi
+ st=' '
+ exim_path=`grep "^[$st]*exim_path" $config | sed "s/.*=[$st]*//"`
+ if test "$exim_path" = ""; then exim_path=BIN_DIRECTORY/exim4; fi
+-spool_directory=`$exim_path -C $config -bP spool_directory | sed "s/.*=[ ]*//"`
+-process_log_path=`$exim_path -C $config -bP process_log_path | sed "s/.*=[ ]*//"`
++spool_directory=`$exim_path -bP spool_directory | sed "s/.*=[ ]*//"`
++process_log_path=`$exim_path -bP process_log_path | sed "s/.*=[ ]*//"`
+
+ # The file that Exim writes when sent the SIGUSR1 signal is specified by
+ # the process_log_path option. If that is not defined, Exim uses the file
diff --git a/debian/patches/70_remove_exim-users_references.dpatch b/debian/patches/70_remove_exim-users_references.dpatch
new file mode 100755
index 0000000..f3dda95
--- /dev/null
+++ b/debian/patches/70_remove_exim-users_references.dpatch
@@ -0,0 +1,38 @@
+Description: Point Debian users to Debian specific ML.
+Author: Marc Haber <mh+debian-packages@zugschlus.de>
+Last-Update: 2018-12-31
+Forwarded: not-needed
+
+--- a/README
++++ b/README
+@@ -14,8 +14,16 @@ from Exim 3, though the basic structure
+ older book may be helpful for the background, but a lot of the detail has
+ changed, so it is likely to be confusing to newcomers.
+
+-There is a website at https://www.exim.org; this contains details of the
+-mailing list exim-users@exim.org.
++Information about the way Debian has built the binary packages is
++obtainable in /usr/share/doc/exim4-base/README.Debian.gz, and there
++is a Debian-centered mailing list,
++pkg-exim4-users@lists.alioth.debian.org. Please ask Debian-specific
++questions there, and only write to the upstream exim-users mailing
++list if you are sure that your question is not Debian-specific. You
++can find the subscription web page on
++http://lists.alioth.debian.org/mailman/listinfo/pkg-exim4-users
++
++There is a website at https://www.exim.org/.
+
+ A copy of the Exim FAQ should be available from the same source that you used
+ to obtain the Exim distribution. Additional formats for the documentation
+--- a/src/eximstats.src
++++ b/src/eximstats.src
+@@ -537,8 +537,7 @@ about how to create charts from the tabl
+
+ =head1 AUTHOR
+
+-There is a website at https://www.exim.org - this contains details of the
+-mailing list exim-users@exim.org.
++There is a website at https://www.exim.org/.
+
+ =head1 TO DO
+
diff --git a/debian/patches/75_01-Fix-exit-on-attempt-to-rewrite-a-malformed-address.-.patch b/debian/patches/75_01-Fix-exit-on-attempt-to-rewrite-a-malformed-address.-.patch
new file mode 100644
index 0000000..bf0f649
--- /dev/null
+++ b/debian/patches/75_01-Fix-exit-on-attempt-to-rewrite-a-malformed-address.-.patch
@@ -0,0 +1,57 @@
+From e7ec503729970a03d4509921342bc81313976126 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Tue, 12 Jul 2022 22:14:04 +0100
+Subject: [PATCH] Fix exit on attempt to rewrite a malformed address. Bug 2903
+
+---
+ doc/ChangeLog | 5 +
+ src/rewrite.c | 9 +-
+ test/confs/0471 | 7 +
+ test/log/0471 | 5 +
+ test/scripts/0000-Basic/0471 | 4 +-
+ test/stderr/0471 | 245 ++++++++++++++++++++++++++++++++++-
+ 6 files changed, 267 insertions(+), 8 deletions(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -1,9 +1,14 @@
+ This document describes *changes* to previous versions, that might
+ affect Exim's operation, with an unchanged configuration file. For new
+ options, and new features, see the NewStuff file next to this ChangeLog.
+
++JH/04 Bug 2903: avoid exit on an attempt to rewrite a malformed address.
++ Make the rewrite never match and keep the logging. Trust the
++ admin to be using verify=header-syntax (to actually reject the message).
++
++
+ Exim version 4.96
+ -----------------
+
+ JH/01 Move the wait-for-next-tick (needed for unique messmage IDs) from
+ after reception to before a subsequent reception. This should
+--- a/src/rewrite.c
++++ b/src/rewrite.c
+@@ -493,19 +493,18 @@
+ empty address, overlong addres. Sometimes the result matters, sometimes not.
+ It seems this function is called for *any* header we see. */
+
+ if (!recipient)
+ {
+- /* Handle unparesable addresses in the header. Slightly ugly because a
++ /* Log unparesable addresses in the header. Slightly ugly because a
+ null output from the extract can also result from a header without an
+- address, "To: undisclosed recpients:;" being the classic case. */
++ address, "To: undisclosed recpients:;" being the classic case. Ignore
++ this one and carry on. */
+
+ if ((rewrite_rules || routed_old) && Ustrcmp(errmess, "empty address") != 0)
+- {
+ log_write(0, LOG_MAIN, "rewrite: %s", errmess);
+- exim_exit(EXIT_FAILURE);
+- }
++
+ loop_reset_point = store_reset(loop_reset_point);
+ continue;
+ }
+
+ /* If routed_old is not NULL, this is a rewrite caused by a router,
diff --git a/debian/patches/75_05-SPF-fix-memory-accounting-for-error-case.patch b/debian/patches/75_05-SPF-fix-memory-accounting-for-error-case.patch
new file mode 100644
index 0000000..e474acf
--- /dev/null
+++ b/debian/patches/75_05-SPF-fix-memory-accounting-for-error-case.patch
@@ -0,0 +1,25 @@
+From 93c722ce0549360af68269f088f4e59ed8fc130e Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Sun, 7 Aug 2022 17:00:27 +0100
+Subject: [PATCH] SPF: fix memory accounting for error case
+
+---
+ src/spf.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/src/spf.c b/src/spf.c
+index db6eea3a8..a8c0f75c4 100644
+--- a/src/spf.c
++++ b/src/spf.c
+@@ -204,7 +204,7 @@ spf_nxdomain = SPF_dns_rr_new_init(spf_dns_server,
+ "", ns_t_any, 24 * 60 * 60, HOST_NOT_FOUND);
+ if (!spf_nxdomain)
+ {
+- free(spf_dns_server);
++ store_free(spf_dns_server);
+ return NULL;
+ }
+
+--
+2.35.1
+
diff --git a/debian/patches/75_08-Fix-regex-n-use-after-free.-Bug-2915.patch b/debian/patches/75_08-Fix-regex-n-use-after-free.-Bug-2915.patch
new file mode 100644
index 0000000..be6ccae
--- /dev/null
+++ b/debian/patches/75_08-Fix-regex-n-use-after-free.-Bug-2915.patch
@@ -0,0 +1,196 @@
+From 4e9ed49f8f12eb331b29bd5b6dc3693c520fddc2 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Wed, 31 Aug 2022 15:37:40 +0100
+Subject: [PATCH] Fix $regex<n> use-after-free. Bug 2915
+
+---
+ doc/ChangeLog | 8 +++++++-
+ src/exim.c | 4 +---
+ src/expand.c | 2 +-
+ src/functions.h | 1 +
+ src/globals.c | 2 +-
+ src/regex.c | 29 ++++++++++++++++++-----------
+ src/smtp_in.c | 2 ++
+ test/confs/4002 | 10 ++++++++++
+ test/mail/4002.userx | 7 +++++++
+ test/scripts/4000-scanning/4002 | 7 +++++++
+ 10 files changed, 55 insertions(+), 17 deletions(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -4,15 +4,21 @@
+
+ JH/04 Bug 2903: avoid exit on an attempt to rewrite a malformed address.
+ Make the rewrite never match and keep the logging. Trust the
+ admin to be using verify=header-syntax (to actually reject the message).
+
++JH/08 Bug 2915: Fix use-after-free for $regex<n> variables. Previously when
++ more than one message arrived in a single connection a reference from
++ the earlier message could be re-used. Often a sigsegv resulted.
++ These variables were introduced in Exim 4.87.
++ Debug help from Graeme Fowler.
++
+
+ Exim version 4.96
+ -----------------
+
+-JH/01 Move the wait-for-next-tick (needed for unique messmage IDs) from
++JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
+ after reception to before a subsequent reception. This should
+ mean slightly faster delivery, and also confirmation of reception
+ to senders.
+
+ JH/02 Move from using the pcre library to pcre2. The former is no longer
+--- a/src/exim.c
++++ b/src/exim.c
+@@ -1999,12 +1999,10 @@
+
+ regex_whitelisted_macro =
+ regex_must_compile(US"^[A-Za-z0-9_/.-]*$", FALSE, TRUE);
+ #endif
+
+-for (i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL;
+-
+ /* If the program is called as "mailq" treat it as equivalent to "exim -bp";
+ this seems to be a generally accepted convention, since one finds symbolic
+ links called "mailq" in standard OS configurations. */
+
+ if ((namelen == 5 && Ustrcmp(argv[0], "mailq") == 0) ||
+@@ -6082,11 +6080,11 @@
+ callout_address = NULL;
+ sending_ip_address = NULL;
+ deliver_localpart_data = deliver_domain_data =
+ recipient_data = sender_data = NULL;
+ acl_var_m = NULL;
+- for(int i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL;
++ regex_vars_clear();
+
+ store_reset(reset_point);
+ }
+
+ exim_exit(EXIT_SUCCESS); /* Never returns */
+--- a/src/expand.c
++++ b/src/expand.c
+@@ -1871,11 +1871,11 @@
+ {
+ tree_node * node = tree_search(router_var, name + 2);
+ return node ? node->data.ptr : strict_acl_vars ? NULL : US"";
+ }
+
+-/* Handle $auth<n> variables. */
++/* Handle $auth<n>, $regex<n> variables. */
+
+ if (Ustrncmp(name, "auth", 4) == 0)
+ {
+ uschar *endptr;
+ int n = Ustrtoul(name + 4, &endptr, 10);
+--- a/src/functions.h
++++ b/src/functions.h
+@@ -436,10 +436,11 @@
+ extern int regex(const uschar **);
+ #endif
+ extern BOOL regex_match(const pcre2_code *, const uschar *, int, uschar **);
+ extern BOOL regex_match_and_setup(const pcre2_code *, const uschar *, int, int);
+ extern const pcre2_code *regex_must_compile(const uschar *, BOOL, BOOL);
++extern void regex_vars_clear(void);
+ extern void retry_add_item(address_item *, uschar *, int);
+ extern BOOL retry_check_address(const uschar *, host_item *, uschar *, BOOL,
+ uschar **, uschar **);
+ extern retry_config *retry_find_config(const uschar *, const uschar *, int, int);
+ extern BOOL retry_ultimate_address_timeout(uschar *, const uschar *,
+--- a/src/globals.c
++++ b/src/globals.c
+@@ -1313,11 +1313,11 @@
+ #ifndef DISABLE_PIPE_CONNECT
+ const pcre2_code *regex_EARLY_PIPE = NULL;
+ #endif
+ const pcre2_code *regex_ismsgid = NULL;
+ const pcre2_code *regex_smtp_code = NULL;
+-const uschar *regex_vars[REGEX_VARS];
++const uschar *regex_vars[REGEX_VARS] = { 0 };;
+ #ifdef WHITELIST_D_MACROS
+ const pcre2_code *regex_whitelisted_macro = NULL;
+ #endif
+ #ifdef WITH_CONTENT_SCAN
+ uschar *regex_match_string = NULL;
+--- a/src/regex.c
++++ b/src/regex.c
+@@ -94,22 +94,32 @@
+ }
+ pcre2_match_data_free(md);
+ return FAIL;
+ }
+
++
++/* reset expansion variables */
++void
++regex_vars_clear(void)
++{
++regex_match_string = NULL;
++for (int i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL;
++}
++
++
++
+ int
+-regex(const uschar **listptr)
++regex(const uschar ** listptr)
+ {
+ unsigned long mbox_size;
+-FILE *mbox_file;
+-pcre_list *re_list_head;
+-uschar *linebuffer;
++FILE * mbox_file;
++pcre_list * re_list_head;
++uschar * linebuffer;
+ long f_pos = 0;
+ int ret = FAIL;
+
+-/* reset expansion variable */
+-regex_match_string = NULL;
++regex_vars_clear();
+
+ if (!mime_stream) /* We are in the DATA ACL */
+ {
+ if (!(mbox_file = spool_mbox(&mbox_size, NULL, NULL)))
+ { /* error while spooling */
+@@ -167,18 +177,17 @@
+
+
+ int
+ mime_regex(const uschar **listptr)
+ {
+-pcre_list *re_list_head = NULL;
+-FILE *f;
+-uschar *mime_subject = NULL;
++pcre_list * re_list_head = NULL;
++FILE * f;
++uschar * mime_subject = NULL;
+ int mime_subject_len = 0;
+ int ret;
+
+-/* reset expansion variable */
+-regex_match_string = NULL;
++regex_vars_clear();
+
+ /* precompile our regexes */
+ if (!(re_list_head = compile(*listptr)))
+ return FAIL; /* no regexes -> nothing to do */
+
+--- a/src/smtp_in.c
++++ b/src/smtp_in.c
+@@ -2155,12 +2155,14 @@
+ prdr_requested = FALSE;
+ #endif
+ #ifdef SUPPORT_I18N
+ message_smtputf8 = FALSE;
+ #endif
++regex_vars_clear();
+ body_linecount = body_zerocount = 0;
+
++lookup_value = NULL; /* Can be set by ACL */
+ sender_rate = sender_rate_limit = sender_rate_period = NULL;
+ ratelimiters_mail = NULL; /* Updated by ratelimit ACL condition */
+ /* Note that ratelimiters_conn persists across resets. */
+
+ /* Reset message ACL variables */
diff --git a/debian/patches/75_09-Fix-non-WITH_CONTENT_SCAN-build.patch b/debian/patches/75_09-Fix-non-WITH_CONTENT_SCAN-build.patch
new file mode 100644
index 0000000..6071fa7
--- /dev/null
+++ b/debian/patches/75_09-Fix-non-WITH_CONTENT_SCAN-build.patch
@@ -0,0 +1,58 @@
+From d8ecc7bf97934a1e2244788c610c958cacd740bd Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Wed, 31 Aug 2022 17:03:37 +0100
+Subject: [PATCH 1/3] Fix non-WITH_CONTENT_SCAN build.
+
+Broken-by: 4e9ed49f8f
+---
+ src/exim.c | 11 +++++++++++
+ src/regex.c | 10 ----------
+ 2 files changed, 11 insertions(+), 10 deletions(-)
+
+--- a/src/exim.c
++++ b/src/exim.c
+@@ -1677,10 +1677,21 @@
+ if ((s = expand_string(big_buffer))) printf("%s\n", CS s);
+ else printf("Failed: %s\n", expand_string_message);
+ }
+
+
++/* reset regex expansion variables */
++void
++regex_vars_clear(void)
++{
++regex_match_string = NULL;
++for (int i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL;
++}
++
++
++
++
+
+ /*************************************************
+ * Entry point and high-level code *
+ *************************************************/
+
+--- a/src/regex.c
++++ b/src/regex.c
+@@ -95,20 +95,10 @@
+ pcre2_match_data_free(md);
+ return FAIL;
+ }
+
+
+-/* reset expansion variables */
+-void
+-regex_vars_clear(void)
+-{
+-regex_match_string = NULL;
+-for (int i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL;
+-}
+-
+-
+-
+ int
+ regex(const uschar ** listptr)
+ {
+ unsigned long mbox_size;
+ FILE * mbox_file;
diff --git a/debian/patches/75_10-Fix-non-WITH_CONTENT_SCAN-build-2.patch b/debian/patches/75_10-Fix-non-WITH_CONTENT_SCAN-build-2.patch
new file mode 100644
index 0000000..0a8ed51
--- /dev/null
+++ b/debian/patches/75_10-Fix-non-WITH_CONTENT_SCAN-build-2.patch
@@ -0,0 +1,135 @@
+From 158dff9936e36a2d31d037d3988b9353458d6471 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Wed, 31 Aug 2022 17:17:59 +0100
+Subject: [PATCH 2/3] Fix non-WITH_CONTENT_SCAN build (2)
+
+Broken-by: d8ecc7bf97
+---
+ src/exim.c | 13 +------------
+ src/functions.h | 2 +-
+ src/globals.h | 2 +-
+ src/regex.c | 10 ++++++++++
+ src/smtp_in.c | 2 ++
+ 5 files changed, 15 insertions(+), 14 deletions(-)
+
+--- a/src/exim.c
++++ b/src/exim.c
+@@ -1677,21 +1677,10 @@
+ if ((s = expand_string(big_buffer))) printf("%s\n", CS s);
+ else printf("Failed: %s\n", expand_string_message);
+ }
+
+
+-/* reset regex expansion variables */
+-void
+-regex_vars_clear(void)
+-{
+-regex_match_string = NULL;
+-for (int i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL;
+-}
+-
+-
+-
+-
+
+ /*************************************************
+ * Entry point and high-level code *
+ *************************************************/
+
+@@ -6085,17 +6074,17 @@
+ deliver_domain_orig = NULL;
+ deliver_host = deliver_host_address = NULL;
+ dnslist_domain = dnslist_matched = NULL;
+ #ifdef WITH_CONTENT_SCAN
+ malware_name = NULL;
++ regex_vars_clear();
+ #endif
+ callout_address = NULL;
+ sending_ip_address = NULL;
+ deliver_localpart_data = deliver_domain_data =
+ recipient_data = sender_data = NULL;
+ acl_var_m = NULL;
+- regex_vars_clear();
+
+ store_reset(reset_point);
+ }
+
+ exim_exit(EXIT_SUCCESS); /* Never returns */
+--- a/src/functions.h
++++ b/src/functions.h
+@@ -432,15 +432,15 @@
+ extern BOOL receive_msg(BOOL);
+ extern int_eximarith_t receive_statvfs(BOOL, int *);
+ extern void receive_swallow_smtp(void);
+ #ifdef WITH_CONTENT_SCAN
+ extern int regex(const uschar **);
++extern void regex_vars_clear(void);
+ #endif
+ extern BOOL regex_match(const pcre2_code *, const uschar *, int, uschar **);
+ extern BOOL regex_match_and_setup(const pcre2_code *, const uschar *, int, int);
+ extern const pcre2_code *regex_must_compile(const uschar *, BOOL, BOOL);
+-extern void regex_vars_clear(void);
+ extern void retry_add_item(address_item *, uschar *, int);
+ extern BOOL retry_check_address(const uschar *, host_item *, uschar *, BOOL,
+ uschar **, uschar **);
+ extern retry_config *retry_find_config(const uschar *, const uschar *, int, int);
+ extern BOOL retry_ultimate_address_timeout(uschar *, const uschar *,
+--- a/src/globals.h
++++ b/src/globals.h
+@@ -895,16 +895,16 @@
+ #ifndef DISABLE_PIPE_CONNECT
+ extern const pcre2_code *regex_EARLY_PIPE; /* For recognizing PIPE_CONNCT */
+ #endif
+ extern const pcre2_code *regex_ismsgid; /* Compiled r.e. for message ID */
+ extern const pcre2_code *regex_smtp_code; /* For recognizing SMTP codes */
+-extern const uschar *regex_vars[]; /* $regexN variables */
+ #ifdef WHITELIST_D_MACROS
+ extern const pcre2_code *regex_whitelisted_macro; /* For -D macro values */
+ #endif
+ #ifdef WITH_CONTENT_SCAN
+ extern uschar *regex_match_string; /* regex that matched a line (regex ACL condition) */
++extern const uschar *regex_vars[];
+ #endif
+ extern int remote_delivery_count; /* Number of remote addresses */
+ extern int remote_max_parallel; /* Maximum parallel delivery */
+ extern uschar *remote_sort_domains; /* Remote domain sorting order */
+ extern retry_config *retries; /* Chain of retry config information */
+--- a/src/regex.c
++++ b/src/regex.c
+@@ -95,10 +95,20 @@
+ pcre2_match_data_free(md);
+ return FAIL;
+ }
+
+
++/* reset expansion variables */
++void
++regex_vars_clear(void)
++{
++regex_match_string = NULL;
++for (int i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL;
++}
++
++
++
+ int
+ regex(const uschar ** listptr)
+ {
+ unsigned long mbox_size;
+ FILE * mbox_file;
+--- a/src/smtp_in.c
++++ b/src/smtp_in.c
+@@ -2155,11 +2155,13 @@
+ prdr_requested = FALSE;
+ #endif
+ #ifdef SUPPORT_I18N
+ message_smtputf8 = FALSE;
+ #endif
++#ifdef WITH_CONTENT_SCAN
+ regex_vars_clear();
++#endif
+ body_linecount = body_zerocount = 0;
+
+ lookup_value = NULL; /* Can be set by ACL */
+ sender_rate = sender_rate_limit = sender_rate_period = NULL;
+ ratelimiters_mail = NULL; /* Updated by ratelimit ACL condition */
diff --git a/debian/patches/75_11-Fix-non-WITH_CONTENT_SCAN-build-3.patch b/debian/patches/75_11-Fix-non-WITH_CONTENT_SCAN-build-3.patch
new file mode 100644
index 0000000..b06d896
--- /dev/null
+++ b/debian/patches/75_11-Fix-non-WITH_CONTENT_SCAN-build-3.patch
@@ -0,0 +1,45 @@
+From 32da6327e434e986a18b75a84f2d8c687ba14619 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Thu, 1 Sep 2022 15:54:35 +0100
+Subject: [PATCH 3/3] Fix non-WITH_CONTENT_SCAN build (3)
+
+Broken-by: d8ecc7bf97
+---
+ src/expand.c | 4 ++++
+ 1 file changed, 4 insertions(+)
+
+diff --git a/src/expand.c b/src/expand.c
+index 89de56255..831ca2b75 100644
+--- a/src/expand.c
++++ b/src/expand.c
+@@ -1869,6 +1869,7 @@ if (Ustrncmp(name, "auth", 4) == 0)
+ if (!*endptr && n != 0 && n <= AUTH_VARS)
+ return auth_vars[n-1] ? auth_vars[n-1] : US"";
+ }
++#ifdef WITH_CONTENT_SCAN
+ else if (Ustrncmp(name, "regex", 5) == 0)
+ {
+ uschar *endptr;
+@@ -1876,6 +1877,7 @@ else if (Ustrncmp(name, "regex", 5) == 0)
+ if (!*endptr && n != 0 && n <= REGEX_VARS)
+ return regex_vars[n-1] ? regex_vars[n-1] : US"";
+ }
++#endif
+
+ /* For all other variables, search the table */
+
+@@ -8715,9 +8717,11 @@ assert_variable_notin() treats as const, so deconst is safe. */
+ for (int i = 0; i < AUTH_VARS; i++) if (auth_vars[i])
+ assert_variable_notin(US"auth<n>", US auth_vars[i], &e);
+
++#ifdef WITH_CONTENT_SCAN
+ /* check regex<n> variables. assert_variable_notin() treats as const. */
+ for (int i = 0; i < REGEX_VARS; i++) if (regex_vars[i])
+ assert_variable_notin(US"regex<n>", US regex_vars[i], &e);
++#endif
+
+ /* check known-name variables */
+ for (var_entry * v = var_table; v < var_table + var_table_size; v++)
+--
+2.35.1
+
diff --git a/debian/patches/75_16-GnuTLS-fix-for-clients-offering-no-TLS-extensions.patch b/debian/patches/75_16-GnuTLS-fix-for-clients-offering-no-TLS-extensions.patch
new file mode 100644
index 0000000..ae2fa16
--- /dev/null
+++ b/debian/patches/75_16-GnuTLS-fix-for-clients-offering-no-TLS-extensions.patch
@@ -0,0 +1,114 @@
+From ece23f05d6a430a461a75639197271c23f6858ec Mon Sep 17 00:00:00 2001
+From: Jasen Betts <jasen@xnet.co.nz>
+Date: Fri, 30 Sep 2022 13:49:41 +0100
+Subject: [PATCH] GnuTLS: fix for clients offering no TLS extensions
+
+---
+ doc/ChangeLog | 3 +++
+ src/tls-gnu.c | 3 ++-
+ src/tls-openssl.c | 39 +++++++++++++++---------------
+ test/confs/2091 | 1 +
+ test/log/2091 | 3 +++
+ test/scripts/2090-GnuTLS-ALPN/2091 | 19 +++++++++++++++
+ test/stdout/2091 | 21 ++++++++++++++++
+ 7 files changed, 68 insertions(+), 21 deletions(-)
+ create mode 120000 test/confs/2091
+ create mode 100644 test/log/2091
+ create mode 100644 test/scripts/2090-GnuTLS-ALPN/2091
+ create mode 100644 test/stdout/2091
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -10,10 +10,14 @@
+ more than one message arrived in a single connection a reference from
+ the earlier message could be re-used. Often a sigsegv resulted.
+ These variables were introduced in Exim 4.87.
+ Debug help from Graeme Fowler.
+
++JH/10 GnuTLS: fix for (IOT?) clients offering no TLS extensions at all.
++ Find and fix by Jasen Betts.
++
++
+
+ 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
+@@ -1130,12 +1130,13 @@
+ static int
+ tls_server_clienthello_cb(gnutls_session_t session, unsigned int htype,
+ unsigned when, unsigned int incoming, const gnutls_datum_t * msg)
+ {
+ /* Call fn for each extension seen. 3.6.3 onwards */
+-return gnutls_ext_raw_parse(NULL, tls_server_clienthello_ext, msg,
++int rc = gnutls_ext_raw_parse(NULL, tls_server_clienthello_ext, msg,
+ GNUTLS_EXT_RAW_FLAG_TLS_CLIENT_HELLO);
++return rc == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE ? 0 : rc;
+ }
+
+
+ # ifdef notdef_crashes
+ /* Make a note that we saw a status-response */
+--- a/src/tls-openssl.c
++++ b/src/tls-openssl.c
+@@ -940,40 +940,39 @@
+
+ Returns: nothing
+ */
+
+ static void
+-info_callback(SSL *s, int where, int ret)
++info_callback(SSL * s, int where, int ret)
+ {
+ DEBUG(D_tls)
+ {
+- const uschar * str;
++ gstring * g = NULL;
+
+- if (where & SSL_ST_CONNECT)
+- str = US"SSL_connect";
+- else if (where & SSL_ST_ACCEPT)
+- str = US"SSL_accept";
+- else
+- str = US"SSL info (undefined)";
++ if (where & SSL_ST_CONNECT) g = string_append_listele(g, ',', US"SSL_connect");
++ if (where & SSL_ST_ACCEPT) g = string_append_listele(g, ',', US"SSL_accept");
++ if (where & SSL_CB_LOOP) g = string_append_listele(g, ',', US"state_chg");
++ if (where & SSL_CB_EXIT) g = string_append_listele(g, ',', US"hshake_exit");
++ if (where & SSL_CB_READ) g = string_append_listele(g, ',', US"read");
++ if (where & SSL_CB_WRITE) g = string_append_listele(g, ',', US"write");
++ if (where & SSL_CB_ALERT) g = string_append_listele(g, ',', US"alert");
++ if (where & SSL_CB_HANDSHAKE_START) g = string_append_listele(g, ',', US"hshake_start");
++ if (where & SSL_CB_HANDSHAKE_DONE) g = string_append_listele(g, ',', US"hshake_done");
+
+ if (where & SSL_CB_LOOP)
+- debug_printf("%s: %s\n", str, SSL_state_string_long(s));
++ debug_printf("SSL %s: %s\n", g->s, SSL_state_string_long(s));
+ else if (where & SSL_CB_ALERT)
+- debug_printf("SSL3 alert %s:%s:%s\n",
+- str = where & SSL_CB_READ ? US"read" : US"write",
++ debug_printf("SSL %s %s:%s\n", g->s,
+ SSL_alert_type_string_long(ret), SSL_alert_desc_string_long(ret));
+ else if (where & SSL_CB_EXIT)
+ {
+- if (ret == 0)
+- debug_printf("%s: failed in %s\n", str, SSL_state_string_long(s));
+- else if (ret < 0)
+- debug_printf("%s: error in %s\n", str, SSL_state_string_long(s));
++ if (ret <= 0)
++ debug_printf("SSL %s: %s in %s\n", g->s,
++ ret == 0 ? "failed" : "error", SSL_state_string_long(s));
+ }
+- else if (where & SSL_CB_HANDSHAKE_START)
+- debug_printf("%s: hshake start: %s\n", str, SSL_state_string_long(s));
+- else if (where & SSL_CB_HANDSHAKE_DONE)
+- debug_printf("%s: hshake done: %s\n", str, SSL_state_string_long(s));
++ else if (where & (SSL_CB_HANDSHAKE_START | SSL_CB_HANDSHAKE_DONE))
++ debug_printf("SSL %s: %s\n", g->s, SSL_state_string_long(s));
+ }
+ }
+
+ #ifdef OPENSSL_HAVE_KEYLOG_CB
+ static void
diff --git a/debian/patches/75_18-Fix-Build-with-libopendmarc-1.4.x-fixes-2728.patch b/debian/patches/75_18-Fix-Build-with-libopendmarc-1.4.x-fixes-2728.patch
new file mode 100644
index 0000000..f261d62
--- /dev/null
+++ b/debian/patches/75_18-Fix-Build-with-libopendmarc-1.4.x-fixes-2728.patch
@@ -0,0 +1,88 @@
+From 1561c5d88b3a23a4348d8e3c1ce28554fcbcfe46 Mon Sep 17 00:00:00 2001
+From: "Heiko Schlittermann (HS12-RIPE)" <hs@schlittermann.de>
+Date: Sat, 15 Oct 2022 19:30:58 +0200
+Subject: [PATCH 1/2] Fix: Build with libopendmarc 1.4.x (fixes 2728)
+
+---
+ doc/ChangeLog | 3 +++
+ src/EDITME | 7 +++++--
+ src/config.h.defaults | 1 +
+ src/dmarc.c | 7 ++++++-
+ 4 files changed, 15 insertions(+), 3 deletions(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -13,10 +13,13 @@
+ Debug help from Graeme Fowler.
+
+ JH/10 GnuTLS: fix for (IOT?) clients offering no TLS extensions at all.
+ Find and fix by Jasen Betts.
+
++HS/01 Bug 2728: Introduce EDITME option "DMARC_API" to work around incompatible
++ API changes in libopendmarc.
++
+
+
+ Exim version 4.96
+ -----------------
+
+--- a/src/EDITME
++++ b/src/EDITME
+@@ -600,18 +600,21 @@
+
+ # EXPERIMENTAL_DCC=yes
+
+ # Uncomment the following line to add DMARC checking capability, implemented
+ # using libopendmarc libraries. You must have SPF and DKIM support enabled also.
+-# Library version libopendmarc-1.4.1-1.fc33.x86_64 (on Fedora 33) is known broken;
+-# 1.3.2-3 works. I seems that the OpenDMARC project broke their API.
+ # SUPPORT_DMARC=yes
+ # CFLAGS += -I/usr/local/include
+ # LDFLAGS += -lopendmarc
+ # Uncomment the following if you need to change the default. You can
+ # override it at runtime (main config option dmarc_tld_file)
+ # DMARC_TLD_FILE=/etc/exim/opendmarc.tlds
++#
++# Library version libopendmarc-1.4.1-1.fc33.x86_64 (on Fedora 33) is known broken;
++# 1.3.2-3 works. It seems that the OpenDMARC project broke their API.
++# Use this option if you need to build with an old library (1.3.x)
++# DMARC_API=100300
+
+ # Uncomment the following line to add ARC (Authenticated Received Chain)
+ # support. You must have SPF and DKIM support enabled also.
+ # EXPERIMENTAL_ARC=yes
+
+--- a/src/config.h.defaults
++++ b/src/config.h.defaults
+@@ -148,10 +148,11 @@
+ #define STRING_SPRINTF_BUFFER_SIZE (8192 * 4)
+
+ #define SUPPORT_CRYPTEQ
+ #define SUPPORT_DANE
+ #define SUPPORT_DMARC
++#define DMARC_API 100400
+ #define DMARC_TLD_FILE "/etc/exim/opendmarc.tlds"
+ #define SUPPORT_I18N
+ #define SUPPORT_I18N_2008
+ #define SUPPORT_MAILDIR
+ #define SUPPORT_MAILSTORE
+--- a/src/dmarc.c
++++ b/src/dmarc.c
+@@ -457,11 +457,16 @@
+ dkim_result = vs == PDKIM_VERIFY_PASS ? DMARC_POLICY_DKIM_OUTCOME_PASS :
+ vs == PDKIM_VERIFY_FAIL ? DMARC_POLICY_DKIM_OUTCOME_FAIL :
+ vs == PDKIM_VERIFY_INVALID ? DMARC_POLICY_DKIM_OUTCOME_TMPFAIL :
+ DMARC_POLICY_DKIM_OUTCOME_NONE;
+ libdm_status = opendmarc_policy_store_dkim(dmarc_pctx, US sig->domain,
+- dkim_result, US"");
++/* The opendmarc project broke its API in a way we can't detect * easily.
++ * The EDITME provides a DMARC_API variable */
++#if DMARC_API >= 100400
++ sig->selector,
++#endif
++ dkim_result, US"");
+ DEBUG(D_receive)
+ debug_printf("DMARC adding DKIM sender domain = %s\n", sig->domain);
+ if (libdm_status != DMARC_PARSE_OKAY)
+ log_write(0, LOG_MAIN|LOG_PANIC,
+ "failure to store dkim (%s) for DMARC: %s",
diff --git a/debian/patches/75_19-DMARC-fix-use-after-free-in-dmarc_dns_lookup.patch b/debian/patches/75_19-DMARC-fix-use-after-free-in-dmarc_dns_lookup.patch
new file mode 100644
index 0000000..e8bda9e
--- /dev/null
+++ b/debian/patches/75_19-DMARC-fix-use-after-free-in-dmarc_dns_lookup.patch
@@ -0,0 +1,39 @@
+From 12fb3842f81bcbd4a4519d5728f2d7e0e3ca1445 Mon Sep 17 00:00:00 2001
+From: Lorenz Brun <lorenz@brun.one>
+Date: Fri, 14 Oct 2022 21:02:51 +0200
+Subject: [PATCH 2/2] DMARC: fix use-after-free in dmarc_dns_lookup
+
+This fixes a use-after-free in dmarc_dns_lookup where the result
+of dns_lookup in dnsa is freed before the required data is copied out.
+
+Fixes: 9258363 ("DNS: explicit alloc/free of workspace")
+---
+ src/dmarc.c | 3 ++-
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+diff --git a/src/dmarc.c b/src/dmarc.c
+index ad0c26c91..53c2752ac 100644
+--- a/src/dmarc.c
++++ b/src/dmarc.c
+@@ -226,16 +226,17 @@ dns_scan dnss;
+ int rc = dns_lookup(dnsa, string_sprintf("_dmarc.%s", dom), T_TXT, NULL);
+
+ if (rc == DNS_SUCCEED)
+ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
+ rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
+ if (rr->type == T_TXT && rr->size > 3)
+ {
++ uschar *record = string_copyn_taint(US rr->data, rr->size, GET_TAINTED);
+ store_free_dns_answer(dnsa);
+- return string_copyn_taint(US rr->data, rr->size, GET_TAINTED);
++ return record;
+ }
+ store_free_dns_answer(dnsa);
+ return NULL;
+ }
+
+
+ static int
+--
+2.35.1
+
diff --git a/debian/patches/75_22-Fix-daemon-startup.-Bug-2930.patch b/debian/patches/75_22-Fix-daemon-startup.-Bug-2930.patch
new file mode 100644
index 0000000..2a3434f
--- /dev/null
+++ b/debian/patches/75_22-Fix-daemon-startup.-Bug-2930.patch
@@ -0,0 +1,68 @@
+From 221321d2c51b83d1feced80ecd6c2fe33ec5456c Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Thu, 3 Nov 2022 20:08:25 +0000
+Subject: [PATCH 1/2] Fix daemon startup. Bug 2930
+
+Broken-by: 7d5055276a
+---
+ doc/ChangeLog | 4 ++++
+ src/daemon.c | 8 ++++++--
+ 2 files changed, 10 insertions(+), 2 deletions(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -16,10 +16,14 @@
+ Find and fix by Jasen Betts.
+
+ HS/01 Bug 2728: Introduce EDITME option "DMARC_API" to work around incompatible
+ API changes in libopendmarc.
+
++JH/12 Bug 2930: Fix daemon startup. When started from any process apart from
++ pid 1, in the normal "background daemon" mode, having to drop process-
++ group leadership also lost track of needing to create listener sockets.
++
+
+
+ Exim version 4.96
+ -----------------
+
+--- a/src/daemon.c
++++ b/src/daemon.c
+@@ -1744,19 +1744,23 @@
+ {
+ /* If the parent process of this one has pid == 1, we are re-initializing the
+ daemon as the result of a SIGHUP. In this case, there is no need to do
+ anything, because the controlling terminal has long gone. Otherwise, fork, in
+ case current process is a process group leader (see 'man setsid' for an
+- explanation) before calling setsid(). */
++ explanation) before calling setsid().
++ All other forks want daemon_listen cleared. Rather than blow a register, jsut
++ restore it here. */
+
+ if (getppid() != 1)
+ {
++ BOOL daemon_listen = f.daemon_listen;
+ pid_t pid = exim_fork(US"daemon");
+ if (pid < 0) log_write(0, LOG_MAIN|LOG_PANIC_DIE,
+ "fork() failed when starting daemon: %s", strerror(errno));
+ if (pid > 0) exit(EXIT_SUCCESS); /* in parent process, just exit */
+ (void)setsid(); /* release controlling terminal */
++ f.daemon_listen = daemon_listen;
+ }
+ }
+
+ /* We are now in the disconnected, daemon process (unless debugging). Set up
+ the listening sockets if required. */
+@@ -2090,11 +2094,11 @@
+ { /* found; append port to list */
+ for (p = i2->log; *p; ) p++; /* end of existing string */
+ if (*--p == '}') *p = '\0'; /* drop EOL */
+ while (isdigit(*--p)) ; /* char before port */
+
+- i2->log = *p == ':' /* no list yet? */
++ i2->log = *p == ':' /* no list yet? { */
+ ? string_sprintf("%.*s{%s,%d}",
+ (int)(p - i2->log + 1), i2->log, p+1, ipa->port)
+ : string_sprintf("%s,%d}", i2->log, ipa->port);
+ ipa->log = NULL;
+ break;
diff --git a/debian/patches/75_23-Fix-reccipients-after-run.-.-Bug-2929.patch b/debian/patches/75_23-Fix-reccipients-after-run.-.-Bug-2929.patch
new file mode 100644
index 0000000..1c98ef2
--- /dev/null
+++ b/debian/patches/75_23-Fix-reccipients-after-run.-.-Bug-2929.patch
@@ -0,0 +1,45 @@
+From 6b331d5834d12bdda21857cd6fffac17038ce3c7 Mon Sep 17 00:00:00 2001
+From: Ruben Jenster <r.jenster@drachenfels.de>
+Date: Thu, 3 Nov 2022 21:38:15 +0000
+Subject: [PATCH 2/2] Fix $reccipients after ${run...}. Bug 2929
+
+Broken-by: cfe6acff2d
+---
+ doc/ChangeLog | 3 +++
+ src/transport.c | 3 ++-
+ 2 files changed, 5 insertions(+), 1 deletion(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -20,10 +20,13 @@
+
+ JH/12 Bug 2930: Fix daemon startup. When started from any process apart from
+ pid 1, in the normal "background daemon" mode, having to drop process-
+ group leadership also lost track of needing to create listener sockets.
+
++JH/13 Bug 2929: Fix using $recipients after ${run...}. A change made for 4.96
++ resulted in the variable appearing empty. Find and fix by Ruben Jenster.
++
+
+
+ Exim version 4.96
+ -----------------
+
+--- a/src/transport.c
++++ b/src/transport.c
+@@ -2342,13 +2342,14 @@
+ /* Handle normal expansion string */
+
+ else
+ {
+ const uschar *expanded_arg;
++ BOOL enable_dollar_recipients_g = f.enable_dollar_recipients;
+ f.enable_dollar_recipients = allow_dollar_recipients;
+ expanded_arg = expand_cstring(argv[i]);
+- f.enable_dollar_recipients = FALSE;
++ f.enable_dollar_recipients = enable_dollar_recipients_g;
+
+ if (!expanded_arg)
+ {
+ uschar *msg = string_sprintf("Expansion of \"%s\" "
+ "from command \"%s\" in %s failed: %s",
diff --git a/debian/patches/75_31-Fix-regext-substring-capture-variables-for-null-matc.patch b/debian/patches/75_31-Fix-regext-substring-capture-variables-for-null-matc.patch
new file mode 100644
index 0000000..bd250f2
--- /dev/null
+++ b/debian/patches/75_31-Fix-regext-substring-capture-variables-for-null-matc.patch
@@ -0,0 +1,79 @@
+From e63825824cc406c160ccbf2b154c5d81b168604a Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Fri, 11 Nov 2022 00:05:59 +0000
+Subject: [PATCH 1/2] Fix regext substring capture variables for null matches.
+ Bug 2933
+
+broken-by: 59d66fdc13f0
+---
+ doc/ChangeLog | 5 +++++
+ src/exim.c | 2 ++
+ src/malware.c | 3 +++
+ src/regex.c | 2 +-
+ 4 files changed, 11 insertions(+), 1 deletion(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -22,10 +22,15 @@
+ pid 1, in the normal "background daemon" mode, having to drop process-
+ group leadership also lost track of needing to create listener sockets.
+
+ JH/13 Bug 2929: Fix using $recipients after ${run...}. A change made for 4.96
+ resulted in the variable appearing empty. Find and fix by Ruben Jenster.
++
++JH/14 Bug 2933: Fix regex substring match variables for null matches. Since 4.96
++ a capture group which obtained no text (eg. "(abc)*" matching zero
++ occurrences) could cause a segfault if the corresponding $<n> was
++ expanded.
+
+
+
+ Exim version 4.96
+ -----------------
+--- a/src/exim.c
++++ b/src/exim.c
+@@ -167,10 +167,12 @@
+ for (int matchnum = setup < 0 ? 0 : 1; matchnum < res; matchnum++)
+ {
+ PCRE2_SIZE len;
+ pcre2_substring_get_bynumber(md, matchnum,
+ (PCRE2_UCHAR **)&expand_nstring[expand_nmax], &len);
++ if (!expand_nstring[expand_nmax])
++ { expand_nstring[expand_nmax] = US""; len = 0; }
+ expand_nlength[expand_nmax++] = (int)len;
+ }
+ expand_nmax--;
+ }
+ else if (res != PCRE2_ERROR_NOMATCH) DEBUG(D_any)
+--- a/src/malware.c
++++ b/src/malware.c
+@@ -323,11 +323,14 @@
+ int i = pcre2_match(cre, text, PCRE2_ZERO_TERMINATED, 0, 0, md, pcre_mtc_ctx);
+ PCRE2_UCHAR * substr = NULL;
+ PCRE2_SIZE slen;
+
+ if (i >= 2) /* Got it */
++ {
+ pcre2_substring_get_bynumber(md, 1, &substr, &slen);
++ if (!substr) substr = US"";
++ }
+ return US substr;
+ }
+
+ static const pcre2_code *
+ m_pcre_nextinlist(const uschar ** list, int * sep,
+--- a/src/regex.c
++++ b/src/regex.c
+@@ -84,11 +84,11 @@
+ for (int nn = 1; nn < n; nn++)
+ {
+ PCRE2_UCHAR * cstr;
+ PCRE2_SIZE cslen;
+ pcre2_substring_get_bynumber(md, nn, &cstr, &cslen);
+- regex_vars[nn-1] = CUS cstr;
++ regex_vars[nn-1] = cstr ? CUS cstr : CUS"";
+ }
+
+ return OK;
+ }
+ }
diff --git a/debian/patches/75_32-Fix-regex-substring-capture-variables-for-null-match.patch b/debian/patches/75_32-Fix-regex-substring-capture-variables-for-null-match.patch
new file mode 100644
index 0000000..7c2c14a
--- /dev/null
+++ b/debian/patches/75_32-Fix-regex-substring-capture-variables-for-null-match.patch
@@ -0,0 +1,94 @@
+From 7ad1a2b2cc57b5f4bcb59186a9a8abcbed9f4f76 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Fri, 11 Nov 2022 18:22:00 +0000
+Subject: [PATCH 2/2] Fix regex substring capture variables for null matches
+ (again). Bug 2933
+
+Broken-by: 59d66fdc13f0
+---
+ src/exim.c | 11 +++++------
+ src/malware.c | 10 +++++-----
+ src/regex.c | 8 ++++----
+ test/aux-var-src/0383.F | 4 ++--
+ test/log/0383 | 4 ++--
+ test/mail/0383.CALLER | 8 ++++----
+ test/scripts/0000-Basic/0002 | 2 ++
+ test/stdout/0002 | 2 ++
+ 8 files changed, 26 insertions(+), 23 deletions(-)
+
+--- a/src/exim.c
++++ b/src/exim.c
+@@ -160,20 +160,19 @@
+ PCRE_EOPT | options, md, pcre_mtc_ctx);
+ BOOL yield;
+
+ if ((yield = (res >= 0)))
+ {
++ PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md);
+ res = pcre2_get_ovector_count(md);
+ expand_nmax = setup < 0 ? 0 : setup + 1;
+ for (int matchnum = setup < 0 ? 0 : 1; matchnum < res; matchnum++)
+ {
+- PCRE2_SIZE len;
+- pcre2_substring_get_bynumber(md, matchnum,
+- (PCRE2_UCHAR **)&expand_nstring[expand_nmax], &len);
+- if (!expand_nstring[expand_nmax])
+- { expand_nstring[expand_nmax] = US""; len = 0; }
+- expand_nlength[expand_nmax++] = (int)len;
++ int off = matchnum * 2;
++ int len = ovec[off + 1] - ovec[off];
++ expand_nstring[expand_nmax] = string_copyn(subject + ovec[off], len);
++ expand_nlength[expand_nmax++] = len;
+ }
+ expand_nmax--;
+ }
+ else if (res != PCRE2_ERROR_NOMATCH) DEBUG(D_any)
+ {
+--- a/src/malware.c
++++ b/src/malware.c
+@@ -319,19 +319,19 @@
+ uschar *
+ m_pcre_exec(const pcre2_code * cre, uschar * text)
+ {
+ pcre2_match_data * md = pcre2_match_data_create(2, pcre_gen_ctx);
+ int i = pcre2_match(cre, text, PCRE2_ZERO_TERMINATED, 0, 0, md, pcre_mtc_ctx);
+-PCRE2_UCHAR * substr = NULL;
+-PCRE2_SIZE slen;
++uschar * substr = NULL;
+
+ if (i >= 2) /* Got it */
+ {
+- pcre2_substring_get_bynumber(md, 1, &substr, &slen);
+- if (!substr) substr = US"";
++ PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md);
++ int len = ovec[3] - ovec[2];
++ substr = string_copyn(text + ovec[2], len);
+ }
+-return US substr;
++return substr;
+ }
+
+ static const pcre2_code *
+ m_pcre_nextinlist(const uschar ** list, int * sep,
+ char * listerr, uschar ** errstr)
+--- a/src/regex.c
++++ b/src/regex.c
+@@ -81,14 +81,14 @@
+ sizeof(regex_match_string_buffer)-1);
+ regex_match_string = regex_match_string_buffer;
+
+ for (int nn = 1; nn < n; nn++)
+ {
+- PCRE2_UCHAR * cstr;
+- PCRE2_SIZE cslen;
+- pcre2_substring_get_bynumber(md, nn, &cstr, &cslen);
+- regex_vars[nn-1] = cstr ? CUS cstr : CUS"";
++ PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md);
++ int off = nn * 2;
++ int len = ovec[off + 1] - ovec[off];
++ regex_vars[nn-1] = string_copyn(linebuffer + ovec[off], len);
+ }
+
+ return OK;
+ }
+ }
diff --git a/debian/patches/75_34-Fix-regex-substring-capture-commentary.-Bug-2933.patch b/debian/patches/75_34-Fix-regex-substring-capture-commentary.-Bug-2933.patch
new file mode 100644
index 0000000..61d4483
--- /dev/null
+++ b/debian/patches/75_34-Fix-regex-substring-capture-commentary.-Bug-2933.patch
@@ -0,0 +1,48 @@
+From 9ba47886c71d40edc99b026a99edee269d9c9c6f Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Sat, 12 Nov 2022 12:38:22 +0000
+Subject: [PATCH] Fix regex substring capture - commentary. Bug 2933
+
+Broken-by (corrected): 22ed7a5295f1
+---
+ src/exim.c | 9 ++++++++-
+ 1 file changed, 8 insertions(+), 1 deletion(-)
+
+diff --git a/src/exim.c b/src/exim.c
+index 16c0184e0..625494ce4 100644
+--- a/src/exim.c
++++ b/src/exim.c
+@@ -102,11 +102,13 @@ pcre_gen_mtc_ctx = pcre2_match_context_create(pcre_gen_ctx);
+ * Execute regular expression and set strings *
+ *************************************************/
+
+ /* This function runs a regular expression match, and sets up the pointers to
+ the matched substrings. The matched strings are copied so the lifetime of
+-the subject is not a problem.
++the subject is not a problem. Matched strings will have the same taint status
++as the subject string (this is not a de-taint method, and must not be made so
++given the support for wildcards in REs).
+
+ Arguments:
+ re the compiled expression
+ subject the subject string
+ options additional PCRE options
+@@ -130,10 +132,15 @@ if ((yield = (res >= 0)))
+ PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md);
+ res = pcre2_get_ovector_count(md);
+ expand_nmax = setup < 0 ? 0 : setup + 1;
+ for (int matchnum = setup < 0 ? 0 : 1; matchnum < res; matchnum++)
+ {
++ /* Although PCRE2 has a pcre2_substring_get_bynumber() conveneience, it
++ seems to return a bad pointer when a capture group had no data, eg. (.*)
++ matching zero letters. So use the underlying ovec and hope (!) that the
++ offsets are sane (including that case). Should we go further and range-
++ check each one vs. the subject string length? */
+ int off = matchnum * 2;
+ int len = ovec[off + 1] - ovec[off];
+ expand_nstring[expand_nmax] = string_copyn(subject + ovec[off], len);
+ expand_nlength[expand_nmax++] = len;
+ }
+--
+2.35.1
+
diff --git a/debian/patches/75_37-OpenSSL-when-preloading-creds-do-the-server-certs-be.patch b/debian/patches/75_37-OpenSSL-when-preloading-creds-do-the-server-certs-be.patch
new file mode 100644
index 0000000..2e21065
--- /dev/null
+++ b/debian/patches/75_37-OpenSSL-when-preloading-creds-do-the-server-certs-be.patch
@@ -0,0 +1,232 @@
+From 7f65a63b60c6ea86db683ac00e221939f3bb1d47 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Tue, 25 Oct 2022 21:26:30 +0100
+Subject: [PATCH 1/2] OpenSSL: when preloading creds do the server certs before
+ the OCSP proofs so that the latter can ve verified before loading
+
+---
+ src/tls-openssl.c | 113 ++++++++++++++++++++++--------------------
+ 1 file changed, 58 insertions(+), 55 deletions(-)
+
+diff --git a/src/tls-openssl.c b/src/tls-openssl.c
+index 68ad6f15b..fdf0d92b2 100644
+--- a/src/tls-openssl.c
++++ b/src/tls-openssl.c
+@@ -441,14 +441,16 @@ exim_openssl_state_st state_server = {.is_server = TRUE};
+ static int
+ setup_certs(SSL_CTX *sctx, uschar *certs, uschar *crl, host_item *host,
+ uschar ** errstr );
+
+ /* Callbacks */
+ #ifndef DISABLE_OCSP
+ static int tls_server_stapling_cb(SSL *s, void *arg);
++static void x509_stack_dump_cert_s_names(const STACK_OF(X509) * sk);
++static void x509_store_dump_cert_s_names(X509_STORE * store);
+ #endif
+
+
+
+ /* Daemon-called, before every connection, key create/rotate */
+ #ifndef DISABLE_TLS_RESUME
+ static void tk_init(void);
+@@ -1307,15 +1309,14 @@ ocsp_load_response(exim_openssl_state_st * state, const uschar * filename,
+ {
+ BIO * bio;
+ OCSP_RESPONSE * resp;
+ OCSP_BASICRESP * basic_response;
+ OCSP_SINGLERESP * single_response;
+ ASN1_GENERALIZEDTIME * rev, * thisupd, * nextupd;
+ STACK_OF(X509) * sk;
+-unsigned long verify_flags;
+ int status, reason, i;
+
+ DEBUG(D_tls)
+ debug_printf("tls_ocsp_file (%s) '%s'\n", is_pem ? "PEM" : "DER", filename);
+
+ if (!filename || !*filename) return;
+
+@@ -1372,28 +1373,28 @@ if ((status = OCSP_response_status(resp)) != OCSP_RESPONSE_STATUS_SUCCESSFUL)
+ if (!(basic_response = OCSP_response_get1_basic(resp)))
+ {
+ DEBUG(D_tls)
+ debug_printf("OCSP response parse error: unable to extract basic response.\n");
+ goto bad;
+ }
+
+-sk = state->verify_stack;
+-verify_flags = OCSP_NOVERIFY; /* check sigs, but not purpose */
++sk = state->verify_stack; /* set by setup_certs() / chain_from_pem_file() */
+
+ /* May need to expose ability to adjust those flags?
+ OCSP_NOSIGS OCSP_NOVERIFY OCSP_NOCHAIN OCSP_NOCHECKS OCSP_NOEXPLICIT
+ OCSP_TRUSTOTHER OCSP_NOINTERN */
+
+-/* This does a full verify on the OCSP proof before we load it for serving
+-up; possibly overkill - just date-checks might be nice enough.
++/* This does a partial verify (only the signer link, not the whole chain-to-CA)
++on the OCSP proof before we load it for serving up; possibly overkill -
++just date-checks might be nice enough.
+
+ OCSP_basic_verify takes a "store" arg, but does not
+-use it for the chain verification, which is all we do
+-when OCSP_NOVERIFY is set. The content from the wire
+-"basic_response" and a cert-stack "sk" are all that is used.
++use it for the chain verification, when OCSP_NOVERIFY is set.
++The content from the wire "basic_response" and a cert-stack "sk" are all
++that is used.
+
+ We have a stack, loaded in setup_certs() if tls_verify_certificates
+ was a file (not a directory, or "system"). It is unfortunate we
+ cannot used the connection context store, as that would neatly
+ handle the "system" case too, but there seems to be no library
+ function for getting a stack from a store.
+ [ In OpenSSL 1.1 - ? X509_STORE_CTX_get0_chain(ctx) ? ]
+@@ -1402,15 +1403,15 @@ SNI handling.
+
+ Separately we might try to replace using OCSP_basic_verify() - which seems to not
+ be a public interface into the OpenSSL library (there's no manual entry) -
+ But what with? We also use OCSP_basic_verify in the client stapling callback.
+ And there we NEED it; we must verify that status... unless the
+ library does it for us anyway? */
+
+-if ((i = OCSP_basic_verify(basic_response, sk, NULL, verify_flags)) < 0)
++if ((i = OCSP_basic_verify(basic_response, sk, NULL, OCSP_NOVERIFY)) < 0)
+ {
+ DEBUG(D_tls)
+ {
+ ERR_error_string_n(ERR_get_error(), ssl_errstring, sizeof(ssl_errstring));
+ debug_printf("OCSP response verify failure: %s\n", US ssl_errstring);
+ }
+ goto bad;
+@@ -1747,61 +1748,18 @@ if (opt_unset_or_noexpand(tls_eccurve))
+ if (init_ecdh(ctx, &dummy_errstr))
+ state_server.lib_state.ecdh = TRUE;
+ }
+ else
+ DEBUG(D_tls) debug_printf("TLS: not preloading ECDH curve for server\n");
+
+ #if defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT)
+-/* If we can, preload the server-side cert, key and ocsp */
+-
+-if ( opt_set_and_noexpand(tls_certificate)
+-# ifndef DISABLE_OCSP
+- && opt_unset_or_noexpand(tls_ocsp_file)
+-#endif
+- && opt_unset_or_noexpand(tls_privatekey))
+- {
+- /* Set watches on the filenames. The implementation does de-duplication
+- so we can just blindly do them all. */
+-
+- if ( tls_set_watch(tls_certificate, TRUE)
+-# ifndef DISABLE_OCSP
+- && tls_set_watch(tls_ocsp_file, TRUE)
+-#endif
+- && tls_set_watch(tls_privatekey, TRUE))
+- {
+- state_server.certificate = tls_certificate;
+- state_server.privatekey = tls_privatekey;
+-#ifndef DISABLE_OCSP
+- state_server.u_ocsp.server.file = tls_ocsp_file;
+-#endif
+-
+- DEBUG(D_tls) debug_printf("TLS: preloading server certs\n");
+- if (tls_expand_session_files(ctx, &state_server, &dummy_errstr) == OK)
+- state_server.lib_state.conn_certs = TRUE;
+- }
+- }
+-else if ( !tls_certificate && !tls_privatekey
+-# ifndef DISABLE_OCSP
+- && !tls_ocsp_file
+-#endif
+- )
+- { /* Generate & preload a selfsigned cert. No files to watch. */
+- if (tls_expand_session_files(ctx, &state_server, &dummy_errstr) == OK)
+- {
+- state_server.lib_state.conn_certs = TRUE;
+- lifetime = f.running_in_test_harness ? 2 : 60 * 60; /* 1 hour */
+- }
+- }
+-else
+- DEBUG(D_tls) debug_printf("TLS: not preloading server certs\n");
+-
+-
+ /* If we can, preload the Authorities for checking client certs against.
+ Actual choice to do verify is made (tls_{,try_}verify_hosts)
+-at TLS conn startup */
++at TLS conn startup.
++Do this before the server ocsp so that its info can verify the ocsp. */
+
+ if ( opt_set_and_noexpand(tls_verify_certificates)
+ && opt_unset_or_noexpand(tls_crl))
+ {
+ /* Watch the default dir also as they are always included */
+
+ if ( tls_set_watch(CUS X509_get_default_cert_file(), FALSE)
+@@ -1809,18 +1767,63 @@ if ( opt_set_and_noexpand(tls_verify_certificates)
+ && tls_set_watch(tls_crl, FALSE))
+ {
+ DEBUG(D_tls) debug_printf("TLS: preloading CA bundle for server\n");
+
+ if (setup_certs(ctx, tls_verify_certificates, tls_crl, NULL, &dummy_errstr)
+ == OK)
+ state_server.lib_state.cabundle = TRUE;
+- }
++
++ /* If we can, preload the server-side cert, key and ocsp */
++
++ if ( opt_set_and_noexpand(tls_certificate)
++# ifndef DISABLE_OCSP
++ && opt_unset_or_noexpand(tls_ocsp_file)
++# endif
++ && opt_unset_or_noexpand(tls_privatekey))
++ {
++ /* Set watches on the filenames. The implementation does de-duplication
++ so we can just blindly do them all. */
++
++ if ( tls_set_watch(tls_certificate, TRUE)
++# ifndef DISABLE_OCSP
++ && tls_set_watch(tls_ocsp_file, TRUE)
++# endif
++ && tls_set_watch(tls_privatekey, TRUE))
++ {
++ state_server.certificate = tls_certificate;
++ state_server.privatekey = tls_privatekey;
++#ifndef DISABLE_OCSP
++ state_server.u_ocsp.server.file = tls_ocsp_file;
++# endif
++
++ DEBUG(D_tls) debug_printf("TLS: preloading server certs\n");
++ if (tls_expand_session_files(ctx, &state_server, &dummy_errstr) == OK)
++ state_server.lib_state.conn_certs = TRUE;
++ }
++ }
++ else if ( !tls_certificate && !tls_privatekey
++# ifndef DISABLE_OCSP
++ && !tls_ocsp_file
++# endif
++ )
++ { /* Generate & preload a selfsigned cert. No files to watch. */
++ if (tls_expand_session_files(ctx, &state_server, &dummy_errstr) == OK)
++ {
++ state_server.lib_state.conn_certs = TRUE;
++ lifetime = f.running_in_test_harness ? 2 : 60 * 60; /* 1 hour */
++ }
++ }
++ else
++ DEBUG(D_tls) debug_printf("TLS: not preloading server certs\n");
++ }
+ }
+ else
+ DEBUG(D_tls) debug_printf("TLS: not preloading CA bundle for server\n");
++
++
+ #endif /* EXIM_HAVE_INOTIFY */
+
+
+ /* If we can, preload the ciphers control string */
+
+ if (opt_set_and_noexpand(tls_require_ciphers))
+ {
+--
+2.35.1
+
diff --git a/debian/patches/75_38-OpenSSL-fix-double-expansion-of-tls_verify_certifica.patch b/debian/patches/75_38-OpenSSL-fix-double-expansion-of-tls_verify_certifica.patch
new file mode 100644
index 0000000..09e4f11
--- /dev/null
+++ b/debian/patches/75_38-OpenSSL-fix-double-expansion-of-tls_verify_certifica.patch
@@ -0,0 +1,217 @@
+From 62b97c2ecf148ee86053d82e5509e4c3a5a20054 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Sat, 29 Oct 2022 22:33:43 +0100
+Subject: [PATCH 2/2] OpenSSL: fix double-expansion of tls_verify_certificates
+
+---
+ src/tls-openssl.c | 66 +++++++++++++++++++++----------------------
+ 1 file changed, 33 insertions(+), 33 deletions(-)
+
+diff --git a/src/tls-openssl.c b/src/tls-openssl.c
+index fdf0d92b2..2e09882d2 100644
+--- a/src/tls-openssl.c
++++ b/src/tls-openssl.c
+@@ -435,15 +435,15 @@ typedef struct exim_openssl_state {
+ /* should figure out a cleanup of API to handle state preserved per
+ implementation, for various reasons, which can be void * in the APIs.
+ For now, we hack around it. */
+ exim_openssl_state_st *client_static_state = NULL; /*XXX should not use static; multiple concurrent clients! */
+ exim_openssl_state_st state_server = {.is_server = TRUE};
+
+ static int
+-setup_certs(SSL_CTX *sctx, uschar *certs, uschar *crl, host_item *host,
++setup_certs(SSL_CTX * sctx, uschar ** certs, uschar * crl, host_item * host,
+ uschar ** errstr );
+
+ /* Callbacks */
+ #ifndef DISABLE_OCSP
+ static int tls_server_stapling_cb(SSL *s, void *arg);
+ static void x509_stack_dump_cert_s_names(const STACK_OF(X509) * sk);
+ static void x509_store_dump_cert_s_names(X509_STORE * store);
+@@ -1762,18 +1762,18 @@ if ( opt_set_and_noexpand(tls_verify_certificates)
+ {
+ /* Watch the default dir also as they are always included */
+
+ if ( tls_set_watch(CUS X509_get_default_cert_file(), FALSE)
+ && tls_set_watch(tls_verify_certificates, FALSE)
+ && tls_set_watch(tls_crl, FALSE))
+ {
++ uschar * v_certs = tls_verify_certificates;
+ DEBUG(D_tls) debug_printf("TLS: preloading CA bundle for server\n");
+
+- if (setup_certs(ctx, tls_verify_certificates, tls_crl, NULL, &dummy_errstr)
+- == OK)
++ if (setup_certs(ctx, &v_certs, tls_crl, NULL, &dummy_errstr) == OK)
+ state_server.lib_state.cabundle = TRUE;
+
+ /* If we can, preload the server-side cert, key and ocsp */
+
+ if ( opt_set_and_noexpand(tls_certificate)
+ # ifndef DISABLE_OCSP
+ && opt_unset_or_noexpand(tls_ocsp_file)
+@@ -1897,18 +1897,19 @@ if ( opt_set_and_noexpand(ob->tls_verify_certificates)
+ {
+ if ( !watch
+ || tls_set_watch(CUS X509_get_default_cert_file(), FALSE)
+ && tls_set_watch(ob->tls_verify_certificates, FALSE)
+ && tls_set_watch(ob->tls_crl, FALSE)
+ )
+ {
++ uschar * v_certs = ob->tls_verify_certificates;
+ DEBUG(D_tls)
+ debug_printf("TLS: preloading CA bundle for transport '%s'\n", t->name);
+
+- if (setup_certs(ctx, ob->tls_verify_certificates,
++ if (setup_certs(ctx, &v_certs,
+ ob->tls_crl, dummy_host, &dummy_errstr) == OK)
+ ob->tls_preload.cabundle = TRUE;
+ }
+ }
+ else
+ DEBUG(D_tls)
+ debug_printf("TLS: not preloading CA bundle, for transport '%s'\n", t->name);
+@@ -2238,22 +2239,20 @@ if (state->u_ocsp.server.file)
+ {
+ SSL_CTX_set_tlsext_status_cb(server_sni, tls_server_stapling_cb);
+ SSL_CTX_set_tlsext_status_arg(server_sni, state);
+ }
+ #endif
+
+ {
+- uschar * expcerts;
+- if ( !expand_check(tls_verify_certificates, US"tls_verify_certificates",
+- &expcerts, &dummy_errstr)
+- || (rc = setup_certs(server_sni, expcerts, tls_crl, NULL,
++ uschar * v_certs = tls_verify_certificates;
++ if ((rc = setup_certs(server_sni, &v_certs, tls_crl, NULL,
+ &dummy_errstr)) != OK)
+ goto bad;
+
+- if (expcerts && *expcerts)
++ if (v_certs && *v_certs)
+ setup_cert_verify(server_sni, FALSE, verify_callback_server);
+ }
+
+ /* do this after setup_certs, because this can require the certs for verifying
+ OCSP information. */
+ if ((rc = tls_expand_session_files(server_sni, state, &dummy_errstr)) != OK)
+ goto bad;
+@@ -3017,32 +3016,33 @@ return TRUE;
+
+
+ /* Called by both client and server startup; on the server possibly
+ repeated after a Server Name Indication.
+
+ Arguments:
+ sctx SSL_CTX* to initialise
+- certs certs file, expanded
++ certs certs file, returned expanded
+ crl CRL file or NULL
+ host NULL in a server; the remote host in a client
+ errstr error string pointer
+
+ Returns: OK/DEFER/FAIL
+ */
+
+ static int
+-setup_certs(SSL_CTX *sctx, uschar *certs, uschar *crl, host_item *host,
++setup_certs(SSL_CTX * sctx, uschar ** certsp, uschar * crl, host_item * host,
+ uschar ** errstr)
+ {
+-uschar *expcerts, *expcrl;
++uschar * expcerts, * expcrl;
+
+-if (!expand_check(certs, US"tls_verify_certificates", &expcerts, errstr))
++if (!expand_check(*certsp, US"tls_verify_certificates", &expcerts, errstr))
+ return DEFER;
+ DEBUG(D_tls) debug_printf("tls_verify_certificates: %s\n", expcerts);
+
++*certsp = expcerts;
+ if (expcerts && *expcerts)
+ {
+ /* Tell the library to use its compiled-in location for the system default
+ CA bundle. Then add the ones specified in the config, if any. */
+
+ if (!SSL_CTX_set_default_verify_paths(sctx))
+ return tls_error(US"SSL_CTX_set_default_verify_paths", host, NULL, errstr);
+@@ -3330,28 +3330,28 @@ if (verify_check_host(&tls_verify_hosts) == OK)
+ server_verify_optional = FALSE;
+ else if (verify_check_host(&tls_try_verify_hosts) == OK)
+ server_verify_optional = TRUE;
+ else
+ goto skip_certs;
+
+ {
+- uschar * expcerts;
+- if (!expand_check(tls_verify_certificates, US"tls_verify_certificates",
+- &expcerts, errstr))
+- return DEFER;
+- DEBUG(D_tls) debug_printf("tls_verify_certificates: %s\n", expcerts);
++ uschar * v_certs = tls_verify_certificates;
+
+ if (state_server.lib_state.cabundle)
+- { DEBUG(D_tls) debug_printf("TLS: CA bundle for server was preloaded\n"); }
++ {
++ DEBUG(D_tls) debug_printf("TLS: CA bundle for server was preloaded\n");
++ setup_cert_verify(ctx, server_verify_optional, verify_callback_server);
++ }
+ else
+- if ((rc = setup_certs(ctx, expcerts, tls_crl, NULL, errstr)) != OK)
++ {
++ if ((rc = setup_certs(ctx, &v_certs, tls_crl, NULL, errstr)) != OK)
+ return rc;
+-
+- if (expcerts && *expcerts)
+- setup_cert_verify(ctx, server_verify_optional, verify_callback_server);
++ if (v_certs && *v_certs)
++ setup_cert_verify(ctx, server_verify_optional, verify_callback_server);
++ }
+ }
+ skip_certs: ;
+
+ #ifndef DISABLE_TLS_RESUME
+ # if OPENSSL_VERSION_NUMBER < 0x30000000L
+ SSL_CTX_set_tlsext_ticket_key_cb(ctx, ticket_key_callback);
+ /* despite working, appears to always return failure, so ignoring */
+@@ -3606,28 +3606,28 @@ if ( ( ( !ob->tls_verify_hosts || !ob->tls_verify_hosts
+ client_verify_optional = FALSE;
+ else if (verify_check_given_host(CUSS &ob->tls_try_verify_hosts, host) == OK)
+ client_verify_optional = TRUE;
+ else
+ return OK;
+
+ {
+- uschar * expcerts;
+- if (!expand_check(ob->tls_verify_certificates, US"tls_verify_certificates",
+- &expcerts, errstr))
+- return DEFER;
+- DEBUG(D_tls) debug_printf("tls_verify_certificates: %s\n", expcerts);
++ uschar * v_certs = ob->tls_verify_certificates;
+
+ if (state->lib_state.cabundle)
+- { DEBUG(D_tls) debug_printf("TLS: CA bundle was preloaded\n"); }
++ {
++ DEBUG(D_tls) debug_printf("TLS: CA bundle for tpt was preloaded\n");
++ setup_cert_verify(ctx, client_verify_optional, verify_callback_client);
++ }
+ else
+- if ((rc = setup_certs(ctx, expcerts, ob->tls_crl, host, errstr)) != OK)
++ {
++ if ((rc = setup_certs(ctx, &v_certs, ob->tls_crl, host, errstr)) != OK)
+ return rc;
+-
+- if (expcerts && *expcerts)
+- setup_cert_verify(ctx, client_verify_optional, verify_callback_client);
++ if (v_certs && *v_certs)
++ setup_cert_verify(ctx, client_verify_optional, verify_callback_client);
++ }
+ }
+
+ if (verify_check_given_host(CUSS &ob->tls_verify_cert_hostnames, host) == OK)
+ {
+ state->verify_cert_hostnames =
+ #ifdef SUPPORT_I18N
+ string_domain_utf8_to_alabel(host->certname, NULL);
+--
+2.35.1
+
diff --git a/debian/patches/75_42-Fix-run-arg-parsing.patch b/debian/patches/75_42-Fix-run-arg-parsing.patch
new file mode 100644
index 0000000..79e55d6
--- /dev/null
+++ b/debian/patches/75_42-Fix-run-arg-parsing.patch
@@ -0,0 +1,100 @@
+From 44b6e099b76f403a55e77650821f8a69e9d2682e Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Sat, 3 Dec 2022 23:13:53 +0000
+Subject: [PATCH] Fix ${run } arg parsing
+ .
+ Backported by Bryce Harrington for Ubuntu
+
+Broken-by: cfe6acff2ddc
+---
+ doc/ChangeLog | 4 ++++
+ src/expand.c | 13 ++++++++++---
+ src/transport.c | 4 +++-
+ test/scripts/0000-Basic/0002 | 2 ++
+ test/stdout/0002 | 2 ++
+ 5 files changed, 21 insertions(+), 4 deletions(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -28,10 +28,14 @@ JH/13 Bug 2929: Fix using $recipients af
+ JH/14 Bug 2933: Fix regex substring match variables for null matches. Since 4.96
+ a capture group which obtained no text (eg. "(abc)*" matching zero
+ occurrences) could cause a segfault if the corresponding $<n> was
+ expanded.
+
++JH/15 Fix argument parsing for ${run } expansion. Previously, when an argument
++ included a close-brace character (eg. it itself used an expansion) an
++ error occurred.
++
+
+
+ Exim version 4.96
+ -----------------
+
+--- a/src/expand.c
++++ b/src/expand.c
+@@ -5529,11 +5529,11 @@ while (*s)
+ {
+ FILE * f;
+ const uschar * arg, ** argv;
+ BOOL late_expand = TRUE;
+
+- if ((expand_forbid & RDO_RUN) != 0)
++ if (expand_forbid & RDO_RUN)
+ {
+ expand_string_message = US"running a command is not permitted";
+ goto EXPAND_FAILED;
+ }
+
+@@ -5561,16 +5561,22 @@ while (*s)
+ }
+ s++;
+
+ if (late_expand) /* this is the default case */
+ {
+- int n = Ustrcspn(s, "}");
++ int n;
++ const uschar * t;
++ /* Locate the end of the args */
++ (void) expand_string_internal(s, TRUE, &t, TRUE, TRUE, NULL);
++ n = t - s;
+ arg = skipping ? NULL : string_copyn(s, n);
+ s += n;
+ }
+ else
+ {
++ DEBUG(D_expand)
++ debug_printf_indent("args string for ${run} expand before split\n");
+ if (!(arg = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok)))
+ goto EXPAND_FAILED;
+ Uskip_whitespace(&s);
+ }
+ /*{*/
+--- a/src/transport.c
++++ b/src/transport.c
+@@ -2187,10 +2187,12 @@ if (expand_arguments)
+ BOOL allow_dollar_recipients = addr && addr->parent
+ && Ustrcmp(addr->parent->address, "system-filter") == 0;
+
+ for (int i = 0; argv[i]; i++)
+ {
++ DEBUG(D_expand) debug_printf_indent("arg %d\n", i);
++
+ /* Handle special fudge for passing an address list */
+
+ if (addr &&
+ (Ustrcmp(argv[i], "$pipe_addresses") == 0 ||
+ Ustrcmp(argv[i], "${pipe_addresses}") == 0))
+@@ -2361,11 +2363,11 @@ if (expand_arguments)
+ }
+ else *errptr = msg;
+ return FALSE;
+ }
+
+- if ( f.running_in_test_harness && is_tainted(expanded_arg)
++ if ( f.running_in_test_harness && is_tainted(expanded_arg)
+ && Ustrcmp(etext, "queryprogram router") == 0)
+ { /* hack, would be good to not need it */
+ DEBUG(D_transport)
+ debug_printf("SPECIFIC TESTSUITE EXEMPTION: tainted arg '%s'\n",
+ expanded_arg);
diff --git a/debian/patches/75_50-Fix-logging-of-max-size-log-line.patch b/debian/patches/75_50-Fix-logging-of-max-size-log-line.patch
new file mode 100644
index 0000000..55c983e
--- /dev/null
+++ b/debian/patches/75_50-Fix-logging-of-max-size-log-line.patch
@@ -0,0 +1,82 @@
+From 1ed24e36e279c922d3366f6c3144570cc5f54d7a Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Mon, 19 Dec 2022 21:09:17 +0000
+Subject: [PATCH] Fix logging of max-size log line
+
+Broken-by: d12746bc15d8
+---
+ doc/ChangeLog | 5 +++++
+ src/log.c | 7 ++++---
+ test/confs/0633 | 21 ++++++++++++++++++++
+ test/scripts/0000-Basic/0633 | 9 +++++++++
+ test/stderr/0633 | 38 ++++++++++++++++++++++++++++++++++++
+ test/stdout/0633 | 15 ++++++++++++++
+ 6 files changed, 92 insertions(+), 3 deletions(-)
+ create mode 100644 test/confs/0633
+ create mode 100644 test/scripts/0000-Basic/0633
+ create mode 100644 test/stderr/0633
+ create mode 100644 test/stdout/0633
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -32,10 +32,15 @@ JH/14 Bug 2933: Fix regex substring matc
+
+ JH/15 Fix argument parsing for ${run } expansion. Previously, when an argument
+ included a close-brace character (eg. it itself used an expansion) an
+ error occurred.
+
++JH/18 Fix a fencepost error in logging. Previously (since 4.92) when a log line
++ was exactly sized compared to the log buffer, a crash occurred with the
++ misleading message "bad memory reference; pool not found".
++ Found and traced by Jasen Betts.
++
+
+
+ Exim version 4.96
+ -----------------
+
+--- a/src/log.c
++++ b/src/log.c
+@@ -803,11 +803,11 @@ Returns: nothing
+ void
+ log_write(unsigned int selector, int flags, const char *format, ...)
+ {
+ int paniclogfd;
+ ssize_t written_len;
+-gstring gs = { .size = LOG_BUFFER_SIZE-1, .ptr = 0, .s = log_buffer };
++gstring gs = { .size = LOG_BUFFER_SIZE-2, .ptr = 0, .s = log_buffer };
+ gstring * g;
+ va_list ap;
+
+ /* If panic_recurseflag is set, we have failed to open the panic log. This is
+ the ultimate disaster. First try to write the message to a debug file and/or
+@@ -949,15 +949,14 @@ DEBUG(D_any|D_v)
+ g->ptr = i;
+ g = string_cat(g, US"**** log string overflowed log buffer ****");
+ }
+ va_end(ap);
+
+- g->size = LOG_BUFFER_SIZE;
+ g = string_catn(g, US"\n", 1);
+ debug_printf("%s", string_from_gstring(g));
+
+- gs.size = LOG_BUFFER_SIZE-1; /* Having used the buffer for debug output, */
++ gs.size = LOG_BUFFER_SIZE-2; /* Having used the buffer for debug output, */
+ gs.ptr = 0; /* reset it for the real use. */
+ gs.s = log_buffer;
+ }
+ /* If no log file is specified, we are in a mess. */
+
+@@ -1035,10 +1034,12 @@ if ( flags & LOG_RECIPIENTS
+ if (LOG_BUFFER_SIZE - g->ptr < Ustrlen(s) + 3) break;
+ g = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, " %s", s);
+ }
+ }
+
++/* actual size, now we are placing the newline (and space for NUL) */
++gs.size = LOG_BUFFER_SIZE;
+ g = string_catn(g, US"\n", 1);
+ string_from_gstring(g);
+
+ /* Handle loggable errors when running a utility, or when address testing.
+ Write to log_stderr unless debugging (when it will already have been written),
diff --git a/debian/patches/75_55-Fix-recursion-on-dns_again_means_nonexist.-Bug-2911.patch b/debian/patches/75_55-Fix-recursion-on-dns_again_means_nonexist.-Bug-2911.patch
new file mode 100644
index 0000000..bbbfbe9
--- /dev/null
+++ b/debian/patches/75_55-Fix-recursion-on-dns_again_means_nonexist.-Bug-2911.patch
@@ -0,0 +1,76 @@
+From 1d38781da934809e6ce0b8c3718c4b3bccdfe1d2 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Wed, 28 Dec 2022 19:39:06 +0000
+Subject: [PATCH] Fix recursion on dns_again_means_nonexist. Bug 2911
+
+---
+ doc/ChangeLog | 8 +++++
+ src/dns.c | 12 ++++++++
+ test/confs/2202 | 18 +++++++++--
+ test/scripts/2200-dnsdb/2202 | 8 +++++
+ test/stderr/2202 | 58 +++++++++++++++++++++++++++++++++++-
+ test/stdout/2202 | 8 +++++
+ 6 files changed, 108 insertions(+), 4 deletions(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -33,10 +33,18 @@ JH/14 Bug 2933: Fix regex substring matc
+ JH/18 Fix a fencepost error in logging. Previously (since 4.92) when a log line
+ was exactly sized compared to the log buffer, a crash occurred with the
+ misleading message "bad memory reference; pool not found".
+ Found and traced by Jasen Betts.
+
++JH/19 Bug 2911: Fix a recursion in DNS lookups. Previously, if the main option
++ dns_again_means_nonexist included an element causing a DNS lookup which
++ iteslf returned DNS_AGAIN, unbounded recursion occurred. Possible results
++ included (though probably not limited to) a process crash from stack
++ memory limit, or from excessive open files. Replace this with a paniclog
++ whine (as this is likely a configuration error), and returning
++ DNS_NOMATCH.
++
+
+
+ Exim version 4.96
+ -----------------
+
+--- a/src/dns.c
++++ b/src/dns.c
+@@ -799,10 +799,11 @@ int
+ dns_basic_lookup(dns_answer * dnsa, const uschar * name, int type)
+ {
+ int rc;
+ #ifndef STAND_ALONE
+ const uschar * save_domain;
++static BOOL try_again_recursion = FALSE;
+ #endif
+
+ /* DNS lookup failures of any kind are cached in a tree. This is mainly so that
+ a timeout on one domain doesn't happen time and time again for messages that
+ have many addresses in the same domain. We rely on the resolver and name server
+@@ -903,15 +904,26 @@ if (dnsa->answerlen < 0) switch (h_errno
+ DEBUG(D_dns) debug_printf("DNS lookup of %s (%s) gave TRY_AGAIN\n",
+ name, dns_text_type(type));
+
+ /* Cut this out for various test programs */
+ #ifndef STAND_ALONE
++ if (try_again_recursion)
++ {
++ log_write(0, LOG_MAIN|LOG_PANIC,
++ "dns_again_means_nonexist recursion seen for %s (assuming nonexist)",
++ name);
++ return dns_fail_return(name, type, dns_expire_from_soa(dnsa, type), DNS_NOMATCH);
++ }
++
++ try_again_recursion = TRUE;
+ save_domain = deliver_domain;
+ deliver_domain = string_copy(name); /* set $domain */
+ rc = match_isinlist(name, CUSS &dns_again_means_nonexist, 0,
+ &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, NULL);
+ deliver_domain = save_domain;
++ try_again_recursion = FALSE;
++
+ if (rc != OK)
+ {
+ DEBUG(D_dns) debug_printf("returning DNS_AGAIN\n");
+ return dns_fail_return(name, type, 0, DNS_AGAIN);
+ }
diff --git a/debian/patches/75_58-Close-server-smtp-socket-explicitly-on-connect-ACL-d.patch b/debian/patches/75_58-Close-server-smtp-socket-explicitly-on-connect-ACL-d.patch
new file mode 100644
index 0000000..ddf54fa
--- /dev/null
+++ b/debian/patches/75_58-Close-server-smtp-socket-explicitly-on-connect-ACL-d.patch
@@ -0,0 +1,50 @@
+From 57d70161718e02927a22d6a3481803b72035ac46 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Sat, 31 Dec 2022 13:37:17 +0000
+Subject: [PATCH] Close server smtp socket explicitly on connect ACL "drop"
+
+---
+ src/smtp_in.c | 13 ++++++++
+ test/confs/0022 | 2 ++
+ test/log/0022 | 2 ++
+ test/rejectlog/0022 | 3 ++
+ test/scripts/0000-Basic/0022 | 13 ++++++++
+ test/stderr/0022 | 60 ++++++++++++++++++------------------
+ test/stdout/0022 | 6 ++++
+ 7 files changed, 69 insertions(+), 30 deletions(-)
+ create mode 100644 test/rejectlog/0022
+
+diff --git a/src/smtp_in.c b/src/smtp_in.c
+index 1cfcc0404..6880e3c09 100644
+--- a/src/smtp_in.c
++++ b/src/smtp_in.c
+@@ -3563,10 +3563,23 @@ log_write(L_smtp_connection, LOG_MAIN, "%s closed by DROP in ACL",
+ /* Run the not-quit ACL, but without any custom messages. This should not be a
+ problem, because we get here only if some other ACL has issued "drop", and
+ in that case, *its* custom messages will have been used above. */
+
+ smtp_notquit_exit(US"acl-drop", NULL, NULL);
++
++/* An overenthusiastic fail2ban/iptables implimentation has been seen to result
++in the TCP conn staying open, and retrying, despite this process exiting. A
++malicious client could possibly do the same, tying up server netowrking
++resources. Close the socket explicitly to try to avoid that (there's a note in
++the Linux socket(7) manpage, SO_LINGER para, to the effect that exim() without
++close() results in the socket always lingering). */
++
++(void) poll_one_fd(fileno(smtp_in), POLLIN, 200);
++DEBUG(D_any) debug_printf_indent("SMTP(close)>>\n");
++(void) fclose(smtp_in);
++(void) fclose(smtp_out);
++
+ return 2;
+ }
+
+
+
+diff --git a/test/rejectlog/0022 b/test/rejectlog/0022
+new file mode 100644
+index 000000000..68e21fff3
+--
+2.39.0
+
diff --git a/debian/patches/75_60-OpenSSL-fix-tls_eccurve-setting-explicit-curve-group.patch b/debian/patches/75_60-OpenSSL-fix-tls_eccurve-setting-explicit-curve-group.patch
new file mode 100644
index 0000000..da05ba9
--- /dev/null
+++ b/debian/patches/75_60-OpenSSL-fix-tls_eccurve-setting-explicit-curve-group.patch
@@ -0,0 +1,184 @@
+From ca4014de81e6aa367aa0a54c49b4c3d4b137814c Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Sun, 1 Jan 2023 12:18:38 +0000
+Subject: [PATCH] OpenSSL: fix tls_eccurve setting explicit curve/group. Bug
+ 2954
+
+---
+ doc/ChangeLog | 4 +++
+ src/tls-openssl.c | 39 ++++++++++++++----------
+ test/confs/2148 | 54 ++++++++++++++++++++++++++++++++++
+ test/confs/2149 | 39 +++++++++++++-----------
+ test/log/2148 | 48 ++++++++++++++++++++++++++++++
+ test/log/2149 | 39 ++++++++++++------------
+ test/paniclog/{2149 => 2148} | 0
+ test/scripts/2100-OpenSSL/2148 | 50 +++++++++++++++++++++++++++++++
+ test/scripts/2100-OpenSSL/2149 | 50 ++++++++++++++++---------------
+ test/stderr/2148 | 5 ++++
+ test/stderr/2149 | 3 --
+ 11 files changed, 250 insertions(+), 81 deletions(-)
+ create mode 100644 test/confs/2148
+ create mode 100644 test/log/2148
+ rename test/paniclog/{2149 => 2148} (100%)
+ create mode 100644 test/scripts/2100-OpenSSL/2148
+ create mode 100644 test/stderr/2148
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -41,10 +41,14 @@ JH/19 Bug 2911: Fix a recursion in DNS l
+ included (though probably not limited to) a process crash from stack
+ memory limit, or from excessive open files. Replace this with a paniclog
+ whine (as this is likely a configuration error), and returning
+ DNS_NOMATCH.
+
++JH/20 Bug 2954: (OpenSSL) Fix setting of explicit EC curve/group. Previously
++ this always failed, probably leading to the usual downgrade to in-clear
++ connections.
++
+
+
+ Exim version 4.96
+ -----------------
+
+--- a/src/tls-openssl.c
++++ b/src/tls-openssl.c
+@@ -657,16 +657,16 @@ if (dh_bitsize <= tls_dh_max_bits)
+ /* EVP_PKEY_free(pkey); crashes */
+ #endif
+ }
+ else
+ DEBUG(D_tls)
+- debug_printf("Diffie-Hellman initialized from %s with %d-bit prime\n",
++ debug_printf(" Diffie-Hellman initialized from %s with %d-bit prime\n",
+ dhexpanded ? dhexpanded : US"default", dh_bitsize);
+ }
+ else
+ DEBUG(D_tls)
+- debug_printf("dhparams '%s' %d bits, is > tls_dh_max_bits limit of %d\n",
++ debug_printf(" dhparams '%s' %d bits, is > tls_dh_max_bits limit of %d\n",
+ dhexpanded ? dhexpanded : US"default", dh_bitsize, tls_dh_max_bits);
+
+ #if OPENSSL_VERSION_NUMBER < 0x30000000L
+ DH_free(dh);
+ #endif
+@@ -712,23 +712,31 @@ init_ecdh(SSL_CTX * sctx, uschar ** errs
+ #ifdef OPENSSL_NO_ECDH
+ return TRUE;
+ #else
+
+ uschar * exp_curve;
+-int nid;
+-BOOL rv;
++int nid, rc;
+
+ # ifndef EXIM_HAVE_ECDH
+ DEBUG(D_tls)
+- debug_printf("No OpenSSL API to define ECDH parameters, skipping\n");
++ debug_printf(" No OpenSSL API to define ECDH parameters, skipping\n");
+ return TRUE;
+ # else
+
+ if (!expand_check(tls_eccurve, US"tls_eccurve", &exp_curve, errstr))
+ return FALSE;
++
++/* Is the option deliberately empty? */
++
+ if (!exp_curve || !*exp_curve)
++ {
++#if OPENSSL_VERSION_NUMBER >= 0x10002000L
++ DEBUG(D_tls) debug_printf( " ECDH OpenSSL 1.0.2+: clearing curves list\n");
++ (void) SSL_CTX_set1_curves(sctx, &nid, 0);
++#endif
+ return TRUE;
++ }
+
+ /* "auto" needs to be handled carefully.
+ * OpenSSL < 1.0.2: we do not select anything, but fallback to prime256v1
+ * OpenSSL < 1.1.0: we have to call SSL_CTX_set_ecdh_auto
+ * (openssl/ssl.h defines SSL_CTRL_SET_ECDH_AUTO)
+@@ -737,27 +745,26 @@ if (!exp_curve || !*exp_curve)
+ */
+ if (Ustrcmp(exp_curve, "auto") == 0)
+ {
+ #if OPENSSL_VERSION_NUMBER < 0x10002000L
+ DEBUG(D_tls) debug_printf(
+- "ECDH OpenSSL < 1.0.2: temp key parameter settings: overriding \"auto\" with \"prime256v1\"\n");
++ " ECDH OpenSSL < 1.0.2: temp key parameter settings: overriding \"auto\" with \"prime256v1\"\n");
+ exp_curve = US"prime256v1";
+ #else
+ # if defined SSL_CTRL_SET_ECDH_AUTO
+ DEBUG(D_tls) debug_printf(
+- "ECDH OpenSSL 1.0.2+: temp key parameter settings: autoselection\n");
++ " ECDH OpenSSL 1.0.2+: temp key parameter settings: autoselection\n");
+ SSL_CTX_set_ecdh_auto(sctx, 1);
+ return TRUE;
+ # else
+ DEBUG(D_tls) debug_printf(
+- "ECDH OpenSSL 1.1.0+: temp key parameter settings: default selection\n");
++ " ECDH OpenSSL 1.1.0+: temp key parameter settings: library default selection\n");
+ return TRUE;
+ # endif
+ #endif
+ }
+
+-DEBUG(D_tls) debug_printf("ECDH: curve '%s'\n", exp_curve);
+ if ( (nid = OBJ_sn2nid (CCS exp_curve)) == NID_undef
+ # ifdef EXIM_HAVE_OPENSSL_EC_NIST2NID
+ && (nid = EC_curve_nist2nid(CCS exp_curve)) == NID_undef
+ # endif
+ )
+@@ -777,27 +784,27 @@ if ( (nid = OBJ_sn2nid (CCS exp_c
+ }
+
+ /* The "tmp" in the name here refers to setting a temporary key
+ not to the stability of the interface. */
+
+- if ((rv = SSL_CTX_set_tmp_ecdh(sctx, ecdh) == 0))
++ if ((rc = SSL_CTX_set_tmp_ecdh(sctx, ecdh) == 0))
+ tls_error(string_sprintf("Error enabling '%s' curve", exp_curve), NULL, NULL, errstr);
+ else
+- DEBUG(D_tls) debug_printf("ECDH: enabled '%s' curve\n", exp_curve);
++ DEBUG(D_tls) debug_printf(" ECDH: enabled '%s' curve\n", exp_curve);
+ EC_KEY_free(ecdh);
+ }
+
+ #else /* v 3.0.0 + */
+
+-if ((rv = SSL_CTX_set1_groups(sctx, &nid, 1)) == 0)
++if ((rc = SSL_CTX_set1_groups(sctx, &nid, 1)) == 0)
+ tls_error(string_sprintf("Error enabling '%s' group", exp_curve), NULL, NULL, errstr);
+ else
+- DEBUG(D_tls) debug_printf("ECDH: enabled '%s' group\n", exp_curve);
++ DEBUG(D_tls) debug_printf(" ECDH: enabled '%s' group\n", exp_curve);
+
+ #endif
+
+-return !rv;
++return !!rc;
+
+ # endif /*EXIM_HAVE_ECDH*/
+ #endif /*OPENSSL_NO_ECDH*/
+ }
+
+@@ -1719,19 +1726,19 @@ state_server.lib_state.lib_ctx = ctx;
+
+ /* Preload DH params and EC curve */
+
+ if (opt_unset_or_noexpand(tls_dhparam))
+ {
+- DEBUG(D_tls) debug_printf("TLS: preloading DH params for server\n");
++ DEBUG(D_tls) debug_printf("TLS: preloading DH params '%s' for server\n", tls_dhparam);
+ if (init_dh(ctx, tls_dhparam, &dummy_errstr))
+ state_server.lib_state.dh = TRUE;
+ }
+ else
+ DEBUG(D_tls) debug_printf("TLS: not preloading DH params for server\n");
+ if (opt_unset_or_noexpand(tls_eccurve))
+ {
+- DEBUG(D_tls) debug_printf("TLS: preloading ECDH curve for server\n");
++ DEBUG(D_tls) debug_printf("TLS: preloading ECDH curve '%s' for server\n", tls_eccurve);
+ if (init_ecdh(ctx, &dummy_errstr))
+ state_server.lib_state.ecdh = TRUE;
+ }
+ else
+ DEBUG(D_tls) debug_printf("TLS: not preloading ECDH curve for server\n");
diff --git a/debian/patches/75_62-OpenSSL-Fix-tls_eccurve-on-earlier-versions-than-3.0.patch b/debian/patches/75_62-OpenSSL-Fix-tls_eccurve-on-earlier-versions-than-3.0.patch
new file mode 100644
index 0000000..e346df1
--- /dev/null
+++ b/debian/patches/75_62-OpenSSL-Fix-tls_eccurve-on-earlier-versions-than-3.0.patch
@@ -0,0 +1,42 @@
+From 7fa5764c203f2f4a900898a79ed02d674075313f Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Mon, 2 Jan 2023 15:04:14 +0000
+Subject: [PATCH 1/3] OpenSSL: Fix tls_eccurve on earlier versions than 3.0.0.
+ Bug 2954
+
+Broken-by: ca4014de81e6
+---
+ src/tls-openssl.c | 7 ++++---
+ test/log/2149 | 28 ++++++++++++++--------------
+ test/runtest | 3 +++
+ test/scripts/2100-OpenSSL/2149 | 22 ++++++++++++----------
+ 4 files changed, 33 insertions(+), 27 deletions(-)
+
+diff --git a/src/tls-openssl.c b/src/tls-openssl.c
+index 4d0f99ea9..e063d29bd 100644
+--- a/src/tls-openssl.c
++++ b/src/tls-openssl.c
+@@ -786,8 +786,9 @@ if ( (nid = OBJ_sn2nid (CCS exp_curve)) == NID_undef
+ # endif
+ )
+ {
+- tls_error(string_sprintf("Unknown curve name tls_eccurve '%s'", exp_curve),
+- NULL, NULL, errstr);
++ uschar * s = string_sprintf("Unknown curve name tls_eccurve '%s'", exp_curve);
++ DEBUG(D_tls) debug_printf("TLS error '%s'\n", s);
++ if (errstr) *errstr = s;
+ return FALSE;
+ }
+
+@@ -803,7 +804,7 @@ if ( (nid = OBJ_sn2nid (CCS exp_curve)) == NID_undef
+ /* The "tmp" in the name here refers to setting a temporary key
+ not to the stability of the interface. */
+
+- if ((rc = SSL_CTX_set_tmp_ecdh(sctx, ecdh) == 0))
++ if ((rc = SSL_CTX_set_tmp_ecdh(sctx, ecdh)) == 0)
+ tls_error(string_sprintf("Error enabling '%s' curve", exp_curve), NULL, NULL, errstr);
+ else
+ DEBUG(D_tls) debug_printf(" ECDH: enabled '%s' curve\n", exp_curve);
+--
+2.39.0
+
diff --git a/debian/patches/75_63-OpenSSL-log-conns-rejected-for-bad-ALPN-with-the-off.patch b/debian/patches/75_63-OpenSSL-log-conns-rejected-for-bad-ALPN-with-the-off.patch
new file mode 100644
index 0000000..15b1b8a
--- /dev/null
+++ b/debian/patches/75_63-OpenSSL-log-conns-rejected-for-bad-ALPN-with-the-off.patch
@@ -0,0 +1,99 @@
+From e1aca33756f73c22b00a98d40ce2be8ed94464b1 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Thu, 5 Jan 2023 13:03:37 +0000
+Subject: [PATCH 2/3] OpenSSL: log conns rejected for bad ALPN, with the
+ offered value
+
+Unfortunately, no way to do this under GnuTLS
+---
+ src/match.c | 1 +
+ src/tls-gnu.c | 9 ++++++++-
+ src/tls-openssl.c | 13 +++++++++++--
+ test/log/1190 | 2 ++
+ test/runtest | 3 +++
+ 5 files changed, 25 insertions(+), 3 deletions(-)
+
+diff --git a/src/match.c b/src/match.c
+index 91a49c0f0..07070362d 100644
+--- a/src/match.c
++++ b/src/match.c
+@@ -968,6 +968,7 @@ Arguments:
+ s string to search for
+ listptr ptr to ptr to colon separated list of patterns, or NULL
+ sep a separator value for the list (see string_nextinlist())
++ or zero for auto
+ anchorptr ptr to tree for named items, or NULL if no named items
+ cache_bits ptr to cache_bits for ditto, or NULL if not caching
+ type MCL_DOMAIN when matching a domain list
+diff --git a/src/tls-gnu.c b/src/tls-gnu.c
+index 729fb5879..b47fabf1d 100644
+--- a/src/tls-gnu.c
++++ b/src/tls-gnu.c
+@@ -1119,21 +1119,28 @@ switch (tls_id)
+ /* The format of "data" here doesn't seem to be documented, but appears
+ to be a 2-byte field with a (redundant, given the "size" arg) total length
+ then a sequence of one-byte size then string (not nul-term) names. The
+- latter is as described in OpenSSL documentation. */
++ latter is as described in OpenSSL documentation.
++ Note that we do not get called for a match_fail, making it hard to log
++ a single bad ALPN being offered (the common case). */
++ {
++ gstring * g = NULL;
+
+ DEBUG(D_tls) debug_printf("Seen ALPN extension from client (s=%u):", size);
+ for (const uschar * s = data+2; s-data < size-1; s += *s + 1)
+ {
+ server_seen_alpn++;
++ g = string_append_listele_n(g, ':', s+1, *s);
+ DEBUG(D_tls) debug_printf(" '%.*s'", (int)*s, s+1);
+ }
+ DEBUG(D_tls) debug_printf("\n");
+ if (server_seen_alpn > 1)
+ {
++ log_write(0, LOG_MAIN, "TLS ALPN (%s) rejected", string_from_gstring(g));
+ DEBUG(D_tls) debug_printf("TLS: too many ALPNs presented in handshake\n");
+ return GNUTLS_E_NO_APPLICATION_PROTOCOL;
+ }
+ break;
++ }
+ #endif
+ }
+ return 0;
+diff --git a/src/tls-openssl.c b/src/tls-openssl.c
+index e063d29bd..513ba0d3a 100644
+--- a/src/tls-openssl.c
++++ b/src/tls-openssl.c
+@@ -2324,6 +2324,8 @@ static int
+ tls_server_alpn_cb(SSL *ssl, const uschar ** out, uschar * outlen,
+ const uschar * in, unsigned int inlen, void * arg)
+ {
++gstring * g = NULL;
++
+ server_seen_alpn = TRUE;
+ DEBUG(D_tls)
+ {
+@@ -2354,12 +2356,19 @@ if ( inlen > 1 /* at least one name */
+ }
+ }
+
+-/* More than one name from clilent, or name did not match our list. */
++/* More than one name from client, or name did not match our list. */
+
+ /* This will be fatal to the TLS conn; would be nice to kill TCP also.
+ Maybe as an option in future; for now leave control to the config (must-tls). */
+
+-DEBUG(D_tls) debug_printf("TLS ALPN rejected\n");
++for (int pos = 0, siz; pos < inlen; pos += siz+1)
++ {
++ siz = in[pos];
++ if (pos + 1 + siz > inlen) siz = inlen - pos - 1;
++ g = string_append_listele_n(g, ':', in + pos + 1, siz);
++ }
++log_write(0, LOG_MAIN, "TLS ALPN (%s) rejected", string_from_gstring(g));
++gstring_release_unused(g);
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ #endif /* EXIM_HAVE_ALPN */
+--
+2.39.0
+
diff --git a/debian/patches/75_64-DANE-do-not-check-dns_again_means_nonexist-for-TLSA-.patch b/debian/patches/75_64-DANE-do-not-check-dns_again_means_nonexist-for-TLSA-.patch
new file mode 100644
index 0000000..417ffa4
--- /dev/null
+++ b/debian/patches/75_64-DANE-do-not-check-dns_again_means_nonexist-for-TLSA-.patch
@@ -0,0 +1,96 @@
+From 30520c8f87fcf660ed99a2344cae7f9787f7bc89 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Thu, 5 Jan 2023 18:39:51 +0000
+Subject: [PATCH 3/3] DANE: do not check dns_again_means_nonexist for TLSA
+ results of TRY_AGAIN
+
+---
+ doc/doc-docbook/spec.xfpt | 7 ++++++-
+ doc/ChangeLog | 4 ++++
+ src/dns.c | 35 ++++++++++++++++++++++-------------
+ 3 files changed, 32 insertions(+), 14 deletions(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -46,10 +46,14 @@ JH/19 Bug 2911: Fix a recursion in DNS l
+ JH/20 Bug 2954: (OpenSSL) Fix setting of explicit EC curve/group. Previously
+ this always failed, probably leading to the usual downgrade to in-clear
+ connections.
+
+
++JH/20 Fix TLSA lookups. Previously dns_again_means_nonexist would affect
++ SERVFAIL results, which breaks the downgrade resistance of DANE. Change
++ to not checking that list for these looks.
++
+
+ Exim version 4.96
+ -----------------
+
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
+--- a/src/dns.c
++++ b/src/dns.c
+@@ -904,25 +904,34 @@ if (dnsa->answerlen < 0) switch (h_errno
+ DEBUG(D_dns) debug_printf("DNS lookup of %s (%s) gave TRY_AGAIN\n",
+ name, dns_text_type(type));
+
+ /* Cut this out for various test programs */
+ #ifndef STAND_ALONE
+- if (try_again_recursion)
++ /* Permitting dns_again_means nonexist for TLSA lookups breaks the
++ doewngrade resistance of dane, so avoid for those. */
++
++ if (type == T_TLSA)
++ rc = FAIL;
++ else
+ {
+- log_write(0, LOG_MAIN|LOG_PANIC,
+- "dns_again_means_nonexist recursion seen for %s (assuming nonexist)",
+- name);
+- return dns_fail_return(name, type, dns_expire_from_soa(dnsa, type), DNS_NOMATCH);
+- }
++ if (try_again_recursion)
++ {
++ log_write(0, LOG_MAIN|LOG_PANIC,
++ "dns_again_means_nonexist recursion seen for %s"
++ " (assuming nonexist)", name);
++ return dns_fail_return(name, type, dns_expire_from_soa(dnsa, type),
++ DNS_NOMATCH);
++ }
+
+- try_again_recursion = TRUE;
+- save_domain = deliver_domain;
+- deliver_domain = string_copy(name); /* set $domain */
+- rc = match_isinlist(name, CUSS &dns_again_means_nonexist, 0,
+- &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, NULL);
+- deliver_domain = save_domain;
+- try_again_recursion = FALSE;
++ try_again_recursion = TRUE;
++ save_domain = deliver_domain;
++ deliver_domain = string_copy(name); /* set $domain */
++ rc = match_isinlist(name, CUSS &dns_again_means_nonexist, 0,
++ &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, NULL);
++ deliver_domain = save_domain;
++ try_again_recursion = FALSE;
++ }
+
+ if (rc != OK)
+ {
+ DEBUG(D_dns) debug_printf("returning DNS_AGAIN\n");
+ return dns_fail_return(name, type, 0, DNS_AGAIN);
+--- a/doc/spec.txt
++++ b/doc/spec.txt
+@@ -14246,11 +14246,13 @@ dns_again_means_nonexist, it is treated
+ should be used with care. You can make it apply to reverse lookups by a setting
+ such as this:
+
+ dns_again_means_nonexist = *.in-addr.arpa
+
+-This option applies to all DNS lookups that Exim does. It also applies when the
++This option applies to all DNS lookups that Exim does, except for TLSA lookups
++(where knowing about such failures +is security-relevant). It also applies
++when the
+ gethostbyname() or getipnodebyname() functions give temporary errors, since
+ these are most likely to be caused by DNS lookup problems. The dnslookup router
+ has some options of its own for controlling what happens when lookups for MX or
+ SRV records give temporary errors. These more specific options are applied
+ after this global option.
diff --git a/debian/patches/75_66-Fix-crash-in-expansions.patch b/debian/patches/75_66-Fix-crash-in-expansions.patch
new file mode 100644
index 0000000..d776c8e
--- /dev/null
+++ b/debian/patches/75_66-Fix-crash-in-expansions.patch
@@ -0,0 +1,84 @@
+From 70069b65a39a7ba73a36fbd95371ff03cde1eb23 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Thu, 2 Feb 2023 20:00:35 +0000
+Subject: [PATCH] Fix crash in expansions
+
+Broken-by: 1058096b8c53
+---
+ doc/ChangeLog | 4 ++++
+ src/expand.c | 9 +++++----
+ test/stderr/0630 | 1 +
+ 3 files changed, 10 insertions(+), 4 deletions(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -50,10 +50,14 @@ JH/20 Bug 2954: (OpenSSL) Fix setting of
+
+ JH/20 Fix TLSA lookups. Previously dns_again_means_nonexist would affect
+ SERVFAIL results, which breaks the downgrade resistance of DANE. Change
+ to not checking that list for these looks.
+
++JH/23 Fix crash in string expansions. Previously, if an empty variable was
++ immediately followed by an expansion operator, a null-indirection read
++ was done, killing the process.
++
+
+ Exim version 4.96
+ -----------------
+
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
+--- a/src/expand.c
++++ b/src/expand.c
+@@ -4652,11 +4652,11 @@ while (*s)
+ yield = string_catn(yield, value, len);
+
+ continue;
+ }
+
+- if (isdigit(*s))
++ if (isdigit(*s)) /* A $<n> variable */
+ {
+ int n;
+ s = read_cnumber(&n, s);
+ if (n >= 0 && n <= expand_nmax)
+ yield = string_catn(yield, expand_nstring[n], expand_nlength[n]);
+@@ -7060,10 +7060,11 @@ NOT_ITEM: ;
+ if (arg) *arg++ = '_'; /* Put back for error messages */
+ }
+
+ /* Deal specially with operators that might take a certificate variable
+ as we do not want to do the usual expansion. For most, expand the string.*/
++
+ switch(c)
+ {
+ #ifndef DISABLE_TLS
+ case EOP_MD5:
+ case EOP_SHA1:
+@@ -7107,11 +7108,11 @@ NOT_ITEM: ;
+
+ /* Otherwise, switch on the operator type. After handling go back
+ to the main loop top. */
+
+ {
+- int start = yield->ptr;
++ unsigned expansion_start = gstring_length(yield);
+ switch(c)
+ {
+ case EOP_BASE32:
+ {
+ uschar *t;
+@@ -8168,12 +8169,12 @@ NOT_ITEM: ;
+ goto EXPAND_FAILED;
+ } /* EOP_* switch */
+
+ DEBUG(D_expand)
+ {
+- const uschar * s = yield->s + start;
+- int i = yield->ptr - start;
++ const uschar * s = yield->s + expansion_start;
++ int i = gstring_length(yield) - expansion_start;
+ BOOL tainted = is_tainted(s);
+
+ DEBUG(D_noutf8)
+ {
+ debug_printf_indent("|-----op-res: %.*s\n", i, s);
diff --git a/debian/patches/75_68-Fix-srs_encode-.-for-mod-1024-day-zero.patch b/debian/patches/75_68-Fix-srs_encode-.-for-mod-1024-day-zero.patch
new file mode 100644
index 0000000..dddd5e9
--- /dev/null
+++ b/debian/patches/75_68-Fix-srs_encode-.-for-mod-1024-day-zero.patch
@@ -0,0 +1,62 @@
+From 51f9c07cd341c9c1a09b3816df988c6f44477c99 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Tue, 11 Apr 2023 11:59:08 +0100
+Subject: [PATCH] Fix ${srs_encode ..} for mod-1024 day zero
+
+---
+ doc/ChangeLog | 3 +++
+ src/expand.c | 10 ++++------
+ 2 files changed, 7 insertions(+), 6 deletions(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -54,10 +54,13 @@ JH/20 Fix TLSA lookups. Previously dns_
+
+ JH/23 Fix crash in string expansions. Previously, if an empty variable was
+ immediately followed by an expansion operator, a null-indirection read
+ was done, killing the process.
+
++JH/27 Fix ${srs_encode ..}. Previously it would give a bad result for one day
++ every 1024 days.
++
+
+ Exim version 4.96
+ -----------------
+
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
+--- a/src/expand.c
++++ b/src/expand.c
+@@ -3440,11 +3440,11 @@ switch(cond_type = identify_operator(&s,
+ case 3: return NULL;
+ }
+
+ /* Match the given local_part against the SRS-encoded pattern */
+
+- re = regex_must_compile(US"^(?i)SRS0=([^=]+)=([A-Z2-7]+)=([^=]*)=(.*)$",
++ re = regex_must_compile(US"^(?i)SRS0=([^=]+)=([A-Z2-7]{2})=([^=]*)=(.*)$",
+ TRUE, FALSE);
+ md = pcre2_match_data_create(4+1, pcre_gen_ctx);
+ if (pcre2_match(re, sub[0], PCRE2_ZERO_TERMINATED, 0, PCRE_EOPT,
+ md, pcre_mtc_ctx) < 0)
+ {
+@@ -6957,17 +6957,15 @@ while (*s)
+
+ /* ${base32:${eval:$tod_epoch/86400&0x3ff}}= */
+ {
+ struct timeval now;
+ unsigned long i;
+- gstring * h = NULL;
+
+ gettimeofday(&now, NULL);
+- for (unsigned long i = (now.tv_sec / 86400) & 0x3ff; i; i >>= 5)
+- h = string_catn(h, &base32_chars[i & 0x1f], 1);
+- if (h) while (h->ptr > 0)
+- g = string_catn(g, &h->s[--h->ptr], 1);
++ i = (now.tv_sec / 86400) & 0x3ff;
++ g = string_catn(g, &base32_chars[i >> 5], 1);
++ g = string_catn(g, &base32_chars[i & 0x1f], 1);
+ }
+ g = string_catn(g, US"=", 1);
+
+ /* ${domain:$return_path}=${local_part:$return_path} */
+ {
diff --git a/debian/patches/75_70-Fix-variable-initialisation-in-smtp-transport.-Bug-2.patch b/debian/patches/75_70-Fix-variable-initialisation-in-smtp-transport.-Bug-2.patch
new file mode 100644
index 0000000..4a2e3b5
--- /dev/null
+++ b/debian/patches/75_70-Fix-variable-initialisation-in-smtp-transport.-Bug-2.patch
@@ -0,0 +1,46 @@
+From a8786a66feb3c003c74551399b345b1634cc6739 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Thu, 4 May 2023 15:41:46 +0100
+Subject: [PATCH 1/3] Fix variable initialisation in smtp transport. Bug 2996
+
+---
+ doc/ChangeLog | 8 ++++++++
+ src/transports/smtp.c | 2 +-
+ 2 files changed, 9 insertions(+), 1 deletion(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -61,10 +61,18 @@ JH/23 Fix crash in string expansions. Pr
+ was done, killing the process.
+
+ JH/27 Fix ${srs_encode ..}. Previously it would give a bad result for one day
+ every 1024 days.
+
++JH/28 Bug 2996: Fix a crash in the smtp transport. When finding that the
++ message being considered for delivery was already being handled by
++ another process, and having an SMTP connection already open, the function
++ to close it tried to use an uninitialized variable. This would afftect
++ high-volume sites more, especially when running mailing-list-style loads.
++ Pollution of logs was the major effect, as the other process delivered
++ the message. Found and partly investigated by Graeme Fowler.
++
+
+ Exim version 4.96
+ -----------------
+
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
+--- a/src/transports/smtp.c
++++ b/src/transports/smtp.c
+@@ -4950,11 +4950,11 @@ Returns: nothing
+ void
+ smtp_transport_closedown(transport_instance *tblock)
+ {
+ smtp_transport_options_block * ob = SOB tblock->options_block;
+ client_conn_ctx cctx;
+-smtp_context sx;
++smtp_context sx = {0};
+ uschar buffer[256];
+ uschar inbuffer[4096];
+ uschar outbuffer[16];
+
+ /*XXX really we need an active-smtp-client ctx, rather than assuming stdout */
diff --git a/debian/patches/75_71-Auths-fix-possible-OOB-write-in-external-authenticat.patch b/debian/patches/75_71-Auths-fix-possible-OOB-write-in-external-authenticat.patch
new file mode 100644
index 0000000..f6936a9
--- /dev/null
+++ b/debian/patches/75_71-Auths-fix-possible-OOB-write-in-external-authenticat.patch
@@ -0,0 +1,22 @@
+From 7bb5bc2c6592e062bf0b514cc71afd2d93e2e0dd Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Thu, 11 May 2023 18:02:43 +0100
+Subject: [PATCH 1/4] Auths: fix possible OOB write in external authenticator.
+ Bug 2999
+
+---
+ doc/doc-txt/ChangeLog | 3 +++
+ src/src/auths/external.c | 2 +-
+ 2 files changed, 4 insertions(+), 1 deletion(-)
+
+--- a/src/auths/external.c
++++ b/src/auths/external.c
+@@ -103,7 +103,7 @@ if (expand_nmax == 0) /* skip if rxd da
+ if (ob->server_param2)
+ {
+ uschar * s = expand_string(ob->server_param2);
+- auth_vars[expand_nmax] = s;
++ auth_vars[expand_nmax = 1] = s;
+ expand_nstring[++expand_nmax] = s;
+ expand_nlength[expand_nmax] = Ustrlen(s);
+ if (ob->server_param3)
diff --git a/debian/patches/75_72-Auths-use-uschar-more-in-spa-authenticator.patch b/debian/patches/75_72-Auths-use-uschar-more-in-spa-authenticator.patch
new file mode 100644
index 0000000..03958fc
--- /dev/null
+++ b/debian/patches/75_72-Auths-use-uschar-more-in-spa-authenticator.patch
@@ -0,0 +1,226 @@
+From 0519dcfb5f149154a416b54865fd8026abb57791 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Thu, 11 May 2023 18:53:25 +0100
+Subject: [PATCH 2/4] Auths: use uschar more in spa authenticator
+
+---
+ src/src/auths/auth-spa.c | 72 +++++++++++++++++++++-------------------
+ src/src/auths/auth-spa.h | 8 ++---
+ src/src/auths/spa.c | 13 ++++----
+ 3 files changed, 47 insertions(+), 46 deletions(-)
+
+--- a/src/auths/auth-spa.c
++++ b/src/auths/auth-spa.c
+@@ -155,6 +155,9 @@ int main (int argc, char ** argv)
+ up with a different answer to the one above)
+ */
+
++#ifndef MACRO_PREDEF
++
++
+ #define DEBUG_X(a,b) ;
+
+ extern int DEBUGLEVEL;
+@@ -1229,21 +1232,21 @@ else \
+
+ #define spa_string_add(ptr, header, string) \
+ { \
+-char *p = string; \
++uschar * p = string; \
+ int len = 0; \
+-if (p) len = strlen(p); \
+-spa_bytes_add(ptr, header, (US p), len); \
++if (p) len = Ustrlen(p); \
++spa_bytes_add(ptr, header, p, len); \
+ }
+
+ #define spa_unicode_add_string(ptr, header, string) \
+ { \
+-char *p = string; \
+-uschar *b = NULL; \
++uschar * p = string; \
++uschar * b = NULL; \
+ int len = 0; \
+ if (p) \
+ { \
+- len = strlen(p); \
+- b = strToUnicode(p); \
++ len = Ustrlen(p); \
++ b = US strToUnicode(CS p); \
+ } \
+ spa_bytes_add(ptr, header, b, len*2); \
+ }
+@@ -1366,15 +1369,15 @@ fprintf (fp, " Flags = %08x\n", IVA
+ #endif
+
+ void
+-spa_build_auth_request (SPAAuthRequest * request, char *user, char *domain)
++spa_build_auth_request (SPAAuthRequest * request, uschar * user, uschar * domain)
+ {
+-char *u = strdup (user);
+-char *p = strchr (u, '@');
++uschar * u = string_copy(user);
++uschar * p = Ustrchr(u, '@');
+
+ if (p)
+ {
+ if (!domain)
+- domain = p + 1;
++ domain = p + 1;
+ *p = '\0';
+ }
+
+@@ -1384,7 +1387,6 @@ 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);
+-free (u);
+ }
+
+
+@@ -1475,16 +1477,16 @@ free (u);
+
+ void
+ spa_build_auth_response (SPAAuthChallenge * challenge,
+- SPAAuthResponse * response, char *user,
+- char *password)
++ SPAAuthResponse * response, uschar * user,
++ uschar * password)
+ {
+ uint8x lmRespData[24];
+ uint8x ntRespData[24];
+ uint32x cf = IVAL(&challenge->flags, 0);
+-char *u = strdup (user);
+-char *p = strchr (u, '@');
+-char *d = NULL;
+-char *domain;
++uschar * u = string_copy(user);
++uschar * p = Ustrchr(u, '@');
++uschar * d = NULL;
++uschar * domain;
+
+ if (p)
+ {
+@@ -1492,33 +1494,33 @@ if (p)
+ *p = '\0';
+ }
+
+-else domain = d = strdup((cf & 0x1)?
+- CCS GetUnicodeString(challenge, uDomain) :
+- CCS GetString(challenge, uDomain));
++else domain = d = string_copy(cf & 0x1
++ ? CUS GetUnicodeString(challenge, uDomain)
++ : CUS GetString(challenge, uDomain));
+
+-spa_smb_encrypt (US password, challenge->challengeData, lmRespData);
+-spa_smb_nt_encrypt (US password, challenge->challengeData, ntRespData);
++spa_smb_encrypt(password, challenge->challengeData, lmRespData);
++spa_smb_nt_encrypt(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, (cf & 0x200) ? 24 : 0);
+-spa_bytes_add (response, ntResponse, ntRespData, (cf & 0x8000) ? 24 : 0);
++spa_bytes_add(response, lmResponse, lmRespData, cf & 0x200 ? 24 : 0);
++spa_bytes_add(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);
++ 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);
++ spa_string_add(response, uDomain, domain);
++ spa_string_add(response, uUser, u);
++ spa_string_add(response, uWks, u);
+ }
+
+-spa_string_add (response, sessionKey, NULL);
++spa_string_add(response, sessionKey, NULL);
+ response->flags = challenge->flags;
+-
+-if (d != NULL) free (d);
+-free (u);
+ }
++
++
++#endif /*!MACRO_PREDEF*/
+--- a/src/auths/auth-spa.h
++++ b/src/auths/auth-spa.h
+@@ -79,10 +79,10 @@ typedef struct
+
+ 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, char *user, char *password);
+-void spa_build_auth_request (SPAAuthRequest *request, char *user,
+- char *domain);
++void spa_build_auth_response (SPAAuthChallenge * challenge,
++ SPAAuthResponse * response, uschar * user, uschar * password);
++void spa_build_auth_request (SPAAuthRequest * request, uschar * user,
++ 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,
+--- a/src/auths/spa.c
++++ b/src/auths/spa.c
+@@ -284,14 +284,13 @@ SPAAuthRequest request;
+ SPAAuthChallenge challenge;
+ SPAAuthResponse response;
+ char msgbuf[2048];
+-char *domain = NULL;
+-char *username, *password;
++uschar * domain = NULL, * username, * password;
+
+ /* Code added by PH to expand the options */
+
+ *buffer = 0; /* Default no message when cancelled */
+
+-if (!(username = CS expand_string(ob->spa_username)))
++if (!(username = expand_string(ob->spa_username)))
+ {
+ if (f.expand_string_forcedfail) return CANCELLED;
+ string_format(buffer, buffsize, "expansion of \"%s\" failed in %s "
+@@ -300,7 +299,7 @@ if (!(username = CS expand_string(ob->sp
+ return ERROR;
+ }
+
+-if (!(password = CS expand_string(ob->spa_password)))
++if (!(password = expand_string(ob->spa_password)))
+ {
+ if (f.expand_string_forcedfail) return CANCELLED;
+ string_format(buffer, buffsize, "expansion of \"%s\" failed in %s "
+@@ -310,7 +309,7 @@ if (!(password = CS expand_string(ob->sp
+ }
+
+ if (ob->spa_domain)
+- if (!(domain = CS expand_string(ob->spa_domain)))
++ if (!(domain = expand_string(ob->spa_domain)))
+ {
+ if (f.expand_string_forcedfail) return CANCELLED;
+ string_format(buffer, buffsize, "expansion of \"%s\" failed in %s "
+@@ -330,7 +329,7 @@ if (!smtp_read_response(sx, US buffer, b
+
+ DSPA("\n\n%s authenticator: using domain %s\n\n", ablock->name, domain);
+
+-spa_build_auth_request(&request, CS username, domain);
++spa_build_auth_request(&request, username, domain);
+ spa_bits_to_base64(US msgbuf, US &request, spa_request_length(&request));
+
+ DSPA("\n\n%s authenticator: sending request (%s)\n\n", ablock->name, msgbuf);
+@@ -347,7 +346,7 @@ if (!smtp_read_response(sx, US buffer, b
+ DSPA("\n\n%s authenticator: challenge (%s)\n\n", ablock->name, buffer + 4);
+ spa_base64_to_bits(CS (&challenge), sizeof(challenge), CCS (buffer + 4));
+
+-spa_build_auth_response(&challenge, &response, CS username, CS password);
++spa_build_auth_response(&challenge, &response, username, password);
+ spa_bits_to_base64(US msgbuf, US &response, spa_request_length(&response));
+ DSPA("\n\n%s authenticator: challenge response (%s)\n\n", ablock->name, msgbuf);
+
diff --git a/debian/patches/75_73-Auths-fix-possible-OOB-write-in-SPA-authenticator.-B.patch b/debian/patches/75_73-Auths-fix-possible-OOB-write-in-SPA-authenticator.-B.patch
new file mode 100644
index 0000000..8c763fa
--- /dev/null
+++ b/debian/patches/75_73-Auths-fix-possible-OOB-write-in-SPA-authenticator.-B.patch
@@ -0,0 +1,24 @@
+From e17b8b0f19b25a223b0cc41933b881c3a1073e61 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Thu, 11 May 2023 19:31:54 +0100
+Subject: [PATCH 3/4] Auths: fix possible OOB write in SPA authenticator. Bug
+ 3000
+
+---
+ doc/doc-txt/ChangeLog | 3 +++
+ src/src/auths/auth-spa.c | 4 +++-
+ 2 files changed, 6 insertions(+), 1 deletion(-)
+
+--- a/src/auths/auth-spa.c
++++ b/src/auths/auth-spa.c
+@@ -1214,7 +1214,9 @@ char versionString[] = "libntlm version
+
+ #define spa_bytes_add(ptr, header, buf, count) \
+ { \
+-if (buf && (count) != 0) /* we hate -Wint-in-bool-contex */ \
++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); \
diff --git a/debian/patches/75_74-Auths-fix-possible-OOB-read-in-SPA-authenticator.-Bu.patch b/debian/patches/75_74-Auths-fix-possible-OOB-read-in-SPA-authenticator.-Bu.patch
new file mode 100644
index 0000000..d4e0eb6
--- /dev/null
+++ b/debian/patches/75_74-Auths-fix-possible-OOB-read-in-SPA-authenticator.-Bu.patch
@@ -0,0 +1,75 @@
+From 04107e98d58efb69f7e2d7b81176e5374c7098a3 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Thu, 11 May 2023 21:08:08 +0100
+Subject: [PATCH 4/4] Auths: fix possible OOB read in SPA authenticator. Bug
+ 3001
+
+---
+ doc/doc-txt/ChangeLog | 3 +++
+ src/src/auths/auth-spa.c | 36 ++++++++++++++++++++++++++++--------
+ 2 files changed, 31 insertions(+), 8 deletions(-)
+
+--- a/src/auths/auth-spa.c
++++ b/src/auths/auth-spa.c
+@@ -1254,15 +1254,10 @@ spa_bytes_add(ptr, header, b, len*2); \
+ }
+
+
+-#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))
+-
+ #ifdef notdef
+
+ #define DumpBuffer(fp, structPtr, header) \
+-dumpRaw(fp,(US structPtr)+IVAL(&structPtr->header.offset,0),SVAL(&structPtr->header.len,0))
++ dumpRaw(fp,(US structPtr)+IVAL(&structPtr->header.offset,0),SVAL(&structPtr->header.len,0))
+
+
+ static void
+@@ -1326,8 +1321,33 @@ 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"";
++}
++
++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"";
++}
++
+ #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)
+ {
+@@ -1497,8 +1517,8 @@ if (p)
+ }
+
+ else domain = d = string_copy(cf & 0x1
+- ? CUS GetUnicodeString(challenge, uDomain)
+- : CUS GetString(challenge, uDomain));
++ ? CUS get_challenge_unistr(challenge, &challenge->uDomain)
++ : CUS get_challenge_str(challenge, &challenge->uDomain));
+
+ spa_smb_encrypt(password, challenge->challengeData, lmRespData);
+ spa_smb_nt_encrypt(password, challenge->challengeData, ntRespData);
diff --git a/debian/patches/75_74-Cancel-early-pipe-on-an-observed-advertising-change.patch b/debian/patches/75_74-Cancel-early-pipe-on-an-observed-advertising-change.patch
new file mode 100644
index 0000000..bcec88f
--- /dev/null
+++ b/debian/patches/75_74-Cancel-early-pipe-on-an-observed-advertising-change.patch
@@ -0,0 +1,35 @@
+From 4d108e7777e9b8e5fb212c31812fef61529cd414 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Mon, 12 Jun 2023 22:13:46 +0100
+Subject: [PATCH] Cancel early-pipe on an observed advertising change
+
+---
+ src/transports/smtp.c | 3 +++
+ 1 file changed, 3 insertions(+)
+
+diff --git a/src/transports/smtp.c b/src/transports/smtp.c
+index c72028ce9..24ee577a2 100644
+--- a/src/transports/smtp.c
++++ b/src/transports/smtp.c
+@@ -1111,15 +1111,18 @@ if (pending_EHLO)
+ *(tls_out.active.sock < 0
+ ? &sx->ehlo_resp.cleartext_features : &sx->ehlo_resp.crypted_features) =
+ peer_offered;
+ *ap = authbits;
+ write_ehlo_cache_entry(sx);
+ }
+ else
++ {
+ invalidate_ehlo_cache_entry(sx);
++ sx->early_pipe_active = FALSE; /* cancel further early-pipe on this conn */
++ }
+
+ return OK; /* just carry on */
+ }
+ # ifdef EXPERIMENTAL_ESMTP_LIMITS
+ /* If we are handling LIMITS, compare the actual EHLO LIMITS values with the
+ cached values and invalidate cache if different. OK to carry on with
+ connect since values are advisory. */
+--
+2.40.1
+
diff --git a/debian/patches/75_76-Expansions-disallow-UTF-16-surrogates-from-utf8clean.patch b/debian/patches/75_76-Expansions-disallow-UTF-16-surrogates-from-utf8clean.patch
new file mode 100644
index 0000000..713435b
--- /dev/null
+++ b/debian/patches/75_76-Expansions-disallow-UTF-16-surrogates-from-utf8clean.patch
@@ -0,0 +1,99 @@
+From 1209e3e19e292cee517e43a2ccfe9b44b33bb1dc Mon Sep 17 00:00:00 2001
+From: Jasen Betts <jasen@xnet.co.nz>
+Date: Sun, 23 Jul 2023 13:43:59 +0100
+Subject: [PATCH] Expansions: disallow UTF-16 surrogates from ${utf8clean:...}.
+ Bug 2998
+
+---
+ doc/ChangeLog | 4 ++++
+ src/expand.c | 27 +++++++++++++++++----------
+ 2 files changed, 21 insertions(+), 10 deletions(-)
+
+--- a/src/expand.c
++++ b/src/expand.c
+@@ -7731,11 +7731,11 @@ NOT_ITEM: ;
+
+ case EOP_UTF8CLEAN:
+ {
+ int seq_len = 0, index = 0;
+ int bytes_left = 0;
+- long codepoint = -1;
++ ulong codepoint = (ulong)-1;
+ int complete;
+ uschar seq_buff[4]; /* accumulate utf-8 here */
+
+ /* Manually track tainting, as we deal in individual chars below */
+
+@@ -7761,40 +7761,47 @@ NOT_ITEM: ;
+ codepoint = (codepoint << 6) | (c & 0x3f);
+ seq_buff[index++] = c;
+ if (--bytes_left == 0) /* codepoint complete */
+ if(codepoint > 0x10FFFF) /* is it too large? */
+ complete = -1; /* error (RFC3629 limit) */
++ else if ( (codepoint & 0x1FF800 ) == 0xD800 ) /* surrogate */
++ /* A UTF-16 surrogate (which should be one of a pair that
++ encode a Unicode codepoint that is outside the Basic
++ Multilingual Plane). Error, not UTF8.
++ RFC2279.2 is slightly unclear on this, but
++ https://unicodebook.readthedocs.io/issues.html#strict-utf8-decoder
++ says "Surrogates characters are also invalid in UTF-8:
++ characters in U+D800—U+DFFF have to be rejected." */
++ complete = -1;
+ else
+ { /* finished; output utf-8 sequence */
+ yield = string_catn(yield, seq_buff, seq_len);
+ index = 0;
+ }
+ }
+ }
+ else /* no bytes left: new sequence */
+ {
+- if(!(c & 0x80)) /* 1-byte sequence, US-ASCII, keep it */
++ if (!(c & 0x80)) /* 1-byte sequence, US-ASCII, keep it */
+ {
+ yield = string_catn(yield, &c, 1);
+ continue;
+ }
+- if((c & 0xe0) == 0xc0) /* 2-byte sequence */
+- {
+- if(c == 0xc0 || c == 0xc1) /* 0xc0 and 0xc1 are illegal */
++ if ((c & 0xe0) == 0xc0) /* 2-byte sequence */
++ if (c == 0xc0 || c == 0xc1) /* 0xc0 and 0xc1 are illegal */
+ complete = -1;
+ else
+ {
+- bytes_left = 1;
+- codepoint = c & 0x1f;
++ bytes_left = 1;
++ codepoint = c & 0x1f;
+ }
+- }
+- else if((c & 0xf0) == 0xe0) /* 3-byte sequence */
++ else if ((c & 0xf0) == 0xe0) /* 3-byte sequence */
+ {
+ bytes_left = 2;
+ codepoint = c & 0x0f;
+ }
+- else if((c & 0xf8) == 0xf0) /* 4-byte sequence */
++ else if ((c & 0xf8) == 0xf0) /* 4-byte sequence */
+ {
+ bytes_left = 3;
+ codepoint = c & 0x07;
+ }
+ else /* invalid or too long (RFC3629 allows only 4 bytes) */
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -69,10 +69,13 @@ JH/28 Bug 2996: Fix a crash in the smtp
+ to close it tried to use an uninitialized variable. This would afftect
+ high-volume sites more, especially when running mailing-list-style loads.
+ Pollution of logs was the major effect, as the other process delivered
+ the message. Found and partly investigated by Graeme Fowler.
+
++JH/31 Bug 2998: Fix ${utf8clean:...} to disallow UTF-16 surrogate codepoints.
++ Found and fixed by Jasen Betts. No testcase for this as my usual text
++ editor insists on emitting only valid UTF-8.
+
+ Exim version 4.96
+ -----------------
+
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
diff --git a/debian/patches/75_77-GnuTLS-fix-crash-with-tls_dhparam-none.patch b/debian/patches/75_77-GnuTLS-fix-crash-with-tls_dhparam-none.patch
new file mode 100644
index 0000000..ced0ce9
--- /dev/null
+++ b/debian/patches/75_77-GnuTLS-fix-crash-with-tls_dhparam-none.patch
@@ -0,0 +1,92 @@
+From 8e9770348dc4173ab83657ee023c22f479ebb712 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Mon, 24 Jul 2023 13:30:40 +0100
+Subject: [PATCH] GnuTLS: fix crash with "tls_dhparam = none"
+
+---
+ doc/ChangeLog | 4 ++++
+ src/tls-gnu.c | 16 +++++++++-------
+ test/log/2049 | 7 +++++++
+ test/scripts/2000-GnuTLS/2049 | 8 ++++++++
+ 4 files changed, 28 insertions(+), 7 deletions(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -73,10 +73,14 @@ JH/28 Bug 2996: Fix a crash in the smtp
+
+ JH/31 Bug 2998: Fix ${utf8clean:...} to disallow UTF-16 surrogate codepoints.
+ Found and fixed by Jasen Betts. No testcase for this as my usual text
+ editor insists on emitting only valid UTF-8.
+
++JH/32 Fix "tls_dhparam = none" under GnuTLS. At least with 3.7.9 this gave
++ a null-indireciton SIGSEGV for the receive process.
++
++
+ Exim version 4.96
+ -----------------
+
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
+ after reception to before a subsequent reception. This should
+--- a/src/tls-gnu.c
++++ b/src/tls-gnu.c
+@@ -712,11 +712,11 @@ exist, we generate them. This means that
+ The new file is written as a temporary file and renamed, so that an incomplete
+ file is never present. If two processes both compute some new parameters, you
+ waste a bit of effort, but it doesn't seem worth messing around with locking to
+ prevent this.
+
+-Returns: OK/DEFER/FAIL
++Returns: OK/DEFER (expansion issue)/FAIL (requested none)
+ */
+
+ static int
+ init_server_dh(uschar ** errstr)
+ {
+@@ -750,11 +750,11 @@ if (!exp_tls_dhparam)
+ else if (Ustrcmp(exp_tls_dhparam, "historic") == 0)
+ use_file_in_spool = TRUE;
+ else if (Ustrcmp(exp_tls_dhparam, "none") == 0)
+ {
+ DEBUG(D_tls) debug_printf("Requested no DH parameters\n");
+- return OK;
++ return FAIL;
+ }
+ else if (exp_tls_dhparam[0] != '/')
+ {
+ if (!(m.data = US std_dh_prime_named(exp_tls_dhparam)))
+ return tls_error(US"No standard prime named", exp_tls_dhparam, NULL, errstr);
+@@ -1971,27 +1971,29 @@ Arguments:
+
+ Returns: OK/DEFER/FAIL
+ */
+
+ static int
+-tls_set_remaining_x509(exim_gnutls_state_st *state, uschar ** errstr)
++tls_set_remaining_x509(exim_gnutls_state_st * state, uschar ** errstr)
+ {
+-int rc;
+-const host_item *host = state->host; /* macro should be reconsidered? */
++int rc = OK;
++const host_item * host = state->host; /* macro should be reconsidered? */
+
+ /* Create D-H parameters, or read them from the cache file. This function does
+ its own SMTP error messaging. This only happens for the server, TLS D-H ignores
+ client-side params. */
+
+ if (!state->host)
+ {
+ if (!dh_server_params)
+- if ((rc = init_server_dh(errstr)) != OK) return rc;
++ if ((rc = init_server_dh(errstr)) == DEFER) return rc;
+
+ /* Unnecessary & discouraged with 3.6.0 or later, according to docs. But without it,
+ no DHE- ciphers are advertised. */
+- gnutls_certificate_set_dh_params(state->lib_state.x509_cred, dh_server_params);
++
++ if (rc == OK)
++ gnutls_certificate_set_dh_params(state->lib_state.x509_cred, dh_server_params);
+ }
+
+ /* Link the credentials to the session. */
+
+ if ((rc = gnutls_credentials_set(state->session,
diff --git a/debian/patches/75_79-Fix-recipients-expansion-when-used-within-run.-.-Bug.patch b/debian/patches/75_79-Fix-recipients-expansion-when-used-within-run.-.-Bug.patch
new file mode 100644
index 0000000..1eb9d8f
--- /dev/null
+++ b/debian/patches/75_79-Fix-recipients-expansion-when-used-within-run.-.-Bug.patch
@@ -0,0 +1,294 @@
+From 6707bfa9fb78858de938a1abca2846c820c5ded7 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Thu, 3 Aug 2023 18:40:42 +0100
+Subject: [PATCH 2/2] Fix $recipients expansion when used within ${run...}.
+ Bug 3013
+
+Broken-by: cfe6acff2ddc
+---
+ doc/ChangeLog | 3 +++
+ src/deliver.c | 2 +-
+ src/expand.c | 5 ++---
+ src/functions.h | 2 +-
+ src/macros.h | 5 +++++
+ src/routers/queryprogram.c | 3 +--
+ src/smtp_in.c | 4 ++--
+ src/transport.c | 18 +++++++++---------
+ src/transports/lmtp.c | 4 ++--
+ src/transports/pipe.c | 11 ++++++-----
+ src/transports/smtp.c | 2 +-
+ test/log/0635 | 2 +-
+ 12 files changed, 34 insertions(+), 27 deletions(-)
+
+--- a/src/deliver.c
++++ b/src/deliver.c
+@@ -2382,11 +2382,11 @@ if ((pid = exim_fork(US"delivery-local")
+
+ if (tp->filter_command)
+ {
+ ok = transport_set_up_command(&transport_filter_argv,
+ tp->filter_command,
+- TRUE, PANIC, addr, FALSE, US"transport filter", NULL);
++ TSUC_EXPAND_ARGS, PANIC, addr, US"transport filter", NULL);
+ transport_filter_timeout = tp->filter_timeout;
+ }
+ else transport_filter_argv = NULL;
+
+ if (ok)
+--- a/src/expand.c
++++ b/src/expand.c
+@@ -5527,11 +5527,11 @@ while (*s)
+
+ case EITEM_RUN:
+ {
+ FILE * f;
+ const uschar * arg, ** argv;
+- BOOL late_expand = TRUE;
++ unsigned late_expand = TSUC_EXPAND_ARGS | TSUC_ALLOW_TAINTED_ARGS | TSUC_ALLOW_RECIPIENTS;
+
+ if (expand_forbid & RDO_RUN)
+ {
+ expand_string_message = US"running a command is not permitted";
+ goto EXPAND_FAILED;
+@@ -5540,11 +5540,11 @@ while (*s)
+ /* Handle options to the "run" */
+
+ while (*s == ',')
+ {
+ if (Ustrncmp(++s, "preexpand", 9) == 0)
+- { late_expand = FALSE; s += 9; }
++ { late_expand = 0; s += 9; }
+ else
+ {
+ const uschar * t = s;
+ while (isalpha(*++t)) ;
+ expand_string_message = string_sprintf("bad option '%.*s' for run",
+@@ -5599,11 +5599,10 @@ while (*s)
+ if (!transport_set_up_command(&argv, /* anchor for arg list */
+ arg, /* raw command */
+ late_expand, /* expand args if not already done */
+ 0, /* not relevant when... */
+ NULL, /* no transporting address */
+- late_expand, /* allow tainted args, when expand-after-split */
+ US"${run} expansion", /* for error messages */
+ &expand_string_message)) /* where to put error message */
+ goto EXPAND_FAILED;
+
+ /* Create the child process, making it a group leader. */
+--- a/src/functions.h
++++ b/src/functions.h
+@@ -616,11 +616,11 @@ extern BOOL transport_pass_socket(con
+ , unsigned, unsigned, unsigned
+ #endif
+ );
+ extern uschar *transport_rcpt_address(address_item *, BOOL);
+ extern BOOL transport_set_up_command(const uschar ***, const uschar *,
+- BOOL, int, address_item *, BOOL, const uschar *, uschar **);
++ unsigned, int, address_item *, const uschar *, uschar **);
+ extern void transport_update_waiting(host_item *, uschar *);
+ extern BOOL transport_write_block(transport_ctx *, uschar *, int, BOOL);
+ extern void transport_write_reset(int);
+ extern BOOL transport_write_string(int, const char *, ...);
+ extern BOOL transport_headers_send(transport_ctx *,
+--- a/src/macros.h
++++ b/src/macros.h
+@@ -1112,6 +1112,11 @@ should not be one active. */
+
+ #define NOTIFIER_SOCKET_NAME "exim_daemon_notify"
+ #define NOTIFY_MSG_QRUN 1 /* Notify message types */
+ #define NOTIFY_QUEUE_SIZE_REQ 2
+
++/* Flags for transport_set_up_command() */
++#define TSUC_EXPAND_ARGS BIT(0)
++#define TSUC_ALLOW_TAINTED_ARGS BIT(1)
++#define TSUC_ALLOW_RECIPIENTS BIT(2)
++
+ /* End of macros.h */
+--- a/src/routers/queryprogram.c
++++ b/src/routers/queryprogram.c
+@@ -286,14 +286,13 @@ if (curr_uid != root_uid && (uid != curr
+
+ /* Set up the command to run */
+
+ if (!transport_set_up_command(&argvptr, /* anchor for arg list */
+ ob->command, /* raw command */
+- TRUE, /* expand the arguments */
++ TSUC_EXPAND_ARGS, /* arguments expanded but must not be tainted */
+ 0, /* not relevant when... */
+ NULL, /* no transporting address */
+- FALSE, /* args must be untainted */
+ US"queryprogram router", /* for error messages */
+ &addr->message)) /* where to put error message */
+ return DEFER;
+
+ /* Create the child process, making it a group leader. */
+--- a/src/smtp_in.c
++++ b/src/smtp_in.c
+@@ -5840,12 +5840,12 @@ while (done <= 0)
+ {
+ uschar *error;
+ BOOL rc;
+ etrn_command = smtp_etrn_command;
+ deliver_domain = smtp_cmd_data;
+- rc = transport_set_up_command(&argv, smtp_etrn_command, TRUE, 0, NULL,
+- FALSE, US"ETRN processing", &error);
++ rc = transport_set_up_command(&argv, smtp_etrn_command, TSUC_EXPAND_ARGS, 0, NULL,
++ US"ETRN processing", &error);
+ deliver_domain = NULL;
+ if (!rc)
+ {
+ log_write(0, LOG_MAIN|LOG_PANIC, "failed to set up ETRN command: %s",
+ error);
+--- a/src/transport.c
++++ b/src/transport.c
+@@ -2082,34 +2082,34 @@ return FALSE;
+ * Set up direct (non-shell) command *
+ *************************************************/
+
+ /* This function is called when a command line is to be parsed and executed
+ directly, without the use of /bin/sh. It is called by the pipe transport,
+-the queryprogram router, and also from the main delivery code when setting up a
++the queryprogram router, for any ${run } expansion,
++and also from the main delivery code when setting up a
+ transport filter process. The code for ETRN also makes use of this; in that
+ case, no addresses are passed.
+
+ Arguments:
+ argvptr pointer to anchor for argv vector
+ cmd points to the command string (modified IN PLACE)
+- expand_arguments true if expansion is to occur
++ flags bits for expand-args, allow taint, allow $recipients
+ expand_failed error value to set if expansion fails; not relevant if
+ addr == NULL
+ addr chain of addresses, or NULL
+- allow_tainted_args as it says; used for ${run}
+ etext text for use in error messages
+ errptr where to put error message if addr is NULL;
+ otherwise it is put in the first address
+
+ Returns: TRUE if all went well; otherwise an error will be
+ set in the first address and FALSE returned
+ */
+
+ BOOL
+ transport_set_up_command(const uschar *** argvptr, const uschar * cmd,
+- BOOL expand_arguments, int expand_failed, address_item * addr,
+- BOOL allow_tainted_args, const uschar * etext, uschar ** errptr)
++ unsigned flags, int expand_failed, address_item * addr,
++ const uschar * etext, uschar ** errptr)
+ {
+ const uschar ** argv, * s;
+ int address_count = 0, argcount = 0, max_args;
+
+ /* Get store in which to build an argument list. Count the number of addresses
+@@ -2180,14 +2180,14 @@ DEBUG(D_transport)
+ debug_printf("direct command:\n");
+ for (int i = 0; argv[i]; i++)
+ debug_printf(" argv[%d] = '%s'\n", i, string_printing(argv[i]));
+ }
+
+-if (expand_arguments)
++if (flags & TSUC_EXPAND_ARGS)
+ {
+- BOOL allow_dollar_recipients = addr && addr->parent
+- && Ustrcmp(addr->parent->address, "system-filter") == 0;
++ BOOL allow_dollar_recipients = (flags & TSUC_ALLOW_RECIPIENTS)
++ || (addr && addr->parent && Ustrcmp(addr->parent->address, "system-filter") == 0); /*XXX could we check this at caller? */
+
+ for (int i = 0; argv[i]; i++)
+ {
+ DEBUG(D_expand) debug_printf_indent("arg %d\n", i);
+
+@@ -2370,11 +2370,11 @@ if (expand_arguments)
+ { /* hack, would be good to not need it */
+ DEBUG(D_transport)
+ debug_printf("SPECIFIC TESTSUITE EXEMPTION: tainted arg '%s'\n",
+ expanded_arg);
+ }
+- else if ( !allow_tainted_args
++ else if ( !(flags & TSUC_ALLOW_TAINTED_ARGS)
+ && arg_is_tainted(expanded_arg, i, addr, etext, errptr))
+ return FALSE;
+ argv[i] = expanded_arg;
+ }
+ }
+--- a/src/transports/lmtp.c
++++ b/src/transports/lmtp.c
+@@ -487,12 +487,12 @@ argument list and expanding the items. *
+
+ if (ob->cmd)
+ {
+ DEBUG(D_transport) debug_printf("using command %s\n", ob->cmd);
+ sprintf(CS buffer, "%.50s transport", tblock->name);
+- if (!transport_set_up_command(&argv, ob->cmd, TRUE, PANIC, addrlist, FALSE,
+- buffer, NULL))
++ if (!transport_set_up_command(&argv, ob->cmd, TSUC_EXPAND_ARGS, PANIC,
++ addrlist, buffer, NULL))
+ return FALSE;
+
+ /* If the -N option is set, can't do any more. Presume all has gone well. */
+ if (f.dont_deliver)
+ goto MINUS_N;
+--- a/src/transports/pipe.c
++++ b/src/transports/pipe.c
+@@ -289,24 +289,25 @@ Arguments:
+ Returns: TRUE if all went well; otherwise an error will be
+ set in the first address and FALSE returned
+ */
+
+ static BOOL
+-set_up_direct_command(const uschar ***argvptr, uschar *cmd,
+- BOOL expand_arguments, int expand_fail, address_item *addr, uschar *tname,
+- pipe_transport_options_block *ob)
++set_up_direct_command(const uschar *** argvptr, uschar * cmd,
++ BOOL expand_arguments, int expand_fail, address_item * addr, uschar * tname,
++ pipe_transport_options_block * ob)
+ {
+ BOOL permitted = FALSE;
+ const uschar **argv;
+
+ /* Set up "transport <name>" to be put in any error messages, and then
+ call the common function for creating an argument list and expanding
+ the items if necessary. If it fails, this function fails (error information
+ is in the addresses). */
+
+-if (!transport_set_up_command(argvptr, cmd, expand_arguments, expand_fail,
+- addr, FALSE, string_sprintf("%.50s transport", tname), NULL))
++if (!transport_set_up_command(argvptr, cmd,
++ expand_arguments ? TSUC_EXPAND_ARGS : 0,
++ expand_fail, addr, string_sprintf("%.50s transport", tname), NULL))
+ return FALSE;
+
+ /* Point to the set-up arguments. */
+
+ argv = *argvptr;
+--- a/src/transports/smtp.c
++++ b/src/transports/smtp.c
+@@ -3803,11 +3803,11 @@ if (tblock->filter_command)
+
+ /* On failure, copy the error to all addresses, abandon the SMTP call, and
+ yield ERROR. */
+
+ if (!transport_set_up_command(&transport_filter_argv,
+- tblock->filter_command, TRUE, DEFER, addrlist, FALSE,
++ tblock->filter_command, TSUC_EXPAND_ARGS, DEFER, addrlist,
+ string_sprintf("%.50s transport filter", tblock->name), NULL))
+ {
+ set_errno_nohost(addrlist->next, addrlist->basic_errno, addrlist->message, DEFER,
+ FALSE, &sx->delivery_start);
+ yield = ERROR;
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -76,10 +76,12 @@ JH/31 Bug 2998: Fix ${utf8clean:...} to
+ editor insists on emitting only valid UTF-8.
+
+ JH/32 Fix "tls_dhparam = none" under GnuTLS. At least with 3.7.9 this gave
+ a null-indireciton SIGSEGV for the receive process.
+
++JH/34 Bug 3013: Fix use of $recipients within arguments for ${run...}.
++ In 4.96 this would expand to empty.
+
+ Exim version 4.96
+ -----------------
+
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
diff --git a/debian/patches/75_82-GnuTLS-fix-autogen-cert-expiry-date.-Bug-3014.patch b/debian/patches/75_82-GnuTLS-fix-autogen-cert-expiry-date.-Bug-3014.patch
new file mode 100644
index 0000000..cdf062e
--- /dev/null
+++ b/debian/patches/75_82-GnuTLS-fix-autogen-cert-expiry-date.-Bug-3014.patch
@@ -0,0 +1,42 @@
+From 36bc854c86908ee921225c1d30e35c4d59eed822 Mon Sep 17 00:00:00 2001
+From: Andreas Metzler <ametzler@bebt.de>
+Date: Mon, 14 Aug 2023 17:27:16 +0100
+Subject: [PATCH] GnuTLS: fix autogen cert expiry date. Bug 3014
+
+Broken-by: 48e9099006
+---
+ doc/ChangeLog | 3 +++
+ src/tls-gnu.c | 2 +-
+ 2 files changed, 4 insertions(+), 1 deletion(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -79,10 +79,13 @@ JH/32 Fix "tls_dhparam = none" under Gnu
+ a null-indireciton SIGSEGV for the receive process.
+
+ JH/34 Bug 3013: Fix use of $recipients within arguments for ${run...}.
+ In 4.96 this would expand to empty.
+
++JH/35 Bug 3014: GnuTLS: fix expiry date for an auto-generated server
++ certificate. Find and fix by Andreas Metzler.
++
+ Exim version 4.96
+ -----------------
+
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
+ after reception to before a subsequent reception. This should
+--- a/src/tls-gnu.c
++++ b/src/tls-gnu.c
+@@ -1001,11 +1001,11 @@ if ((rc = gnutls_x509_privkey_generate(p
+ where = US"configuring cert";
+ now = 1;
+ if ( (rc = gnutls_x509_crt_set_version(cert, 3))
+ || (rc = gnutls_x509_crt_set_serial(cert, &now, sizeof(now)))
+ || (rc = gnutls_x509_crt_set_activation_time(cert, now = time(NULL)))
+- || (rc = gnutls_x509_crt_set_expiration_time(cert, (long)2 * 60 * 60)) /* 2 hour */
++ || (rc = gnutls_x509_crt_set_expiration_time(cert, now + (long)2 * 60 * 60)) /* 2 hour */
+ || (rc = gnutls_x509_crt_set_key(cert, pkey))
+
+ || (rc = gnutls_x509_crt_set_dn_by_oid(cert,
+ GNUTLS_OID_X520_COUNTRY_NAME, 0, "UK", 2))
+ || (rc = gnutls_x509_crt_set_dn_by_oid(cert,
diff --git a/debian/patches/75_83-Re-fix-live-variable-value-free.-The-inital-fix-resu.patch b/debian/patches/75_83-Re-fix-live-variable-value-free.-The-inital-fix-resu.patch
new file mode 100644
index 0000000..0cfc69e
--- /dev/null
+++ b/debian/patches/75_83-Re-fix-live-variable-value-free.-The-inital-fix-resu.patch
@@ -0,0 +1,77 @@
+From 21b172df101c2c52faf0cc56a502395451975be9 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Thu, 24 Aug 2023 15:51:21 +0100
+Subject: [PATCH 2/2] Re-fix live variable $value free. The inital fix
+ resulted in $value from ${run...} not being available later, which is a
+ documented feature.
+
+Broken=by: cf3fecb9e873
+---
+ doc/doc-docbook/spec.xfpt | 1 +
+ doc/ChangeLog | 4 ++--
+ src/exim.c | 3 ++-
+ test/confs/0635 | 1 +
+ test/log/0635 | 1 +
+ test/mail/0635.CALLER | 13 +++++++++++++
+ 6 files changed, 20 insertions(+), 3 deletions(-)
+ create mode 100644 test/mail/0635.CALLER
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -76,10 +76,13 @@ JH/31 Bug 2998: Fix ${utf8clean:...} to
+ editor insists on emitting only valid UTF-8.
+
+ JH/32 Fix "tls_dhparam = none" under GnuTLS. At least with 3.7.9 this gave
+ a null-indireciton SIGSEGV for the receive process.
+
++JH/33 Fix free for live variable $value created by a ${run ...} expansion during
++ -bh use. Internal checking would spot this and take a panic.
++
+ JH/34 Bug 3013: Fix use of $recipients within arguments for ${run...}.
+ In 4.96 this would expand to empty.
+
+ JH/35 Bug 3014: GnuTLS: fix expiry date for an auto-generated server
+ certificate. Find and fix by Andreas Metzler.
+--- a/src/exim.c
++++ b/src/exim.c
+@@ -5754,11 +5754,11 @@ for (BOOL more = TRUE; more; )
+ for (int i = 0; i < count; i++)
+ {
+ int start, end, domain;
+ uschar * errmess;
+ /* There can be multiple addresses, so EXIM_DISPLAYMAIL_MAX (tuned for 1) is too short.
+- * We'll still want to cap it to something, just in case. */
++ We'll still want to cap it to something, just in case. */
+ uschar * s = string_copy_taint(
+ exim_str_fail_toolong(list[i], BIG_BUFFER_SIZE, "address argument"),
+ GET_TAINTED);
+
+ /* Loop for each comma-separated address */
+@@ -6089,10 +6089,11 @@ MORELOOP:
+ callout_address = NULL;
+ sending_ip_address = NULL;
+ deliver_localpart_data = deliver_domain_data =
+ recipient_data = sender_data = NULL;
+ acl_var_m = NULL;
++ lookup_value = NULL; /* Can be set by ACL */
+
+ store_reset(reset_point);
+ }
+
+ exim_exit(EXIT_SUCCESS); /* Never returns */
+--- a/doc/spec.txt
++++ b/doc/spec.txt
+@@ -9650,10 +9650,13 @@ ${run <options> {<command arg list>}{<st
+ If the command requires shell idioms, such as the > redirect operator, the
+ shell must be invoked directly, such as with:
+
+ ${run{/bin/bash -c "/usr/bin/id >/tmp/id"}{yes}{yes}}
+
++ Note that $value will not persist beyond the reception of a single
++ message.
++
+ The return code from the command is put in the variable $runrc, and this
+ remains set afterwards, so in a filter file you can do things like this:
+
+ if "${run{x y z}{}}$runrc" is 1 then ...
+ elif $runrc is 2 then ...
diff --git a/debian/patches/76-01-fix-string_is_ip_address-CVE-2023-42117-Bug-3031.patch b/debian/patches/76-01-fix-string_is_ip_address-CVE-2023-42117-Bug-3031.patch
new file mode 100644
index 0000000..cbddde0
--- /dev/null
+++ b/debian/patches/76-01-fix-string_is_ip_address-CVE-2023-42117-Bug-3031.patch
@@ -0,0 +1,309 @@
+From a95acb1c19c2e3600ef327c71318e33316d34440 Mon Sep 17 00:00:00 2001
+From: "Heiko Schlittermann (HS12-RIPE)" <hs@schlittermann.de>
+Date: Thu, 5 Oct 2023 22:49:57 +0200
+Subject: [PATCH 1/3] fix: string_is_ip_address (CVE-2023-42117) Bug 3031
+
+---
+ doc/ChangeLog | 206 ++++++++++++++++++++++++++++++++++++++++++
+ src/expand.c | 14 ++-
+ src/functions.h | 1 +
+ src/string.c | 200 +++++++++++++++++++++-------------------
+ 4 files changed, 323 insertions(+), 98 deletions(-)
+
+diff --git a/src/expand.c b/src/expand.c
+index 36c9f423b..4986e4657 100644
+--- a/src/expand.c
++++ b/src/expand.c
+@@ -2646,17 +2646,25 @@ switch(cond_type = identify_operator(&s, &opname))
+ }
+ *yield = (Ustat(sub[0], &statbuf) == 0) == testfor;
+ break;
+
+ case ECOND_ISIP:
+ case ECOND_ISIP4:
+ case ECOND_ISIP6:
+- rc = string_is_ip_address(sub[0], NULL);
+- *yield = ((cond_type == ECOND_ISIP)? (rc != 0) :
+- (cond_type == ECOND_ISIP4)? (rc == 4) : (rc == 6)) == testfor;
++ {
++ const uschar *errp;
++ const uschar **errpp;
++ DEBUG(D_expand) errpp = &errp; else errpp = 0;
++ if (0 == (rc = string_is_ip_addressX(sub[0], NULL, errpp)))
++ DEBUG(D_expand) debug_printf("failed: %s\n", errp);
++
++ *yield = ( cond_type == ECOND_ISIP ? rc != 0 :
++ cond_type == ECOND_ISIP4 ? rc == 4 : rc == 6) == testfor;
++ }
++
+ break;
+
+ /* Various authentication tests - all optionally compiled */
+
+ case ECOND_PAM:
+ #ifdef SUPPORT_PAM
+ rc = auth_call_pam(sub[0], &expand_string_message);
+diff --git a/src/functions.h b/src/functions.h
+index 224666cb1..3c8104d25 100644
+--- a/src/functions.h
++++ b/src/functions.h
+@@ -552,14 +552,15 @@ extern gstring *string_catn(gstring *, const uschar *, int) WARN_UNUSED_RESULT;
+ extern int string_compare_by_pointer(const void *, const void *);
+ extern uschar *string_copy_dnsdomain(uschar *);
+ extern uschar *string_copy_malloc(const uschar *);
+ extern uschar *string_dequote(const uschar **);
+ extern uschar *string_format_size(int, uschar *);
+ extern int string_interpret_escape(const uschar **);
+ extern int string_is_ip_address(const uschar *, int *);
++extern int string_is_ip_addressX(const uschar *, int *, const uschar **);
+ #ifdef SUPPORT_I18N
+ extern BOOL string_is_utf8(const uschar *);
+ #endif
+ extern const uschar *string_printing2(const uschar *, int);
+ extern uschar *string_split_message(uschar *);
+ extern uschar *string_unprinting(uschar *);
+ #ifdef SUPPORT_I18N
+diff --git a/src/string.c b/src/string.c
+index a5161bb31..9aefc2b58 100644
+--- a/src/string.c
++++ b/src/string.c
+@@ -25,131 +25,141 @@ address (assuming HAVE_IPV6 is set). If a mask is permitted and one is present,
+ and maskptr is not NULL, its offset is placed there.
+
+ Arguments:
+ s a string
+ maskptr NULL if no mask is permitted to follow
+ otherwise, points to an int where the offset of '/' is placed
+ if there is no / followed by trailing digits, *maskptr is set 0
++ errp NULL if no diagnostic information is required, and if the netmask
++ length should not be checked. Otherwise it is set pointing to a short
++ descriptive text.
+
+ Returns: 0 if the string is not a textual representation of an IP address
+ 4 if it is an IPv4 address
+ 6 if it is an IPv6 address
+-*/
+
++The legacy string_is_ip_address() function follows below.
++*/
+ int
+-string_is_ip_address(const uschar *s, int *maskptr)
+-{
+-int yield = 4;
++string_is_ip_addressX(const uschar *ip_addr, int *maskptr, const uschar **errp) {
++ struct addrinfo hints;
++ struct addrinfo *res;
+
+-/* If an optional mask is permitted, check for it. If found, pass back the
+-offset. */
++ uschar *slash, *percent;
+
+-if (maskptr)
++ uschar *endp = 0;
++ long int mask = 0;
++ const uschar *addr = 0;
++
++ /* If there is a slash, but we didn't request a (optional) netmask,
++ we return failure, as we do if the mask isn't a pure numerical value,
++ or if it is negative. The actual length is checked later, once we know
++ the address family. */
++ if (slash = Ustrchr(ip_addr, '/'))
+ {
+- const uschar *ss = s + Ustrlen(s);
+- *maskptr = 0;
+- if (s != ss && isdigit(*(--ss)))
++ if (!maskptr)
+ {
+- while (ss > s && isdigit(ss[-1])) ss--;
+- if (ss > s && *(--ss) == '/') *maskptr = ss - s;
++ if (errp) *errp = "netmask found, but not requested";
++ return 0;
+ }
+- }
+-
+-/* A colon anywhere in the string => IPv6 address */
+-
+-if (Ustrchr(s, ':') != NULL)
+- {
+- BOOL had_double_colon = FALSE;
+- BOOL v4end = FALSE;
+-
+- yield = 6;
+-
+- /* An IPv6 address must start with hex digit or double colon. A single
+- colon is invalid. */
+-
+- if (*s == ':' && *(++s) != ':') return 0;
+-
+- /* Now read up to 8 components consisting of up to 4 hex digits each. There
+- may be one and only one appearance of double colon, which implies any number
+- of binary zero bits. The number of preceding components is held in count. */
+
+- for (int count = 0; count < 8; count++)
++ uschar *rest;
++ mask = Ustrtol(slash+1, &rest, 10);
++ if (*rest || mask < 0)
+ {
+- /* If the end of the string is reached before reading 8 components, the
+- address is valid provided a double colon has been read. This also applies
+- if we hit the / that introduces a mask or the % that introduces the
+- interface specifier (scope id) of a link-local address. */
+-
+- if (*s == 0 || *s == '%' || *s == '/') return had_double_colon ? yield : 0;
+-
+- /* If a component starts with an additional colon, we have hit a double
+- colon. This is permitted to appear once only, and counts as at least
+- one component. The final component may be of this form. */
+-
+- if (*s == ':')
+- {
+- if (had_double_colon) return 0;
+- had_double_colon = TRUE;
+- s++;
+- continue;
+- }
+-
+- /* If the remainder of the string contains a dot but no colons, we
+- can expect a trailing IPv4 address. This is valid if either there has
+- been no double-colon and this is the 7th component (with the IPv4 address
+- being the 7th & 8th components), OR if there has been a double-colon
+- and fewer than 6 components. */
+-
+- if (Ustrchr(s, ':') == NULL && Ustrchr(s, '.') != NULL)
+- {
+- if ((!had_double_colon && count != 6) ||
+- (had_double_colon && count > 6)) return 0;
+- v4end = TRUE;
+- yield = 6;
+- break;
+- }
+-
+- /* Check for at least one and not more than 4 hex digits for this
+- component. */
+-
+- if (!isxdigit(*s++)) return 0;
+- if (isxdigit(*s) && isxdigit(*(++s)) && isxdigit(*(++s))) s++;
+-
+- /* If the component is terminated by colon and there is more to
+- follow, skip over the colon. If there is no more to follow the address is
+- invalid. */
+-
+- if (*s == ':' && *(++s) == 0) return 0;
++ if (errp) *errp = "netmask not numeric or <0";
++ return 0;
+ }
+
+- /* If about to handle a trailing IPv4 address, drop through. Otherwise
+- all is well if we are at the end of the string or at the mask or at a percent
+- sign, which introduces the interface specifier (scope id) of a link local
+- address. */
++ *maskptr = slash - ip_addr; /* offset of the slash */
++ endp = slash;
++ } else if (maskptr) *maskptr = 0; /* no slash found */
+
+- if (!v4end)
+- return (*s == 0 || *s == '%' ||
+- (*s == '/' && maskptr != NULL && *maskptr != 0))? yield : 0;
++ /* The interface-ID suffix (%<id>) is optional (for IPv6). If it
++ exists, we check it syntactically. Later, if we know the address
++ family is IPv4, we might reject it.
++ The interface-ID is mutually exclusive with the netmask, to the
++ best of my knowledge. */
++ if (percent = Ustrchr(ip_addr, '%'))
++ {
++ if (slash)
++ {
++ if (errp) *errp = "interface-ID and netmask are mutually exclusive";
++ return 0;
++ }
++ for (uschar *p = percent+1; *p; p++)
++ if (!isalnum(*p) && !ispunct(*p))
++ {
++ if (errp) *errp = "interface-ID must match [[:alnum:][:punct:]]";
++ return 0;
++ }
++ endp = percent;
+ }
+
+-/* Test for IPv4 address, which may be the tail-end of an IPv6 address. */
+-
+-for (int i = 0; i < 4; i++)
++ /* inet_pton() can't parse netmasks and interface IDs, so work on a shortened copy
++ allocated on the current stack */
++ if (endp) {
++ ptrdiff_t l = endp - ip_addr;
++ if (l > 255)
++ {
++ if (errp) *errp = "rudiculous long ip address string";
++ return 0;
++ }
++ addr = alloca(l+1); /* *BSD does not have strndupa() */
++ Ustrncpy((uschar *)addr, ip_addr, l);
++ ((uschar*)addr)[l] = '\0';
++ } else addr = ip_addr;
++
++ int af;
++ union { /* we do not need this, but inet_pton() needs a place for storage */
++ struct in_addr sa4;
++ struct in6_addr sa6;
++ } sa;
++
++ af = Ustrchr(addr, ':') ? AF_INET6 : AF_INET;
++ if (!inet_pton(af, addr, &sa))
+ {
+- long n;
+- uschar * end;
+-
+- if (i != 0 && *s++ != '.') return 0;
+- n = strtol(CCS s, CSS &end, 10);
+- if (n > 255 || n < 0 || end <= s || end > s+3) return 0;
+- s = end;
++ if (errp) *errp = af == AF_INET6 ? "IP address string not parsable as IPv6"
++ : "IP address string not parsable IPv4";
++ return 0;
+ }
++ /* we do not check the values of the mask here, as
++ this is done on the callers side (but I don't understand why), so
++ actually I'd like to do it here, but it breaks at least 0002 */
++ switch (af)
++ {
++ case AF_INET6:
++ if (errp && mask > 128)
++ {
++ *errp = "IPv6 netmask value must not be >128";
++ return 0;
++ }
++ return 6;
++ case AF_INET:
++ if (percent)
++ {
++ if (errp) *errp = "IPv4 address string must not have an interface-ID";
++ return 0;
++ }
++ if (errp && mask > 32) {
++ *errp = "IPv4 netmask value must not be >32";
++ return 0;
++ }
++ return 4;
++ default:
++ if (errp) *errp = "unknown address family (should not happen)";
++ return 0;
++ }
++}
+
+-return !*s || (*s == '/' && maskptr && *maskptr != 0) ? yield : 0;
++int
++string_is_ip_address(const uschar *ip_addr, int *maskptr) {
++ return string_is_ip_addressX(ip_addr, maskptr, 0);
+ }
++
+ #endif /* COMPILE_UTILITY */
+
+
+ /*************************************************
+ * Format message size *
+ *************************************************/
+
+--
+2.42.0
+
diff --git a/debian/patches/76-02-SPF-harden-against-crafted-DNS-responses.patch b/debian/patches/76-02-SPF-harden-against-crafted-DNS-responses.patch
new file mode 100644
index 0000000..b11ef14
--- /dev/null
+++ b/debian/patches/76-02-SPF-harden-against-crafted-DNS-responses.patch
@@ -0,0 +1,64 @@
+From 654056e44fc93a0ee7c09d1228933e8af6862206 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Tue, 10 Oct 2023 12:45:27 +0100
+Subject: [PATCH 2/3] SPF: harden against crafted DNS responses
+
+(cherry picked from commit 4f07f38374f8662c318699fb30432273ffcfe0d3)
+---
+ src/spf.c | 9 +++++++--
+ 1 file changed, 7 insertions(+), 2 deletions(-)
+
+diff --git a/src/spf.c b/src/spf.c
+index db6eea3a8..1981d81b6 100644
+--- a/src/spf.c
++++ b/src/spf.c
+@@ -116,14 +116,15 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
+ {
+ const uschar * s = rr->data;
+
+ srr.ttl = rr->ttl;
+ switch(rr_type)
+ {
+ case T_MX:
++ if (rr->size < 2) continue;
+ s += 2; /* skip the MX precedence field */
+ case T_PTR:
+ {
+ uschar * buf = store_malloc(256);
+ (void)dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, s,
+ (DN_EXPAND_ARG4_TYPE)buf, 256);
+ s = buf;
+@@ -131,24 +132,28 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
+ }
+
+ case T_TXT:
+ {
+ gstring * g = NULL;
+ uschar chunk_len;
+
++ if (rr->size < 1+6) continue; /* min for version str */
+ if (strncmpic(rr->data+1, US SPF_VER_STR, 6) != 0)
+ {
+ HDEBUG(D_host_lookup) debug_printf("not an spf record: %.*s\n",
+ (int) s[0], s+1);
+ continue;
+ }
+
+- for (int off = 0; off < rr->size; off += chunk_len)
++ /* require 1 byte for the chunk_len */
++ for (int off = 0; off < rr->size - 1; off += chunk_len)
+ {
+- if (!(chunk_len = s[off++])) break;
++ if ( !(chunk_len = s[off++])
++ || rr->size < off + chunk_len /* ignore bogus size chunks */
++ ) break;
+ g = string_catn(g, s+off, chunk_len);
+ }
+ if (!g)
+ continue;
+ gstring_release_unused(g);
+ s = string_copy_malloc(string_from_gstring(g));
+ DEBUG(D_receive) debug_printf("SPF_dns_exim_lookup '%s'\n", s);
+--
+2.42.0
+
diff --git a/debian/patches/76-03-Harden-dnsdb-against-crafted-DNS-responses.-Bug-3033.patch b/debian/patches/76-03-Harden-dnsdb-against-crafted-DNS-responses.-Bug-3033.patch
new file mode 100644
index 0000000..b748661
--- /dev/null
+++ b/debian/patches/76-03-Harden-dnsdb-against-crafted-DNS-responses.-Bug-3033.patch
@@ -0,0 +1,244 @@
+From f6b1f8e7d642f82d830a71b78699a4349e0158e1 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Tue, 10 Oct 2023 23:03:28 +0100
+Subject: [PATCH 3/3] Harden dnsdb against crafted DNS responses. Bug 3033
+
+(cherry picked from commit 8787c8994f07c23c3664d76926e02f07314d699d)
+---
+ doc/ChangeLog | 3 ++
+ src/dns.c | 11 +++---
+ src/lookups/dnsdb.c | 78 +++++++++++++++++++++++++++--------------
+ 3 files changed, 59 insertions(+), 33 deletions(-)
+
+diff --git a/src/dns.c b/src/dns.c
+index 7d7ee0c04..8dc3695a1 100644
+--- a/src/dns.c
++++ b/src/dns.c
+@@ -300,15 +300,15 @@ return string_from_gstring(g);
+
+ /* Increment the aptr in dnss, checking against dnsa length.
+ Return: TRUE for a bad result
+ */
+ static BOOL
+ dnss_inc_aptr(const dns_answer * dnsa, dns_scan * dnss, unsigned delta)
+ {
+-return (dnss->aptr += delta) >= dnsa->answer + dnsa->answerlen;
++return (dnss->aptr += delta) > dnsa->answer + dnsa->answerlen;
+ }
+
+ /*************************************************
+ * Get next DNS record from answer block *
+ *************************************************/
+
+ /* Call this with reset == RESET_ANSWERS to scan the answer block, reset ==
+@@ -384,15 +384,15 @@ if (reset != RESET_NEXT)
+ namelen = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen,
+ dnss->aptr, (DN_EXPAND_ARG4_TYPE) &dnss->srr.name, DNS_MAXNAME);
+ if (namelen < 0) goto null_return;
+ /* skip name, type, class & TTL */
+ TRACE trace = "A-hdr";
+ if (dnss_inc_aptr(dnsa, dnss, namelen+8)) goto null_return;
+ GETSHORT(dnss->srr.size, dnss->aptr); /* size of data portion */
+- /* skip over it */
++ /* skip over it, checking for a bogus size */
+ TRACE trace = "A-skip";
+ if (dnss_inc_aptr(dnsa, dnss, dnss->srr.size)) goto null_return;
+ }
+ dnss->rrcount = reset == RESET_AUTHORITY
+ ? ntohs(h->nscount) : ntohs(h->arcount);
+ TRACE debug_printf("%s: reset (%s rrcount %d)\n", __FUNCTION__,
+ reset == RESET_AUTHORITY ? "NS" : "AR", dnss->rrcount);
+@@ -424,18 +424,17 @@ if (dnss_inc_aptr(dnsa, dnss, namelen)) goto null_return;
+ GETSHORT(dnss->srr.type, dnss->aptr); /* Record type */
+ TRACE trace = "R-class";
+ if (dnss_inc_aptr(dnsa, dnss, 2)) goto null_return; /* Don't want class */
+ GETLONG(dnss->srr.ttl, dnss->aptr); /* TTL */
+ GETSHORT(dnss->srr.size, dnss->aptr); /* Size of data portion */
+ dnss->srr.data = dnss->aptr; /* The record's data follows */
+
+-/* Unchecked increment ok here since no further access on this iteration;
+-will be checked on next at "R-name". */
+-
+-dnss->aptr += dnss->srr.size; /* Advance to next RR */
++/* skip over it, checking for a bogus size */
++if (dnss_inc_aptr(dnsa, dnss, dnss->srr.size))
++ goto null_return;
+
+ /* Return a pointer to the dns_record structure within the dns_answer. This is
+ for convenience so that the scans can use nice-looking for loops. */
+
+ TRACE debug_printf("%s: return %s\n", __FUNCTION__, dns_text_type(dnss->srr.type));
+ return &dnss->srr;
+
+diff --git a/src/lookups/dnsdb.c b/src/lookups/dnsdb.c
+index 355be1b5d..020dc9a52 100644
+--- a/src/lookups/dnsdb.c
++++ b/src/lookups/dnsdb.c
+@@ -394,80 +394,101 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
+ /* Other kinds of record just have one piece of data each, but there may be
+ several of them, of course. */
+
+ if (yield->ptr) yield = string_catn(yield, outsep, 1);
+
+ if (type == T_TXT || type == T_SPF)
+ {
+- if (outsep2 == NULL) /* output only the first item of data */
+- yield = string_catn(yield, US (rr->data+1), (rr->data)[0]);
++ if (!outsep2) /* output only the first item of data */
++ {
++ uschar n = (rr->data)[0];
++ /* size byte + data bytes must not excced the RRs length */
++ if (n + 1 <= rr->size)
++ yield = string_catn(yield, US (rr->data+1), n);
++ }
+ else
+ {
+ /* output all items */
+ int data_offset = 0;
+ while (data_offset < rr->size)
+ {
+- uschar chunk_len = (rr->data)[data_offset++];
+- if (outsep2[0] != '\0' && data_offset != 1)
++ uschar chunk_len = (rr->data)[data_offset];
++ int remain = rr->size - data_offset;
++
++ /* Apparently there are resolvers that do not check RRs before passing
++ them on, and glibc fails to do so. So every application must...
++ Check for chunk len exceeding RR */
++
++ if (chunk_len > remain)
++ chunk_len = remain;
++
++ if (*outsep2 && data_offset != 0)
+ yield = string_catn(yield, outsep2, 1);
+- yield = string_catn(yield, US ((rr->data)+data_offset), chunk_len);
++ yield = string_catn(yield, US ((rr->data) + ++data_offset), --chunk_len);
+ data_offset += chunk_len;
+ }
+ }
+ }
+ else if (type == T_TLSA)
+- {
+- uint8_t usage, selector, matching_type;
+- uint16_t payload_length;
+- uschar s[MAX_TLSA_EXPANDED_SIZE];
+- uschar * sp = s;
+- uschar * p = US rr->data;
++ if (rr->size < 3)
++ continue;
++ else
++ {
++ uint8_t usage, selector, matching_type;
++ uint16_t payload_length;
++ uschar s[MAX_TLSA_EXPANDED_SIZE];
++ uschar * sp = s;
++ uschar * p = US rr->data;
++
++ usage = *p++;
++ selector = *p++;
++ matching_type = *p++;
++ /* What's left after removing the first 3 bytes above */
++ payload_length = rr->size - 3;
++ sp += sprintf(CS s, "%d%c%d%c%d%c", usage, *outsep2,
++ selector, *outsep2, matching_type, *outsep2);
++ /* Now append the cert/identifier, one hex char at a time */
++ while (payload_length-- > 0 && sp-s < (MAX_TLSA_EXPANDED_SIZE - 4))
++ sp += sprintf(CS sp, "%02x", *p++);
+
+- usage = *p++;
+- selector = *p++;
+- matching_type = *p++;
+- /* What's left after removing the first 3 bytes above */
+- payload_length = rr->size - 3;
+- sp += sprintf(CS s, "%d%c%d%c%d%c", usage, *outsep2,
+- selector, *outsep2, matching_type, *outsep2);
+- /* Now append the cert/identifier, one hex char at a time */
+- while (payload_length-- > 0 && sp-s < (MAX_TLSA_EXPANDED_SIZE - 4))
+- sp += sprintf(CS sp, "%02x", *p++);
+-
+- yield = string_cat(yield, s);
+- }
++ yield = string_cat(yield, s);
++ }
+ else /* T_CNAME, T_CSA, T_MX, T_MXH, T_NS, T_PTR, T_SOA, T_SRV */
+ {
+ int priority, weight, port;
+ uschar s[264];
+ uschar * p = US rr->data;
+
+ switch (type)
+ {
+ case T_MXH:
++ if (rr->size < sizeof(u_int16_t)) continue;
+ /* mxh ignores the priority number and includes only the hostnames */
+ GETSHORT(priority, p);
+ break;
+
+ case T_MX:
++ if (rr->size < sizeof(u_int16_t)) continue;
+ GETSHORT(priority, p);
+ sprintf(CS s, "%d%c", priority, *outsep2);
+ yield = string_cat(yield, s);
+ break;
+
+ case T_SRV:
++ if (rr->size < 3*sizeof(u_int16_t)) continue;
+ GETSHORT(priority, p);
+ GETSHORT(weight, p);
+ GETSHORT(port, p);
+ sprintf(CS s, "%d%c%d%c%d%c", priority, *outsep2,
+ weight, *outsep2, port, *outsep2);
+ yield = string_cat(yield, s);
+ break;
+
+ case T_CSA:
++ if (rr->size < 3*sizeof(u_int16_t)) continue;
+ /* See acl_verify_csa() for more comments about CSA. */
+ GETSHORT(priority, p);
+ GETSHORT(weight, p);
+ GETSHORT(port, p);
+
+ if (priority != 1) continue; /* CSA version must be 1 */
+
+@@ -510,15 +531,15 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
+ "domain=%s", dns_text_type(type), domain);
+ break;
+ }
+ else yield = string_cat(yield, s);
+
+ if (type == T_SOA && outsep2 != NULL)
+ {
+- unsigned long serial, refresh, retry, expire, minimum;
++ unsigned long serial = 0, refresh = 0, retry = 0, expire = 0, minimum = 0;
+
+ p += rc;
+ yield = string_catn(yield, outsep2, 1);
+
+ rc = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, p,
+ (DN_EXPAND_ARG4_TYPE)s, sizeof(s));
+ if (rc < 0)
+@@ -526,16 +547,19 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
+ log_write(0, LOG_MAIN, "responsible-mailbox truncated: type=%s "
+ "domain=%s", dns_text_type(type), domain);
+ break;
+ }
+ else yield = string_cat(yield, s);
+
+ p += rc;
+- GETLONG(serial, p); GETLONG(refresh, p);
+- GETLONG(retry, p); GETLONG(expire, p); GETLONG(minimum, p);
++ if (rr->size >= p - rr->data - 5*sizeof(u_int32_t))
++ {
++ GETLONG(serial, p); GETLONG(refresh, p);
++ GETLONG(retry, p); GETLONG(expire, p); GETLONG(minimum, p);
++ }
+ sprintf(CS s, "%c%lu%c%lu%c%lu%c%lu%c%lu",
+ *outsep2, serial, *outsep2, refresh,
+ *outsep2, retry, *outsep2, expire, *outsep2, minimum);
+ yield = string_cat(yield, s);
+ }
+ }
+ } /* Loop for list of returned records */
+--
+2.42.0
+
diff --git a/debian/patches/76-10-Fix-tr.-and-empty-strings.-Bug-3023.patch b/debian/patches/76-10-Fix-tr.-and-empty-strings.-Bug-3023.patch
new file mode 100644
index 0000000..f297336
--- /dev/null
+++ b/debian/patches/76-10-Fix-tr.-and-empty-strings.-Bug-3023.patch
@@ -0,0 +1,56 @@
+From 0f8814a1d8db65d1815a6a544a08fb2b6b9207ed Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Mon, 11 Sep 2023 15:50:35 +0100
+Subject: [PATCH] Fix ${tr...} and empty-strings. Bug 3023
+
+(cherry picked from commit b015574531cf18b2126edb9da5a99dad659207dd)
+---
+ doc/ChangeLog | 3 +++
+ src/expand.c | 9 ++++-----
+ test/scripts/0000-Basic/0002 | 1 +
+ test/stdout/0002 | 1 +
+ 4 files changed, 9 insertions(+), 5 deletions(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -85,10 +85,13 @@ JH/34 Bug 3013: Fix use of $recipients w
+ In 4.96 this would expand to empty.
+
+ JH/35 Bug 3014: GnuTLS: fix expiry date for an auto-generated server
+ certificate. Find and fix by Andreas Metzler.
+
++JH/39 Bug 3023: Fix crash induced by some combinations of zero-length strings
++ and ${tr...}. Found and diagnosed by Heiko Schlichting.
++
+ Exim version 4.96
+ -----------------
+
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
+ after reception to before a subsequent reception. This should
+--- a/src/expand.c
++++ b/src/expand.c
+@@ -5696,20 +5696,19 @@ while (*s)
+ case 1: goto EXPAND_FAILED_CURLY;
+ case 2:
+ case 3: goto EXPAND_FAILED;
+ }
+
+- yield = string_cat(yield, sub[0]);
+- o2m = Ustrlen(sub[2]) - 1;
+-
+- if (o2m >= 0) for (; oldptr < yield->ptr; oldptr++)
++ if ( (yield = string_cat(yield, sub[0]))
++ && (o2m = Ustrlen(sub[2]) - 1) >= 0)
++ for (; oldptr < yield->ptr; oldptr++)
+ {
+ uschar *m = Ustrrchr(sub[1], yield->s[oldptr]);
+ if (m)
+ {
+ int o = m - sub[1];
+- yield->s[oldptr] = sub[2][(o < o2m)? o : o2m];
++ yield->s[oldptr] = sub[2][o < o2m ? o : o2m];
+ }
+ }
+
+ if (skipping) continue;
+ break;
diff --git a/debian/patches/76-12-DNS-more-hardening-against-crafted-responses.patch b/debian/patches/76-12-DNS-more-hardening-against-crafted-responses.patch
new file mode 100644
index 0000000..150c92c
--- /dev/null
+++ b/debian/patches/76-12-DNS-more-hardening-against-crafted-responses.patch
@@ -0,0 +1,219 @@
+From b94ea1bd61485a97c2d0dc2cab4c4d86ffe82e89 Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Sun, 15 Oct 2023 12:15:06 +0100
+Subject: [PATCH] DNS: more hardening against crafted responses
+
+---
+ src/acl.c | 1 +
+ src/dns.c | 39 ++++++++++++++++++++++++++++++---------
+ src/functions.h | 16 ++++++++++++++++
+ src/host.c | 3 +++
+ src/lookups/dnsdb.c | 10 +++++-----
+ 5 files changed, 55 insertions(+), 14 deletions(-)
+
+diff --git a/src/acl.c b/src/acl.c
+index 118e4b35d..302dedaeb 100644
+--- a/src/acl.c
++++ b/src/acl.c
+@@ -1434,6 +1434,7 @@ for (rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS);
+
+ /* Extract the numerical SRV fields (p is incremented) */
+
++ if (rr_bad_size(rr, 3 * sizeof(uint16_t))) continue;
+ GETSHORT(priority, p);
+ GETSHORT(weight, p);
+ GETSHORT(port, p);
+diff --git a/src/dns.c b/src/dns.c
+index db566f2e8..1347deec8 100644
+--- a/src/dns.c
++++ b/src/dns.c
+@@ -299,13 +299,23 @@ return string_from_gstring(g);
+
+
+
++/* Check a pointer for being past the end of a dns answer.
++Exactly one past the end is defined as ok.
++Return TRUE iff bad.
++*/
++static BOOL
++dnsa_bad_ptr(const dns_answer * dnsa, const uschar * ptr)
++{
++return ptr > dnsa->answer + dnsa->answerlen;
++}
++
+ /* Increment the aptr in dnss, checking against dnsa length.
+ Return: TRUE for a bad result
+ */
+ static BOOL
+ dnss_inc_aptr(const dns_answer * dnsa, dns_scan * dnss, unsigned delta)
+ {
+-return (dnss->aptr += delta) > dnsa->answer + dnsa->answerlen;
++return dnsa_bad_ptr(dnsa, dnss->aptr += delta);
+ }
+
+ /*************************************************
+@@ -385,10 +395,14 @@ if (reset != RESET_NEXT)
+ namelen = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen,
+ dnss->aptr, (DN_EXPAND_ARG4_TYPE) &dnss->srr.name, DNS_MAXNAME);
+ if (namelen < 0) goto null_return;
++
+ /* skip name, type, class & TTL */
+ TRACE trace = "A-hdr";
+ if (dnss_inc_aptr(dnsa, dnss, namelen+8)) goto null_return;
++
++ if (dnsa_bad_ptr(dnsa, dnss->aptr + sizeof(uint16_t))) goto null_return;
+ GETSHORT(dnss->srr.size, dnss->aptr); /* size of data portion */
++
+ /* skip over it, checking for a bogus size */
+ TRACE trace = "A-skip";
+ if (dnss_inc_aptr(dnsa, dnss, dnss->srr.size)) goto null_return;
+@@ -422,12 +436,18 @@ from the following bytes. */
+ TRACE trace = "R-name";
+ if (dnss_inc_aptr(dnsa, dnss, namelen)) goto null_return;
+
+-GETSHORT(dnss->srr.type, dnss->aptr); /* Record type */
++/* Check space for type, class, TTL & data-size-word */
++if (dnsa_bad_ptr(dnsa, dnss->aptr + 3 * sizeof(uint16_t) + sizeof(uint32_t)))
++ goto null_return;
++
++GETSHORT(dnss->srr.type, dnss->aptr); /* Record type */
++
+ TRACE trace = "R-class";
+-if (dnss_inc_aptr(dnsa, dnss, 2)) goto null_return; /* Don't want class */
+-GETLONG(dnss->srr.ttl, dnss->aptr); /* TTL */
+-GETSHORT(dnss->srr.size, dnss->aptr); /* Size of data portion */
+-dnss->srr.data = dnss->aptr; /* The record's data follows */
++(void) dnss_inc_aptr(dnsa, dnss, sizeof(uint16_t)); /* skip class */
++
++GETLONG(dnss->srr.ttl, dnss->aptr); /* TTL */
++GETSHORT(dnss->srr.size, dnss->aptr); /* Size of data portion */
++dnss->srr.data = dnss->aptr; /* The record's data follows */
+
+ /* skip over it, checking for a bogus size */
+ if (dnss_inc_aptr(dnsa, dnss, dnss->srr.size))
+@@ -743,17 +763,17 @@ if (fake_dnsa_len_for_fail(dnsa, type))
+ /* Skip the mname & rname strings */
+
+ if ((len = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen,
+- p, (DN_EXPAND_ARG4_TYPE)discard_buf, 256)) < 0)
++ p, (DN_EXPAND_ARG4_TYPE)discard_buf, sizeof(discard_buf))) < 0)
+ break;
+ p += len;
+ if ((len = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen,
+- p, (DN_EXPAND_ARG4_TYPE)discard_buf, 256)) < 0)
++ p, (DN_EXPAND_ARG4_TYPE)discard_buf, sizeof(discard_buf))) < 0)
+ break;
+ p += len;
+
+ /* Skip the SOA serial, refresh, retry & expire. Grab the TTL */
+
+- if (p > dnsa->answer + dnsa->answerlen - 5 * INT32SZ)
++ if (dnsa_bad_ptr(dnsa, p + 5 * INT32SZ))
+ break;
+ p += 4 * INT32SZ;
+ GETLONG(ttl, p);
+@@ -1257,6 +1277,7 @@ switch (type)
+ const uschar * p = rr->data;
+
+ /* Extract the numerical SRV fields (p is incremented) */
++ if (rr_bad_size(rr, 3 * sizeof(uint16_t))) continue;
+ GETSHORT(priority, p);
+ GETSHORT(dummy_weight, p);
+ GETSHORT(port, p);
+diff --git a/src/functions.h b/src/functions.h
+index 8f85165e7..39119ca09 100644
+--- a/src/functions.h
++++ b/src/functions.h
+@@ -1110,6 +1110,22 @@ store_free_dns_answer_trc(dns_answer * dnsa, const uschar * func, unsigned line)
+ store_free_3(dnsa, CCS func, line);
+ }
+
++
++/* Check for an RR being large enough. Return TRUE iff bad. */
++static inline BOOL
++rr_bad_size(const dns_record * rr, size_t minbytes)
++{
++return rr->size < minbytes;
++}
++
++/* Check for an RR having further data beyond a given pointer.
++Return TRUE iff bad. */
++static inline BOOL
++rr_bad_increment(const dns_record * rr, const uschar * ptr, size_t minbytes)
++{
++return rr_bad_size(rr, ptr - rr->data + minbytes);
++}
++
+ /******************************************************************************/
+ /* Routines with knowledge of spool layout */
+
+diff --git a/src/host.c b/src/host.c
+index 3e5a88660..ce7ca2bab 100644
+--- a/src/host.c
++++ b/src/host.c
+@@ -2725,6 +2725,7 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS);
+ const uschar * s = rr->data; /* MUST be unsigned for GETSHORT */
+ uschar data[256];
+
++ if (rr_bad_size(rr, sizeof(uint16_t))) continue;
+ GETSHORT(precedence, s); /* Pointer s is advanced */
+
+ /* For MX records, we use a random "weight" which causes multiple records of
+@@ -2737,6 +2738,8 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS);
+ /* SRV records are specified with a port and a weight. The weight is used
+ in a special algorithm. However, to start with, we just use it to order the
+ records of equal priority (precedence). */
++
++ if (rr_bad_increment(rr, s, 2 * sizeof(uint16_t))) continue;
+ GETSHORT(weight, s);
+ GETSHORT(port, s);
+ }
+diff --git a/src/lookups/dnsdb.c b/src/lookups/dnsdb.c
+index 35a946447..fcf80e3dd 100644
+--- a/src/lookups/dnsdb.c
++++ b/src/lookups/dnsdb.c
+@@ -452,20 +452,20 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
+ switch (type)
+ {
+ case T_MXH:
+- if (rr->size < sizeof(u_int16_t)) continue;
++ if (rr_bad_size(rr, sizeof(u_int16_t))) continue;
+ /* mxh ignores the priority number and includes only the hostnames */
+ GETSHORT(priority, p);
+ break;
+
+ case T_MX:
+- if (rr->size < sizeof(u_int16_t)) continue;
++ if (rr_bad_size(rr, sizeof(u_int16_t))) continue;
+ GETSHORT(priority, p);
+ sprintf(CS s, "%d%c", priority, *outsep2);
+ yield = string_cat(yield, s);
+ break;
+
+ case T_SRV:
+- if (rr->size < 3*sizeof(u_int16_t)) continue;
++ if (rr_bad_size(rr, 3*sizeof(u_int16_t))) continue;
+ GETSHORT(priority, p);
+ GETSHORT(weight, p);
+ GETSHORT(port, p);
+@@ -475,7 +475,7 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
+ break;
+
+ case T_CSA:
+- if (rr->size < 3*sizeof(u_int16_t)) continue;
++ if (rr_bad_size(rr, 3*sizeof(u_int16_t))) continue;
+ /* See acl_verify_csa() for more comments about CSA. */
+ GETSHORT(priority, p);
+ GETSHORT(weight, p);
+@@ -542,7 +542,7 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
+ else yield = string_cat(yield, s);
+
+ p += rc;
+- if (rr->size >= p - rr->data - 5*sizeof(u_int32_t))
++ if (!rr_bad_increment(rr, p, 5 * sizeof(u_int32_t)))
+ {
+ GETLONG(serial, p); GETLONG(refresh, p);
+ GETLONG(retry, p); GETLONG(expire, p); GETLONG(minimum, p);
+--
+2.42.0
+
diff --git a/debian/patches/76-14-Lookups-Fix-dnsdb-lookup-of-multi-chunk-TXT.-Bug-305.patch b/debian/patches/76-14-Lookups-Fix-dnsdb-lookup-of-multi-chunk-TXT.-Bug-305.patch
new file mode 100644
index 0000000..7b9eb6e
--- /dev/null
+++ b/debian/patches/76-14-Lookups-Fix-dnsdb-lookup-of-multi-chunk-TXT.-Bug-305.patch
@@ -0,0 +1,118 @@
+From 79670d3c32ccb37fe06f25d8192943b58606a32a Mon Sep 17 00:00:00 2001
+From: Jeremy Harris <jgh146exb@wizmail.org>
+Date: Fri, 17 Nov 2023 16:55:17 +0000
+Subject: [PATCH] Lookups: Fix dnsdb lookup of multi-chunk TXT. Bug 3054
+
+Broken=by: f6b1f8e7d642
+---
+ doc/ChangeLog | 6 ++++-
+ src/dns.c | 4 +++-
+ src/lookups/dnsdb.c | 45 +++++++++++++++---------------------
+ test/dnszones-src/db.test.ex | 1 +
+ test/scripts/2200-dnsdb/2200 | 1 +
+ test/src/fakens.c | 1 +
+ test/stdout/2200 | 1 +
+ 7 files changed, 31 insertions(+), 28 deletions(-)
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -87,10 +87,13 @@ JH/34 Bug 3013: Fix use of $recipients w
+ JH/35 Bug 3014: GnuTLS: fix expiry date for an auto-generated server
+ certificate. Find and fix by Andreas Metzler.
+
+ JH/39 Bug 3023: Fix crash induced by some combinations of zero-length strings
+ and ${tr...}. Found and diagnosed by Heiko Schlichting.
++
++JH/06 Bug 3054: Fix dnsdb lookup for a TXT record with multiple chunks. This
++ was broken by hardening introduced for Bug 3033.
+
+ Exim version 4.96
+ -----------------
+
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
+--- a/src/dns.c
++++ b/src/dns.c
+@@ -428,11 +428,13 @@ TRACE trace = "R-namelen";
+ namelen = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, dnss->aptr,
+ (DN_EXPAND_ARG4_TYPE) &dnss->srr.name, DNS_MAXNAME);
+ if (namelen < 0) goto null_return;
+
+ /* Move the pointer past the name and fill in the rest of the data structure
+-from the following bytes. */
++from the following bytes. We seem to be assuming here that the RR blob passed
++to us by the resolver library is the same as that defined for an RR by RFC 1035
++section 3.2.1 */
+
+ TRACE trace = "R-name";
+ if (dnss_inc_aptr(dnsa, dnss, namelen)) goto null_return;
+
+ /* Check space for type, class, TTL & data-size-word */
+--- a/src/lookups/dnsdb.c
++++ b/src/lookups/dnsdb.c
+@@ -390,46 +390,36 @@ while ((domain = string_nextinlist(&keys
+ }
+ continue;
+ }
+
+ /* Other kinds of record just have one piece of data each, but there may be
+- several of them, of course. */
++ several of them, of course. TXT & SPF can have data in multiple chunks. */
+
+ if (yield->ptr) yield = string_catn(yield, outsep, 1);
+
+ if (type == T_TXT || type == T_SPF)
+- {
+- if (!outsep2) /* output only the first item of data */
++ for (unsigned data_offset = 0; data_offset + 1 < rr->size; )
+ {
+- uschar n = (rr->data)[0];
+- /* size byte + data bytes must not excced the RRs length */
+- if (n + 1 <= rr->size)
+- yield = string_catn(yield, US (rr->data+1), n);
++ uschar chunk_len = (rr->data)[data_offset];
++ int remain;
++
++ if (outsep2 && *outsep2 && data_offset != 0)
++ yield = string_catn(yield, outsep2, 1);
++
++ /* Apparently there are resolvers that do not check RRs before passing
++ them on, and glibc fails to do so. So every application must...
++ Check for chunk len exceeding RR */
++
++ remain = rr->size - ++data_offset;
++ if (chunk_len > remain)
++ chunk_len = remain;
++ yield = string_catn(yield, US ((rr->data) + data_offset), chunk_len);
++ data_offset += chunk_len;
++
++ if (!outsep2) break; /* output only the first chunk of the RR */
++
+ }
+- else
+- {
+- /* output all items */
+- int data_offset = 0;
+- while (data_offset < rr->size)
+- {
+- uschar chunk_len = (rr->data)[data_offset];
+- int remain = rr->size - data_offset;
+-
+- /* Apparently there are resolvers that do not check RRs before passing
+- them on, and glibc fails to do so. So every application must...
+- Check for chunk len exceeding RR */
+-
+- if (chunk_len > remain)
+- chunk_len = remain;
+-
+- if (*outsep2 && data_offset != 0)
+- yield = string_catn(yield, outsep2, 1);
+- yield = string_catn(yield, US ((rr->data) + ++data_offset), --chunk_len);
+- data_offset += chunk_len;
+- }
+- }
+- }
+ else if (type == T_TLSA)
+ if (rr->size < 3)
+ continue;
+ else
+ {
diff --git a/debian/patches/77_CVE-2023-51766_4.97.1-release.diff b/debian/patches/77_CVE-2023-51766_4.97.1-release.diff
new file mode 100644
index 0000000..77d52ce
--- /dev/null
+++ b/debian/patches/77_CVE-2023-51766_4.97.1-release.diff
@@ -0,0 +1,440 @@
+Description: Fix smtp-smuggling (CVE-2023-51766)
+ Pull upstream changes from 4.97.1 security release.
+Author: Jeremy Harris <jgh146exb@wizmail.org>
+Bug-Debian: https://bugs.debian.org/1059387
+Origin: upstream
+Last-Update: 2023-12-31
+
+--- a/doc/ChangeLog
++++ b/doc/ChangeLog
+@@ -91,10 +91,16 @@ JH/39 Bug 3023: Fix crash induced by som
+ and ${tr...}. Found and diagnosed by Heiko Schlichting.
+
+ JH/06 Bug 3054: Fix dnsdb lookup for a TXT record with multiple chunks. This
+ was broken by hardening introduced for Bug 3033.
+
++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).
++
++
+ Exim version 4.96
+ -----------------
+
+ JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
+ after reception to before a subsequent reception. This should
+--- /dev/null
++++ b/doc/doc-txt/cve-2023-51766
+@@ -0,0 +1,69 @@
++CVE ID: CVE-2023-51766
++Date: 2016-12-15
++Credits: https://sec-consult.com/blog/detail/smtp-smuggling-spoofing-e-mails-worldwide/
++Version(s): all up to 4.97 inclusive
++Issue: Given a buggy relay, Exim can be induced to accept a second message embedded
++ as part of the body of a first message
++
++Conditions
++==========
++
++If *all* the following conditions are met
++
++ Runtime options
++ ---------------
++
++ * Exim offers PIPELINING on incoming connections
++
++ * Exim offers CHUNKING on incoming connections
++
++ Operation
++ ---------
++
++ * DATA (as opposed to BDAT) is used for a message reception
++
++ * The relay host sends to the Exim MTA message data including
++ one of "LF . LF" or "CR LF . LF" or "LF . CR LF".
++
++ * Exim interprets the sequence as signalling the end of data for
++ the SMTP DATA command, and hence a first message.
++
++ * Exim interprets further input which the relay had as message body
++ data, as SMTP commands and data. This could include a MAIL, RCPT,
++ BDAT (etc) sequence, resulting in a further message acceptance.
++
++Impact
++======
++
++One or more messages can be accepted by Exim that have not been
++properly validated by the buggy relay.
++
++Fix
++===
++
++Install a fixed Exim version:
++
++ 4.98 (once available)
++ 4.97.1
++
++If you can't install one of the above versions, ask your package
++maintainer for a version containing the backported fix. On request and
++depending on our resources we will support you in backporting the fix.
++(Please note, that Exim project officially doesn't support versions
++prior the current stable version.)
++
++
++Workaround
++==========
++
++ Disable CHUNKING advertisement for incoming connections.
++
++ An attempt to "smuggle" a DATA command will trip a syncronisation
++ check.
++
++*or*
++
++ Disable PIPELINING advertisement for incoming connections.
++
++ The "smuggled" MAIL FROM command will then trip a syncronisation
++ check.
+--- a/src/receive.c
++++ b/src/receive.c
+@@ -826,104 +826,118 @@ we make the CRs optional in all cases.
+
+ July 2003: Bare CRs cause trouble. We now treat them as line terminators as
+ well, so that there are no CRs in spooled messages. However, the message
+ terminating dot is not recognized between two bare CRs.
+
++Dec 2023: getting a site to send a body including an "LF . LF" sequence
++followed by SMTP commands is a possible "smtp smuggling" attack. If
++the first (header) line for the message has a proper CRLF then enforce
++that for the body: convert bare LF to a space.
++
+ Arguments:
+- fout a FILE to which to write the message; NULL if skipping
++ fout a FILE to which to write the message; NULL if skipping
++ strict_crlf require full CRLF sequence as a line ending
+
+ Returns: One of the END_xxx values indicating why it stopped reading
+ */
+
+ static int
+-read_message_data_smtp(FILE *fout)
++read_message_data_smtp(FILE * fout, BOOL strict_crlf)
+ {
+-int ch_state = 0;
+-int ch;
+-int linelength = 0;
++enum { s_linestart, s_normal, s_had_cr, s_had_nl_dot, s_had_dot_cr } ch_state =
++ s_linestart;
++int linelength = 0, ch;
+
+ while ((ch = (receive_getc)(GETC_BUFFER_UNLIMITED)) != EOF)
+ {
+ if (ch == 0) body_zerocount++;
+ switch (ch_state)
+ {
+- case 0: /* After LF or CRLF */
+- if (ch == '.')
+- {
+- ch_state = 3;
+- continue; /* Don't ever write . after LF */
+- }
+- ch_state = 1;
++ case s_linestart: /* After LF or CRLF */
++ if (ch == '.')
++ {
++ ch_state = s_had_nl_dot;
++ continue; /* Don't ever write . after LF */
++ }
++ ch_state = s_normal;
+
+- /* Else fall through to handle as normal uschar. */
++ /* Else fall through to handle as normal uschar. */
+
+- case 1: /* Normal state */
+- if (ch == '\n')
+- {
+- ch_state = 0;
+- body_linecount++;
++ case s_normal: /* Normal state */
++ if (ch == '\r')
++ {
++ ch_state = s_had_cr;
++ continue; /* Don't write the CR */
++ }
++ if (ch == '\n') /* Bare LF at end of line */
++ if (strict_crlf)
++ ch = ' '; /* replace LF with space */
++ else
++ { /* treat as line ending */
++ ch_state = s_linestart;
++ body_linecount++;
++ if (linelength > max_received_linelength)
++ max_received_linelength = linelength;
++ linelength = -1;
++ }
++ break;
++
++ case s_had_cr: /* After (unwritten) CR */
++ body_linecount++; /* Any char ends line */
+ if (linelength > max_received_linelength)
+- max_received_linelength = linelength;
++ max_received_linelength = linelength;
+ linelength = -1;
+- }
+- else if (ch == '\r')
+- {
+- ch_state = 2;
+- continue;
+- }
+- break;
++ if (ch == '\n') /* proper CRLF */
++ ch_state = s_linestart;
++ else
++ {
++ message_size++; /* convert the dropped CR to a stored NL */
++ if (fout && fputc('\n', fout) == EOF) return END_WERROR;
++ cutthrough_data_put_nl();
++ if (ch == '\r') /* CR; do not write */
++ continue;
++ ch_state = s_normal; /* not LF or CR; process as standard */
++ }
++ break;
+
+- case 2: /* After (unwritten) CR */
+- body_linecount++;
+- if (linelength > max_received_linelength)
+- max_received_linelength = linelength;
+- linelength = -1;
+- if (ch == '\n')
+- {
+- ch_state = 0;
+- }
+- else
+- {
+- message_size++;
+- if (fout != NULL && fputc('\n', fout) == EOF) return END_WERROR;
+- cutthrough_data_put_nl();
+- if (ch != '\r') ch_state = 1; else continue;
+- }
+- break;
++ case s_had_nl_dot: /* After [CR] LF . */
++ if (ch == '\n') /* [CR] LF . LF */
++ if (strict_crlf)
++ ch = ' '; /* replace LF with space */
++ else
++ return END_DOT;
++ else if (ch == '\r') /* [CR] LF . CR */
++ {
++ ch_state = s_had_dot_cr;
++ continue; /* Don't write the CR */
++ }
++ /* The dot was removed on reaching s_had_nl_dot. For a doubled dot, here,
++ reinstate it to cutthrough. The current ch, dot or not, is passed both to
++ cutthrough and to file below. */
++ else if (ch == '.')
++ {
++ uschar c = ch;
++ cutthrough_data_puts(&c, 1);
++ }
++ ch_state = s_normal;
++ break;
+
+- case 3: /* After [CR] LF . */
+- if (ch == '\n')
+- return END_DOT;
+- if (ch == '\r')
+- {
+- ch_state = 4;
+- continue;
+- }
+- /* The dot was removed at state 3. For a doubled dot, here, reinstate
+- it to cutthrough. The current ch, dot or not, is passed both to cutthrough
+- and to file below. */
+- if (ch == '.')
+- {
+- uschar c= ch;
+- cutthrough_data_puts(&c, 1);
+- }
+- ch_state = 1;
+- break;
++ case s_had_dot_cr: /* After [CR] LF . CR */
++ if (ch == '\n')
++ return END_DOT; /* Preferred termination */
+
+- case 4: /* After [CR] LF . CR */
+- if (ch == '\n') return END_DOT;
+- message_size++;
+- body_linecount++;
+- if (fout != NULL && fputc('\n', fout) == EOF) return END_WERROR;
+- cutthrough_data_put_nl();
+- if (ch == '\r')
+- {
+- ch_state = 2;
+- continue;
+- }
+- ch_state = 1;
+- break;
++ message_size++; /* convert the dropped CR to a stored NL */
++ body_linecount++;
++ if (fout && fputc('\n', fout) == EOF) return END_WERROR;
++ cutthrough_data_put_nl();
++ if (ch == '\r')
++ {
++ ch_state = s_had_cr;
++ continue; /* CR; do not write */
++ }
++ ch_state = s_normal;
++ break;
+ }
+
+ /* Add the character to the spool file, unless skipping; then loop for the
+ next. */
+
+@@ -1135,11 +1149,11 @@ Returns: nothing
+ void
+ receive_swallow_smtp(void)
+ {
+ if (message_ended >= END_NOTENDED)
+ message_ended = chunking_state <= CHUNKING_OFFERED
+- ? read_message_data_smtp(NULL)
++ ? read_message_data_smtp(NULL, FALSE)
+ : read_message_bdat_smtp_wire(NULL);
+ }
+
+
+
+@@ -1946,28 +1960,36 @@ for (;;)
+ LF specially by inserting a white space after it to ensure that the header
+ line is not terminated. */
+
+ if (ch == '\n')
+ {
+- if (first_line_ended_crlf == TRUE_UNSET) first_line_ended_crlf = FALSE;
+- else if (first_line_ended_crlf) receive_ungetc(' ');
++ if (first_line_ended_crlf == TRUE_UNSET)
++ first_line_ended_crlf = FALSE;
++ else if (first_line_ended_crlf)
++ receive_ungetc(' ');
+ goto EOL;
+ }
+
+ /* This is not the end of the line. If this is SMTP input and this is
+ the first character in the line and it is a "." character, ignore it.
+ This implements the dot-doubling rule, though header lines starting with
+ dots aren't exactly common. They are legal in RFC 822, though. If the
+ following is CRLF or LF, this is the line that that terminates the
++
+ entire message. We set message_ended to indicate this has happened (to
+ prevent further reading), and break out of the loop, having freed the
+ empty header, and set next = NULL to indicate no data line. */
+
+ if (f.dot_ends && ptr == 0 && ch == '.')
+ {
++ /* leading dot while in headers-read mode */
+ ch = (receive_getc)(GETC_BUFFER_UNLIMITED);
+- if (ch == '\r')
++ if (ch == '\n' && first_line_ended_crlf == TRUE /* and not TRUE_UNSET */ )
++ /* dot, LF but we are in CRLF mode. Attack? */
++ ch = ' '; /* replace the LF with a space */
++
++ else if (ch == '\r')
+ {
+ ch = (receive_getc)(GETC_BUFFER_UNLIMITED);
+ if (ch != '\n')
+ {
+ if (ch >= 0) receive_ungetc(ch);
+@@ -1999,11 +2021,12 @@ for (;;)
+ if (ch == '\r')
+ {
+ ch = (receive_getc)(GETC_BUFFER_UNLIMITED);
+ if (ch == '\n')
+ {
+- if (first_line_ended_crlf == TRUE_UNSET) first_line_ended_crlf = TRUE;
++ if (first_line_ended_crlf == TRUE_UNSET)
++ first_line_ended_crlf = TRUE;
+ goto EOL;
+ }
+
+ /* Otherwise, put back the character after CR, and turn the bare CR
+ into LF SP. */
+@@ -3144,11 +3167,11 @@ if (cutthrough.cctx.sock >= 0 && cutthro
+ (void) cutthrough_headers_send();
+ }
+
+
+ /* Open a new spool file for the data portion of the message. We need
+-to access it both via a file descriptor and a stream. Try to make the
++to access it both via a file descriptor and a stdio stream. Try to make the
+ directory if it isn't there. */
+
+ spool_name = spool_fname(US"input", message_subdir, message_id, US"-D");
+ DEBUG(D_receive) debug_printf("Data file name: %s\n", spool_name);
+
+@@ -3213,11 +3236,11 @@ message id or "next" line. */
+ if (!ferror(spool_data_file) && !(receive_feof)() && message_ended != END_DOT)
+ {
+ if (smtp_input)
+ {
+ message_ended = chunking_state <= CHUNKING_OFFERED
+- ? read_message_data_smtp(spool_data_file)
++ ? read_message_data_smtp(spool_data_file, first_line_ended_crlf)
+ : spool_wireformat
+ ? read_message_bdat_smtp_wire(spool_data_file)
+ : read_message_bdat_smtp(spool_data_file);
+ receive_linecount++; /* The terminating "." line */
+ }
+--- a/src/smtp_in.c
++++ b/src/smtp_in.c
+@@ -5447,16 +5447,16 @@ while (done <= 0)
+ }
+ break;
+ }
+
+ if (chunking_state > CHUNKING_OFFERED)
+- rc = OK; /* No predata ACL or go-ahead output for BDAT */
++ rc = OK; /* There is no predata ACL or go-ahead output for BDAT */
+ else
+ {
+- /* If there is an ACL, re-check the synchronization afterwards, since the
+- ACL may have delayed. To handle cutthrough delivery enforce a dummy call
+- to get the DATA command sent. */
++ /* If there is a predata-ACL, re-check the synchronization afterwards,
++ since the ACL may have delayed. To handle cutthrough delivery enforce a
++ dummy call to get the DATA command sent. */
+
+ if (!acl_smtp_predata && cutthrough.cctx.sock < 0)
+ rc = OK;
+ else
+ {
+--- a/doc/spec.txt
++++ b/doc/spec.txt
+@@ -33555,12 +33555,10 @@ MTA within an operating system would use
+ has shown that this is not the case; for example, there are Unix applications
+ that use CRLF in this circumstance. For this reason, and for compatibility with
+ other MTAs, the way Exim handles line endings for all messages is now as
+ follows:
+
+- * LF not preceded by CR is treated as a line ending.
+-
+ * CR is treated as a line ending; if it is immediately followed by LF, the LF
+ is ignored.
+
+ * The sequence "CR, dot, CR" does not terminate an incoming SMTP message, nor
+ a local message in the state where a line containing only a dot is a
+@@ -33571,11 +33569,14 @@ follows:
+ behind this is that bare CRs in header lines are most likely either to be
+ mistakes, or people trying to play silly games.
+
+ * If the first header line received in a message ends with CRLF, a subsequent
+ bare LF in a header line is treated in the same way as a bare CR in a
+- header line.
++ header line and a bare LF in a body line is replaced with a space.
++
++ * If the first header line received in a message does not end with CRLF, a
++ subsequent LF not preceded by CR is treated as a line ending.
+
+
+ 48.3 Unqualified addresses
+ --------------------------
+
diff --git a/debian/patches/90_localscan_dlopen.dpatch b/debian/patches/90_localscan_dlopen.dpatch
new file mode 100644
index 0000000..21c96b1
--- /dev/null
+++ b/debian/patches/90_localscan_dlopen.dpatch
@@ -0,0 +1,347 @@
+Description: Allow one to use and switch between different local_scan functions
+ without recompiling exim.
+ http://marc.merlins.org/linux/exim/files/sa-exim-current/ Original patch from
+ David Woodhouse, modified first by Derrick 'dman' Hudson and then by Marc
+ MERLIN for SA-Exim and minor/major API version tracking
+Author: David Woodhouse, Derrick 'dman' Hudson, Marc MERLIN
+Origin: other, http://marc.merlins.org/linux/exim/files/sa-exim-current/
+Forwarded: https://bugs.exim.org/show_bug.cgi?id=2671
+Last-Update: 2022-12-19
+
+--- a/src/EDITME
++++ b/src/EDITME
+@@ -874,10 +874,25 @@ HEADERS_CHARSET="ISO-8859-1"
+ # as the traditional crypt() function.
+ # *** WARNING *** WARNING *** WARNING *** WARNING *** WARNING ***
+
+
+ #------------------------------------------------------------------------------
++# On systems which support dynamic loading of shared libraries, Exim can
++# load a local_scan function specified in its config file instead of having
++# to be recompiled with the desired local_scan function. For a full
++# description of the API to this function, see the Exim specification.
++
++DLOPEN_LOCAL_SCAN=yes
++
++# If you set DLOPEN_LOCAL_SCAN, then you need to include -rdynamic in the
++# linker flags. Without it, the loaded .so won't be able to access any
++# functions from exim.
++
++LDFLAGS += -rdynamic
++CFLAGS += -fvisibility=hidden
++
++#------------------------------------------------------------------------------
+ # The default distribution of Exim contains only the plain text form of the
+ # documentation. Other forms are available separately. If you want to install
+ # the documentation in "info" format, first fetch the Texinfo documentation
+ # sources from the ftp directory and unpack them, which should create files
+ # with the extension "texinfo" in the doc directory. You may find that the
+--- a/src/config.h.defaults
++++ b/src/config.h.defaults
+@@ -31,10 +31,12 @@ Do not put spaces between # and the 'def
+ #define AUTH_SPA
+ #define AUTH_TLS
+
+ #define AUTH_VARS 4
+
++#define DLOPEN_LOCAL_SCAN
++
+ #define BIN_DIRECTORY
+
+ #define CONFIGURE_FILE
+ #define CONFIGURE_FILE_USE_EUID
+ #define CONFIGURE_FILE_USE_NODE
+--- a/src/globals.c
++++ b/src/globals.c
+@@ -115,10 +115,14 @@ tls_support tls_out = {
+ uschar *dsn_envid = NULL;
+ int dsn_ret = 0;
+ const pcre2_code *regex_DSN = NULL;
+ uschar *dsn_advertise_hosts = NULL;
+
++#ifdef DLOPEN_LOCAL_SCAN
++uschar *local_scan_path = NULL;
++#endif
++
+ #ifndef DISABLE_TLS
+ BOOL gnutls_compat_mode = FALSE;
+ BOOL gnutls_allow_auto_pkcs11 = FALSE;
+ uschar *hosts_require_alpn = NULL;
+ uschar *openssl_options = NULL;
+--- a/src/globals.h
++++ b/src/globals.h
+@@ -153,10 +153,14 @@ extern uschar *tls_advertise_hosts; /
+ extern uschar *dsn_envid; /* DSN envid string */
+ extern int dsn_ret; /* DSN ret type*/
+ extern const pcre2_code *regex_DSN; /* For recognizing DSN settings */
+ extern uschar *dsn_advertise_hosts; /* host for which TLS is advertised */
+
++#ifdef DLOPEN_LOCAL_SCAN
++extern uschar *local_scan_path; /* Path to local_scan() library */
++#endif
++
+ /* Input-reading functions for messages, so we can use special ones for
+ incoming TCP/IP. */
+
+ extern int (*lwr_receive_getc)(unsigned);
+ extern uschar * (*lwr_receive_getbuf)(unsigned *);
+--- a/src/local_scan.c
++++ b/src/local_scan.c
+@@ -5,60 +5,136 @@
+ /* Copyright (c) University of Cambridge 1995 - 2009 */
+ /* Copyright (c) The Exim Maintainers 2021 */
+ /* See the file NOTICE for conditions of use and distribution. */
+
+
+-/******************************************************************************
+-This file contains a template local_scan() function that just returns ACCEPT.
+-If you want to implement your own version, you should copy this file to, say
+-Local/local_scan.c, and edit the copy. To use your version instead of the
+-default, you must set
+-
+-HAVE_LOCAL_SCAN=yes
+-LOCAL_SCAN_SOURCE=Local/local_scan.c
+-
+-in your Local/Makefile. This makes it easy to copy your version for use with
+-subsequent Exim releases.
+-
+-For a full description of the API to this function, see the Exim specification.
+-******************************************************************************/
+-
+-
+ /* This is the only Exim header that you should include. The effect of
+ including any other Exim header is not defined, and may change from release to
+ release. Use only the documented interface! */
+
+ #include "local_scan.h"
+
+-
+-/* This is a "do-nothing" version of a local_scan() function. The arguments
+-are:
+-
+- fd The file descriptor of the open -D file, which contains the
+- body of the message. The file is open for reading and
+- writing, but modifying it is dangerous and not recommended.
+-
+- return_text A pointer to an unsigned char* variable which you can set in
+- order to return a text string. It is initialized to NULL.
+-
+-The return values of this function are:
+-
+- LOCAL_SCAN_ACCEPT
+- The message is to be accepted. The return_text argument is
+- saved in $local_scan_data.
+-
+- LOCAL_SCAN_REJECT
+- The message is to be rejected. The returned text is used
+- in the rejection message.
+-
+- LOCAL_SCAN_TEMPREJECT
+- This specifies a temporary rejection. The returned text
+- is used in the rejection message.
+-*/
++#ifdef DLOPEN_LOCAL_SCAN
++#include <dlfcn.h>
++#include <stdlib.h>
++static int (*local_scan_fn)(int fd, uschar **return_text) = NULL;
++static int load_local_scan_library(void);
++#endif
+
+ int
+ local_scan(int fd, uschar **return_text)
+ {
+-return LOCAL_SCAN_ACCEPT;
++
++#ifdef DLOPEN_LOCAL_SCAN
++/* local_scan_path is defined AND not the empty string */
++if (local_scan_path && *local_scan_path)
++ {
++ if (!local_scan_fn)
++ {
++ if (!load_local_scan_library())
++ {
++ char *base_msg , *error_msg , *final_msg ;
++ int final_length = -1 ;
++
++ base_msg=US"Local configuration error - local_scan() library failure\n";
++ error_msg = dlerror() ;
++
++ final_length = strlen(base_msg) + strlen(error_msg) + 1 ;
++ final_msg = (char*)malloc( final_length*sizeof(char) ) ;
++ *final_msg = '\0' ;
++
++ strcat( final_msg , base_msg ) ;
++ strcat( final_msg , error_msg ) ;
++
++ *return_text = final_msg ;
++ return LOCAL_SCAN_TEMPREJECT;
++ }
++ }
++ return local_scan_fn(fd, return_text);
++ }
++else
++#endif
++ return LOCAL_SCAN_ACCEPT;
++}
++
++#ifdef DLOPEN_LOCAL_SCAN
++
++static int load_local_scan_library(void)
++{
++/* No point in keeping local_scan_lib since we'll never dlclose() anyway */
++void *local_scan_lib = NULL;
++int (*local_scan_version_fn)(void);
++int vers_maj;
++int vers_min;
++
++local_scan_lib = dlopen(local_scan_path, RTLD_NOW);
++if (!local_scan_lib)
++ {
++ log_write(0, LOG_MAIN|LOG_REJECT, "local_scan() library open failed - "
++ "message temporarily rejected");
++ return FALSE;
++ }
++
++local_scan_version_fn = dlsym(local_scan_lib, "local_scan_version_major");
++if (!local_scan_version_fn)
++ {
++ dlclose(local_scan_lib);
++ log_write(0, LOG_MAIN|LOG_REJECT, "local_scan() library doesn't contain "
++ "local_scan_version_major() function - message temporarily rejected");
++ return FALSE;
++ }
++
++/* The major number is increased when the ABI is changed in a non
++ backward compatible way. */
++vers_maj = local_scan_version_fn();
++
++local_scan_version_fn = dlsym(local_scan_lib, "local_scan_version_minor");
++if (!local_scan_version_fn)
++ {
++ dlclose(local_scan_lib);
++ log_write(0, LOG_MAIN|LOG_REJECT, "local_scan() library doesn't contain "
++ "local_scan_version_minor() function - message temporarily rejected");
++ return FALSE;
++ }
++
++/* The minor number is increased each time a new feature is added (in a
++ way that doesn't break backward compatibility) -- Marc */
++vers_min = local_scan_version_fn();
++
++
++if (vers_maj != LOCAL_SCAN_ABI_VERSION_MAJOR)
++ {
++ dlclose(local_scan_lib);
++ local_scan_lib = NULL;
++ log_write(0, LOG_MAIN|LOG_REJECT, "local_scan() has an incompatible major"
++ "version number, you need to recompile your module for this version"
++ "of exim (The module was compiled for version %d.%d and this exim provides"
++ "ABI version %d.%d)", vers_maj, vers_min, LOCAL_SCAN_ABI_VERSION_MAJOR,
++ LOCAL_SCAN_ABI_VERSION_MINOR);
++ return FALSE;
++ }
++else if (vers_min > LOCAL_SCAN_ABI_VERSION_MINOR)
++ {
++ dlclose(local_scan_lib);
++ local_scan_lib = NULL;
++ log_write(0, LOG_MAIN|LOG_REJECT, "local_scan() has an incompatible minor"
++ "version number, you need to recompile your module for this version"
++ "of exim (The module was compiled for version %d.%d and this exim provides"
++ "ABI version %d.%d)", vers_maj, vers_min, LOCAL_SCAN_ABI_VERSION_MAJOR,
++ LOCAL_SCAN_ABI_VERSION_MINOR);
++ return FALSE;
++ }
++
++local_scan_fn = dlsym(local_scan_lib, "local_scan");
++if (!local_scan_fn)
++ {
++ dlclose(local_scan_lib);
++ log_write(0, LOG_MAIN|LOG_REJECT, "local_scan() library doesn't contain "
++ "local_scan() function - message temporarily rejected");
++ return FALSE;
++ }
++return TRUE;
+ }
+
++#endif /* DLOPEN_LOCAL_SCAN */
++
+ /* End of local_scan.c */
+--- a/src/local_scan.h
++++ b/src/local_scan.h
+@@ -25,10 +25,11 @@ store.c
+ /* Some basic types that make some things easier, the Exim configuration
+ settings, and the store functions. */
+
+ #include <stdarg.h>
+ #include <sys/types.h>
++#pragma GCC visibility push(default)
+ #include "config.h"
+ #include "mytypes.h"
+ #include "store.h"
+
+
+@@ -164,10 +165,13 @@ extern const uschar *headers_charset; /
+ extern header_line *header_last; /* Final header */
+ extern header_line *header_list; /* First header */
+ extern BOOL host_checking; /* Set when checking a host */
+ extern uschar *interface_address; /* Interface for incoming call */
+ extern int interface_port; /* Port number for incoming call */
++#ifdef DLOPEN_LOCAL_SCAN
++extern uschar *local_scan_path;
++#endif
+ extern uschar *message_id; /* Internal id of message being handled */
+ extern uschar *received_protocol; /* Name of incoming protocol */
+ extern int recipients_count; /* Number of recipients */
+ extern recipient_item *recipients_list;/* List of recipient addresses */
+ extern unsigned char *sender_address; /* Sender address */
+@@ -234,6 +238,8 @@ extern uschar * string_copy_taint_functi
+ extern pid_t child_open_exim_function(int *, const uschar *);
+ extern pid_t child_open_exim2_function(int *, uschar *, uschar *, const uschar *);
+ extern pid_t child_open_function(uschar **, uschar **, int, int *, int *, BOOL, const uschar *);
+ #endif
+
++#pragma GCC visibility pop
++
+ /* End of local_scan.h */
+--- a/src/readconf.c
++++ b/src/readconf.c
+@@ -210,10 +210,13 @@ static optionlist optionlist_config[] =
+ #endif
+ { "local_from_check", opt_bool, {&local_from_check} },
+ { "local_from_prefix", opt_stringptr, {&local_from_prefix} },
+ { "local_from_suffix", opt_stringptr, {&local_from_suffix} },
+ { "local_interfaces", opt_stringptr, {&local_interfaces} },
++#ifdef DLOPEN_LOCAL_SCAN
++ { "local_scan_path", opt_stringptr, &local_scan_path },
++#endif
+ #ifdef HAVE_LOCAL_SCAN
+ { "local_scan_timeout", opt_time, {&local_scan_timeout} },
+ #endif
+ { "local_sender_retain", opt_bool, {&local_sender_retain} },
+ { "localhost_number", opt_stringptr, {&host_number_string} },
+--- a/src/string.c
++++ b/src/string.c
+@@ -416,10 +416,11 @@ return ss;
+
+
+
+ #if (defined(HAVE_LOCAL_SCAN) || defined(EXPAND_DLFUNC)) \
+ && !defined(MACRO_PREDEF) && !defined(COMPILE_UTILITY)
++#pragma GCC visibility push(default)
+ /*************************************************
+ * Copy and save string *
+ *************************************************/
+
+ /*
+@@ -461,10 +462,11 @@ Returns: copy of string in new store
+ uschar *
+ string_copyn_function(const uschar * s, int n)
+ {
+ return string_copyn(s, n);
+ }
++#pragma GCC visibility pop
+ #endif
+
+
+ /*************************************************
+ * Copy and save string in malloc'd store *
diff --git a/debian/patches/series b/debian/patches/series
new file mode 100644
index 0000000..b57ca5e
--- /dev/null
+++ b/debian/patches/series
@@ -0,0 +1,53 @@
+31_eximmanpage.dpatch
+32_exim4.dpatch
+33_eximon.binary.dpatch
+34_eximstatsmanpage.dpatch
+35_install.dpatch
+60_convert4r4.dpatch
+67_unnecessaryCopt.diff
+70_remove_exim-users_references.dpatch
+75_01-Fix-exit-on-attempt-to-rewrite-a-malformed-address.-.patch
+75_05-SPF-fix-memory-accounting-for-error-case.patch
+75_08-Fix-regex-n-use-after-free.-Bug-2915.patch
+75_09-Fix-non-WITH_CONTENT_SCAN-build.patch
+75_10-Fix-non-WITH_CONTENT_SCAN-build-2.patch
+75_11-Fix-non-WITH_CONTENT_SCAN-build-3.patch
+75_16-GnuTLS-fix-for-clients-offering-no-TLS-extensions.patch
+75_18-Fix-Build-with-libopendmarc-1.4.x-fixes-2728.patch
+75_19-DMARC-fix-use-after-free-in-dmarc_dns_lookup.patch
+75_22-Fix-daemon-startup.-Bug-2930.patch
+75_23-Fix-reccipients-after-run.-.-Bug-2929.patch
+75_31-Fix-regext-substring-capture-variables-for-null-matc.patch
+75_32-Fix-regex-substring-capture-variables-for-null-match.patch
+75_34-Fix-regex-substring-capture-commentary.-Bug-2933.patch
+75_37-OpenSSL-when-preloading-creds-do-the-server-certs-be.patch
+75_38-OpenSSL-fix-double-expansion-of-tls_verify_certifica.patch
+75_42-Fix-run-arg-parsing.patch
+75_50-Fix-logging-of-max-size-log-line.patch
+75_55-Fix-recursion-on-dns_again_means_nonexist.-Bug-2911.patch
+75_58-Close-server-smtp-socket-explicitly-on-connect-ACL-d.patch
+75_60-OpenSSL-fix-tls_eccurve-setting-explicit-curve-group.patch
+75_62-OpenSSL-Fix-tls_eccurve-on-earlier-versions-than-3.0.patch
+75_63-OpenSSL-log-conns-rejected-for-bad-ALPN-with-the-off.patch
+75_64-DANE-do-not-check-dns_again_means_nonexist-for-TLSA-.patch
+75_66-Fix-crash-in-expansions.patch
+75_68-Fix-srs_encode-.-for-mod-1024-day-zero.patch
+75_70-Fix-variable-initialisation-in-smtp-transport.-Bug-2.patch
+75_71-Auths-fix-possible-OOB-write-in-external-authenticat.patch
+75_72-Auths-use-uschar-more-in-spa-authenticator.patch
+75_73-Auths-fix-possible-OOB-write-in-SPA-authenticator.-B.patch
+75_74-Auths-fix-possible-OOB-read-in-SPA-authenticator.-Bu.patch
+75_74-Cancel-early-pipe-on-an-observed-advertising-change.patch
+75_76-Expansions-disallow-UTF-16-surrogates-from-utf8clean.patch
+75_77-GnuTLS-fix-crash-with-tls_dhparam-none.patch
+75_79-Fix-recipients-expansion-when-used-within-run.-.-Bug.patch
+75_82-GnuTLS-fix-autogen-cert-expiry-date.-Bug-3014.patch
+75_83-Re-fix-live-variable-value-free.-The-inital-fix-resu.patch
+76-01-fix-string_is_ip_address-CVE-2023-42117-Bug-3031.patch
+76-02-SPF-harden-against-crafted-DNS-responses.patch
+76-03-Harden-dnsdb-against-crafted-DNS-responses.-Bug-3033.patch
+76-10-Fix-tr.-and-empty-strings.-Bug-3023.patch
+76-12-DNS-more-hardening-against-crafted-responses.patch
+76-14-Lookups-Fix-dnsdb-lookup-of-multi-chunk-TXT.-Bug-305.patch
+77_CVE-2023-51766_4.97.1-release.diff
+90_localscan_dlopen.dpatch