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

os.fork() called from DummyThread confuses threading shutdown logic #102512

Closed
marmarek opened this issue Mar 7, 2023 · 6 comments
Closed

os.fork() called from DummyThread confuses threading shutdown logic #102512

marmarek opened this issue Mar 7, 2023 · 6 comments
Labels
3.11 only security fixes 3.12 bugs and security fixes 3.13 new features, bugs and security fixes topic-multiprocessing type-bug An unexpected behavior, bug, or error

Comments

@marmarek
Copy link
Contributor

marmarek commented Mar 7, 2023

Bug report

threading._shutdown() relies on _main_thread having _tstate_lock not None (there is assert for that). When fork is called from a DummyThread (in my case, that's a thread created by (Py)Qt), it gets promoted to main thread, but remains very simplistic DummyThread. Especially, nobody initializes its _tstate_lock. threading._after_fork() handles the case of current thread not being in _active dict at all (by creating new MainThread object), but it doesn't handle the case of having DummyThread there already. This results in AssertionError in thread shutdown method - which for example confuses multiprocessing.Process (it gets exit code 1, even if the process function was successful).

Reproducer:

#!/usr/bin/python3

import threading
import multiprocessing
import _thread

class Bar(multiprocessing.Process):
    def run(self):
        print("process")

def run_thread(lock):
    # the call to current_thread() is crucial for reproducer - it allocates
    # DummyThread()
    print(f"thread: {threading.current_thread()}")
    p = Bar()
    p.start()
    p.join()
    print(f"proc exit code: {p.exitcode}")
    lock.release()


def main():
    lock = _thread.allocate_lock()
    lock.acquire()
    t = _thread.start_new_thread(run_thread, (lock,))
    # t.join
    lock.acquire()
    print(f"thread exit")

main()

It should print:

thread: <_DummyThread(Dummy-1, started daemon 135243893053120)>
process
proc exit code: 0
thread exit

but it prints:

thread: <_DummyThread(Dummy-1, started daemon 135243893053120)>
process
proc exit code: 1
thread exit

(see exit code difference)

multiprocessing.Process (or rather multiprocessing.popen_fork.Popen._launch() to be specific) swallows the exception, but adding some debug prints there I get:

Traceback (most recent call last):
  File "/usr/lib64/python3.11/multiprocessing/popen_fork.py", line 71, in _launch
    code = process_obj._bootstrap(parent_sentinel=child_r)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.11/multiprocessing/process.py", line 332, in _bootstrap
    threading._shutdown()
  File "/usr/lib64/python3.11/threading.py", line 1553, in _shutdown
    assert tlock is not None
           ^^^^^^^^^^^^^^^^^

Your environment

  • CPython versions tested on: 3.11.2
  • Operating system and architecture: Fedora 37, x86_64

Linked PRs

@marmarek
Copy link
Contributor Author

marmarek commented Mar 7, 2023

Simplified reproducer (not importing multiprocessing, but doing what it does directly):

#!/usr/bin/python3

import threading
import _thread
import os
import traceback

def run_thread(lock):
    # the call to current_thread() is crucial for reproducer - it allocates
    # DummyThread()
    print(f"thread: {threading.current_thread()}")
    if os.fork() == 0:
        print("process")
        try:
            threading._shutdown()
            os._exit(0)
        except:
            traceback.print_exc()
            os._exit(1)
    else:
        retcode = os.wait()
    print(f"proc exit code: {os.WEXITSTATUS(retcode[1])}")
    lock.release()


def main():
    lock = _thread.allocate_lock()
    lock.acquire()
    t = _thread.start_new_thread(run_thread, (lock,))
    # t.join
    lock.acquire()
    print(f"thread exit")

main()

It fails this way:

thread: <_DummyThread(Dummy-1, started daemon 127509727442624)>
process
Traceback (most recent call last):
  File "/tmp/test.py", line 15, in run_thread
    threading._shutdown()
  File "/usr/lib64/python3.11/threading.py", line 1553, in _shutdown
    assert tlock is not None
           ^^^^^^^^^^^^^^^^^
AssertionError
proc exit code: 1
thread exit

marmarek added a commit to marmarek/cpython that referenced this issue Mar 7, 2023
marmarek added a commit to marmarek/cpython that referenced this issue Mar 7, 2023
If os.fork() is called from a DummyThread (a thread started not by the
threading module, in which somebody called threading.current_thread()),
it becomes main thread. Threading shutdown logic relies on the main
thread having tstate_lock, so initialize it for the DummyThread too.

Fixes python#102512
marmarek added a commit to marmarek/cpython that referenced this issue Mar 7, 2023
marmarek added a commit to marmarek/cpython that referenced this issue Mar 7, 2023
If os.fork() is called from a DummyThread (a thread started not by the
threading module, in which somebody called threading.current_thread()),
it becomes main thread. Threading shutdown logic relies on the main
thread having tstate_lock, so initialize it for the DummyThread too.

Fixes python#102512
marmarek added a commit to marmarek/qubes-core-admin-client that referenced this issue Mar 7, 2023
marmarek added a commit to marmarek/qubes-core-admin-client that referenced this issue Mar 7, 2023
marmarek added a commit to marmarek/qubes-core-admin-client that referenced this issue Mar 8, 2023
andy-slac added a commit to lsst/ctrl_mpexec that referenced this issue Apr 12, 2023
The bug causes occasional issues with multi-process pipeline executions
with pytest using concurrent running. This is a known issue
(python/cpython#102512) which will eventually
be fixed in Python, at that time the patch should become no-op and can
be removed.
@gvanrossum
Copy link
Member

@marmarek Do you have enough of a handle on the problem to propose a PR that fixes it?

@marmarek
Copy link
Contributor Author

@marmarek Do you have enough of a handle on the problem to propose a PR that fixes it?

I already did, over half a year ago (it's linked in the issue description). Should I rebase it to get it merged?

@gvanrossum
Copy link
Member

My bad! I stumbled upon this thread and didn't notice that (I blame GitHub :-). It needs to be reviewed, perhaps by @serhiy-storchaka.

@serhiy-storchaka serhiy-storchaka added 3.11 only security fixes 3.12 bugs and security fixes 3.13 new features, bugs and security fixes labels Dec 18, 2023
@serhiy-storchaka
Copy link
Member

Would not be more correct to replace the current _DummyThread with a _MainThread in _after_fork()? Or maybe the current thread of any type with a new _MainThread?

serhiy-storchaka added a commit that referenced this issue Jan 22, 2024
…from a foreign thread (GH-113261)

Always set a _MainThread as a main thread after os.fork() is called from
a thread started not by the threading module.

A new _MainThread was already set as a new main thread after fork if
threading.current_thread() was not called for a foreign thread before fork.
Now, if it was called before fork, the implicitly created _DummyThread will
be turned into _MainThread after fork.

It fixes, in particularly, an incompatibility of _DummyThread with
the threading shutdown logic which relies on the main thread
having tstate_lock.

Co-authored-by: Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Jan 22, 2024
…alled from a foreign thread (pythonGH-113261)

Always set a _MainThread as a main thread after os.fork() is called from
a thread started not by the threading module.

A new _MainThread was already set as a new main thread after fork if
threading.current_thread() was not called for a foreign thread before fork.
Now, if it was called before fork, the implicitly created _DummyThread will
be turned into _MainThread after fork.

It fixes, in particularly, an incompatibility of _DummyThread with
the threading shutdown logic which relies on the main thread
having tstate_lock.

(cherry picked from commit 49785b0)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Co-authored-by: Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
serhiy-storchaka added a commit to miss-islington/cpython that referenced this issue Jan 22, 2024
…ork() called from a foreign thread (pythonGH-113261)

Always set a _MainThread as a main thread after os.fork() is called from
a thread started not by the threading module.

A new _MainThread was already set as a new main thread after fork if
threading.current_thread() was not called for a foreign thread before fork.
Now, if it was called before fork, the implicitly created _DummyThread will
be turned into _MainThread after fork.

It fixes, in particularly, an incompatibility of _DummyThread with
the threading shutdown logic which relies on the main thread
having tstate_lock.

(cherry picked from commit 49785b0)

Co-authored-by: Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
serhiy-storchaka added a commit to serhiy-storchaka/cpython that referenced this issue Jan 22, 2024
…ork() called from a foreign thread (pythonGH-113261)

Always set a _MainThread as a main thread after os.fork() is called from
a thread started not by the threading module.

A new _MainThread was already set as a new main thread after fork if
threading.current_thread() was not called for a foreign thread before fork.
Now, if it was called before fork, the implicitly created _DummyThread will
be turned into _MainThread after fork.

It fixes, in particularly, an incompatibility of _DummyThread with
the threading shutdown logic which relies on the main thread
having tstate_lock.

(cherry picked from commit 49785b0)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Co-authored-by: Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
@serhiy-storchaka
Copy link
Member

Thank you for your report and PR @marmarek. I created a more radical PR based on it. It is not only create a lock for a main thread after fork, it makes it non-demonic and changes it type.

Perhaps a similar should be done for normal threads, but this is a different issue. It can be an instance of a custom Thread subclass, so changing its type may be not safe.

serhiy-storchaka added a commit that referenced this issue Jan 22, 2024
…called from a foreign thread (GH-113261) (GH-114431)

Always set a _MainThread as a main thread after os.fork() is called from
a thread started not by the threading module.

A new _MainThread was already set as a new main thread after fork if
threading.current_thread() was not called for a foreign thread before fork.
Now, if it was called before fork, the implicitly created _DummyThread will
be turned into _MainThread after fork.

It fixes, in particularly, an incompatibility of _DummyThread with
the threading shutdown logic which relies on the main thread
having tstate_lock.

(cherry picked from commit 49785b0)

Co-authored-by: Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
serhiy-storchaka added a commit to miss-islington/cpython that referenced this issue Jan 22, 2024
…alled from a foreign thread (pythonGH-113261)

Always set a _MainThread as a main thread after os.fork() is called from
a thread started not by the threading module.

A new _MainThread was already set as a new main thread after fork if
threading.current_thread() was not called for a foreign thread before fork.
Now, if it was called before fork, the implicitly created _DummyThread will
be turned into _MainThread after fork.

It fixes, in particularly, an incompatibility of _DummyThread with
the threading shutdown logic which relies on the main thread
having tstate_lock.

(cherry picked from commit 49785b0)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Co-authored-by: Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
serhiy-storchaka added a commit that referenced this issue Jan 22, 2024
…called from a foreign thread (GH-113261) (GH-114430)

Always set a _MainThread as a main thread after os.fork() is called from
a thread started not by the threading module.

A new _MainThread was already set as a new main thread after fork if
threading.current_thread() was not called for a foreign thread before fork.
Now, if it was called before fork, the implicitly created _DummyThread will
be turned into _MainThread after fork.

It fixes, in particularly, an incompatibility of _DummyThread with
the threading shutdown logic which relies on the main thread
having tstate_lock.

(cherry picked from commit 49785b0)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Co-authored-by: Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
aisk pushed a commit to aisk/cpython that referenced this issue Feb 11, 2024
…alled from a foreign thread (pythonGH-113261)

Always set a _MainThread as a main thread after os.fork() is called from
a thread started not by the threading module.

A new _MainThread was already set as a new main thread after fork if
threading.current_thread() was not called for a foreign thread before fork.
Now, if it was called before fork, the implicitly created _DummyThread will
be turned into _MainThread after fork.

It fixes, in particularly, an incompatibility of _DummyThread with
the threading shutdown logic which relies on the main thread
having tstate_lock.

Co-authored-by: Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.11 only security fixes 3.12 bugs and security fixes 3.13 new features, bugs and security fixes topic-multiprocessing type-bug An unexpected behavior, bug, or error
Projects
Development

No branches or pull requests

4 participants