Skip to content

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

@marmarek

Description

@marmarek
Contributor

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

Activity

marmarek

marmarek commented on Mar 7, 2023

@marmarek
ContributorAuthor

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
added a commit that references this issue on Mar 7, 2023

8 remaining items

added a commit that references this issue on Jan 22, 2024
added a commit that references this issue on Jan 22, 2024
added 2 commits that reference this issue on Jan 22, 2024
serhiy-storchaka

serhiy-storchaka commented on Jan 22, 2024

@serhiy-storchaka
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.

added 3 commits that reference this issue on Jan 22, 2024
added a commit that references this issue on Feb 11, 2024
added a commit that references this issue on Sep 2, 2024
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

    3.11only security fixes3.12only security fixes3.13bugs and security fixestopic-multiprocessingtype-bugAn unexpected behavior, bug, or error

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @marmarek@gvanrossum@serhiy-storchaka@arhadthedev

        Issue actions

          `os.fork()` called from DummyThread confuses threading shutdown logic · Issue #102512 · python/cpython