]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
openssl: check SSL_write() length on retries
authorStefan Eissing <stefan@eissing.org>
Fri, 1 Aug 2025 12:55:52 +0000 (14:55 +0200)
committerDaniel Stenberg <daniel@haxx.se>
Fri, 1 Aug 2025 15:54:05 +0000 (17:54 +0200)
When an SSL_write() blocks we need to retry it with the
same length as before or stupid OpenSSL freaks out. Remember
it, limit any longer sends and fail shorter ones.

Fixes #18121
Reported-by: adamse on github
Closes #18132

lib/request.c
lib/vtls/openssl.c
lib/vtls/openssl.h
tests/http/test_02_download.py

index 0d9b23ef0c461df06d222858c13707664acba2ae..9fc9ab4d6380da31c1c80eb303b5003ffb3fed51 100644 (file)
@@ -380,8 +380,14 @@ CURLcode Curl_req_send(struct Curl_easy *data, struct dynbuf *req,
   data->req.httpversion_sent = httpversion;
   buf = curlx_dyn_ptr(req);
   blen = curlx_dyn_len(req);
-  if(!Curl_creader_total_length(data)) {
-    /* Request without body. Try to send directly from the buf given. */
+  /* if the sendbuf is empty and the request without body and
+   * the length to send fits info a sendbuf chunk, we send it directly.
+   * If `blen` is larger then `chunk_size`, we can not. Because we
+   * might have to retry a blocked send later from sendbuf and that
+   * would result in retry sends with a shrunken length. That is trouble. */
+  if(Curl_bufq_is_empty(&data->req.sendbuf) &&
+     !Curl_creader_total_length(data) &&
+     (blen <= data->req.sendbuf.chunk_size)) {
     data->req.eos_read = TRUE;
     result = xfer_send(data, buf, blen, blen, &nwritten);
     if(result)
index a6b7d4e8e29f4ff59942fafdf2502de263d9165d..3cc0d8630b52bcc6802506e84d726f0cb6b44088 100644 (file)
@@ -5316,6 +5316,17 @@ static CURLcode ossl_send(struct Curl_cfilter *cf,
 
   connssl->io_need = CURL_SSL_IO_NEED_NONE;
   memlen = (len > (size_t)INT_MAX) ? INT_MAX : (int)len;
+  if(octx->blocked_ssl_write_len && (octx->blocked_ssl_write_len != memlen)) {
+    /* The previous SSL_write() call was blocked, using that length.
+     * We need to use that again or OpenSSL will freak out. A shorter
+     * length should not happen and is a bug in libcurl. */
+    if(octx->blocked_ssl_write_len > memlen) {
+      DEBUGASSERT(0);
+      return CURLE_BAD_FUNCTION_ARGUMENT;
+    }
+    memlen = octx->blocked_ssl_write_len;
+  }
+  octx->blocked_ssl_write_len = 0;
   nwritten = SSL_write(octx->ssl, mem, memlen);
 
   if(nwritten > 0)
@@ -5326,16 +5337,19 @@ static CURLcode ossl_send(struct Curl_cfilter *cf,
     switch(err) {
     case SSL_ERROR_WANT_READ:
       connssl->io_need = CURL_SSL_IO_NEED_RECV;
+      octx->blocked_ssl_write_len = memlen;
       result = CURLE_AGAIN;
       goto out;
     case SSL_ERROR_WANT_WRITE:
       result = CURLE_AGAIN;
+      octx->blocked_ssl_write_len = memlen;
       goto out;
     case SSL_ERROR_SYSCALL:
     {
       int sockerr = SOCKERRNO;
 
       if(octx->io_result == CURLE_AGAIN) {
+        octx->blocked_ssl_write_len = memlen;
         result = CURLE_AGAIN;
         goto out;
       }
index 581afee068271492a5970d9fcf62659b3a81457e..54cfc5d663283173467718f9d2695c765ac3682b 100644 (file)
@@ -68,6 +68,8 @@ struct ossl_ctx {
   X509*    server_cert;
   BIO_METHOD *bio_method;
   CURLcode io_result;       /* result of last BIO cfilter operation */
+  /* blocked writes need to retry with same length, remember it */
+  int      blocked_ssl_write_len;
 #ifndef HAVE_KEYLOG_CALLBACK
   /* Set to true once a valid keylog entry has been created to avoid dupes.
      This is a bool and not a bitfield because it is passed by address. */
index 462e7645dfa1f2d96851114df5f022ad6b0723a6..4ec781148c943539567f1d794b19dccaff8671cc 100644 (file)
@@ -754,7 +754,7 @@ class TestDownload:
 
     # download with looong urls
     @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
-    @pytest.mark.parametrize("url_junk", [1024, 16*1024, 32*1024, 64*1024])
+    @pytest.mark.parametrize("url_junk", [1024, 16*1024, 32*1024, 64*1024, 80*1024, 96*1024])
     def test_02_36_looong_urls(self, env: Env, httpd, nghttpx, proto, url_junk):
         if proto == 'h3' and not env.have_h3():
             pytest.skip("h3 not supported")
@@ -784,6 +784,11 @@ class TestDownload:
                 # h2 is unable to send such large headers (frame limits)
                 r.check_exit_code(55)
             elif proto == 'h3':
-                r.check_exit_code(0)
-                # nghttpx reports 431 Request Header Field too Large
-                r.check_response(http_status=431)
+                if url_junk <= 64*1024:
+                    r.check_exit_code(0)
+                    # nghttpx reports 431 Request Header Field too Large
+                    r.check_response(http_status=431)
+                else:
+                    # nghttpx destroys the connection with internal error
+                    # ERR_QPACK_HEADER_TOO_LARGE
+                    r.check_exit_code(56)