svn commit: r1933012 - spamassassin/trunk/spamc

[email protected] Mon, 13 Apr 2026 07:19:38 -0000
Newsgroups gmane.mail.spam.spamassassin.cvs
Message-ID <177606477802.2277102.10071857038238237361@svn03-he-fi>
Author: gbechis
Date: Mon Apr 13 07:19:37 2026
New Revision: 1933012

Log:
Fix spamc hang on TLS 1.3 connections due to post-handshake NewSessionTicket records
Add a --debug parameter to spamc(1)

Submitted by: Dan Mahoney <[email protected]>
with tweaks by me

Github: closes #26

Modified:
   spamassassin/trunk/spamc/libspamc.c
   spamassassin/trunk/spamc/libspamc.h
   spamassassin/trunk/spamc/spamc.c
   spamassassin/trunk/spamc/spamc.pod
   spamassassin/trunk/spamc/utils.c

Modified: spamassassin/trunk/spamc/libspamc.c
==============================================================================
--- spamassassin/trunk/spamc/libspamc.c	Mon Apr 13 06:56:39 2026	(r1933011)
+++ spamassassin/trunk/spamc/libspamc.c	Mon Apr 13 07:19:37 2026	(r1933012)
@@ -714,11 +714,40 @@ static int _try_ssl_connect(SSL_CTX *ctx
     if (ssl_rtn != 1) {
 	int ssl_err = SSL_get_error(ssl, ssl_rtn);
 	libspamc_log(flags, LOG_ERR,
-		     "SSL_connect error: %s", _ssl_err_as_string());
+		     "SSL_connect error: %d %s", ssl_err, _ssl_err_as_string());
 	return EX_UNAVAILABLE;
     }
+    if (flags & SPAMC_DEBUG) {
+	libspamc_log(flags, LOG_DEBUG,
+		     "SSL connected: version=%s cipher=%s state=%s",
+		     SSL_get_version(ssl), SSL_get_cipher(ssl),
+		     SSL_state_string_long(ssl));
+    }
     return EX_OK;
 }
