Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 6df22cb

Browse filesBrowse 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
Copy full SHA for 6df22cb

File tree

Expand file treeCollapse file tree

7 files changed

+250
-116
lines changed
Filter options
Expand file treeCollapse file tree

7 files changed

+250
-116
lines changed

‎Include/tracemalloc.h

Copy file name to clipboardExpand all lines: Include/tracemalloc.h
+1-1Lines changed: 1 addition & 1 deletion
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

Copy file name to clipboardExpand all lines: Lib/test/test_tracemalloc.py
+9-1Lines changed: 9 additions & 1 deletion
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()
+3Lines changed: 3 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: Modules/_testcapimodule.c
+100Lines changed: 100 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: Modules/_tracemalloc.c
-5Lines changed: 0 additions & 5 deletions
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

Copy file name to clipboardExpand all lines: Python/pylifecycle.c
+5Lines changed: 5 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.