Skip to content

test_threading.test_4_daemon_threads() crash randomly #110052

Description

@vstinner

On Linux, when I stress test test_threading.test_4_daemon_threads(), it does crash randomly:

./python -m test test_threading -m test_4_daemon_threads -j50 -F --fail-env-changed  

gdb traceback:

Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x00000000006e02f4 in PyInterpreterState_ThreadHead (interp=0xdddddddddddddddd) at Python/pystate.c:1961

warning: Source file is more recent than executable.
1961	    return interp->threads.head;
[Current thread is 1 (Thread 0x7fcbb8ff96c0 (LWP 510515))]
Missing separate debuginfos, use: dnf debuginfo-install bzip2-libs-1.0.8-13.fc38.x86_64 glibc-2.37-5.fc38.x86_64 libffi-3.4.4-2.fc38.x86_64 libgcc-13.2.1-1.fc38.x86_64 openssl-libs-3.0.9-2.fc38.x86_64 xz-libs-5.4.1-1.fc38.x86_64 zlib-1.2.13-3.fc38.x86_64
(gdb) where
#0  0x00000000006e02f4 in PyInterpreterState_ThreadHead (interp=0xdddddddddddddddd) at Python/pystate.c:1961
#1  0x0000000000703090 in _Py_DumpTracebackThreads (fd=2, interp=0xdddddddddddddddd, current_tstate=0x24a34e0) at Python/traceback.c:1331
#2  0x000000000071bcef in faulthandler_dump_traceback (fd=2, all_threads=1, interp=0xa53688 <_PyRuntime+92712>) at ./Modules/faulthandler.c:195
#3  0x000000000071bfed in faulthandler_fatal_error (signum=11) at ./Modules/faulthandler.c:313
#4  <signal handler called>
#5  0x000000000069f808 in take_gil (tstate=0x24a34e0) at Python/ceval_gil.c:360
#6  0x00000000006a03ab in PyEval_RestoreThread (tstate=0x24a34e0) at Python/ceval_gil.c:714
#7  0x000000000074dd93 in portable_lseek (self=0x7fcc186c3590, posobj=0x0, whence=1, suppress_pipe_error=false) at ./Modules/_io/fileio.c:934
#8  0x000000000074de88 in _io_FileIO_tell_impl (self=0x7fcc186c3590) at ./Modules/_io/fileio.c:997
#9  0x000000000074ea4c in _io_FileIO_tell (self=0x7fcc186c3590, _unused_ignored=0x0) at ./Modules/_io/clinic/fileio.c.h:460
(...)

gdb debug:


(gdb) frame 5
#5  0x000000000069f808 in take_gil (tstate=0x24a34e0) at Python/ceval_gil.c:360
360	    struct _gil_runtime_state *gil = ceval->gil;
(gdb) l
355	    }
356	
357	    assert(_PyThreadState_CheckConsistency(tstate));
358	    PyInterpreterState *interp = tstate->interp;
359	    struct _ceval_state *ceval = &interp->ceval;
360	    struct _gil_runtime_state *gil = ceval->gil;
361	
362	    /* Check that _PyEval_InitThreads() was called to create the lock */
363	    assert(gil_created(gil));
364	

(gdb) p /x ceval
$1 = 0xdddddddddddddddd

(gdb) p /x tstate->interp
$2 = 0xdddddddddddddddd


(gdb) p /x tstate
$5 = 0x24a34e0

(gdb) p /x _PyRuntime._finalizing._value 
$3 = 0xab8fa8

(gdb) p /x _PyRuntime._finalizing_id 
$4 = 0x7fcc49fce740

I don't understand why the test didn't exit: _PyThreadState_MustExit() should return, no?

I don't understand why assert(_PyThreadState_CheckConsistency(tstate)); didn't fail.

Maybe Py_Finalize() was called between the pre-check:

    if (_PyThreadState_MustExit(tstate)) {
        /* bpo-39877: If Py_Finalize() has been called and tstate is not the
           thread which called Py_Finalize(), exit immediately the thread.

           This code path can be reached by a daemon thread after Py_Finalize()
           completes. In this case, tstate is a dangling pointer: points to
           PyThreadState freed memory. */
        PyThread_exit_thread();
    }

    assert(_PyThreadState_CheckConsistency(tstate));

