Summary
When loop.create_connection(protocol_factory, sock=sock) is cancelled (e.g. by a connect timeout), the passed-in Python socket object is never detached. Its __del__ later closes whatever fd the OS recycled into that slot, silently corrupting an unrelated transport.
Standard asyncio is not affected — its transport directly owns the Python socket and properly closes it on the error path, setting fileno() to -1.
Root cause
In loop.pyx (the sock is not None branch of create_connection):
# uvloop/loop.pyx ~line 2060waiter=self._new_future()
tr=TCPTransport.new(self, protocol, None, waiter, context)
try:
tr._open(sock.fileno()) # libuv takes over the fdtr._init_protocol() # _transports[fd] = trawaitwaiter# ← cancellation pointexceptBaseException:
tr._close() # libuv closes fd, but sock doesn't knowraisetr._attach_fileobj(sock) # only reached on success — calls sock.detach() later via _close()
On the success path, _attach_fileobj saves a reference to sock. When the transport is later closed, _close() calls sock.detach(), which sets the socket's internal fd to -1 so GC won't double-close it.
On the error/cancellation path, _attach_fileobj is never called → sock.detach() never happens → the Python socket still believes it owns the (now-recycled) fd number → sock.__del__() closes an unrelated fd.
Reproduction
importasyncioimportsocketimportuvloopasyncdeftest(loop):
srv=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 0))
srv.listen(1)
addr=srv.getsockname()
client=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.setblocking(False)
try:
client.connect(addr)
exceptBlockingIOError:
passawaitasyncio.sleep(0.01)
task=asyncio.ensure_future(loop.create_connection(asyncio.Protocol, sock=client))
awaitasyncio.sleep(0)
task.cancel()
try:
awaittaskexceptasyncio.CancelledError:
passprint(f"uvloop: sock.fileno() = {client.fileno()}") # NOT -1 → bugsrv.close()
try:
client.close()
exceptOSError:
pass# uvloop — socket NOT detacheduvloop.install()
loop=asyncio.new_event_loop()
loop.run_until_complete(test(loop))
loop.close()
# standard asyncio — socket properly detachedasyncio.set_event_loop_policy(None)
loop=asyncio.new_event_loop()
loop.run_until_complete(test(loop))
loop.close()Output:
uvloop: sock.fileno() = 13 ← should be -1
asyncio: sock.fileno() = -1 ← correct
Impact
Under high concurrency with force_close=True (aiohttp) and intermittent connection timeouts, the GC'd socket closes a live transport's fd. This manifests as:
RuntimeError: File descriptor 1768 is used by transport <TCPTransport closed=False reading=True 0x...>
followed by Assertion failed: ok (src/mailbox.cpp:72) → SIGABRT, crashing the process.
Suggested fix
Detach the socket in the error path:
exceptBaseException:
tr._close()
ifsockisnotNone:
sock.detach()
raise
This matches the semantics of standard asyncio, where the transport always takes full ownership of the socket.
Environment
- uvloop 0.21.0
- Python 3.12
- Linux 5.15
Summary
When
loop.create_connection(protocol_factory, sock=sock)is cancelled (e.g. by a connect timeout), the passed-in Pythonsocketobject is never detached. Its__del__later closes whatever fd the OS recycled into that slot, silently corrupting an unrelated transport.Standard asyncio is not affected — its transport directly owns the Python socket and properly closes it on the error path, setting
fileno()to-1.Root cause
In
loop.pyx(thesock is not Nonebranch ofcreate_connection):On the success path,
_attach_fileobjsaves a reference tosock. When the transport is later closed,_close()callssock.detach(), which sets the socket's internal fd to-1so GC won't double-close it.On the error/cancellation path,
_attach_fileobjis never called →sock.detach()never happens → the Python socket still believes it owns the (now-recycled) fd number →sock.__del__()closes an unrelated fd.Reproduction
Output:
Impact
Under high concurrency with
force_close=True(aiohttp) and intermittent connection timeouts, the GC'd socket closes a live transport's fd. This manifests as:followed by
Assertion failed: ok (src/mailbox.cpp:72)→ SIGABRT, crashing the process.Suggested fix
Detach the socket in the error path:
This matches the semantics of standard asyncio, where the transport always takes full ownership of the socket.
Environment