Skip to content

Commit 5bb5f69

Browse files
committed
gh-87135: Hang non-main threads that attempt to acquire the GIL during finalization
1 parent 698a0da commit 5bb5f69

File tree

9 files changed

+215
-22
lines changed

9 files changed

+215
-22
lines changed

Doc/c-api/init.rst

+52-16
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,11 @@ Initializing and finalizing the interpreter
401401
freed. Some memory allocated by extension modules may not be freed. Some
402402
extensions may not work properly if their initialization routine is called more
403403
than once; this can happen if an application calls :c:func:`Py_Initialize` and
404-
:c:func:`Py_FinalizeEx` more than once.
404+
:c:func:`Py_FinalizeEx` more than once. :c:func:`Py_FinalizeEx` must not be
405+
called recursively from within itself. Therefore, it must not be called by any
406+
code that may be run as part of the interpreter shutdown process, such as
407+
:py:mod:`atexit` handlers, object finalizers, or any code that may be run while
408+
flushing the stdout and stderr files.
405409

406410
.. audit-event:: cpython._PySys_ClearAuditHooks "" c.Py_FinalizeEx
407411

@@ -804,6 +808,29 @@ thread, where the CPython global runtime was originally initialized.
804808
The only exception is if :c:func:`exec` will be called immediately
805809
after.
806810
811+
.. _cautions-regarding-runtime-finalization:
812+
813+
Cautions regarding runtime finalization
814+
---------------------------------------
815+
816+
In the late stage of :term:`interpreter shutdown`, after attempting to wait for
817+
non-daemon threads to exit (though this can be interrupted by
818+
:class:`KeyboardInterrupt`) and running the :mod:`atexit` functions, the runtime
819+
is marked as *finalizing*: :c:func:`_Py_IsFinalizing` and
820+
:func:`sys.is_finalizing` return true. At this point, only the *finalization
821+
thread* that initiated finalization (typically the main thread) is allowed to
822+
acquire the :term:`GIL`.
823+
824+
If any thread, other than the finalization thread, attempts to acquire the GIL
825+
during finalization, either explicitly via :c:func:`PyGILState_Ensure`,
826+
:c:macro:`Py_END_ALLOW_THREADS`, :c:func:`PyEval_AcquireThread`, or
827+
:c:func:`PyEval_AcquireLock`, or implicitly when the interpreter attempts to
828+
reacquire it after having yielded it, the thread enters a permanently blocked
829+
state where it remains until the program exits. In most cases this is harmless,
830+
but this can result in deadlock if a later stage of finalization attempts to
831+
acquire a lock owned by the blocked thread, or otherwise waits on the blocked
832+
thread.
833+
807834
808835
High-level API
809836
--------------
@@ -847,11 +874,14 @@ code, or when embedding the Python interpreter:
847874
ensues.
848875
849876
.. note::
850-
Calling this function from a thread when the runtime is finalizing
851-
will terminate the thread, even if the thread was not created by Python.
852-
You can use :c:func:`_Py_IsFinalizing` or :func:`sys.is_finalizing` to
853-
check if the interpreter is in process of being finalized before calling
854-
this function to avoid unwanted termination.
877+
Calling this function from a thread when the runtime is finalizing will
878+
hang the thread until the program exits, even if the thread was not
879+
created by Python. Refer to
880+
:ref:`cautions-regarding-runtime-finalization` for more details.
881+
882+
.. versionchanged:: 3.12
883+
Hangs the current thread, rather than terminating it, if called while the
884+
interpreter is finalizing.
855885
856886
.. c:function:: PyThreadState* PyThreadState_Get()
857887
@@ -893,11 +923,14 @@ with sub-interpreters:
893923
to call arbitrary Python code. Failure is a fatal error.
894924
895925
.. note::
896-
Calling this function from a thread when the runtime is finalizing
897-
will terminate the thread, even if the thread was not created by Python.
898-
You can use :c:func:`_Py_IsFinalizing` or :func:`sys.is_finalizing` to
899-
check if the interpreter is in process of being finalized before calling
900-
this function to avoid unwanted termination.
926+
Calling this function from a thread when the runtime is finalizing will
927+
hang the thread until the program exits, even if the thread was not
928+
created by Python. Refer to
929+
:ref:`cautions-regarding-runtime-finalization` for more details.
930+
931+
.. versionchanged:: 3.12
932+
Hangs the current thread, rather than terminating it, if called while the
933+
interpreter is finalizing.
901934
902935
.. c:function:: void PyGILState_Release(PyGILState_STATE)
903936
@@ -1175,17 +1208,20 @@ All of the following functions must be called after :c:func:`Py_Initialize`.
11751208
If this thread already has the lock, deadlock ensues.
11761209
11771210
.. note::
1178-
Calling this function from a thread when the runtime is finalizing
1179-
will terminate the thread, even if the thread was not created by Python.
1180-
You can use :c:func:`_Py_IsFinalizing` or :func:`sys.is_finalizing` to
1181-
check if the interpreter is in process of being finalized before calling
1182-
this function to avoid unwanted termination.
1211+
Calling this function from a thread when the runtime is finalizing will
1212+
hang the thread until the program exits, even if the thread was not
1213+
created by Python. Refer to
1214+
:ref:`cautions-regarding-runtime-finalization` for more details.
11831215
11841216
.. versionchanged:: 3.8
11851217
Updated to be consistent with :c:func:`PyEval_RestoreThread`,
11861218
:c:func:`Py_END_ALLOW_THREADS`, and :c:func:`PyGILState_Ensure`,
11871219
and terminate the current thread if called while the interpreter is finalizing.
11881220
1221+
.. versionchanged:: 3.12
1222+
Hangs the current thread, rather than terminating it, if called while the
1223+
interpreter is finalizing.
1224+
11891225
:c:func:`PyEval_RestoreThread` is a higher-level function which is always
11901226
available (even when threads have not been initialized).
11911227

