[3.15] gh-154902: Type-check the SET_ADD operand (#155071)

hugovk <[email protected]> Tue, 04 Aug 2026 06:08:24 -0400 (EDT)
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/398dbef62a2542644b2c1e31d25bcc61837671d5
commit: 398dbef62a2542644b2c1e31d25bcc61837671d5
branch: 3.15
author: Timofei Ivankov <[email protected]>
committer: hugovk <[email protected]>
date: 2026-08-04T13:03:28+03:00
summary:

[3.15] gh-154902: Type-check the SET_ADD operand (#155071)

files:
A Misc/NEWS.d/next/Core_and_Builtins/2026-08-02-11-47-15.gh-issue-154902.DIi6sf.rst
M Lib/test/test_type_annotations.py
M Modules/_testinternalcapi/test_cases.c.h
M Python/bytecodes.c
M Python/executor_cases.c.h
M Python/generated_cases.c.h

diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py
index b751f825bb97d59..54db123a5dc2bf3 100644
--- a/Lib/test/test_type_annotations.py
+++ b/Lib/test/test_type_annotations.py
@@ -1,5 +1,6 @@
 import annotationlib
 import inspect
+import itertools
 import textwrap
 import types
 import unittest
@@ -896,3 +897,18 @@ class Generic:
                 mod = build_module(code)
                 annos = mod.__annotations__
                 self.assertEqual(annos, {"annotated_name": 0})
+
+    # gh-154902
+    def test_conditional_annotations_rebound(self):
+        # user code can rebind __conditional_annotations__ to any object
+        lefts = ("__conditional_annotations__",
+                 'globals()["__conditional_annotations__"]')
+        values = ("0", "{}", "[]", "''", "object()", "frozenset()")
+        for left, value in itertools.product(lefts, values):
+            with self.subTest(left=left, value=value):
+                code = f"""
+                    {left} = {value}
+                    x: int
+                """
+                with self.assertRaises(TypeError):
+                    run_code(code)
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-02-11-47-15.gh-issue-154902.DIi6sf.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-02-11-47-15.gh-issue-154902.DIi6sf.rst
new file mode 100644
index 000000000000000..49f34eaeddec4e4
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-02-11-47-15.gh-issue-154902.DIi6sf.rst
@@ -0,0 +1,2 @@
+Fix a crash when ``__conditional_annotations__`` is rebound to a non-set
+object.
diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h
index e19ff840458e0e0..c51ed3c812523a7 100644
--- a/Modules/_testinternalcapi/test_cases.c.h
+++ b/Modules/_testinternalcapi/test_cases.c.h
@@ -11424,8 +11424,21 @@
             _PyStackRef v;
             v = stack_pointer[-1];
             set = stack_pointer[-2 - (oparg-1)];
+            PyObject *set_o = PyStackRef_AsPyObjectBorrow(set);
+            if (!PySet_CheckExact(set_o)) {
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                _PyErr_Format(tstate, PyExc_TypeError,
+                              "'%T' object is not a set", set_o);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+                stack_pointer += -1;
+                ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                PyStackRef_CLOSE(v);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+                JUMP_TO_LABEL(error);
+            }
             _PyFrame_SetStackPointer(frame, stack_pointer);
-            int err = _PySet_AddTakeRef((PySetObject *)PyStackRef_AsPyObjectBorrow(set),
+            int err = _PySet_AddTakeRef((PySetObject *)set_o,
                                         PyStackRef_AsPyObjectSteal(v));
             stack_pointer = _PyFrame_GetStackPointer(frame);
             if (err) {
diff --git a/Python/bytecodes.c b/Python/bytecodes.c
index f39a5f0b07a5fcc..4f0d0227cc2eb04 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -1378,7 +1378,15 @@ dummy_func(
         }
 
         inst(SET_ADD, (set, unused[oparg-1], v -- set, unused[oparg-1])) {
-            int err = _PySet_AddTakeRef((PySetObject *)PyStackRef_AsPyObjectBorrow(set),
+            PyObject *set_o = PyStackRef_AsPyObjectBorrow(set);
+            // gh-154902: user code can rebind __conditional_annotations__
+            if (!PySet_CheckExact(set_o)) {
+                _PyErr_Format(tstate, PyExc_TypeError,
+                              "'%T' object is not a set", set_o);
+                PyStackRef_CLOSE(v);
+                ERROR_IF(true);
+            }
+            int err = _PySet_AddTakeRef((PySetObject *)set_o,
                                         PyStackRef_AsPyObjectSteal(v));
             ERROR_IF(err);
         }
diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h
index 4d5a8ab6b8af0b9..7644c2d38d7f068 100644
--- a/Python/executor_cases.c.h
+++ b/Python/executor_cases.c.h
@@ -8212,11 +8212,28 @@
             oparg = CURRENT_OPARG();
             v = _stack_item_0;
             set = stack_pointer[-1 - (oparg-1)];
+            PyObject *set_o = PyStackRef_AsPyObjectBorrow(set);
+            if (!PySet_CheckExact(set_o)) {
+                stack_pointer[0] = v;
+                stack_pointer += 1;
+                ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                _PyErr_Format(tstate, PyExc_TypeError,
+                              "'%T' object is not a set", set_o);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+                stack_pointer += -1;
+                ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                PyStackRef_CLOSE(v);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+                SET_CURRENT_CACHED_VALUES(0);
+                JUMP_TO_ERROR();
+            }
             stack_pointer[0] = v;
             stack_pointer += 1;
             ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
             _PyFrame_SetStackPointer(frame, stack_pointer);
-            int err = _PySet_AddTakeRef((PySetObject *)PyStackRef_AsPyObjectBorrow(set),
+            int err = _PySet_AddTakeRef((PySetObject *)set_o,
                                         PyStackRef_AsPyObjectSteal(v));
             stack_pointer = _PyFrame_GetStackPointer(frame);
             if (err) {
diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h
index 512c358014fcffe..741306f7cf27f7c 100644
--- a/Python/generated_cases.c.h
+++ b/Python/generated_cases.c.h
@@ -11421,8 +11421,21 @@
             _PyStackRef v;
             v = stack_pointer[-1];
             set = stack_pointer[-2 - (oparg-1)];
+            PyObject *set_o = PyStackRef_AsPyObjectBorrow(set);
+            if (!PySet_CheckExact(set_o)) {
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                _PyErr_Format(tstate, PyExc_TypeError,
+                              "'%T' object is not a set", set_o);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+                stack_pointer += -1;
+                ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                PyStackRef_CLOSE(v);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+                JUMP_TO_LABEL(error);
+            }
             _PyFrame_SetStackPointer(frame, stack_pointer);
-            int err = _PySet_AddTakeRef((PySetObject *)PyStackRef_AsPyObjectBorrow(set),
+            int err = _PySet_AddTakeRef((PySetObject *)set_o,
                                         PyStackRef_AsPyObjectSteal(v));
             stack_pointer = _PyFrame_GetStackPointer(frame);
             if (err) {

_______________________________________________
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]