Skip to content

Commit fbc6431

Browse files
committed
pythonGH-119462: Enforce invariants of type versioning (pythonGH-120731)
* Remove uses of Py_TPFLAGS_VALID_VERSION_TAG
1 parent bb5d194 commit fbc6431

File tree

6 files changed

+83
-85
lines changed

6 files changed

+83
-85
lines changed

Doc/c-api/typeobj.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,8 +1328,8 @@ and :c:data:`PyType_Type` effectively act as defaults.)
13281328
To indicate that a class has changed call :c:func:`PyType_Modified`
13291329

13301330
.. warning::
1331-
This flag is present in header files, but is an internal feature and should
1332-
not be used. It will be removed in a future version of CPython
1331+
This flag is present in header files, but is not be used.
1332+
It will be removed in a future version of CPython
13331333

13341334

13351335
.. c:member:: const char* PyTypeObject.tp_doc

Include/object.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,7 @@ given type object has a specified feature.
703703
/* Objects behave like an unbound method */
704704
#define Py_TPFLAGS_METHOD_DESCRIPTOR (1UL << 17)
705705

706-
/* Object has up-to-date type attribute cache */
706+
/* Unused. Legacy flag */
707707
#define Py_TPFLAGS_VALID_VERSION_TAG (1UL << 19)
708708

709709
/* Type is abstract and cannot be instantiated */

Lib/test/test_type_cache.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,21 @@ class C:
9292
new_version = type_get_version(C)
9393
self.assertEqual(new_version, 0)
9494

95+
def test_119462(self):
96+
97+
class Holder:
98+
value = None
99+
100+
@classmethod
101+
def set_value(cls):
102+
cls.value = object()
103+
104+
class HolderSub(Holder):
105+
pass
106+
107+
for _ in range(1050):
108+
Holder.set_value()
109+
HolderSub.value
95110

96111
@support.cpython_only
97112
@requires_specialization
@@ -105,8 +120,10 @@ def _assign_valid_version_or_skip(self, type_):
105120
if type_get_version(type_) == 0:
106121
self.skipTest("Could not assign valid type version")
107122

108-
def _assign_and_check_version_0(self, user_type):
123+
def _no_more_versions(self, user_type):
109124
type_modified(user_type)
125+
for _ in range(1001):
126+
type_assign_specific_version_unsafe(user_type, 1000_000_000)
110127
type_assign_specific_version_unsafe(user_type, 0)
111128
self.assertEqual(type_get_version(user_type), 0)
112129

@@ -135,7 +152,7 @@ def load_foo_1(type_):
135152
self._check_specialization(load_foo_1, A, "LOAD_ATTR", should_specialize=True)
136153
del load_foo_1
137154

138-
self._assign_and_check_version_0(A)
155+
self._no_more_versions(A)
139156

140157
def load_foo_2(type_):
141158
return type_.foo
@@ -186,7 +203,7 @@ def load_x_1(instance):
186203
self._check_specialization(load_x_1, G(), "LOAD_ATTR", should_specialize=True)
187204
del load_x_1
188205

189-
self._assign_and_check_version_0(G)
206+
self._no_more_versions(G)
190207

191208
def load_x_2(instance):
192209
instance.x
@@ -205,7 +222,7 @@ def store_bar_1(type_):
205222
self._check_specialization(store_bar_1, B(), "STORE_ATTR", should_specialize=True)
206223
del store_bar_1
207224

208-
self._assign_and_check_version_0(B)
225+
self._no_more_versions(B)
209226

210227
def store_bar_2(type_):
211228
type_.bar = 10
@@ -225,7 +242,7 @@ def call_class_1(type_):
225242
self._check_specialization(call_class_1, F, "CALL", should_specialize=True)
226243
del call_class_1
227244

228-
self._assign_and_check_version_0(F)
245+
self._no_more_versions(F)
229246

230247
def call_class_2(type_):
231248
type_()
@@ -244,7 +261,7 @@ def to_bool_1(instance):
244261
self._check_specialization(to_bool_1, H(), "TO_BOOL", should_specialize=True)
245262
del to_bool_1
246263

247-
self._assign_and_check_version_0(H)
264+
self._no_more_versions(H)
248265

249266
def to_bool_2(instance):
250267
not instance
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Make sure that invariants of type versioning are maintained:
2+
* Superclasses always have their version number assigned before subclasses
3+
* The version tag is always zero if the tag is not valid.
4+
* The version tag is always non-if the tag is valid.

