https://github.com/python/cpython/commit/af13832751892b9921db12cc2ff7500e2884b8e6
commit: af13832751892b9921db12cc2ff7500e2884b8e6
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2026-10-22T14:45:39Z
summary:

[3.15] gh-159739: Release replaced type alias values (GH-160167) (#160242)



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.
(cherry picked from commit d36a3f6970407bb91dd863a10f0b42425a7e954e)

Co-authored-by: Jelle Zijlstra <[email protected]>

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-19-55-00.gh-issue-159739.tali01.rst
M Lib/test/test_type_aliases.py
M Objects/typevarobject.c

diff --git a/Lib/test/test_type_aliases.py b/Lib/test/test_type_aliases.py
index 05bc6a80888a06..d80a7fbfd18796 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 00000000000000..a4f672fed87cfe
--- /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 398a63257beb4a..11ff8320fd9a48 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;
 }
 

_______________________________________________
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