Skip to content

Commit 2bff4e1

Browse files
committed
gh-151218: fix data race in sys_set_flag for free-threading
Protect sys.flags updates with a mutex in free-threaded builds and keep interpreter int_max_str_digits state consistent with the flag value.
1 parent ce916dc commit 2bff4e1

3 files changed

Lines changed: 85 additions & 4 deletions

File tree

Lib/test/test_sys.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,40 @@ def test_sys_flags_name_only_attributes(self):
887887
self.assertIsInstance(sys.flags.context_aware_warnings, int|type(None))
888888
self.assertIsInstance(sys.flags.lazy_imports, int|type(None))
889889

890+
@unittest.skipUnless(support.Py_GIL_DISABLED,
891+
"test is only useful if the GIL is disabled")
892+
@threading_helper.reap_threads
893+
@threading_helper.requires_working_threading()
894+
def test_set_int_max_str_digits_concurrent(self):
895+
import threading
896+
import time
897+
898+
done = threading.Event()
899+
errors = []
900+
901+
def worker():
902+
try:
903+
while not done.is_set():
904+
sys.set_int_max_str_digits(4300)
905+
sys.set_int_max_str_digits(5000)
906+
sys.set_int_max_str_digits(0)
907+
value = sys.get_int_max_str_digits()
908+
if value != sys.flags.int_max_str_digits:
909+
errors.append((value, sys.flags.int_max_str_digits))
910+
except BaseException as exc:
911+
errors.append(exc)
912+
913+
threads = [threading.Thread(target=worker) for _ in range(8)]
914+
for thread in threads:
915+
thread.start()
916+
try:
917+
time.sleep(SHORT_TIMEOUT)
918+
finally:
919+
done.set()
920+
for thread in threads:
921+
thread.join()
922+
self.assertEqual(errors, [])
923+
890924
def assert_raise_on_new_sys_type(self, sys_attr):
891925
# Users are intentionally prevented from creating new instances of
892926
# sys.flags, sys.version_info, and sys.getwindowsversion.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a data race in :func:`sys.set_int_max_str_digits` when updating
2+
:data:`sys.flags` in the free-threaded build.

Python/sysmodule.c

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ Data members:
2222
#include "pycore_import.h" // _PyImport_SetDLOpenFlags()
2323
#include "pycore_initconfig.h" // _PyStatus_EXCEPTION()
2424
#include "pycore_interpframe.h" // _PyFrame_GetFirstComplete()
25+
#ifdef Py_GIL_DISABLED
26+
# include "pycore_lock.h" // PyMutex_Lock()
27+
#endif
2528
#include "pycore_long.h" // _PY_LONG_MAX_STR_DIGITS_THRESHOLD
2629
#include "pycore_modsupport.h" // _PyModule_CreateInitialized()
2730
#include "pycore_namespace.h" // _PyNamespace_New()
@@ -3476,13 +3479,31 @@ static PyStructSequence_Desc flags_desc = {
34763479
// https://github.com/python/cpython/issues/122575#issuecomment-2416497086
34773480
};
34783481

3482+
#ifdef Py_GIL_DISABLED
3483+
static PyMutex sys_flags_mutex;
3484+
#endif
3485+
34793486
static void
3480-
sys_set_flag(PyObject *flags, Py_ssize_t pos, PyObject *value)
3487+
sys_set_flag_unlocked(PyObject *flags, Py_ssize_t pos, PyObject *value,
3488+
PyObject **p_old_value)
34813489
{
34823490
assert(pos >= 0 && pos < (Py_ssize_t)(Py_ARRAY_LENGTH(flags_fields) - 1));
34833491

3484-
PyObject *old_value = PyStructSequence_GET_ITEM(flags, pos);
3492+
*p_old_value = PyStructSequence_GET_ITEM(flags, pos);
34853493
PyStructSequence_SET_ITEM(flags, pos, Py_NewRef(value));
3494+
}
3495+
3496+
static void
3497+
sys_set_flag(PyObject *flags, Py_ssize_t pos, PyObject *value)
3498+
{
3499+
PyObject *old_value;
3500+
#ifdef Py_GIL_DISABLED
3501+
PyMutex_Lock(&sys_flags_mutex);
3502+
#endif
3503+
sys_set_flag_unlocked(flags, pos, value, &old_value);
3504+
#ifdef Py_GIL_DISABLED
3505+
PyMutex_Unlock(&sys_flags_mutex);
3506+
#endif
34863507
Py_XDECREF(old_value);
34873508
}
34883509

@@ -4666,16 +4687,40 @@ _PySys_SetIntMaxStrDigits(int maxdigits)
46664687
return -1;
46674688
}
46684689

4669-
// Set sys.flags.int_max_str_digits
46704690
const Py_ssize_t pos = SYS_FLAGS_INT_MAX_STR_DIGITS;
4671-
if (_PySys_SetFlagInt(pos, maxdigits) < 0) {
4691+
PyObject *obj = PyLong_FromLong(maxdigits);
4692+
if (obj == NULL) {
4693+
return -1;
4694+
}
4695+
4696+
#ifdef Py_GIL_DISABLED
4697+
PyMutex_Lock(&sys_flags_mutex);
4698+
#endif
4699+
4700+
PyObject *flags = PySys_GetAttrString("flags");
4701+
if (flags == NULL) {
4702+
Py_DECREF(obj);
4703+
#ifdef Py_GIL_DISABLED
4704+
PyMutex_Unlock(&sys_flags_mutex);
4705+
#endif
46724706
return -1;
46734707
}
46744708

4709+
PyObject *old_value;
4710+
sys_set_flag_unlocked(flags, pos, obj, &old_value);
4711+
Py_DECREF(flags);
4712+
46754713
// Set PyInterpreterState.long_state.max_str_digits
46764714
// and PyInterpreterState.config.int_max_str_digits.
46774715
PyInterpreterState *interp = _PyInterpreterState_GET();
46784716
interp->long_state.max_str_digits = maxdigits;
46794717
interp->config.int_max_str_digits = maxdigits;
4718+
4719+
#ifdef Py_GIL_DISABLED
4720+
PyMutex_Unlock(&sys_flags_mutex);
4721+
#endif
4722+
4723+
Py_DECREF(obj);
4724+
Py_XDECREF(old_value);
46804725
return 0;
46814726
}

0 commit comments

Comments
 (0)