gh-127773: Disable attribute cache on incompatible MRO entries (GH-127924)

This commit is contained in:
Petr Viktorin 2025-01-13 14:10:41 +01:00 committed by GitHub
parent 76ffaef729
commit aa6579cb60
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 50 additions and 2 deletions

View file

@ -221,7 +221,9 @@ struct _typeobject {
PyObject *tp_weaklist; /* not used for static builtin types */
destructor tp_del;
/* Type attribute cache version tag. Added in version 2.6 */
/* Type attribute cache version tag. Added in version 2.6.
* If zero, the cache is invalid and must be initialized.
*/
unsigned int tp_version_tag;
destructor tp_finalize;
@ -229,9 +231,17 @@ struct _typeobject {
/* bitset of which type-watchers care about this type */
unsigned char tp_watched;
/* Number of tp_version_tag values used.
* Set to _Py_ATTR_CACHE_UNUSED if the attribute cache is
* disabled for this type (e.g. due to custom MRO entries).
* Otherwise, limited to MAX_VERSIONS_PER_CLASS (defined elsewhere).
*/
uint16_t tp_versions_used;
};
#define _Py_ATTR_CACHE_UNUSED (30000) // (see tp_versions_used)
/* This struct is used by the specializer
* It should be treated as an opaque blob
* by code other than the specializer and interpreter. */

View file

@ -254,6 +254,33 @@ Test failures in looking up the __prepare__ method work.
[...]
test.test_metaclass.ObscureException
Test setting attributes with a non-base type in mro() (gh-127773).
>>> class Base:
... value = 1
...
>>> class Meta(type):
... def mro(cls):
... return (cls, Base, object)
...
>>> class WeirdClass(metaclass=Meta):
... pass
...
>>> Base.value
1
>>> WeirdClass.value
1
>>> Base.value = 2
>>> Base.value
2
>>> WeirdClass.value
2
>>> Base.value = 3
>>> Base.value
3
>>> WeirdClass.value
3
"""
import sys

View file

@ -0,0 +1 @@
Do not use the type attribute cache for types with incompatible :term:`MRO`.

View file

@ -992,6 +992,7 @@ static void
set_version_unlocked(PyTypeObject *tp, unsigned int version)
{
ASSERT_TYPE_LOCK_HELD();
assert(version == 0 || (tp->tp_versions_used != _Py_ATTR_CACHE_UNUSED));
#ifndef Py_GIL_DISABLED
PyInterpreterState *interp = _PyInterpreterState_GET();
// lookup the old version and set to null
@ -1148,6 +1149,10 @@ type_mro_modified(PyTypeObject *type, PyObject *bases) {
PyObject *b = PyTuple_GET_ITEM(bases, i);
PyTypeObject *cls = _PyType_CAST(b);
if (cls->tp_versions_used >= _Py_ATTR_CACHE_UNUSED) {
goto clear;
}
if (!is_subtype_with_mro(lookup_tp_mro(type), type, cls)) {
goto clear;
}
@ -1156,7 +1161,8 @@ type_mro_modified(PyTypeObject *type, PyObject *bases) {
clear:
assert(!(type->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN));
set_version_unlocked(type, 0); /* 0 is not a valid version tag */
set_version_unlocked(type, 0); /* 0 is not a valid version tag */
type->tp_versions_used = _Py_ATTR_CACHE_UNUSED;
if (PyType_HasFeature(type, Py_TPFLAGS_HEAPTYPE)) {
// This field *must* be invalidated if the type is modified (see the
// comment on struct _specialization_cache):
@ -1208,6 +1214,9 @@ _PyType_GetVersionForCurrentState(PyTypeObject *tp)
#define MAX_VERSIONS_PER_CLASS 1000
#if _Py_ATTR_CACHE_UNUSED < MAX_VERSIONS_PER_CLASS
#error "_Py_ATTR_CACHE_UNUSED must be bigger than max"
#endif
static int
assign_version_tag(PyInterpreterState *interp, PyTypeObject *type)
@ -1225,6 +1234,7 @@ assign_version_tag(PyInterpreterState *interp, PyTypeObject *type)
return 0;
}
if (type->tp_versions_used >= MAX_VERSIONS_PER_CLASS) {
/* (this includes `tp_versions_used == _Py_ATTR_CACHE_UNUSED`) */
return 0;
}