https://github.com/python/cpython/commit/7dca47d7fade29d8d7e6725ed9782682cb9703ce
commit: 7dca47d7fade29d8d7e6725ed9782682cb9703ce
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: nascheme <[email protected]>
date: 2026-07-22T10:54:26-07:00
summary:

[3.15] gh-153291: Fix data race in readline.get_completer() and 
get_pre_input_hook() (gh-153362) (gh-154418)

The setters store these hooks while holding the module critical section
(via set_hook's Py_XSETREF), but the getters read and Py_NewRef the same
fields without it. Annotate both getters with @critical_section, matching
the other readline functions (gh-126895).

(cherry picked from commit 90b6a7992ae0ce6959eaa6b6401105582d866037)

Co-authored-by: Harjoth Khara <[email protected]>
Co-authored-by: Neil Schemenauer <[email protected]>

files:
A Lib/test/test_free_threading/test_readline.py
A Misc/NEWS.d/next/Library/2026-07-08-00-00-00.gh-issue-153291.rdlnCS.rst
M Lib/test/test_readline.py
M Modules/clinic/readline.c.h
M Modules/readline.c

diff --git a/Lib/test/test_free_threading/test_readline.py 
b/Lib/test/test_free_threading/test_readline.py
new file mode 100644
index 000000000000000..f7aa9894031495c
--- /dev/null
+++ b/Lib/test/test_free_threading/test_readline.py
@@ -0,0 +1,59 @@
+import unittest
+
+from test.support import import_helper
+from test.support import threading_helper
+
+readline = import_helper.import_module("readline")
+
+
+@threading_helper.requires_working_threading()
+class TestReadlineRaces(unittest.TestCase):
+    def test_completer_delims_get_set(self):
+        def worker():
+            for _ in range(100):
+                readline.get_completer_delims()
+                readline.set_completer_delims(
+                    ' \t\n`@#%^&*()=+[{]}\\|;:\'",<>?')
+                readline.set_completer_delims(
+                    ' \t\n`@#%^&*()=+[{]}\\|;:\'",<>?')
+                readline.get_completer_delims()
+
+        threading_helper.run_concurrently(worker, nthreads=40)
+
+    # get_completer()/get_pre_input_hook() must take the module critical
+    # section like their setters do; otherwise reading and Py_NewRef-ing the
+    # stored hook races the setter replacing it (gh-153291).
+
+    def test_completer_get_set(self):
+        def setter():
+            for _ in range(1000):
+                readline.set_completer(lambda text, state: None)
+                readline.set_completer(None)
+
+        def getter():
+            for _ in range(1000):
+                readline.get_completer()
+
+        original = readline.get_completer()
+        self.addCleanup(readline.set_completer, original)
+        threading_helper.run_concurrently([setter] * 2 + [getter] * 6)
+
+    @unittest.skipUnless(hasattr(readline, "set_pre_input_hook"),
+                         "needs readline.set_pre_input_hook")
+    def test_pre_input_hook_get_set(self):
+        def setter():
+            for _ in range(1000):
+                readline.set_pre_input_hook(lambda: None)
+                readline.set_pre_input_hook(None)
+
+        def getter():
+            for _ in range(1000):
+                readline.get_pre_input_hook()
+
+        original = readline.get_pre_input_hook()
+        self.addCleanup(readline.set_pre_input_hook, original)
+        threading_helper.run_concurrently([setter] * 2 + [getter] * 6)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/Lib/test/test_readline.py b/Lib/test/test_readline.py
index 3982686dd10aecf..430996937bc21a2 100644
--- a/Lib/test/test_readline.py
+++ b/Lib/test/test_readline.py
@@ -7,10 +7,7 @@
 import sys
 import tempfile
 import textwrap
-import threading
 import unittest
-from test import support
-from test.support import threading_helper
 from test.support import verbose
 from test.support.import_helper import import_module
 from test.support.os_helper import unlink, temp_dir, TESTFN
@@ -432,26 +429,5 @@ def my_hook():
         self.assertIs(readline.get_pre_input_hook(), my_hook)
 
 
[email protected](support.Py_GIL_DISABLED, 'these tests can only possibly 
fail with GIL disabled')
-class FreeThreadingTest(unittest.TestCase):
-    @threading_helper.reap_threads
-    @threading_helper.requires_working_threading()
-    def test_free_threading(self):
-        def completer_delims(b):
-            b.wait()
-            for _ in range(100):
-                readline.get_completer_delims()
-                readline.set_completer_delims(' 
\t\n`@#%^&*()=+[{]}\\|;:\'",<>?')
-                readline.set_completer_delims(' 
\t\n`@#%^&*()=+[{]}\\|;:\'",<>?')
-                readline.get_completer_delims()
-
-        count   = 40
-        barrier = threading.Barrier(count)
-        threads = [threading.Thread(target=completer_delims, args=(barrier,)) 
for _ in range(count)]
-
-        with threading_helper.start_threads(threads):
-            pass
-
-
 if __name__ == "__main__":
     unittest.main()
diff --git 
a/Misc/NEWS.d/next/Library/2026-07-08-00-00-00.gh-issue-153291.rdlnCS.rst 
b/Misc/NEWS.d/next/Library/2026-07-08-00-00-00.gh-issue-153291.rdlnCS.rst
new file mode 100644
index 000000000000000..dbe3cd417e0d607
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-08-00-00-00.gh-issue-153291.rdlnCS.rst
@@ -0,0 +1,4 @@
+Fix a data race in :func:`readline.get_completer` and
+:func:`readline.get_pre_input_hook` on the :term:`free-threaded <free
+threading>` build: the getters read the stored hook without the critical
+section that the corresponding setters hold.
diff --git a/Modules/clinic/readline.c.h b/Modules/clinic/readline.c.h
index dc9381e4b976acd..a7df757f3f6c85c 100644
--- a/Modules/clinic/readline.c.h
+++ b/Modules/clinic/readline.c.h
@@ -366,7 +366,13 @@ readline_get_pre_input_hook_impl(PyObject *module);
 static PyObject *
 readline_get_pre_input_hook(PyObject *module, PyObject *Py_UNUSED(ignored))
 {
-    return readline_get_pre_input_hook_impl(module);
+    PyObject *return_value = NULL;
+
+    Py_BEGIN_CRITICAL_SECTION(module);
+    return_value = readline_get_pre_input_hook_impl(module);
+    Py_END_CRITICAL_SECTION();
+
+    return return_value;
 }
 
 #endif /* defined(HAVE_RL_PRE_INPUT_HOOK) */
@@ -651,7 +657,13 @@ readline_get_completer_impl(PyObject *module);
 static PyObject *
 readline_get_completer(PyObject *module, PyObject *Py_UNUSED(ignored))
 {
-    return readline_get_completer_impl(module);
+    PyObject *return_value = NULL;
+
+    Py_BEGIN_CRITICAL_SECTION(module);
+    return_value = readline_get_completer_impl(module);
+    Py_END_CRITICAL_SECTION();
+
+    return return_value;
 }
 
 PyDoc_STRVAR(readline_get_history_item__doc__,
@@ -823,4 +835,4 @@ readline_redisplay(PyObject *module, PyObject 
*Py_UNUSED(ignored))
 #ifndef READLINE_CLEAR_HISTORY_METHODDEF
     #define READLINE_CLEAR_HISTORY_METHODDEF
 #endif /* !defined(READLINE_CLEAR_HISTORY_METHODDEF) */
-/*[clinic end generated code: output=4bd95070973cd0e2 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=acf4e4c35191cf09 input=a9049054013a1b77]*/
diff --git a/Modules/readline.c b/Modules/readline.c
index c580d2022fccf3d..9d4904d0085b8e5 100644
--- a/Modules/readline.c
+++ b/Modules/readline.c
@@ -578,6 +578,7 @@ readline_set_pre_input_hook_impl(PyObject *module, PyObject 
*function)
 /* Get pre-input hook */
 
 /*[clinic input]
+@critical_section
 readline.get_pre_input_hook
 
 Get the current pre-input hook function.
@@ -585,7 +586,7 @@ Get the current pre-input hook function.
 
 static PyObject *
 readline_get_pre_input_hook_impl(PyObject *module)
-/*[clinic end generated code: output=ad56b77a8e8981ca input=fb1e1b1fbd94e4e5]*/
+/*[clinic end generated code: output=ad56b77a8e8981ca input=fbbf0106bd015414]*/
 {
     readlinestate *state = get_readline_state(module);
     if (state->pre_input_hook == NULL) {
@@ -886,6 +887,7 @@ readline_set_completer_impl(PyObject *module, PyObject 
*function)
 }
 
 /*[clinic input]
+@critical_section
 readline.get_completer
 
 Get the current completer function.
@@ -893,7 +895,7 @@ Get the current completer function.
 
 static PyObject *
 readline_get_completer_impl(PyObject *module)
-/*[clinic end generated code: output=6e6bbd8226d14475 input=6457522e56d70d13]*/
+/*[clinic end generated code: output=6e6bbd8226d14475 input=0df9ba4107115c44]*/
 {
     readlinestate *state = get_readline_state(module);
     if (state->completer == NULL) {

_______________________________________________
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