Skip to content

Connect timeout is ignored for TLS connections #646

Description

@juanjosegarciaripoll

IMAP4_TLS stores the connect timeout it is given and then never reads it,
so IMAPClient(..., ssl=True, timeout=SocketTimeout(connect=N, ...)) leaves
the socket blocking. The handshake runs to the kernel's SYN retry ceiling
instead of to N — about 130 s on Linux with the default
tcp_syn_retries=6. The non-TLS path is unaffected.

This bites whenever SYNs are dropped rather than refused: a silent packet
filter, a blackholed ECMP path member, a host that has gone away. The
application asked for a bounded connect and does not get one.

Version: imapclient 3.1.0, Python 3.13, Linux

Reproducing

repro.py connects to 192.0.2.1 (RFC 5737 TEST-NET-1, routed nowhere, drops
SYNs) with a 5 second connect timeout, once with ssl=False and once with
ssl=True:

imapclient 3.1.0, connect timeout 5.0s

ssl=False    gave up after    5.0s  (TimeoutError)  OK
ssl=True     gave up after  134.0s  (TimeoutError)  BUG: timeout ignored

Both should give up after ~5 s.

Cause

imapclient/tls.py:

def __init__(self, host, port=993, ssl_context=None, timeout=None):
    self.ssl_context = ssl_context
    self._timeout = timeout           # line 39: stored
    super().__init__(host, port)      # line 40: not passed on

def _create_socket(self, timeout):    # line 42
    sock = socket.create_connection((self.host, self.port), timeout=timeout)

imaplib.IMAP4.__init__ has signature (self, host='', port=IMAP4_PORT, timeout=None), so the call on line 40 leaves timeout at None and passes
that straight to self.open(host, port, timeout), which hands it to
_create_socket. _create_socket reads only its argument, so it connects
with timeout=None.

self._timeout is written on line 39 and never read anywhere — it is the
only occurrence of the name in the file:

$ grep -rn _timeout imapclient/tls.py
39:        self._timeout = timeout

The non-TLS sibling imapclient/imap4.py already does the right thing, which
is why only ssl=True is affected:

def _create_socket(self, timeout=None):
    return socket.create_connection(
        (self.host, self.port), timeout if timeout is not None else self._timeout
    )

Suggested fix

Apply the same fallback in IMAP4_TLS._create_socket. PATCH.diff:

--- a/imapclient/tls.py
+++ b/imapclient/tls.py
@@ -39,7 +39,9 @@
         self._timeout = timeout
         super().__init__(host, port)

-    def _create_socket(self, timeout: Optional[float]) -> socket.socket:
+    def _create_socket(self, timeout: Optional[float] = None) -> socket.socket:
+        if timeout is None:
+            timeout = self._timeout
         sock = socket.create_connection((self.host, self.port), timeout=timeout)

         return wrap_socket(sock, self.ssl_context, self.host)

With it applied, the reproduction gives:

ssl=False    gave up after    5.0s  (TimeoutError)  OK
ssl=True     gave up after    5.0s  (TimeoutError)  OK

The default on the parameter keeps _create_socket callable with no argument,
matching imap4.IMAP4WithTimeout and imaplib's own callers.

repro.py

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions