]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-127773: Disable attribute cache on incompatible MRO entries (GH-127924)
authorPetr Viktorin <encukou@gmail.com>
Mon, 13 Jan 2025 13:10:41 +0000 (14:10 +0100)
committerGitHub <noreply@github.com>
Mon, 13 Jan 2025 13:10:41 +0000 (14:10 +0100)
Include/cpython/object.h
Lib/test/test_metaclass.py
Misc/NEWS.d/next/Core_and_Builtins/2024-12-13-15-21-45.gh-issue-127773.E-DZR4.rst [new file with mode: 0644]
Objects/typeobject.c

index e4797029da431e55d6996297f8b1385a327c9be5..c8c6bc97fa32ee569fe46ab185830c8e5f53504b 100644 (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. */
index b37b7defe84d1cc09e2f4b0aeafff46c7a0e5804..07a333f98fa0a9393c785acc15ae798adffedb12 100644 (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
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-12-13-15-21-45.gh-issue-127773.E-DZR4.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-12-13-15-21-45.gh-issue-127773.E-DZR4.rst
new file mode 100644 (file)
index 0000000..7e68b3f
--- /dev/null
@@ -0,0 +1 @@
+Do not use the type attribute cache for types with incompatible :term:`MRO`.
index 0f5ebc6f90773d8f80856f143ac9084ca16a3297..d8f5f6d9cb2366d3711afc9f35471d6a6ea8de57 100644 (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;
     }