From f528d130c065d5b0f918e444b85a71200c0de8b4 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 2 Nov 2026 13:58:57 +0100 Subject: [PATCH 1/5] gh-161185: Fix strict aliasing in Py_SETREF() macros family Py_SETREF(), Py_XSETREF() and Py_CLEAR() now use memcpy() to set _tmp_old_dst/_tmp_old_op to fix a strict aliasing issue. Py_SETREF(), Py_XSETREF() and Py_CLEAR() now also cast _tmp_old_dst/_tmp_old_op to PyObject* when calling Py_DECREF()/Py_XDECREF() to avoid compiler warnings with the limited C API. Co-Authored-by: Nathan Goldbaum --- Include/refcount.h | 21 +++-- Lib/test/test_cext/extension.c | 88 +++++++++++++++++++ ...-11-02-15-17-50.gh-issue-161185.4bpyDC.rst | 3 + 3 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/C_API/2026-11-02-15-17-50.gh-issue-161185.4bpyDC.rst diff --git a/Include/refcount.h b/Include/refcount.h index afbecd48ab31a1b..2bc2e83ef1c340f 100644 --- a/Include/refcount.h +++ b/Include/refcount.h @@ -489,7 +489,7 @@ static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op) _Py_TYPEOF(op) _tmp_old_op = (*_tmp_op_ptr); \ if (_tmp_old_op != _Py_NULL) { \ *_tmp_op_ptr = _Py_NULL; \ - Py_DECREF(_tmp_old_op); \ + Py_DECREF(_PyObject_CAST(_tmp_old_op)); \ } \ } while (0) #elif defined(__cplusplus) && (__cplusplus >= 201103L || _MSVC_LANG >= 201103L) @@ -499,14 +499,15 @@ static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op) auto _tmp_old_op = (*_tmp_op_ptr); \ if (_tmp_old_op != _Py_NULL) { \ *_tmp_op_ptr = _Py_NULL; \ - Py_DECREF(_tmp_old_op); \ + Py_DECREF(_PyObject_CAST(_tmp_old_op)); \ } \ } while (0) #else #define Py_CLEAR(op) \ do { \ PyObject **_tmp_op_ptr = _Py_CAST(PyObject**, &(op)); \ - PyObject *_tmp_old_op = (*_tmp_op_ptr); \ + PyObject *_tmp_old_op; \ + memcpy(&_tmp_old_op, _tmp_op_ptr, sizeof(PyObject*)); \ if (_tmp_old_op != _Py_NULL) { \ PyObject *_null_ptr = _Py_NULL; \ memcpy(_tmp_op_ptr, &_null_ptr, sizeof(PyObject*)); \ @@ -545,7 +546,7 @@ static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op) _Py_TYPEOF(&(dst)) _tmp_dst_ptr = &(dst); \ _Py_TYPEOF(dst) _tmp_old_dst = (*_tmp_dst_ptr); \ *_tmp_dst_ptr = (src); \ - Py_DECREF(_tmp_old_dst); \ + Py_DECREF(_PyObject_CAST(_tmp_old_dst)); \ } while (0) #elif defined(__cplusplus) && (__cplusplus >= 201103L || _MSVC_LANG >= 201103L) #define Py_SETREF(dst, src) \ @@ -553,13 +554,14 @@ static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op) auto _tmp_dst_ptr = &(dst); \ auto _tmp_old_dst = (*_tmp_dst_ptr); \ *_tmp_dst_ptr = (src); \ - Py_DECREF(_tmp_old_dst); \ + Py_DECREF(_PyObject_CAST(_tmp_old_dst)); \ } while (0) #else #define Py_SETREF(dst, src) \ do { \ PyObject **_tmp_dst_ptr = _Py_CAST(PyObject**, &(dst)); \ - PyObject *_tmp_old_dst = (*_tmp_dst_ptr); \ + PyObject *_tmp_old_dst; \ + memcpy(&_tmp_old_dst, _tmp_dst_ptr, sizeof(PyObject*)); \ PyObject *_tmp_src = _PyObject_CAST(src); \ memcpy(_tmp_dst_ptr, &_tmp_src, sizeof(PyObject*)); \ Py_DECREF(_tmp_old_dst); \ @@ -575,7 +577,7 @@ static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op) _Py_TYPEOF(&(dst)) _tmp_dst_ptr = &(dst); \ _Py_TYPEOF(dst) _tmp_old_dst = (*_tmp_dst_ptr); \ *_tmp_dst_ptr = (src); \ - Py_XDECREF(_tmp_old_dst); \ + Py_XDECREF(_PyObject_CAST(_tmp_old_dst)); \ } while (0) #elif defined(__cplusplus) && (__cplusplus >= 201103L || _MSVC_LANG >= 201103L) #define Py_XSETREF(dst, src) \ @@ -583,13 +585,14 @@ static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op) auto _tmp_dst_ptr = &(dst); \ auto _tmp_old_dst = (*_tmp_dst_ptr); \ *_tmp_dst_ptr = (src); \ - Py_XDECREF(_tmp_old_dst); \ + Py_XDECREF(_PyObject_CAST(_tmp_old_dst)); \ } while (0) #else #define Py_XSETREF(dst, src) \ do { \ PyObject **_tmp_dst_ptr = _Py_CAST(PyObject**, &(dst)); \ - PyObject *_tmp_old_dst = (*_tmp_dst_ptr); \ + PyObject *_tmp_old_dst; \ + memcpy(&_tmp_old_dst, _tmp_dst_ptr, sizeof(PyObject*)); \ PyObject *_tmp_src = _PyObject_CAST(src); \ memcpy(_tmp_dst_ptr, &_tmp_src, sizeof(PyObject*)); \ Py_XDECREF(_tmp_old_dst); \ diff --git a/Lib/test/test_cext/extension.c b/Lib/test/test_cext/extension.c index 779ea9facca8c78..15e58cf8bba723f 100644 --- a/Lib/test/test_cext/extension.c +++ b/Lib/test/test_cext/extension.c @@ -140,6 +140,91 @@ test_macros(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } +// sizeof(PyObject) in PySlot below is incompatible with opaque PyObject +#ifndef Py_TARGET_ABI3T +Py_NO_INLINE static void +pyclear_strict_aliasing(PyTypeObject **a, PyTypeObject **b) +{ + Py_CLEAR(*a); + *b = NULL; +} + + +Py_NO_INLINE static void +setref_strict_aliasing(PyTypeObject **a, PyTypeObject **b, + PyTypeObject *first, PyTypeObject *second) +{ + *a = first; + Py_SETREF(*b, second); +} + + +Py_NO_INLINE static void +xsetref_strict_aliasing(PyTypeObject **a, PyTypeObject **b, + PyTypeObject *first, PyTypeObject *second) +{ + *a = first; + Py_XSETREF(*b, second); +} + + +// gh-161185: Test that Py_SETREF(), Py_XSETREF() and Py_CLEAR() macros +// have no strict aliasing issue +static PyObject * +test_setref_strict_aliasing(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + PySlot slots[] = { + PySlot_PTR_STATIC(Py_tp_name, STR(MODULE_NAME) ".setref_type"), + PySlot_PTR_STATIC(Py_tp_basicsize, sizeof(PyObject)), + PySlot_PTR_STATIC(Py_tp_flags, Py_TPFLAGS_DEFAULT), + PySlot_END + }; + PyObject *first, *second; + Py_ssize_t first_refcnt, second_refcnt; + PyTypeObject *slot = NULL; + + first = PyType_FromSlots(slots); + if (first == NULL) { + return NULL; + } + second = PyType_FromSlots(slots); + if (second == NULL) { + Py_DECREF(first); + return NULL; + } + first_refcnt = Py_REFCNT(first); + second_refcnt = Py_REFCNT(first); + + // Both pointer arguments alias. The assignment makes first the old value + // that Py_SETREF() must release before storing second. + setref_strict_aliasing(&slot, &slot, + (PyTypeObject*)Py_NewRef(first), + (PyTypeObject*)Py_NewRef(second)); + assert((PyObject*)slot == second); + assert(Py_REFCNT(first) == first_refcnt); + assert(Py_REFCNT(second) == (second_refcnt + 1)); + + pyclear_strict_aliasing(&slot, &slot); + assert(slot == NULL); + assert(Py_REFCNT(first) == first_refcnt); + assert(Py_REFCNT(second) == second_refcnt); + + xsetref_strict_aliasing(&slot, &slot, + (PyTypeObject*)Py_NewRef(first), + (PyTypeObject*)Py_NewRef(second)); + assert((PyObject*)slot == second); + assert(Py_REFCNT(first) == first_refcnt); + assert(Py_REFCNT(second) == (second_refcnt + 1)); + + Py_DECREF(first); + Py_DECREF(second); + Py_DECREF((PyObject*)slot); + + Py_RETURN_NONE; +} +#endif // !Py_TARGET_ABI3T + + static PyObject * test_datetime(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { @@ -366,6 +451,9 @@ test_virtual_object(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) static PyMethodDef module_methods[] = { {"add", test_add, METH_VARARGS, test_add_doc}, {"test_macros", test_macros, METH_NOARGS, _Py_NULL}, +#ifndef Py_TARGET_ABI3T + {"test_setref_strict_aliasing", test_setref_strict_aliasing, METH_NOARGS, _Py_NULL}, +#endif {"test_datetime", test_datetime, METH_NOARGS, _Py_NULL}, {"test_unicode", test_unicode, METH_NOARGS, _Py_NULL}, #ifdef __cplusplus diff --git a/Misc/NEWS.d/next/C_API/2026-11-02-15-17-50.gh-issue-161185.4bpyDC.rst b/Misc/NEWS.d/next/C_API/2026-11-02-15-17-50.gh-issue-161185.4bpyDC.rst new file mode 100644 index 000000000000000..92887e84ade4cd2 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-11-02-15-17-50.gh-issue-161185.4bpyDC.rst @@ -0,0 +1,3 @@ +Fix a strict aliasing issue in the :c:func:`!memcpy` implementation of +:c:macro:`Py_SETREF`, :c:macro:`Py_XSETREF` and :c:macro:`Py_CLEAR` macros. +Patch by Nathan Goldbaum and Victor Stinner. From 569ddd8c9f8901502963fa75b70d6a3980412cdd Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 4 Nov 2026 18:11:33 +0100 Subject: [PATCH 2/5] Fix typo: Py_REFCNT(second) --- Lib/test/test_cext/extension.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_cext/extension.c b/Lib/test/test_cext/extension.c index 15e58cf8bba723f..0421ea9e9969c4a 100644 --- a/Lib/test/test_cext/extension.c +++ b/Lib/test/test_cext/extension.c @@ -193,7 +193,7 @@ test_setref_strict_aliasing(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(arg return NULL; } first_refcnt = Py_REFCNT(first); - second_refcnt = Py_REFCNT(first); + second_refcnt = Py_REFCNT(second); // Both pointer arguments alias. The assignment makes first the old value // that Py_SETREF() must release before storing second. From a02b2c321a716009193864f995e4500c8e6469cb Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 4 Nov 2026 18:19:20 +0100 Subject: [PATCH 3/5] Fix ISO C90 compatibility Move variable declaration before code. --- Include/refcount.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Include/refcount.h b/Include/refcount.h index 2bc2e83ef1c340f..9b681f6a254a42a 100644 --- a/Include/refcount.h +++ b/Include/refcount.h @@ -561,8 +561,8 @@ static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op) do { \ PyObject **_tmp_dst_ptr = _Py_CAST(PyObject**, &(dst)); \ PyObject *_tmp_old_dst; \ - memcpy(&_tmp_old_dst, _tmp_dst_ptr, sizeof(PyObject*)); \ PyObject *_tmp_src = _PyObject_CAST(src); \ + memcpy(&_tmp_old_dst, _tmp_dst_ptr, sizeof(PyObject*)); \ memcpy(_tmp_dst_ptr, &_tmp_src, sizeof(PyObject*)); \ Py_DECREF(_tmp_old_dst); \ } while (0) @@ -592,8 +592,8 @@ static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op) do { \ PyObject **_tmp_dst_ptr = _Py_CAST(PyObject**, &(dst)); \ PyObject *_tmp_old_dst; \ - memcpy(&_tmp_old_dst, _tmp_dst_ptr, sizeof(PyObject*)); \ PyObject *_tmp_src = _PyObject_CAST(src); \ + memcpy(&_tmp_old_dst, _tmp_dst_ptr, sizeof(PyObject*)); \ memcpy(_tmp_dst_ptr, &_tmp_src, sizeof(PyObject*)); \ Py_XDECREF(_tmp_old_dst); \ } while (0) From d6583b755f77e35af6f06e0cff3890c4eca4a520 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 4 Nov 2026 18:28:02 +0100 Subject: [PATCH 4/5] Test the memcpy() implementation in test_cext Add the _Py_TESTCEXT_NO_TYPEOF macro to undefine the _Py_TYPEOF macro. Co-Authored-by: Nathan Goldbaum --- Include/pyport.h | 5 +++++ Lib/test/test_cext/__init__.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/Include/pyport.h b/Include/pyport.h index 3de7fb3d317521a..305b467cfa5c922 100644 --- a/Include/pyport.h +++ b/Include/pyport.h @@ -550,6 +550,11 @@ extern "C" { # define _Py_TYPEOF(expr) __typeof__(expr) #endif +#ifdef _Py_TESTCEXT_NO_TYPEOF + // Exercise fallback implementations in test_cext. +# undef _Py_TYPEOF +#endif + /* A convenient way for code to know if sanitizers are enabled. */ #if defined(__has_feature) diff --git a/Lib/test/test_cext/__init__.py b/Lib/test/test_cext/__init__.py index 185f10911370347..9518f45e5294dd3 100644 --- a/Lib/test/test_cext/__init__.py +++ b/Lib/test/test_cext/__init__.py @@ -113,6 +113,13 @@ class TestPublicC(BaseTests, unittest.TestCase): def test_build(self): self.check_build('_test_cext') + def test_build_no_typeof(self): + # gh-161185: Exercise the Py_CLEAR/Py_SETREF() memcpy implementation + # with strict aliasing enabled: undefine the _Py_TYPEOF macro. + self.check_build( + '_test_cext_no_typeof', + extra_cflags='-D_Py_TESTCEXT_NO_TYPEOF -O2 -fstrict-aliasing') + @unittest.skipIf(MSVC, "MSVC doesn't support /std:c99") def test_build_c99(self): # In public docs, we say C API is compatible with C11. However, @@ -146,6 +153,15 @@ def test_build_cpp03(self): # Please ask the C API WG before adding a new C++11-only feature. self.check_build('_test_cppext_cpp03', std='c++03') + @unittest.skipIf(MSVC, "MSVC doesn't support /std:c++03") + def test_build_cpp03_no_typeof(self): + # gh-161185: Exercise the Py_CLEAR/Py_SETREF() memcpy implementation + # with strict aliasing enabled: undefine the _Py_TYPEOF macro. + self.check_build( + '_test_cppext_cpp03_no_typeof', + std='c++03', + extra_cflags='-D_Py_TESTCEXT_NO_TYPEOF -O2 -fstrict-aliasing') + @unittest.skipIf(MSVC, "MSVC doesn't support /std:c++11") def test_build_cpp11(self): self.check_build('_test_cppext_cpp11', std='c++11') From 4d3f620c5d725920b70aa1d1685075a405a84acc Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 4 Nov 2026 18:52:29 +0100 Subject: [PATCH 5/5] Enable the test on Free Threading The Py_tp_basicsize slot is not required to define a type. --- Lib/test/test_cext/extension.c | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Lib/test/test_cext/extension.c b/Lib/test/test_cext/extension.c index 0421ea9e9969c4a..65dd25d71f494be 100644 --- a/Lib/test/test_cext/extension.c +++ b/Lib/test/test_cext/extension.c @@ -140,8 +140,6 @@ test_macros(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } -// sizeof(PyObject) in PySlot below is incompatible with opaque PyObject -#ifndef Py_TARGET_ABI3T Py_NO_INLINE static void pyclear_strict_aliasing(PyTypeObject **a, PyTypeObject **b) { @@ -175,7 +173,6 @@ test_setref_strict_aliasing(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(arg { PySlot slots[] = { PySlot_PTR_STATIC(Py_tp_name, STR(MODULE_NAME) ".setref_type"), - PySlot_PTR_STATIC(Py_tp_basicsize, sizeof(PyObject)), PySlot_PTR_STATIC(Py_tp_flags, Py_TPFLAGS_DEFAULT), PySlot_END }; @@ -222,7 +219,6 @@ test_setref_strict_aliasing(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(arg Py_RETURN_NONE; } -#endif // !Py_TARGET_ABI3T static PyObject * @@ -451,9 +447,7 @@ test_virtual_object(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) static PyMethodDef module_methods[] = { {"add", test_add, METH_VARARGS, test_add_doc}, {"test_macros", test_macros, METH_NOARGS, _Py_NULL}, -#ifndef Py_TARGET_ABI3T {"test_setref_strict_aliasing", test_setref_strict_aliasing, METH_NOARGS, _Py_NULL}, -#endif {"test_datetime", test_datetime, METH_NOARGS, _Py_NULL}, {"test_unicode", test_unicode, METH_NOARGS, _Py_NULL}, #ifdef __cplusplus