Skip to content

Commit 6b47499

Browse files
authored
[3.13] gh-128679: Fix tracemalloc.stop() race conditions (#128897)
tracemalloc_alloc(), tracemalloc_realloc(), PyTraceMalloc_Track(), PyTraceMalloc_Untrack() and _PyTraceMalloc_TraceRef() now check tracemalloc_config.tracing after calling TABLES_LOCK(). _PyTraceMalloc_Stop() now protects more code with TABLES_LOCK(), especially setting tracemalloc_config.tracing to 1. Add a test using PyTraceMalloc_Track() to test tracemalloc.stop() race condition. Call _PyTraceMalloc_Init() at Python startup.
1 parent ef99618 commit 6b47499

File tree

7 files changed

+250
-117
lines changed

7 files changed

+250
-117
lines changed

Include/internal/pycore_tracemalloc.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ extern PyObject* _PyTraceMalloc_GetTraces(void);
144144
extern PyObject* _PyTraceMalloc_GetObjectTraceback(PyObject *obj);
145145

146146
/* Initialize tracemalloc */
147-
extern int _PyTraceMalloc_Init(void);
147+
extern PyStatus _PyTraceMalloc_Init(void);
148148

149149
/* Start tracemalloc */
150150
extern int _PyTraceMalloc_Start(int max_nframe);

Lib/test/test_tracemalloc.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
from test.support.script_helper import (assert_python_ok, assert_python_failure,
88
interpreter_requires_environment)
99
from test import support
10-
from test.support import os_helper
1110
from test.support import force_not_colorized
11+
from test.support import os_helper
12+
from test.support import threading_helper
1213

1314
try:
1415
import _testcapi
@@ -952,7 +953,6 @@ def check_env_var_invalid(self, nframe):
952953
return
953954
self.fail(f"unexpected output: {stderr!a}")
954955

955-
956956
def test_env_var_invalid(self):
957957
for nframe in INVALID_NFRAME:
958958
with self.subTest(nframe=nframe):
@@ -1101,6 +1101,14 @@ def test_stop_untrack(self):
11011101
with self.assertRaises(RuntimeError):
11021102
self.untrack()
11031103

1104+
@unittest.skipIf(_testcapi is None, 'need _testcapi')
1105+
@threading_helper.requires_working_threading()
1106+
# gh-128679: Test crash on a debug build (especially on FreeBSD).
1107+
@unittest.skipIf(support.Py_DEBUG, 'need release build')
1108+
def test_tracemalloc_track_race(self):
1109+
# gh-128679: Test fix for tracemalloc.stop() race condition
1110+
_testcapi.tracemalloc_track_race()
1111+
11041112

11051113
if __name__ == "__main__":
11061114
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :func:`tracemalloc.stop` race condition. Fix :mod:`tracemalloc` to
2+
support calling :func:`tracemalloc.stop` in one thread, while another thread
3+
is tracing memory allocations. Patch by Victor Stinner.

Modules/_testcapimodule.c

+99
Original file line numberDiff line numberDiff line change
@@ -3322,6 +3322,104 @@ test_atexit(PyObject *self, PyObject *Py_UNUSED(args))
33223322
Py_RETURN_NONE;
33233323
}
33243324

