Skip to content

Commit 545053f

Browse files
committed
gh-128679: Fix tracemalloc.stop() race conditions
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.
1 parent 639e0f3 commit 545053f

File tree

4 files changed

+178
-44
lines changed

4 files changed

+178
-44
lines changed

Lib/test/test_tracemalloc.py

Lines changed: 8 additions & 2 deletions
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,12 @@ 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+
def test_tracemalloc_track_race(self):
1107+
# gh-128679: Test fix for tracemalloc.stop() race condition
1108+
_testcapi.tracemalloc_track_race()
1109+
11041110

11051111
if __name__ == "__main__":
11061112
unittest.main()
Lines 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

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3386,6 +3386,104 @@ test_atexit(PyObject *self, PyObject *Py_UNUSED(args))
33863386
Py_RETURN_NONE;
33873387
}
33883388

3389+
3390+
static void
3391+
tracemalloc_track_race_thread(void *data)
3392+
{
3393+
PyTraceMalloc_Track(123, 10, 1);
3394+
3395+
PyThread_type_lock lock = (PyThread_type_lock)data;
3396+
PyThread_release_lock(lock);
3397+
}
3398+
3399+
// gh-128679: Test fix for tracemalloc.stop() race condition
3400+
static PyObject *
3401+
tracemalloc_track_race(PyObject *self, PyObject *args)
3402+
{
3403+
#define NTHREAD 50
3404+
PyObject *tracemalloc = NULL;
3405+
PyObject *stop = NULL;
3406+
PyThread_type_lock locks[NTHREAD];
3407+
memset(locks, 0, sizeof(locks));
3408+
3409+
// Call tracemalloc.start()
3410+
tracemalloc = PyImport_ImportModule("tracemalloc");
3411+
if (tracemalloc == NULL) {
3412+
goto error;
3413+
}
3414+
PyObject *start = PyObject_GetAttrString(tracemalloc, "start");
3415+
if (start == NULL) {
3416+
goto error;
3417+
}
3418+
PyObject *res = PyObject_CallNoArgs(start);
3419+
Py_DECREF(start);
3420+
if (res == NULL) {
3421+
goto error;
3422+
}
3423+
Py_DECREF(res);
3424+
3425+
stop = PyObject_GetAttrString(tracemalloc, "stop");
3426+
Py_CLEAR(tracemalloc);
3427+
if (stop == NULL) {
3428+
goto error;
3429+
}
3430+
3431+
// Start threads
3432+
for (size_t i = 0; i < NTHREAD; i++) {
3433+
PyThread_type_lock lock = PyThread_allocate_lock();
3434+
if (!lock) {
3435+
PyErr_NoMemory();
3436+
goto error;
3437+
}
3438+
locks[i] = lock;
3439+
PyThread_acquire_lock(lock, 1);
3440+
3441+
unsigned long thread;
3442+
thread = PyThread_start_new_thread(tracemalloc_track_race_thread,
3443+
(void*)lock);
3444+
if (thread == (unsigned long)-1) {
3445+
PyErr_SetString(PyExc_RuntimeError, "can't start new thread");
3446+
goto error;
3447+
}
3448+
}
3449+
3450+
// Call tracemalloc.stop() while threads are running
3451+
res = PyObject_CallNoArgs(stop);
3452+
Py_CLEAR(stop);
3453+
if (res == NULL) {
3454+
goto error;
3455+
}
3456+
Py_DECREF(res);
3457+
3458+
// Wait until threads complete with the GIL released
3459+
Py_BEGIN_ALLOW_THREADS
3460+
for (size_t i = 0; i < NTHREAD; i++) {
3461+
PyThread_type_lock lock = locks[i];
3462+
PyThread_acquire_lock(lock, 1);
3463+
PyThread_release_lock(lock);
3464+
}
3465+
Py_END_ALLOW_THREADS
3466+
3467+
// Free threads locks
3468+
for (size_t i=0; i < NTHREAD; i++) {
3469+
PyThread_type_lock lock = locks[i];
3470+
PyThread_free_lock(lock);
3471+
}
3472+
Py_RETURN_NONE;
3473+
3474+
error:
3475+
Py_CLEAR(tracemalloc);
3476+
Py_CLEAR(stop);
3477+
for (size_t i=0; i < NTHREAD; i++) {
3478+
PyThread_type_lock lock = locks[i];
3479+
if (lock) {
3480+
PyThread_free_lock(lock);
3481+
}
3482+
}
3483+
return NULL;
3484+
#undef NTHREAD
3485+
}
3486+
33893487
static PyMethodDef TestMethods[] = {
33903488
{"set_errno", set_errno, METH_VARARGS},
33913489
{"test_config", test_config, METH_NOARGS},
@@ -3532,6 +3630,7 @@ static PyMethodDef TestMethods[] = {
35323630
{"test_critical_sections", test_critical_sections, METH_NOARGS},
35333631
{"pyeval_getlocals", pyeval_getlocals, METH_NOARGS},
35343632
{"test_atexit", test_atexit, METH_NOARGS},
3633+
{"tracemalloc_track_race", tracemalloc_track_race, METH_NOARGS},
35353634
{NULL, NULL} /* sentinel */
35363635
};
35373636

Python/tracemalloc.c

Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -538,12 +538,16 @@ tracemalloc_alloc(int use_calloc, void *ctx, size_t nelem, size_t elsize)
538538
return NULL;
539539

540540
TABLES_LOCK();
541-
if (ADD_TRACE(ptr, nelem * elsize) < 0) {
542-
/* Failed to allocate a trace for the new memory block */
543-
TABLES_UNLOCK();
544-
alloc->free(alloc->ctx, ptr);
545-
return NULL;
541+
542+
if (tracemalloc_config.tracing) {
543+
if (ADD_TRACE(ptr, nelem * elsize) < 0) {
544+
/* Failed to allocate a trace for the new memory block */
545+
alloc->free(alloc->ctx, ptr);
546+
ptr = NULL;
547+
}
546548
}
549+
// else: gh-128679: tracemalloc.stop() was called by another thread
550+
547551
TABLES_UNLOCK();
548552
return ptr;
549553
}
@@ -559,11 +563,15 @@ tracemalloc_realloc(void *ctx, void *ptr, size_t new_size)
559563
if (ptr2 == NULL)
560564
return NULL;
561565

566+
TABLES_LOCK();
567+
if (!tracemalloc_config.tracing) {
568+
// gh-128679: tracemalloc.stop() was called by another thread
569+
goto done;
570+
}
571+
562572
if (ptr != NULL) {
563573
/* an existing memory block has been resized */
564574

565-
TABLES_LOCK();
566-
567575
/* tracemalloc_add_trace() updates the trace if there is already
568576
a trace at address ptr2 */
569577
if (ptr2 != ptr) {
@@ -582,20 +590,19 @@ tracemalloc_realloc(void *ctx, void *ptr, size_t new_size)
582590
allocating memory. */
583591
Py_FatalError("tracemalloc_realloc() failed to allocate a trace");
584592
}
585-
TABLES_UNLOCK();
586593
}
587594
else {
588595
/* new allocation */
589596

590-
TABLES_LOCK();
591597
if (ADD_TRACE(ptr2, new_size) < 0) {
592598
/* Failed to allocate a trace for the new memory block */
593-
TABLES_UNLOCK();
594599
alloc->free(alloc->ctx, ptr2);
595-
return NULL;
600+
ptr2 = NULL;
596601
}
597-
TABLES_UNLOCK();
598602
}
603+
604+
done:
605+
TABLES_UNLOCK();
599606
return ptr2;
600607
}
601608