+
+static int _ssl_write_with_retry(SSL *ssl, const void *buf, int len,
+				 int flags)
+{
+    int rc;
+    int ssl_err;
+    do {
+	rc = SSL_write(ssl, buf, len);
+	if (rc > 0) {
+	    if (flags & SPAMC_DEBUG) {
+		libspamc_log(flags, LOG_DEBUG, "SSL_write: wrote %d bytes", rc);
+	    }
+	    return rc;
+	}
+	ssl_err = SSL_get_error(ssl, rc);
+	if (flags & SPAMC_DEBUG) {
+	    libspamc_log(flags, LOG_DEBUG,
+			"SSL_write: rc=%d ssl_err=%d state=%s",
+			rc, ssl_err, SSL_state_string_long(ssl));
+	}
+    } while (ssl_err == SSL_ERROR_WANT_READ || ssl_err == SSL_ERROR_WANT_WRITE);
+    return rc;
+}
 #endif
 
 /* Aug 14, 2002 bj: Reworked things. Now we have message_read, message_write,
@@ -1009,6 +1038,15 @@ _spamc_read_full_line(struct message *m,
     for (len = 0; len < bufsiz - 1; len++) {
 	if (flags & SPAMC_USE_SSL) {
 	    bytesread = ssl_timeout_read(ssl, buf + len, 1);
+	    if (bytesread <= 0 && (flags & SPAMC_DEBUG)) {
+#ifdef SPAMC_SSL
+		libspamc_log(flags, LOG_DEBUG,
+			     "ssl_timeout_read: returned %d, ssl_err=%d, "
+			     "errno=%d, state=%s",
+			     bytesread, SSL_get_error(ssl, bytesread),
+			     errno, SSL_state_string_long(ssl));
+#endif
+	    }
 	}
 	else {
 	    bytesread = fd_timeout_read(sock, 0, buf + len, 1);
@@ -1497,20 +1535,32 @@ int message_filter(struct transport *tp,
         /* Send to spamd */
         if (flags & SPAMC_USE_SSL) {
 #ifdef SPAMC_SSL
-            rc = SSL_write(ssl, buf, len);
-            if (rc <= 0) {
-                libspamc_log(flags, LOG_ERR, "SSL write failed (%d)",
-                             SSL_get_error(ssl, rc));
-                failureval = EX_IOERR;
-                goto failure;
+            if (flags & SPAMC_DEBUG) {
+                libspamc_log(flags, LOG_DEBUG,
+                             "SSL_write: sending header (%d bytes), state=%s",
+                             len, SSL_state_string_long(ssl));
             }
-            rc = SSL_write(ssl, towrite_buf, towrite_len);
+            rc = _ssl_write_with_retry(ssl, buf, len, flags);
             if (rc <= 0) {
-                libspamc_log(flags, LOG_ERR, "SSL write failed (%d)",
-                             SSL_get_error(ssl, rc));
+                libspamc_log(flags, LOG_ERR, "SSL write failed: error=%d state=%s",
+                             SSL_get_error(ssl, rc), SSL_state_string_long(ssl));
                 failureval = EX_IOERR;
                 goto failure;
             }
+            if (towrite_len > 0) {
+                if (flags & SPAMC_DEBUG) {
+                    libspamc_log(flags, LOG_DEBUG,
+                                 "SSL_write: sending body (%d bytes), state=%s",
+                                 towrite_len, SSL_state_string_long(ssl));
+                }
+                rc = _ssl_write_with_retry(ssl, towrite_buf, towrite_len, flags);
+                if (rc <= 0) {
+                    libspamc_log(flags, LOG_ERR, "SSL write failed: error=%d state=%s",
+                                 SSL_get_error(ssl, rc), SSL_state_string_long(ssl));
+                    failureval = EX_IOERR;
+                    goto failure;
+                }
+            }
             SSL_shutdown(ssl);
             shutdown(sock, SHUT_WR);
 #endif

Modified: spamassassin/trunk/spamc/libspamc.h
==============================================================================
--- spamassassin/trunk/spamc/libspamc.h	Mon Apr 13 06:56:39 2026	(r1933011)
+++ spamassassin/trunk/spamc/libspamc.h	Mon Apr 13 07:19:37 2026	(r1933012)
@@ -142,6 +142,9 @@
 /* April 2022, add SSL client certificate support, bug 7267 */
 #define SPAMC_CLIENT_SSL_CERT (1<<12)
 
+/* April 2026, add spamc(1) log debug support */
+#define SPAMC_DEBUG           (1<<11)
+
 #define SPAMC_MESSAGE_CLASS_SPAM 1
 #define SPAMC_MESSAGE_CLASS_HAM  2
 

Modified: spamassassin/trunk/spamc/spamc.c
==============================================================================
--- spamassassin/trunk/spamc/spamc.c	Mon Apr 13 06:56:39 2026	(r1933011)
+++ spamassassin/trunk/spamc/spamc.c	Mon Apr 13 07:19:37 2026	(r1933012)
@@ -150,6 +150,7 @@ print_usage(void)
     usg("  --ssl-key key       Specify an SSL client key PEM file.\n");
     usg("  --ssl-ca-file file  Specify the location of the CA PEM file.\n");
     usg("  --ssl-ca-path path  Specify a directory containin CA files.\n");
+    usg("  -D, --debug         Print debugging messages\n");
 #endif
 #ifndef _WIN32
     usg("  -U, --socket path   Connect to spamd via UNIX domain sockets.\n");
@@ -239,9 +240,9 @@ read_args(int argc, char **argv,
           struct transport *ptrn)
 {
 #ifndef _WIN32
-    const char *opts = "-BcrR46d:e:fyp:n:t:s:u:L:C:xXzSHU:ElhVKF:0:1:2";
+    const char *opts = "-BcrR46d:e:fyp:n:t:s:u:L:C:xXzSHU:ElhVKDF:0:1:2";
 #else
-    const char *opts = "-BcrR46d:fyp:n:t:s:u:L:C:xXzSHElhVKF:0:1:2";
+    const char *opts = "-BcrR46d:fyp:n:t:s:u:L:C:xXzSHElhVKDF:0:1:2";
 #endif
     int opt;
     int ret = EX_OK;
@@ -282,6 +283,7 @@ read_args(int argc, char **argv,
        { "help", no_argument, 0, 'h' },
        { "version", no_argument, 0, 'V' },
        { "compress", no_argument, 0, 'z' },
+       { "debug", no_argument, 0, 'D' },
        { 0, 0, 0, 0} /* last element _must_ be all zeroes */
     };
     
@@ -344,6 +346,11 @@ read_args(int argc, char **argv,
                 flags |= SPAMC_PING;
                 break;
             }
+            case 'D':
+            {
+                flags |= SPAMC_DEBUG;
+                break;
+            }
             case 'l':
             {
                 flags |= SPAMC_LOG_TO_STDERR;

Modified: spamassassin/trunk/spamc/spamc.pod
==============================================================================
--- spamassassin/trunk/spamc/spamc.pod	Mon Apr 13 06:56:39 2026	(r1933011)
+++ spamassassin/trunk/spamc/spamc.pod	Mon Apr 13 07:19:37 2026	(r1933012)
@@ -168,6 +168,11 @@ The maximum message size is 256 MB.
 The size is specified in bytes, as a positive integer greater than 0.
 For example, B<-s 500000>. 
 
+=item B<-D>, B<--debug>
+
+Produce debugging output, if spamc was built with SSL support, log SSL state information to stderr
+at each stage of the connection. Requires B<-l> to direct log output to stderr.
+
 =item B<--connect-retries>=I<retries>
 
 Retry connecting to spamd I<retries> times.  The default is 3 times.

Modified: spamassassin/trunk/spamc/utils.c
==============================================================================
--- spamassassin/trunk/spamc/utils.c	Mon Apr 13 06:56:39 2026	(r1933011)
+++ spamassassin/trunk/spamc/utils.c	Mon Apr 13 07:19:37 2026	(r1933012)
@@ -152,18 +152,27 @@ int ssl_timeout_read(SSL * ssl, void *bu
     }
 #endif
 
+#ifdef SPAMC_SSL
+	int ssl_err = 0;
+#endif
+
     do {
 
 #ifdef SPAMC_SSL
 	nred = SSL_read(ssl, buf, nbytes);
+	if (nred < 0) ssl_err = SSL_get_error(ssl, nred);
+	if (nred < 0 && (errno == EWOULDBLOCK || ssl_err == SSL_ERROR_WANT_READ))
+	    continue;
+	break;
 #else
 	UNUSED_VARIABLE(ssl);
 	UNUSED_VARIABLE(buf);
 	UNUSED_VARIABLE(nbytes);
 	nred = 0;		/* never used */
+	break;
 #endif
 
-    } while (nred < 0 && errno == EWOULDBLOCK);
+    } while (1);
 
 #ifndef _WIN32
     if (nred < 0 && errno == EINTR)