Skip to content

Commit 6df22cb

Browse files
authored
[3.12] gh-128679: Fix tracemalloc.stop() race conditions (#128897) (#129022)
[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. (cherry picked from commit 6b47499)
1 parent 83de72e commit 6df22cb

File tree

7 files changed

+250
-116
lines changed

7 files changed

+250
-116
lines changed

Include/tracemalloc.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ PyAPI_FUNC(PyObject *) _PyTraceMalloc_GetTraces(void);
5050
PyAPI_FUNC(PyObject *) _PyTraceMalloc_GetObjectTraceback(PyObject *obj);
5151

5252
/* Initialize tracemalloc */
53-
PyAPI_FUNC(int) _PyTraceMalloc_Init(void);
53+
PyAPI_FUNC(PyStatus) _PyTraceMalloc_Init(void);
5454

5555
/* Start tracemalloc */
5656
PyAPI_FUNC(int) _PyTraceMalloc_Start(int max_nframe);

Lib/test/test_tracemalloc.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
interpreter_requires_environment)
99
from test import support
1010
from test.support import os_helper
11+
from test.support import threading_helper
1112

1213
try:
1314
import _testcapi
@@ -946,7 +947,6 @@ def check_env_var_invalid(self, nframe):
946947
return
947948
self.fail(f"unexpected output: {stderr!a}")
948949

949-
950950
def test_env_var_invalid(self):
951951
for nframe in INVALID_NFRAME:
952952
with self.subTest(nframe=nframe):
@@ -1095,6 +1095,14 @@ def test_stop_untrack(self):
10951095
with self.assertRaises(RuntimeError):
10961096
self.untrack()
10971097

1098+
@unittest.skipIf(_testcapi is None, 'need _testcapi')
1099+
@threading_helper.requires_working_threading()
1100+
# gh-128679: Test crash on a debug build (especially on FreeBSD).
1101+
@unittest.skipIf(support.Py_DEBUG, 'need release build')
1102+
def test_tracemalloc_track_race(self):
1103+
# gh-128679: Test fix for tracemalloc.stop() race condition
1104+
_testcapi.tracemalloc_track_race()
1105+
10981106

10991107
if __name__ == "__main__":
11001108
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

+100
Original file line numberDiff line numberDiff line change
@@ -3238,6 +3238,105 @@ test_atexit(PyObject *self, PyObject *Py_UNUSED(args))
32383238

32393239
static PyObject *test_buildvalue_issue38913(PyObject *, PyObject *);
32403240

3241+
3242+
static void
3243+
tracemalloc_track_race_thread(void *data)
3244+
{
3245+
PyTraceMalloc_Track(123, 10, 1);
3246+
3247+
PyThread_type_lock lock = (PyThread_type_lock)data;
3248+
PyThread_release_lock(lock);
3249+
}
3250+
3251+
// gh-128679: Test fix for tracemalloc.stop() race condition
3252+
static PyObject *
3253+
tracemalloc_track_race(PyObject *self, PyObject *args)
3254+
{
3255+
#define NTHREAD 50
3256+
PyObject *tracemalloc = NULL;
3257+
PyObject *stop = NULL;
3258+
PyThread_type_lock locks[NTHREAD];
3259+
memset(locks, 0, sizeof(locks));
3260+
3261+
// Call tracemalloc.start()
3262+
tracemalloc = PyImport_ImportModule("tracemalloc");
3263+
if (tracemalloc == NULL) {
3264+
goto error;
3265+
}
3266+
PyObject *start = PyObject_GetAttrString(tracemalloc, "start");
3267+
if (start == NULL) {
3268+
goto error;
3269+
}
3270+
PyObject *res = PyObject_CallNoArgs(start);
3271+
Py_DECREF(start);
3272+
if (res == NULL) {
3273+
goto error;
3274+
}
3275+
Py_DECREF(res);
3276+
3277+
stop = PyObject_GetAttrString(tracemalloc, "stop");
3278+
Py_CLEAR(tracemalloc);
3279+
if (stop == NULL) {
3280+
goto error;
3281+
}
3282+
3283+
// Start threads
3284+
for (size_t i = 0; i < NTHREAD; i++) {
3285+
PyThread_type_lock lock = PyThread_allocate_lock();
3286+
if (!lock) {
3287+
PyErr_NoMemory();
3288+
goto error;
3289+
}
3290+
locks[i] = lock;
3291+
PyThread_acquire_lock(lock, 1);
3292+
3293+
unsigned long thread;
3294+
thread = PyThread_start_new_thread(tracemalloc_track_race_thread,
3295+
(void*)lock);
3296+
if (thread == (unsigned long)-1) {
3297+
PyErr_SetString(PyExc_RuntimeError, "can't start new thread");
3298+
goto error;
3299+
}
3300+
}
3301+
3302+
// Call tracemalloc.stop() while threads are running
3303+
res = PyObject_CallNoArgs(stop);
3304+
Py_CLEAR(stop);
3305+
if (res == NULL) {
3306+
goto error;
3307+
}
3308+
Py_DECREF(res);
3309+
3310+
// Wait until threads complete with the GIL released
3311+
Py_BEGIN_ALLOW_THREADS
3312+
for (size_t i = 0; i < NTHREAD; i++) {
3313+
PyThread_type_lock lock = locks[i];
3314+
PyThread_acquire_lock(lock, 1);
3315+
PyThread_release_lock(lock);
3316+
}
3317+
Py_END_ALLOW_THREADS
3318+
3319+
// Free threads locks
3320+
for (size_t i=0; i < NTHREAD; i++) {
3321+
PyThread_type_lock lock = locks[i];
3322+
PyThread_free_lock(lock);
3323+
}
3324+
Py_RETURN_NONE;
3325+
3326+
error:
3327+
Py_CLEAR(tracemalloc);
3328+
Py_CLEAR(stop);
3329+
for (size_t i=0; i < NTHREAD; i++) {
3330+
PyThread_type_lock lock = locks[i];
3331+
if (lock) {
3332+
PyThread_free_lock(lock);
3333+
}
3334+
}
3335+
return NULL;
3336+
#undef NTHREAD
3337+
}
3338+
3339+
32413340
static PyMethodDef TestMethods[] = {
32423341
{"set_errno", set_errno, METH_VARARGS},
32433342
{"test_config", test_config, METH_NOARGS},
@@ -3378,6 +3477,7 @@ static PyMethodDef TestMethods[] = {
33783477
{"function_get_kw_defaults", function_get_kw_defaults, METH_O, NULL},
33793478
{"function_set_kw_defaults", function_set_kw_defaults, METH_VARARGS, NULL},
33803479
{"test_atexit", test_atexit, METH_NOARGS},
3480+
{"tracemalloc_track_race", tracemalloc_track_race, METH_NOARGS},
33813481
{NULL, NULL} /* sentinel */
33823482
};
33833483

Modules/_tracemalloc.c

-5
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,5 @@ PyInit__tracemalloc(void)
219219
if (m == NULL)
220220
return NULL;
221221

222-
if (_PyTraceMalloc_Init() < 0) {
223-
Py_DECREF(m);
224-
return NULL;
225-
}
226-
227222
return m;
228223
}

Python/pylifecycle.c

+5
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,11 @@ pycore_create_interpreter(_PyRuntimeState *runtime,
654654
// didn't depend on interp->feature_flags being set already.
655655
_PyObject_InitState(interp);
656656

657+
status = _PyTraceMalloc_Init();
658+
if (_PyStatus_EXCEPTION(status)) {
659+
return status;
660+
}
661+
657662
PyThreadState *tstate = _PyThreadState_New(interp);
658663
if (tstate == NULL) {
659664
return _PyStatus_ERR("can't make first thread");

0 commit comments

Comments
 (0)