@@ -614,7 +621,12 @@ tracemalloc_free(void *ctx, void *ptr)
614621
alloc->free(alloc->ctx, ptr);
615622

616623
TABLES_LOCK();
617-
REMOVE_TRACE(ptr);
624+
625+
if (tracemalloc_config.tracing) {
626+
REMOVE_TRACE(ptr);
627+
}
628+
// else: gh-128679: tracemalloc.stop() was called by another thread
629+
618630
TABLES_UNLOCK();
619631
}
620632

@@ -779,17 +791,15 @@ tracemalloc_clear_filename(void *value)
779791

780792
/* reentrant flag must be set to call this function and GIL must be held */
781793
static void
782-
tracemalloc_clear_traces(void)
794+
tracemalloc_clear_traces_unlocked(void)
783795
{
784796
/* The GIL protects variables against concurrent access */
785797
assert(PyGILState_Check());
786798

787-
TABLES_LOCK();
788799
_Py_hashtable_clear(tracemalloc_traces);
789800
_Py_hashtable_clear(tracemalloc_domains);
790801
tracemalloc_traced_memory = 0;
791802
tracemalloc_peak_traced_memory = 0;
792-
TABLES_UNLOCK();
793803

794804
_Py_hashtable_clear(tracemalloc_tracebacks);
795805

@@ -963,6 +973,10 @@ _PyTraceMalloc_Stop(void)
963973
if (!tracemalloc_config.tracing)
964974
return;
965975

976+
// Lock to synchronize with tracemalloc_free() which checks
977+
// 'tracing' while holding the lock.
978+
TABLES_LOCK();
979+
966980
/* stop tracing Python memory allocations */
967981
tracemalloc_config.tracing = 0;
968982

@@ -973,11 +987,13 @@ _PyTraceMalloc_Stop(void)
973987
PyMem_SetAllocator(PYMEM_DOMAIN_MEM, &allocators.mem);
974988
PyMem_SetAllocator(PYMEM_DOMAIN_OBJ, &allocators.obj);
975989

976-
tracemalloc_clear_traces();
990+
tracemalloc_clear_traces_unlocked();
977991

978992
/* release memory */
979993
raw_free(tracemalloc_traceback);
980994
tracemalloc_traceback = NULL;
995+
996+
TABLES_UNLOCK();
981997
}
982998

