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 insideBaseSelectorEventLoop._accept_connection2()(Lib/asyncio/selector_events.py), two things go wrong.First, The accepted socket is never closed. Ownership of
connonly passes to the transport once it is created; if creation fails, nothing closes the socket, so thefdleaks until garbage collection(Log:
ResourceWarning: unclosed <socket.socket ...>with the server's address asladdr).Second, The error is reported only in debug mode. The
except BaseExceptionblock callscall_exception_handler()behindif self._debug:, so with the defaultdebug=Falsethe 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, ...) viacall_exception_handler()unconditionally.Reproducer
Output on current main:
The leaked socket's
laddris 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
connand report the exception unconditionally. Reporting should stay limited to that case because the sameexceptblock also seesawait waiterfailures 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