Skip to content

Commit e10c9de

Browse files
authored
bpo-20891: Fix PyGILState_Ensure() (#4650) (#4655)
When PyGILState_Ensure() is called in a non-Python thread before PyEval_InitThreads(), only call PyEval_InitThreads() after calling PyThreadState_New() to fix a crash. Add an unit test in test_embed. Enhance also embedded tests, backport from master: * Add test_pre_initialization_api() * Set PYTHONIOENCODING environment variable in test_forced_io_encoding() (cherry picked from commit b4d1e1f)
1 parent 29cb50b commit e10c9de

File tree

4 files changed

+174
-26
lines changed

4 files changed

+174
-26
lines changed

Lib/test/test_capi.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -401,23 +401,30 @@ def setUp(self):
401401
def tearDown(self):
402402
os.chdir(self.oldcwd)
403403

404-
def run_embedded_interpreter(self, *args):
404+
def run_embedded_interpreter(self, *args, env=None):
405405
"""Runs a test in the embedded interpreter"""
406406
cmd = [self.test_exe]
407407
cmd.extend(args)
408+
if env is not None and sys.platform == 'win32':
409+
# Windows requires at least the SYSTEMROOT environment variable to
410+
# start Python.
411+
env = env.copy()
412+
env['SYSTEMROOT'] = os.environ['SYSTEMROOT']
413+
408414
p = subprocess.Popen(cmd,
409415
stdout=subprocess.PIPE,
410416
stderr=subprocess.PIPE,
411-
universal_newlines=True)
417+
universal_newlines=True,
418+
env=env)
412419
(out, err) = p.communicate()
413420
self.assertEqual(p.returncode, 0,
414421
"bad returncode %d, stderr is %r" %
415422
(p.returncode, err))
416423
return out, err
417424

418-
def test_subinterps(self):
425+
def test_repeated_init_and_subinterpreters(self):
419426
# This is just a "don't crash" test
420-
out, err = self.run_embedded_interpreter()
427+
out, err = self.run_embedded_interpreter('repeated_init_and_subinterpreters')
421428
if support.verbose:
422429
print()
423430
print(out)
@@ -435,13 +442,14 @@ def _get_default_pipe_encoding():
435442

436443
def test_forced_io_encoding(self):
437444
# Checks forced configuration of embedded interpreter IO streams
438-
out, err = self.run_embedded_interpreter("forced_io_encoding")
445+
env = dict(os.environ, PYTHONIOENCODING="utf-8:surrogateescape")
446+
out, err = self.run_embedded_interpreter("forced_io_encoding", env=env)
439447
if support.verbose:
440448
print()
441449
print(out)
442450
print(err)
443-
expected_errors = sys.__stdout__.errors
444-
expected_stdin_encoding = sys.__stdin__.encoding
451+
expected_stream_encoding = "utf-8"
452+
expected_errors = "surrogateescape"
445453
expected_pipe_encoding = self._get_default_pipe_encoding()
446454
expected_output = '\n'.join([
447455
"--- Use defaults ---",
@@ -469,13 +477,33 @@ def test_forced_io_encoding(self):
469477
"stdout: latin-1:replace",
470478
"stderr: latin-1:backslashreplace"])
471479
expected_output = expected_output.format(
472-
in_encoding=expected_stdin_encoding,
473-
out_encoding=expected_pipe_encoding,
480+
in_encoding=expected_stream_encoding,
481+
out_encoding=expected_stream_encoding,
474482
errors=expected_errors)
475483
# This is useful if we ever trip over odd platform behaviour
476484
self.maxDiff = None
477485
self.assertEqual(out.strip(), expected_output)
478486

487+
def test_pre_initialization_api(self):
488+
"""
489+
Checks the few parts of the C-API that work before the runtine
490+
is initialized (via Py_Initialize()).
491+
"""
492+
env = dict(os.environ, PYTHONPATH=os.pathsep.join(sys.path))
493+
out, err = self.run_embedded_interpreter("pre_initialization_api", env=env)
494+
self.assertEqual(out, '')
495+
self.assertEqual(err, '')
496+
497+
def test_bpo20891(self):
498+
"""
499+
bpo-20891: Calling PyGILState_Ensure in a non-Python thread before
500+
calling PyEval_InitThreads() must not crash. PyGILState_Ensure() must
501+
call PyEval_InitThreads() for us in this case.
502+
"""
503+
out, err = self.run_embedded_interpreter("bpo20891")
504+
self.assertEqual(out, '')
505+
self.assertEqual(err, '')
506+
479507

