https://github.com/python/cpython/commit/0dc1a9602605f15ccf805c37ed4347307cd46cc5
commit: 0dc1a9602605f15ccf805c37ed4347307cd46cc5
branch: 3.15
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-11-04T22:10:34Z
summary:

[3.15] gh-161185: Fix strict aliasing issue in Py_SETREF() macros family 
(#161186) (#161360)

gh-161185: Fix strict aliasing issue in Py_SETREF() macros family (#161186)

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.

Add the _Py_TESTCEXT_NO_TYPEOF macro to undefine the _Py_TYPEOF
macro.


(cherry picked from commit f8e65ee35d05cc47e1e02e716c934b8acaa9b5bd)

Co-authored-by: Nathan Goldbaum <[email protected]>

files:
A Misc/NEWS.d/next/C_API/2026-11-02-15-17-50.gh-issue-161185.4bpyDC.rst
M Include/cpython/object.h
M Include/pyport.h
M Include/refcount.h
M Lib/test/test_cext/__init__.py
M Lib/test/test_cext/extension.c

diff --git a/Include/cpython/object.h b/Include/cpython/object.h
index c0809effb996107..82d88b2e4c513c9 100644
--- a/Include/cpython/object.h
+++ b/Include/cpython/object.h
@@ -353,7 +353,7 @@ PyAPI_FUNC(PyObject *) _PyObject_FunctionStr(PyObject *);
         _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) \
@@ -361,14 +361,15 @@ PyAPI_FUNC(PyObject *) _PyObject_FunctionStr(PyObject *);
         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; \
         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)
@@ -383,7 +384,7 @@ PyAPI_FUNC(PyObject *) _PyObject_FunctionStr(PyObject *);
         _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) \
@@ -391,14 +392,15 @@ PyAPI_FUNC(PyObject *) _PyObject_FunctionStr(PyObject *);
         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; \
         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)
diff --git a/Include/pyport.h b/Include/pyport.h
index 9b262ff0022aca6..002770d7bc94344 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/Include/refcount.h b/Include/refcount.h
index 39661680e85a183..f21432cc3061fb4 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*)); \
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')
diff --git a/Lib/test/test_cext/extension.c b/Lib/test/test_cext/extension.c
index dd2895520f751d0..41c66ae3516183c 100644
--- a/Lib/test/test_cext/extension.c
+++ b/Lib/test/test_cext/extension.c
@@ -142,6 +142,90 @@ test_macros(PyObject *Py_UNUSED(module), PyObject 
*Py_UNUSED(args))
 }
 
 
+// Py_SETREF/XSETREF() macros are excluded from the limited C API
+#ifndef Py_LIMITED_API
+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_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(second);
+
+    // 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_LIMITED_API
+
+
 static PyObject *
 test_datetime(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
 {
@@ -368,6 +452,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_LIMITED_API
+    {"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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to