Skip to content

Commit 07d6226

Browse files
committed
gh-140746: Fix Thread.start() hanging when the thread dies during bootstrap
If the new thread dies before _bootstrap_inner() reaches self._started.set() (e.g. a MemoryError raised while calling _bootstrap under memory pressure, or a silently-cleared SystemExit), the parent thread waits forever in Thread.start() and the dead thread lingers in threading._limbo. Add a C-level thread_started PyEvent to ThreadHandle, notified by the thread right after it sets _started and unconditionally by thread_run() on exit (covering a failed bootstrap call, SystemExit, and the finalization early exit). Thread.start() now waits on this event instead of the Python-level event alone and raises RuntimeError if the thread terminated before signalling startup.
1 parent 93beea7 commit 07d6226

5 files changed

Lines changed: 121 additions & 2 deletions

File tree

Doc/library/threading.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,10 @@ since it is impossible to detect the termination of alien threads.
567567
of control.
568568

569569
This method will raise a :exc:`RuntimeError` if called more than once
570-
on the same thread object.
570+
on the same thread object. It also raises :exc:`RuntimeError` if the
571+
new thread terminates before it has fully started, which can happen
572+
under memory pressure (for example when a :exc:`MemoryError` occurs
573+
while the thread is bootstrapping).
571574

572575
If supported, set the operating system thread name to
573576
:attr:`threading.Thread.name`. The name can be truncated depending on the
@@ -576,6 +579,10 @@ since it is impossible to detect the termination of alien threads.
576579
.. versionchanged:: 3.14
577580
Set the operating system thread name.
578581

582+
.. versionchanged:: next
583+
:exc:`RuntimeError` is now raised if the thread dies before it has
584+
fully started; previously this method could hang forever.
585+
579586
.. method:: run()
580587

581588
Method representing the thread's activity.

Lib/test/test_threading.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1505,6 +1505,55 @@ def run_in_bg():
15051505
self.assertEqual(err, b"")
15061506
self.assertEqual(out.strip(), b"Exiting...")
15071507

1508+
def test_start_thread_dies_in_bootstrap(self):
1509+
# gh-140746: if the newly spawned thread dies before signalling
1510+
# that it has started (e.g. a MemoryError while calling _bootstrap),
1511+
# Thread.start() must raise instead of hanging forever.
1512+
class BrokenBootstrapThread(threading.Thread):
1513+
def _bootstrap(self):
1514+
# Simulates a failure before self._started.set(): the
1515+
# C-level thread_run() catches the exception and the
1516+
# thread exits without ever signalling startup.
1517+
raise MemoryError('out of memory during bootstrap')
1518+
1519+
t = BrokenBootstrapThread(target=lambda: None)
1520+
with support.catch_unraisable_exception():
1521+
with self.assertRaisesRegex(RuntimeError,
1522+
"thread failed to start"):
1523+
t.start()
1524+
self.assertFalse(t.is_alive())
1525+
self.assertNotIn(t, threading._limbo)
1526+
self.assertNotIn(t, threading.enumerate())
1527+
1528+
def test_start_thread_dies_in_bootstrap_inner(self):
1529+
# gh-140746: same as above with the failure inside
1530+
# _bootstrap_inner(), before self._started.set().
1531+
t = threading.Thread(target=lambda: None)
1532+
with (support.catch_unraisable_exception(),
1533+
mock.patch.object(t, '_set_ident',
1534+
side_effect=MemoryError('no memory'))):
1535+
with self.assertRaisesRegex(RuntimeError,
1536+
"thread failed to start"):
1537+
t.start()
1538+
self.assertFalse(t.is_alive())
1539+
self.assertNotIn(t, threading._limbo)
1540+
self.assertNotIn(t, threading.enumerate())
1541+
1542+
def test_start_thread_exits_in_bootstrap(self):
1543+
# gh-140746: SystemExit is silently ignored by the C-level
1544+
# thread_run(); it must not make Thread.start() hang either.
1545+
class ExitingBootstrapThread(threading.Thread):
1546+
def _bootstrap(self):
1547+
raise SystemExit
1548+
1549+
t = ExitingBootstrapThread(target=lambda: None)
1550+
with self.assertRaisesRegex(RuntimeError, "thread failed to start"):
1551+
t.start()
1552+
self.assertFalse(t.is_alive())
1553+
self.assertNotIn(t, threading._limbo)
1554+
self.assertNotIn(t, threading.enumerate())
1555+
1556+
15081557
class ThreadJoinOnShutdown(BaseTestCase):
15091558

15101559
def _run_and_join(self, script):

Lib/threading.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1144,7 +1144,21 @@ def start(self):
11441144
with _active_limbo_lock:
11451145
del _limbo[self]
11461146
raise
1147-
self._started.wait() # Will set ident and native_id
1147+
# Wait for the thread to signal that it has started (this also
1148+
# ensures that ident and native_id are set). Waiting on the
1149+
# Python-level _started event alone can hang forever: if the thread
1150+
# dies during its bootstrap (e.g. a MemoryError while calling
1151+
# _bootstrap), _started is never set. The C-level event is also
1152+
# notified when the thread exits, so this wait cannot hang
1153+
# (see gh-140746).
1154+
self._os_thread_handle._wait_for_started()
1155+
if not self._started.is_set():
1156+
# The thread terminated before signalling that it started: it
1157+
# never ran and never will. Clean up and fail loudly rather
1158+
# than returning a dead, half-initialized thread.
1159+
with _active_limbo_lock:
1160+
del _limbo[self]
1161+
raise RuntimeError("thread failed to start")
11481162

11491163
def run(self):
11501164
"""Method representing the thread's activity.
@@ -1205,6 +1219,9 @@ def _bootstrap_inner(self):
12051219
self._set_native_id()
12061220
self._set_os_name()
12071221
self._started.set()
1222+
# Signal the C-level event *after* _started so that a waiter
1223+
# woken by it always observes _started as set (gh-140746).
1224+
self._os_thread_handle._set_started()
12081225
with _active_limbo_lock:
12091226
_active[self._ident] = self
12101227
del _limbo[self]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix :meth:`threading.Thread.start` hanging forever when the new thread dies
2+
before signalling that it has started, for instance when a
3+
:exc:`MemoryError` is raised during the thread's bootstrap under memory
4+
pressure. :meth:`!Thread.start` now raises :exc:`RuntimeError` in that case
5+
and the dead thread no longer lingers in :func:`threading.enumerate`.

Modules/_threadmodule.c

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ typedef struct {
146146
// for a more detailed explanation.
147147
PyEvent thread_is_exiting;
148148

149+
// Set by the thread when its Python-level bootstrap has signalled
150+
// startup (see threading.Thread._bootstrap_inner), or on thread exit
151+
// if the bootstrap terminated before doing so. threading.Thread.start()
152+
// waits on this event rather than on the Python-level started event
153+
// alone, so that it cannot hang forever if the thread dies during its
154+
// bootstrap (e.g. a MemoryError, see gh-140746).
155+
PyEvent thread_started;
156+
149157
// Serializes calls to `join` and `set_done`.
150158
_PyOnceFlag once;
151159

@@ -232,6 +240,7 @@ ThreadHandle_new(void)
232240
self->os_handle = 0;
233241
self->has_os_handle = 0;
234242
self->thread_is_exiting = (PyEvent){0};
243+
self->thread_started = (PyEvent){0};
235244
self->mutex = (PyMutex){_Py_UNLOCKED};
236245
self->once = (_PyOnceFlag){0};
237246
self->state = THREAD_HANDLE_NOT_STARTED;
@@ -323,6 +332,7 @@ _PyThread_AfterFork(struct _pythread_runtime_state *state)
323332
handle->once = (_PyOnceFlag){_Py_ONCE_INITIALIZED};
324333
handle->mutex = (PyMutex){_Py_UNLOCKED};
325334
_PyEvent_Notify(&handle->thread_is_exiting);
335+
_PyEvent_Notify(&handle->thread_started);
326336
llist_remove(node);
327337
remove_from_shutdown_handles(handle);
328338
}
@@ -409,6 +419,12 @@ thread_run(void *boot_raw)
409419
// Don't need to wait for this thread anymore
410420
remove_from_shutdown_handles(handle);
411421

422+
// gh-140746: Unblock any thread waiting in Thread.start(). If the
423+
// bootstrap function terminated before signalling startup (e.g. a
424+
// MemoryError while calling it), this is the only notification the
425+
// waiter will get. If startup was already signalled, this is a no-op.
426+
_PyEvent_Notify(&handle->thread_started);
427+
412428
_PyEvent_Notify(&handle->thread_is_exiting);
413429
ThreadHandle_decref(handle);
414430

@@ -708,6 +724,28 @@ PyThreadHandleObject_join(PyObject *op, PyObject *args)
708724
Py_RETURN_NONE;
709725
}
710726

727+
static PyObject *
728+
PyThreadHandleObject_set_started(PyObject *op, PyObject *Py_UNUSED(dummy))
729+
{
730+
PyThreadHandleObject *self = PyThreadHandleObject_CAST(op);
731+
_PyEvent_Notify(&self->handle->thread_started);
732+
Py_RETURN_NONE;
733+
}
734+
735+
static PyObject *
736+
PyThreadHandleObject_wait_for_started(PyObject *op, PyObject *Py_UNUSED(dummy))
737+
{
738+
PyThreadHandleObject *self = PyThreadHandleObject_CAST(op);
739+
while (!PyEvent_WaitTimed(&self->handle->thread_started, -1,
740+
/*detach=*/1)) {
741+
// Interrupted
742+
if (Py_MakePendingCalls() < 0) {
743+
return NULL;
744+
}
745+
}
746+
Py_RETURN_NONE;
747+
}
748+
711749
static PyObject *
712750
PyThreadHandleObject_is_done(PyObject *op, PyObject *Py_UNUSED(dummy))
713751
{
@@ -741,6 +779,9 @@ static PyGetSetDef ThreadHandle_getsetlist[] = {
741779
static PyMethodDef ThreadHandle_methods[] = {
742780
{"join", PyThreadHandleObject_join, METH_VARARGS, NULL},
743781
{"_set_done", PyThreadHandleObject_set_done, METH_NOARGS, NULL},
782+
{"_set_started", PyThreadHandleObject_set_started, METH_NOARGS, NULL},
783+
{"_wait_for_started", PyThreadHandleObject_wait_for_started,
784+
METH_NOARGS, NULL},
744785
{"is_done", PyThreadHandleObject_is_done, METH_NOARGS, NULL},
745786
{0, 0}
746787
};

0 commit comments

Comments
 (0)