480508
class SkipitemTest(unittest.TestCase):
481509

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix PyGILState_Ensure(). When PyGILState_Ensure() is called in a non-Python
2+
thread before PyEval_InitThreads(), only call PyEval_InitThreads() after
3+
calling PyThreadState_New() to fix a crash.

Programs/_testembed.c

Lines changed: 117 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <Python.h>
2+
#include "pythread.h"
23
#include <stdio.h>
34

45
/*********************************************************
@@ -33,7 +34,7 @@ static void print_subinterp(void)
3334
);
3435
}
3536

36-
static void test_repeated_init_and_subinterpreters(void)
37+
static int test_repeated_init_and_subinterpreters(void)
3738
{
3839
PyThreadState *mainstate, *substate;
3940
#ifdef WITH_THREAD
@@ -70,6 +71,7 @@ static void test_repeated_init_and_subinterpreters(void)
7071
PyEval_RestoreThread(mainstate);
7172
Py_Finalize();
7273
}
74+
return 0;
7375
}
7476

7577
/*****************************************************
@@ -103,7 +105,7 @@ static void check_stdio_details(const char *encoding, const char * errors)
103105
Py_Finalize();
104106
}
105107

106-
static void test_forced_io_encoding(void)
108+
static int test_forced_io_encoding(void)
107109
{
108110
/* Check various combinations */
109111
printf("--- Use defaults ---\n");
@@ -122,19 +124,123 @@ static void test_forced_io_encoding(void)
122124
printf("Unexpected success calling Py_SetStandardStreamEncoding");
123125
}
124126
Py_Finalize();
127+
return 0;
125128
}
126129

