Skip to content

Socket leak in TCPClient.connect when TLS handshake times out #3614

@ptazithos

Description

@ptazithos

When TCPClient.connect is called with both ssl_options and timeout, a TLS handshake timeout causes the underlying socket to leak. The socket becomes unreachable from the caller and is never closed.

Affected Code

tornado/tcpclient.py lines 284-290:

if ssl_options is not None:
    if timeout is not None:
        stream = await gen.with_timeout(
            timeout,
            stream.start_tls(
                False, ssl_options=ssl_options, server_hostname=host
            ),
        )

Root Cause

IOStream.start_tls() transfers socket ownership to a new SSLIOStream before the TLS handshake completes:

  1. start_tls() extracts the raw socket from the original IOStream and sets self.socket = None (iostream.py:1251)
  2. It wraps the socket in SSL and creates a new SSLIOStream as a local variable (iostream.py:1263)
  3. It returns a Future that resolves only when the handshake completes

When gen.with_timeout fires:

  • TimeoutError is raised to the caller
  • The caller holds a reference to the original IOStream, whose .socket is already None -- calling stream.close() is a no-op
  • The new SSLIOStream (holding the real socket) is only reachable through the inner Future
  • gen.with_timeout explicitly does not cancel the wrapped Future (documented at gen.py:609)
  • The SSLIOStream remains registered on the IOLoop, waiting for a handshake that will never complete
  • The socket file descriptor is leaked permanently

Workaround

from tornado.iostream import SSLIOStream
from tornado.ioloop import IOLoop
from tornado import gen


async def tcp_connect_ssl(client, host, port, ssl_options, timeout=5):
    deadline = IOLoop.current().time() + timeout
    stream = await client.connect(host, port, timeout=timeout)

    sock = stream.socket
    stream.io_loop.remove_handler(sock)
    stream.socket = None

    ssl_sock = ssl_options.wrap_socket(
        sock, server_hostname=host, do_handshake_on_connect=False,
    )
    ssl_stream = SSLIOStream(ssl_sock, ssl_options=ssl_options)

    try:
        await gen.with_timeout(deadline, ssl_stream.wait_for_handshake())
    except Exception:
        ssl_stream.close()
        raise
    return ssl_stream

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions