Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
67 changes: 49 additions & 18 deletions Python/sysmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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);
}

Expand All @@ -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)
{
Expand Down Expand Up @@ -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;
}
Loading