Skip to content

Commit f79daaa

Browse files
committed
Fix race in filter_search().
Using the critical section seems the easiest fix for this. Add a unit test that fails TSAN check if the fix is not applied.
1 parent 9cb6f73 commit f79daaa

File tree

2 files changed

+52
-10
lines changed

2 files changed

+52
-10
lines changed

Lib/test/test_free_threading/test_races.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import time
66
import unittest
77
import _testinternalcapi
8+
import warnings
89

910
from test.support import threading_helper
1011

@@ -286,5 +287,36 @@ def set_recursion_limit():
286287
do_race(something_recursive, set_recursion_limit)
287288

288289

290+
@threading_helper.requires_working_threading()
291+
class TestWarningsRaces(TestBase):
292+
def setUp(self):
293+
self.saved_filters = warnings.filters[:]
294+
# Add multiple filters to the list to increase odds of race.
295+
for lineno in range(20):
296+
warnings.filterwarnings('ignore', message='not matched', category=Warning, lineno=lineno)
297+
# Override showwarning() so that we don't actually show warnings.
298+
def showwarning(*args):
299+
pass
300+
warnings.showwarning = showwarning
301+
302+
def tearDown(self):
303+
warnings.filters[:] = self.saved_filters
304+
warnings.showwarning = warnings._showwarning_orig
305+
306+
def test_racing_warnings_filter(self):
307+
# Modifying the warnings.filters list while another thread is using
308+
# warn() should not crash or race.
309+
def modify_filters():
310+
time.sleep(0)
311+
warnings.filters[:] = [('ignore', None, UserWarning, None, 0)]
312+
time.sleep(0)
313+
warnings.filters[:] = self.saved_filters
314+
315+
def emit_warning():
316+
warnings.warn('dummy message', category=UserWarning)
317+
318+
do_race(modify_filters, emit_warning)
319+
320+
289321
if __name__ == "__main__":
290322
unittest.main()

Python/_warnings.c

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,10 @@ filter_search(PyInterpreterState *interp, PyObject *category,
424424
PyObject *text, Py_ssize_t lineno,
425425
PyObject *module, char *list_name, PyObject *filters,
426426
PyObject **item, PyObject **matched_action) {
427-
/* filters list could change while we are iterating over it. */
427+
bool result = true;
428+
*matched_action = NULL;
429+
/* Avoid the filters list changing while we iterate over it. */
430+
Py_BEGIN_CRITICAL_SECTION(filters);
428431
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(filters); i++) {
429432
PyObject *tmp_item, *action, *msg, *cat, *mod, *ln_obj;
430433
Py_ssize_t ln;
@@ -434,7 +437,8 @@ filter_search(PyInterpreterState *interp, PyObject *category,
434437
if (!PyTuple_Check(tmp_item) || PyTuple_GET_SIZE(tmp_item) != 5) {
435438
PyErr_Format(PyExc_ValueError,
436439
"warnings.%s item %zd isn't a 5-tuple", list_name, i);
437-
return false;
440+
result = false;
441+
break;
438442
}
439443

440444
/* Python code: action, msg, cat, mod, ln = item */
@@ -450,43 +454,49 @@ filter_search(PyInterpreterState *interp, PyObject *category,
450454
"action must be a string, not '%.200s'",
451455
Py_TYPE(action)->tp_name);
452456
Py_DECREF(tmp_item);
453-
return false;
457+
result = false;
458+
break;
454459
}
455460

456461
good_msg = check_matched(interp, msg, text);
457462
if (good_msg == -1) {
458463
Py_DECREF(tmp_item);
459-
return false;
464+
result = false;
465+
break;
460466
}
461467

462468
good_mod = check_matched(interp, mod, module);
463469
if (good_mod == -1) {
464470
Py_DECREF(tmp_item);
465-
return false;
471+
result = false;
472+
break;
466473
}
467474

468475
is_subclass = PyObject_IsSubclass(category, cat);
469476
if (is_subclass == -1) {
470477
Py_DECREF(tmp_item);
471-
return false;
478+
result = false;
479+
break;
472480
}
473481

474482
ln = PyLong_AsSsize_t(ln_obj);
475483
if (ln == -1 && PyErr_Occurred()) {
476484
Py_DECREF(tmp_item);
477-
return false;
485+
result = false;
486+
break;
478487
}
479488

480489
if (good_msg && is_subclass && good_mod && (ln == 0 || lineno == ln)) {
481490
*item = tmp_item;
482491
*matched_action = action;
483-
return true;
492+
result = true;
493+
break;
484494
}
485495

486496
Py_DECREF(tmp_item);
487497
}
488-
*matched_action = NULL;
489-
return true;
498+
Py_END_CRITICAL_SECTION();
499+
return result;
490500
}
491501

492502
/* The item is a new reference. */

0 commit comments

Comments
 (0)