Include/pythread.h

+31-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,37 @@ typedef enum PyLockStatus {
1717

1818
PyAPI_FUNC(void) PyThread_init_thread(void);
1919
PyAPI_FUNC(unsigned long) PyThread_start_new_thread(void (*)(void *), void *);
20-
PyAPI_FUNC(void) _Py_NO_RETURN PyThread_exit_thread(void);
20+
/* Terminates the current thread.
21+
*
22+
* WARNING: This function is only safe to call if all functions in the full call
23+
* stack are written to safely allow it. Additionally, the behavior is
24+
* platform-dependent. This function should be avoided, and is no longer called
25+
* by Python itself. It is retained only for compatibility with existing C
26+
* extension code.
27+
*
28+
* With pthreads, calls `pthread_exit` which attempts to unwind the stack and
29+
* call C++ destructors. If a `noexcept` function is reached, the program is
30+
* terminated.
31+
*
32+
* On Windows, calls `_endthreadex` which kills the thread without calling C++
33+
* destructors.
34+
*
35+
* In either case there is a risk of invalid references remaining to data on the
36+
* thread stack.
37+
*/
38+
Py_DEPRECATED(3.12) PyAPI_FUNC(void) _Py_NO_RETURN PyThread_exit_thread(void);
39+
40+
#ifndef Py_LIMITED_API
41+
/* Hangs the thread indefinitely without exiting it.
42+
*
43+
* bpo-42969: There is no safe way to exit a thread other than returning
44+
* normally from its start function. This is used during finalization in lieu
45+
* of actually exiting the thread. Since the program is expected to terminate
46+
* soon anyway, it does not matter if the thread stack stays around until then.
47+
*/
48+
PyAPI_FUNC(void) _Py_NO_RETURN _PyThread_hang_thread(void);
49+
#endif /* !Py_LIMITED_API */
50+
2151
PyAPI_FUNC(unsigned long) PyThread_get_thread_ident(void);
2252

2353
#if (defined(__APPLE__) || defined(__linux__) || defined(_WIN32) \

Lib/test/test_threading.py

+64
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,70 @@ def exit_handler():
10371037
self.assertEqual(out, b'')
10381038
self.assertIn(b"can't create new thread at interpreter shutdown", err)
10391039

1040+
@cpython_only
1041+
def test_finalize_daemon_thread_hang(self):
1042+
# bpo-42969: tests that daemon threads hang during finalization
1043+
script = textwrap.dedent('''
1044+
import os
1045+
import sys
1046+
import threading
1047+
import time
1048+
import _testcapi
1049+
1050+
lock = threading.Lock()
1051+
lock.acquire()
1052+
thread_started_event = threading.Event()
1053+
def thread_func():
1054+
try:
1055+
thread_started_event.set()
1056+
_testcapi.finalize_thread_hang(lock.acquire)
1057+
finally:
1058+
# Control must not reach here.
1059+
os._exit(2)
1060+
1061+
t = threading.Thread(target=thread_func)
1062+
t.daemon = True
1063+
t.start()
1064+
thread_started_event.wait()
1065+
# Sleep to ensure daemon thread is blocked on `lock.acquire`
1066+
#
1067+
# Note: This test is designed so that in the unlikely case that
1068+
# `0.1` seconds is not sufficient time for the thread to become
1069+
# blocked on `lock.acquire`, the test will still pass, it just
1070+
# won't be properly testing the thread behavior during
1071+
# finalization.
1072+
time.sleep(0.1)
1073+
1074+
def run_during_finalization():
1075+
# Wake up daemon thread
1076+
lock.release()
1077+
# Sleep to give the daemon thread time to crash if it is going
1078+
# to.
1079+
#
1080+
# Note: If due to an exceptionally slow execution this delay is
1081+
# insufficient, the test will still pass but will simply be
1082+
# ineffective as a test.
1083+
time.sleep(0.1)
1084+
# If control reaches here, the test succeeded.
1085+
os._exit(0)
1086+
1087+
# Replace sys.stderr.flush as a way to run code during finalization
1088+
orig_flush = sys.stderr.flush
1089+
def do_flush(*args, **kwargs):
1090+
orig_flush(*args, **kwargs)
1091+
if not sys.is_finalizing:
1092+
return
1093+
sys.stderr.flush = orig_flush
1094+
run_during_finalization()
1095+
1096+
sys.stderr.flush = do_flush
1097+
1098+
# If the follow exit code is retained, `run_during_finalization`
1099+
# did not run.
1100+
sys.exit(1)
1101+
''')
1102+
assert_python_ok("-c", script)
1103+
10401104
class ThreadJoinOnShutdown(BaseTestCase):
10411105

10421106
def _run_and_join(self, script):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Attempting to acquire the GIL after runtime finalization has begun in a
2+
different thread now causes the thread to hang rather than terminate, which
3+
avoids potential crashes or memory corruption caused by attempting to
4+
terminate a thread that is running code not specifically designed to support
5+
termination. In most cases this hanging is harmless since the process will
6+
soon exit anyway.
7+
8+
The ``PyThread_exit_thread`` function is now deprecated. Its behavior is
9+
inconsistent across platforms, and it can only be used safely in the
10+
unlikely case that every function in the entire call stack has been designed
11+
to support the platform-dependent termination mechanism. It is recommended
12+
that users of this function change their design to not require thread
13+
termination. In the unlikely case that thread termination is needed and can
14+
be done safely, users may migrate to calling platform-specific APIs such as
15+
``pthread_exit`` (POSIX) or ``_endthreadex`` (Windows) directly.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Daemon threads and other threads not created by Python are now paused rather
2+
than unsafely terminated if they attempt to acquire the GIL during Python
3+
finalization.

Modules/_testcapimodule.c

+28
Original file line numberDiff line numberDiff line change
@@ -3324,6 +3324,33 @@ test_atexit(PyObject *self, PyObject *Py_UNUSED(args))
33243324
Py_RETURN_NONE;
33253325
}
33263326

