Skip to content

Commit 2997fec

Browse files
Dino Viehlandned-deily
authored andcommitted
[3.6] bpo-30604: Move co_extra_freefuncs to interpreter state to avoid crashes in threads (#2015)
* Move co_extra_freefuncs to interpreter state to avoid crashes in multi-threaded scenarios involving deletion of code objects * Don't require that extra be zero initialized * Build test list instead of defining empty test class * Ensure extra is always assigned on success * Keep the old fields in the thread state object, just don't use them Add new linked list of code extra objects on a per-interpreter basis so that interpreter state size isn't changed * Rename __PyCodeExtraState_Get and add comment about it going away in 3.7 Fix sort order of import's in test_code.py * Remove an extraneous space * Remove docstrings for comments * Touch up formatting * Fix casing of coextra local * Fix casing of another variable * Prefix PyCodeExtraState with __ to match C API for getting it * Update NEWS file for bpo-30604
1 parent f59cac4 commit 2997fec

File tree

6 files changed

+180
-23
lines changed

6 files changed

+180
-23
lines changed

‎Include/pystate.h‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ typedef struct _is {
5151
} PyInterpreterState;
5252
#endif
5353

54+
typedef struct _co_extra_state {
55+
struct _co_extra_state *next;
56+
PyInterpreterState* interp;
57+
58+
Py_ssize_t co_extra_user_count;
59+
freefunc co_extra_freefuncs[MAX_CO_EXTRA_USERS];
60+
} __PyCodeExtraState;
61+
62+
/* This is temporary for backwards compat in 3.6 and will be removed in 3.7 */
63+
__PyCodeExtraState* __PyCodeExtraState_Get();
5464

5565
/* State unique per thread */
5666

@@ -142,8 +152,10 @@ typedef struct _ts {
142152
PyObject *coroutine_wrapper;
143153
int in_coroutine_wrapper;
144154

145-
Py_ssize_t co_extra_user_count;
146-
freefunc co_extra_freefuncs[MAX_CO_EXTRA_USERS];
155+
/* Now used from PyInterpreterState, kept here for ABI
156+
compatibility with PyThreadState */
157+
Py_ssize_t _preserve_36_ABI_1;
158+
freefunc _preserve_36_ABI_2[MAX_CO_EXTRA_USERS];
147159

148160
PyObject *async_gen_firstiter;
149161
PyObject *async_gen_finalizer;

‎Lib/test/test_code.py‎

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,11 @@
103103
"""
104104

105105
import sys
106+
import threading
106107
import unittest
107108
import weakref
108-
from test.support import run_doctest, run_unittest, cpython_only
109+
from test.support import (run_doctest, run_unittest, cpython_only,
110+
check_impl_detail)
109111

110112

111113
def consts(t):
@@ -212,11 +214,106 @@ def callback(code):
212214
self.assertTrue(self.called)
213215

214216

217+
if check_impl_detail(cpython=True):
218+
import ctypes
219+
py = ctypes.pythonapi
220+
freefunc = ctypes.CFUNCTYPE(None,ctypes.c_voidp)
221+
222+
RequestCodeExtraIndex = py._PyEval_RequestCodeExtraIndex
223+
RequestCodeExtraIndex.argtypes = (freefunc,)
224+
RequestCodeExtraIndex.restype = ctypes.c_ssize_t
225+
226+
SetExtra = py._PyCode_SetExtra
227+
SetExtra.argtypes = (ctypes.py_object, ctypes.c_ssize_t, ctypes.c_voidp)
228+
SetExtra.restype = ctypes.c_int
229+
230+
GetExtra = py._PyCode_GetExtra
231+
GetExtra.argtypes = (ctypes.py_object, ctypes.c_ssize_t,
232+
ctypes.POINTER(ctypes.c_voidp))
233+
GetExtra.restype = ctypes.c_int
234+
235+
LAST_FREED = None
236+
def myfree(ptr):
237+
global LAST_FREED
238+
LAST_FREED = ptr
239+
240+
FREE_FUNC = freefunc(myfree)
241+
FREE_INDEX = RequestCodeExtraIndex(FREE_FUNC)
242+
243+
class CoExtra(unittest.TestCase):
244+
def get_func(self):
245+
# Defining a function causes the containing function to have a
246+
# reference to the code object. We need the code objects to go
247+
# away, so we eval a lambda.
248+
return eval('lambda:42')
249+
250+
def test_get_non_code(self):
251+
f = self.get_func()
252+
253+
self.assertRaises(SystemError, SetExtra, 42, FREE_INDEX,
254+
ctypes.c_voidp(100))
255+
self.assertRaises(SystemError, GetExtra, 42, FREE_INDEX,
256+
ctypes.c_voidp(100))
257+
258+
def test_bad_index(self):
259+
f = self.get_func()
260+
self.assertRaises(SystemError, SetExtra, f.__code__,
261+
FREE_INDEX+100, ctypes.c_voidp(100))
262+
self.assertEqual(GetExtra(f.__code__, FREE_INDEX+100,
263+
ctypes.c_voidp(100)), 0)
264+
265+
def test_free_called(self):
266+
# Verify that the provided free function gets invoked
267+
# when the code object is cleaned up.
268+
f = self.get_func()
269+
270+
SetExtra(f.__code__, FREE_INDEX, ctypes.c_voidp(100))
271+
del f
272+
self.assertEqual(LAST_FREED, 100)
273+
274+
def test_get_set(self):
275+
# Test basic get/set round tripping.
276+
f = self.get_func()
277+
278+
extra = ctypes.c_voidp()
279+
280+
SetExtra(f.__code__, FREE_INDEX, ctypes.c_voidp(200))
281+
# reset should free...
282+
SetExtra(f.__code__, FREE_INDEX, ctypes.c_voidp(300))
283+
self.assertEqual(LAST_FREED, 200)
284+
285+
extra = ctypes.c_voidp()
286+
GetExtra(f.__code__, FREE_INDEX, extra)
287+
self.assertEqual(extra.value, 300)
288+
del f
289+
290+
def test_free_different_thread(self):
291+
# Freeing a code object on a different thread then
292+
# where the co_extra was set should be safe.
293+
f = self.get_func()
294+
class ThreadTest(threading.Thread):
295+
def __init__(self, f, test):
296+
super().__init__()
297+
self.f = f
298+
self.test = test
299+
def run(self):
300+
del self.f
301+
self.test.assertEqual(LAST_FREED, 500)
302+
303+
SetExtra(f.__code__, FREE_INDEX, ctypes.c_voidp(500))
304+
tt = ThreadTest(f, self)
305+
del f
306+
tt.start()
307+
tt.join()
308+
self.assertEqual(LAST_FREED, 500)
309+
215310
def test_main(verbose=None):
216311
from test import test_code
217312
run_doctest(test_code, verbose)
218-
run_unittest(CodeTest, CodeConstsTest, CodeWeakRefTest)
219-
313+
tests = [CodeTest, CodeConstsTest, CodeWeakRefTest]
314+
if check_impl_detail(cpython=True):
315+
tests.append(CoExtra)
316+
run_unittest(*tests)
220317

221318
if __name__ == "__main__":
222319
test_main()

‎Misc/NEWS‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
+++++++++++
1+
+++++++++++
22
Python News
33
+++++++++++
44

@@ -10,6 +10,8 @@ What's New in Python 3.6.2 release candidate 1?
1010
Core and Builtins
1111
-----------------
1212

13+
- bpo-30604: Move co_extra_freefuncs to not be per-thread to avoid crashes
14+
1315
- bpo-29104: Fixed parsing backslashes in f-strings.
1416

1517
- bpo-27945: Fixed various segfaults with dict when input collections are

‎Objects/codeobject.c‎

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -411,11 +411,11 @@ static void
411411
code_dealloc(PyCodeObject *co)
412412
{
413413
if (co->co_extra != NULL) {
414-
PyThreadState *tstate = PyThreadState_Get();
414+
__PyCodeExtraState *state = __PyCodeExtraState_Get();
415415
_PyCodeObjectExtra *co_extra = co->co_extra;
416416

417417
for (Py_ssize_t i = 0; i < co_extra->ce_size; i++) {
418-
freefunc free_extra = tstate->co_extra_freefuncs[i];
418+
freefunc free_extra = state->co_extra_freefuncs[i];
419419

420420
if (free_extra != NULL) {
421421
free_extra(co_extra->ce_extras[i]);
@@ -825,8 +825,6 @@ _PyCode_CheckLineNumber(PyCodeObject* co, int lasti, PyAddrPair *bounds)
825825
int
826826
_PyCode_GetExtra(PyObject *code, Py_ssize_t index, void **extra)
827827
{
828-
assert(*extra == NULL);
829-
830828
if (!PyCode_Check(code)) {
831829
PyErr_BadInternalCall();
832830
return -1;
@@ -837,6 +835,7 @@ _PyCode_GetExtra(PyObject *code, Py_ssize_t index, void **extra)
837835

838836

839837
if (co_extra == NULL || co_extra->ce_size <= index) {
838+
*extra = NULL;
840839
return 0;
841840
}
842841

@@ -848,10 +847,10 @@ _PyCode_GetExtra(PyObject *code, Py_ssize_t index, void **extra)
848847
int
849848
_PyCode_SetExtra(PyObject *code, Py_ssize_t index, void *extra)
850849
{
851-
PyThreadState *tstate = PyThreadState_Get();
850+
__PyCodeExtraState *state = __PyCodeExtraState_Get();
852851

853852
if (!PyCode_Check(code) || index < 0 ||
854-
index >= tstate->co_extra_user_count) {
853+
index >= state->co_extra_user_count) {
855854
PyErr_BadInternalCall();
856855
return -1;
857856
}
@@ -866,13 +865,13 @@ _PyCode_SetExtra(PyObject *code, Py_ssize_t index, void *extra)
866865
}
867866

868867
co_extra->ce_extras = PyMem_Malloc(
869-
tstate->co_extra_user_count * sizeof(void*));
868+
state->co_extra_user_count * sizeof(void*));
870869
if (co_extra->ce_extras == NULL) {
871870
PyMem_Free(co_extra);
872871
return -1;
873872
}
874873

875-
co_extra->ce_size = tstate->co_extra_user_count;
874+
co_extra->ce_size = state->co_extra_user_count;
876875

877876
for (Py_ssize_t i = 0; i < co_extra->ce_size; i++) {
878877
co_extra->ce_extras[i] = NULL;
@@ -882,20 +881,27 @@ _PyCode_SetExtra(PyObject *code, Py_ssize_t index, void *extra)
882881
}
883882
else if (co_extra->ce_size <= index) {
884883
void** ce_extras = PyMem_Realloc(
885-
co_extra->ce_extras, tstate->co_extra_user_count * sizeof(void*));
884+
co_extra->ce_extras, state->co_extra_user_count * sizeof(void*));
886885

887886
if (ce_extras == NULL) {
888887
return -1;
889888
}
890889

891890
for (Py_ssize_t i = co_extra->ce_size;
892-
i < tstate->co_extra_user_count;
891+
i < state->co_extra_user_count;
893892
i++) {
894893
ce_extras[i] = NULL;
895894
}
896895

897896
co_extra->ce_extras = ce_extras;
898-
co_extra->ce_size = tstate->co_extra_user_count;
897+
co_extra->ce_size = state->co_extra_user_count;
898+
}
899+
900+
if (co_extra->ce_extras[index] != NULL) {
901+
freefunc free = state->co_extra_freefuncs[index];
902+
if (free != NULL) {
903+
free(co_extra->ce_extras[index]);
904+
}
899905
}
900906

901907
co_extra->ce_extras[index] = extra;

‎Python/ceval.c‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5453,14 +5453,14 @@ _Py_GetDXProfile(PyObject *self, PyObject *args)
54535453
Py_ssize_t
54545454
_PyEval_RequestCodeExtraIndex(freefunc free)
54555455
{
5456-
PyThreadState *tstate = PyThreadState_Get();
5456+
__PyCodeExtraState *state = __PyCodeExtraState_Get();
54575457
Py_ssize_t new_index;
54585458

5459-
if (tstate->co_extra_user_count == MAX_CO_EXTRA_USERS - 1) {
5459+
if (state->co_extra_user_count == MAX_CO_EXTRA_USERS - 1) {
54605460
return -1;
54615461
}
5462-
new_index = tstate->co_extra_user_count++;
5463-
tstate->co_extra_freefuncs[new_index] = free;
5462+
new_index = state->co_extra_user_count++;
5463+
state->co_extra_freefuncs[new_index] = free;
54645464
return new_index;
54655465
}
54665466

‎Python/pystate.c‎

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ static int autoTLSkey = -1;
5555
#endif
5656

5757
static PyInterpreterState *interp_head = NULL;
58+
static __PyCodeExtraState *coextra_head = NULL;
5859

5960
/* Assuming the current thread holds the GIL, this is the
6061
PyThreadState for the current thread. */
@@ -73,6 +74,12 @@ PyInterpreterState_New(void)
7374
PyMem_RawMalloc(sizeof(PyInterpreterState));
7475

7576
if (interp != NULL) {
77+
__PyCodeExtraState* coextra = PyMem_RawMalloc(sizeof(__PyCodeExtraState));
78+
if (coextra == NULL) {
79+
PyMem_RawFree(interp);
80+
return NULL;
81+
}
82+
7683
HEAD_INIT();
7784
#ifdef WITH_THREAD
7885
if (head_mutex == NULL)
@@ -92,6 +99,8 @@ PyInterpreterState_New(void)
9299
interp->importlib = NULL;
93100
interp->import_func = NULL;
94101
interp->eval_frame = _PyEval_EvalFrameDefault;
102+
coextra->co_extra_user_count = 0;
103+
coextra->interp = interp;
95104
#ifdef HAVE_DLOPEN
96105
#if HAVE_DECL_RTLD_NOW
97106
interp->dlopenflags = RTLD_NOW;
@@ -103,6 +112,8 @@ PyInterpreterState_New(void)
103112
HEAD_LOCK();
104113
interp->next = interp_head;
105114
interp_head = interp;
115+
coextra->next = coextra_head;
116+
coextra_head = coextra;
106117
HEAD_UNLOCK();
107118
}
108119

@@ -147,9 +158,10 @@ void
147158
PyInterpreterState_Delete(PyInterpreterState *interp)
148159
{
149160
PyInterpreterState **p;
161+
__PyCodeExtraState **pextra;
150162
zapthreads(interp);
151163
HEAD_LOCK();
152-
for (p = &interp_head; ; p = &(*p)->next) {
164+
for (p = &interp_head; /* N/A */; p = &(*p)->next) {
153165
if (*p == NULL)
154166
Py_FatalError(
155167
"PyInterpreterState_Delete: invalid interp");
@@ -159,6 +171,18 @@ PyInterpreterState_Delete(PyInterpreterState *interp)
159171
if (interp->tstate_head != NULL)
160172
Py_FatalError("PyInterpreterState_Delete: remaining threads");
161173
*p = interp->next;
174+
175+
for (pextra = &coextra_head; ; pextra = &(*pextra)->next) {
176+
if (*pextra == NULL)
177+
Py_FatalError(
178+
"PyInterpreterState_Delete: invalid extra");
179+
__PyCodeExtraState* extra = *pextra;
180+
if (extra->interp == interp) {
181+
*pextra = extra->next;
182+
PyMem_RawFree(extra);
183+
break;
184+
}
185+
}
162186
HEAD_UNLOCK();
163187
PyMem_RawFree(interp);
164188
#ifdef WITH_THREAD
@@ -224,7 +248,6 @@ new_threadstate(PyInterpreterState *interp, int init)
224248

225249
tstate->coroutine_wrapper = NULL;
226250
tstate->in_coroutine_wrapper = 0;
227-
tstate->co_extra_user_count = 0;
228251

229252
tstate->async_gen_firstiter = NULL;
230253
tstate->async_gen_finalizer = NULL;
@@ -548,6 +571,23 @@ PyThreadState_Swap(PyThreadState *newts)
548571
return oldts;
549572
}
550573

574+
__PyCodeExtraState*
575+
__PyCodeExtraState_Get() {
576+
PyInterpreterState* interp = PyThreadState_Get()->interp;
577+
578+
HEAD_LOCK();
579+
for (__PyCodeExtraState* cur = coextra_head; cur != NULL; cur = cur->next) {
580+
if (cur->interp == interp) {
581+
HEAD_UNLOCK();
582+
return cur;
583+
}
584+
}
585+
HEAD_UNLOCK();
586+
587+
Py_FatalError("__PyCodeExtraState_Get: no code state for interpreter");
588+
return NULL;
589+
}
590+
551591
/* An extension mechanism to store arbitrary additional per-thread state.
552592
PyThreadState_GetDict() returns a dictionary that can be used to hold such
553593
state; the caller should pick a unique key and store its state there. If

0 commit comments

Comments
 (0)