Skip to content

Commit 049ae93

Browse files
committed
[lldb] Fix that the embedded Python REPL crashes if it receives SIGINT
When LLDB receives a SIGINT while running the embedded Python REPL it currently just crashes in ScriptInterpreterPythonImpl::Interrupt with an error such as the one below: Fatal Python error: PyThreadState_Get: the function must be called with the GIL held, but the GIL is released (the current Python thread state is NULL) The faulty code that causes this error is this part of ScriptInterpreterPythonImpl::Interrupt: PyThreadState *state = PyThreadState_GET(); if (!state) state = GetThreadState(); if (state) { long tid = state->thread_id; PyThreadState_Swap(state); int num_threads = PyThreadState_SetAsyncExc(tid, PyExc_KeyboardInterrupt); The obvious fix I tried is to just acquire the GIL before this code is running which fixes the crash but the KeyboardInterrupt we want to raise immediately is actually just queued and would only be raised once the next line of input has been parsed (which e.g. won't interrupt Python code that is currently waiting on a timer or IO from what I can see). Also none of the functions we call here is marked as safe to be called from a signal handler from what I can see, so we might still end up crashing here with some bad timing. Python 3.2 introduced PyErr_SetInterrupt to solve this and the function takes care of all the details and avoids doing anything that isn't safe to do inside a signal handler. The only thing we need to do is to manually setup our own fake SIGINT handler that behaves the same way as the standalone Python REPL signal handler (which raises a KeyboardInterrupt). From what I understand the old code used to work with Python 2 so I kept the old code around until we officially drop support for Python 2. There is a small gap here with Python 3.0->3.1 where we might still be crashing, but those versions have reached their EOL more than a decade ago so I think we don't need to bother about them. Differential revision: https://reviews.llvm.org/D104886
1 parent ba37c3b commit 049ae93

File tree

2 files changed

+143
-0
lines changed

2 files changed

+143
-0
lines changed

lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ extern "C" void init_lldb(void);
7070
#define LLDBSwigPyInit init_lldb
7171
#endif
7272

73+
#if defined(_WIN32)
74+
// Don't mess with the signal handlers on Windows.
75+
#define LLDB_USE_PYTHON_SET_INTERRUPT 0
76+
#else
77+
// PyErr_SetInterrupt was introduced in 3.2.
78+
#define LLDB_USE_PYTHON_SET_INTERRUPT \
79+
(PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION >= 2) || (PY_MAJOR_VERSION > 3)
80+
#endif
7381

7482
static ScriptInterpreterPythonImpl *GetPythonInterpreter(Debugger &debugger) {
7583
ScriptInterpreter *script_interpreter =
@@ -920,6 +928,22 @@ void ScriptInterpreterPythonImpl::ExecuteInterpreterLoop() {
920928
}
921929

922930
bool ScriptInterpreterPythonImpl::Interrupt() {
931+
#if LLDB_USE_PYTHON_SET_INTERRUPT
932+
// If the interpreter isn't evaluating any Python at the moment then return
933+
// false to signal that this function didn't handle the interrupt and the
934+
// next component should try handling it.
935+
if (!IsExecutingPython())
936+
return false;
937+
938+
// Tell Python that it should pretend to have received a SIGINT.
939+
PyErr_SetInterrupt();
940+
// PyErr_SetInterrupt has no way to return an error so we can only pretend the
941+
// signal got successfully handled and return true.
942+
// Python 3.10 introduces PyErr_SetInterruptEx that could return an error, but
943+
// the error handling is limited to checking the arguments which would be
944+
// just our (hardcoded) input signal code SIGINT, so that's not useful at all.
945+
return true;
946+
#else
923947
Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_SCRIPT));
924948

925949
if (IsExecutingPython()) {
@@ -941,6 +965,7 @@ bool ScriptInterpreterPythonImpl::Interrupt() {
941965
"ScriptInterpreterPythonImpl::Interrupt() python code not running, "
942966
"can't interrupt");
943967
return false;
968+
#endif
944969
}
945970

946971
bool ScriptInterpreterPythonImpl::ExecuteOneLineWithReturn(
@@ -3144,6 +3169,30 @@ ScriptInterpreterPythonImpl::AcquireInterpreterLock() {
31443169
return py_lock;
31453170
}
31463171

3172+
#if LLDB_USE_PYTHON_SET_INTERRUPT
3173+
namespace {
3174+
/// Saves the current signal handler for the specified signal and restores
3175+
/// it at the end of the current scope.
3176+
struct RestoreSignalHandlerScope {
3177+
/// The signal handler.
3178+
struct sigaction m_prev_handler;
3179+
int m_signal_code;
3180+
RestoreSignalHandlerScope(int signal_code) : m_signal_code(signal_code) {
3181+
// Initialize sigaction to their default state.
3182+
std::memset(&m_prev_handler, 0, sizeof(m_prev_handler));
3183+
// Don't install a new handler, just read back the old one.
3184+
struct sigaction *new_handler = nullptr;
3185+
int signal_err = ::sigaction(m_signal_code, new_handler, &m_prev_handler);
3186+
lldbassert(signal_err == 0 && "sigaction failed to read handler");
3187+
}
3188+
~RestoreSignalHandlerScope() {
3189+
int signal_err = ::sigaction(m_signal_code, &m_prev_handler, nullptr);
3190+
lldbassert(signal_err == 0 && "sigaction failed to restore old handler");
3191+
}
3192+
};
3193+
} // namespace
3194+
#endif
3195+
31473196
void ScriptInterpreterPythonImpl::InitializePrivate() {
31483197
if (g_initialized)
31493198
return;
@@ -3179,6 +3228,25 @@ void ScriptInterpreterPythonImpl::InitializePrivate() {
31793228
"lldb.embedded_interpreter; from "
31803229
"lldb.embedded_interpreter import run_python_interpreter; "
31813230
"from lldb.embedded_interpreter import run_one_line");
3231+
3232+
#if LLDB_USE_PYTHON_SET_INTERRUPT
3233+
// Python will not just overwrite its internal SIGINT handler but also the
3234+
// one from the process. Backup the current SIGINT handler to prevent that
3235+
// Python deletes it.
3236+
RestoreSignalHandlerScope save_sigint(SIGINT);
3237+
3238+
// Setup a default SIGINT signal handler that works the same way as the
3239+
// normal Python REPL signal handler which raises a KeyboardInterrupt.
3240+
// Also make sure to not pollute the user's REPL with the signal module nor
3241+
// our utility function.
3242+
PyRun_SimpleString("def lldb_setup_sigint_handler():\n"
3243+
" import signal;\n"
3244+
" def signal_handler(sig, frame):\n"
3245+
" raise KeyboardInterrupt()\n"
3246+
" signal.signal(signal.SIGINT, signal_handler);\n"
3247+
"lldb_setup_sigint_handler();\n"
3248+
"del lldb_setup_sigint_handler\n");
3249+
#endif
31823250
}
31833251

31843252
void ScriptInterpreterPythonImpl::AddToSysPath(AddLocation location,
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""
2+
Test sending SIGINT to the embedded Python REPL.
3+
"""
4+
5+
import os
6+
7+
import lldb
8+
from lldbsuite.test.decorators import *
9+
from lldbsuite.test.lldbtest import *
10+
from lldbsuite.test.lldbpexpect import PExpectTest
11+
12+
class TestCase(PExpectTest):
13+
14+
mydir = TestBase.compute_mydir(__file__)
15+
16+
def start_python_repl(self):
17+
""" Starts up the embedded Python REPL."""
18+
self.launch()
19+
# Start the embedded Python REPL via the 'script' command.
20+
self.child.send("script -l python --\n")
21+
# Wait for the Python REPL prompt.
22+
self.child.expect(">>>")
23+
24+
# PExpect uses many timeouts internally and doesn't play well
25+
# under ASAN on a loaded machine..
26+
@skipIfAsan
27+
@skipIfWindows
28+
def test_while_evaluating_code(self):
29+
""" Tests SIGINT handling while Python code is being evaluated."""
30+
self.start_python_repl()
31+
32+
# Start a long-running command that we try to abort with SIGINT.
33+
# Note that we dont actually wait 10000s in this code as pexpect or
34+
# lit will kill the test way before that.
35+
self.child.send("import time; print('running' + 'now'); time.sleep(10000);\n")
36+
37+
# Make sure the command is actually being evaluated at the moment by
38+
# looking at the string that the command is printing.
39+
# Don't check for a needle that also occurs in the program itself to
40+
# prevent that echoing will make this check pass unintentionally.
41+
self.child.expect("runningnow")
42+
43+
# Send SIGINT to the LLDB process.
44+
self.child.sendintr()
45+
46+
# This should get transformed to a KeyboardInterrupt which is the same
47+
# behaviour as the standalone Python REPL. It should also interrupt
48+
# the evaluation of our sleep statement.
49+
self.child.expect("KeyboardInterrupt")
50+
# Send EOF to quit the Python REPL.
51+
self.child.sendeof()
52+
53+
self.quit()
54+
55+
# PExpect uses many timeouts internally and doesn't play well
56+
# under ASAN on a loaded machine..
57+
@skipIfAsan
58+
# FIXME: On Linux the Python code that reads from stdin seems to block until
59+
# it has finished reading a line before handling any queued signals.
60+
@skipIfLinux
61+
@skipIfWindows
62+
def test_while_waiting_on_input(self):
63+
""" Tests SIGINT handling while the REPL is waiting on input from
64+
stdin."""
65+
self.start_python_repl()
66+
67+
# Send SIGINT to the LLDB process.
68+
self.child.sendintr()
69+
# This should get transformed to a KeyboardInterrupt which is the same
70+
# behaviour as the standalone Python REPL.
71+
self.child.expect("KeyboardInterrupt")
72+
# Send EOF to quit the Python REPL.
73+
self.child.sendeof()
74+
75+
self.quit()

0 commit comments

Comments
 (0)