Skip to content

Commit 562ff73

Browse files
miss-islingtonbharelcolesbury
authored
[3.12] gh-123321: Fix Parser/myreadline.c to prevent a segfault during a multi-threaded race (GH-123323) (#123677)
* gh-123321: Fix Parser/myreadline.c to prevent a segfault during a multi-threaded race (GH-123323) (cherry picked from commit a4562fe) Co-authored-by: Bar Harel <bharel@barharel.com> * Remove @requires_gil_enabled for 3.12 --------- Co-authored-by: Bar Harel <bharel@barharel.com> Co-authored-by: Sam Gross <colesbury@gmail.com>
1 parent c3a866c commit 562ff73

File tree

3 files changed

+37
-3
lines changed

3 files changed

+37
-3
lines changed

Lib/test/test_readline.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
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')
@@ -346,6 +347,30 @@ def test_history_size(self):
346347
self.assertEqual(len(lines), history_size)
347348
self.assertEqual(lines[-1].strip(), b"last input")
348349

350+
@requires_working_threading()
351+
def test_gh123321_threadsafe(self):
352+
"""gh-123321: readline should be thread-safe and not crash"""
353+
script = textwrap.dedent(r"""
354+
import threading
355+
from test.support.threading_helper import join_thread
356+
357+
def func():
358+
input()
359+
360+
thread1 = threading.Thread(target=func)
361+
thread2 = threading.Thread(target=func)
362+
thread1.start()
363+
thread2.start()
364+
join_thread(thread1)
365+
join_thread(thread2)
366+
print("done")
367+
""")
368+
369+
output = run_pty(script, input=b"input1\rinput2\r")
370+
371+
self.assertIn(b"done", output)
372+
373+
349374
def test_write_read_limited_history(self):
350375
previous_length = readline.get_history_length()
351376
self.addCleanup(readline.set_history_length, previous_length)
Lines changed: 2 additions & 0 deletions
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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -386,9 +386,14 @@ PyOS_Readline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt)
386386
}
387387
}
388388

389-
_PyOS_ReadlineTState = tstate;
390389
Py_BEGIN_ALLOW_THREADS
390+
391+
// GH-123321: We need to acquire the lock before setting
392+
// _PyOS_ReadlineTState and after the release of the GIL, otherwise
393+
// the variable may be nullified by a different thread or a deadlock
394+
// may occur if the GIL is taken in any sub-function.
391395
PyThread_acquire_lock(_PyOS_ReadlineLock, 1);
396+
_PyOS_ReadlineTState = tstate;
392397

393398
/* This is needed to handle the unlikely case that the
394399
* interpreter is in interactive mode *and* stdin/out are not
@@ -412,11 +417,13 @@ PyOS_Readline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt)
412417
else {
413418
rv = (*PyOS_ReadlineFunctionPointer)(sys_stdin, sys_stdout, prompt);
414419
}
415-
Py_END_ALLOW_THREADS
416420

421+
// gh-123321: Must set the variable and then release the lock before
422+
// taking the GIL. Otherwise a deadlock or segfault may occur.
423+
_PyOS_ReadlineTState = NULL;
417424
PyThread_release_lock(_PyOS_ReadlineLock);
418425

419-
_PyOS_ReadlineTState = NULL;
426+
Py_END_ALLOW_THREADS
420427

421428
if (rv == NULL)
422429
return NULL;

0 commit comments

Comments
 (0)