127-
/* Different embedding tests */
128-
int main(int argc, char *argv[])
130+
131+
/*********************************************************
132+
* Test parts of the C-API that work before initialization
133+
*********************************************************/
134+
135+
static int test_pre_initialization_api(void)
129136
{
137+
/* Leading "./" ensures getpath.c can still find the standard library */
138+
wchar_t *program = Py_DecodeLocale("./spam", NULL);
139+
if (program == NULL) {
140+
fprintf(stderr, "Fatal error: cannot decode program name\n");
141+
return 1;
142+
}
143+
Py_SetProgramName(program);
130144

131-
/* TODO: Check the argument string to allow for more test cases */
132-
if (argc > 1) {
133-
/* For now: assume "forced_io_encoding */
134-
test_forced_io_encoding();
135-
} else {
136-
/* Run the original embedding test case by default */
137-
test_repeated_init_and_subinterpreters();
145+
Py_Initialize();
146+
Py_Finalize();
147+
148+
PyMem_RawFree(program);
149+
return 0;
150+
}
151+
152+
static void bpo20891_thread(void *lockp)
153+
{
154+
PyThread_type_lock lock = *((PyThread_type_lock*)lockp);
155+
156+
PyGILState_STATE state = PyGILState_Ensure();
157+
if (!PyGILState_Check()) {
158+
fprintf(stderr, "PyGILState_Check failed!");
159+
abort();
160+
}
161+
162+
PyGILState_Release(state);
163+
164+
PyThread_release_lock(lock);
165+
166+
PyThread_exit_thread();
167+
}
168+
169+
static int test_bpo20891(void)
170+
{
171+
/* bpo-20891: Calling PyGILState_Ensure in a non-Python thread before
172+
calling PyEval_InitThreads() must not crash. PyGILState_Ensure() must
173+
call PyEval_InitThreads() for us in this case. */
174+
PyThread_type_lock lock = PyThread_allocate_lock();
175+
if (!lock) {
176+
fprintf(stderr, "PyThread_allocate_lock failed!");
177+
return 1;
178+
}
179+
180+
_testembed_Py_Initialize();
181+
182+
long thrd = PyThread_start_new_thread(bpo20891_thread, &lock);
183+
if (thrd == -1) {
184+
fprintf(stderr, "PyThread_start_new_thread failed!");
185+
return 1;
138186
}
187+
PyThread_acquire_lock(lock, WAIT_LOCK);
188+
189+
Py_BEGIN_ALLOW_THREADS
190+
/* wait until the thread exit */
191+
PyThread_acquire_lock(lock, WAIT_LOCK);
192+
Py_END_ALLOW_THREADS
193+
194+
PyThread_free_lock(lock);
195+
139196
return 0;
140197
}
198+
199+
200+
/* *********************************************************
201+
* List of test cases and the function that implements it.
202+
*
203+
* Names are compared case-sensitively with the first
204+
* argument. If no match is found, or no first argument was
205+
* provided, the names of all test cases are printed and
206+
* the exit code will be -1.
207+
*
208+
* The int returned from test functions is used as the exit
209+
* code, and test_capi treats all non-zero exit codes as a
210+
* failed test.
211+
*********************************************************/
212+
struct TestCase
213+
{
214+
const char *name;
215+
int (*func)(void);
216+
};
217+
218+
static struct TestCase TestCases[] = {
219+
{ "forced_io_encoding", test_forced_io_encoding },
220+
{ "repeated_init_and_subinterpreters", test_repeated_init_and_subinterpreters },
221+
{ "pre_initialization_api", test_pre_initialization_api },
222+
{ "bpo20891", test_bpo20891 },
223+
{ NULL, NULL }
224+
};
225+
226+
int main(int argc, char *argv[])
227+
{
228+
if (argc > 1) {
229+
for (struct TestCase *tc = TestCases; tc && tc->name; tc++) {
230+
if (strcmp(argv[1], tc->name) == 0)
231+
return (*tc->func)();
232+
}
233+
}
234+
235+
/* No match found, or no test name provided, so display usage */
236+
printf("Python " PY_VERSION " _testembed executable for embedded interpreter tests\n"
237+
"Normally executed via 'EmbeddingTests' in Lib/test/test_capi.py\n\n"
238+
"Usage: %s TESTNAME\n\nAll available tests:\n", argv[0]);
239+
for (struct TestCase *tc = TestCases; tc && tc->name; tc++) {
240+
printf(" %s\n", tc->name);
241+
}
242+
243+
/* Non-zero exit code will cause test_capi.py tests to fail.
244+
This is intentional. */
245+
return -1;
246+
}

Python/pystate.c

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,8 @@ PyGILState_Ensure(void)
865865
{
866866
int current;
867867
PyThreadState *tcur;
868+
int need_init_threads = 0;
869+
868870
/* Note that we do not auto-init Python here - apart from
869871
potential races with 2 threads auto-initializing, pep-311
870872
spells out other issues. Embedders are expected to have
@@ -873,10 +875,7 @@ PyGILState_Ensure(void)
873875
assert(autoInterpreterState); /* Py_Initialize() hasn't been called! */
874876
tcur = (PyThreadState *)PyThread_get_key_value(autoTLSkey);
875877
if (tcur == NULL) {
876-
/* At startup, Python has no concrete GIL. If PyGILState_Ensure() is
877-
called from a new thread for the first time, we need the create the
878-
GIL. */
879-
PyEval_InitThreads();
878+
need_init_threads = 1;
880879

881880
/* Create a new thread state for this thread */
882881
tcur = PyThreadState_New(autoInterpreterState);
@@ -887,16 +886,28 @@ PyGILState_Ensure(void)
887886
tcur->gilstate_counter = 0;
888887
current = 0; /* new thread state is never current */
889888
}
890-
else
889+
else {
891890
current = PyThreadState_IsCurrent(tcur);
892-
if (current == 0)
891+
}
892+
893+
if (current == 0) {
893894
PyEval_RestoreThread(tcur);
895+
}
896+
894897
/* Update our counter in the thread-state - no need for locks:
895898
- tcur will remain valid as we hold the GIL.
896899
- the counter is safe as we are the only thread "allowed"
897900
to modify this value
898901
*/
899902
++tcur->gilstate_counter;
903+
904+
if (need_init_threads) {
905+
/* At startup, Python has no concrete GIL. If PyGILState_Ensure() is
906+
called from a new thread for the first time, we need the create the
907+
GIL. */
908+
PyEval_InitThreads();
909+
}
910+
900911
return current ? PyGILState_LOCKED : PyGILState_UNLOCKED;
901912
}
902913

0 commit comments

Comments
 (0)