From bf54c38b895e6c624d2692e5df251141aa340bcc Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 14 Oct 2026 12:03:43 -0700 Subject: [PATCH] gh-159434: Avoid borrowed references in func_get_annotation_dict() Calling a function's __annotate__ can run arbitrary code that replaces __annotate__ or __annotations__, either directly or, on the free-threaded build, from another thread while the critical section is suspended. The callable was used through a borrowed reference for the duration of the call, and the compiler-generated annotations tuple was read through a borrowed reference while building the dict. Hold strong references to both. If __annotations__ was set while __annotate__ was running, keep that value instead of overwriting it with the result of the call. --- Lib/test/test_type_annotations.py | 12 ++++++++ ...-10-14-19-16-53.gh-issue-159434.T4AsrQ.rst | 4 +++ Objects/funcobject.c | 28 +++++++++++++++---- 3 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-14-19-16-53.gh-issue-159434.T4AsrQ.rst diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 54db123a5dc2bf..cb164ccd147127 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -328,6 +328,18 @@ def check_annotations(self, f): f.__annotations__ = {"z": 43} self.assertIs(f.__annotate__, None) + def test_annotate_sets_annotations(self): + # If __annotate__ sets the function's annotations while it runs, + # those are kept instead of the result of the call. + def f(): + pass + def annotate(format): + f.__annotations__ = {"x": int} + return {"y": str} + f.__annotate__ = annotate + self.assertEqual(f.__annotations__, {"x": int}) + self.assertIs(f.__annotate__, None) + def test_user_defined_annotate(self): class X: a: int diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-14-19-16-53.gh-issue-159434.T4AsrQ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-14-19-16-53.gh-issue-159434.T4AsrQ.rst new file mode 100644 index 00000000000000..f7d7fbffa3371e --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-14-19-16-53.gh-issue-159434.T4AsrQ.rst @@ -0,0 +1,4 @@ +Fix reading a function's :attr:`~function.__annotations__` using borrowed +references to :attr:`~function.__annotate__` and the internal annotations +tuple while running code that can replace them. If ``__annotations__`` is +set while ``__annotate__`` runs, the newly set value is now kept. diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 4d3da234f87e49..cace408427f4db 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -539,11 +539,18 @@ static PyObject * func_get_annotation_dict(PyFunctionObject *op) { if (op->func_annotations == NULL) { - if (op->func_annotate == NULL || !PyCallable_Check(op->func_annotate)) { + PyObject *annotate = op->func_annotate; + if (annotate == NULL || !PyCallable_Check(annotate)) { Py_RETURN_NONE; } + // Calling __annotate__ can run arbitrary code which may replace + // func_annotate or func_annotations, either directly or from another + // thread while the critical section is suspended. Keep the callable + // alive during the call. + Py_INCREF(annotate); PyObject *one = _PyLong_GetOne(); - PyObject *ann_dict = _PyObject_CallOneArg(op->func_annotate, one); + PyObject *ann_dict = _PyObject_CallOneArg(annotate, one); + Py_DECREF(annotate); if (ann_dict == NULL) { return NULL; } @@ -554,13 +561,22 @@ func_get_annotation_dict(PyFunctionObject *op) Py_DECREF(ann_dict); return NULL; } - Py_XSETREF(op->func_annotations, ann_dict); + if (op->func_annotations != NULL) { + // Annotations were set during the call, keep them. + Py_DECREF(ann_dict); + assert(PyDict_Check(op->func_annotations)); + return op->func_annotations; + } + op->func_annotations = ann_dict; return ann_dict; } if (PyTuple_CheckExact(op->func_annotations)) { - PyObject *ann_tuple = op->func_annotations; + // Keep the tuple alive while building the dict, since it can be + // replaced if the critical section is suspended. + PyObject *ann_tuple = Py_NewRef(op->func_annotations); PyObject *ann_dict = PyDict_New(); if (ann_dict == NULL) { + Py_DECREF(ann_tuple); return NULL; } @@ -573,10 +589,12 @@ func_get_annotation_dict(PyFunctionObject *op) if (err < 0) { Py_DECREF(ann_dict); + Py_DECREF(ann_tuple); return NULL; } } - Py_SETREF(op->func_annotations, ann_dict); + Py_XSETREF(op->func_annotations, ann_dict); + Py_DECREF(ann_tuple); } assert(PyDict_Check(op->func_annotations)); return op->func_annotations;