Skip to content

asyncio.BaseSelectorEventLoop._sock_accept can raise InvalidStateError and drop a pending connection #153761

Description

@deadlovelll

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    type-bugAn unexpected behavior, bug, or error

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions