From 788b0b6c8c85eb8e9037b306e92078d218450c39 Mon Sep 17 00:00:00 2001 From: Radislav Chugunov Date: Fri, 22 Sep 2023 23:18:43 +0300 Subject: [PATCH 1/6] Delete state of new thread on its startup failure If Python fails to start newly created thread due to failure of underlying PyThread_start_new_thread() call, its state should be removed from interpreter' thread states list to avoid its double cleanup * make 'PyThreadState_Delete' accept unbound thread state * in 'thread_PyThread_start_new_thread', delete state of new thread from interpreter if 'PyThread_start_new_thread' call fails --- Lib/test/test_threading.py | 20 ++++++++++++++++++++ Modules/_threadmodule.c | 1 + Python/pystate.c | 4 +++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 6465a446565844..0d82877339a751 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -1045,6 +1045,26 @@ def exit_handler(): self.assertEqual(out, b'') self.assertIn(b"can't create new thread at interpreter shutdown", err) + def test_start_new_thread_failed(self): + # gh-109746: if Python fails to start newly created thread + # due to failure of underlying PyThread_start_new_thread() call, + # its state should be removed from interpreter' thread states list + # to avoid its double cleanup + code = """if 1: + import resource + import threading + + resource.setrlimit(resource.RLIMIT_NPROC, (150, 150)) + + while True: + t = threading.Thread() + t.start() + t.join() + """ + _, out, err = assert_python_failure("-c", code) + self.assertEqual(out, b'') + self.assertIn(b"can't start new thread", err) + class ThreadJoinOnShutdown(BaseTestCase): def _run_and_join(self, script): diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 7692bacccc4909..071dc90a24f97e 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -1204,6 +1204,7 @@ thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs) if (ident == PYTHREAD_INVALID_THREAD_ID) { PyErr_SetString(ThreadError, "can't start new thread"); PyThreadState_Clear(boot->tstate); + PyThreadState_Delete(boot->tstate); thread_bootstate_free(boot, 1); return NULL; } diff --git a/Python/pystate.c b/Python/pystate.c index dcc6c112215b30..983937202b1069 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1589,7 +1589,9 @@ tstate_delete_common(PyThreadState *tstate) if (tstate->_status.bound_gilstate) { unbind_gilstate_tstate(tstate); } - unbind_tstate(tstate); + if (tstate->_status.bound) { + unbind_tstate(tstate); + } // XXX Move to PyThreadState_Clear()? clear_datastack(tstate); From a2973aed54933ee9cb2c87ef5547984eed4f135e Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 22 Sep 2023 21:01:58 +0000 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst b/Misc/NEWS.d/next/Core and Builtins/2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst new file mode 100644 index 00000000000000..a4fe9a9bcd34c8 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst @@ -0,0 +1 @@ +If :~func:_thread.start_new_thread` fails to start a new thread, it deletes its state from interpreter and thus avoids its repeated cleanup on finalization. From 6ee16d7158f60f63e1d1768baca6a03a49a30207 Mon Sep 17 00:00:00 2001 From: Radislav Chugunov <52372310+chgnrdv@users.noreply.github.com> Date: Sat, 23 Sep 2023 22:52:43 +0300 Subject: [PATCH 3/6] fix news entry --- .../2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst b/Misc/NEWS.d/next/Core and Builtins/2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst index a4fe9a9bcd34c8..2f53a224543668 100644 --- a/Misc/NEWS.d/next/Core and Builtins/2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst +++ b/Misc/NEWS.d/next/Core and Builtins/2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst @@ -1 +1 @@ -If :~func:_thread.start_new_thread` fails to start a new thread, it deletes its state from interpreter and thus avoids its repeated cleanup on finalization. +If :~func:`_thread.start_new_thread` fails to start a new thread, it deletes its state from interpreter and thus avoids its repeated cleanup on finalization. From ce3129bde57954764e258e542690b8b9045e2352 Mon Sep 17 00:00:00 2001 From: Radislav Chugunov <52372310+chgnrdv@users.noreply.github.com> Date: Sat, 23 Sep 2023 23:04:39 +0300 Subject: [PATCH 4/6] fix news entry (again) --- .../2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst b/Misc/NEWS.d/next/Core and Builtins/2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst index 2f53a224543668..2d350c33aa6975 100644 --- a/Misc/NEWS.d/next/Core and Builtins/2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst +++ b/Misc/NEWS.d/next/Core and Builtins/2023-09-22-21-01-56.gh-issue-109746.32MHt9.rst @@ -1 +1 @@ -If :~func:`_thread.start_new_thread` fails to start a new thread, it deletes its state from interpreter and thus avoids its repeated cleanup on finalization. +If :func:`!_thread.start_new_thread` fails to start a new thread, it deletes its state from interpreter and thus avoids its repeated cleanup on finalization. From 1f3ecd9be70a2bbe9a05c6f9397975c1c5d3b0bd Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 18 Jan 2024 20:29:57 +0200 Subject: [PATCH 5/6] Fix tests. --- Lib/test/test_threading.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 5b2e6d78c0412f..be211c7ce68346 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -1111,20 +1111,30 @@ def test_start_new_thread_failed(self): # due to failure of underlying PyThread_start_new_thread() call, # its state should be removed from interpreter' thread states list # to avoid its double cleanup + try: + from resource import setrlimit, RLIMIT_NPROC + except ImportError as err: + self.skipTest(err) # RLIMIT_NPROC is specific to Linux and BSD code = """if 1: import resource - import threading + import _thread - resource.setrlimit(resource.RLIMIT_NPROC, (150, 150)) + def f(): + print("shouldn't be printed") - while True: - t = threading.Thread() - t.start() - t.join() + resource.setrlimit(resource.RLIMIT_NPROC, (0, 0)) + + try: + _thread.start_new_thread(f, ()) + except RuntimeError: + pass + else: + exit('successfully created thread') """ - _, out, err = assert_python_failure("-c", code) + _, out, err = assert_python_ok("-c", code) + self.assertEqual(err, b'') self.assertEqual(out, b'') - self.assertIn(b"can't start new thread", err) + class ThreadJoinOnShutdown(BaseTestCase): From 316220224dec9dabcca081c57c98028fed52f58f Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 22 Nov 2024 20:16:19 +0200 Subject: [PATCH 6/6] Skip test for root user. --- Lib/test/test_threading.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index fa487eba29dd15..fb6d268e5869f4 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -1187,18 +1187,23 @@ def test_start_new_thread_failed(self): def f(): print("shouldn't be printed") - resource.setrlimit(resource.RLIMIT_NPROC, (0, 0)) + limits = resource.getrlimit(resource.RLIMIT_NPROC) + [_, hard] = limits + resource.setrlimit(resource.RLIMIT_NPROC, (0, hard)) try: _thread.start_new_thread(f, ()) except RuntimeError: - pass + print('ok') else: - exit('successfully created thread') + print('skip') """ - _, out, err = assert_python_ok("-c", code) + _, out, err = assert_python_ok("-u", "-c", code) + out = out.strip() + if out == b'skip': + self.skipTest('RLIMIT_NPROC had no effect; probably superuser') + self.assertEqual(out, b'ok') self.assertEqual(err, b'') - self.assertEqual(out, b'') @cpython_only def test_finalize_daemon_thread_hang(self):