3325+
3326+
static void
3327+
tracemalloc_track_race_thread(void *data)
3328+
{
3329+
PyTraceMalloc_Track(123, 10, 1);
3330+
3331+
PyThread_type_lock lock = (PyThread_type_lock)data;
3332+
PyThread_release_lock(lock);
3333+
}
3334+
3335+
// gh-128679: Test fix for tracemalloc.stop() race condition
3336+
static PyObject *
3337+
tracemalloc_track_race(PyObject *self, PyObject *args)
3338+
{
3339+
#define NTHREAD 50
3340+
PyObject *tracemalloc = NULL;
3341+
PyObject *stop = NULL;
3342+
PyThread_type_lock locks[NTHREAD];
3343+
memset(locks, 0, sizeof(locks));
3344+
3345+
// Call tracemalloc.start()
3346+
tracemalloc = PyImport_ImportModule("tracemalloc");
3347+
if (tracemalloc == NULL) {
3348+
goto error;
3349+
}
3350+
PyObject *start = PyObject_GetAttrString(tracemalloc, "start");
3351+
if (start == NULL) {
3352+
goto error;
3353+
}
3354+
PyObject *res = PyObject_CallNoArgs(start);
3355+
Py_DECREF(start);
3356+
if (res == NULL) {
3357+
goto error;
3358+
}
3359+
Py_DECREF(res);
3360+
3361+
stop = PyObject_GetAttrString(tracemalloc, "stop");
3362+
Py_CLEAR(tracemalloc);
3363+
if (stop == NULL) {
3364+
goto error;
3365+
}
3366+
3367+
// Start threads
3368+
for (size_t i = 0; i < NTHREAD; i++) {
3369+
PyThread_type_lock lock = PyThread_allocate_lock();
3370+
if (!lock) {
3371+
PyErr_NoMemory();
3372+
goto error;
3373+
}
3374+
locks[i] = lock;
3375+
PyThread_acquire_lock(lock, 1);
3376+
3377+
unsigned long thread;
3378+
thread = PyThread_start_new_thread(tracemalloc_track_race_thread,
3379+
(void*)lock);
3380+
if (thread == (unsigned long)-1) {
3381+
PyErr_SetString(PyExc_RuntimeError, "can't start new thread");
3382+
goto error;
3383+
}
3384+
}
3385+
3386+
// Call tracemalloc.stop() while threads are running
3387+
res = PyObject_CallNoArgs(stop);
3388+
Py_CLEAR(stop);
3389+
if (res == NULL) {
3390+
goto error;
3391+
}
3392+
Py_DECREF(res);
3393+
3394+
// Wait until threads complete with the GIL released
3395+
Py_BEGIN_ALLOW_THREADS
3396+
for (size_t i = 0; i < NTHREAD; i++) {
3397+
PyThread_type_lock lock = locks[i];
3398+
PyThread_acquire_lock(lock, 1);
3399+
PyThread_release_lock(lock);
3400+
}
3401+
Py_END_ALLOW_THREADS
3402+
3403+
// Free threads locks
3404+
for (size_t i=0; i < NTHREAD; i++) {
3405+
PyThread_type_lock lock = locks[i];
3406+
PyThread_free_lock(lock);
3407+
}
3408+
Py_RETURN_NONE;
3409+
3410+
error:
3411+
Py_CLEAR(tracemalloc);
3412+
Py_CLEAR(stop);
3413+
for (size_t i=0; i < NTHREAD; i++) {
3414+
PyThread_type_lock lock = locks[i];
3415+
if (lock) {
3416+
PyThread_free_lock(lock);
3417+
}
3418+
}
3419+
return NULL;
3420+
#undef NTHREAD
3421+
}
3422+
33253423
static PyMethodDef TestMethods[] = {
33263424
{"set_errno", set_errno, METH_VARARGS},
33273425
{"test_config", test_config, METH_NOARGS},
@@ -3464,6 +3562,7 @@ static PyMethodDef TestMethods[] = {
34643562
{"function_set_warning", function_set_warning, METH_NOARGS},
34653563
{"test_critical_sections", test_critical_sections, METH_NOARGS},
34663564
{"test_atexit", test_atexit, METH_NOARGS},
3565+
{"tracemalloc_track_race", tracemalloc_track_race, METH_NOARGS},
34673566
{NULL, NULL} /* sentinel */
34683567
};
34693568

Modules/_tracemalloc.c

-5
Original file line numberDiff line numberDiff line change
@@ -223,10 +223,5 @@ PyInit__tracemalloc(void)
223223
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
224224
#endif
225225

226-
if (_PyTraceMalloc_Init() < 0) {
227-
Py_DECREF(m);
228-
return NULL;
229-
}
230-
231226
return m;
232227
}

Python/pylifecycle.c

+5
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,11 @@ pycore_create_interpreter(_PyRuntimeState *runtime,
706706
return _PyStatus_NO_MEMORY();
707707
}
708708

709+
status = _PyTraceMalloc_Init();
710+
if (_PyStatus_EXCEPTION(status)) {
711+
return status;
712+
}
713+
709714
PyThreadState *tstate = _PyThreadState_New(interp,
710715
_PyThreadState_WHENCE_INIT);
711716
if (tstate == NULL) {

0 commit comments

Comments
 (0)