Skip to content

Commit 3ab94d6

Browse files
authored
gh-148393: Use atomic ops on _ma_watcher_tag in free threading build (gh-148397)
Fixes data races between dict mutation and watch/unwatch on the same dict.
1 parent 03d2f03 commit 3ab94d6

File tree

6 files changed

+44
-6
lines changed

6 files changed

+44
-6
lines changed

Include/internal/pycore_dict.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ _PyDict_NotifyEvent(PyDict_WatchEvent event,
292292
PyObject *value)
293293
{
294294
assert(Py_REFCNT((PyObject*)mp) > 0);
295-
int watcher_bits = mp->_ma_watcher_tag & DICT_WATCHER_MASK;
295+
int watcher_bits = FT_ATOMIC_LOAD_UINT64_RELAXED(mp->_ma_watcher_tag) & DICT_WATCHER_MASK;
296296
if (watcher_bits) {
297297
RARE_EVENT_STAT_INC(watched_dict_modification);
298298
_PyDict_SendEvent(watcher_bits, event, mp, key, value);
@@ -368,7 +368,7 @@ PyDictObject *_PyObject_MaterializeManagedDict_LockHeld(PyObject *);
368368
static inline Py_ssize_t
369369
_PyDict_UniqueId(PyDictObject *mp)
370370
{
371-
return (Py_ssize_t)(mp->_ma_watcher_tag >> DICT_UNIQUE_ID_SHIFT);
371+
return (Py_ssize_t)(FT_ATOMIC_LOAD_UINT64_RELAXED(mp->_ma_watcher_tag) >> DICT_UNIQUE_ID_SHIFT);
372372
}
373373

374374
static inline void

Include/internal/pycore_pyatomic_ft_wrappers.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ extern "C" {
4949
_Py_atomic_load_uint16_relaxed(&value)
5050
#define FT_ATOMIC_LOAD_UINT32_RELAXED(value) \
5151
_Py_atomic_load_uint32_relaxed(&value)
52+
#define FT_ATOMIC_LOAD_UINT64_RELAXED(value) \
53+
_Py_atomic_load_uint64_relaxed(&value)
5254
#define FT_ATOMIC_LOAD_ULONG_RELAXED(value) \
5355
_Py_atomic_load_ulong_relaxed(&value)
5456
#define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) \
@@ -71,6 +73,12 @@ extern "C" {
7173
_Py_atomic_store_uint16_relaxed(&value, new_value)
7274
#define FT_ATOMIC_STORE_UINT32_RELAXED(value, new_value) \
7375
_Py_atomic_store_uint32_relaxed(&value, new_value)
76+
#define FT_ATOMIC_AND_UINT64(value, new_value) \
77+
(void)_Py_atomic_and_uint64(&value, new_value)
78+
#define FT_ATOMIC_OR_UINT64(value, new_value) \
79+
(void)_Py_atomic_or_uint64(&value, new_value)
80+
#define FT_ATOMIC_ADD_UINT64(value, new_value) \
81+
(void)_Py_atomic_add_uint64(&value, new_value)
7482
#define FT_ATOMIC_STORE_CHAR_RELAXED(value, new_value) \
7583
_Py_atomic_store_char_relaxed(&value, new_value)
7684
#define FT_ATOMIC_LOAD_CHAR_RELAXED(value) \
@@ -146,6 +154,7 @@ extern "C" {
146154
#define FT_ATOMIC_LOAD_UINT8_RELAXED(value) value
147155
#define FT_ATOMIC_LOAD_UINT16_RELAXED(value) value
148156
#define FT_ATOMIC_LOAD_UINT32_RELAXED(value) value
157+
#define FT_ATOMIC_LOAD_UINT64_RELAXED(value) value
149158
#define FT_ATOMIC_LOAD_ULONG_RELAXED(value) value
150159
#define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) value = new_value
151160
#define FT_ATOMIC_STORE_PTR_RELEASE(value, new_value) value = new_value
@@ -157,6 +166,9 @@ extern "C" {
157166
#define FT_ATOMIC_STORE_UINT8_RELAXED(value, new_value) value = new_value
158167
#define FT_ATOMIC_STORE_UINT16_RELAXED(value, new_value) value = new_value
159168
#define FT_ATOMIC_STORE_UINT32_RELAXED(value, new_value) value = new_value
169+
#define FT_ATOMIC_AND_UINT64(value, new_value) (void)(value &= new_value)
170+
#define FT_ATOMIC_OR_UINT64(value, new_value) (void)(value |= new_value)
171+
#define FT_ATOMIC_ADD_UINT64(value, new_value) (void)(value += new_value)
160172
#define FT_ATOMIC_LOAD_CHAR_RELAXED(value) value
161173
#define FT_ATOMIC_STORE_CHAR_RELAXED(value, new_value) value = new_value
162174
#define FT_ATOMIC_LOAD_UCHAR_RELAXED(value) value

Lib/test/test_free_threading/test_dict.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,29 @@ def reader():
245245
with threading_helper.start_threads([t1, t2]):
246246
pass
247247

248+
@unittest.skipIf(_testcapi is None, "requires _testcapi")
249+
def test_racing_watch_unwatch_dict(self):
250+
# gh-148393: race between PyDict_Watch / PyDict_Unwatch
251+
# and concurrent dict mutation reading _ma_watcher_tag.
252+
wid = _testcapi.add_dict_watcher(0)
253+
try:
254+
d = {}
255+
ITERS = 1000
256+
257+
def writer():
258+
for i in range(ITERS):
259+
d[i] = i
260+
del d[i]
261+
262+
def watcher():
263+
for _ in range(ITERS):
264+
_testcapi.watch_dict(wid, d)
265+
_testcapi.unwatch_dict(wid, d)
266+
267+
threading_helper.run_concurrently([writer, watcher])
268+
finally:
269+
_testcapi.clear_dict_watcher(wid)
270+
248271
def test_racing_dict_update_and_method_lookup(self):
249272
# gh-144295: test race between dict modifications and method lookups.
250273
# Uses BytesIO because the race requires a type without Py_TPFLAGS_INLINE_VALUES
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix data races between :c:func:`PyDict_Watch` / :c:func:`PyDict_Unwatch`
2+
and concurrent dict mutation in the :term:`free-threaded build`.

Objects/dictobject.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8028,7 +8028,7 @@ PyDict_Watch(int watcher_id, PyObject* dict)
80288028
if (validate_watcher_id(interp, watcher_id)) {
80298029
return -1;
80308030
}
8031-
((PyDictObject*)dict)->_ma_watcher_tag |= (1LL << watcher_id);
8031+
FT_ATOMIC_OR_UINT64(((PyDictObject*)dict)->_ma_watcher_tag, (1LL << watcher_id));
80328032
return 0;
80338033
}
80348034

@@ -8043,7 +8043,7 @@ PyDict_Unwatch(int watcher_id, PyObject* dict)
80438043
if (validate_watcher_id(interp, watcher_id)) {
80448044
return -1;
80458045
}
8046-
((PyDictObject*)dict)->_ma_watcher_tag &= ~(1LL << watcher_id);
8046+
FT_ATOMIC_AND_UINT64(((PyDictObject*)dict)->_ma_watcher_tag, ~(1LL << watcher_id));
80478047
return 0;
80488048
}
80498049

Python/optimizer_analysis.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,15 @@ static int
119119
get_mutations(PyObject* dict) {
120120
assert(PyDict_CheckExact(dict));
121121
PyDictObject *d = (PyDictObject *)dict;
122-
return (d->_ma_watcher_tag >> DICT_MAX_WATCHERS) & ((1 << DICT_WATCHED_MUTATION_BITS)-1);
122+
uint64_t tag = FT_ATOMIC_LOAD_UINT64_RELAXED(d->_ma_watcher_tag);
123+
return (tag >> DICT_MAX_WATCHERS) & ((1 << DICT_WATCHED_MUTATION_BITS) - 1);
123124
}
124125

125126
static void
126127
increment_mutations(PyObject* dict) {
127128
assert(PyDict_CheckExact(dict));
128129
PyDictObject *d = (PyDictObject *)dict;
129-
d->_ma_watcher_tag += (1 << DICT_MAX_WATCHERS);
130+
FT_ATOMIC_ADD_UINT64(d->_ma_watcher_tag, (1 << DICT_MAX_WATCHERS));
130131
}
131132

132133
/* The first two dict watcher IDs are reserved for CPython,

0 commit comments

Comments
 (0)