Skip to content

Commit fbbbc10

Browse files
authored
gh-127266: avoid data races when updating type slots (gh-133177)
In the free-threaded build, avoid data races caused by updating type slots or type flags after the type was initially created. For those (typically rare) cases, use the stop-the-world mechanism. Remove the use of atomics when reading or writing type flags.
1 parent 7ca6d79 commit fbbbc10

File tree

10 files changed

+574
-132
lines changed

10 files changed

+574
-132
lines changed

Include/internal/pycore_interp_structs.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,8 +677,11 @@ struct _Py_interp_cached_objects {
677677

678678
/* object.__reduce__ */
679679
PyObject *objreduce;
680+
#ifndef Py_GIL_DISABLED
681+
/* resolve_slotdups() */
680682
PyObject *type_slots_pname;
681683
pytype_slotdef *type_slots_ptrs[MAX_EQUIV];
684+
#endif
682685

683686
/* TypeVar and related types */
684687
PyTypeObject *generic_type;

Include/internal/pycore_object.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ extern int _PyDict_CheckConsistency(PyObject *mp, int check_content);
313313
// Fast inlined version of PyType_HasFeature()
314314
static inline int
315315
_PyType_HasFeature(PyTypeObject *type, unsigned long feature) {
316-
return ((FT_ATOMIC_LOAD_ULONG_RELAXED(type->tp_flags) & feature) != 0);
316+
return ((type->tp_flags) & feature) != 0;
317317
}
318318

319319
extern void _PyType_InitCache(PyInterpreterState *interp);

Include/internal/pycore_typeobject.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ extern int _PyType_AddMethod(PyTypeObject *, PyMethodDef *);
134134
extern void _PyType_SetFlagsRecursive(PyTypeObject *self, unsigned long mask,
135135
unsigned long flags);
136136

137-
extern unsigned int _PyType_GetVersionForCurrentState(PyTypeObject *tp);
138137
PyAPI_FUNC(void) _PyType_SetVersion(PyTypeObject *tp, unsigned int version);
139138
PyTypeObject *_PyType_LookupByVersion(unsigned int version);
140139

Include/object.h

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,12 @@ given type object has a specified feature.
620620
#define Py_TPFLAGS_HAVE_FINALIZE (1UL << 0)
621621
#define Py_TPFLAGS_HAVE_VERSION_TAG (1UL << 18)
622622

623+
// Flag values for ob_flags (16 bits available, if SIZEOF_VOID_P > 4).
624+
#define _Py_IMMORTAL_FLAGS (1 << 0)
625+
#define _Py_STATICALLY_ALLOCATED_FLAG (1 << 2)
626+
#if defined(Py_GIL_DISABLED) && defined(Py_DEBUG)
627+
#define _Py_TYPE_REVEALED_FLAG (1 << 3)
628+
#endif
623629

624630
#define Py_CONSTANT_NONE 0
625631
#define Py_CONSTANT_FALSE 1
@@ -776,11 +782,7 @@ PyType_HasFeature(PyTypeObject *type, unsigned long feature)
776782
// PyTypeObject is opaque in the limited C API
777783
flags = PyType_GetFlags(type);
778784
#else
779-
# ifdef Py_GIL_DISABLED
780-
flags = _Py_atomic_load_ulong_relaxed(&type->tp_flags);
781-
# else
782-
flags = type->tp_flags;
783-
# endif
785+
flags = type->tp_flags;
784786
#endif
785787
return ((flags & feature) != 0);
786788
}

Include/refcount.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ immortal. The latter should be the only instances that require
1919
cleanup during runtime finalization.
2020
*/
2121

22-
#define _Py_STATICALLY_ALLOCATED_FLAG 4
23-
#define _Py_IMMORTAL_FLAGS 1
24-
2522
#if SIZEOF_VOID_P > 4
2623
/*
2724
In 64+ bit systems, any object whose 32 bit reference count is >= 2**31

Lib/test/test_descr.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4114,6 +4114,34 @@ class E(D):
41144114
else:
41154115
self.fail("shouldn't be able to create inheritance cycles")
41164116

4117+
def test_assign_bases_many_subclasses(self):
4118+
# This is intended to check that typeobject.c:queue_slot_update() can
4119+
# handle updating many subclasses when a slot method is re-assigned.
4120+
class A:
4121+
x = 'hello'
4122+
def __call__(self):
4123+
return 123
4124+
def __getitem__(self, index):
4125+
return None
4126+
4127+
class X:
4128+
x = 'bye'
4129+
4130+
class B(A):
4131+
pass
4132+
4133+
subclasses = []
4134+
for i in range(1000):
4135+
sc = type(f'Sub{i}', (B,), {})
4136+
subclasses.append(sc)
4137+
4138+
self.assertEqual(subclasses[0]()(), 123)
4139+
self.assertEqual(subclasses[0]().x, 'hello')
4140+
B.__bases__ = (X,)
4141+
with self.assertRaises(TypeError):
4142+
subclasses[0]()()
4143+
self.assertEqual(subclasses[0]().x, 'bye')
4144+
41174145
def test_builtin_bases(self):
41184146
# Make sure all the builtin types can have their base queried without
41194147
# segfaulting. See issue #5787.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
In the free-threaded build, avoid data races caused by updating type slots
2+
or type flags after the type was initially created. For those (typically
3+
rare) cases, use the stop-the-world mechanism. Remove the use of atomics
4+
when reading or writing type flags. The use of atomics is not sufficient to
5+
avoid races (since flags are sometimes read without a lock and without
6+
atomics) and are no longer required.

0 commit comments

Comments
 (0)