From 4371b163756bb74a73939cabdbd680319617bf93 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 21 Oct 2026 12:55:54 -0700 Subject: [PATCH] gh-159739: Release replaced type alias values A type alias can be evaluated again before an earlier evaluation finishes, either recursively or in another thread. The later completion overwrites the cached value without releasing it, permanently leaking that value; synchronize cache access and release the replaced value outside the lock. Backport to 3.15 and 3.14. --- Lib/test/test_type_aliases.py | 21 ++++++++++++++++++- ...-10-21-19-55-00.gh-issue-159739.tali01.rst | 2 ++ Objects/typevarobject.c | 15 ++++++++++--- 3 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-19-55-00.gh-issue-159739.tali01.rst diff --git a/Lib/test/test_type_aliases.py b/Lib/test/test_type_aliases.py index 05bc6a80888a067..d80a7fbfd18796d 100644 --- a/Lib/test/test_type_aliases.py +++ b/Lib/test/test_type_aliases.py @@ -1,7 +1,8 @@ import pickle import types import unittest -from test.support import check_syntax_error, run_code +import weakref +from test.support import check_syntax_error, gc_collect, run_code from test.typinganndata import mod_generics_cache from typing import ( @@ -131,6 +132,24 @@ def more_generic[T, *Ts, **P](): T, Ts, P = more_generic.__type_params__ self.assertEqual(alias.__value__, tuple[Callable[P, tuple[T, *Ts]], Callable[P2, tuple[T2, *Ts2]]]) + def test_reentrant_value_does_not_leak(self): + class Value: + pass + + refs = [] + + def compute(): + value = Value() + refs.append(weakref.ref(value)) + if len(refs) == 1: + Alias.__value__ + return value + + type Alias = compute() + self.assertIs(Alias.__value__, refs[0]()) + gc_collect() + self.assertIsNone(refs[1]()) + def test_subscripting(self): type NonGeneric = int type Generic[A] = dict[A, A] diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-19-55-00.gh-issue-159739.tali01.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-19-55-00.gh-issue-159739.tali01.rst new file mode 100644 index 000000000000000..a4f672fed87cfe7 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-19-55-00.gh-issue-159739.tali01.rst @@ -0,0 +1,2 @@ +Fix a reference leak when the value of a type alias is evaluated +recursively or concurrently. diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index 398a63257beb4a2..11ff8320fd9a487 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -1899,14 +1899,23 @@ typealias_dealloc(PyObject *self) static PyObject * typealias_get_value(typealiasobject *ta) { - if (ta->value != NULL) { - return Py_NewRef(ta->value); + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(ta); + result = Py_XNewRef(ta->value); + Py_END_CRITICAL_SECTION(); + if (result != NULL) { + return result; } - PyObject *result = PyObject_CallNoArgs(ta->compute_value); + result = PyObject_CallNoArgs(ta->compute_value); if (result == NULL) { return NULL; } + PyObject *old_value; + Py_BEGIN_CRITICAL_SECTION(ta); + old_value = ta->value; ta->value = Py_NewRef(result); + Py_END_CRITICAL_SECTION(); + Py_XDECREF(old_value); return result; }