Skip to content

ResourceWarning in ssl.SSLSocket connected detection #113280

@graingert

Description

@graingert

Bug report

Bug description:

from __future__ import annotations

import sys
import contextlib
import socket
import ssl
import typing

import threading
import os

ALPN_PROTOCOLS = ["http/1.1"]

CERTS_PATH = os.path.join(os.path.dirname(__file__), "certs")
DEFAULT_CERTS: dict[str, typing.Any] = {
    "certfile": os.path.join(CERTS_PATH, "server.crt"),
    "keyfile": os.path.join(CERTS_PATH, "server.key"),
    "cert_reqs": ssl.CERT_OPTIONAL,
    "ca_certs": os.path.join(CERTS_PATH, "cacert.pem"),
    "alpn_protocols": ALPN_PROTOCOLS,
}
DEFAULT_CA = os.path.join(CERTS_PATH, "cacert.pem")
DEFAULT_CA_KEY = os.path.join(CERTS_PATH, "cacert.key")


class SocketServerThread(threading.Thread):
    """
    :param socket_handler: Callable which receives a socket argument for one
        request.
    :param ready_event: Event which gets set when the socket handler is
        ready to receive requests.
    """

    USE_IPV6 = False

    def __init__(
        self,
        socket_handler: typing.Callable[[socket.socket], None],
        host: str = "localhost",
        ready_event: threading.Event | None = None,
    ) -> None:
        super().__init__()
        self.daemon = True

        self.socket_handler = socket_handler
        self.host = host
        self.ready_event = ready_event

    def _start_server(self) -> None:
        sock = socket.socket(socket.AF_INET)
        if sys.platform != "win32":
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        with sock:
            sock.bind((self.host, 0))
            self.port = sock.getsockname()[1]

            # Once listen() returns, the server socket is ready
            sock.listen(1)

            if self.ready_event:
                self.ready_event.set()

            self.socket_handler(sock)

    def run(self) -> None:
        self._start_server()


class NewConnectionError(Exception):
    pass


def original_ssl_wrap_socket(
    sock: socket.socket,
    keyfile: StrOrBytesPath | None = None,
    certfile: StrOrBytesPath | None = None,
    server_side: bool = False,
    cert_reqs: ssl.VerifyMode = ssl.CERT_NONE,
    ssl_version: int = ssl.PROTOCOL_TLS_SERVER,
    ca_certs: str | None = None,
    do_handshake_on_connect: bool = True,
    suppress_ragged_eofs: bool = True,
    ciphers: str | None = None,
) -> ssl.SSLSocket:
    if server_side and not certfile:
        raise ValueError("certfile must be specified for server-side operations")
    if keyfile and not certfile:
        raise ValueError("certfile must be specified")
    context = ssl.SSLContext(ssl_version)
    context.verify_mode = cert_reqs
    if ca_certs:
        context.load_verify_locations(ca_certs)
    if certfile:
        context.load_cert_chain(certfile, keyfile)
    if ciphers:
        context.set_ciphers(ciphers)
    return context.wrap_socket(
        sock=sock,
        server_side=server_side,
        do_handshake_on_connect=do_handshake_on_connect,
        suppress_ragged_eofs=suppress_ragged_eofs,
    )


@contextlib.contextmanager
def _socket_server(handler):
    ready_event = threading.Event()
    server_thread = SocketServerThread(
        socket_handler=handler, ready_event=ready_event, host="localhost"
    )
    server_thread.start()
    ready_event.wait(5)
    if not ready_event.is_set():
        raise Exception("timeout")
    try:
        yield server_thread.port
    finally:
        server_thread.join()


def _test_ssl_failed_fingerprint_verification() -> None:
    def socket_handler(listener: socket.socket) -> None:
        with listener.accept()[0] as sock:
            try:
                ssl_sock = original_ssl_wrap_socket(
                    sock,
                    server_side=True,
                    keyfile=DEFAULT_CERTS["keyfile"],
                    certfile=DEFAULT_CERTS["certfile"],
                    ca_certs=DEFAULT_CA,
                )
            except (ssl.SSLError, ConnectionResetError):
                pass
            else:
                with ssl_sock:
                    ssl_sock.send(
                        b"HTTP/1.1 200 OK\r\n"
                        b"Content-Type: text/plain\r\n"
                        b"Content-Length: 5\r\n\r\n"
                        b"Hello"
                    )

    with _socket_server(socket_handler) as port:
        # GitHub's fingerprint. Valid, but not matching.
        def request() -> None:
            try:
                try:
                    sock = socket.create_connection(
                        ("localhost", port),
                        source_address=None,
                    )
                except OSError as e:
                    raise NewConnectionError(None, None)
                ssl.create_default_context().wrap_socket(
                    sock, server_hostname="localhost"
                ).close()
            except BaseException as e:
                err = e
                raise

        with contextlib.suppress(ssl.SSLCertVerificationError):
            request()
        # Should not hang, see https://github.com/urllib3/urllib3/issues/529
        with contextlib.suppress(NewConnectionError):
            request()


def test_foo():
    for i in range(100):
        print(i)
        _test_ssl_failed_fingerprint_verification()


def test_gc():
    import gc

    for i in range(5):
        gc.collect()


def main():
    test_foo()
    test_gc()


if __name__ == "__main__":
    sys.exit(main())

run like this:

python -W error test_socketlevel.py
0
1
2
3
4
Traceback (most recent call last):
  File "/usr/lib/python3.12/ssl.py", line 992, in _create
    self.getpeername()
OSError: [Errno 107] Transport endpoint is not connected

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/graingert/projects/demo-resource-warning/test_socketlevel.py", line 188, in <module>
    sys.exit(main())
             ^^^^^^
  File "/home/graingert/projects/demo-resource-warning/test_socketlevel.py", line 183, in main
    test_foo()
  File "/home/graingert/projects/demo-resource-warning/test_socketlevel.py", line 172, in test_foo
    _test_ssl_failed_fingerprint_verification()
  File "/home/graingert/projects/demo-resource-warning/test_socketlevel.py", line 166, in _test_ssl_failed_fingerprint_verification
    request()
  File "/home/graingert/projects/demo-resource-warning/test_socketlevel.py", line 155, in request
    ssl.create_default_context().wrap_socket(
  File "/usr/lib/python3.12/ssl.py", line 455, in wrap_socket
    return self.sslsocket_class._create(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/ssl.py", line 1004, in _create
    notconn_pre_handshake_data = self.recv(1)
                                 ^^^^^^^^^^^^
  File "/usr/lib/python3.12/ssl.py", line 1237, in recv
    return super().recv(buflen, flags)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
ConnectionResetError: [Errno 104] Connection reset by peer
Exception ignored in: <ssl.SSLSocket fd=5, family=2, type=1, proto=6, laddr=('127.0.0.1', 54864)>
ResourceWarning: unclosed <ssl.SSLSocket fd=5, family=2, type=1, proto=6, laddr=('127.0.0.1', 54864)>

certs from here https://github.com/graingert/demo-resource-warning

CPython versions tested on:

3.12

Operating systems tested on:

Linux

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.11only security fixes3.12only security fixes3.13bugs and security fixesstdlibStandard Library Python modules in the Lib/ directorytopic-SSLtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions