From e8e9e889b8e307fbd31b53baef44ffc1e1904fb4 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sun, 2 Aug 2026 11:59:39 +0300 Subject: [PATCH 1/2] gh-154902: Type-check the SET_ADD operand --- Lib/test/test_type_annotations.py | 16 ++++++++++++++++ ...026-08-02-11-47-15.gh-issue-154902.DIi6sf.rst | 2 ++ Python/bytecodes.c | 11 ++++++++++- Python/executor_cases.c.h | 16 +++++++++++++++- Python/generated_cases.c.h | 16 +++++++++++++++- 5 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-02-11-47-15.gh-issue-154902.DIi6sf.rst diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index c98b99e98e9c8e6..960756263d26735 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/Python/bytecodes.c b/Python/bytecodes.c index 509a5a6e13f8570..8ea8db292c231ba 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -1056,7 +1056,16 @@ 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_Check(set_o)) { + _PyErr_Format(tstate, PyExc_TypeError, + "__conditional_annotations__ must be a set, not %T", + 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 f5c11e0f98dae68..3fc8e6e146b1a14 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -1609,8 +1609,22 @@ oparg = CURRENT_OPARG(); v = stack_pointer[-1]; set = stack_pointer[-2 - (oparg-1)]; + PyObject *set_o = PyStackRef_AsPyObjectBorrow(set); + if (!PySet_Check(set_o)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyErr_Format(tstate, PyExc_TypeError, + "__conditional_annotations__ must be a set, not %T", + set_o); + stack_pointer = _PyFrame_GetStackPointer(frame); + stack_pointer += -1; + assert(WITHIN_STACK_BOUNDS()); + _PyFrame_SetStackPointer(frame, stack_pointer); + PyStackRef_CLOSE(v); + stack_pointer = _PyFrame_GetStackPointer(frame); + JUMP_TO_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/generated_cases.c.h b/Python/generated_cases.c.h index 9e825c529c9dd2d..a36d72c6c549f19 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -10859,8 +10859,22 @@ _PyStackRef v; v = stack_pointer[-1]; set = stack_pointer[-2 - (oparg-1)]; + PyObject *set_o = PyStackRef_AsPyObjectBorrow(set); + if (!PySet_Check(set_o)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyErr_Format(tstate, PyExc_TypeError, + "__conditional_annotations__ must be a set, not %T", + set_o); + stack_pointer = _PyFrame_GetStackPointer(frame); + stack_pointer += -1; + assert(WITHIN_STACK_BOUNDS()); + _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) { From becf4dae01457ec163f6687f14f97949e8be579a Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sun, 2 Aug 2026 19:21:13 +0300 Subject: [PATCH 2/2] changed check to PySet_CheckExact, changed exception message --- Python/bytecodes.c | 5 ++--- Python/executor_cases.c.h | 5 ++--- Python/generated_cases.c.h | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 8ea8db292c231ba..e17c77828389a30 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -1058,10 +1058,9 @@ dummy_func( inst(SET_ADD, (set, unused[oparg-1], v -- set, unused[oparg-1])) { PyObject *set_o = PyStackRef_AsPyObjectBorrow(set); // gh-154902: user code can rebind __conditional_annotations__ - if (!PySet_Check(set_o)) { + if (!PySet_CheckExact(set_o)) { _PyErr_Format(tstate, PyExc_TypeError, - "__conditional_annotations__ must be a set, not %T", - set_o); + "'%T' object is not a set", set_o); PyStackRef_CLOSE(v); ERROR_IF(true); } diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 3fc8e6e146b1a14..0312157af76df39 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -1610,11 +1610,10 @@ v = stack_pointer[-1]; set = stack_pointer[-2 - (oparg-1)]; PyObject *set_o = PyStackRef_AsPyObjectBorrow(set); - if (!PySet_Check(set_o)) { + if (!PySet_CheckExact(set_o)) { _PyFrame_SetStackPointer(frame, stack_pointer); _PyErr_Format(tstate, PyExc_TypeError, - "__conditional_annotations__ must be a set, not %T", - set_o); + "'%T' object is not a set", set_o); stack_pointer = _PyFrame_GetStackPointer(frame); stack_pointer += -1; assert(WITHIN_STACK_BOUNDS()); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index a36d72c6c549f19..6269016801264db 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -10860,11 +10860,10 @@ v = stack_pointer[-1]; set = stack_pointer[-2 - (oparg-1)]; PyObject *set_o = PyStackRef_AsPyObjectBorrow(set); - if (!PySet_Check(set_o)) { + if (!PySet_CheckExact(set_o)) { _PyFrame_SetStackPointer(frame, stack_pointer); _PyErr_Format(tstate, PyExc_TypeError, - "__conditional_annotations__ must be a set, not %T", - set_o); + "'%T' object is not a set", set_o); stack_pointer = _PyFrame_GetStackPointer(frame); stack_pointer += -1; assert(WITHIN_STACK_BOUNDS());