From dc97cd1a8fc402a07b6b646780b2697b0d24f55d Mon Sep 17 00:00:00 2001 From: Brij <97006829+bkap123@users.noreply.github.com> Date: Fri, 29 May 2026 10:18:08 -0400 Subject: [PATCH 1/9] gh-144774: Add critical section in `exceptions.c` with `PyDict_Next` --- Lib/test/test_exceptions.py | 18 ++++++++++++++++++ Objects/exceptions.c | 9 ++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 3f5fcb29b442dec..8934e0ce0de31ad 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -1954,6 +1954,24 @@ def test_exec_set_nomemory_hang(self): self.assertGreater(len(output), 0) # At minimum, should not hang self.assertIn(b"MemoryError", output) + def test_data_race(self): + from threading import Thread + import copy + + E = Exception() + + def func(): + for i in range(100): + setattr(E, 'x', i) + copy.copy(E) + + threads = [Thread(target=func) for _ in range(4)] + + for t in threads: + t.start() + + for t in threads: + t.join() class NameErrorTests(unittest.TestCase): def test_name_error_has_name(self): diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 5e5e87cd6d7559f..363ac54d7275850 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -243,6 +243,8 @@ BaseException___setstate___impl(PyBaseExceptionObject *self, PyObject *state) PyErr_SetString(PyExc_TypeError, "state is not a dictionary"); return NULL; } + int error = 0; + Py_BEGIN_CRITICAL_SECTION(state); while (PyDict_Next(state, &i, &d_key, &d_value)) { Py_INCREF(d_key); Py_INCREF(d_value); @@ -250,9 +252,14 @@ BaseException___setstate___impl(PyBaseExceptionObject *self, PyObject *state) Py_DECREF(d_value); Py_DECREF(d_key); if (res < 0) { - return NULL; + error = 1; + break; } } + Py_END_CRITICAL_SECTION(); + if (error) { + return NULL; + } } Py_RETURN_NONE; } From 65d4d18ced0c2be6c434e7a0b4307fadcc4606f5 Mon Sep 17 00:00:00 2001 From: Brij <97006829+bkap123@users.noreply.github.com> Date: Fri, 29 May 2026 13:13:12 -0400 Subject: [PATCH 2/9] move critical section into arg clinic --- Objects/clinic/exceptions.c.h | 6 +++--- Objects/exceptions.c | 13 +++---------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/Objects/clinic/exceptions.c.h b/Objects/clinic/exceptions.c.h index 9baac8b1cc660b0..427eb05fc089114 100644 --- a/Objects/clinic/exceptions.c.h +++ b/Objects/clinic/exceptions.c.h @@ -44,9 +44,9 @@ BaseException___setstate__(PyObject *self, PyObject *state) { PyObject *return_value = NULL; - Py_BEGIN_CRITICAL_SECTION(self); + Py_BEGIN_CRITICAL_SECTION2(self, state); return_value = BaseException___setstate___impl((PyBaseExceptionObject *)self, state); - Py_END_CRITICAL_SECTION(); + Py_END_CRITICAL_SECTION2(); return return_value; } @@ -380,4 +380,4 @@ BaseExceptionGroup_subgroup(PyObject *self, PyObject *matcher_value) return return_value; } -/*[clinic end generated code: output=fcf70b3b71f3d14a input=a9049054013a1b77]*/ +/*[clinic end generated code: output=8dfe83ced9f55506 input=a9049054013a1b77]*/ diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 363ac54d7275850..56f1a96aae6ffad 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -225,7 +225,7 @@ BaseException___reduce___impl(PyBaseExceptionObject *self) */ /*[clinic input] -@critical_section +@critical_section self state BaseException.__setstate__ state: object / @@ -233,7 +233,7 @@ BaseException.__setstate__ static PyObject * BaseException___setstate___impl(PyBaseExceptionObject *self, PyObject *state) -/*[clinic end generated code: output=f3834889950453ab input=5524b61cfe9b9856]*/ +/*[clinic end generated code: output=f3834889950453ab input=2ddc941f42fc5bf6]*/ { PyObject *d_key, *d_value; Py_ssize_t i = 0; @@ -243,8 +243,6 @@ BaseException___setstate___impl(PyBaseExceptionObject *self, PyObject *state) PyErr_SetString(PyExc_TypeError, "state is not a dictionary"); return NULL; } - int error = 0; - Py_BEGIN_CRITICAL_SECTION(state); while (PyDict_Next(state, &i, &d_key, &d_value)) { Py_INCREF(d_key); Py_INCREF(d_value); @@ -252,14 +250,9 @@ BaseException___setstate___impl(PyBaseExceptionObject *self, PyObject *state) Py_DECREF(d_value); Py_DECREF(d_key); if (res < 0) { - error = 1; - break; + return NULL; } } - Py_END_CRITICAL_SECTION(); - if (error) { - return NULL; - } } Py_RETURN_NONE; } From 49725863f69759a4d850cf5192a55fba632ea28e Mon Sep 17 00:00:00 2001 From: Brij Kapadia <97006829+brijkapadia@users.noreply.github.com> Date: Fri, 29 May 2026 13:17:43 -0400 Subject: [PATCH 3/9] Update Objects/exceptions.c Co-authored-by: Sam Gross --- Objects/exceptions.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 56f1a96aae6ffad..aa0469412985697 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -225,7 +225,7 @@ BaseException___reduce___impl(PyBaseExceptionObject *self) */ /*[clinic input] -@critical_section self state +@critical_section state BaseException.__setstate__ state: object / From daa45fe4de281cf71c2f941d44137d63013a46f8 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 17:51:13 +0000 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-05-29-17-51-11.gh-issue-144774.jPcixe.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-05-29-17-51-11.gh-issue-144774.jPcixe.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-29-17-51-11.gh-issue-144774.jPcixe.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-29-17-51-11.gh-issue-144774.jPcixe.rst new file mode 100644 index 000000000000000..7cc135d2986cf7a --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-29-17-51-11.gh-issue-144774.jPcixe.rst @@ -0,0 +1 @@ +Fix potential data race in `meth:: BaseException.__setstate__`. From b39087af6e997f739f27f97fb381562456693383 Mon Sep 17 00:00:00 2001 From: Brij <97006829+bkap123@users.noreply.github.com> Date: Fri, 29 May 2026 14:06:45 -0400 Subject: [PATCH 5/9] Update test --- Lib/test/test_exceptions.py | 19 ------------------- .../test_free_threading/test_exceptions.py | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 19 deletions(-) create mode 100644 Lib/test/test_free_threading/test_exceptions.py diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 8934e0ce0de31ad..faa30f59a660f6b 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -1954,25 +1954,6 @@ def test_exec_set_nomemory_hang(self): self.assertGreater(len(output), 0) # At minimum, should not hang self.assertIn(b"MemoryError", output) - def test_data_race(self): - from threading import Thread - import copy - - E = Exception() - - def func(): - for i in range(100): - setattr(E, 'x', i) - copy.copy(E) - - threads = [Thread(target=func) for _ in range(4)] - - for t in threads: - t.start() - - for t in threads: - t.join() - class NameErrorTests(unittest.TestCase): def test_name_error_has_name(self): try: diff --git a/Lib/test/test_free_threading/test_exceptions.py b/Lib/test/test_free_threading/test_exceptions.py new file mode 100644 index 000000000000000..61146f29c7868e5 --- /dev/null +++ b/Lib/test/test_free_threading/test_exceptions.py @@ -0,0 +1,16 @@ +import unittest +import copy + +from test.support import threading_helper + +threading_helper.requires_working_threading(module=True) +class ExceptionTests(unittest.TestCase): + def test_setstate_data_race(self): + E = Exception() + + def func(): + for i in range(100): + setattr(E, 'x', i) + copy.copy(E) + + threading_helper.run_concurrently(func, nthreads=4) From fdc8bdf7e9dffd793e08825aeb75bff7282fe5eb Mon Sep 17 00:00:00 2001 From: Brij <97006829+bkap123@users.noreply.github.com> Date: Fri, 29 May 2026 14:15:17 -0400 Subject: [PATCH 6/9] Update NEWS --- .../2026-05-29-17-51-11.gh-issue-144774.jPcixe.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-29-17-51-11.gh-issue-144774.jPcixe.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-29-17-51-11.gh-issue-144774.jPcixe.rst index 7cc135d2986cf7a..19eb96b188cd8a8 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-29-17-51-11.gh-issue-144774.jPcixe.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-29-17-51-11.gh-issue-144774.jPcixe.rst @@ -1 +1 @@ -Fix potential data race in `meth:: BaseException.__setstate__`. +Fix data race in :meth:`BaseException.__setstate__` when an exception is copied while being mutated. From 3ea8fe4632cebeee84c0727d1cd030c2b34efb85 Mon Sep 17 00:00:00 2001 From: Brij Kapadia <97006829+brijkapadia@users.noreply.github.com> Date: Fri, 29 May 2026 14:18:09 -0400 Subject: [PATCH 7/9] fix stray change --- Lib/test/test_exceptions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index faa30f59a660f6b..3f5fcb29b442dec 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -1954,6 +1954,7 @@ def test_exec_set_nomemory_hang(self): self.assertGreater(len(output), 0) # At minimum, should not hang self.assertIn(b"MemoryError", output) + class NameErrorTests(unittest.TestCase): def test_name_error_has_name(self): try: From cebc239ba349e23267ff57d4b1ee31cc41f4f539 Mon Sep 17 00:00:00 2001 From: Brij <97006829+bkap123@users.noreply.github.com> Date: Sat, 30 May 2026 08:44:48 -0400 Subject: [PATCH 8/9] Update generated files --- Objects/clinic/exceptions.c.h | 6 +++--- Objects/exceptions.c | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Objects/clinic/exceptions.c.h b/Objects/clinic/exceptions.c.h index 427eb05fc089114..5047a673e579c66 100644 --- a/Objects/clinic/exceptions.c.h +++ b/Objects/clinic/exceptions.c.h @@ -44,9 +44,9 @@ BaseException___setstate__(PyObject *self, PyObject *state) { PyObject *return_value = NULL; - Py_BEGIN_CRITICAL_SECTION2(self, state); + Py_BEGIN_CRITICAL_SECTION(state); return_value = BaseException___setstate___impl((PyBaseExceptionObject *)self, state); - Py_END_CRITICAL_SECTION2(); + Py_END_CRITICAL_SECTION(); return return_value; } @@ -380,4 +380,4 @@ BaseExceptionGroup_subgroup(PyObject *self, PyObject *matcher_value) return return_value; } -/*[clinic end generated code: output=8dfe83ced9f55506 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=e63b88d0443b4f92 input=a9049054013a1b77]*/ diff --git a/Objects/exceptions.c b/Objects/exceptions.c index aa0469412985697..ce1e6d93da3fc45 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -233,7 +233,7 @@ BaseException.__setstate__ static PyObject * BaseException___setstate___impl(PyBaseExceptionObject *self, PyObject *state) -/*[clinic end generated code: output=f3834889950453ab input=2ddc941f42fc5bf6]*/ +/*[clinic end generated code: output=f3834889950453ab input=f9b1aea70382cdb6]*/ { PyObject *d_key, *d_value; Py_ssize_t i = 0; From 0627679b2288d489691659542783f3f76e743386 Mon Sep 17 00:00:00 2001 From: Brij <97006829+bkap123@users.noreply.github.com> Date: Sat, 30 May 2026 10:52:26 -0400 Subject: [PATCH 9/9] Update news --- .../2026-05-29-17-51-11.gh-issue-144774.jPcixe.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-29-17-51-11.gh-issue-144774.jPcixe.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-29-17-51-11.gh-issue-144774.jPcixe.rst index 19eb96b188cd8a8..3dd117e7d3c3502 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-29-17-51-11.gh-issue-144774.jPcixe.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-29-17-51-11.gh-issue-144774.jPcixe.rst @@ -1 +1 @@ -Fix data race in :meth:`BaseException.__setstate__` when an exception is copied while being mutated. +Fix data race in :class:`BaseException` when an exception is copied while being mutated.