diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 02c70403185f60..c58fe9d93796a0 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -887,6 +887,59 @@ def test_sys_flags_name_only_attributes(self): self.assertIsInstance(sys.flags.context_aware_warnings, int|type(None)) self.assertIsInstance(sys.flags.lazy_imports, int|type(None)) + @unittest.skipUnless(support.Py_GIL_DISABLED, + "test is only useful if the GIL is disabled") + @threading_helper.reap_threads + @threading_helper.requires_working_threading() + def test_set_int_max_str_digits_concurrent(self): + # Regression test for gh-151218: concurrent + # sys.set_int_max_str_digits() in free-threaded builds previously + # double-freed the old sys.flags tuple item. With the fix in place + # the loop must not crash and must only ever observe valid values + # that some worker wrote. + import threading + + original = sys.get_int_max_str_digits() + self.addCleanup(sys.set_int_max_str_digits, original) + + values = (4300, 5000, 0) + allowed = set(values) | {original} + + start = threading.Barrier(4) + done = threading.Event() + errors = [] + + def worker(): + try: + start.wait(timeout=support.SHORT_TIMEOUT) + iterations = 0 + while iterations < 200 and not done.is_set(): + for value in values: + sys.set_int_max_str_digits(value) + observed = sys.get_int_max_str_digits() + if observed not in allowed: + errors.append(('getter', observed)) + observed_flag = sys.flags.int_max_str_digits + if observed_flag not in allowed: + errors.append(('flag', observed_flag)) + iterations += 1 + except BaseException as exc: + errors.append(exc) + + threads = [threading.Thread(target=worker) for _ in range(4)] + try: + for thread in threads: + thread.start() + finally: + done.set() + for thread in threads: + thread.join() + + self.assertEqual(errors, []) + sys.set_int_max_str_digits(original) + self.assertEqual(sys.get_int_max_str_digits(), original) + self.assertEqual(sys.flags.int_max_str_digits, original) + def assert_raise_on_new_sys_type(self, sys_attr): # Users are intentionally prevented from creating new instances of # sys.flags, sys.version_info, and sys.getwindowsversion. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-10-14-30-00.gh-issue-151218.mK3pRt.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-10-14-30-00.gh-issue-151218.mK3pRt.rst new file mode 100644 index 00000000000000..c0220dee729d55 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-10-14-30-00.gh-issue-151218.mK3pRt.rst @@ -0,0 +1,2 @@ +Fix a data race in :func:`sys.set_int_max_str_digits` when updating +:data:`sys.flags` in the free-threaded build. diff --git a/Python/sysmodule.c b/Python/sysmodule.c index f7e28086d84fab..afbf9b26dd0c21 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -22,6 +22,9 @@ Data members: #include "pycore_import.h" // _PyImport_SetDLOpenFlags() #include "pycore_initconfig.h" // _PyStatus_EXCEPTION() #include "pycore_interpframe.h" // _PyFrame_GetFirstComplete() +#ifdef Py_GIL_DISABLED +# include "pycore_lock.h" // PyMutex_Lock() +#endif #include "pycore_long.h" // _PY_LONG_MAX_STR_DIGITS_THRESHOLD #include "pycore_modsupport.h" // _PyModule_CreateInitialized() #include "pycore_namespace.h" // _PyNamespace_New() @@ -3476,13 +3479,31 @@ static PyStructSequence_Desc flags_desc = { // https://github.com/python/cpython/issues/122575#issuecomment-2416497086 }; +#ifdef Py_GIL_DISABLED +static PyMutex sys_flags_mutex; +#endif + static void -sys_set_flag(PyObject *flags, Py_ssize_t pos, PyObject *value) +sys_set_flag_unlocked(PyObject *flags, Py_ssize_t pos, PyObject *value, + PyObject **p_old_value) { assert(pos >= 0 && pos < (Py_ssize_t)(Py_ARRAY_LENGTH(flags_fields) - 1)); - PyObject *old_value = PyStructSequence_GET_ITEM(flags, pos); + *p_old_value = PyStructSequence_GET_ITEM(flags, pos); PyStructSequence_SET_ITEM(flags, pos, Py_NewRef(value)); +} + +static void +sys_set_flag(PyObject *flags, Py_ssize_t pos, PyObject *value) +{ + PyObject *old_value; +#ifdef Py_GIL_DISABLED + PyMutex_Lock(&sys_flags_mutex); +#endif + sys_set_flag_unlocked(flags, pos, value, &old_value); +#ifdef Py_GIL_DISABLED + PyMutex_Unlock(&sys_flags_mutex); +#endif Py_XDECREF(old_value); } @@ -3501,20 +3522,6 @@ _PySys_SetFlagObj(Py_ssize_t pos, PyObject *value) } -static int -_PySys_SetFlagInt(Py_ssize_t pos, int value) -{ - PyObject *obj = PyLong_FromLong(value); - if (obj == NULL) { - return -1; - } - - int res = _PySys_SetFlagObj(pos, obj); - Py_DECREF(obj); - return res; -} - - static int set_flags_from_config(PyInterpreterState *interp, PyObject *flags) { @@ -4666,16 +4673,40 @@ _PySys_SetIntMaxStrDigits(int maxdigits) return -1; } - // Set sys.flags.int_max_str_digits const Py_ssize_t pos = SYS_FLAGS_INT_MAX_STR_DIGITS; - if (_PySys_SetFlagInt(pos, maxdigits) < 0) { + PyObject *obj = PyLong_FromLong(maxdigits); + if (obj == NULL) { + return -1; + } + +#ifdef Py_GIL_DISABLED + PyMutex_Lock(&sys_flags_mutex); +#endif + + PyObject *flags = PySys_GetAttrString("flags"); + if (flags == NULL) { + Py_DECREF(obj); +#ifdef Py_GIL_DISABLED + PyMutex_Unlock(&sys_flags_mutex); +#endif return -1; } + PyObject *old_value; + sys_set_flag_unlocked(flags, pos, obj, &old_value); + Py_DECREF(flags); + // Set PyInterpreterState.long_state.max_str_digits // and PyInterpreterState.config.int_max_str_digits. PyInterpreterState *interp = _PyInterpreterState_GET(); interp->long_state.max_str_digits = maxdigits; interp->config.int_max_str_digits = maxdigits; + +#ifdef Py_GIL_DISABLED + PyMutex_Unlock(&sys_flags_mutex); +#endif + + Py_DECREF(obj); + Py_XDECREF(old_value); return 0; }