From a90576d72c8a409f7c3ffcaefafb13945e3405ab Mon Sep 17 00:00:00 2001 From: Steve Stagg Date: Fri, 3 Jul 2026 18:43:24 +0100 Subject: [PATCH 1/4] gh-152951 - Fix double DECREF when `newblock` fails during deque.extend calls (#152961) --- .../next/Library/2026-07-03-14-54-33.gh-issue-152951.u8tPCI.rst | 2 ++ Modules/_collectionsmodule.c | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-03-14-54-33.gh-issue-152951.u8tPCI.rst diff --git a/Misc/NEWS.d/next/Library/2026-07-03-14-54-33.gh-issue-152951.u8tPCI.rst b/Misc/NEWS.d/next/Library/2026-07-03-14-54-33.gh-issue-152951.u8tPCI.rst new file mode 100644 index 000000000000000..c3d56c6d59c91b3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-03-14-54-33.gh-issue-152951.u8tPCI.rst @@ -0,0 +1,2 @@ +:class:`collections.deque` prevent rare crash when calling ``extend`` under +high memory pressure conditions. diff --git a/Modules/_collectionsmodule.c b/Modules/_collectionsmodule.c index e96a546a818d3d7..62d1dad5735ec8e 100644 --- a/Modules/_collectionsmodule.c +++ b/Modules/_collectionsmodule.c @@ -512,7 +512,6 @@ deque_extend_impl(dequeobject *deque, PyObject *iterable) iternext = *Py_TYPE(it)->tp_iternext; while ((item = iternext(it)) != NULL) { if (deque_append_lock_held(deque, item, maxlen) == -1) { - Py_DECREF(item); Py_DECREF(it); return NULL; } From 423ae0ff36c6485f722e0fe3274124dc3df09862 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 3 Jul 2026 14:11:16 -0400 Subject: [PATCH 2/4] gh-150579: use lazy imports for concurrent.futures (GH-150585) This module has a manual lazy import hack using `__getattr__`. Now that lazy imports exist and cannot be disabled, this could use lazy imports instead. Key differences: this will now show up in sys.lazy_modules when accessed. Error messages should be a bit better without the wrapper `__getattr__` involved. That's the only differences I can think of. Signed-off-by: Henry Schreiner Co-authored-by: Gregory P. Smith --- Lib/concurrent/futures/__init__.py | 22 ++++--------------- ...-03-17-29-34.gh-issue-150579.0tLpQukIU.rst | 2 ++ 2 files changed, 6 insertions(+), 18 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-03-17-29-34.gh-issue-150579.0tLpQukIU.rst diff --git a/Lib/concurrent/futures/__init__.py b/Lib/concurrent/futures/__init__.py index d6ac4b3e0b675f6..5d53a5ac50931d3 100644 --- a/Lib/concurrent/futures/__init__.py +++ b/Lib/concurrent/futures/__init__.py @@ -17,6 +17,9 @@ wait, as_completed) +lazy from .process import ProcessPoolExecutor +lazy from .thread import ThreadPoolExecutor + __all__ = [ 'FIRST_COMPLETED', 'FIRST_EXCEPTION', @@ -40,26 +43,9 @@ _interpreters = None if _interpreters: + lazy from .interpreter import InterpreterPoolExecutor # noqa: F401 __all__.append('InterpreterPoolExecutor') def __dir__(): return __all__ + ['__author__', '__doc__'] - - -def __getattr__(name): - global ProcessPoolExecutor, ThreadPoolExecutor, InterpreterPoolExecutor - - if name == 'ProcessPoolExecutor': - from .process import ProcessPoolExecutor - return ProcessPoolExecutor - - if name == 'ThreadPoolExecutor': - from .thread import ThreadPoolExecutor - return ThreadPoolExecutor - - if _interpreters and name == 'InterpreterPoolExecutor': - from .interpreter import InterpreterPoolExecutor - return InterpreterPoolExecutor - - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/Misc/NEWS.d/next/Library/2026-07-03-17-29-34.gh-issue-150579.0tLpQukIU.rst b/Misc/NEWS.d/next/Library/2026-07-03-17-29-34.gh-issue-150579.0tLpQukIU.rst new file mode 100644 index 000000000000000..7aff384431edc77 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-03-17-29-34.gh-issue-150579.0tLpQukIU.rst @@ -0,0 +1,2 @@ +:mod:`concurrent.futures` now uses lazy imports for its executor submodules +instead of a module ``__getattr__`` hook. From cde31ec135905472f1137dddcc8227af2430d89c Mon Sep 17 00:00:00 2001 From: "Jiucheng(Oliver)" Date: Fri, 3 Jul 2026 16:19:09 -0400 Subject: [PATCH 3/4] gh-151644: Fix data race in sys.setdlopenflags/getdlopenflags under free-threading (gh-151768) In free-threading builds, concurrent calls to sys.getdlopenflags() and sys.setdlopenflags() race on interp->imports.dlopenflags. Fix by using FT_ATOMIC_LOAD_INT_RELAXED / FT_ATOMIC_STORE_INT_RELAXED in _PyImport_GetDLOpenFlags and _PyImport_SetDLOpenFlags, consistent with how analogous interpreter-state integer fields (lazy_imports_mode, pystats_enabled) are protected. Relaxed ordering is correct here: dlopenflags is a standalone config integer with no ordering relationship to other memory. Co-Authored-By: Claude Sonnet 4.6 --- Lib/test/test_free_threading/test_sys.py | 21 +++++++++++++++++++ ...-06-18-00-00-00.gh-issue-151644.5cFffN.rst | 4 ++++ Python/import.c | 4 ++-- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-18-00-00-00.gh-issue-151644.5cFffN.rst diff --git a/Lib/test/test_free_threading/test_sys.py b/Lib/test/test_free_threading/test_sys.py index 37b53bd723fd767..271fdd13c62b668 100644 --- a/Lib/test/test_free_threading/test_sys.py +++ b/Lib/test/test_free_threading/test_sys.py @@ -4,6 +4,27 @@ class SysModuleTest(unittest.TestCase): + @unittest.skipUnless(hasattr(sys, "setdlopenflags"), + "test needs sys.setdlopenflags()") + def test_dlopenflags_concurrent(self): + # gh-151644: getdlopenflags() and setdlopenflags() must be safe to + # call concurrently in free-threaded builds. + original = sys.getdlopenflags() + self.addCleanup(sys.setdlopenflags, original) + + # Use a small set of known-valid flag values to avoid integer overflow. + flag_values = [1, 2, 256, 257] + + def worker(worker_id): + for i in range(20_000): + if worker_id % 2 == 0: + sys.getdlopenflags() + else: + sys.setdlopenflags(flag_values[worker_id % len(flag_values)]) + + workers = [lambda i=i: worker(i) for i in range(6)] + threading_helper.run_concurrently(workers) + def test_int_max_str_digits_thread(self): # gh-151218: Check that it's safe to call get_int_max_str_digits() and # set_int_max_str_digits() in parallel. Previously, this test triggered diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-18-00-00-00.gh-issue-151644.5cFffN.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-18-00-00-00.gh-issue-151644.5cFffN.rst new file mode 100644 index 000000000000000..ff5058e14b42f15 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-18-00-00-00.gh-issue-151644.5cFffN.rst @@ -0,0 +1,4 @@ +Fix a data race in :func:`sys.setdlopenflags` and :func:`sys.getdlopenflags` +when called concurrently in the free-threaded build. The underlying +``_PyImport_GetDLOpenFlags`` and ``_PyImport_SetDLOpenFlags`` functions now +use atomic load/store operations. diff --git a/Python/import.c b/Python/import.c index 9b315b3125bd942..63e23e21beb1266 100644 --- a/Python/import.c +++ b/Python/import.c @@ -908,13 +908,13 @@ _PyImport_SwapPackageContext(const char *newcontext) int _PyImport_GetDLOpenFlags(PyInterpreterState *interp) { - return DLOPENFLAGS(interp); + return FT_ATOMIC_LOAD_INT_RELAXED(DLOPENFLAGS(interp)); } void _PyImport_SetDLOpenFlags(PyInterpreterState *interp, int new_val) { - DLOPENFLAGS(interp) = new_val; + FT_ATOMIC_STORE_INT_RELAXED(DLOPENFLAGS(interp), new_val); } #endif // HAVE_DLOPEN From 548c7314138b85cab3b945a1ce05440dd836a2ea Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:01:58 -0700 Subject: [PATCH 4/4] gh-83386: Enable test_hang_gh83386 for ProcessPoolExecutor (GH-152976) The hang this test guards against (interpreter exit after shutdown(wait=False) with running futures) was fixed for ProcessPoolExecutor by the executor management rewrite years ago, but the test still skipped it citing the issue. The skip also hid a latent NameError in the subprocess template, which only selects a start method in the process pool variants; rewrite it to use the same mp_context=get_context() shape as the sibling templates. Remove a stale comment claiming ProcessPoolExecutor often hangs with wait=False. Co-authored-by: Claude Fable 5 --- .../test_concurrent_futures/test_shutdown.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_concurrent_futures/test_shutdown.py b/Lib/test/test_concurrent_futures/test_shutdown.py index c576df1068ed320..129b5cb5057093f 100644 --- a/Lib/test/test_concurrent_futures/test_shutdown.py +++ b/Lib/test/test_concurrent_futures/test_shutdown.py @@ -115,20 +115,21 @@ def test_hang_gh83386(self): See https://github.com/python/cpython/issues/83386. """ - if self.executor_type == futures.ProcessPoolExecutor: - raise unittest.SkipTest( - "Hangs, see https://github.com/python/cpython/issues/83386") - rc, out, err = assert_python_ok('-c', """if True: from concurrent.futures import {executor_type} from test.test_concurrent_futures.test_shutdown import sleep_and_print if __name__ == "__main__": - if {context!r}: multiprocessing.set_start_method({context!r}) - t = {executor_type}(max_workers=3) + context = '{context}' + if context == "": + t = {executor_type}(max_workers=3) + else: + from multiprocessing import get_context + t = {executor_type}(max_workers=3, + mp_context=get_context(context)) t.submit(sleep_and_print, 1.0, "apple") t.shutdown(wait=False) """.format(executor_type=self.executor_type.__name__, - context=getattr(self, 'ctx', None))) + context=getattr(self, 'ctx', ''))) self.assertFalse(err) self.assertEqual(out.strip(), b"apple") @@ -243,8 +244,6 @@ def test_thread_names_default(self): t.join() def test_cancel_futures_wait_false(self): - # Can only be reliably tested for TPE, since PPE often hangs with - # `wait=False` (even without *cancel_futures*). rc, out, err = assert_python_ok('-c', """if True: from concurrent.futures import ThreadPoolExecutor from test.test_concurrent_futures.test_shutdown import sleep_and_print