983999

@@ -1307,20 +1323,19 @@ int
13071323
PyTraceMalloc_Track(unsigned int domain, uintptr_t ptr,
13081324
size_t size)
13091325
{
1310-
int res;
1311-
PyGILState_STATE gil_state;
1326+
PyGILState_STATE gil_state = PyGILState_Ensure();
1327+
TABLES_LOCK();
13121328

1313-
if (!tracemalloc_config.tracing) {
1314-
/* tracemalloc is not tracing: do nothing */
1315-
return -2;
1329+
int res;
1330+
if (tracemalloc_config.tracing) {
1331+
res = tracemalloc_add_trace(domain, ptr, size);
1332+
}
1333+
else {
1334+
// gh-128679: tracemalloc.stop() was called by another thread
1335+
res = -2;
13161336
}
13171337

1318-
gil_state = PyGILState_Ensure();
1319-
1320-
TABLES_LOCK();
1321-
res = tracemalloc_add_trace(domain, ptr, size);
13221338
TABLES_UNLOCK();
1323-
13241339
PyGILState_Release(gil_state);
13251340
return res;
13261341
}
@@ -1329,16 +1344,20 @@ PyTraceMalloc_Track(unsigned int domain, uintptr_t ptr,
13291344
int
13301345
PyTraceMalloc_Untrack(unsigned int domain, uintptr_t ptr)
13311346
{
1332-
if (!tracemalloc_config.tracing) {
1347+
TABLES_LOCK();
1348+
1349+
int result;
1350+
if (tracemalloc_config.tracing) {
1351+
tracemalloc_remove_trace(domain, ptr);
1352+
result = 0;
1353+
}
1354+
else {
13331355
/* tracemalloc is not tracing: do nothing */
1334-
return -2;
1356+
result = -2;
13351357
}
13361358

1337-
TABLES_LOCK();
1338-
tracemalloc_remove_trace(domain, ptr);
13391359
TABLES_UNLOCK();
1340-
1341-
return 0;
1360+
return result;
13421361
}
13431362

13441363

@@ -1376,16 +1395,21 @@ _PyTraceMalloc_TraceRef(PyObject *op, PyRefTracerEvent event, void* Py_UNUSED(ig
13761395
int res = -1;
13771396

13781397
TABLES_LOCK();
1379-
trace_t *trace = _Py_hashtable_get(tracemalloc_traces, TO_PTR(ptr));
1380-
if (trace != NULL) {
1381-
/* update the traceback of the memory block */
1382-
traceback_t *traceback = traceback_new();
1383-
if (traceback != NULL) {
1384-
trace->traceback = traceback;
1385-
res = 0;
1398+
1399+
if (tracemalloc_config.tracing) {
1400+
trace_t *trace = _Py_hashtable_get(tracemalloc_traces, TO_PTR(ptr));
1401+
if (trace != NULL) {
1402+
/* update the traceback of the memory block */
1403+
traceback_t *traceback = traceback_new();
1404+
if (traceback != NULL) {
1405+
trace->traceback = traceback;
1406+
res = 0;
1407+
}
13861408
}
1409+
/* else: cannot track the object, its memory block size is unknown */
13871410
}
1388-
/* else: cannot track the object, its memory block size is unknown */
1411+
// else: gh-128679: tracemalloc.stop() was called by another thread
1412+
13891413
TABLES_UNLOCK();
13901414

13911415
return res;
@@ -1418,7 +1442,9 @@ _PyTraceMalloc_ClearTraces(void)
14181442
return;
14191443
}
14201444
set_reentrant(1);
1421-
tracemalloc_clear_traces();
1445+
TABLES_LOCK();
1446+
tracemalloc_clear_traces_unlocked();
1447+
TABLES_UNLOCK();
14221448
set_reentrant(0);
14231449
}
14241450

0 commit comments

Comments
 (0)