https://github.com/python/cpython/commit/adb9185d8ae31684e5cd7a2649c96d1a66f22ea6
commit: adb9185d8ae31684e5cd7a2649c96d1a66f22ea6
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: vstinner <[email protected]>
date: 2026-11-05T00:45:06+01:00
summary:

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

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

gh-161185: Fix strict aliasing issue in Py_SETREF() macros family (GH-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)
(cherry picked from commit 0dc1a9602605f15ccf805c37ed4347307cd46cc5)

Co-authored-by: Victor Stinner <[email protected]>
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 bc10958e867e1a5..8ebbdc4db189d3d 100644
--- a/Include/cpython/object.h
+++ b/Include/cpython/object.h
@@ -344,7 +344,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) \
@@ -352,14 +352,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)
@@ -374,7 +375,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) \
@@ -382,14 +383,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 b8e60d3bd359fca..49b96e57dbd165a 100644
--- a/Include/pyport.h
+++ b/Include/pyport.h
@@ -577,6 +577,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 e0730606646430e..5c1cdae5dfb3935 100644
--- a/Include/refcount.h
+++ b/Include/refcount.h
@@ -483,7 +483,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)
@@ -493,14 +493,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 373dae136c951a9..dfdee106bc4c8c0 100644
--- a/Lib/test/test_cext/__init__.py
+++ b/Lib/test/test_cext/__init__.py
@@ -115,6 +115,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,
@@ -144,6 +151,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 109d809b4db059f..20df15998e4b852 100644
--- a/Lib/test/test_cext/extension.c
+++ b/Lib/test/test_cext/extension.c
@@ -145,6 +145,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))
 {
@@ -372,6 +456,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