3327+
// Used by `finalize_thread_hang`.
3328+
#ifdef _POSIX_THREADS
3329+
static void finalize_thread_hang_cleanup_callback(void *Py_UNUSED(arg)) {
3330+
// Should not reach here.
3331+
assert(0 && "pthread thread termination was triggered unexpectedly");
3332+
}
3333+
#endif
3334+
3335+
// Tests that finalization does not trigger pthread cleanup.
3336+
//
3337+
// Must be called with a single nullary callable function that should block
3338+
// (with GIL released) until finalization is in progress.
3339+
static PyObject *
3340+
finalize_thread_hang(PyObject *self, PyObject *arg)
3341+
{
3342+
#ifdef _POSIX_THREADS
3343+
pthread_cleanup_push(finalize_thread_hang_cleanup_callback, NULL);
3344+
#endif
3345+
PyObject_CallNoArgs(arg);
3346+
// Should not reach here.
3347+
Py_FatalError("thread unexpectedly did not hang");
3348+
#ifdef _POSIX_THREADS
3349+
pthread_cleanup_pop(0);
3350+
#endif
3351+
Py_RETURN_NONE;
3352+
}
3353+
33273354

33283355
static PyMethodDef TestMethods[] = {
33293356
{"set_errno", set_errno, METH_VARARGS},
@@ -3468,6 +3495,7 @@ static PyMethodDef TestMethods[] = {
34683495
{"function_get_kw_defaults", function_get_kw_defaults, METH_O, NULL},
34693496
{"function_set_kw_defaults", function_set_kw_defaults, METH_VARARGS, NULL},
34703497
{"test_atexit", test_atexit, METH_NOARGS},
3498+
{"finalize_thread_hang", finalize_thread_hang, METH_O, NULL},
34713499
{NULL, NULL} /* sentinel */
34723500
};
34733501

Python/ceval_gil.c

+6-5
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ take_gil(PyThreadState *tstate)
382382
This code path can be reached by a daemon thread after Py_Finalize()
383383
completes. In this case, tstate is a dangling pointer: points to
384384
PyThreadState freed memory. */
385-
PyThread_exit_thread();
385+
_PyThread_hang_thread();
386386
}
387387

