Skip to content

Commit cd74ed0

Browse files
[3.13] gh-120198: Stop the world when setting __class__ on free-threaded build (#121591)
(cherry-picked from commit 3bfc9c8)
1 parent 15c875a commit cd74ed0

File tree

5 files changed

+65
-58
lines changed

5 files changed

+65
-58
lines changed

Include/internal/pycore_dict.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,8 @@ _PyInlineValuesSize(PyTypeObject *tp)
323323
int
324324
_PyDict_DetachFromObject(PyDictObject *dict, PyObject *obj);
325325

326+
PyDictObject *_PyObject_MaterializeManagedDict_LockHeld(PyObject *);
327+
326328
#ifdef __cplusplus
327329
}
328330
#endif

Include/object.h

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -327,11 +327,7 @@ static inline Py_ssize_t Py_REFCNT(PyObject *ob) {
327327

328328
// bpo-39573: The Py_SET_TYPE() function must be used to set an object type.
329329
static inline PyTypeObject* Py_TYPE(PyObject *ob) {
330-
#ifdef Py_GIL_DISABLED
331-
return (PyTypeObject *)_Py_atomic_load_ptr_relaxed(&ob->ob_type);
332-
#else
333330
return ob->ob_type;
334-
#endif
335331
}
336332
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000
337333
# define Py_TYPE(ob) Py_TYPE(_PyObject_CAST(ob))
@@ -421,11 +417,7 @@ static inline void Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) {
421417

422418

423419
static inline void Py_SET_TYPE(PyObject *ob, PyTypeObject *type) {
424-
#ifdef Py_GIL_DISABLED
425-
_Py_atomic_store_ptr(&ob->ob_type, type);
426-
#else
427420
ob->ob_type = type;
428-
#endif
429421
}
430422
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000
431423
# define Py_SET_TYPE(ob, type) Py_SET_TYPE(_PyObject_CAST(ob), type)

Lib/test/test_free_threading/test_type.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ class Bar:
106106
thing = Foo()
107107
def work():
108108
foo = thing
109-
for _ in range(10000):
109+
for _ in range(5000):
110110
foo.__class__ = Bar
111111
type(foo)
112112
foo.__class__ = Foo

Objects/dictobject.c

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ ASSERT_DICT_LOCKED(PyObject *op)
158158
if (!_PyInterpreterState_GET()->stoptheworld.world_stopped) { \
159159
ASSERT_DICT_LOCKED(op); \
160160
}
161+
#define ASSERT_WORLD_STOPPED_OR_OBJ_LOCKED(op) \
162+
if (!_PyInterpreterState_GET()->stoptheworld.world_stopped) { \
163+
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op); \
164+
}
161165

162166
#define IS_DICT_SHARED(mp) _PyObject_GC_IS_SHARED(mp)
163167
#define SET_DICT_SHARED(mp) _PyObject_GC_SET_SHARED(mp)
@@ -226,6 +230,7 @@ static inline void split_keys_entry_added(PyDictKeysObject *keys)
226230

227231
#define ASSERT_DICT_LOCKED(op)
228232
#define ASSERT_WORLD_STOPPED_OR_DICT_LOCKED(op)
233+
#define ASSERT_WORLD_STOPPED_OR_OBJ_LOCKED(op)
229234
#define LOCK_KEYS(keys)
230235
#define UNLOCK_KEYS(keys)
231236
#define ASSERT_KEYS_LOCKED(keys)
@@ -6673,10 +6678,10 @@ make_dict_from_instance_attributes(PyInterpreterState *interp,
66736678
return res;
66746679
}
66756680

6676-
static PyDictObject *
6677-
materialize_managed_dict_lock_held(PyObject *obj)
6681+
PyDictObject *
6682+
_PyObject_MaterializeManagedDict_LockHeld(PyObject *obj)
66786683
{
6679-
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(obj);
6684+
ASSERT_WORLD_STOPPED_OR_OBJ_LOCKED(obj);
66806685

66816686
PyDictValues *values = _PyObject_InlineValues(obj);
66826687
PyInterpreterState *interp = _PyInterpreterState_GET();
@@ -6705,7 +6710,7 @@ _PyObject_MaterializeManagedDict(PyObject *obj)
67056710
goto exit;
67066711
}
67076712
#endif
6708-
dict = materialize_managed_dict_lock_held(obj);
6713+
dict = _PyObject_MaterializeManagedDict_LockHeld(obj);
67096714

