Bug report
Bug description:
BaseSelectorEventLoop._sock_accept doesn't check whether its future is already done before using it, unlike the others (_sock_recv, _sock_recv_into, _sock_recvfrom etc). When _sock_accept calls with already cancelled future, it will raise InvalidStateError and connection is dropped instead of being left for the next calls
def _sock_accept(self, fut, sock): fd = sock.fileno() try: conn, address = sock.accept() conn.setblocking(False) except (BlockingIOError, InterruptedError): self._ensure_fd_no_transport(fd) handle = self._add_reader(fd, self._sock_accept, fut, sock) fut.add_done_callback( functools.partial(self._sock_read_done, fd, handle=handle)) except (SystemExit, KeyboardInterrupt): raise except BaseException as exc: fut.set_exception(exc) else: fut.set_result((conn, address))
Reproducer:
import asyncio import select import socket async def main(): errors = [] loop = asyncio.get_running_loop() loop.set_exception_handler(lambda l, c: errors.append(c)) lsock = socket.socket() lsock.setblocking(False) lsock.bind(("127.0.0.1", 0)) lsock.listen() acc = loop.create_task(loop.sock_accept(lsock)) await asyncio.sleep(0) client = socket.socket() client.connect(lsock.getsockname()) select.select([lsock], [], []) loop.call_soon(acc.cancel) await asyncio.sleep(0) await asyncio.sleep(0) print(errors) try: await asyncio.wait_for(loop.sock_accept(lsock), 0.5) print("ALIVE") except TimeoutError: print("DEAD") asyncio.run(main())
Expected:
[]
ALIVE
Actually:
[{'message': 'Exception in callback BaseSelectorEventLoop._sock_accept()', 'exception': InvalidStateError('invalid state'), 'handle': <Handle cancelled>}]
DEAD
Proposed fix is to add the guard:
def _sock_accept(self, fut, sock): if fut.done(): return fd = sock.fileno() try: conn, address = sock.accept() conn.setblocking(False) except (BlockingIOError, InterruptedError): self._ensure_fd_no_transport(fd) handle = self._add_reader(fd, self._sock_accept, fut, sock) fut.add_done_callback( functools.partial(self._sock_read_done, fd, handle=handle)) except (SystemExit, KeyboardInterrupt): raise except BaseException as exc: fut.set_exception(exc) else: fut.set_result((conn, address))
I have a fix ready
CPython versions tested on:
CPython main branch
Operating systems tested on:
macOS
Linked PRs
- gh-153761: Fix asyncio sock_accept() dropping a connection when cancelled #153762
- [3.13] gh-153761: Fix asyncio sock_accept() dropping a connection when cancelled (GH-153762) #153827
- [3.14] gh-153761: Fix asyncio sock_accept() dropping a connection when cancelled (GH-153762) #153828
- [3.15] gh-153761: Fix asyncio sock_accept() dropping a connection when cancelled (GH-153762) #153829