Skip to content

Commit bc9baba

Browse files
committed
wip: thread safety for tp_flags
Use separate structure member in PyHeapTypeObject, `ht_flags`, to store a copy of the type flags. That allows safe toggling of some of the flags after the type has been exposed.
1 parent d7bb7c7 commit bc9baba

File tree

12 files changed

+166
-41
lines changed

12 files changed

+166
-41
lines changed

Include/cpython/object.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,15 @@ typedef struct _heaptypeobject {
284284
struct _specialization_cache _spec_cache; // For use by the specializer.
285285
#ifdef Py_GIL_DISABLED
286286
Py_ssize_t unique_id; // ID used for per-thread refcounting
287+
288+
// Used to store type flags that can be toggled after the type
289+
// structure has been potentially exposed to other threads (i.e. after
290+
// type_ready()). We need to use atomic loads and stores for these
291+
// flags, unlike for tp_flags. The set of flags allowed to be toggled are
292+
// POST_READY_FLAGS. They are toggled with type_set_flags_with_mask() and
293+
// need to be tested with _PyType_HasFeatureSafe(), in order to avoid data
294+
// races.
295+
unsigned long ht_flags;
287296
#endif
288297
/* here are optional user slots, followed by the members. */
289298
} PyHeapTypeObject;

Include/internal/pycore_call.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ extern "C" {
99
#endif
1010

1111
#include "pycore_pystate.h" // _PyThreadState_GET()
12+
#include "pycore_object.h" // _PyType_HaveFeatureSafe()
1213

1314
/* Suggested size (number of positional arguments) for arrays of PyObject*
1415
allocated on a C stack to avoid allocating memory on the heap memory. Such
@@ -116,7 +117,7 @@ _PyVectorcall_FunctionInline(PyObject *callable)
116117
assert(callable != NULL);
117118

118119
PyTypeObject *tp = Py_TYPE(callable);
119-
if (!PyType_HasFeature(tp, Py_TPFLAGS_HAVE_VECTORCALL)) {
120+
if (!_PyType_HasFeatureSafe(tp, Py_TPFLAGS_HAVE_VECTORCALL)) {
120121
return NULL;
121122
}
122123
assert(PyCallable_Check(callable));

Include/internal/pycore_object.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,20 @@ extern int _PyDict_CheckConsistency(PyObject *mp, int check_content);
303303
// Fast inlined version of PyType_HasFeature()
304304
static inline int
305305
_PyType_HasFeature(PyTypeObject *type, unsigned long feature) {
306-
return ((FT_ATOMIC_LOAD_ULONG_RELAXED(type->tp_flags) & feature) != 0);
306+
return (type->tp_flags & feature) != 0;
307+
}
308+
309+
// Variant of above function that uses safely reads type flags that can be
310+
// toggled after the type is first created.
311+
static inline int
312+
_PyType_HasFeatureSafe(PyTypeObject *type, unsigned long feature) {
313+
#ifdef Py_GIL_DISABLED
314+
if (_PyType_HasFeature(type, Py_TPFLAGS_HEAPTYPE)) {
315+
PyHeapTypeObject *ht = (PyHeapTypeObject*)type;
316+
return (FT_ATOMIC_LOAD_ULONG_RELAXED(ht->ht_flags) & feature) != 0;
317+
}
318+
#endif
319+
return (type->tp_flags & feature) != 0;
307320
}
308321

309322
extern void _PyType_InitCache(PyInterpreterState *interp);

Include/object.h

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -774,11 +774,7 @@ PyType_HasFeature(PyTypeObject *type, unsigned long feature)
774774
// PyTypeObject is opaque in the limited C API
775775
flags = PyType_GetFlags(type);
776776
#else
777-
# ifdef Py_GIL_DISABLED
778-
flags = _Py_atomic_load_ulong_relaxed(&type->tp_flags);
779-
# else
780-
flags = type->tp_flags;
781-
# endif
777+
flags = type->tp_flags;
782778
#endif
783779
return ((flags & feature) != 0);
784780
}

Lib/test/test_free_threading/test_races.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,45 @@ def mutate():
130130
# with the cell binding being changed).
131131
do_race(access, mutate)
132132

133+
def test_racing_tp_flags_is_abstract(self):
134+
class C:
135+
pass
136+
137+
def access():
138+
obj = C()
139+
140+
def mutate():
141+
C.__abstractmethods__ = set()
142+
time.sleep(0)
143+
del C.__abstractmethods__
144+
time.sleep(0)
145+
146+
# The "mutate" method will set and clear the Py_TPFLAGS_IS_ABSTRACT type flag.
147+
do_race(access, mutate)
148+
149+
def test_racing_tp_flags_vectorcall(self):
150+
class C:
151+
pass
152+
153+
def access():
154+
try:
155+
C()()
156+
except Exception:
157+
pass
158+
159+
def mutate():
160+
nonlocal C
161+
# This will clear the Py_TPFLAGS_VECTORCALL flag.
162+
C.__call__ = lambda self: None
163+
time.sleep(0)
164+
# There is no way to re-set the flag it so we create a new class.
165+
class C:
166+
pass
167+
time.sleep(0)
168+
169+
do_race(access, mutate)
170+
171+
133172
def test_racing_to_bool(self):
134173

135174
seq = [1]

Lib/test/test_sys.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1750,6 +1750,7 @@ def delx(self): del self.__x
17501750
s = vsize(fmt)
17511751
check(int, s)
17521752
typeid = 'n' if support.Py_GIL_DISABLED else ''
1753+
ht_flags = 'l' if support.Py_GIL_DISABLED else ''
17531754
# class
17541755
s = vsize(fmt + # PyTypeObject
17551756
'4P' # PyAsyncMethods
@@ -1760,6 +1761,7 @@ def delx(self): del self.__x
17601761
'7P'
17611762
'1PIP' # Specializer cache
17621763
+ typeid # heap type id (free-threaded only)
1764+
+ ht_flags
17631765
)
17641766
class newstyleclass(object): pass
17651767
# Separate block for PyDictKeysObject with 8 keys and 5 entries

Modules/_testcapi/vectorcall.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include "parts.h"
55
#include "clinic/vectorcall.c.h"
6+
#include "pycore_object.h" // _PyType_HasFeatureSafe()
67

78

89
#include <stddef.h> // offsetof
@@ -255,7 +256,7 @@ static int
255256
_testcapi_has_vectorcall_flag_impl(PyObject *module, PyTypeObject *type)
256257
/*[clinic end generated code: output=3ae8d1374388c671 input=8eee492ac548749e]*/
257258
{
258-
return PyType_HasFeature(type, Py_TPFLAGS_HAVE_VECTORCALL);
259+
return _PyType_HasFeatureSafe(type, Py_TPFLAGS_HAVE_VECTORCALL);
259260
}
260261

261262
static PyMethodDef TestMethods[] = {

Objects/typeobject.c

Lines changed: 90 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -347,23 +347,43 @@ _PyStaticType_GetBuiltins(void)
347347
static void
348348
type_set_flags(PyTypeObject *tp, unsigned long flags)
349349
{
350-
if (tp->tp_flags & Py_TPFLAGS_READY) {
351-
// It's possible the type object has been exposed to other threads
352-
// if it's been marked ready. In that case, the type lock should be
353-
// held when flags are modified.
354-
ASSERT_TYPE_LOCK_HELD();
355-
}
356-
// Since PyType_HasFeature() reads the flags without holding the type
357-
// lock, we need an atomic store here.
358-
FT_ATOMIC_STORE_ULONG_RELAXED(tp->tp_flags, flags);
350+
// In the free-threaded build, this can only be used on types that have
351+
// not yet been exposed to other threads. Use type_set_flags_with_mask()
352+
// for flags that need to be toggled afterwards.
353+
tp->tp_flags = flags;
359354
}
360355

356+
// Flags used by pattern matching
357+
#define COLLECTION_FLAGS (Py_TPFLAGS_SEQUENCE | Py_TPFLAGS_MAPPING)
358+
359+
// This is the set of flags are allowed to be toggled after type_ready()
360+
// is called (and the type potentially exposed to other threads).
361+
#define POST_READY_FLAGS \
362+
(COLLECTION_FLAGS | Py_TPFLAGS_IS_ABSTRACT | Py_TPFLAGS_HAVE_VECTORCALL)
363+
364+
// Used to toggle type flags after the type has been created. Only the flags
365+
// in POST_READY_FLAGS are allowed to be toggled.
361366
static void
362367
type_set_flags_with_mask(PyTypeObject *tp, unsigned long mask, unsigned long flags)
363368
{
364369
ASSERT_TYPE_LOCK_HELD();
370+
// Non-heap types are immutable and so these flags can only be toggled
371+
// after creation on heap types.
372+
assert(_PyType_HasFeature(tp, Py_TPFLAGS_HEAPTYPE));
365373
unsigned long new_flags = (tp->tp_flags & ~mask) | flags;
366-
type_set_flags(tp, new_flags);
374+
#ifdef Py_DEBUG
375+
unsigned long changed_flags = tp->tp_flags ^ new_flags;
376+
assert((changed_flags & ~POST_READY_FLAGS) == 0);
377+
#endif
378+
#ifdef Py_GIL_DISABLED
379+
PyHeapTypeObject *ht = (PyHeapTypeObject*)tp;
380+
FT_ATOMIC_STORE_ULONG_RELAXED(ht->ht_flags, new_flags);
381+
#else
382+
// In this case the GIL protects us and we don't need the separate ht_flags
383+
// member and atomics to read and write. Instead we can just toggle the
384+
// flags directly in tp_flags.
385+
tp->tp_flags = new_flags;
386+
#endif
367387
}
368388

369389
static void
@@ -1318,7 +1338,6 @@ int PyUnstable_Type_AssignVersionTag(PyTypeObject *type)
13181338
static PyMemberDef type_members[] = {
13191339
{"__basicsize__", Py_T_PYSSIZET, offsetof(PyTypeObject,tp_basicsize),Py_READONLY},
13201340
{"__itemsize__", Py_T_PYSSIZET, offsetof(PyTypeObject, tp_itemsize), Py_READONLY},
1321-
{"__flags__", Py_T_ULONG, offsetof(PyTypeObject, tp_flags), Py_READONLY},
13221341
/* Note that this value is misleading for static builtin types,
13231342
since the memory at this offset will always be NULL. */
13241343
{"__weakrefoffset__", Py_T_PYSSIZET,
@@ -1584,9 +1603,9 @@ type_set_abstractmethods(PyObject *tp, PyObject *value, void *Py_UNUSED(closure)
15841603
BEGIN_TYPE_LOCK();
15851604
type_modified_unlocked(type);
15861605
if (abstract)
1587-
type_add_flags(type, Py_TPFLAGS_IS_ABSTRACT);
1606+
type_set_flags_with_mask(type, 0, Py_TPFLAGS_IS_ABSTRACT);
15881607
else
1589-
type_clear_flags(type, Py_TPFLAGS_IS_ABSTRACT);
1608+
type_set_flags_with_mask(type, Py_TPFLAGS_IS_ABSTRACT, 0);
15901609
END_TYPE_LOCK();
15911610

15921611
return 0;
@@ -2109,6 +2128,24 @@ type_set_type_params(PyObject *tp, PyObject *value, void *Py_UNUSED(closure))
21092128
return result;
21102129
}
21112130

2131+
static PyObject *
2132+
type_get_flags(PyObject *tp, void *Py_UNUSED(closure))
2133+
{
2134+
PyTypeObject *type = PyTypeObject_CAST(tp);
2135+
unsigned long flags;
2136+
#ifdef Py_GIL_DISABLED
2137+
if (_PyType_HasFeature(type, Py_TPFLAGS_HEAPTYPE)) {
2138+
PyHeapTypeObject *ht = (PyHeapTypeObject*)type;
2139+
flags = FT_ATOMIC_LOAD_ULONG_RELAXED(ht->ht_flags);
2140+
}
2141+
else {
2142+
flags = type->tp_flags;
2143+
}
2144+
#else
2145+
flags = type->tp_flags;
2146+
#endif
2147+
return PyLong_FromLong(flags);
2148+
}
21122149

21132150
/*[clinic input]
21142151
type.__instancecheck__ -> bool
@@ -2157,6 +2194,7 @@ static PyGetSetDef type_getsets[] = {
21572194
{"__annotations__", type_get_annotations, type_set_annotations, NULL},
21582195
{"__annotate__", type_get_annotate, type_set_annotate, NULL},
21592196
{"__type_params__", type_get_type_params, type_set_type_params, NULL},
2197+
{"__flags__", type_get_flags, NULL, NULL},
21602198
{0}
21612199
};
21622200

@@ -3687,7 +3725,13 @@ type_init(PyObject *cls, PyObject *args, PyObject *kwds)
36873725
unsigned long
36883726
PyType_GetFlags(PyTypeObject *type)
36893727
{
3690-
return FT_ATOMIC_LOAD_ULONG_RELAXED(type->tp_flags);
3728+
#ifdef Py_GIL_DISABLED
3729+
if (_PyType_HasFeature(type, Py_TPFLAGS_HEAPTYPE)) {
3730+
PyHeapTypeObject *ht = (PyHeapTypeObject*)type;
3731+
return FT_ATOMIC_LOAD_ULONG_RELAXED(ht->ht_flags);
3732+
}
3733+
#endif
3734+
return type->tp_flags;
36913735
}
36923736

36933737

@@ -4005,6 +4049,7 @@ type_new_alloc(type_new_ctx *ctx)
40054049

40064050
#ifdef Py_GIL_DISABLED
40074051
et->unique_id = _PyObject_AssignUniqueId((PyObject *)et);
4052+
et->ht_flags = type->tp_flags;
40084053
#endif
40094054

40104055
return type;
@@ -4428,6 +4473,7 @@ type_new_init(type_new_ctx *ctx)
44284473
return NULL;
44294474
}
44304475

4476+
static int type_ready(PyTypeObject *type, int initial);
44314477

44324478
static PyObject*
44334479
type_new_impl(type_new_ctx *ctx)
@@ -4441,13 +4487,28 @@ type_new_impl(type_new_ctx *ctx)
44414487
goto error;
44424488
}
44434489

4490+
4491+
int res;
4492+
4493+
// This lock isn't strictly necessary because the type has not been
4494+
// exposed to anyone else yet, but update_one_slot calls find_name_in_mro
4495+
// where we'd like to assert that the type is locked. Also, type_ready()
4496+
// will assert that the type lock is held.
4497+
BEGIN_TYPE_LOCK();
4498+
44444499
/* Initialize the rest */
4445-
if (PyType_Ready(type) < 0) {
4446-
goto error;
4500+
res = type_ready(type, 1);
4501+
4502+
if (res >= 0) {
4503+
// Put the proper slots in place
4504+
fixup_slot_dispatchers(type);
44474505
}
44484506

4449-
// Put the proper slots in place
4450-
fixup_slot_dispatchers(type);
4507+
END_TYPE_LOCK();
4508+
4509+
if (res < 0) {
4510+
goto error;
4511+
}
44514512

44524513
if (!_PyDict_HasOnlyStringKeys(type->tp_dict)) {
44534514
if (PyErr_WarnFormat(
@@ -6603,7 +6664,7 @@ object_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
66036664
}
66046665
}
66056666

6606-
if (type->tp_flags & Py_TPFLAGS_IS_ABSTRACT) {
6667+
if (_PyType_HasFeatureSafe(type, Py_TPFLAGS_IS_ABSTRACT)) {
66076668
PyObject *abstract_methods;
66086669
PyObject *sorted_methods;
66096670
PyObject *joined;
@@ -8100,7 +8161,7 @@ inherit_slots(PyTypeObject *type, PyTypeObject *base)
81008161

81018162
/* Inherit Py_TPFLAGS_HAVE_VECTORCALL if tp_call is not overridden */
81028163
if (!type->tp_call &&
8103-
_PyType_HasFeature(base, Py_TPFLAGS_HAVE_VECTORCALL))
8164+
_PyType_HasFeatureSafe(base, Py_TPFLAGS_HAVE_VECTORCALL))
81048165
{
81058166
type_add_flags(type, Py_TPFLAGS_HAVE_VECTORCALL);
81068167
}
@@ -8169,8 +8230,6 @@ inherit_slots(PyTypeObject *type, PyTypeObject *base)
81698230
static int add_operators(PyTypeObject *type);
81708231
static int add_tp_new_wrapper(PyTypeObject *type);
81718232

8172-
#define COLLECTION_FLAGS (Py_TPFLAGS_SEQUENCE | Py_TPFLAGS_MAPPING)
8173-
81748233
static int
81758234
type_ready_pre_checks(PyTypeObject *type)
81768235
{
@@ -8451,7 +8510,7 @@ type_ready_inherit_as_structs(PyTypeObject *type, PyTypeObject *base)
84518510
static void
84528511
inherit_patma_flags(PyTypeObject *type, PyTypeObject *base) {
84538512
if ((type->tp_flags & COLLECTION_FLAGS) == 0) {
8454-
type_add_flags(type, base->tp_flags & COLLECTION_FLAGS);
8513+
type_add_flags(type, PyType_GetFlags(base) & COLLECTION_FLAGS);
84558514
}
84568515
}
84578516

@@ -8718,6 +8777,13 @@ type_ready(PyTypeObject *type, int initial)
87188777
}
87198778
}
87208779

8780+
#ifdef Py_GIL_DISABLED
8781+
if (_PyType_HasFeature(type, Py_TPFLAGS_HEAPTYPE)) {
8782+
PyHeapTypeObject *ht = (PyHeapTypeObject*)type;
8783+
FT_ATOMIC_STORE_ULONG_RELAXED(ht->ht_flags, type->tp_flags);
8784+
}
8785+
#endif
8786+
87218787
/* All done -- set the ready flag */
87228788
type_add_flags(type, Py_TPFLAGS_READY);
87238789
stop_readying(type);
@@ -11159,7 +11225,7 @@ update_one_slot(PyTypeObject *type, pytype_slotdef *p)
1115911225
generic = p->function;
1116011226
if (p->function == slot_tp_call) {
1116111227
/* A generic __call__ is incompatible with vectorcall */
11162-
type_clear_flags(type, Py_TPFLAGS_HAVE_VECTORCALL);
11228+
_PyType_SetFlags(type, Py_TPFLAGS_HAVE_VECTORCALL, 0);
1116311229
}
1116411230
}
1116511231
Py_DECREF(descr);
@@ -11228,9 +11294,6 @@ update_slot(PyTypeObject *type, PyObject *name)
1122811294
static void
1122911295
fixup_slot_dispatchers(PyTypeObject *type)
1123011296
{
11231-
// This lock isn't strictly necessary because the type has not been
11232-
// exposed to anyone else yet, but update_ont_slot calls find_name_in_mro
11233-
// where we'd like to assert that the type is locked.
1123411297
BEGIN_TYPE_LOCK();
1123511298

1123611299
assert(!PyErr_Occurred());

0 commit comments

Comments
 (0)