Skip to content
Closed
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()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PyMutex_Lock() is part of Python.h. There is no need to include the internal pycore_lock.h header.

#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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment to explain the purpose of this lock. IMO it should only be used when modifying sys.flags. It's not needed to acquire this lock to get the sys.flags attribute.

#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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you call Py_XDECREF(old_value); in the caller? It seems same to keep Py_XDECREF(old_value); in this function, no?

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can keep this function, it doesn't hurt.

{
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that you need to acquire the lock to get sys.flags. You only need the lock around the code changing sys.flags members.

#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