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
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
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