Skip to content

asyncio: socket leak and silent error when transport creation fails for an accepted connection #155934

Description

@danielKim614

Bug report

Bug description

When an incoming connection has been accepted and protocol_factory() (or transport creation) then raises inside BaseSelectorEventLoop._accept_connection2() (Lib/asyncio/selector_events.py), two things go wrong.

First, The accepted socket is never closed. Ownership of conn only passes to the transport once it is created; if creation fails, nothing closes the socket, so the fd leaks until garbage collection
(Log: ResourceWarning: unclosed <socket.socket ...> with the server's address as laddr).

Second, The error is reported only in debug mode. The except BaseException block calls call_exception_handler() behind if self._debug:, so with the default debug=False the failure is not reported anywhere. The client sees an established connection that never answers (the TCP handshake and ACKs come from the kernel), while the server keeps no record that the connection ever existed.

This is the server-side counterpart of gh-153133, which fixed the same socket leak in create_connection() (gh-153134).
It is also inconsistent with _accept_connection() itself, which reports accept-stage errors (e.g. EMFILE, ENFILE, ...) via call_exception_handler() unconditionally.

Reproducer

import asyncio
import sys

async def main(debug):
    loop = asyncio.get_running_loop()
    loop.set_debug(debug)
    handler_calls = []
    loop.set_exception_handler(lambda l, ctx: handler_calls.append(ctx.get("message")))

    def bad_factory():
        raise RuntimeError("boom in protocol_factory")

    server = await loop.create_server(bad_factory, "127.0.0.1", 0)
    port = server.sockets[0].getsockname()[1]

    reader, writer = await asyncio.open_connection("127.0.0.1", port)
    await asyncio.sleep(0.2)

    writer.close()
    server.close()
    await server.wait_closed()
    print(f"debug={debug}  exception_handler calls={len(handler_calls)}  {handler_calls}")

asyncio.run(main(debug=(sys.argv[1:] == ["1"])))

Output on current main:

$ ./python repro.py
debug=False  exception_handler calls=
ResourceWarning: unclosed <socket.socket fd=8, ..., laddr=('127.0.0.1', <server port>), ...>

$ ./python repro.py 1
debug=True  exception_handler calls=1ation for incoming connection']

The leaked socket's laddr is the server port, it is the accepted server-side socket.

Suggested fix

Mirror gh-153134 on the server side. When the transport was never created, close conn and report the exception unconditionally. Reporting should stay limited to that case because the same except block also sees await waiter failures such as SSL handshake errors from port scanners, and reporting those unconditionally would be noisy.

I'd like to work on this as part of the PyCon KR sprint. @hugovk @corona10

Found while verifying entries from [devdanzin's stdlib audit catalog] n.22 (https://gist.github.com/devdanzin/3198710e3c0128fda5e0a7b4e0768e5f).

CPython versions tested on:

CPython main branch

Operating systems tested on:

macOS

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytopic-asynciotype-bugAn unexpected behavior, bug, or error

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions