Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

multiprocessing.connection.Client deadlocks when trying to connect to a listener without a password #123736

Open
MarcinKonowalczyk opened this issue Sep 5, 2024 · 4 comments
Labels
stdlib Python modules in the Lib dir topic-multiprocessing type-bug An unexpected behavior, bug, or error

Comments

@MarcinKonowalczyk
Copy link

MarcinKonowalczyk commented Sep 5, 2024

Bug report

Bug description:

We set up a multiprocessing.connection.Listener with no authkey. When we try to connect to it with a Client with an authkey, the Client deadlocks. Here is a working example:

import socket
import threading
import time
from multiprocessing.connection import Client, Listener


def _test(*, listener: str | None, client: str | None) -> tuple[Exception | None, Exception | None, list]:
    _autkey_listener = listener.encode() if listener else None
    _authkey_client = client.encode() if client else None

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind(("localhost", 0))
        address = s.getsockname()

    thread_exc = None
    thread_got = []
    client_exc = None

    def target() -> None:
        try:
            with Listener(address, authkey=_autkey_listener) as l:
                c = l.accept()
                while True:
                    value = c.recv()
                    if not value:
                        break
                    thread_got.append(value)
        except Exception as e:
            nonlocal thread_exc
            thread_exc = e

    t = threading.Thread(target=target)
    t.start()

    try:
        with Client(address, authkey=_authkey_client) as c:
            c.send("hello")
            time.sleep(0.1)
            c.send(None)

        t.join()
    except Exception as e:
        client_exc = e

    return thread_exc, client_exc, thread_got


if __name__ == "__main__":
    print(" no passwords ".center(40, "-"))
    print(_test(listener=None, client=None))

    print(" two matching passwords ".center(40, "-"))
    print(_test(listener="password", client="password"))

    print(" client has wrong password ".center(40, "-"))
    print(_test(listener="password", client="wrong"))

    print(" client has no password ".center(40, "-"))
    print(_test(listener="password", client=None))

    # This is the case which deadlocks
    print(" listener has no password ".center(40, "-"))
    print(_test(listener=None, client="password"))

which outputs:

------------- no passwords -------------
(None, None, ['hello'])
-------- two matching passwords --------
(None, None, ['hello'])
------ client has wrong password -------
(AuthenticationError('digest received was wrong'), AuthenticationError('digest sent was rejected'), [])
-------- client has no password --------
(AuthenticationError("expected 'md5' of length 16 got 20"), None, [])
------- listener has no password -------

and then stops.

Upon some investigation, the following bit of code from Client is to blame:

  if authkey is not None:
      answer_challenge(c, authkey)
      deliver_challenge(c, authkey)

since the listener is not configured to deliver the challenge to the connections. I think this would be resolved with an optional timeout kwarg in answer_challenge. If this suggestion is accepted I would be very happy to work on it 👍

CPython versions tested on:

3.9, 3.10, 3.11, 3.12

Operating systems tested on:

macOS, Windows

@MarcinKonowalczyk MarcinKonowalczyk added the type-bug An unexpected behavior, bug, or error label Sep 5, 2024
@picnixz picnixz added topic-multiprocessing stdlib Python modules in the Lib dir labels Sep 5, 2024
@picnixz
Copy link
Contributor

picnixz commented Sep 5, 2024

I'm not sure how to categorize it. I feel it's a bug because we don't want the client to possibly be deadlocked. On the other hand, I'd say it's a feature because when setting the connection, you'd expect both participants to at least agree on the kind of protocol (namely, at least agree on whether an authentication is needed or not).

I am personally in favor of adding a timeout keyword argument to def Client(...) which would times out if the answer/delivery takes too much time:

def Client(..., *, timeout=None): ...
def answer_challenge(client, authkey, *, timeout=None): ...
def deliver_challenge(client, authkey, *, timeout=None): ...

@MarcinKonowalczyk
Copy link
Author

i'd also then add a def accept(self, *, timeout=None): ... on Listener to allow protecting the server from a malicious client who does not reply to deliver_challenge

@MarcinKonowalczyk
Copy link
Author

worth adding this to the Multiprocessing issues project?

also; @picnixz, should i wait a bit for anyone else to have a look (@vstinner ?)? can i attempt adding the timeout and make a PR?

@picnixz
Copy link
Contributor

picnixz commented Sep 6, 2024

I'd prefer waiting for a core dev's opinion on that matter. Personally I think having a timeout makes sense but someone with more insight on this part of the library would be good.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stdlib Python modules in the Lib dir topic-multiprocessing type-bug An unexpected behavior, bug, or error
Projects
Development

No branches or pull requests

2 participants