GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — create_connection(sock=sock) does not detach socket on cancellation, causing fd double-close · Issue #738 · MagicStack/uvloop

create_connection(sock=sock) does not detach socket on cancellation, causing fd double-close #738

New issue
Copy link
New issue
Copy link
Closed
#740
Closed
create_connection(sock=sock) does not detach socket on cancellation, causing fd double-close#738
#740
Copy link

Description

@junjzhang
junjzhang
opened on Mar 19, 2026
Issue body actions

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 2060
waiter = self._new_future()
tr = TCPTransport.new(self, protocol, None, waiter, context)
try:
    tr._open(sock.fileno())      # libuv takes over the fd
    tr._init_protocol()          # _transports[fd] = tr
    await waiter                 # ← cancellation point
except BaseException:
    tr._close()                  # libuv closes fd, but sock doesn't know
    raise

tr._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

import asyncio
import socket
import uvloop

async def test(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)
    except BlockingIOError:
        pass
    await asyncio.sleep(0.01)

    task = asyncio.ensure_future(loop.create_connection(asyncio.Protocol, sock=client))
    await asyncio.sleep(0)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        pass

    print(f"uvloop:  sock.fileno() = {client.fileno()}")  # NOT -1 → bug
    srv.close()
    try:
        client.close()
    except OSError:
        pass

# uvloop — socket NOT detached
uvloop.install()
loop = asyncio.new_event_loop()
loop.run_until_complete(test(loop))
loop.close()

# standard asyncio — socket properly detached
asyncio.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:

except BaseException:
    tr._close()
    if sock is not None:
        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
Reacted by Matthew Lapointe and Huaiyi Zhao

Activity

  1. junjzhang
    added a commit that references this issue on Mar 23, 2026

    Detach socket on create_connection/create_unix_connection cancellation

    This issue will close once commit 920c2b9 is merged into the 'master' branch.
    920c2b9
  2. fantix
    closed this as completedin #740on May 1, 2026
  3. fantix
    added a commit that references this issue on May 1, 2026

    Detach socket on create_connection cancellation to prevent fd double-…

    dc680eb
  4. jensbjorgensen
    added a commit that references this issue on Oct 6, 2026

    uvloop 0.23.0

    c5f7deb
  5. revmischa
    added a commit that references this issue on Oct 12, 2026

    fix(relay): bump uvloop to 0.23.0 to fix a double-closed file descriptor

    a276da6
  6. dmitrii
    added a commit that references this issue on Oct 12, 2026

    fix(relay): bump uvloop to 0.23.0 to fix a double-closed file descrip…

    4d1beec
  7. jackmisbach
    added a commit that references this issue on Oct 20, 2026

    fix(api): bump uvloop to 0.23.0 to stop cross-connection response mis…

    3f545c2
  8. SumanthRH
    added a commit that references this issue on Oct 29, 2026

    [fix] Run the sampler weight broadcast off the event loop (#2524)

    a45caba
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.