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 6da6faf5f28cc3b..ae44ebc5c01a39d 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