From 5d414f1f91bdca118413301b148f0b188eb1cdc6 Mon Sep 17 00:00:00 2001
From: Zdenek Dohnal <zdohnal@redhat.com>
Date: Mon, 13 Oct 2025 10:16:48 +0200
Subject: [PATCH] Fix unresponsive cupsd process caused by a slow client

If client is very slow, it will slow cupsd process for other clients.
The fix is the best effort without turning scheduler cupsd into
multithreaded process which would be too complex and error-prone when
backporting to 2.4.x series.

The fix for unencrypted communication is to follow up on communication
only if there is the whole line on input, and the waiting time is
guarded by timeout.

Encrypted communication now starts after we have the whole client hello
packet, which conflicts with optional upgrade support to HTTPS via
methods other than method OPTIONS, so this optional support defined in
RFC 2817, section 3.1 is removed. Too slow or incomplete requests are
handled by connection timeout.

Fixes CVE-2025-58436
---
 cups/http-private.h |   7 +-
 cups/http.c         |  80 +++++++++++++-------
 cups/tls-openssl.c  |  15 +++-
 scheduler/client.c  | 178 ++++++++++++++++++++++++++++----------------
 scheduler/client.h  |   3 +
 scheduler/select.c  |  12 +++
 6 files changed, 198 insertions(+), 97 deletions(-)

--- a/cups/http-private.h
+++ b/cups/http-private.h
@@ -116,6 +116,7 @@ extern "C" {
  * Constants...
  */
 
+#  define _HTTP_MAX_BUFFER	32768	/* Size of read buffer */
 #  define _HTTP_MAX_SBUFFER	65536	/* Size of (de)compression buffer */
 #  define _HTTP_RESOLVE_DEFAULT	0	/* Just resolve with default options */
 #  define _HTTP_RESOLVE_STDERR	1	/* Log resolve progress to stderr */
@@ -227,8 +228,8 @@ struct _http_s				/**** HTTP connection
   http_encoding_t	data_encoding;	/* Chunked or not */
   int			_data_remaining;/* Number of bytes left (deprecated) */
   int			used;		/* Number of bytes used in buffer */
-  char			buffer[HTTP_MAX_BUFFER];
-					/* Buffer for incoming data */
+  char			_buffer[HTTP_MAX_BUFFER];
+					/* Old read buffer (deprecated) */
   int			_auth_type;	/* Authentication in use (deprecated) */
   unsigned char		_md5_state[88];	/* MD5 state (deprecated) */
   char			nonce[HTTP_MAX_VALUE];
@@ -302,6 +303,8 @@ struct _http_s				/**** HTTP connection
 					/* Allocated field values */
   			*default_fields[HTTP_FIELD_MAX];
 					/* Default field values, if any */
+  char			buffer[_HTTP_MAX_BUFFER];
+					/* Read buffer */
 };
 #  endif /* !_HTTP_NO_PRIVATE */
 
