Skip to content

asyncio.create_unix_server has an off-by-one error concerning the backlog parameter #90871

Closed
@jnsnow

Description

@jnsnow
mannequin
BPO 46715
Nosy @asvetlov, @1st1, @jnsnow
Files
  • issue.patch
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = None
    created_at = <Date 2022-02-10.21:24:35.963>
    labels = ['type-bug', '3.8', '3.9', '3.10', '3.11', '3.7', 'expert-asyncio']
    title = 'asyncio.create_unix_server has an off-by-one error concerning the backlog parameter'
    updated_at = <Date 2022-02-10.21:24:35.963>
    user = 'https://github.com/jnsnow'

    bugs.python.org fields:

    activity = <Date 2022-02-10.21:24:35.963>
    actor = 'jnsnow'
    assignee = 'none'
    closed = False
    closed_date = None
    closer = None
    components = ['asyncio']
    creation = <Date 2022-02-10.21:24:35.963>
    creator = 'jnsnow'
    dependencies = []
    files = ['50618']
    hgrepos = []
    issue_num = 46715
    keywords = ['patch']
    message_count = 1.0
    messages = ['413025']
    nosy_count = 3.0
    nosy_names = ['asvetlov', 'yselivanov', 'jnsnow']
    pr_nums = []
    priority = 'normal'
    resolution = None
    stage = None
    status = 'open'
    superseder = None
    type = 'behavior'
    url = 'https://bugs.python.org/issue46715'
    versions = ['Python 3.7', 'Python 3.8', 'Python 3.9', 'Python 3.10', 'Python 3.11']

    Linked PRs

    Activity

    jnsnow

    jnsnow commented on Feb 10, 2022

    @jnsnow
    MannequinAuthor

    Hi, asyncio.create_unix_server appears to treat the "backlog" parameter as where 0 means that *no connection will ever possibly be pending*, which (at the very least for UNIX sockets on my machine) is untrue.

    Consider a (non-asyncio) server:

    import os, socket, sys, time
    
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind('test.sock')
    
    sock.listen(backlog=0)
    
    while True:
        print('.', end='', file=sys.stderr)
        time.sleep(1)

    This server never calls accept(), and uses a backlog of zero. However, a client can actually still successfully call connect against such a server:

    import os, socket, time
    
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.setblocking(False)
    
    sock.connect('test.sock')
    print("Connected!")

    When run against the server example, the first invocation of this client will actually connect successfully (Surprising, but that's how the C syscalls work too, so... alright) but the second invocation of this client will raise BlockingIOError (EAGAIN).

    Further, if we amend the first server example to actually call accept(), it will succeed when the first client connects -- demonstrating that the actual total queue length here was actually effectively 1, not 0.

    (i.e. there's always room for at least one connection to be considered, and the backlog counts everybody else.)

    However, in asyncio.BaseSelectorEventLoop._accept_connection(...), the code uses for _ in range(backlog) to determine the maximum number of accept calls to make. When backlog is set to zero, this means we will never call accept, even when there are pending connections.

    Note that when backlog=1, this actually allows for *two* pending connections before clients are rejected, but this loop will only fire once. This behavior is surprising, because backlog==0 means we'll accept no clients, but backlog==1 means we will allow for two to enqueue before accepting both. There is seemingly no way with asyncio to actually specify "Exactly one pending connection".

    I think this loop should be amended to reflect the actual truth of the backlog parameter, and it should iterate over backlog + 1. This does necessitate a change to [Lib/test/test_asyncio/test_selector_events.py](https://github.com/python/cpython/blob/main/Lib/test/test_asyncio/test_selector_events.py) which believes that backlog=100 means that accept() should be called 100 times (instead of 101.)

    A (very) simple fix is attached here; if it seems sound, I can spin a real PR on GitHub.

    added
    3.9only security fixes
    3.10only security fixes
    3.11only security fixes
    type-bugAn unexpected behavior, bug, or error
    on Feb 10, 2022

    26 remaining items

    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

      Projects

      Status

      Done

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

        asyncio.create_unix_server has an off-by-one error concerning the backlog parameter · Issue #90871 · python/cpython

        Follow Lee on X/Twitter - Father, Husband, Serial builder creating AI, crypto, games & web tools. We are friends :) AI Will Come To Life!

        Check out: eBank.nz (Art Generator) | Netwrck.com (AI Tools) | Text-Generator.io (AI API) | BitBank.nz (Crypto AI) | ReadingTime (Kids Reading) | RewordGame | BigMultiplayerChess | WebFiddle | How.nz | Helix AI Assistant