diff --git a/Doc/c-api/init.rst b/Doc/c-api/init.rst index 26762969ef8eba..2e549c3b6f1388 100644 --- a/Doc/c-api/init.rst +++ b/Doc/c-api/init.rst @@ -409,7 +409,11 @@ Initializing and finalizing the interpreter freed. Some memory allocated by extension modules may not be freed. Some extensions may not work properly if their initialization routine is called more than once; this can happen if an application calls :c:func:`Py_Initialize` and - :c:func:`Py_FinalizeEx` more than once. + :c:func:`Py_FinalizeEx` more than once. :c:func:`Py_FinalizeEx` must not be + called recursively from within itself. Therefore, it must not be called by any + code that may be run as part of the interpreter shutdown process, such as + :py:mod:`atexit` handlers, object finalizers, or any code that may be run while + flushing the stdout and stderr files. .. audit-event:: cpython._PySys_ClearAuditHooks "" c.Py_FinalizeEx @@ -1000,6 +1004,78 @@ thread, where the CPython global runtime was originally initialized. The only exception is if :c:func:`exec` will be called immediately after. +.. _cautions-regarding-runtime-finalization: + +Cautions regarding runtime finalization +--------------------------------------- + +In the late stage of :term:`interpreter shutdown`, after attempting to wait for +non-daemon threads to exit (though this can be interrupted by +:class:`KeyboardInterrupt`) and running the :mod:`atexit` functions, the runtime +is marked as *finalizing*: :c:func:`_Py_IsFinalizing` and +:func:`sys.is_finalizing` return true. At this point, only the *finalization +thread* that initiated finalization (typically the main thread) is allowed to +acquire the :term:`GIL`. + +If any thread, other than the finalization thread, attempts to acquire the GIL +during finalization, either explicitly via :c:func:`PyGILState_Ensure`, +:c:macro:`Py_END_ALLOW_THREADS`, :c:func:`PyEval_AcquireThread`, or +:c:func:`PyEval_AcquireLock`, or implicitly when the interpreter attempts to +reacquire it after having yielded it, the thread enters a permanently blocked +state where it remains until the program exits. In most cases this is harmless, +but this can result in deadlock if a later stage of finalization attempts to +acquire a lock owned by the blocked thread, or otherwise waits on the blocked +thread. + +To avoid non-Python threads becoming blocked, or Python-created threads becoming +blocked while executing C extension code, you can use +:c:func:`PyThread_TryAcquireFinalizeBlock` and +:c:func:`PyThread_ReleaseFinalizeBlock`. + +For example, to deliver an asynchronous notification to Python from a C +extension, you might be inclined to write the following code that is *not* safe +to execute during finalization: + +.. code-block:: c + + // some non-Python created thread that wants to send Python an async notification + PyGILState_STATE state = PyGILState_Ensure(); // may hang thread + // call `call_soon_threadsafe` on some event loop object + PyGILState_Release(state); + +To avoid the possibility of the thread hanging during finalization, and also +support older Python versions: + +.. code-block:: c + + // some non-Python created thread that wants to send Python an async notification + PyGILState_STATE state; + #if PY_VERSION_HEX >= 0x030c0000 // API added in Python 3.12 + int acquired = PyThread_TryAcquireFinalizeBlock(); + if (!acquired) { + // skip sending notification since python is exiting + return; + } + #endif // PY_VERSION_HEX + state = PyGILState_Ensure(); // safe now + // call `call_soon_threadsafe` on some event loop object + PyGILState_Release(state); + #if PY_VERSION_HEX >= 0x030c0000 // API added in Python 3.12 + PyThread_ReleaseFinalizeBlock(); + #endif // PY_VERSION_HEX + +Or with the convenience interface (requires Python >=3.12): + +.. code-block:: c + + // some non-Python created thread that wants to send Python an async notification + PyGILState_TRY_STATE state = PyGILState_TryAcquireFinalizeBlockAndGIL(); + if (!state) { + // skip sending notification since python is exiting + return; + } + // call `call_soon_threadsafe` on some event loop object + PyGILState_ReleaseGILAndFinalizeBlock(state); High-level API -------------- @@ -1082,11 +1158,14 @@ code, or when embedding the Python interpreter: ensues. .. note:: - Calling this function from a thread when the runtime is finalizing - will terminate the thread, even if the thread was not created by Python. - You can use :c:func:`_Py_IsFinalizing` or :func:`sys.is_finalizing` to - check if the interpreter is in process of being finalized before calling - this function to avoid unwanted termination. + Calling this function from a thread when the runtime is finalizing will + hang the thread until the program exits, even if the thread was not + created by Python. Refer to + :ref:`cautions-regarding-runtime-finalization` for more details. + + .. versionchanged:: 3.12 + Hangs the current thread, rather than terminating it, if called while the + interpreter is finalizing. .. c:function:: PyThreadState* PyThreadState_Get() @@ -1128,11 +1207,14 @@ with sub-interpreters: to call arbitrary Python code. Failure is a fatal error. .. note:: - Calling this function from a thread when the runtime is finalizing - will terminate the thread, even if the thread was not created by Python. - You can use :c:func:`_Py_IsFinalizing` or :func:`sys.is_finalizing` to - check if the interpreter is in process of being finalized before calling - this function to avoid unwanted termination. + Calling this function from a thread when the runtime is finalizing will + hang the thread until the program exits, even if the thread was not + created by Python. Refer to + :ref:`cautions-regarding-runtime-finalization` for more details. + + .. versionchanged:: 3.12 + Hangs the current thread, rather than terminating it, if called while the + interpreter is finalizing. .. c:function:: void PyGILState_Release(PyGILState_STATE) @@ -1144,6 +1226,36 @@ with sub-interpreters: Every call to :c:func:`PyGILState_Ensure` must be matched by a call to :c:func:`PyGILState_Release` on the same thread. +.. c:function:: PyGILState_TRY_STATE PyGILState_AcquireFinalizeBlockAndGIL() + + Attempts to acquire a :ref:`finalize + block`, and if successful, acquires + the :term:`GIL`. + + This is a simple convenience interface that saves having to call + :c:func:`PyThread_TryAcquireFinalizeBlock` and :c:func:`PyGILState_Ensure` + separately. + + Returns ``PyGILState_TRY_LOCK_FAILED`` (equal to 0) if the interpreter is + already waiting to finalize. In this case, the :term:`GIL` is not acquired + and Python C APIs that require the :term:`GIL` must not be called. + + Otherwise, acquires a finalize block and then acquires the :term:`GIL`. + + Each call that is successful (i.e. returns a non-zero + ``PyGILState_TRY_STATE`` value) must be paired with a subsequent call to + :c:func:`PyGILState_ReleaseGILAndFinalizeBlock` with the same value returned + by this function. Calling :c:func:`PyGILState_ReleaseGILAndFinalizeBlock` with the + error value ``PyGILState_TRY_LOCK_FAILED`` is safe and does nothing. + + .. versionadded:: 3.12 + +.. c:function:: void PyGILState_ReleaseGILAndFinalizeBlock(PyGILState_TRY_STATE) + + Releases any locks acquired by the corresponding call to + :c:func:`PyGILState_AcquireFinalizeBlockAndGIL`. + + .. versionadded:: 3.12 .. c:function:: PyThreadState* PyGILState_GetThisThreadState() @@ -1410,17 +1522,20 @@ All of the following functions must be called after :c:func:`Py_Initialize`. If this thread already has the lock, deadlock ensues. .. note:: - Calling this function from a thread when the runtime is finalizing - will terminate the thread, even if the thread was not created by Python. - You can use :c:func:`_Py_IsFinalizing` or :func:`sys.is_finalizing` to - check if the interpreter is in process of being finalized before calling - this function to avoid unwanted termination. + Calling this function from a thread when the runtime is finalizing will + hang the thread until the program exits, even if the thread was not + created by Python. Refer to + :ref:`cautions-regarding-runtime-finalization` for more details. .. versionchanged:: 3.8 Updated to be consistent with :c:func:`PyEval_RestoreThread`, :c:func:`Py_END_ALLOW_THREADS`, and :c:func:`PyGILState_Ensure`, and terminate the current thread if called while the interpreter is finalizing. + .. versionchanged:: 3.12 + Hangs the current thread, rather than terminating it, if called while the + interpreter is finalizing. + :c:func:`PyEval_RestoreThread` is a higher-level function which is always available (even when threads have not been initialized). @@ -1448,17 +1563,19 @@ All of the following functions must be called after :c:func:`Py_Initialize`. instead. .. note:: - Calling this function from a thread when the runtime is finalizing - will terminate the thread, even if the thread was not created by Python. - You can use :c:func:`_Py_IsFinalizing` or :func:`sys.is_finalizing` to - check if the interpreter is in process of being finalized before calling - this function to avoid unwanted termination. + Calling this function from a thread when the runtime is finalizing will + hang the thread until the program exits, even if the thread was not + created by Python. Refer to + :ref:`cautions-regarding-runtime-finalization` for more details. .. versionchanged:: 3.8 Updated to be consistent with :c:func:`PyEval_RestoreThread`, :c:func:`Py_END_ALLOW_THREADS`, and :c:func:`PyGILState_Ensure`, and terminate the current thread if called while the interpreter is finalizing. + .. versionchanged:: 3.12 + Hangs the current thread, rather than terminating it, if called while the + interpreter is finalizing. .. c:function:: void PyEval_ReleaseLock() @@ -1469,6 +1586,37 @@ All of the following functions must be called after :c:func:`Py_Initialize`. :c:func:`PyEval_SaveThread` or :c:func:`PyEval_ReleaseThread` instead. +.. c:function:: int PyThread_AcquireFinalizeBlock() + + Attempts to prevent finalizing the current interpreter. + + If the interpreter is already finalizing then this returns 0 and has no + effect. Likewise if the interpreter is about to begin finalization but + is waiting for earlier calls to ``PyThread_AcquireFinalizeBlock()`` to be + resolved. The caller should then proceed knowing that they should + not use this interpreter any more. + + If the interpreter is not finalizing (nor about to) then it is immediately + prevented from finalizing, though this does not otherwise affect it. + This function returns 1 for this case. + + Every successful call must be paired with a call to + :c:func:`PyThread_ReleaseFinalizeBlock`. Until that happens, the + interpreter will be prevented from finalizing. During this period of + time, the caller is guaranteed that the :term:`GIL` can be safely + acquired without the risk of hanging the thread. + Refer to :ref:`cautions-regarding-runtime-finalization` for more details. + + This function may be safely called with or without holding the :term:`GIL`. + + .. versionadded:: 3.12 + +.. c:function:: void PyThread_ReleaseFinalizeBlock() + + Releases a finalize block acquired by a prior successful call to + :c:func:`PyThread_AcquireFinalizeBlock` (return value of 1). + + .. versionadded:: 3.12 .. _sub-interpreter-support: @@ -2007,4 +2155,3 @@ be used in new code. .. c:function:: void* PyThread_get_key_value(int key) .. c:function:: void PyThread_delete_key_value(int key) .. c:function:: void PyThread_ReInitTLS() - diff --git a/Doc/data/stable_abi.dat b/Doc/data/stable_abi.dat index f112d268129fd1..e1c09852a4bbd7 100644 --- a/Doc/data/stable_abi.dat +++ b/Doc/data/stable_abi.dat @@ -301,7 +301,9 @@ function,PyGC_IsEnabled,3.10,, function,PyGILState_Ensure,3.2,, function,PyGILState_GetThisThreadState,3.2,, function,PyGILState_Release,3.2,, +function,PyGILState_ReleaseGILAndFinalizeBlock,3.12,, type,PyGILState_STATE,3.2,, +function,PyGILState_TryAcquireFinalizeBlockAndGIL,3.12,, type,PyGetSetDef,3.2,,full-abi var,PyGetSetDescr_Type,3.2,, function,PyImport_AddModule,3.2,, @@ -627,6 +629,8 @@ function,PyThreadState_SetAsyncExc,3.2,, function,PyThreadState_Swap,3.2,, function,PyThread_GetInfo,3.3,, function,PyThread_ReInitTLS,3.2,, +function,PyThread_ReleaseFinalizeBlock,3.12,, +function,PyThread_TryAcquireFinalizeBlock,3.12,, function,PyThread_acquire_lock,3.2,, function,PyThread_acquire_lock_timed,3.2,, function,PyThread_allocate_lock,3.2,, diff --git a/Include/internal/pycore_runtime.h b/Include/internal/pycore_runtime.h index 6e06e874711bc2..da040c61a16b5f 100644 --- a/Include/internal/pycore_runtime.h +++ b/Include/internal/pycore_runtime.h @@ -84,6 +84,19 @@ typedef struct pyruntimestate { to access it, don't access it directly. */ _Py_atomic_address _finalizing; + /* Tracks the finalize blocks. + + Bit 0 is set to 1 by `Py_FinalizeEx` to indicate it is waiting to set `_finalizing`. + + The remaining bits are a count of the number of finalize blocks that are + currently held. Once bit 0 is set to 1, the number of finalize blocks is + not allowed to increase. + + Protected by the main interpreter's GIL `main_interp->ceval.gil->mutex`; + `main_interp->ceval.gil->cond` must be broadcast when it becomes 1. + */ + unsigned long finalize_blocks; + struct _pymem_allocators allocators; struct _obmalloc_global_state obmalloc; struct pyhash_runtime_state pyhash_state; diff --git a/Include/pystate.h b/Include/pystate.h index e6b4de979c87b8..96a9efa470812c 100644 --- a/Include/pystate.h +++ b/Include/pystate.h @@ -119,6 +119,53 @@ PyAPI_FUNC(void) PyGILState_Release(PyGILState_STATE); */ PyAPI_FUNC(PyThreadState *) PyGILState_GetThisThreadState(void); +/* Attempts to acquire a block on interpreter finalization. + + Returns 1 on success, or 0 if the interpreter is already waiting to finalize. + + While the lock is held, the interpreter will not enter the finalization + state. + + Each call that returns 1 must be paired with a subsequent call to + `PyThread_ReleaseFinalizeBlock`. + + It is not necessary to hold the GIL. While holding a block on interpreter + finalization, a non-main thread can safely acquire the GIL without risking + becoming permanently blocked. + */ +PyAPI_FUNC(int) PyThread_TryAcquireFinalizeBlock(void); + +/* Releases the block acquired by a successful call to + `PyThread_TryAcquireFinalizeBlock`. */ +PyAPI_FUNC(void) PyThread_ReleaseFinalizeBlock(void); + +typedef enum { + PyGILState_TRY_LOCK_FAILED, + PyGILState_TRY_LOCK_LOCKED, + PyGILState_TRY_LOCK_UNLOCKED +} PyGILState_TRY_STATE; + +/* Attempts to acquire a finalize block, and if successful, acquires the GIL. + + This is a simple convenience interface that saves having to call + `PyThread_TryAcquireFinalizeBlock()` and `PyGILState_Ensure()` separately. + + Returns `PyGILState_TRY_LOCK_FAILED` (equal to 0) if the interpreter is + already waiting to finalize. In this case, the GIL is not acquired and + Python C APIs that require the GIL must not be called. + + Otherwise, acquires a finalize block and then acquires the GIL. + + Each call that is successful (i.e. returns a non-zero `PyGILState_TRY_STATE` + value) must be paired with a subsequent call to + `PyGILState_ReleaseGILAndFinalizeBlock` with the same value returned by this + function. Calling `PyGILState_ReleaseGILAndFinalizeBlock` with the error + value `PyGILState_TRY_LOCK_FAILED` is safe and does nothing. */ +PyAPI_FUNC(PyGILState_TRY_STATE) PyGILState_TryAcquireFinalizeBlockAndGIL(void); + +/* Releases any locks acquired by the corresponding call to + `PyGILState_TryAcquireFinalizeBlockAndGIL`. */ +PyAPI_FUNC(void) PyGILState_ReleaseGILAndFinalizeBlock(PyGILState_TRY_STATE); #ifndef Py_LIMITED_API # define Py_CPYTHON_PYSTATE_H diff --git a/Include/pythread.h b/Include/pythread.h index 63714437c496b7..2374fe47ec3f0b 100644 --- a/Include/pythread.h +++ b/Include/pythread.h @@ -17,7 +17,37 @@ typedef enum PyLockStatus { PyAPI_FUNC(void) PyThread_init_thread(void); PyAPI_FUNC(unsigned long) PyThread_start_new_thread(void (*)(void *), void *); -PyAPI_FUNC(void) _Py_NO_RETURN PyThread_exit_thread(void); +/* Terminates the current thread. + * + * WARNING: This function is only safe to call if all functions in the full call + * stack are written to safely allow it. Additionally, the behavior is + * platform-dependent. This function should be avoided, and is no longer called + * by Python itself. It is retained only for compatibility with existing C + * extension code. + * + * With pthreads, calls `pthread_exit` which attempts to unwind the stack and + * call C++ destructors. If a `noexcept` function is reached, the program is + * terminated. + * + * On Windows, calls `_endthreadex` which kills the thread without calling C++ + * destructors. + * + * In either case there is a risk of invalid references remaining to data on the + * thread stack. + */ +Py_DEPRECATED(3.12) PyAPI_FUNC(void) _Py_NO_RETURN PyThread_exit_thread(void); + +#ifndef Py_LIMITED_API +/* Hangs the thread indefinitely without exiting it. + * + * bpo-42969: There is no safe way to exit a thread other than returning + * normally from its start function. This is used during finalization in lieu + * of actually exiting the thread. Since the program is expected to terminate + * soon anyway, it does not matter if the thread stack stays around until then. + */ +PyAPI_FUNC(void) _Py_NO_RETURN _PyThread_hang_thread(void); +#endif /* !Py_LIMITED_API */ + PyAPI_FUNC(unsigned long) PyThread_get_thread_ident(void); #if (defined(__APPLE__) || defined(__linux__) || defined(_WIN32) \ diff --git a/Lib/test/test_stable_abi_ctypes.py b/Lib/test/test_stable_abi_ctypes.py index 4ca39d85e5460c..019a7f59957769 100644 --- a/Lib/test/test_stable_abi_ctypes.py +++ b/Lib/test/test_stable_abi_ctypes.py @@ -326,6 +326,8 @@ def test_windows_feature_macros(self): "PyGILState_Ensure", "PyGILState_GetThisThreadState", "PyGILState_Release", + "PyGILState_ReleaseGILAndFinalizeBlock", + "PyGILState_TryAcquireFinalizeBlockAndGIL", "PyGetSetDescr_Type", "PyImport_AddModule", "PyImport_AddModuleObject", @@ -633,6 +635,8 @@ def test_windows_feature_macros(self): "PyThreadState_Swap", "PyThread_GetInfo", "PyThread_ReInitTLS", + "PyThread_ReleaseFinalizeBlock", + "PyThread_TryAcquireFinalizeBlock", "PyThread_acquire_lock", "PyThread_acquire_lock_timed", "PyThread_allocate_lock", diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 97165264b34bbe..78fc95e4d8a1c4 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -1048,6 +1048,150 @@ def import_threading(): self.assertEqual(out, b'') self.assertEqual(err, b'') + @cpython_only + def test_finalize_daemon_thread_hang(self): + # bpo-42969: tests that daemon threads hang during finalization + script = textwrap.dedent(''' + import os + import sys + import threading + import time + import _testcapi + + lock = threading.Lock() + lock.acquire() + thread_started_event = threading.Event() + def thread_func(): + try: + thread_started_event.set() + _testcapi.finalize_thread_hang(lock.acquire) + finally: + # Control must not reach here. + os._exit(2) + + t = threading.Thread(target=thread_func) + t.daemon = True + t.start() + thread_started_event.wait() + # Sleep to ensure daemon thread is blocked on `lock.acquire` + # + # Note: This test is designed so that in the unlikely case that + # `0.1` seconds is not sufficient time for the thread to become + # blocked on `lock.acquire`, the test will still pass, it just + # won't be properly testing the thread behavior during + # finalization. + time.sleep(0.1) + + def run_during_finalization(): + # Wake up daemon thread + lock.release() + # Sleep to give the daemon thread time to crash if it is going + # to. + # + # Note: If due to an exceptionally slow execution this delay is + # insufficient, the test will still pass but will simply be + # ineffective as a test. + time.sleep(0.1) + # If control reaches here, the test succeeded. + os._exit(0) + + # Replace sys.stderr.flush as a way to run code during finalization + orig_flush = sys.stderr.flush + def do_flush(*args, **kwargs): + orig_flush(*args, **kwargs) + if not sys.is_finalizing: + return + sys.stderr.flush = orig_flush + run_during_finalization() + + sys.stderr.flush = do_flush + + # If the follow exit code is retained, `run_during_finalization` + # did not run. + sys.exit(1) + ''') + assert_python_ok("-c", script) + + @cpython_only + def test_finalize_block(self): + # bpo-42969: tests `PyThread_{Acquire,Release}FinalizeBlock` + script = textwrap.dedent(''' + import atexit + import os + import sys + import threading + import time + import _testcapi + + sem = threading.Semaphore(0) + atexit_handlers_called = threading.Event() + atexit.register(atexit_handlers_called.set) + + def thread_func(): + # Should not return False + if not _testcapi.acquire_finalize_block(): + os._exit(5) + try: + sem.release() + # Wait until `atexit` handlers are called. + atexit_handlers_called.wait() + # Sleep to allow Python interpreter to begin exiting, so + # that we are properly testing the finalize block. + # + # Note: If for some reason this delay is insufficient, the + # test will still pass but will simply be ineffective as a + # test. + time.sleep(0.1) + sys.stdout.write('A') + sys.stdout.flush() + finally: + _testcapi.release_finalize_block() + + time.sleep(0.1) + # Thread should hang before control reaches here. + os._exit(1) + + + t = threading.Thread(target=thread_func) + t.daemon = True + t.start() + + # Wait until thread has blocked finalization. + sem.acquire() + + def run_during_finalization(): + # Should return False since this is during finalization. + if _testcapi.acquire_finalize_block(): + os._exit(4) + sys.stdout.write('B') + sys.stdout.flush() + # Sleep to give the daemon thread time to crash if it is going + # to. + # + # Note: If for some reason this delay is insufficient, the + # test will still pass but will simply be ineffective as a + # test. + time.sleep(0.2) + # If control reaches here, the test succeeded. + os._exit(0) + + # Replace sys.stderr.flush as a way to run code during finalization + orig_flush = sys.stderr.flush + def do_flush(*args, **kwargs): + orig_flush(*args, **kwargs) + if not sys.is_finalizing: + return + sys.stderr.flush = orig_flush + run_during_finalization() + + sys.stderr.flush = do_flush + + # If the follow exit code is retained, `run_during_finalization` + # did not run. + sys.exit(2) + ''') + _, out, _ = assert_python_ok("-c", script) + assert out == b"AB" class ThreadJoinOnShutdown(BaseTestCase): diff --git a/Misc/NEWS.d/next/C API/2022-08-05-19-41-20.gh-issue-87135.SCNBYj.rst b/Misc/NEWS.d/next/C API/2022-08-05-19-41-20.gh-issue-87135.SCNBYj.rst new file mode 100644 index 00000000000000..6803ca925878bd --- /dev/null +++ b/Misc/NEWS.d/next/C API/2022-08-05-19-41-20.gh-issue-87135.SCNBYj.rst @@ -0,0 +1,17 @@ +Attempting to acquire the GIL after runtime finalization has begun in a +different thread now causes the thread to hang rather than terminate, which +avoids potential crashes or memory corruption caused by attempting to +terminate a thread that is running code not specifically designed to support +termination. In most cases this hanging is harmless since the process will +soon exit anyway, but in cases where a thread must avoid hanging, the new +API functions :c:func:`PyThread_TryAcquireFinalizeBlock` or +:c:func:`PyGILState_AcquireFinalizeBlockAndGIL` may be used. + +The ``PyThread_exit_thread`` function is now deprecated. Its behavior is +inconsistent across platforms, and it can only be used safely in the +unlikely case that every function in the entire call stack has been designed +to support the platform-dependent termination mechanism. It is recommended +that users of this function change their design to not require thread +termination. In the unlikely case that thread termination is needed and can +be done safely, users may migrate to calling platform-specific APIs such as +``pthread_exit`` (POSIX) or ``_endthreadex`` (Windows) directly. diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-09-22-14-19-02.bpo-42969.8Z2mth.rst b/Misc/NEWS.d/next/Core and Builtins/2021-09-22-14-19-02.bpo-42969.8Z2mth.rst new file mode 100644 index 00000000000000..e5e03baa6ac0df --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2021-09-22-14-19-02.bpo-42969.8Z2mth.rst @@ -0,0 +1,3 @@ +Daemon threads and other threads not created by Python are now paused rather +than unsafely terminated if they attempt to acquire the GIL during Python +finalization. diff --git a/Misc/stable_abi.toml b/Misc/stable_abi.toml index 48299e9b35ff97..d3b7400605c72e 100644 --- a/Misc/stable_abi.toml +++ b/Misc/stable_abi.toml @@ -2397,7 +2397,6 @@ added = '3.12' # Before 3.12, available in "structmember.h" w/o Py_ prefix [const.Py_AUDIT_READ] added = '3.12' # Before 3.12, available in "structmember.h" - [function.PyObject_GetTypeData] added = '3.12' [function.PyType_GetTypeDataSize] @@ -2406,3 +2405,11 @@ added = '3.12' [const.Py_TPFLAGS_ITEMS_AT_END] added = '3.12' +[function.PyGILState_ReleaseGILAndFinalizeBlock] + added = '3.12' +[function.PyGILState_TryAcquireFinalizeBlockAndGIL] + added = '3.12' +[function.PyThread_ReleaseFinalizeBlock] + added = '3.12' +[function.PyThread_TryAcquireFinalizeBlock] + added = '3.12' diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index aff09aac80c610..77adfe31e7b69b 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -3244,6 +3244,45 @@ test_atexit(PyObject *self, PyObject *Py_UNUSED(args)) Py_RETURN_NONE; } +// Used by `finalize_thread_hang`. +#ifdef _POSIX_THREADS +static void finalize_thread_hang_cleanup_callback(void *Py_UNUSED(arg)) { + // Should not reach here. + assert(0 && "pthread thread termination was triggered unexpectedly"); +} +#endif + +// Tests that finalization does not trigger pthread cleanup. +// +// Must be called with a single nullary callable function that should block +// (with GIL released) until finalization is in progress. +static PyObject * +finalize_thread_hang(PyObject *self, PyObject *arg) +{ +#ifdef _POSIX_THREADS + pthread_cleanup_push(finalize_thread_hang_cleanup_callback, NULL); +#endif + PyObject_CallNoArgs(arg); + // Should not reach here. + Py_FatalError("thread unexpectedly did not hang"); +#ifdef _POSIX_THREADS + pthread_cleanup_pop(0); +#endif + Py_RETURN_NONE; +} + +static PyObject * +acquire_finalize_block(PyObject *self, PyObject *Py_UNUSED(args)) { + PyObject *result; + result = PyThread_TryAcquireFinalizeBlock() ? Py_True : Py_False; + return Py_NewRef(result); +} + +static PyObject * +release_finalize_block(PyObject *self, PyObject *Py_UNUSED(args)) { + PyThread_ReleaseFinalizeBlock(); + Py_RETURN_NONE; +} static PyObject *test_buildvalue_issue38913(PyObject *, PyObject *); @@ -3387,6 +3426,9 @@ static PyMethodDef TestMethods[] = { {"function_get_kw_defaults", function_get_kw_defaults, METH_O, NULL}, {"function_set_kw_defaults", function_set_kw_defaults, METH_VARARGS, NULL}, {"test_atexit", test_atexit, METH_NOARGS}, + {"finalize_thread_hang", finalize_thread_hang, METH_O, NULL}, + {"acquire_finalize_block", acquire_finalize_block, METH_NOARGS, NULL}, + {"release_finalize_block", release_finalize_block, METH_NOARGS, NULL}, {NULL, NULL} /* sentinel */ }; diff --git a/PC/python3dll.c b/PC/python3dll.c index 7e848abccfd1fa..e472086f531a67 100755 --- a/PC/python3dll.c +++ b/PC/python3dll.c @@ -287,6 +287,8 @@ EXPORT_FUNC(PyGC_IsEnabled) EXPORT_FUNC(PyGILState_Ensure) EXPORT_FUNC(PyGILState_GetThisThreadState) EXPORT_FUNC(PyGILState_Release) +EXPORT_FUNC(PyGILState_ReleaseGILAndFinalizeBlock) +EXPORT_FUNC(PyGILState_TryAcquireFinalizeBlockAndGIL) EXPORT_FUNC(PyImport_AddModule) EXPORT_FUNC(PyImport_AddModuleObject) EXPORT_FUNC(PyImport_AppendInittab) @@ -577,9 +579,11 @@ EXPORT_FUNC(PyThread_GetInfo) EXPORT_FUNC(PyThread_init_thread) EXPORT_FUNC(PyThread_ReInitTLS) EXPORT_FUNC(PyThread_release_lock) +EXPORT_FUNC(PyThread_ReleaseFinalizeBlock) EXPORT_FUNC(PyThread_set_key_value) EXPORT_FUNC(PyThread_set_stacksize) EXPORT_FUNC(PyThread_start_new_thread) +EXPORT_FUNC(PyThread_TryAcquireFinalizeBlock) EXPORT_FUNC(PyThread_tss_alloc) EXPORT_FUNC(PyThread_tss_create) EXPORT_FUNC(PyThread_tss_delete) diff --git a/Python/ceval_gil.c b/Python/ceval_gil.c index b9bdb74fcedf32..289b4791cf4e6b 100644 --- a/Python/ceval_gil.c +++ b/Python/ceval_gil.c @@ -181,43 +181,6 @@ is_tstate_valid(PyThreadState *tstate) #include "condvar.h" -#define MUTEX_INIT(mut) \ - if (PyMUTEX_INIT(&(mut))) { \ - Py_FatalError("PyMUTEX_INIT(" #mut ") failed"); }; -#define MUTEX_FINI(mut) \ - if (PyMUTEX_FINI(&(mut))) { \ - Py_FatalError("PyMUTEX_FINI(" #mut ") failed"); }; -#define MUTEX_LOCK(mut) \ - if (PyMUTEX_LOCK(&(mut))) { \ - Py_FatalError("PyMUTEX_LOCK(" #mut ") failed"); }; -#define MUTEX_UNLOCK(mut) \ - if (PyMUTEX_UNLOCK(&(mut))) { \ - Py_FatalError("PyMUTEX_UNLOCK(" #mut ") failed"); }; - -#define COND_INIT(cond) \ - if (PyCOND_INIT(&(cond))) { \ - Py_FatalError("PyCOND_INIT(" #cond ") failed"); }; -#define COND_FINI(cond) \ - if (PyCOND_FINI(&(cond))) { \ - Py_FatalError("PyCOND_FINI(" #cond ") failed"); }; -#define COND_SIGNAL(cond) \ - if (PyCOND_SIGNAL(&(cond))) { \ - Py_FatalError("PyCOND_SIGNAL(" #cond ") failed"); }; -#define COND_WAIT(cond, mut) \ - if (PyCOND_WAIT(&(cond), &(mut))) { \ - Py_FatalError("PyCOND_WAIT(" #cond ") failed"); }; -#define COND_TIMED_WAIT(cond, mut, microseconds, timeout_result) \ - { \ - int r = PyCOND_TIMEDWAIT(&(cond), &(mut), (microseconds)); \ - if (r < 0) \ - Py_FatalError("PyCOND_WAIT(" #cond ") failed"); \ - if (r) /* 1 == timeout, 2 == impl. can't say, so assume timeout */ \ - timeout_result = 1; \ - else \ - timeout_result = 0; \ - } \ - - #define DEFAULT_INTERVAL 5000 static void _gil_initialize(struct _gil_runtime_state *gil) @@ -353,12 +316,12 @@ take_gil(PyThreadState *tstate) if (tstate_must_exit(tstate)) { /* bpo-39877: If Py_Finalize() has been called and tstate is not the - thread which called Py_Finalize(), exit immediately the thread. + thread which called Py_Finalize(), bpo-42969: hang 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(); + _PyThread_hang_thread(); } assert(is_tstate_valid(tstate)); @@ -400,7 +363,9 @@ take_gil(PyThreadState *tstate) if (drop_requested) { RESET_GIL_DROP_REQUEST(interp); } - PyThread_exit_thread(); + // gh-87135: hang the thread as *thread_exit() is not a safe + // API. It lacks stack unwind and local variable destruction. + _PyThread_hang_thread(); } assert(is_tstate_valid(tstate)); @@ -431,7 +396,7 @@ take_gil(PyThreadState *tstate) if (tstate_must_exit(tstate)) { /* bpo-36475: If Py_Finalize() has been called and tstate is not - the thread which called Py_Finalize(), exit immediately the + the thread which called Py_Finalize(), bpo-42969: hang the thread. This code path can be reached by a daemon thread which was waiting @@ -439,7 +404,7 @@ take_gil(PyThreadState *tstate) wait_for_thread_shutdown() from Py_Finalize(). */ MUTEX_UNLOCK(gil->mutex); drop_gil(ceval, tstate); - PyThread_exit_thread(); + _PyThread_hang_thread(); } assert(is_tstate_valid(tstate)); diff --git a/Python/condvar.h b/Python/condvar.h index 4ddc5311cf8fad..2b0f819a48bfe7 100644 --- a/Python/condvar.h +++ b/Python/condvar.h @@ -306,4 +306,44 @@ PyCOND_BROADCAST(PyCOND_T *cv) #endif /* _POSIX_THREADS, NT_THREADS */ +/* Convenience interfaces that terminate the program in case of an error. */ +#define MUTEX_INIT(mut) \ + if (PyMUTEX_INIT(&(mut))) { \ + Py_FatalError("PyMUTEX_INIT(" #mut ") failed"); }; +#define MUTEX_FINI(mut) \ + if (PyMUTEX_FINI(&(mut))) { \ + Py_FatalError("PyMUTEX_FINI(" #mut ") failed"); }; +#define MUTEX_LOCK(mut) \ + if (PyMUTEX_LOCK(&(mut))) { \ + Py_FatalError("PyMUTEX_LOCK(" #mut ") failed"); }; +#define MUTEX_UNLOCK(mut) \ + if (PyMUTEX_UNLOCK(&(mut))) { \ + Py_FatalError("PyMUTEX_UNLOCK(" #mut ") failed"); }; + +#define COND_INIT(cond) \ + if (PyCOND_INIT(&(cond))) { \ + Py_FatalError("PyCOND_INIT(" #cond ") failed"); }; +#define COND_FINI(cond) \ + if (PyCOND_FINI(&(cond))) { \ + Py_FatalError("PyCOND_FINI(" #cond ") failed"); }; +#define COND_SIGNAL(cond) \ + if (PyCOND_SIGNAL(&(cond))) { \ + Py_FatalError("PyCOND_SIGNAL(" #cond ") failed"); }; +#define COND_BROADCAST(cond) \ + if (PyCOND_BROADCAST(&(cond))) { \ + Py_FatalError("PyCOND_BROADCAST(" #cond ") failed"); }; +#define COND_WAIT(cond, mut) \ + if (PyCOND_WAIT(&(cond), &(mut))) { \ + Py_FatalError("PyCOND_WAIT(" #cond ") failed"); }; +#define COND_TIMED_WAIT(cond, mut, microseconds, timeout_result) \ + { \ + int r = PyCOND_TIMEDWAIT(&(cond), &(mut), (microseconds)); \ + if (r < 0) \ + Py_FatalError("PyCOND_WAIT(" #cond ") failed"); \ + if (r) /* 1 == timeout, 2 == impl. can't say, so assume timeout */ \ + timeout_result = 1; \ + else \ + timeout_result = 0; \ + } \ + #endif /* _CONDVAR_IMPL_H_ */ diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 06c43459624c67..f926dbdf1f3979 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -2,6 +2,7 @@ #include "Python.h" +#include "condvar.h" #include "pycore_ceval.h" // _PyEval_FiniGIL() #include "pycore_context.h" // _PyContext_Init() #include "pycore_exceptions.h" // _PyExc_InitTypes() @@ -1739,6 +1740,39 @@ finalize_interp_delete(PyInterpreterState *interp) PyInterpreterState_Delete(interp); } +/* Prevents new exit blocks from being acquired and waits for existing blocks to + * be released. + * + * This is only to be called from `Py_FinalizeEx`. + */ +static void wait_for_exit_blocks_to_be_released(void) +{ + _PyRuntimeState *runtime = &_PyRuntime; + + /* We release the GIL to avoid deadlock. */ + Py_BEGIN_ALLOW_THREADS + + PyInterpreterState *main_interp = PyInterpreterState_Main(); + MUTEX_LOCK(main_interp->ceval.gil->mutex) + runtime->finalize_blocks |= 1; + /* Note: It is possible that there is another concurrent call to + `Py_FinalizeEx` in a different thread. In that case, the LSB of + `runtime->finalize_blocks` may have already been set to 1. Finalization + will still work correctly, though, because only one thread will + successfully acquire the GIL from `Py_END_ALLOW_THREADS` below. That + thread will then initiate finalization, and the other thread will then + hang in `Py_END_ALLOW_THREADS` until the process exits. + + Calling `Py_FinalizeEx` recursively, e.g. by an atexit handler, is *not* + allowed and will not work correctly. + */ + while(runtime->finalize_blocks != 1) { + COND_WAIT(main_interp->ceval.gil->cond, main_interp->ceval.gil->mutex) + } + MUTEX_UNLOCK(main_interp->ceval.gil->mutex) + + Py_END_ALLOW_THREADS +} int Py_FinalizeEx(void) @@ -1777,6 +1811,8 @@ Py_FinalizeEx(void) _PyAtExit_Call(tstate->interp); PyUnstable_PerfMapState_Fini(); + wait_for_exit_blocks_to_be_released(); + /* Copy the core config, PyInterpreterState_Delete() free the core config memory */ #ifdef Py_REF_DEBUG diff --git a/Python/pystate.c b/Python/pystate.c index 25e655a2027918..839a7a48d18a84 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -2,6 +2,7 @@ /* Thread and interpreter state structures and their interfaces */ #include "Python.h" +#include "condvar.h" #include "pycore_ceval.h" #include "pycore_code.h" // stats #include "pycore_dtoa.h" // _dtoa_state_INIT() @@ -2293,6 +2294,56 @@ PyGILState_Release(PyGILState_STATE oldstate) } } +int +PyThread_TryAcquireFinalizeBlock(void) +{ + int ret; + _PyRuntimeState *runtime = &_PyRuntime; + PyInterpreterState *main_interp = PyInterpreterState_Main(); + MUTEX_LOCK(main_interp->ceval.gil->mutex) + if (runtime->finalize_blocks & 1) { + ret = 0; + } else { + ret = 1; + runtime->finalize_blocks += 2; + } + MUTEX_UNLOCK(main_interp->ceval.gil->mutex) + return ret; +} + +void +PyThread_ReleaseFinalizeBlock(void) +{ + _PyRuntimeState *runtime = &_PyRuntime; + PyInterpreterState *main_interp = PyInterpreterState_Main(); + MUTEX_LOCK(main_interp->ceval.gil->mutex) + assert(runtime->finalize_blocks >= 2); + if ((runtime->finalize_blocks -= 2) == 0) { + COND_BROADCAST(main_interp->ceval.gil->cond) + } + MUTEX_UNLOCK(main_interp->ceval.gil->mutex) +} + +PyGILState_TRY_STATE +PyGILState_TryAcquireFinalizeBlockAndGIL(void) +{ + if (!PyThread_TryAcquireFinalizeBlock()) { + return PyGILState_TRY_LOCK_FAILED; + } + return PyGILState_Ensure() == PyGILState_LOCKED + ? PyGILState_TRY_LOCK_LOCKED + : PyGILState_TRY_LOCK_UNLOCKED; +} + +void +PyGILState_ReleaseGILAndFinalizeBlock(PyGILState_TRY_STATE state) { + if (state == PyGILState_TRY_LOCK_FAILED) { + return; + } + PyGILState_Release(state == PyGILState_TRY_LOCK_LOCKED + ? PyGILState_LOCKED + : PyGILState_UNLOCKED); +} /**************************/ /* cross-interpreter data */ diff --git a/Python/thread_nt.h b/Python/thread_nt.h index 26f441bd6d3c56..8343ce9307f7a3 100644 --- a/Python/thread_nt.h +++ b/Python/thread_nt.h @@ -257,6 +257,14 @@ PyThread_exit_thread(void) _endthreadex(0); } +void _Py_NO_RETURN +_PyThread_hang_thread(void) +{ + while (1) { + SleepEx(INFINITE, TRUE); + } +} + /* * Lock support. It has to be implemented as semaphores. * I [Dag] tried to implement it with mutex but I could find a way to diff --git a/Python/thread_pthread.h b/Python/thread_pthread.h index 76d6f3bcdf9c40..bcf953f34e8748 100644 --- a/Python/thread_pthread.h +++ b/Python/thread_pthread.h @@ -359,6 +359,14 @@ PyThread_exit_thread(void) pthread_exit(0); } +void _Py_NO_RETURN +_PyThread_hang_thread(void) +{ + while (1) { + pause(); + } +} + #ifdef USE_SEMAPHORES /*