388388
assert(is_tstate_valid(tstate));
@@ -424,7 +424,9 @@ take_gil(PyThreadState *tstate)
424424
if (drop_requested) {
425425
RESET_GIL_DROP_REQUEST(interp);
426426
}
427-
PyThread_exit_thread();
427+
// gh-87135: hang the thread as *thread_exit() is not a safe
428+
// API. It lacks stack unwind and local variable destruction.
429+
_PyThread_hang_thread();
428430
}
429431
assert(is_tstate_valid(tstate));
430432

@@ -455,15 +457,15 @@ take_gil(PyThreadState *tstate)
455457

456458
if (tstate_must_exit(tstate)) {
457459
/* bpo-36475: If Py_Finalize() has been called and tstate is not
458-
the thread which called Py_Finalize(), exit immediately the
460+
the thread which called Py_Finalize(), bpo-42969: hang the
459461
thread.
460462
461463
This code path can be reached by a daemon thread which was waiting
462464
in take_gil() while the main thread called
463465
wait_for_thread_shutdown() from Py_Finalize(). */
464466
MUTEX_UNLOCK(gil->mutex);
465467
drop_gil(ceval, tstate);
466-
PyThread_exit_thread();
468+
_PyThread_hang_thread();
467469
}
468470
assert(is_tstate_valid(tstate));
469471

@@ -1123,4 +1125,3 @@ _Py_HandlePending(PyThreadState *tstate)
11231125

11241126
return 0;
11251127
}
1126-

Python/thread_nt.h

+8
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,14 @@ PyThread_exit_thread(void)
257257
_endthreadex(0);
258258
}
259259

260+
void _Py_NO_RETURN
261+
_PyThread_hang_thread(void)
262+
{
263+
while (1) {
264+
SleepEx(INFINITE, TRUE);
265+
}
266+
}
267+
260268
/*
261269
* Lock support. It has to be implemented as semaphores.
262270
* I [Dag] tried to implement it with mutex but I could find a way to

Python/thread_pthread.h

+8
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,14 @@ PyThread_exit_thread(void)
359359
pthread_exit(0);
360360
}
361361

362+
void _Py_NO_RETURN
363+
_PyThread_hang_thread(void)
364+
{
365+
while (1) {
366+
pause();
367+
}
368+
}
369+
362370
#ifdef USE_SEMAPHORES
363371

364372
/*

0 commit comments

Comments
 (0)