67106715
#ifdef Py_GIL_DISABLED
67116716
exit:
@@ -7138,7 +7143,7 @@ PyObject_ClearManagedDict(PyObject *obj)
71387143
int
71397144
_PyDict_DetachFromObject(PyDictObject *mp, PyObject *obj)
71407145
{
7141-
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(obj);
7146+
ASSERT_WORLD_STOPPED_OR_OBJ_LOCKED(obj);
71427147
assert(_PyObject_ManagedDictPointer(obj)->dict == mp);
71437148
assert(_PyObject_InlineValuesConsistencyCheck(obj));
71447149

Objects/typeobject.c

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6378,28 +6378,11 @@ compatible_for_assignment(PyTypeObject* oldto, PyTypeObject* newto, const char*
63786378
return 0;
63796379
}
63806380

6381-
static int
6382-
object_set_class(PyObject *self, PyObject *value, void *closure)
6383-
{
6384-
6385-
if (value == NULL) {
6386-
PyErr_SetString(PyExc_TypeError,
6387-
"can't delete __class__ attribute");
6388-
return -1;
6389-
}
6390-
if (!PyType_Check(value)) {
6391-
PyErr_Format(PyExc_TypeError,
6392-
"__class__ must be set to a class, not '%s' object",
6393-
Py_TYPE(value)->tp_name);
6394-
return -1;
6395-
}
6396-
PyTypeObject *newto = (PyTypeObject *)value;
63976381

6398-
if (PySys_Audit("object.__setattr__", "OsO",
6399-
self, "__class__", value) < 0) {
6400-
return -1;
6401-
}
64026382

6383+
static int
6384+
object_set_class_world_stopped(PyObject *self, PyTypeObject *newto)
6385+
{
64036386
PyTypeObject *oldto = Py_TYPE(self);
64046387

64056388
/* In versions of CPython prior to 3.5, the code in
@@ -6465,49 +6448,74 @@ object_set_class(PyObject *self, PyObject *value, void *closure)
64656448
/* Changing the class will change the implicit dict keys,
64666449
* so we must materialize the dictionary first. */
64676450
if (oldto->tp_flags & Py_TPFLAGS_INLINE_VALUES) {
6468-
PyDictObject *dict = _PyObject_MaterializeManagedDict(self);
6451+
PyDictObject *dict = _PyObject_GetManagedDict(self);
64696452
if (dict == NULL) {
6470-
return -1;
6453+
dict = _PyObject_MaterializeManagedDict_LockHeld(self);
6454+
if (dict == NULL) {
6455+
return -1;
6456+
}
64716457
}
64726458

6473-
bool error = false;
6474-
6475-
Py_BEGIN_CRITICAL_SECTION2(self, dict);
6476-
6477-
// If we raced after materialization and replaced the dict
6478-
// then the materialized dict should no longer have the
6479-
// inline values in which case detach is a nop.
6480-
assert(_PyObject_GetManagedDict(self) == dict ||
6481-
dict->ma_values != _PyObject_InlineValues(self));
6459+
assert(_PyObject_GetManagedDict(self) == dict);
64826460

64836461
if (_PyDict_DetachFromObject(dict, self) < 0) {
6484-
error = true;
6485-
}
6486-
6487-
Py_END_CRITICAL_SECTION2();
6488-
if (error) {
64896462
return -1;
64906463
}
6464+
64916465
}
64926466
if (newto->tp_flags & Py_TPFLAGS_HEAPTYPE) {
64936467
Py_INCREF(newto);
64946468
}
6495-
Py_BEGIN_CRITICAL_SECTION(self);
6496-
// The real Py_TYPE(self) (`oldto`) may have changed from
6497-
// underneath us in another thread, so we re-fetch it here.
6498-
oldto = Py_TYPE(self);
6469+
64996470
Py_SET_TYPE(self, newto);
6500-
Py_END_CRITICAL_SECTION();
6471+
6472+
return 0;
6473+
}
6474+
else {
6475+
return -1;
6476+
}
6477+
}
6478+
6479+
static int
6480+
object_set_class(PyObject *self, PyObject *value, void *closure)
6481+
{
6482+
6483+
if (value == NULL) {
6484+
PyErr_SetString(PyExc_TypeError,
6485+
"can't delete __class__ attribute");
6486+
return -1;
6487+
}
6488+
if (!PyType_Check(value)) {
6489+
PyErr_Format(PyExc_TypeError,
6490+
"__class__ must be set to a class, not '%s' object",
6491+
Py_TYPE(value)->tp_name);
6492+
return -1;
6493+
}
6494+
PyTypeObject *newto = (PyTypeObject *)value;
6495+
6496+
if (PySys_Audit("object.__setattr__", "OsO",
6497+
self, "__class__", value) < 0) {
6498+
return -1;
6499+
}
6500+
6501+
#ifdef Py_GIL_DISABLED
6502+
PyInterpreterState *interp = _PyInterpreterState_GET();
6503+
_PyEval_StopTheWorld(interp);
6504+
#endif
6505+
PyTypeObject *oldto = Py_TYPE(self);
6506+
int res = object_set_class_world_stopped(self, newto);
6507+
#ifdef Py_GIL_DISABLED
6508+
_PyEval_StartTheWorld(interp);
6509+
#endif
6510+
if (res == 0) {
65016511
if (oldto->tp_flags & Py_TPFLAGS_HEAPTYPE) {
65026512
Py_DECREF(oldto);
65036513
}
65046514

65056515
RARE_EVENT_INC(set_class);
65066516
return 0;
65076517
}
6508-
else {
6509-
return -1;
6510-
}
6518+
return res;
65116519
}
65126520

65136521
static PyGetSetDef object_getsets[] = {

0 commit comments

Comments
 (0)