https://github.com/python/cpython/commit/1bdfc0f253730077ccd3a4b0714388e8227b1b71
commit: 1bdfc0f253730077ccd3a4b0714388e8227b1b71
branch: main
author: Daniele Parmeggiani <[email protected]>
committer: colesbury <[email protected]>
date: 2026-05-06T09:50:24-04:00
summary:

gh-146270: Fix `PyMember_SetOne(..., NULL)` not being atomic (gh-148800)

Fixes a sequential consistency bug whereby two threads that are deleting a 
struct member may observe both their deletions to be successful.

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-04-20-15-25-55.gh-issue-146270.qZYfyc.rst
M Lib/test/test_free_threading/test_slots.py
M Python/structmember.c

diff --git a/Lib/test/test_free_threading/test_slots.py 
b/Lib/test/test_free_threading/test_slots.py
index a3b9f4b0175ae7..a73525e1bebfb4 100644
--- a/Lib/test/test_free_threading/test_slots.py
+++ b/Lib/test/test_free_threading/test_slots.py
@@ -16,18 +16,19 @@ def run_in_threads(targets):
         thread.join()
 
 
+class Spam:
+    __slots__ = [
+        "eggs",
+    ]
+
+    def __init__(self, initial_value):
+        self.eggs = initial_value
+
+
 @threading_helper.requires_working_threading()
 class TestSlots(TestCase):
 
     def test_object(self):
-        class Spam:
-            __slots__ = [
-                "eggs",
-            ]
-
-            def __init__(self, initial_value):
-                self.eggs = initial_value
-
         spam = Spam(0)
         iters = 20_000
 
@@ -43,6 +44,24 @@ def reader():
 
         run_in_threads([writer, reader, reader, reader])
 
+    def test_del_object_is_atomic(self):
+        # Testing whether the implementation of `del slots_object.attribute`
+        # removes the attribute atomically, thus avoiding non-sequentially-
+        # consistent behaviors.
+        # https://github.com/python/cpython/issues/146270
+        def deleter(spam, successes):
+            try:
+                del spam.eggs
+                successes.append(True)
+            except AttributeError:
+                successes.append(False)
+
+        for _ in range(10):
+            spam = Spam(0)
+            successes = []
+            threading_helper.run_concurrently(deleter, nthreads=4, args=(spam, 
successes))
+            self.assertEqual(sum(successes), 1)
+
     def test_T_BOOL(self):
         spam_old = _testcapi._test_structmembersType_OldAPI()
         spam_new = _testcapi._test_structmembersType_NewAPI()
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-20-15-25-55.gh-issue-146270.qZYfyc.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-20-15-25-55.gh-issue-146270.qZYfyc.rst
new file mode 100644
index 00000000000000..46c292e183e0fd
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-20-15-25-55.gh-issue-146270.qZYfyc.rst
@@ -0,0 +1 @@
+Fix a sequential consistency bug in ``structmember.c``.
diff --git a/Python/structmember.c b/Python/structmember.c
index b88e13ac0462b8..adea8216b8796b 100644
--- a/Python/structmember.c
+++ b/Python/structmember.c
@@ -171,19 +171,10 @@ PyMember_SetOne(char *addr, PyMemberDef *l, PyObject *v)
         PyErr_SetString(PyExc_AttributeError, "readonly attribute");
         return -1;
     }
-    if (v == NULL) {
-        if (l->type == Py_T_OBJECT_EX) {
-            /* Check if the attribute is set. */
-            if (*(PyObject **)addr == NULL) {
-                PyErr_SetString(PyExc_AttributeError, l->name);
-                return -1;
-            }
-        }
-        else if (l->type != _Py_T_OBJECT) {
-            PyErr_SetString(PyExc_TypeError,
-                            "can't delete numeric/char attribute");
-            return -1;
-        }
+    if (v == NULL && l->type != Py_T_OBJECT_EX && l->type != _Py_T_OBJECT) {
+        PyErr_SetString(PyExc_TypeError,
+                        "can't delete numeric/char attribute");
+        return -1;
     }
     switch (l->type) {
     case Py_T_BOOL:{
@@ -334,6 +325,15 @@ PyMember_SetOne(char *addr, PyMemberDef *l, PyObject *v)
         oldv = *(PyObject **)addr;
         FT_ATOMIC_STORE_PTR_RELEASE(*(PyObject **)addr, Py_XNewRef(v));
         Py_END_CRITICAL_SECTION();
+        if (v == NULL && oldv == NULL && l->type == Py_T_OBJECT_EX) {
+            // Raise an exception when attempting to delete an already deleted
+            // attribute.
+            // Differently from Py_T_OBJECT_EX, _Py_T_OBJECT does not raise an
+            // exception here (PyMember_GetOne will return Py_None instead of
+            // NULL).
+            PyErr_SetString(PyExc_AttributeError, l->name);
+            return -1;
+        }
         Py_XDECREF(oldv);
         break;
     case Py_T_CHAR: {

_______________________________________________
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