and the code:

    PyInterpreterState *interp = tstate->interp;
    struct _ceval_state *ceval = &interp->ceval;
    struct _gil_runtime_state *gil = ceval->gil;

Linked PRs

Activity

  1. vstinner commented on Sep 28, 2023

    @vstinner
    MemberAuthor

    See also issue gh-110031 which looks similar.

  2. vstinner commented on Sep 28, 2023

    @vstinner
    MemberAuthor

    The regression was introduced by PR gh-109794: #109794 (comment)

  3. ericsnowcurrently commented on Sep 28, 2023

    @ericsnowcurrently
    Member

    I'm looking into this.

    ./python -m test test_threading -m test_4_daemon_threads -j50 -F --fail-env-changed

    I haven't been able to get a crash in over 16 minutes (but I certainly believe you).

  4. vstinner commented on Sep 28, 2023

    @vstinner
    MemberAuthor

    I haven't been able to get a crash in over 16 minutes (but I certainly believe you).

    What is your operating system? I reproduced the crash on Linux. I didn't try other operating systems.

    If you want to make daemon threads crashes more likely, add sleep(1) at the end of PyInterpreterState_Delete(), after free_interpreter().

  5. ericsnowcurrently commented on Sep 28, 2023

    @ericsnowcurrently
    Member

    What is your operating system? I reproduced the crash on Linux. I didn't try other operating systems.

    It's an 8-core, 24GB RAM Hyper-V Ubuntu VM running on a Windows laptop.

    If you want to make daemon threads crashes more likely, add sleep(1) at the end of PyInterpreterState_Delete(), after free_interpreter().

    I'll try it.

  6. ericsnowcurrently commented on Sep 28, 2023

    @ericsnowcurrently
    Member

    gdb traceback:
    ,,,
    gdb debug:
    ...
    I don't understand why the test didn't exit: _PyThreadState_MustExit() should return, no?

    Yeah, that's the critical question.

  7. ericsnowcurrently commented on Sep 29, 2023

    @ericsnowcurrently
    Member

    Here's the critical part from gh-109794:

    diff --git a/Python/pystate.c b/Python/pystate.c
    index dcc6c112215b3..570b5242600c0 100644
    --- a/Python/pystate.c
    +++ b/Python/pystate.c
    @@ -2964,11 +2964,26 @@ _PyThreadState_MustExit(PyThreadState *tstate)
            tstate->interp->runtime to support calls from Python daemon threads.
            After Py_Finalize() has been called, tstate can be a dangling pointer:
            point to PyThreadState freed memory. */
    +    unsigned long finalizing_id = _PyRuntimeState_GetFinalizingID(&_PyRuntime);
         PyThreadState *finalizing = _PyRuntimeState_GetFinalizing(&_PyRuntime);
         if (finalizing == NULL) {
    +        // XXX This isn't completely safe from daemon thraeds,
    +        // since tstate might be a dangling pointer.
             finalizing = _PyInterpreterState_GetFinalizing(tstate->interp);
    +        finalizing_id = _PyInterpreterState_GetFinalizingID(tstate->interp);
         }
    -    return (finalizing != NULL && finalizing != tstate);
    +    // XXX else check &_PyRuntime._main_interpreter._initial_thread
    +    if (finalizing == NULL) {
    +        return 0;
    +    }
    +    else if (finalizing == tstate) {
    +        return 0;
    +    }
    +    else if (finalizing_id == PyThread_get_thread_ident()) {
    +        /* gh-109793: we must have switched interpreters. */
    +        return 0;
    +    }
    +    return 1;
     }
    

    The most obvious possibility is that we switched to a different thread state during finalization in the thread where finalization is happening and the thread state belongs to the same interpreter. However, that isn't an expected use case for PyThread_Swap() and I'm pretty sure we don't have any code that does that.

  8. ericsnowcurrently commented on Sep 29, 2023

    @ericsnowcurrently
    Member

    If you want to make daemon threads crashes more likely, add sleep(1) at the end of PyInterpreterState_Delete(), after free_interpreter().

    I'll try it.

    No failures in 13 minutes.

  9. vstinner commented on Sep 29, 2023

    @vstinner
    MemberAuthor

    No failures in 13 minutes.

    How do you build Python? Which configure options?

    I'm usually using ./configure --with-pydebug to get assertions.

  10. ericsnowcurrently commented on Sep 29, 2023

    @ericsnowcurrently
    Member

    Basically: ./configure --with-pydebug CFLAGS=-O0

  11. ericsnowcurrently commented on Sep 29, 2023

    @ericsnowcurrently
    Member

    Is this issue tied to any buildbot/CI failures?

  12. terryjreedy commented on Sep 29, 2023

    @terryjreedy
    Member

    6/12 core Win 10, standard PCBuild/build.bat -d

    ...
    0:00:31 load avg: 76.65 [ 87/1] test_threading failed (1 failure)
    test test_threading failed -- Traceback (most recent call last):
      File "f:\dev\3x\Lib\test\test_threading.py", line 1172, in test_4_daemon_threads
        rc, out, err = assert_python_ok('-c', script)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "f:\dev\3x\Lib\test\support\script_helper.py", line 166, in assert_python_ok
        return _assert_python(True, *args, **env_vars)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "f:\dev\3x\Lib\test\support\script_helper.py", line 151, in _assert_python
        res.fail(cmd_line)
      File "f:\dev\3x\Lib\test\support\script_helper.py", line 76, in fail
        raise AssertionError("Process return code is %d\n"
    AssertionError: Process return code is 3221225477
    command line: ['f:\\dev\\3x\\PCbuild\\amd64\\python_d.exe', '-X', 'faulthandler', '-c', "if True:\n            import os\n            import random\n            import sys\n            import time\n            import threading\n\n            thread_has_run = set()\n\n            def random_io():\n                '''Loop for a while sleeping random tiny amounts and doing some I/O.'''\n                import test.test_threading as mod\n                while True:\n                    with open(mod.__file__, 'rb') as in_f:\n                        stuff = in_f.read(200)\n                        with open(os.devnull, 'wb') as null_f:\n                            null_f.write(stuff)\n                            time.sleep(random.random() / 1995)\n                    thread_has_run.add(threading.current_thread())\n\n            def main():\n                count = 0\n                for _ in range(40):\n                    new_thread = threading.Thread(target=random_io)\n                    new_thread.daemon = True\n                    new_thread.start()\n                    count += 1\n                while len(thread_has_run) < count:\n                    time.sleep(0.001)\n                # Trigger process shutdown\n                sys.exit(0)\n\n            main()\n            "]
    
    stdout:
    ---
    
    ---
    
    stderr:
    ---
    Windows fatal exception: access violation
    
  13. terryjreedy commented on Sep 29, 2023

    @terryjreedy
    Member

    Rerun showed this warning first.

    0:00:29 load avg: 84.65 [ 75] test_threading passed
    f:\dev\3x\Lib\test\support\os_helper.py:531: RuntimeWarning: tests may fail, unable to create temporary directory 'F:\\dev\\3x\\build\\test_python_3812æ': [WinError 183] Cannot create a file when that file already exists: 'F:\\dev\\3x\\build\\test_python_3812æ'
      with temp_dir(path=name, quiet=quiet) as temp_path:
    test_4_daemon_threads (test.test_threading.ThreadJoinOnShutdown.test_4_daemon_threads) ... ok
    

    Then same failure at 319.

  14. vstinner commented on Sep 29, 2023

    @vstinner
    MemberAuthor

    f:\dev\3x\Lib\test\support\os_helper.py:531: RuntimeWarning: tests may fail, unable to create temporary directory 'F:\dev\3x\build\test_python_3812æ': [WinError 183] Cannot create a file when that file already exists: 'F:\dev\3x\build\test_python_3812æ'

    Time to time, you may want to run python -m test --cleanup, to remove test directories which tests left when they crashed. regrtest is supposed to clean them. But well, regrtest itself has also bugs :-)

  15. 18 remaining items

  16. vstinner commented on Sep 29, 2023

    @vstinner
    MemberAuthor

    Since this bug exists since Python 3.9 and nobody (before me) complained, I don't think that there is an emergency to fix it in Python 3.12. We can take time in Python 3.13 to come up with a clean design, as the one described by @colesbury: refcount and states.

  17. ericsnowcurrently commented on Sep 29, 2023

    @ericsnowcurrently
    Member

    I have been thinking about making use of thread-local storage for the problems caused by daemon threads. Something like this:

    #Include/internal/pycore_pystate.h
    
    typedef struct {
        PyThreadState *tstate;
        int finalizing;
    } _Py_tss_state_t;
    
    #Include/cpython/pystate.h
    
    struct _ts {
        ...
        _Py_tss_state_t *tss;
        ...
    } ;
    
    # Python/pystate.c
    
    // Replaces _Py_tss_tstate.
    // Guaranteed initialized to {0}?
    _Py_thread_local _Py_tss_state pystate;
    
    // Update tstate_activate() and tstate_deactivate() to set pystate.tstate
    // (and update pystate.finalizing, if necessary).
    
    void
    _PyThreadState_Bind(PyThreadState *tstate)
    {
        ...
        tstate->tss = &pystate;
    }
    
    // Use this with (or in place of) _PyInterpreterState_SetFinalizing()?
    void
    _PyInterpreterState_SetTheadsFinalizing(PyInterpreterState *interp)
    {
        HEAD_LOCK(&_PyRuntime);
        assert(interp->finalizing);  // If true, new threads won't be created.
        PyThreadState *tstate = interp->threads.head;
        while (tstate != NULL) {
            if (tstate->tss->tstate == tstate) {
                tstate->tss->finalizing = 1;
            }
            tstate = tstate->next;
        }
        HEAD_UNLOCK(&_PyRuntime);
    }
    
    int
    _PyThreadState_IsFinalizing(void)
    {
        return pystate.finalizing;
    }
    
    # Python/ceval_gil.c
    
    // Update _PyThreadState_MustExit() to rely on _PyThreadState_IsFinalizing().
    
    // XXX Update take_gil() to use critical data stored in the pystate threadlocal?
  18. ericsnowcurrently commented on Sep 29, 2023

    @ericsnowcurrently
    Member

    Since we only need to worry about races in take_gil() with daemon threads, we could refcount the GIL instead of the whole thread state, if a refcount is necessary.

  19. colesbury commented on Sep 29, 2023

    @colesbury
    Contributor

    @ericsnowcurrently - I think that approach opens you up to problems if the daemon thread exits before the main thread. Then the thread->tss will point to invalid memory. There's no guarantee that the daemon thread necessarily exits cleanly and cleans up it's PyThreadState.

  20. ericsnowcurrently commented on Sep 29, 2023

    @ericsnowcurrently
    Member

    Yeah, if a thread (not just daemon) exits before we call PyThreadState_Delete() then that would be a problem, at least without some thread finalizer hook (like C++).

    That said, this seems like a something that would apply broadly to the whole C community, whereas our current issue of finalization races in take_gil() with daemon threads is fairly specific to us. Thus it's more likely to have been solved already, no?

  21. ericsnowcurrently commented on Sep 29, 2023

    @ericsnowcurrently
    Member

    Hmm, tss_create() can be used for key-based TSS, a la the PEP 539 API. Unlike our API though, tss_create() takes a destructor arg that gets called when the thread exits (but not with the program exit()). We could use that to clean up the thread-local variable. I'm not sure how that works for Windows. Given that C++ can do it, I'd think we could do it from C on WIndows.

  22. colesbury commented on Sep 29, 2023

    @colesbury
    Contributor

    It's hard to sequence destruction of thread-local variables. In other words, the tss destructors may be called before or after thread-local storage is destroyed. So unless you create pystate via tss_create (which would make access slower), then it's really hard to know if the tss destructor will be called before or after pystate is destroyed.

    For an example of this problem, see microsoft/mimalloc#164.

    Is there anything wrong with the solution I've outlined above? I've tested it in nogil-3.12 to address this crash, which occurs more frequently once the GIL is disabled.

  23. ericsnowcurrently commented on Sep 29, 2023

    @ericsnowcurrently
    Member

    I didn't have a chance yet.

  24. added a commit that references this issue on Oct 2, 2023
  25. added a commit that references this issue on Sep 2, 2024
  26. vstinner commented on Mar 13, 2025

    @vstinner
    MemberAuthor

    It should be fixed by #130649 in the main branch. I close the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

testsTests in the Lib/test dir

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions