From f7bf5f8e1bf80b28cfe1ccd46f512a77939354d5 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Tue, 24 Jun 2025 01:38:23 +0500 Subject: [PATCH 01/14] Py_TPFLAGS_MANAGED_WEAKREF implies Py_TPFLAGS_HAVE_GC too and force checking of it presence --- Doc/c-api/typeobj.rst | 2 ++ Doc/extending/newtypes.rst | 2 ++ Doc/whatsnew/3.15.rst | 8 ++++++++ Include/object.h | 2 +- Objects/typeobject.c | 14 ++++++++++++++ 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Doc/c-api/typeobj.rst b/Doc/c-api/typeobj.rst index af2bead3bb5004..979b29917cc01c 100644 --- a/Doc/c-api/typeobj.rst +++ b/Doc/c-api/typeobj.rst @@ -1279,6 +1279,8 @@ and :c:data:`PyType_Type` effectively act as defaults.) This bit indicates that instances of the class should be weakly referenceable. + If this flag is set, :c:macro:`Py_TPFLAGS_HAVE_GC` should also be set. + .. versionadded:: 3.12 **Inheritance:** diff --git a/Doc/extending/newtypes.rst b/Doc/extending/newtypes.rst index e3612f3a1875ca..26085b5cebd3ad 100644 --- a/Doc/extending/newtypes.rst +++ b/Doc/extending/newtypes.rst @@ -560,6 +560,8 @@ For an object to be weakly referenceable, the extension type must set the field. The legacy :c:member:`~PyTypeObject.tp_weaklistoffset` field should be left as zero. +If this flag is set, :c:macro:`Py_TPFLAGS_HAVE_GC` should also be set. + Concretely, here is how the statically declared type object would look:: static PyTypeObject TrivialType = { diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 9f327cf904da1b..a4027c6e9e69f9 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -310,6 +310,14 @@ New features (Contributed by Petr Viktorin in :gh:`131510`) +Limited C API changes +--------------------- + +* If the :c:macro:`Py_TPFLAGS_MANAGED_DICT` and :c:macro:`Py_TPFLAGS_MANAGED_WEAKREF` + flags are set then :c:macro:`Py_TPFLAGS_HAVE_GC` should be set too. + (Contributed by Sergey Miryanov.) + + Porting to Python 3.15 ---------------------- diff --git a/Include/object.h b/Include/object.h index c75e9db0cbd935..5627377204238f 100644 --- a/Include/object.h +++ b/Include/object.h @@ -525,7 +525,7 @@ given type object has a specified feature. #define Py_TPFLAGS_INLINE_VALUES (1 << 2) /* Placement of weakref pointers are managed by the VM, not by the type. - * The VM will automatically set tp_weaklistoffset. + * The VM will automatically set tp_weaklistoffset. Implies Py_TPFLAGS_HAVE_GC. */ #define Py_TPFLAGS_MANAGED_WEAKREF (1 << 3) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index b9d549610693c1..4caea0d1620fa8 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -8804,6 +8804,13 @@ type_ready_preheader(PyTypeObject *type) type->tp_name); return -1; } + if (!(type->tp_flags & Py_TPFLAGS_HAVE_GC)) { + PyErr_Format(PyExc_SystemError, + "type %s has the Py_TPFLAGS_MANAGED_DICT flag " + "but not Py_TPFLAGS_HAVE_GC flag", + type->tp_name); + return -1; + } type->tp_dictoffset = -1; } if (type->tp_flags & Py_TPFLAGS_MANAGED_WEAKREF) { @@ -8816,6 +8823,13 @@ type_ready_preheader(PyTypeObject *type) type->tp_name); return -1; } + if (!(type->tp_flags & Py_TPFLAGS_HAVE_GC)) { + PyErr_Format(PyExc_SystemError, + "type %s has the Py_TPFLAGS_MANAGED_WEAKREF flag " + "but not Py_TPFLAGS_HAVE_GC flag", + type->tp_name); + return -1; + } type->tp_weaklistoffset = MANAGED_WEAKREF_OFFSET; } return 0; From dcca4658684aa5e7ca583d37308e156da060a42f Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Tue, 24 Jun 2025 13:13:57 +0500 Subject: [PATCH 02/14] Add news --- .../2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst new file mode 100644 index 00000000000000..87523ad893b89d --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst @@ -0,0 +1,3 @@ +Force to use :c:macro:`Py_TPFLAGS_HAVE_GC` if +:c:macro:`Py_TPFLAGS_MANAGED_DICT` or :c:macro:`Py_TPFLAGS_MANAGED_WEAKREF` +used. From 9e83c06465d86319e68b156ceb0dcd5e0324006b Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Tue, 24 Jun 2025 13:14:06 +0500 Subject: [PATCH 03/14] Update whatsnew --- Doc/whatsnew/3.15.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index a4027c6e9e69f9..e106aa5a2ac5d7 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -315,7 +315,7 @@ Limited C API changes * If the :c:macro:`Py_TPFLAGS_MANAGED_DICT` and :c:macro:`Py_TPFLAGS_MANAGED_WEAKREF` flags are set then :c:macro:`Py_TPFLAGS_HAVE_GC` should be set too. - (Contributed by Sergey Miryanov.) + (Contributed by Sergey Miryanov in :gh:`134786`) Porting to Python 3.15 From 42c09320e6a8581ff46d51b9b8d42dd7235298ee Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Thu, 25 Sep 2025 23:54:41 +0500 Subject: [PATCH 04/14] Add support of weakrefs without gc --- Include/internal/pycore_object.h | 1 + Lib/test/test_capi/test_type.py | 9 ++++++ Modules/_testcapimodule.c | 41 ++++++++++++++++++++++++++ Objects/typeobject.c | 49 +++++++++++++++++++++----------- 4 files changed, 84 insertions(+), 16 deletions(-) diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h index 40f8ca68c00b72..96f65ab4b804b7 100644 --- a/Include/internal/pycore_object.h +++ b/Include/internal/pycore_object.h @@ -939,6 +939,7 @@ extern int _PyType_CacheInitForSpecialization(PyHeapTypeObject *type, #else # define MANAGED_DICT_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-3) # define MANAGED_WEAKREF_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-4) +# define MANAGED_WEAKREF_OFFSET_NO_GC (((Py_ssize_t)sizeof(PyObject *))*-2) #endif typedef union { diff --git a/Lib/test/test_capi/test_type.py b/Lib/test/test_capi/test_type.py index 15fb4a93e2ad74..c3583cc4c84bed 100644 --- a/Lib/test/test_capi/test_type.py +++ b/Lib/test/test_capi/test_type.py @@ -1,5 +1,6 @@ from test.support import import_helper, Py_GIL_DISABLED, refleak_helper import unittest +import weakref _testcapi = import_helper.import_module('_testcapi') @@ -274,3 +275,11 @@ def test_extension_managed_dict_type(self): obj.__dict__ = {'bar': 3} self.assertEqual(obj.__dict__, {'bar': 3}) self.assertEqual(obj.bar, 3) + + def test_type_have_weakref_and_no_gc(self): + ManagedWeakrefNoGCType = _testcapi.ManagedWeakrefNoGCType + obj = ManagedWeakrefNoGCType() + wr = weakref.ref(obj) + + del obj # shouldn't segfault + del wr diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 508ef55511e49d..43347f58f0a11c 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -3227,12 +3227,44 @@ static PyType_Spec ManagedDict_spec = { ManagedDict_slots }; +typedef struct { + PyObject_HEAD +} ManagedWeakrefNoGCObject; + +static void +ManagedWeakrefNoGC_dealloc(PyObject *self) +{ + PyObject_ClearWeakRefs(self); + PyTypeObject *tp = Py_TYPE(self); + tp->tp_free(self); + Py_DECREF(tp); +} + +static PyType_Slot ManagedWeakrefNoGC_slots[] = { + {Py_tp_dealloc, ManagedWeakrefNoGC_dealloc}, + {0, 0} +}; + +static PyType_Spec ManagedWeakrefNoGC_spec = { + .name = "_testcapi.ManagedWeakrefNoGCType", + .basicsize = sizeof(ManagedWeakrefNoGCObject), + .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_MANAGED_WEAKREF), + .slots = ManagedWeakrefNoGC_slots, +}; + + static PyObject * create_managed_dict_type(void) { return PyType_FromSpec(&ManagedDict_spec); } +static PyObject * +create_managed_weakref_no_gc_type(void) +{ + return PyType_FromSpec(&ManagedWeakrefNoGC_spec); +} + static int _testcapi_exec(PyObject *m) { @@ -3362,6 +3394,15 @@ _testcapi_exec(PyObject *m) return -1; } + PyObject *managed_weakref_no_gc_type = create_managed_weakref_no_gc_type(); + if (managed_weakref_no_gc_type == NULL) { + return -1; + } + if (PyModule_Add(m, "ManagedWeakrefNoGCType", managed_weakref_no_gc_type) < 0) { + return -1; + } + + /* Include tests from the _testcapi/ directory */ if (_PyTestCapi_Init_Vectorcall(m) < 0) { return -1; diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 9e1b428422f0dc..8c3ef9d879b50b 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -4655,7 +4655,12 @@ type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type, PyObject *dict if (ctx->add_weak) { assert((type->tp_flags & Py_TPFLAGS_MANAGED_WEAKREF) == 0); type_add_flags(type, Py_TPFLAGS_MANAGED_WEAKREF); - type->tp_weaklistoffset = MANAGED_WEAKREF_OFFSET; + if (_PyType_IS_GC(type)) { + type->tp_weaklistoffset = MANAGED_WEAKREF_OFFSET; + } + else { + type->tp_weaklistoffset = MANAGED_WEAKREF_OFFSET_NO_GC; + } } if (ctx->add_dict) { assert((type->tp_flags & Py_TPFLAGS_MANAGED_DICT) == 0); @@ -8500,6 +8505,13 @@ overrides_hash(PyTypeObject *type) return r; } +void +PyObject_NoGC_Preheader_Del(void *op) +{ + size_t presize = _PyType_PreHeaderSize(Py_TYPE(op)); + PyObject_Free(((char *)op) - presize); +} + static int inherit_slots(PyTypeObject *type, PyTypeObject *base) { @@ -8677,7 +8689,21 @@ inherit_slots(PyTypeObject *type, PyTypeObject *base) if ((type->tp_flags & Py_TPFLAGS_HAVE_GC) == (base->tp_flags & Py_TPFLAGS_HAVE_GC)) { /* They agree about gc. */ - COPYSLOT(tp_free); + + if ((type->tp_flags & Py_TPFLAGS_PREHEADER) && + type->tp_free == NULL && + base->tp_free == PyObject_Free) { + /* Because type has preheader fields, its + * objects will be allocated with those fields + * and it should be take in account when object + * is freed, so we use special tp_free. + */ + type->tp_free = PyObject_NoGC_Preheader_Del; + } + else { + COPYSLOT(tp_free); + } + } else if ((type->tp_flags & Py_TPFLAGS_HAVE_GC) && type->tp_free == NULL && @@ -8893,13 +8919,6 @@ type_ready_preheader(PyTypeObject *type) type->tp_name); return -1; } - if (!(type->tp_flags & Py_TPFLAGS_HAVE_GC)) { - PyErr_Format(PyExc_SystemError, - "type %s has the Py_TPFLAGS_MANAGED_DICT flag " - "but not Py_TPFLAGS_HAVE_GC flag", - type->tp_name); - return -1; - } type->tp_dictoffset = -1; } if (type->tp_flags & Py_TPFLAGS_MANAGED_WEAKREF) { @@ -8912,14 +8931,12 @@ type_ready_preheader(PyTypeObject *type) type->tp_name); return -1; } - if (!(type->tp_flags & Py_TPFLAGS_HAVE_GC)) { - PyErr_Format(PyExc_SystemError, - "type %s has the Py_TPFLAGS_MANAGED_WEAKREF flag " - "but not Py_TPFLAGS_HAVE_GC flag", - type->tp_name); - return -1; + if (_PyType_IS_GC(type)) { + type->tp_weaklistoffset = MANAGED_WEAKREF_OFFSET; + } + else { + type->tp_weaklistoffset = MANAGED_WEAKREF_OFFSET_NO_GC; } - type->tp_weaklistoffset = MANAGED_WEAKREF_OFFSET; } return 0; } From 10fce207590e1463b34cb83933fe1196293c8e29 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Thu, 25 Sep 2025 23:57:23 +0500 Subject: [PATCH 05/14] Make FT happy --- Include/internal/pycore_object.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h index 96f65ab4b804b7..04704f6799b0d8 100644 --- a/Include/internal/pycore_object.h +++ b/Include/internal/pycore_object.h @@ -936,6 +936,7 @@ extern int _PyType_CacheInitForSpecialization(PyHeapTypeObject *type, #ifdef Py_GIL_DISABLED # define MANAGED_DICT_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-1) # define MANAGED_WEAKREF_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-2) +# define MANAGED_WEAKREF_OFFSET_NO_GC (((Py_ssize_t)sizeof(PyObject *))*-2) #else # define MANAGED_DICT_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-3) # define MANAGED_WEAKREF_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-4) From 6dbabaf170128c0b9fbff6aa19b55aaf38bd6df8 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Fri, 26 Sep 2025 00:32:42 +0500 Subject: [PATCH 06/14] Update docs according review's feedback --- Doc/c-api/typeobj.rst | 4 ++-- Doc/whatsnew/3.15.rst | 7 ------- Include/object.h | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/Doc/c-api/typeobj.rst b/Doc/c-api/typeobj.rst index 716e2ae00dc82e..e9609dfaab59f3 100644 --- a/Doc/c-api/typeobj.rst +++ b/Doc/c-api/typeobj.rst @@ -1260,7 +1260,7 @@ and :c:data:`PyType_Type` effectively act as defaults.) This bit indicates that instances of the class have a :attr:`~object.__dict__` attribute, and that the space for the dictionary is managed by the VM. - If this flag is set, :c:macro:`Py_TPFLAGS_HAVE_GC` should also be set. + If this flag is set, :c:macro:`Py_TPFLAGS_HAVE_GC` must also be set. The type traverse function must call :c:func:`PyObject_VisitManagedDict` and its clear function must call :c:func:`PyObject_ClearManagedDict`. @@ -1278,7 +1278,7 @@ and :c:data:`PyType_Type` effectively act as defaults.) This bit indicates that instances of the class should be weakly referenceable. - If this flag is set, :c:macro:`Py_TPFLAGS_HAVE_GC` should also be set. + If this flag is set, :c:macro:`Py_TPFLAGS_HAVE_GC` must also be set. .. versionadded:: 3.12 diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index f3274be42f3ba7..b199761701b9c0 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -785,13 +785,6 @@ New features (Contributed by Victor Stinner in :gh:`129813`.) -Limited C API changes ---------------------- - -* If the :c:macro:`Py_TPFLAGS_MANAGED_DICT` and :c:macro:`Py_TPFLAGS_MANAGED_WEAKREF` - flags are set then :c:macro:`Py_TPFLAGS_HAVE_GC` should be set too. - (Contributed by Sergey Miryanov in :gh:`134786`) - Porting to Python 3.15 ---------------------- diff --git a/Include/object.h b/Include/object.h index 117d057a18178f..9585f4a1d67a52 100644 --- a/Include/object.h +++ b/Include/object.h @@ -529,7 +529,7 @@ given type object has a specified feature. #define Py_TPFLAGS_INLINE_VALUES (1 << 2) /* Placement of weakref pointers are managed by the VM, not by the type. - * The VM will automatically set tp_weaklistoffset. Implies Py_TPFLAGS_HAVE_GC. + * The VM will automatically set tp_weaklistoffset. */ #define Py_TPFLAGS_MANAGED_WEAKREF (1 << 3) From 7da03e2b4501774f50edf8f1026c5a8378987f40 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Fri, 26 Sep 2025 00:33:20 +0500 Subject: [PATCH 07/14] Support Py_TPFLAGS_MANAGED_DICT without gc --- Include/internal/pycore_object.h | 11 ++++++++-- Lib/test/test_capi/test_type.py | 7 +++++- Modules/_testcapimodule.c | 37 ++++++++++++++++++++++++++++++++ Python/bytecodes.c | 8 ++++++- Python/executor_cases.c.h | 7 +++++- Python/generated_cases.c.h | 7 +++++- Python/specialize.c | 7 +++++- 7 files changed, 77 insertions(+), 7 deletions(-) diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h index 04704f6799b0d8..7debf6d9704e5d 100644 --- a/Include/internal/pycore_object.h +++ b/Include/internal/pycore_object.h @@ -936,10 +936,12 @@ extern int _PyType_CacheInitForSpecialization(PyHeapTypeObject *type, #ifdef Py_GIL_DISABLED # define MANAGED_DICT_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-1) # define MANAGED_WEAKREF_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-2) +# define MANAGED_DICT_OFFSET_NO_GC (((Py_ssize_t)sizeof(PyObject *))*-1) # define MANAGED_WEAKREF_OFFSET_NO_GC (((Py_ssize_t)sizeof(PyObject *))*-2) #else # define MANAGED_DICT_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-3) # define MANAGED_WEAKREF_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-4) +# define MANAGED_DICT_OFFSET_NO_GC (((Py_ssize_t)sizeof(PyObject *))*-1) # define MANAGED_WEAKREF_OFFSET_NO_GC (((Py_ssize_t)sizeof(PyObject *))*-2) #endif @@ -950,8 +952,13 @@ typedef union { static inline PyManagedDictPointer * _PyObject_ManagedDictPointer(PyObject *obj) { - assert(Py_TYPE(obj)->tp_flags & Py_TPFLAGS_MANAGED_DICT); - return (PyManagedDictPointer *)((char *)obj + MANAGED_DICT_OFFSET); + PyTypeObject *type = Py_TYPE(obj); + assert(type->tp_flags & Py_TPFLAGS_MANAGED_DICT); + Py_ssize_t offset = MANAGED_DICT_OFFSET; + if (!_PyType_IS_GC(type)) { + offset = MANAGED_DICT_OFFSET_NO_GC; + } + return (PyManagedDictPointer *)((char *)obj + offset); } static inline PyDictObject * diff --git a/Lib/test/test_capi/test_type.py b/Lib/test/test_capi/test_type.py index c3583cc4c84bed..a1adff3b5ca1b1 100644 --- a/Lib/test/test_capi/test_type.py +++ b/Lib/test/test_capi/test_type.py @@ -276,10 +276,15 @@ def test_extension_managed_dict_type(self): self.assertEqual(obj.__dict__, {'bar': 3}) self.assertEqual(obj.bar, 3) - def test_type_have_weakref_and_no_gc(self): + def test_type_have_managed_weakref_and_no_gc(self): ManagedWeakrefNoGCType = _testcapi.ManagedWeakrefNoGCType obj = ManagedWeakrefNoGCType() wr = weakref.ref(obj) del obj # shouldn't segfault del wr + + def test_type_have_managed_dict_and_no_gc(self): + ManagedDictNoGCType = _testcapi.ManagedDictNoGCType + obj = ManagedDictNoGCType() + del obj # shouldn't segfault diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 43347f58f0a11c..2a103551747ffc 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -3252,6 +3252,30 @@ static PyType_Spec ManagedWeakrefNoGC_spec = { .slots = ManagedWeakrefNoGC_slots, }; +typedef struct { + PyObject_HEAD +} ManagedDictNoGCObject; + +static void +ManagedDictNoGC_dealloc(PyObject *self) +{ + PyTypeObject *tp = Py_TYPE(self); + tp->tp_free(self); + Py_DECREF(tp); +} + +static PyType_Slot ManagedDictNoGC_slots[] = { + {Py_tp_dealloc, ManagedDictNoGC_dealloc}, + {0, 0} +}; + +static PyType_Spec ManagedDictNoGC_spec = { + .name = "_testcapi.ManagedDictNoGCType", + .basicsize = sizeof(ManagedDictNoGCObject), + .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_MANAGED_DICT), + .slots = ManagedDictNoGC_slots, +}; + static PyObject * create_managed_dict_type(void) @@ -3265,6 +3289,12 @@ create_managed_weakref_no_gc_type(void) return PyType_FromSpec(&ManagedWeakrefNoGC_spec); } +static PyObject * +create_managed_dict_no_gc_type(void) +{ + return PyType_FromSpec(&ManagedDictNoGC_spec); +} + static int _testcapi_exec(PyObject *m) { @@ -3402,6 +3432,13 @@ _testcapi_exec(PyObject *m) return -1; } + PyObject *managed_dict_no_gc_type = create_managed_dict_no_gc_type(); + if (managed_dict_no_gc_type == NULL) { + return -1; + } + if (PyModule_Add(m, "ManagedDictNoGCType", managed_dict_no_gc_type) < 0) { + return -1; + } /* Include tests from the _testcapi/ directory */ if (_PyTestCapi_Init_Vectorcall(m) < 0) { diff --git a/Python/bytecodes.c b/Python/bytecodes.c index f9f14322df0a5e..3dc368ad07e76a 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -3638,7 +3638,13 @@ dummy_func( _LOAD_ATTR_NONDESCRIPTOR_NO_DICT; op(_CHECK_ATTR_METHOD_LAZY_DICT, (dictoffset/1, owner -- owner)) { - char *ptr = ((char *)PyStackRef_AsPyObjectBorrow(owner)) + MANAGED_DICT_OFFSET + dictoffset; + PyObject *borrowed = PyStackRef_AsPyObjectBorrow(owner); + Py_ssize_t offset = MANAGED_DICT_OFFSET; + if (!PyType_IS_GC(Py_TYPE(borrowed))) { + offset = MANAGED_DICT_OFFSET_NO_GC; + } + + char *ptr = ((char *)borrowed) + offset + dictoffset; PyObject *dict = FT_ATOMIC_LOAD_PTR_ACQUIRE(*(PyObject **)ptr); /* This object has a __dict__, just not yet created */ DEOPT_IF(dict != NULL); diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 0e4d86463761a0..7692ada5305200 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -4832,7 +4832,12 @@ _PyStackRef owner; owner = stack_pointer[-1]; uint16_t dictoffset = (uint16_t)CURRENT_OPERAND0(); - char *ptr = ((char *)PyStackRef_AsPyObjectBorrow(owner)) + MANAGED_DICT_OFFSET + dictoffset; + PyObject *borrowed = PyStackRef_AsPyObjectBorrow(owner); + Py_ssize_t offset = MANAGED_DICT_OFFSET; + if (!PyType_IS_GC(Py_TYPE(borrowed))) { + offset = MANAGED_DICT_OFFSET_NO_GC; + } + char *ptr = ((char *)borrowed) + offset + dictoffset; PyObject *dict = FT_ATOMIC_LOAD_PTR_ACQUIRE(*(PyObject **)ptr); if (dict != NULL) { UOP_STAT_INC(uopcode, miss); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 79328a7b725613..a1c9347a0c417b 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -8175,7 +8175,12 @@ // _CHECK_ATTR_METHOD_LAZY_DICT { uint16_t dictoffset = read_u16(&this_instr[4].cache); - char *ptr = ((char *)PyStackRef_AsPyObjectBorrow(owner)) + MANAGED_DICT_OFFSET + dictoffset; + PyObject *borrowed = PyStackRef_AsPyObjectBorrow(owner); + Py_ssize_t offset = MANAGED_DICT_OFFSET; + if (!PyType_IS_GC(Py_TYPE(borrowed))) { + offset = MANAGED_DICT_OFFSET_NO_GC; + } + char *ptr = ((char *)borrowed) + offset + dictoffset; PyObject *dict = FT_ATOMIC_LOAD_PTR_ACQUIRE(*(PyObject **)ptr); if (dict != NULL) { UPDATE_MISS_STATS(LOAD_ATTR); diff --git a/Python/specialize.c b/Python/specialize.c index 47f7b27b4908fd..96576eb6c971f8 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -1626,7 +1626,12 @@ specialize_attr_loadclassattr(PyObject *owner, _Py_CODEUNIT *instr, else { Py_ssize_t dictoffset; if (tp_flags & Py_TPFLAGS_MANAGED_DICT) { - dictoffset = MANAGED_DICT_OFFSET; + if (tp_flags & Py_TPFLAGS_HAVE_GC) { + dictoffset = MANAGED_DICT_OFFSET; + } + else { + dictoffset = MANAGED_DICT_OFFSET_NO_GC; + } } else { dictoffset = owner_cls->tp_dictoffset; From f195b04c081717b04705e1eb9fbdf138d39735ab Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Fri, 26 Sep 2025 00:33:31 +0500 Subject: [PATCH 08/14] Update news --- .../2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst index 87523ad893b89d..8d2d9df6b385a8 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst @@ -1,3 +1,3 @@ -Force to use :c:macro:`Py_TPFLAGS_HAVE_GC` if -:c:macro:`Py_TPFLAGS_MANAGED_DICT` or :c:macro:`Py_TPFLAGS_MANAGED_WEAKREF` -used. +Fix segmentation fault when deallocating object of the type with +:c:macro:`Py_TPFLAGS_MANAGED_WEAKREF` or :c:macro:`Py_TPFLAGS_MANAGED_DICT` +enabled but without :c:macro:`Py_TPFLAGS_HAVE_GC`. From f71062703e329e710d247626bcfbe6b50accdc2b Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Fri, 26 Sep 2025 18:34:12 +0500 Subject: [PATCH 09/14] Revert last changes --- Doc/c-api/typeobj.rst | 4 +- Doc/whatsnew/3.15.rst | 7 ++ Include/internal/pycore_object.h | 13 +--- Include/object.h | 2 +- Lib/test/test_capi/test_type.py | 14 ---- ...-06-24-13-12-58.gh-issue-134786.MF0VVk.rst | 6 +- Modules/_testcapimodule.c | 78 ------------------- Objects/typeobject.c | 49 ++++-------- Python/bytecodes.c | 8 +- Python/executor_cases.c.h | 7 +- Python/generated_cases.c.h | 7 +- Python/specialize.c | 7 +- 12 files changed, 35 insertions(+), 167 deletions(-) diff --git a/Doc/c-api/typeobj.rst b/Doc/c-api/typeobj.rst index e9609dfaab59f3..716e2ae00dc82e 100644 --- a/Doc/c-api/typeobj.rst +++ b/Doc/c-api/typeobj.rst @@ -1260,7 +1260,7 @@ and :c:data:`PyType_Type` effectively act as defaults.) This bit indicates that instances of the class have a :attr:`~object.__dict__` attribute, and that the space for the dictionary is managed by the VM. - If this flag is set, :c:macro:`Py_TPFLAGS_HAVE_GC` must also be set. + If this flag is set, :c:macro:`Py_TPFLAGS_HAVE_GC` should also be set. The type traverse function must call :c:func:`PyObject_VisitManagedDict` and its clear function must call :c:func:`PyObject_ClearManagedDict`. @@ -1278,7 +1278,7 @@ and :c:data:`PyType_Type` effectively act as defaults.) This bit indicates that instances of the class should be weakly referenceable. - If this flag is set, :c:macro:`Py_TPFLAGS_HAVE_GC` must also be set. + If this flag is set, :c:macro:`Py_TPFLAGS_HAVE_GC` should also be set. .. versionadded:: 3.12 diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index b199761701b9c0..f3274be42f3ba7 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -785,6 +785,13 @@ New features (Contributed by Victor Stinner in :gh:`129813`.) +Limited C API changes +--------------------- + +* If the :c:macro:`Py_TPFLAGS_MANAGED_DICT` and :c:macro:`Py_TPFLAGS_MANAGED_WEAKREF` + flags are set then :c:macro:`Py_TPFLAGS_HAVE_GC` should be set too. + (Contributed by Sergey Miryanov in :gh:`134786`) + Porting to Python 3.15 ---------------------- diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h index 7debf6d9704e5d..40f8ca68c00b72 100644 --- a/Include/internal/pycore_object.h +++ b/Include/internal/pycore_object.h @@ -936,13 +936,9 @@ extern int _PyType_CacheInitForSpecialization(PyHeapTypeObject *type, #ifdef Py_GIL_DISABLED # define MANAGED_DICT_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-1) # define MANAGED_WEAKREF_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-2) -# define MANAGED_DICT_OFFSET_NO_GC (((Py_ssize_t)sizeof(PyObject *))*-1) -# define MANAGED_WEAKREF_OFFSET_NO_GC (((Py_ssize_t)sizeof(PyObject *))*-2) #else # define MANAGED_DICT_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-3) # define MANAGED_WEAKREF_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-4) -# define MANAGED_DICT_OFFSET_NO_GC (((Py_ssize_t)sizeof(PyObject *))*-1) -# define MANAGED_WEAKREF_OFFSET_NO_GC (((Py_ssize_t)sizeof(PyObject *))*-2) #endif typedef union { @@ -952,13 +948,8 @@ typedef union { static inline PyManagedDictPointer * _PyObject_ManagedDictPointer(PyObject *obj) { - PyTypeObject *type = Py_TYPE(obj); - assert(type->tp_flags & Py_TPFLAGS_MANAGED_DICT); - Py_ssize_t offset = MANAGED_DICT_OFFSET; - if (!_PyType_IS_GC(type)) { - offset = MANAGED_DICT_OFFSET_NO_GC; - } - return (PyManagedDictPointer *)((char *)obj + offset); + assert(Py_TYPE(obj)->tp_flags & Py_TPFLAGS_MANAGED_DICT); + return (PyManagedDictPointer *)((char *)obj + MANAGED_DICT_OFFSET); } static inline PyDictObject * diff --git a/Include/object.h b/Include/object.h index 9585f4a1d67a52..117d057a18178f 100644 --- a/Include/object.h +++ b/Include/object.h @@ -529,7 +529,7 @@ given type object has a specified feature. #define Py_TPFLAGS_INLINE_VALUES (1 << 2) /* Placement of weakref pointers are managed by the VM, not by the type. - * The VM will automatically set tp_weaklistoffset. + * The VM will automatically set tp_weaklistoffset. Implies Py_TPFLAGS_HAVE_GC. */ #define Py_TPFLAGS_MANAGED_WEAKREF (1 << 3) diff --git a/Lib/test/test_capi/test_type.py b/Lib/test/test_capi/test_type.py index a1adff3b5ca1b1..15fb4a93e2ad74 100644 --- a/Lib/test/test_capi/test_type.py +++ b/Lib/test/test_capi/test_type.py @@ -1,6 +1,5 @@ from test.support import import_helper, Py_GIL_DISABLED, refleak_helper import unittest -import weakref _testcapi = import_helper.import_module('_testcapi') @@ -275,16 +274,3 @@ def test_extension_managed_dict_type(self): obj.__dict__ = {'bar': 3} self.assertEqual(obj.__dict__, {'bar': 3}) self.assertEqual(obj.bar, 3) - - def test_type_have_managed_weakref_and_no_gc(self): - ManagedWeakrefNoGCType = _testcapi.ManagedWeakrefNoGCType - obj = ManagedWeakrefNoGCType() - wr = weakref.ref(obj) - - del obj # shouldn't segfault - del wr - - def test_type_have_managed_dict_and_no_gc(self): - ManagedDictNoGCType = _testcapi.ManagedDictNoGCType - obj = ManagedDictNoGCType() - del obj # shouldn't segfault diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst index 8d2d9df6b385a8..87523ad893b89d 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst @@ -1,3 +1,3 @@ -Fix segmentation fault when deallocating object of the type with -:c:macro:`Py_TPFLAGS_MANAGED_WEAKREF` or :c:macro:`Py_TPFLAGS_MANAGED_DICT` -enabled but without :c:macro:`Py_TPFLAGS_HAVE_GC`. +Force to use :c:macro:`Py_TPFLAGS_HAVE_GC` if +:c:macro:`Py_TPFLAGS_MANAGED_DICT` or :c:macro:`Py_TPFLAGS_MANAGED_WEAKREF` +used. diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 2a103551747ffc..508ef55511e49d 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -3227,74 +3227,12 @@ static PyType_Spec ManagedDict_spec = { ManagedDict_slots }; -typedef struct { - PyObject_HEAD -} ManagedWeakrefNoGCObject; - -static void -ManagedWeakrefNoGC_dealloc(PyObject *self) -{ - PyObject_ClearWeakRefs(self); - PyTypeObject *tp = Py_TYPE(self); - tp->tp_free(self); - Py_DECREF(tp); -} - -static PyType_Slot ManagedWeakrefNoGC_slots[] = { - {Py_tp_dealloc, ManagedWeakrefNoGC_dealloc}, - {0, 0} -}; - -static PyType_Spec ManagedWeakrefNoGC_spec = { - .name = "_testcapi.ManagedWeakrefNoGCType", - .basicsize = sizeof(ManagedWeakrefNoGCObject), - .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_MANAGED_WEAKREF), - .slots = ManagedWeakrefNoGC_slots, -}; - -typedef struct { - PyObject_HEAD -} ManagedDictNoGCObject; - -static void -ManagedDictNoGC_dealloc(PyObject *self) -{ - PyTypeObject *tp = Py_TYPE(self); - tp->tp_free(self); - Py_DECREF(tp); -} - -static PyType_Slot ManagedDictNoGC_slots[] = { - {Py_tp_dealloc, ManagedDictNoGC_dealloc}, - {0, 0} -}; - -static PyType_Spec ManagedDictNoGC_spec = { - .name = "_testcapi.ManagedDictNoGCType", - .basicsize = sizeof(ManagedDictNoGCObject), - .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_MANAGED_DICT), - .slots = ManagedDictNoGC_slots, -}; - - static PyObject * create_managed_dict_type(void) { return PyType_FromSpec(&ManagedDict_spec); } -static PyObject * -create_managed_weakref_no_gc_type(void) -{ - return PyType_FromSpec(&ManagedWeakrefNoGC_spec); -} - -static PyObject * -create_managed_dict_no_gc_type(void) -{ - return PyType_FromSpec(&ManagedDictNoGC_spec); -} - static int _testcapi_exec(PyObject *m) { @@ -3424,22 +3362,6 @@ _testcapi_exec(PyObject *m) return -1; } - PyObject *managed_weakref_no_gc_type = create_managed_weakref_no_gc_type(); - if (managed_weakref_no_gc_type == NULL) { - return -1; - } - if (PyModule_Add(m, "ManagedWeakrefNoGCType", managed_weakref_no_gc_type) < 0) { - return -1; - } - - PyObject *managed_dict_no_gc_type = create_managed_dict_no_gc_type(); - if (managed_dict_no_gc_type == NULL) { - return -1; - } - if (PyModule_Add(m, "ManagedDictNoGCType", managed_dict_no_gc_type) < 0) { - return -1; - } - /* Include tests from the _testcapi/ directory */ if (_PyTestCapi_Init_Vectorcall(m) < 0) { return -1; diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 8c3ef9d879b50b..9e1b428422f0dc 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -4655,12 +4655,7 @@ type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type, PyObject *dict if (ctx->add_weak) { assert((type->tp_flags & Py_TPFLAGS_MANAGED_WEAKREF) == 0); type_add_flags(type, Py_TPFLAGS_MANAGED_WEAKREF); - if (_PyType_IS_GC(type)) { - type->tp_weaklistoffset = MANAGED_WEAKREF_OFFSET; - } - else { - type->tp_weaklistoffset = MANAGED_WEAKREF_OFFSET_NO_GC; - } + type->tp_weaklistoffset = MANAGED_WEAKREF_OFFSET; } if (ctx->add_dict) { assert((type->tp_flags & Py_TPFLAGS_MANAGED_DICT) == 0); @@ -8505,13 +8500,6 @@ overrides_hash(PyTypeObject *type) return r; } -void -PyObject_NoGC_Preheader_Del(void *op) -{ - size_t presize = _PyType_PreHeaderSize(Py_TYPE(op)); - PyObject_Free(((char *)op) - presize); -} - static int inherit_slots(PyTypeObject *type, PyTypeObject *base) { @@ -8689,21 +8677,7 @@ inherit_slots(PyTypeObject *type, PyTypeObject *base) if ((type->tp_flags & Py_TPFLAGS_HAVE_GC) == (base->tp_flags & Py_TPFLAGS_HAVE_GC)) { /* They agree about gc. */ - - if ((type->tp_flags & Py_TPFLAGS_PREHEADER) && - type->tp_free == NULL && - base->tp_free == PyObject_Free) { - /* Because type has preheader fields, its - * objects will be allocated with those fields - * and it should be take in account when object - * is freed, so we use special tp_free. - */ - type->tp_free = PyObject_NoGC_Preheader_Del; - } - else { - COPYSLOT(tp_free); - } - + COPYSLOT(tp_free); } else if ((type->tp_flags & Py_TPFLAGS_HAVE_GC) && type->tp_free == NULL && @@ -8919,6 +8893,13 @@ type_ready_preheader(PyTypeObject *type) type->tp_name); return -1; } + if (!(type->tp_flags & Py_TPFLAGS_HAVE_GC)) { + PyErr_Format(PyExc_SystemError, + "type %s has the Py_TPFLAGS_MANAGED_DICT flag " + "but not Py_TPFLAGS_HAVE_GC flag", + type->tp_name); + return -1; + } type->tp_dictoffset = -1; } if (type->tp_flags & Py_TPFLAGS_MANAGED_WEAKREF) { @@ -8931,12 +8912,14 @@ type_ready_preheader(PyTypeObject *type) type->tp_name); return -1; } - if (_PyType_IS_GC(type)) { - type->tp_weaklistoffset = MANAGED_WEAKREF_OFFSET; - } - else { - type->tp_weaklistoffset = MANAGED_WEAKREF_OFFSET_NO_GC; + if (!(type->tp_flags & Py_TPFLAGS_HAVE_GC)) { + PyErr_Format(PyExc_SystemError, + "type %s has the Py_TPFLAGS_MANAGED_WEAKREF flag " + "but not Py_TPFLAGS_HAVE_GC flag", + type->tp_name); + return -1; } + type->tp_weaklistoffset = MANAGED_WEAKREF_OFFSET; } return 0; } diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 3dc368ad07e76a..f9f14322df0a5e 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -3638,13 +3638,7 @@ dummy_func( _LOAD_ATTR_NONDESCRIPTOR_NO_DICT; op(_CHECK_ATTR_METHOD_LAZY_DICT, (dictoffset/1, owner -- owner)) { - PyObject *borrowed = PyStackRef_AsPyObjectBorrow(owner); - Py_ssize_t offset = MANAGED_DICT_OFFSET; - if (!PyType_IS_GC(Py_TYPE(borrowed))) { - offset = MANAGED_DICT_OFFSET_NO_GC; - } - - char *ptr = ((char *)borrowed) + offset + dictoffset; + char *ptr = ((char *)PyStackRef_AsPyObjectBorrow(owner)) + MANAGED_DICT_OFFSET + dictoffset; PyObject *dict = FT_ATOMIC_LOAD_PTR_ACQUIRE(*(PyObject **)ptr); /* This object has a __dict__, just not yet created */ DEOPT_IF(dict != NULL); diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 7692ada5305200..0e4d86463761a0 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -4832,12 +4832,7 @@ _PyStackRef owner; owner = stack_pointer[-1]; uint16_t dictoffset = (uint16_t)CURRENT_OPERAND0(); - PyObject *borrowed = PyStackRef_AsPyObjectBorrow(owner); - Py_ssize_t offset = MANAGED_DICT_OFFSET; - if (!PyType_IS_GC(Py_TYPE(borrowed))) { - offset = MANAGED_DICT_OFFSET_NO_GC; - } - char *ptr = ((char *)borrowed) + offset + dictoffset; + char *ptr = ((char *)PyStackRef_AsPyObjectBorrow(owner)) + MANAGED_DICT_OFFSET + dictoffset; PyObject *dict = FT_ATOMIC_LOAD_PTR_ACQUIRE(*(PyObject **)ptr); if (dict != NULL) { UOP_STAT_INC(uopcode, miss); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index a1c9347a0c417b..79328a7b725613 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -8175,12 +8175,7 @@ // _CHECK_ATTR_METHOD_LAZY_DICT { uint16_t dictoffset = read_u16(&this_instr[4].cache); - PyObject *borrowed = PyStackRef_AsPyObjectBorrow(owner); - Py_ssize_t offset = MANAGED_DICT_OFFSET; - if (!PyType_IS_GC(Py_TYPE(borrowed))) { - offset = MANAGED_DICT_OFFSET_NO_GC; - } - char *ptr = ((char *)borrowed) + offset + dictoffset; + char *ptr = ((char *)PyStackRef_AsPyObjectBorrow(owner)) + MANAGED_DICT_OFFSET + dictoffset; PyObject *dict = FT_ATOMIC_LOAD_PTR_ACQUIRE(*(PyObject **)ptr); if (dict != NULL) { UPDATE_MISS_STATS(LOAD_ATTR); diff --git a/Python/specialize.c b/Python/specialize.c index 96576eb6c971f8..47f7b27b4908fd 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -1626,12 +1626,7 @@ specialize_attr_loadclassattr(PyObject *owner, _Py_CODEUNIT *instr, else { Py_ssize_t dictoffset; if (tp_flags & Py_TPFLAGS_MANAGED_DICT) { - if (tp_flags & Py_TPFLAGS_HAVE_GC) { - dictoffset = MANAGED_DICT_OFFSET; - } - else { - dictoffset = MANAGED_DICT_OFFSET_NO_GC; - } + dictoffset = MANAGED_DICT_OFFSET; } else { dictoffset = owner_cls->tp_dictoffset; From 360874767ab82b1c9f233b50b29dfa1e72b64272 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Fri, 26 Sep 2025 18:46:02 +0500 Subject: [PATCH 10/14] Fix wording --- Doc/c-api/typeobj.rst | 4 ++-- Doc/whatsnew/3.15.rst | 2 +- .../2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Doc/c-api/typeobj.rst b/Doc/c-api/typeobj.rst index 716e2ae00dc82e..e9609dfaab59f3 100644 --- a/Doc/c-api/typeobj.rst +++ b/Doc/c-api/typeobj.rst @@ -1260,7 +1260,7 @@ and :c:data:`PyType_Type` effectively act as defaults.) This bit indicates that instances of the class have a :attr:`~object.__dict__` attribute, and that the space for the dictionary is managed by the VM. - If this flag is set, :c:macro:`Py_TPFLAGS_HAVE_GC` should also be set. + If this flag is set, :c:macro:`Py_TPFLAGS_HAVE_GC` must also be set. The type traverse function must call :c:func:`PyObject_VisitManagedDict` and its clear function must call :c:func:`PyObject_ClearManagedDict`. @@ -1278,7 +1278,7 @@ and :c:data:`PyType_Type` effectively act as defaults.) This bit indicates that instances of the class should be weakly referenceable. - If this flag is set, :c:macro:`Py_TPFLAGS_HAVE_GC` should also be set. + If this flag is set, :c:macro:`Py_TPFLAGS_HAVE_GC` must also be set. .. versionadded:: 3.12 diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index f3274be42f3ba7..b01257c5b0d693 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -789,7 +789,7 @@ Limited C API changes --------------------- * If the :c:macro:`Py_TPFLAGS_MANAGED_DICT` and :c:macro:`Py_TPFLAGS_MANAGED_WEAKREF` - flags are set then :c:macro:`Py_TPFLAGS_HAVE_GC` should be set too. + flags are set then :c:macro:`Py_TPFLAGS_HAVE_GC` must be set too. (Contributed by Sergey Miryanov in :gh:`134786`) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst index 87523ad893b89d..664e4d2db384ad 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-24-13-12-58.gh-issue-134786.MF0VVk.rst @@ -1,3 +1,2 @@ -Force to use :c:macro:`Py_TPFLAGS_HAVE_GC` if -:c:macro:`Py_TPFLAGS_MANAGED_DICT` or :c:macro:`Py_TPFLAGS_MANAGED_WEAKREF` -used. +If :c:macro:`Py_TPFLAGS_MANAGED_DICT` and :c:macro:`Py_TPFLAGS_MANAGED_WEAKREF` +are used, then :c:macro:`Py_TPFLAGS_HAVE_GC` must be used as well. From c6914498acfb7984710f3c96e2c5420f1f5af50a Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Sat, 27 Sep 2025 13:54:02 +0500 Subject: [PATCH 11/14] Fix whatsnew section --- Doc/whatsnew/3.15.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 1c2bae8c130129..b03017f17d64df 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -803,8 +803,8 @@ New features (Contributed by Victor Stinner in :gh:`129813`.) -Limited C API changes ---------------------- +Changed C APIs +-------------- * If the :c:macro:`Py_TPFLAGS_MANAGED_DICT` and :c:macro:`Py_TPFLAGS_MANAGED_WEAKREF` flags are set then :c:macro:`Py_TPFLAGS_HAVE_GC` must be set too. From 86acf79f928bd622bc1131cb2e746b891902f0ea Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Wed, 22 Oct 2025 18:39:56 +0500 Subject: [PATCH 12/14] Update Doc/whatsnew/3.15.rst Co-authored-by: Mikhail Efimov --- Doc/whatsnew/3.15.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index b03017f17d64df..1b19bab43234e4 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -806,8 +806,8 @@ New features Changed C APIs -------------- -* If the :c:macro:`Py_TPFLAGS_MANAGED_DICT` and :c:macro:`Py_TPFLAGS_MANAGED_WEAKREF` - flags are set then :c:macro:`Py_TPFLAGS_HAVE_GC` must be set too. +* If the :c:macro:`Py_TPFLAGS_MANAGED_DICT` or :c:macro:`Py_TPFLAGS_MANAGED_WEAKREF` + flag is set then :c:macro:`Py_TPFLAGS_HAVE_GC` must be set too. (Contributed by Sergey Miryanov in :gh:`134786`) From 252c6bdfd98ca2203fc2463260011b086313cece Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Tue, 28 Oct 2025 22:37:46 +0500 Subject: [PATCH 13/14] Add tests --- Lib/test/test_capi/test_type.py | 7 +++++++ Modules/_testcapimodule.c | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/Lib/test/test_capi/test_type.py b/Lib/test/test_capi/test_type.py index 15fb4a93e2ad74..93874fbee326dd 100644 --- a/Lib/test/test_capi/test_type.py +++ b/Lib/test/test_capi/test_type.py @@ -274,3 +274,10 @@ def test_extension_managed_dict_type(self): obj.__dict__ = {'bar': 3} self.assertEqual(obj.__dict__, {'bar': 3}) self.assertEqual(obj.bar, 3) + + def test_extension_managed_weakref_nogc_type(self): + msg = ("type _testcapi.ManagedWeakrefNoGCType " + "has the Py_TPFLAGS_MANAGED_WEAKREF " + "flag but not Py_TPFLAGS_HAVE_GC flag") + with self.assertRaisesRegex(SystemError, msg): + _testcapi.create_managed_weakref_nogc_type() diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 4e73be20e1b709..df23aab06829f8 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2562,6 +2562,39 @@ toggle_reftrace_printer(PyObject *ob, PyObject *arg) Py_RETURN_NONE; } + +typedef struct { + PyObject_HEAD +} ManagedWeakrefNoGCObject; + +static void +ManagedWeakrefNoGC_dealloc(PyObject *self) +{ + PyObject_ClearWeakRefs(self); + PyTypeObject *tp = Py_TYPE(self); + tp->tp_free(self); + Py_DECREF(tp); +} + +static PyType_Slot ManagedWeakrefNoGC_slots[] = { + {Py_tp_dealloc, ManagedWeakrefNoGC_dealloc}, + {0, 0} +}; + +static PyType_Spec ManagedWeakrefNoGC_spec = { + .name = "_testcapi.ManagedWeakrefNoGCType", + .basicsize = sizeof(ManagedWeakrefNoGCObject), + .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_MANAGED_WEAKREF), + .slots = ManagedWeakrefNoGC_slots, +}; + +static PyObject * +test_create_managed_weakref_nogc_type(PyObject *self, PyObject *Py_UNUSED(args)) +{ + return PyType_FromSpec(&ManagedWeakrefNoGC_spec); +} + + static PyMethodDef TestMethods[] = { {"set_errno", set_errno, METH_VARARGS}, {"test_config", test_config, METH_NOARGS}, @@ -2656,6 +2689,8 @@ static PyMethodDef TestMethods[] = { {"test_atexit", test_atexit, METH_NOARGS}, {"code_offset_to_line", _PyCFunction_CAST(code_offset_to_line), METH_FASTCALL}, {"toggle_reftrace_printer", toggle_reftrace_printer, METH_O}, + {"create_managed_weakref_nogc_type", + test_create_managed_weakref_nogc_type, METH_NOARGS}, {NULL, NULL} /* sentinel */ }; From 1715eff7dd0fafd36dbf4a0926b053a2501ef7fa Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Wed, 29 Oct 2025 00:04:42 +0500 Subject: [PATCH 14/14] Fix name for create_managed_weakref_nogc_type --- Modules/_testcapimodule.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index df23aab06829f8..e29b9ae354bc1d 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2589,7 +2589,7 @@ static PyType_Spec ManagedWeakrefNoGC_spec = { }; static PyObject * -test_create_managed_weakref_nogc_type(PyObject *self, PyObject *Py_UNUSED(args)) +create_managed_weakref_nogc_type(PyObject *self, PyObject *Py_UNUSED(args)) { return PyType_FromSpec(&ManagedWeakrefNoGC_spec); } @@ -2690,7 +2690,7 @@ static PyMethodDef TestMethods[] = { {"code_offset_to_line", _PyCFunction_CAST(code_offset_to_line), METH_FASTCALL}, {"toggle_reftrace_printer", toggle_reftrace_printer, METH_O}, {"create_managed_weakref_nogc_type", - test_create_managed_weakref_nogc_type, METH_NOARGS}, + create_managed_weakref_nogc_type, METH_NOARGS}, {NULL, NULL} /* sentinel */ };