]> git.eshelyaron.com Git - emacs.git/commitdiff
Block TLS handshake until TCP connection established
authorMattias Engdegård <mattiase@acm.org>
Mon, 12 Jul 2021 11:58:28 +0000 (13:58 +0200)
committerMattias Engdegård <mattiase@acm.org>
Tue, 13 Jul 2021 17:07:41 +0000 (19:07 +0200)
If a TLS handshake is attempted before the completion of an
asynchronous TCP connection has been ascertained, our local state will
not be set up correctly for further progress and the sentinel "open"
event will never be sent.  This can occur if sufficient time passes
after the initiation of an async TCP connection so that by the time
`wait_reading_process_output` is called, the connection has already
been established on the TCP level.

This somewhat timing-sensitive bug has plagued HTTPS connections on
some platforms, notably macOS, for a long time (bug#49449).

* src/process.c (wait_reading_process_output): Gate the TLS handshake
by the NON_BLOCKING_CONNECT_FD flag.  The flag will be cleared as soon
as the TCP socket is found to be writable.
* test/src/process-tests.el (process-async-https-with-delay):
New test.

src/process.c
test/src/process-tests.el

index b8c3e4ecfbcbb4dd0792223d84a896c7e543b126..c3186eed750ab23fe51923e06a1e479707136eb1 100644 (file)
@@ -5232,7 +5232,10 @@ wait_reading_process_output (intmax_t time_limit, int nsecs, int read_kbd,
 #ifdef HAVE_GNUTLS
                /* Continue TLS negotiation. */
                if (p->gnutls_initstage == GNUTLS_STAGE_HANDSHAKE_TRIED
-                   && p->is_non_blocking_client)
+                   && p->is_non_blocking_client
+                   /* Don't proceed until we have established a connection. */
+                   && !(fd_callback_info[p->outfd].flags
+                        & NON_BLOCKING_CONNECT_FD))
                  {
                    gnutls_try_handshake (p);
                    p->gnutls_handshakes_tried++;
index 1774f2fc74e2845a151b9c9470cc024efcb6740b..9bab523708ec9dfde80a2882ca08c99e3fa7b950 100644 (file)
@@ -28,6 +28,7 @@
 (require 'puny)
 (require 'subr-x)
 (require 'dns)
+(require 'url-http)
 
 ;; Timeout in seconds; the test fails if the timeout is reached.
 (defvar process-test-sentinel-wait-timeout 2.0)
@@ -916,5 +917,34 @@ Return nil if FILENAME doesn't exist."
       ;; ...and the change description should be "interrupt".
       (should (equal '("interrupt\n") events)))))
 
+(ert-deftest process-async-https-with-delay ()
+  "Bug#49449: asynchronous TLS connection with delayed completion."
+  (skip-unless (and internet-is-working (gnutls-available-p)))
+  (let* ((status nil)
+         (buf (url-http
+                 #s(url "https" nil nil "elpa.gnu.org" nil
+                        "/packages/archive-contents" nil nil t silent t t)
+                 (lambda (s) (setq status s))
+                 '(nil) nil 'tls)))
+    (unwind-protect
+        (progn
+          ;; Busy-wait for 1 s to allow for the TCP connection to complete.
+          (let ((delay 1.0)
+                (t0 (float-time)))
+            (while (< (float-time) (+ t0 delay))))
+          ;; Wait for the entire operation to finish.
+          (let ((limit 4.0)
+                (t0 (float-time)))
+            (while (and (null status)
+                        (< (float-time) (+ t0 limit)))
+              (sit-for 0.1)))
+          (should status)
+          (should-not (assq :error status))
+          (should buf)
+          (should (> (buffer-size buf) 0))
+          )
+      (when buf
+        (kill-buffer buf)))))
+
 (provide 'process-tests)
 ;;; process-tests.el ends here