From f3e301d89696c615ee5fa74d8f8286c58d83d5db Mon Sep 17 00:00:00 2001 From: Jeremy Maitin-Shepard Date: Wed, 2 Oct 2024 09:17:49 -0700 Subject: [PATCH 1/4] [3.13] gh-87135: Hang non-main threads that attempt to acquire the GIL during finalization (GH-105805) Instead of surprise crashes and memory corruption, we now hang threads that attempt to re-enter the Python interpreter after Python runtime finalization has started. These are typically daemon threads (our long standing mis-feature) but could also be threads spawned by extension modules that then try to call into Python. This marks the `PyThread_exit_thread` public C API as deprecated as there is no plausible safe way to accomplish that on any supported platform in the face of things like C++ code with finalizers anywhere on a thread's stack. Doing this was the least bad option. (cherry picked from commit 8cc5aa47ee464ddfd8da5461edecf4a5c72df2ff) Co-authored-by: Jeremy Maitin-Shepard Co-authored-by: Gregory P. Smith --- Doc/c-api/init.rst | 76 +++++++++++++++---- Include/internal/pycore_pythread.h | 13 ++++ Include/pythread.h | 21 ++++- Lib/test/test_threading.py | 71 +++++++++++++++++ ...2-08-05-19-41-20.gh-issue-87135.SCNBYj.rst | 15 ++++ Modules/_testcapimodule.c | 29 +++++++ Python/ceval_gil.c | 26 ++++--- Python/pylifecycle.c | 2 +- Python/thread_nt.h | 8 ++ Python/thread_pthread.h | 15 +++- 10 files changed, 247 insertions(+), 29 deletions(-) create mode 100644 Misc/NEWS.d/next/C API/2022-08-05-19-41-20.gh-issue-87135.SCNBYj.rst diff --git a/Doc/c-api/init.rst b/Doc/c-api/init.rst index 84d6a090068e06..f86eee70757d96 100644 --- a/Doc/c-api/init.rst +++ b/Doc/c-api/init.rst @@ -447,7 +447,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 @@ -1074,6 +1078,37 @@ 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. + +Gross? Yes. This prevents random crashes and/or unexpectedly skipped C++ +finalizations further up the call stack when such threads were forcibly exited +here in CPython 3.13 and earlier. The CPython runtime GIL acquiring C APIs +have never had any error reporting or handling expectations at GIL acquisition +time that would've allowed for graceful exit from this situation. Changing that +would require new stable C APIs and rewriting the majority of C code in the +CPython ecosystem to use those with error handling. + High-level API -------------- @@ -1147,11 +1182,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:: next + Hangs the current thread, rather than terminating it, if called while the + interpreter is finalizing. .. c:function:: PyThreadState* PyThreadState_Get() @@ -1207,11 +1245,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:: next + Hangs the current thread, rather than terminating it, if called while the + interpreter is finalizing. .. c:function:: void PyGILState_Release(PyGILState_STATE) @@ -1503,17 +1544,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:: next + 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). diff --git a/Include/internal/pycore_pythread.h b/Include/internal/pycore_pythread.h index 3610c6254db6af..bc389a05e9452d 100644 --- a/Include/internal/pycore_pythread.h +++ b/Include/internal/pycore_pythread.h @@ -152,6 +152,19 @@ PyAPI_FUNC(int) PyThread_join_thread(PyThread_handle_t); * a non-zero value on failure. */ PyAPI_FUNC(int) PyThread_detach_thread(PyThread_handle_t); +/* + * Hangs the thread indefinitely without exiting it. + * + * gh-87135: 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. + * + * This is unfortunate for embedders who may not be terminating their process + * when they're done with the interpreter, but our C API design does not allow + * for safely exiting threads attempting to re-enter Python post finalization. + */ +void _Py_NO_RETURN PyThread_hang_thread(void); #ifdef __cplusplus } diff --git a/Include/pythread.h b/Include/pythread.h index a3216c51d66165..82247daf8e0aa0 100644 --- a/Include/pythread.h +++ b/Include/pythread.h @@ -17,7 +17,26 @@ 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. Considered unsafe. + * + * 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` causes some libcs (glibc?) to attempt to + * unwind the stack and call C++ destructors; if a `noexcept` function is + * reached, they may terminate the process. Others (macOS) do unwinding. + * + * 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.14) PyAPI_FUNC(void) _Py_NO_RETURN PyThread_exit_thread(void); + PyAPI_FUNC(unsigned long) PyThread_get_thread_ident(void); #if (defined(__APPLE__) || defined(__linux__) || defined(_WIN32) \ diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 3ed7fdb3fabde6..bdc84e451d5ad7 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -1237,6 +1237,77 @@ def test_native_id_after_fork(self): self.assertEqual(len(native_ids), 2) self.assertNotEqual(native_ids[0], native_ids[1]) + @cpython_only + def test_finalize_daemon_thread_hang(self): + if support.check_sanitizer(thread=True, memory=True): + # the thread running `time.sleep(100)` below will still be alive + # at process exit + self.skipTest( + "https://github.com/python/cpython/issues/124878 - Known" + " race condition that TSAN identifies.") + # gh-87135: 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) + + class ThreadJoinOnShutdown(BaseTestCase): def _run_and_join(self, script): 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..6387d69bc267c6 --- /dev/null +++ b/Misc/NEWS.d/next/C API/2022-08-05-19-41-20.gh-issue-87135.SCNBYj.rst @@ -0,0 +1,15 @@ +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. + +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/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 97e046a48225a1..72df72e9889ef5 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -3322,6 +3322,34 @@ 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. + Py_FatalError("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 *callback) +{ + // WASI builds some pthread stuff but doesn't have these APIs today? +#if defined(_POSIX_THREADS) && !defined(__wasi__) + pthread_cleanup_push(finalize_thread_hang_cleanup_callback, NULL); +#endif + PyObject_CallNoArgs(callback); + // Should not reach here. + Py_FatalError("thread unexpectedly did not hang"); +#if defined(_POSIX_THREADS) && !defined(__wasi__) + pthread_cleanup_pop(0); +#endif + Py_RETURN_NONE; +} + static void tracemalloc_track_race_thread(void *data) @@ -3590,6 +3618,7 @@ static PyMethodDef TestMethods[] = { {"test_atexit", test_atexit, METH_NOARGS}, {"tracemalloc_track_race", tracemalloc_track_race, METH_NOARGS}, {"toggle_reftrace_printer", toggle_reftrace_printer, METH_O}, + {"finalize_thread_hang", finalize_thread_hang, METH_O, NULL}, {NULL, NULL} /* sentinel */ }; diff --git a/Python/ceval_gil.c b/Python/ceval_gil.c index e82993b8f7ca7f..a1433e5b25fea4 100644 --- a/Python/ceval_gil.c +++ b/Python/ceval_gil.c @@ -7,6 +7,7 @@ #include "pycore_pylifecycle.h" // _PyErr_Print() #include "pycore_pymem.h" // _PyMem_IsPtrFreed() #include "pycore_pystats.h" // _Py_PrintSpecializationStats() +#include "pycore_pythread.h" // PyThread_hang_thread() /* Notes about the implementation: @@ -284,10 +285,9 @@ drop_gil(PyInterpreterState *interp, PyThreadState *tstate, int final_release) /* Take the GIL. The function saves errno at entry and restores its value at exit. + It may hang rather than return if the interpreter has been finalized. - tstate must be non-NULL. - - Returns 1 if the GIL was acquired, or 0 if not. */ + tstate must be non-NULL. */ static void take_gil(PyThreadState *tstate) { @@ -300,12 +300,18 @@ take_gil(PyThreadState *tstate) 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. + thread which called Py_Finalize(), this thread cannot continue. 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(); + PyThreadState freed memory. + + This used to call a *thread_exit API, but that was not safe as it + lacks stack unwinding and local variable destruction important to + C++. gh-87135: The best that can be done is to hang the thread as + the public APIs calling this have no error reporting mechanism (!). + */ + PyThread_hang_thread(); } assert(_PyThreadState_CheckConsistency(tstate)); @@ -349,7 +355,9 @@ take_gil(PyThreadState *tstate) if (drop_requested) { _Py_unset_eval_breaker_bit(holder_tstate, _PY_GIL_DROP_REQUEST_BIT); } - 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(_PyThreadState_CheckConsistency(tstate)); @@ -390,7 +398,7 @@ take_gil(PyThreadState *tstate) if (_PyThreadState_MustExit(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(), gh-87135: hang the thread. This code path can be reached by a daemon thread which was waiting @@ -400,7 +408,7 @@ take_gil(PyThreadState *tstate) /* tstate could be a dangling pointer, so don't pass it to drop_gil(). */ drop_gil(interp, NULL, 1); - PyThread_exit_thread(); + PyThread_hang_thread(); } assert(_PyThreadState_CheckConsistency(tstate)); diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 5e420bdf3377c8..c79fe7a09cad66 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -2069,7 +2069,7 @@ _Py_Finalize(_PyRuntimeState *runtime) /* Ensure that remaining threads are detached */ _PyEval_StopTheWorldAll(runtime); - /* Remaining daemon threads will automatically exit + /* Remaining daemon threads will be trapped in PyThread_hang_thread when they attempt to take the GIL (ex: PyEval_RestoreThread()). */ _PyInterpreterState_SetFinalizing(tstate->interp, tstate); _PyRuntimeState_SetFinalizing(runtime, tstate); diff --git a/Python/thread_nt.h b/Python/thread_nt.h index 425658131c2fce..3a01a7fe327c8f 100644 --- a/Python/thread_nt.h +++ b/Python/thread_nt.h @@ -291,6 +291,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 de95e733ce09f3..da4058242448f3 100644 --- a/Python/thread_pthread.h +++ b/Python/thread_pthread.h @@ -16,6 +16,7 @@ #undef destructor #endif #include +#include /* pause(), also getthrid() on OpenBSD */ #if defined(__linux__) # include /* syscall(SYS_gettid) */ @@ -23,8 +24,6 @@ # include /* pthread_getthreadid_np() */ #elif defined(__FreeBSD_kernel__) # include /* syscall(SYS_thr_self) */ -#elif defined(__OpenBSD__) -# include /* getthrid() */ #elif defined(_AIX) # include /* thread_self() */ #elif defined(__NetBSD__) @@ -431,6 +430,18 @@ PyThread_exit_thread(void) #endif } +void _Py_NO_RETURN +PyThread_hang_thread(void) +{ + while (1) { +#if defined(__wasi__) + sleep(9999999); // WASI doesn't have pause() ?! +#else + pause(); +#endif + } +} + #ifdef USE_SEMAPHORES /* From d946b3d4af98a06b403c4231c26fbc3b8b63b1e8 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Fri, 15 Aug 2025 15:44:24 +0000 Subject: [PATCH 2/4] state "3.13.7 and earlier" --- Doc/c-api/init.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/c-api/init.rst b/Doc/c-api/init.rst index f86eee70757d96..0487d4d7be1fe8 100644 --- a/Doc/c-api/init.rst +++ b/Doc/c-api/init.rst @@ -1103,7 +1103,7 @@ the blocked thread. Gross? Yes. This prevents random crashes and/or unexpectedly skipped C++ finalizations further up the call stack when such threads were forcibly exited -here in CPython 3.13 and earlier. The CPython runtime GIL acquiring C APIs +here in CPython 3.13.7 and earlier. The CPython runtime GIL acquiring C APIs have never had any error reporting or handling expectations at GIL acquisition time that would've allowed for graceful exit from this situation. Changing that would require new stable C APIs and rewriting the majority of C code in the From f412737615013cfbda38934f9cb49f7d927721a0 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Fri, 15 Aug 2025 16:24:15 +0000 Subject: [PATCH 3/4] backport: do not add the deprecated marker --- Include/pythread.h | 4 ++-- .../2022-08-05-19-41-20.gh-issue-87135.SCNBYj.rst | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Include/pythread.h b/Include/pythread.h index 82247daf8e0aa0..4aa221cf21843d 100644 --- a/Include/pythread.h +++ b/Include/pythread.h @@ -33,9 +33,9 @@ PyAPI_FUNC(unsigned long) PyThread_start_new_thread(void (*)(void *), void *); * destructors. * * In either case there is a risk of invalid references remaining to data on the - * thread stack. + * thread stack. This is deprecated in 3.14 onwards. Retained for API compat. */ -Py_DEPRECATED(3.14) PyAPI_FUNC(void) _Py_NO_RETURN PyThread_exit_thread(void); +PyAPI_FUNC(void) _Py_NO_RETURN PyThread_exit_thread(void); PyAPI_FUNC(unsigned long) PyThread_get_thread_ident(void); 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 index 6387d69bc267c6..6753c64598520d 100644 --- 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 @@ -5,11 +5,12 @@ 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. -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 +While not officially marked deprecated until 3.14, ``PyThread_exit_thread`` +is no longer called internally and remains solely for interface compatibility. +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. From d24d14e48a08c54732ceeebd9f75a2accb5a8252 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Sat, 16 Aug 2025 01:18:47 +0000 Subject: [PATCH 4/4] fix Py_IsFinalizing doc ref --- Doc/c-api/init.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/c-api/init.rst b/Doc/c-api/init.rst index 0487d4d7be1fe8..44dc9f1703d10e 100644 --- a/Doc/c-api/init.rst +++ b/Doc/c-api/init.rst @@ -1086,7 +1086,7 @@ 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 +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`.