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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy