Skip to content

Commit 80af1eb

Browse files
committed
gh-153291: Fix data race in readline.get_completer() and get_pre_input_hook()
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).
1 parent fc19ad7 commit 80af1eb

4 files changed

Lines changed: 77 additions & 5 deletions

File tree

Lib/test/test_readline.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,22 @@ def my_hook():
431431
readline.set_pre_input_hook(my_hook)
432432
self.assertIs(readline.get_pre_input_hook(), my_hook)
433433

434+
def test_get_completer(self):
435+
# Save and restore the original completer to avoid side effects
436+
original_completer = readline.get_completer()
437+
self.addCleanup(readline.set_completer, original_completer)
438+
439+
# Test that get_completer returns None when no completer is set
440+
readline.set_completer(None)
441+
self.assertIsNone(readline.get_completer())
442+
443+
# Set a completer and verify we can retrieve it
444+
def my_completer(text, state):
445+
return None
446+
447+
readline.set_completer(my_completer)
448+
self.assertIs(readline.get_completer(), my_completer)
449+
434450

435451
@unittest.skipUnless(support.Py_GIL_DISABLED, 'these tests can only possibly fail with GIL disabled')
436452
class FreeThreadingTest(unittest.TestCase):
@@ -452,6 +468,44 @@ def completer_delims(b):
452468
with threading_helper.start_threads(threads):
453469
pass
454470

471+
@threading_helper.reap_threads
472+
@threading_helper.requires_working_threading()
473+
def test_free_threading_get_completer(self):
474+
def completer(b):
475+
b.wait()
476+
for _ in range(100):
477+
readline.get_completer()
478+
readline.set_completer(lambda text, state: None)
479+
readline.set_completer(None)
480+
readline.get_completer()
481+
482+
count = 40
483+
barrier = threading.Barrier(count)
484+
threads = [threading.Thread(target=completer, args=(barrier,)) for _ in range(count)]
485+
486+
with threading_helper.start_threads(threads):
487+
pass
488+
489+
@unittest.skipUnless(hasattr(readline, "get_pre_input_hook"),
490+
"get_pre_input_hook not available")
491+
@threading_helper.reap_threads
492+
@threading_helper.requires_working_threading()
493+
def test_free_threading_get_pre_input_hook(self):
494+
def pre_input_hook(b):
495+
b.wait()
496+
for _ in range(100):
497+
readline.get_pre_input_hook()
498+
readline.set_pre_input_hook(lambda: None)
499+
readline.set_pre_input_hook(None)
500+
readline.get_pre_input_hook()
501+
502+
count = 40
503+
barrier = threading.Barrier(count)
504+
threads = [threading.Thread(target=pre_input_hook, args=(barrier,)) for _ in range(count)]
505+
506+
with threading_helper.start_threads(threads):
507+
pass
508+
455509

456510
if __name__ == "__main__":
457511
unittest.main()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix a data race in :func:`readline.get_completer` and
2+
:func:`readline.get_pre_input_hook` on the :term:`free-threaded <free
3+
threading>` build: the getters read the stored hook without the critical
4+
section that the corresponding setters hold.

Modules/clinic/readline.c.h

Lines changed: 15 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/readline.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -578,14 +578,15 @@ readline_set_pre_input_hook_impl(PyObject *module, PyObject *function)
578578
/* Get pre-input hook */
579579

580580
/*[clinic input]
581+
@critical_section
581582
readline.get_pre_input_hook
582583
583584
Get the current pre-input hook function.
584585
[clinic start generated code]*/
585586

586587
static PyObject *
587588
readline_get_pre_input_hook_impl(PyObject *module)
588-
/*[clinic end generated code: output=ad56b77a8e8981ca input=fb1e1b1fbd94e4e5]*/
589+
/*[clinic end generated code: output=ad56b77a8e8981ca input=fbbf0106bd015414]*/
589590
{
590591
readlinestate *state = get_readline_state(module);
591592
if (state->pre_input_hook == NULL) {
@@ -886,14 +887,15 @@ readline_set_completer_impl(PyObject *module, PyObject *function)
886887
}
887888

888889
/*[clinic input]
890+
@critical_section
889891
readline.get_completer
890892
891893
Get the current completer function.
892894
[clinic start generated code]*/
893895

894896
static PyObject *
895897
readline_get_completer_impl(PyObject *module)
896-
/*[clinic end generated code: output=6e6bbd8226d14475 input=6457522e56d70d13]*/
898+
/*[clinic end generated code: output=6e6bbd8226d14475 input=0df9ba4107115c44]*/
897899
{
898900
readlinestate *state = get_readline_state(module);
899901
if (state->completer == NULL) {

0 commit comments

Comments
 (0)