--- a/cups/http.c
+++ b/cups/http.c
@@ -53,7 +53,7 @@ static http_t		*http_create(const char *
 static void		http_debug_hex(const char *prefix, const char *buffer,
 			               int bytes);
 #endif /* DEBUG */
-static ssize_t		http_read(http_t *http, char *buffer, size_t length);
+static ssize_t		http_read(http_t *http, char *buffer, size_t length, int timeout);
 static ssize_t		http_read_buffered(http_t *http, char *buffer, size_t length);
 static ssize_t		http_read_chunk(http_t *http, char *buffer, size_t length);
 static int		http_send(http_t *http, http_state_t request,
@@ -1188,7 +1188,7 @@ httpGets(char   *line,			/* I - Line to
         return (NULL);
       }
 
-      bytes = http_read(http, http->buffer + http->used, (size_t)(HTTP_MAX_BUFFER - http->used));
+      bytes = http_read(http, http->buffer + http->used, (size_t)(_HTTP_MAX_BUFFER - http->used), http->wait_value);
 
       DEBUG_printf(("4httpGets: read " CUPS_LLFMT " bytes.", CUPS_LLCAST bytes));
 
@@ -1706,24 +1706,13 @@ httpPeek(http_t *http,			/* I - HTTP con
 
     ssize_t	buflen;			/* Length of read for buffer */
 
-    if (!http->blocking)
-    {
-      while (!httpWait(http, http->wait_value))
-      {
-	if (http->timeout_cb && (*http->timeout_cb)(http, http->timeout_data))
-	  continue;
-
-	return (0);
-      }
-    }
-
     if ((size_t)http->data_remaining > sizeof(http->buffer))
       buflen = sizeof(http->buffer);
     else
       buflen = (ssize_t)http->data_remaining;
 
     DEBUG_printf(("2httpPeek: Reading %d bytes into buffer.", (int)buflen));
-    bytes = http_read(http, http->buffer, (size_t)buflen);
+    bytes = http_read(http, http->buffer, (size_t)buflen, http->wait_value);
 
     DEBUG_printf(("2httpPeek: Read " CUPS_LLFMT " bytes into buffer.",
                   CUPS_LLCAST bytes));
@@ -1744,9 +1733,9 @@ httpPeek(http_t *http,			/* I - HTTP con
     int		zerr;			/* Decompressor error */
     z_stream	stream;			/* Copy of decompressor stream */
 
-    if (http->used > 0 && ((z_stream *)http->stream)->avail_in < HTTP_MAX_BUFFER)
+    if (http->used > 0 && ((z_stream *)http->stream)->avail_in < _HTTP_MAX_BUFFER)
     {
-      size_t buflen = HTTP_MAX_BUFFER - ((z_stream *)http->stream)->avail_in;
+      size_t buflen = _HTTP_MAX_BUFFER - ((z_stream *)http->stream)->avail_in;
 					/* Number of bytes to copy */
 
       if (((z_stream *)http->stream)->avail_in > 0 &&
@@ -2004,7 +1993,7 @@ httpRead2(http_t *http,			/* I - HTTP co
 
       if (bytes == 0)
       {
-        ssize_t buflen = HTTP_MAX_BUFFER - (ssize_t)((z_stream *)http->stream)->avail_in;
+        ssize_t buflen = _HTTP_MAX_BUFFER - (ssize_t)((z_stream *)http->stream)->avail_in;
 					/* Additional bytes for buffer */
 
         if (buflen > 0)
@@ -2754,7 +2743,7 @@ int					/* O - 1 to continue, 0 to stop
 _httpUpdate(http_t        *http,	/* I - HTTP connection */
             http_status_t *status)	/* O - Current HTTP status */
 {
-  char		line[32768],		/* Line from connection... */
+  char		line[_HTTP_MAX_BUFFER],	/* Line from connection... */
 		*value;			/* Pointer to value on line */
   http_field_t	field;			/* Field index */
   int		major, minor;		/* HTTP version numbers */
@@ -2762,12 +2751,46 @@ _httpUpdate(http_t        *http,	/* I -
 
   DEBUG_printf(("_httpUpdate(http=%p, status=%p), state=%s", (void *)http, (void *)status, httpStateString(http->state)));
 
+  /* When doing non-blocking I/O, make sure we have a whole line... */
+  if (!http->blocking)
+  {
+    ssize_t	bytes;			/* Bytes "peeked" from connection */
+
+    /* See whether our read buffer is full... */
+    DEBUG_printf(("2_httpUpdate: used=%d", http->used));
+
+    if (http->used > 0 && !memchr(http->buffer, '\n', (size_t)http->used) && (size_t)http->used < sizeof(http->buffer))
+    {
+      /* No, try filling in more data... */
+      if ((bytes = http_read(http, http->buffer + http->used, sizeof(http->buffer) - (size_t)http->used, /*timeout*/0)) > 0)
+      {
+	DEBUG_printf(("2_httpUpdate: Read %d bytes.", (int)bytes));
+	http->used += (int)bytes;
+      }
+    }
+
+    /* Peek at the incoming data... */
+    if (!http->used || !memchr(http->buffer, '\n', (size_t)http->used))
+    {
+      /* Don't have a full line, tell the reader to try again when there is more data... */
+      DEBUG_puts("1_htttpUpdate: No newline in buffer yet.");
+      if ((size_t)http->used == sizeof(http->buffer))
+	*status = HTTP_STATUS_ERROR;
+      else
+	*status = HTTP_STATUS_CONTINUE;
+      return (0);
+    }
+
+    DEBUG_puts("2_httpUpdate: Found newline in buffer.");
+  }
+
  /*
   * Grab a single line from the connection...
   */
 
   if (!httpGets(line, sizeof(line), http))
   {
+    DEBUG_puts("1_httpUpdate: Error reading request line.");
     *status = HTTP_STATUS_ERROR;
     return (0);
   }
@@ -4089,7 +4112,8 @@ http_debug_hex(const char *prefix,	/* I
 static ssize_t				/* O - Number of bytes read or -1 on error */
 http_read(http_t *http,			/* I - HTTP connection */
           char   *buffer,		/* I - Buffer */
-          size_t length)		/* I - Maximum bytes to read */
+          size_t length,		/* I - Maximum bytes to read */
+          int    timeout)		/* I - Wait timeout */
 {
   ssize_t	bytes;			/* Bytes read */
 
@@ -4098,7 +4122,7 @@ http_read(http_t *http,			/* I - HTTP co
 
   if (!http->blocking || http->timeout_value > 0.0)
   {
-    while (!httpWait(http, http->wait_value))
+    while (!_httpWait(http, timeout, 1))
     {
       if (http->timeout_cb && (*http->timeout_cb)(http, http->timeout_data))
 	continue;
@@ -4201,7 +4225,7 @@ http_read_buffered(http_t *http,	/* I -
     else
       bytes = (ssize_t)length;
 
-    DEBUG_printf(("8http_read: Grabbing %d bytes from input buffer.",
+    DEBUG_printf(("8http_read_buffered: Grabbing %d bytes from input buffer.",
                   (int)bytes));
 
     memcpy(buffer, http->buffer, (size_t)bytes);
@@ -4211,7 +4235,7 @@ http_read_buffered(http_t *http,	/* I -
       memmove(http->buffer, http->buffer + bytes, (size_t)http->used);
   }
   else
-    bytes = http_read(http, buffer, length);
+    bytes = http_read(http, buffer, length, http->wait_value);
 
   return (bytes);
 }
@@ -4557,15 +4581,15 @@ http_set_timeout(int    fd,		/* I - File
 static void
 http_set_wait(http_t *http)		/* I - HTTP connection */
 {
-  if (http->blocking)
-  {
-    http->wait_value = (int)(http->timeout_value * 1000);
+  http->wait_value = (int)(http->timeout_value * 1000);
 
-    if (http->wait_value <= 0)
+  if (http->wait_value <= 0)
+  {
+    if (http->blocking)
       http->wait_value = 60000;
+    else
+      http->wait_value = 1000;
   }
-  else
-    http->wait_value = 10000;
 }
 
 
--- a/scheduler/client.c
+++ b/scheduler/client.c
@@ -34,11 +34,11 @@
 
 static int		check_if_modified(cupsd_client_t *con,
 			                  struct stat *filestats);
-static int		compare_clients(cupsd_client_t *a, cupsd_client_t *b,
-			                void *data);
 #ifdef HAVE_TLS
-static int		cupsd_start_tls(cupsd_client_t *con, http_encryption_t e);
+static int		check_start_tls(cupsd_client_t *con);
 #endif /* HAVE_TLS */
+static int		compare_clients(cupsd_client_t *a, cupsd_client_t *b,
+			                void *data);
 static char		*get_file(cupsd_client_t *con, struct stat *filestats,
 			          char *filename, size_t len);
 static http_status_t	install_cupsd_conf(cupsd_client_t *con);
@@ -360,14 +360,20 @@ cupsdAcceptClient(cupsd_listener_t *lis)
   if (lis->encryption == HTTP_ENCRYPTION_ALWAYS)
   {
    /*
-    * https connection; go secure...
+    * HTTPS connection, force TLS negotiation...
     */
 
-    if (cupsd_start_tls(con, HTTP_ENCRYPTION_ALWAYS))
-      cupsdCloseClient(con);
+    con->tls_start = time(NULL);
+    con->encryption = HTTP_ENCRYPTION_ALWAYS;
   }
   else
+  {
+   /*
+    * HTTP connection, but check for HTTPS negotiation on first data...
+    */
+
     con->auto_ssl = 1;
+  }
 #endif /* HAVE_TLS */
 }
 
@@ -597,17 +603,46 @@ cupsdReadClient(cupsd_client_t *con)	/*
 
     con->auto_ssl = 0;
 
-    if (recv(httpGetFd(con->http), buf, 1, MSG_PEEK) == 1 &&
-        (!buf[0] || !strchr("DGHOPT", buf[0])))
+    if (recv(httpGetFd(con->http), buf, 5, MSG_PEEK) == 5 && buf[0] == 0x16 && buf[1] == 3 && buf[2])
     {
      /*
-      * Encrypt this connection...
+      * Client hello record, encrypt this connection...
       */
 
-      cupsdLogClient(con, CUPSD_LOG_DEBUG2, "Saw first byte %02X, auto-negotiating SSL/TLS session.", buf[0] & 255);
+      cupsdLogClient(con, CUPSD_LOG_DEBUG2, "Saw client hello record, auto-negotiating TLS session.");
+      con->tls_start = time(NULL);
+      con->encryption = HTTP_ENCRYPTION_ALWAYS;
+    }
+  }
 
-      if (cupsd_start_tls(con, HTTP_ENCRYPTION_ALWAYS))
-        cupsdCloseClient(con);
+  if (con->tls_start)
+  {
+   /*
+    * Try negotiating TLS...
+    */
+
+    int tls_status = check_start_tls(con);
+
+    if (tls_status < 0)
+    {
+     /*
+      * TLS negotiation failed, close the connection.
+      */
+
+      cupsdCloseClient(con);
+      return;
+    }
+    else if (tls_status == 0)
+    {
+     /*
+      * Nothing to do yet...
+      */
+
+      if ((time(NULL) - con->tls_start) > 5)
+      {
+	// Timeout, close the connection...
+	cupsdCloseClient(con);
+      }
 
       return;
     }
@@ -771,9 +806,7 @@ cupsdReadClient(cupsd_client_t *con)	/*
         * Parse incoming parameters until the status changes...
 	*/
 
-        while ((status = httpUpdate(con->http)) == HTTP_STATUS_CONTINUE)
-	  if (!httpGetReady(con->http))
-	    break;
+	status = httpUpdate(con->http);
 
 	if (status != HTTP_STATUS_OK && status != HTTP_STATUS_CONTINUE)
 	{
@@ -935,11 +968,10 @@ cupsdReadClient(cupsd_client_t *con)	/*
 	  return;
 	}
 
-        if (cupsd_start_tls(con, HTTP_ENCRYPTION_REQUIRED))
-        {
-	  cupsdCloseClient(con);
-	  return;
-	}
+	con->tls_start = time(NULL);
+	con->tls_upgrade = 1;
+	con->encryption = HTTP_ENCRYPTION_REQUIRED;
+	return;
 #else
 	if (!cupsdSendError(con, HTTP_STATUS_NOT_IMPLEMENTED, CUPSD_AUTH_NONE))
 	{
@@ -978,32 +1010,11 @@ cupsdReadClient(cupsd_client_t *con)	/*
       if (!_cups_strcasecmp(httpGetField(con->http, HTTP_FIELD_CONNECTION),
                             "Upgrade") && !httpIsEncrypted(con->http))
       {
-#ifdef HAVE_TLS
-       /*
-        * Do encryption stuff...
-	*/
-
-        httpClearFields(con->http);
-
-	if (!cupsdSendHeader(con, HTTP_STATUS_SWITCHING_PROTOCOLS, NULL,
-	                     CUPSD_AUTH_NONE))
-	{
-	  cupsdCloseClient(con);
-	  return;
-	}
-
-        if (cupsd_start_tls(con, HTTP_ENCRYPTION_REQUIRED))
-        {
-	  cupsdCloseClient(con);
-	  return;
-	}
-#else
 	if (!cupsdSendError(con, HTTP_STATUS_NOT_IMPLEMENTED, CUPSD_AUTH_NONE))
 	{
 	  cupsdCloseClient(con);
 	  return;
 	}
-#endif /* HAVE_TLS */
       }
 
       if ((status = cupsdIsAuthorized(con, NULL)) != HTTP_STATUS_OK)
@@ -2631,6 +2642,69 @@ check_if_modified(
 }
 
 
+#ifdef HAVE_TLS
+/*
+ * 'check_start_tls()' - Start encryption on a connection.
+ */
+
+static int				/* O - 0 to continue, 1 on success, -1 on error */
+check_start_tls(cupsd_client_t *con)	/* I - Client connection */
+{
+  unsigned char	chello[4096];		/* Client hello record */
+  ssize_t	chello_bytes;		/* Bytes read/peeked */
+  int		chello_len;		/* Length of record */
+
+
+ /*
+  * See if we have a good and complete client hello record...
+  */
+
+  if ((chello_bytes = recv(httpGetFd(con->http), (char *)chello, sizeof(chello), MSG_PEEK)) < 5)
+    return (0);				/* Not enough bytes (yet) */
+
+  if (chello[0] != 0x016 || chello[1] != 3 || chello[2] == 0)
+    return (-1);			/* Not a TLS Client Hello record */
+
+  chello_len = (chello[3] << 8) | chello[4];
+
+  if ((chello_len + 5) > chello_bytes)
+    return (0);				/* Not enough bytes yet */
+
+ /*
+  * OK, we do, try negotiating...
+  */
+
+  con->tls_start = 0;
+
+  if (httpEncryption(con->http, con->encryption))
+  {
+    cupsdLogClient(con, CUPSD_LOG_ERROR, "Unable to encrypt connection: %s", cupsLastErrorString());
+    return (-1);
+  }
+
+  cupsdLogClient(con, CUPSD_LOG_DEBUG, "Connection now encrypted.");
+
+  if (con->tls_upgrade)
+  {
+    // Respond to the original OPTIONS command...
+    con->tls_upgrade = 0;
+
+    httpClearFields(con->http);
+    httpClearCookie(con->http);
+    httpSetField(con->http, HTTP_FIELD_CONTENT_LENGTH, "0");
+
+    if (!cupsdSendHeader(con, HTTP_STATUS_OK, NULL, CUPSD_AUTH_NONE))
+    {
+      cupsdCloseClient(con);
+      return (-1);
+    }
+  }
+
+  return (1);
+}
+#endif /* HAVE_TLS */
+
+
 /*
  * 'compare_clients()' - Compare two client connections.
  */
@@ -2651,28 +2725,6 @@ compare_clients(cupsd_client_t *a,	/* I
 }
 
 
-#ifdef HAVE_TLS
-/*
- * 'cupsd_start_tls()' - Start encryption on a connection.
- */
-
-static int				/* O - 0 on success, -1 on error */
-cupsd_start_tls(cupsd_client_t    *con,	/* I - Client connection */
-                http_encryption_t e)	/* I - Encryption mode */
-{
-  if (httpEncryption(con->http, e))
-  {
-    cupsdLogClient(con, CUPSD_LOG_ERROR, "Unable to encrypt connection: %s",
-                   cupsLastErrorString());
-    return (-1);
-  }
-
-  cupsdLogClient(con, CUPSD_LOG_DEBUG, "Connection now encrypted.");
-  return (0);
-}
-#endif /* HAVE_TLS */
-
-
 /*
  * 'get_file()' - Get a filename and state info.
  */
--- a/scheduler/client.h
+++ b/scheduler/client.h
@@ -51,6 +51,9 @@ struct cupsd_client_s
   cups_lang_t		*language;	/* Language to use */
 #ifdef HAVE_TLS
   int			auto_ssl;	/* Automatic test for SSL/TLS */
+  time_t		tls_start;	/* Do TLS negotiation? */
+  int			tls_upgrade;	/* Doing TLS upgrade via OPTIONS? */
+  http_encryption_t	encryption;	/* Type of TLS negotiation */
 #endif /* HAVE_TLS */
   http_addr_t		clientaddr;	/* Client's server address */
   char			clientname[256];/* Client's server name for connection */
--- a/scheduler/select.c
+++ b/scheduler/select.c
@@ -408,6 +408,9 @@ cupsdDoSelect(long timeout)		/* I - Time
 
   cupsd_in_select = 1;
 
+  // Prevent 100% CPU by releasing control before the kevent call...
+  usleep(1);
+
   if (timeout >= 0 && timeout < 86400)
   {
     ktimeout.tv_sec  = timeout;
@@ -454,6 +457,9 @@ cupsdDoSelect(long timeout)		/* I - Time
     struct epoll_event	*event;		/* Current event */
 
 
+    // Prevent 100% CPU by releasing control before the epoll_wait call...
+    usleep(1);
+
     if (timeout >= 0 && timeout < 86400)
       nfds = epoll_wait(cupsd_epoll_fd, cupsd_epoll_events, MaxFDs,
                 	timeout * 1000);
@@ -546,6 +552,9 @@ cupsdDoSelect(long timeout)		/* I - Time
     }
   }
 
+  // Prevent 100% CPU by releasing control before the poll call...
+  usleep(1);
+
   if (timeout >= 0 && timeout < 86400)
     nfds = poll(cupsd_pollfds, (nfds_t)count, timeout * 1000);
   else
@@ -599,6 +608,9 @@ cupsdDoSelect(long timeout)		/* I - Time
   cupsd_current_input  = cupsd_global_input;
   cupsd_current_output = cupsd_global_output;
 
+  // Prevent 100% CPU by releasing control before the select call...
+  usleep(1);
+
   if (timeout >= 0 && timeout < 86400)
   {
     stimeout.tv_sec  = timeout;
