Skip to content

Commit a4562fe

Browse files
authored
gh-123321: Fix Parser/myreadline.c to prevent a segfault during a multi-threaded race (#123323)
1 parent c530ce1 commit a4562fe

File tree

3 files changed

+39
-4
lines changed

3 files changed

+39
-4
lines changed

Lib/test/test_readline.py

+27-1
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
import tempfile
88
import textwrap
99
import unittest
10-
from test.support import verbose
10+
from test.support import requires_gil_enabled, verbose
1111
from test.support.import_helper import import_module
1212
from test.support.os_helper import unlink, temp_dir, TESTFN
1313
from test.support.pty_helper import run_pty
1414
from test.support.script_helper import assert_python_ok
15+
from test.support.threading_helper import requires_working_threading
1516

1617
# Skip tests if there is no readline module
1718
readline = import_module('readline')
@@ -349,6 +350,31 @@ def test_history_size(self):
349350
self.assertEqual(len(lines), history_size)
350351
self.assertEqual(lines[-1].strip(), b"last input")
351352

353+
@requires_working_threading()
354+
@requires_gil_enabled()
355+
def test_gh123321_threadsafe(self):
356+
"""gh-123321: readline should be thread-safe and not crash"""
357+
script = textwrap.dedent(r"""
358+
import threading
359+
from test.support.threading_helper import join_thread
360+
361+
def func():
362+
input()
363+
364+
thread1 = threading.Thread(target=func)
365+
thread2 = threading.Thread(target=func)
366+
thread1.start()
367+
thread2.start()
368+
join_thread(thread1)
369+
join_thread(thread2)
370+
print("done")
371+
""")
372+
373+
output = run_pty(script, input=b"input1\rinput2\r")
374+
375+
self.assertIn(b"done", output)
376+
377+
352378
def test_write_read_limited_history(self):
353379
previous_length = readline.get_history_length()
354380
self.addCleanup(readline.set_history_length, previous_length)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Prevent Parser/myreadline race condition from segfaulting on multi-threaded
2+
use. Patch by Bar Harel and Amit Wienner.

Parser/myreadline.c

+10-3
Original file line numberDiff line numberDiff line change
@@ -392,9 +392,14 @@ PyOS_Readline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt)
392392
}
393393
}
394394

395-
_PyOS_ReadlineTState = tstate;
396395
Py_BEGIN_ALLOW_THREADS
396+
397+
// GH-123321: We need to acquire the lock before setting
398+
// _PyOS_ReadlineTState and after the release of the GIL, otherwise
399+
// the variable may be nullified by a different thread or a deadlock
400+
// may occur if the GIL is taken in any sub-function.
397401
PyThread_acquire_lock(_PyOS_ReadlineLock, 1);
402+
_PyOS_ReadlineTState = tstate;
398403

399404
/* This is needed to handle the unlikely case that the
400405
* interpreter is in interactive mode *and* stdin/out are not
@@ -418,11 +423,13 @@ PyOS_Readline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt)
418423
else {
419424
rv = (*PyOS_ReadlineFunctionPointer)(sys_stdin, sys_stdout, prompt);
420425
}
421-
Py_END_ALLOW_THREADS
422426

427+
// gh-123321: Must set the variable and then release the lock before
428+
// taking the GIL. Otherwise a deadlock or segfault may occur.
429+
_PyOS_ReadlineTState = NULL;
423430
PyThread_release_lock(_PyOS_ReadlineLock);
424431

425-
_PyOS_ReadlineTState = NULL;
432+
Py_END_ALLOW_THREADS
426433

427434
if (rv == NULL)
428435
return NULL;

0 commit comments

Comments
 (0)