Modules/pyexpat.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1651,7 +1651,7 @@ PyDoc_STRVAR(pyexpat_module_documentation,
16511651
static int init_handler_descrs(pyexpat_state *state)
16521652
{
16531653
int i;
1654-
assert(!PyType_HasFeature(state->xml_parse_type, Py_TPFLAGS_VALID_VERSION_TAG));
1654+
assert(state->xml_parse_type->tp_version_tag == 0);
16551655
for (i = 0; handler_info[i].name != NULL; i++) {
16561656
struct HandlerInfo *hi = &handler_info[i];
16571657
hi->getset.name = hi->name;

Objects/typeobject.c

Lines changed: 52 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -973,43 +973,37 @@ PyType_Unwatch(int watcher_id, PyObject* obj)
973973
return 0;
974974
}
975975

976-
#ifdef Py_GIL_DISABLED
977-
978976
static void
979-
type_modification_starting_unlocked(PyTypeObject *type)
977+
set_version_unlocked(PyTypeObject *tp, unsigned int version)
980978
{
981979
ASSERT_TYPE_LOCK_HELD();
982-
983-
/* Clear version tags on all types, but leave the valid
984-
version tag intact. This prepares for a modification so that
985-
any concurrent readers of the type cache will not see invalid
986-
values.
987-
*/
988-
if (!_PyType_HasFeature(type, Py_TPFLAGS_VALID_VERSION_TAG)) {
989-
return;
980+
#ifndef Py_GIL_DISABLED
981+
PyInterpreterState *interp = _PyInterpreterState_GET();
982+
// lookup the old version and set to null
983+
if (tp->tp_version_tag != 0) {
984+
PyTypeObject **slot =
985+
interp->types.type_version_cache
986+
+ (tp->tp_version_tag % TYPE_VERSION_CACHE_SIZE);
987+
*slot = NULL;
990988
}
991-
992-
PyObject *subclasses = lookup_tp_subclasses(type);
993-
if (subclasses != NULL) {
994-
assert(PyDict_CheckExact(subclasses));
995-
996-
Py_ssize_t i = 0;
997-
PyObject *ref;
998-
while (PyDict_Next(subclasses, &i, NULL, &ref)) {
999-
PyTypeObject *subclass = type_from_ref(ref);
1000-
if (subclass == NULL) {
1001-
continue;
1002-
}
1003-
type_modification_starting_unlocked(subclass);
1004-
Py_DECREF(subclass);
1005-
}
989+
if (version) {
990+
tp->tp_versions_used++;
991+
}
992+
#else
993+
if (version) {
994+
_Py_atomic_add_uint16(&tp->tp_versions_used, 1);
1006995
}
1007-
1008-
/* 0 is not a valid version tag */
1009-
_Py_atomic_store_uint32_release(&type->tp_version_tag, 0);
1010-
}
1011-
1012996
#endif
997+
FT_ATOMIC_STORE_UINT32_RELAXED(tp->tp_version_tag, version);
998+
#ifndef Py_GIL_DISABLED
999+
if (version != 0) {
1000+
PyTypeObject **slot =
1001+
interp->types.type_version_cache
1002+
+ (version % TYPE_VERSION_CACHE_SIZE);
1003+
*slot = tp;
1004+
}
1005+
#endif
1006+
}
10131007

10141008
static void
10151009
type_modified_unlocked(PyTypeObject *type)
@@ -1020,16 +1014,16 @@ type_modified_unlocked(PyTypeObject *type)
10201014
10211015
Invariants:
10221016
1023-
- before Py_TPFLAGS_VALID_VERSION_TAG can be set on a type,
1017+
- before tp_version_tag can be set on a type,
10241018
it must first be set on all super types.
10251019
1026-
This function clears the Py_TPFLAGS_VALID_VERSION_TAG of a
1020+
This function clears the tp_version_tag of a
10271021
type (so it must first clear it on all subclasses). The
1028-
tp_version_tag value is meaningless unless this flag is set.
1022+
tp_version_tag value is meaningless when equal to zero.
10291023
We don't assign new version tags eagerly, but only as
10301024
needed.
10311025
*/
1032-
if (!_PyType_HasFeature(type, Py_TPFLAGS_VALID_VERSION_TAG)) {
1026+
if (type->tp_version_tag == 0) {
10331027
return;
10341028
}
10351029

@@ -1069,8 +1063,7 @@ type_modified_unlocked(PyTypeObject *type)
10691063
}
10701064
}
10711065

1072-
type->tp_flags &= ~Py_TPFLAGS_VALID_VERSION_TAG;
1073-
FT_ATOMIC_STORE_UINT32_RELAXED(type->tp_version_tag, 0); /* 0 is not a valid version tag */
1066+
set_version_unlocked(type, 0); /* 0 is not a valid version tag */
10741067
if (PyType_HasFeature(type, Py_TPFLAGS_HEAPTYPE)) {
10751068
// This field *must* be invalidated if the type is modified (see the
10761069
// comment on struct _specialization_cache):
@@ -1082,7 +1075,7 @@ void
10821075
PyType_Modified(PyTypeObject *type)
10831076
{
10841077
// Quick check without the lock held
1085-
if (!_PyType_HasFeature(type, Py_TPFLAGS_VALID_VERSION_TAG)) {
1078+
if (type->tp_version_tag == 0) {
10861079
return;
10871080
}
10881081

@@ -1146,8 +1139,7 @@ type_mro_modified(PyTypeObject *type, PyObject *bases) {
11461139

11471140
clear:
11481141
assert(!(type->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN));
1149-
type->tp_flags &= ~Py_TPFLAGS_VALID_VERSION_TAG;
1150-
FT_ATOMIC_STORE_UINT32_RELAXED(type->tp_version_tag, 0); /* 0 is not a valid version tag */
1142+
set_version_unlocked(type, 0); /* 0 is not a valid version tag */
11511143
if (PyType_HasFeature(type, Py_TPFLAGS_HEAPTYPE)) {
11521144
// This field *must* be invalidated if the type is modified (see the
11531145
// comment on struct _specialization_cache):
@@ -1162,12 +1154,11 @@ assign_version_tag(PyInterpreterState *interp, PyTypeObject *type)
11621154
{
11631155
ASSERT_TYPE_LOCK_HELD();
11641156

1165-
/* Ensure that the tp_version_tag is valid and set
1166-
Py_TPFLAGS_VALID_VERSION_TAG. To respect the invariant, this
1167-
must first be done on all super classes. Return 0 if this
1168-
cannot be done, 1 if Py_TPFLAGS_VALID_VERSION_TAG.
1157+
/* Ensure that the tp_version_tag is valid.
1158+
* To respect the invariant, this must first be done on all super classes.
1159+
* Return 0 if this cannot be done, 1 if tp_version_tag is set.
11691160
*/
1170-
if (_PyType_HasFeature(type, Py_TPFLAGS_VALID_VERSION_TAG)) {
1161+
if (type->tp_version_tag != 0) {
11711162
return 1;
11721163
}
11731164
if (!_PyType_HasFeature(type, Py_TPFLAGS_READY)) {
@@ -1176,15 +1167,22 @@ assign_version_tag(PyInterpreterState *interp, PyTypeObject *type)
11761167
if (type->tp_versions_used >= MAX_VERSIONS_PER_CLASS) {
11771168
return 0;
11781169
}
1179-
type->tp_versions_used++;
1170+
1171+
PyObject *bases = lookup_tp_bases(type);
1172+
Py_ssize_t n = PyTuple_GET_SIZE(bases);
1173+
for (Py_ssize_t i = 0; i < n; i++) {
1174+
PyObject *b = PyTuple_GET_ITEM(bases, i);
1175+
if (!assign_version_tag(interp, _PyType_CAST(b))) {
1176+
return 0;
1177+
}
1178+
}
11801179
if (type->tp_flags & Py_TPFLAGS_IMMUTABLETYPE) {
11811180
/* static types */
11821181
if (NEXT_GLOBAL_VERSION_TAG > _Py_MAX_GLOBAL_TYPE_VERSION_TAG) {
11831182
/* We have run out of version numbers */
11841183
return 0;
11851184
}
1186-
FT_ATOMIC_STORE_UINT32_RELAXED(type->tp_version_tag,
1187-
NEXT_GLOBAL_VERSION_TAG++);
1185+
set_version_unlocked(type, NEXT_GLOBAL_VERSION_TAG++);
11881186
assert (type->tp_version_tag <= _Py_MAX_GLOBAL_TYPE_VERSION_TAG);
11891187
}
11901188
else {
@@ -1193,19 +1191,9 @@ assign_version_tag(PyInterpreterState *interp, PyTypeObject *type)
11931191
/* We have run out of version numbers */
11941192
return 0;
11951193
}
1196-
FT_ATOMIC_STORE_UINT32_RELAXED(type->tp_version_tag,
1197-
NEXT_VERSION_TAG(interp)++);
1194+
set_version_unlocked(type, NEXT_VERSION_TAG(interp)++);
11981195
assert (type->tp_version_tag != 0);
11991196
}
1200-
1201-
PyObject *bases = lookup_tp_bases(type);
1202-
Py_ssize_t n = PyTuple_GET_SIZE(bases);
1203-
for (Py_ssize_t i = 0; i < n; i++) {
1204-
PyObject *b = PyTuple_GET_ITEM(bases, i);
1205-
if (!assign_version_tag(interp, _PyType_CAST(b)))
1206-
return 0;
1207-
}
1208-
type->tp_flags |= Py_TPFLAGS_VALID_VERSION_TAG;
12091197
return 1;
12101198
}
12111199

@@ -3126,7 +3114,7 @@ mro_internal_unlocked(PyTypeObject *type, int initial, PyObject **p_old_mro)
31263114
else {
31273115
/* For static builtin types, this is only called during init
31283116
before the method cache has been populated. */
3129-
assert(_PyType_HasFeature(type, Py_TPFLAGS_VALID_VERSION_TAG));
3117+
assert(type->tp_version_tag);
31303118
}
31313119

31323120
if (p_old_mro != NULL)
@@ -5275,7 +5263,7 @@ _PyType_LookupRef(PyTypeObject *type, PyObject *name)
52755263
#else
52765264
if (entry->version == type->tp_version_tag &&
52775265
entry->name == name) {
5278-
assert(_PyType_HasFeature(type, Py_TPFLAGS_VALID_VERSION_TAG));
5266+
assert(type->tp_version_tag);
52795267
OBJECT_STAT_INC_COND(type_cache_hits, !is_dunder_name(name));
52805268
OBJECT_STAT_INC_COND(type_cache_dunder_hits, is_dunder_name(name));
52815269
Py_XINCREF(entry->value);
@@ -5298,7 +5286,6 @@ _PyType_LookupRef(PyTypeObject *type, PyObject *name)
52985286
if (MCACHE_CACHEABLE_NAME(name)) {
52995287
has_version = assign_version_tag(interp, type);
53005288
version = type->tp_version_tag;
5301-
assert(!has_version || _PyType_HasFeature(type, Py_TPFLAGS_VALID_VERSION_TAG));
53025289
}
53035290
END_TYPE_LOCK()
53045291

@@ -5582,23 +5569,15 @@ type_setattro(PyObject *self, PyObject *name, PyObject *value)
55825569
return -1;
55835570
}
55845571

5585-
#ifdef Py_GIL_DISABLED
5586-
// In free-threaded builds readers can race with the lock-free portion
5587-
// of the type cache and the assignment into the dict. We clear all of the
5588-
// versions initially so no readers will succeed in the lock-free case.
5589-
// They'll then block on the type lock until the update below is done.
5590-
type_modification_starting_unlocked(type);
5591-
#endif
5592-
5593-
res = _PyDict_SetItem_LockHeld((PyDictObject *)dict, name, value);
5594-
55955572
/* Clear the VALID_VERSION flag of 'type' and all its
55965573
subclasses. This could possibly be unified with the
55975574
update_subclasses() recursion in update_slot(), but carefully:
55985575
they each have their own conditions on which to stop
55995576
recursing into subclasses. */
56005577
type_modified_unlocked(type);
56015578

5579+
res = _PyDict_SetItem_LockHeld((PyDictObject *)dict, name, value);
5580+
56025581
if (res == 0) {
56035582
if (is_dunder_name(name)) {
56045583
res = update_slot(type, name);
@@ -5710,7 +5689,6 @@ fini_static_type(PyInterpreterState *interp, PyTypeObject *type,
57105689

57115690
if (final) {
57125691
type->tp_flags &= ~Py_TPFLAGS_READY;
5713-
type->tp_flags &= ~Py_TPFLAGS_VALID_VERSION_TAG;
57145692
type->tp_version_tag = 0;
57155693
}
57165694

@@ -8329,12 +8307,11 @@ init_static_type(PyInterpreterState *interp, PyTypeObject *self,
83298307

83308308
assert(NEXT_GLOBAL_VERSION_TAG <= _Py_MAX_GLOBAL_TYPE_VERSION_TAG);
83318309
self->tp_version_tag = NEXT_GLOBAL_VERSION_TAG++;
8332-
self->tp_flags |= Py_TPFLAGS_VALID_VERSION_TAG;
83338310
}
83348311
else {
83358312
assert(!initial);
83368313
assert(self->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN);
8337-
assert(self->tp_flags & Py_TPFLAGS_VALID_VERSION_TAG);
8314+
assert(self->tp_version_tag != 0);
83388315
}
83398316

83408317
managed_static_type_state_init(interp, self, isbuiltin, initial);

0 commit comments

Comments
 (0)