From 31bbc40a2b31381e3b13f0a59070844b1b698805 Mon Sep 17 00:00:00 2001 From: edward_xu Date: Fri, 29 May 2026 00:46:34 +0800 Subject: [PATCH 1/5] fix: fix `decimal.Context.status` race in free-threading --- Lib/test/test_free_threading/test_decimal.py | 32 ++++++++++++ ...-05-30-00-08-49.gh-issue-149142.zWeH0z.rst | 2 + Modules/_decimal/_decimal.c | 51 +++++++++++++++++-- 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 Lib/test/test_free_threading/test_decimal.py create mode 100644 Misc/NEWS.d/next/Library/2026-05-30-00-08-49.gh-issue-149142.zWeH0z.rst diff --git a/Lib/test/test_free_threading/test_decimal.py b/Lib/test/test_free_threading/test_decimal.py new file mode 100644 index 000000000000000..59eeb757fc6da26 --- /dev/null +++ b/Lib/test/test_free_threading/test_decimal.py @@ -0,0 +1,32 @@ +import unittest + +from test.support import threading_helper + +import decimal + +N_THREADS = 8 +ITERATIONS = 100_000 + +@threading_helper.requires_working_threading() +class TestDecimal(unittest.TestCase): + def test_add_status(self): + # prec=4 makes "1.23456" Inexact|Rounded + shared_ctx = decimal.Context(prec=4) + def worker(): + for _ in range(ITERATIONS): + shared_ctx.create_decimal("1.23456") + threading_helper.run_concurrently(worker, N_THREADS) + + + def test_clear_flags(self): + shared_ctx = decimal.Context(prec=4) + + def producer(): + for _ in range(ITERATIONS): + shared_ctx.create_decimal("1.23456") + + def clearer(): + for _ in range(ITERATIONS): + shared_ctx.clear_flags() + + threading_helper.run_concurrently([producer]*N_THREADS + [clearer]*N_THREADS) diff --git a/Misc/NEWS.d/next/Library/2026-05-30-00-08-49.gh-issue-149142.zWeH0z.rst b/Misc/NEWS.d/next/Library/2026-05-30-00-08-49.gh-issue-149142.zWeH0z.rst new file mode 100644 index 000000000000000..7fdff3f1b312ce7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-30-00-08-49.gh-issue-149142.zWeH0z.rst @@ -0,0 +1,2 @@ +Fix a data race in :class:`decimal.Context` status flag updates in the +:term:`free-threaded build`. diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 2760792a3fe18ed..bd57353734bc774 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -218,6 +218,7 @@ typedef struct { typedef struct PyDecContextObject { PyObject_HEAD + PyMutex ctx_lock; mpd_context_t ctx; PyObject *traps; PyObject *flags; @@ -227,6 +228,7 @@ typedef struct PyDecContextObject { } PyDecContextObject; #define _PyDecContextObject_CAST(op) ((PyDecContextObject *)(op)) +#define _PyDecContextObject_LOCKED_CAST(op) ((PyDecContextObject *)(op)) typedef struct { PyObject_HEAD @@ -246,6 +248,7 @@ typedef struct { #define SdFlagAddr(v) (_PyDecSignalDictObject_CAST(v)->flags) #define SdFlags(v) (*_PyDecSignalDictObject_CAST(v)->flags) #define CTX(v) (&_PyDecContextObject_CAST(v)->ctx) +#define CTX_LOCK(v) (&_PyDecContextObject_CAST(v)->ctx_lock) #define CtxCaps(v) (_PyDecContextObject_CAST(v)->capitals) static inline decimal_state * @@ -611,9 +614,12 @@ static int dec_addstatus(PyObject *context, uint32_t status) { mpd_context_t *ctx = CTX(context); + PyMutex* ctx_lock = CTX_LOCK(context); decimal_state *state = get_module_state_from_ctx(context); + PyMutex_Lock(ctx_lock); ctx->status |= status; + PyMutex_Unlock(ctx_lock); if (status & (ctx->traps|MPD_Malloc_error)) { PyObject *ex, *siglist; @@ -1418,7 +1424,10 @@ static PyObject * _decimal_Context_clear_flags_impl(PyObject *self) /*[clinic end generated code: output=c86719a70177d0b6 input=a06055e2f3e7edb1]*/ { + PyMutex* ctx_lock = CTX_LOCK(self); + PyMutex_Lock(ctx_lock); CTX(self)->status = 0; + PyMutex_Unlock(ctx_lock); Py_RETURN_NONE; } @@ -1437,6 +1446,7 @@ context_new(PyTypeObject *type, { PyDecContextObject *self = NULL; mpd_context_t *ctx; + PyMutex* ctx_lock; decimal_state *state = get_module_state_by_def(type); if (type == state->PyDecContext_Type) { @@ -1449,6 +1459,7 @@ context_new(PyTypeObject *type, if (self == NULL) { return NULL; } + self->ctx_lock = (PyMutex){0}; self->traps = PyObject_CallObject((PyObject *)state->PyDecSignalDict_Type, NULL); if (self->traps == NULL) { @@ -1471,8 +1482,12 @@ context_new(PyTypeObject *type, *ctx = dflt_ctx; } + ctx_lock = CTX_LOCK(self); + SdFlagAddr(self->traps) = &ctx->traps; + PyMutex_Lock(ctx_lock); SdFlagAddr(self->flags) = &ctx->status; + PyMutex_Unlock(ctx_lock); CtxCaps(self) = 1; self->tstate = NULL; @@ -1556,6 +1571,7 @@ static PyObject * context_repr(PyObject *self) { mpd_context_t *ctx; + PyMutex* ctx_lock; char flags[MPD_MAX_SIGNAL_LIST]; char traps[MPD_MAX_SIGNAL_LIST]; int n, mem; @@ -1566,8 +1582,13 @@ context_repr(PyObject *self) #endif ctx = CTX(self); + ctx_lock = CTX_LOCK(self); + PyMutex_Lock(ctx_lock); + uint32_t ctx_status = ctx->status; + PyMutex_Unlock(ctx_lock); + mem = MPD_MAX_SIGNAL_LIST; - n = mpd_lsnprint_signals(flags, mem, ctx->status, dec_signal_string); + n = mpd_lsnprint_signals(flags, mem, ctx_status, dec_signal_string); if (n < 0 || n >= mem) { INTERNAL_ERROR_PTR("context_repr"); } @@ -1594,6 +1615,7 @@ init_basic_context(PyObject *v) ctx.round = MPD_ROUND_HALF_UP; *CTX(v) = ctx; + *CTX_LOCK(v) = (PyMutex){0}; CtxCaps(v) = 1; } @@ -1606,6 +1628,7 @@ init_extended_context(PyObject *v) ctx.traps = 0; *CTX(v) = ctx; + *CTX_LOCK(v) = (PyMutex){0}; CtxCaps(v) = 1; } @@ -1715,11 +1738,16 @@ _decimal_Context___reduce___impl(PyObject *self, PyTypeObject *cls) PyObject *traps; PyObject *ret; mpd_context_t *ctx; + PyMutex* ctx_lock; decimal_state *state = PyType_GetModuleState(cls); ctx = CTX(self); + ctx_lock = CTX_LOCK(self); - flags = signals_as_list(state, ctx->status); + PyMutex_Lock(ctx_lock); + uint32_t ctx_status = ctx->status; + PyMutex_Unlock(ctx_lock); + flags = signals_as_list(state, ctx_status); if (flags == NULL) { return NULL; } @@ -1917,11 +1945,17 @@ PyDec_SetCurrentContext(PyObject *self, PyObject *v) static PyObject * init_current_context(decimal_state *state) { + mpd_context_t* ctx; + PyMutex* ctx_lock; PyObject *tl_context = context_copy(state, state->default_context_template); if (tl_context == NULL) { return NULL; } - CTX(tl_context)->status = 0; + ctx = CTX(tl_context); + ctx_lock = CTX_LOCK(tl_context); + PyMutex_Lock(ctx_lock); + ctx->status = 0; + PyMutex_Unlock(ctx_lock); PyObject *tok = PyContextVar_Set(state->current_context_var, tl_context); if (tok == NULL) { @@ -1982,7 +2016,11 @@ PyDec_SetCurrentContext(PyObject *self, PyObject *v) if (v == NULL) { return NULL; } - CTX(v)->status = 0; + mpd_context_t* ctx = CTX(v); + PyMutex* ctx_lock = CTX_LOCK(v); + PyMutex_Lock(ctx_lock); + ctx->status = 0; + PyMutex_Unlock(ctx_lock); } else { Py_INCREF(v); @@ -3479,6 +3517,7 @@ convert_op_cmp(PyObject **vcmp, PyObject **wcmp, PyObject *v, PyObject *w, int op, PyObject *context) { mpd_context_t *ctx = CTX(context); + PyMutex* ctx_lock = CTX_LOCK(context); *vcmp = v; @@ -3495,7 +3534,9 @@ convert_op_cmp(PyObject **vcmp, PyObject **wcmp, PyObject *v, PyObject *w, *wcmp = NULL; } else { + PyMutex_Lock(ctx_lock); ctx->status |= MPD_Float_operation; + PyMutex_Unlock(ctx_lock); *wcmp = PyDec_FromFloatExact(state, w, context); } } @@ -3510,7 +3551,9 @@ convert_op_cmp(PyObject **vcmp, PyObject **wcmp, PyObject *v, PyObject *w, *wcmp = NULL; } else { + PyMutex_Lock(ctx_lock); ctx->status |= MPD_Float_operation; + PyMutex_Unlock(ctx_lock); *wcmp = PyDec_FromFloatExact(state, tmp, context); Py_DECREF(tmp); } From 71397b22cd29b069ba047f2f5766ae5811c3f728 Mon Sep 17 00:00:00 2001 From: edward_xu Date: Sat, 30 May 2026 00:55:29 +0800 Subject: [PATCH 2/5] fix comment changes --- Modules/_decimal/_decimal.c | 53 +++++++++++++------------------------ 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index bd57353734bc774..5df9470ca118742 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -228,7 +228,6 @@ typedef struct PyDecContextObject { } PyDecContextObject; #define _PyDecContextObject_CAST(op) ((PyDecContextObject *)(op)) -#define _PyDecContextObject_LOCKED_CAST(op) ((PyDecContextObject *)(op)) typedef struct { PyObject_HEAD @@ -248,7 +247,9 @@ typedef struct { #define SdFlagAddr(v) (_PyDecSignalDictObject_CAST(v)->flags) #define SdFlags(v) (*_PyDecSignalDictObject_CAST(v)->flags) #define CTX(v) (&_PyDecContextObject_CAST(v)->ctx) -#define CTX_LOCK(v) (&_PyDecContextObject_CAST(v)->ctx_lock) +#define CTX_LOCK_INIT(v) _PyDecContextObject_CAST(v)->ctx_lock = (PyMutex){0} +#define CTX_LOCK(v) PyMutex_Lock(&_PyDecContextObject_CAST(v)->ctx_lock) +#define CTX_UNLOCK(v) PyMutex_Unlock(&_PyDecContextObject_CAST(v)->ctx_lock) #define CtxCaps(v) (_PyDecContextObject_CAST(v)->capitals) static inline decimal_state * @@ -614,12 +615,11 @@ static int dec_addstatus(PyObject *context, uint32_t status) { mpd_context_t *ctx = CTX(context); - PyMutex* ctx_lock = CTX_LOCK(context); decimal_state *state = get_module_state_from_ctx(context); - PyMutex_Lock(ctx_lock); + CTX_LOCK(context); ctx->status |= status; - PyMutex_Unlock(ctx_lock); + CTX_UNLOCK(context); if (status & (ctx->traps|MPD_Malloc_error)) { PyObject *ex, *siglist; @@ -1424,10 +1424,9 @@ static PyObject * _decimal_Context_clear_flags_impl(PyObject *self) /*[clinic end generated code: output=c86719a70177d0b6 input=a06055e2f3e7edb1]*/ { - PyMutex* ctx_lock = CTX_LOCK(self); - PyMutex_Lock(ctx_lock); + CTX_LOCK(self); CTX(self)->status = 0; - PyMutex_Unlock(ctx_lock); + CTX_UNLOCK(self); Py_RETURN_NONE; } @@ -1446,7 +1445,6 @@ context_new(PyTypeObject *type, { PyDecContextObject *self = NULL; mpd_context_t *ctx; - PyMutex* ctx_lock; decimal_state *state = get_module_state_by_def(type); if (type == state->PyDecContext_Type) { @@ -1482,12 +1480,8 @@ context_new(PyTypeObject *type, *ctx = dflt_ctx; } - ctx_lock = CTX_LOCK(self); - SdFlagAddr(self->traps) = &ctx->traps; - PyMutex_Lock(ctx_lock); SdFlagAddr(self->flags) = &ctx->status; - PyMutex_Unlock(ctx_lock); CtxCaps(self) = 1; self->tstate = NULL; @@ -1571,7 +1565,6 @@ static PyObject * context_repr(PyObject *self) { mpd_context_t *ctx; - PyMutex* ctx_lock; char flags[MPD_MAX_SIGNAL_LIST]; char traps[MPD_MAX_SIGNAL_LIST]; int n, mem; @@ -1582,10 +1575,9 @@ context_repr(PyObject *self) #endif ctx = CTX(self); - ctx_lock = CTX_LOCK(self); - PyMutex_Lock(ctx_lock); + CTX_LOCK(self); uint32_t ctx_status = ctx->status; - PyMutex_Unlock(ctx_lock); + CTX_UNLOCK(self); mem = MPD_MAX_SIGNAL_LIST; n = mpd_lsnprint_signals(flags, mem, ctx_status, dec_signal_string); @@ -1615,7 +1607,7 @@ init_basic_context(PyObject *v) ctx.round = MPD_ROUND_HALF_UP; *CTX(v) = ctx; - *CTX_LOCK(v) = (PyMutex){0}; + CTX_LOCK_INIT(v); CtxCaps(v) = 1; } @@ -1628,7 +1620,7 @@ init_extended_context(PyObject *v) ctx.traps = 0; *CTX(v) = ctx; - *CTX_LOCK(v) = (PyMutex){0}; + CTX_LOCK_INIT(v); CtxCaps(v) = 1; } @@ -1738,15 +1730,14 @@ _decimal_Context___reduce___impl(PyObject *self, PyTypeObject *cls) PyObject *traps; PyObject *ret; mpd_context_t *ctx; - PyMutex* ctx_lock; decimal_state *state = PyType_GetModuleState(cls); ctx = CTX(self); - ctx_lock = CTX_LOCK(self); - PyMutex_Lock(ctx_lock); + CTX_LOCK(self); uint32_t ctx_status = ctx->status; - PyMutex_Unlock(ctx_lock); + CTX_UNLOCK(self); + flags = signals_as_list(state, ctx_status); if (flags == NULL) { return NULL; @@ -1946,16 +1937,12 @@ static PyObject * init_current_context(decimal_state *state) { mpd_context_t* ctx; - PyMutex* ctx_lock; PyObject *tl_context = context_copy(state, state->default_context_template); if (tl_context == NULL) { return NULL; } ctx = CTX(tl_context); - ctx_lock = CTX_LOCK(tl_context); - PyMutex_Lock(ctx_lock); ctx->status = 0; - PyMutex_Unlock(ctx_lock); PyObject *tok = PyContextVar_Set(state->current_context_var, tl_context); if (tok == NULL) { @@ -2017,10 +2004,7 @@ PyDec_SetCurrentContext(PyObject *self, PyObject *v) return NULL; } mpd_context_t* ctx = CTX(v); - PyMutex* ctx_lock = CTX_LOCK(v); - PyMutex_Lock(ctx_lock); ctx->status = 0; - PyMutex_Unlock(ctx_lock); } else { Py_INCREF(v); @@ -3517,7 +3501,6 @@ convert_op_cmp(PyObject **vcmp, PyObject **wcmp, PyObject *v, PyObject *w, int op, PyObject *context) { mpd_context_t *ctx = CTX(context); - PyMutex* ctx_lock = CTX_LOCK(context); *vcmp = v; @@ -3534,9 +3517,9 @@ convert_op_cmp(PyObject **vcmp, PyObject **wcmp, PyObject *v, PyObject *w, *wcmp = NULL; } else { - PyMutex_Lock(ctx_lock); + CTX_LOCK(ctx); ctx->status |= MPD_Float_operation; - PyMutex_Unlock(ctx_lock); + CTX_UNLOCK(ctx); *wcmp = PyDec_FromFloatExact(state, w, context); } } @@ -3551,9 +3534,9 @@ convert_op_cmp(PyObject **vcmp, PyObject **wcmp, PyObject *v, PyObject *w, *wcmp = NULL; } else { - PyMutex_Lock(ctx_lock); + CTX_LOCK(ctx); ctx->status |= MPD_Float_operation; - PyMutex_Unlock(ctx_lock); + CTX_UNLOCK(ctx); *wcmp = PyDec_FromFloatExact(state, tmp, context); Py_DECREF(tmp); } From 012b81f8e88f61c7e41871d7e5aae8ae65e33298 Mon Sep 17 00:00:00 2001 From: edward_xu Date: Sat, 30 May 2026 10:15:28 +0800 Subject: [PATCH 3/5] fix wrong lock --- Modules/_decimal/_decimal.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 5df9470ca118742..9557efc1fbafe81 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -3517,9 +3517,9 @@ convert_op_cmp(PyObject **vcmp, PyObject **wcmp, PyObject *v, PyObject *w, *wcmp = NULL; } else { - CTX_LOCK(ctx); + CTX_LOCK(context); ctx->status |= MPD_Float_operation; - CTX_UNLOCK(ctx); + CTX_UNLOCK(context); *wcmp = PyDec_FromFloatExact(state, w, context); } } @@ -3534,9 +3534,9 @@ convert_op_cmp(PyObject **vcmp, PyObject **wcmp, PyObject *v, PyObject *w, *wcmp = NULL; } else { - CTX_LOCK(ctx); + CTX_LOCK(context); ctx->status |= MPD_Float_operation; - CTX_UNLOCK(ctx); + CTX_UNLOCK(context); *wcmp = PyDec_FromFloatExact(state, tmp, context); Py_DECREF(tmp); } From 5676be42f93abbfa3fe0dcdc56992c7605aa1d1d Mon Sep 17 00:00:00 2001 From: edward_xu Date: Sat, 30 May 2026 11:00:58 +0800 Subject: [PATCH 4/5] add guard for free threading --- .../2026-05-30-00-08-49.gh-issue-149142.zWeH0z.rst | 5 +++-- Modules/_decimal/_decimal.c | 13 +++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-05-30-00-08-49.gh-issue-149142.zWeH0z.rst b/Misc/NEWS.d/next/Library/2026-05-30-00-08-49.gh-issue-149142.zWeH0z.rst index 7fdff3f1b312ce7..c0ec8542aa627c4 100644 --- a/Misc/NEWS.d/next/Library/2026-05-30-00-08-49.gh-issue-149142.zWeH0z.rst +++ b/Misc/NEWS.d/next/Library/2026-05-30-00-08-49.gh-issue-149142.zWeH0z.rst @@ -1,2 +1,3 @@ -Fix a data race in :class:`decimal.Context` status flag updates in the -:term:`free-threaded build`. +Fix data races in :meth:`decimal.Context.create_decimal` and +:meth:`decimal.Context.clear_flags` when updating +:class:`decimal.Context` status flags in the :term:`free-threaded build`. diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 9557efc1fbafe81..a92d64421d19126 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -218,7 +218,9 @@ typedef struct { typedef struct PyDecContextObject { PyObject_HEAD +#ifdef Py_GIL_DISABLED PyMutex ctx_lock; +#endif mpd_context_t ctx; PyObject *traps; PyObject *flags; @@ -247,10 +249,17 @@ typedef struct { #define SdFlagAddr(v) (_PyDecSignalDictObject_CAST(v)->flags) #define SdFlags(v) (*_PyDecSignalDictObject_CAST(v)->flags) #define CTX(v) (&_PyDecContextObject_CAST(v)->ctx) +#define CtxCaps(v) (_PyDecContextObject_CAST(v)->capitals) + +#ifdef Py_GIL_DISABLED #define CTX_LOCK_INIT(v) _PyDecContextObject_CAST(v)->ctx_lock = (PyMutex){0} #define CTX_LOCK(v) PyMutex_Lock(&_PyDecContextObject_CAST(v)->ctx_lock) #define CTX_UNLOCK(v) PyMutex_Unlock(&_PyDecContextObject_CAST(v)->ctx_lock) -#define CtxCaps(v) (_PyDecContextObject_CAST(v)->capitals) +#else +#define CTX_LOCK_INIT(v) ((void)0) +#define CTX_LOCK(v) ((void)0) +#define CTX_UNLOCK(v) ((void)0) +#endif static inline decimal_state * get_module_state_from_ctx(PyObject *v) @@ -1457,7 +1466,7 @@ context_new(PyTypeObject *type, if (self == NULL) { return NULL; } - self->ctx_lock = (PyMutex){0}; + CTX_LOCK_INIT(self); self->traps = PyObject_CallObject((PyObject *)state->PyDecSignalDict_Type, NULL); if (self->traps == NULL) { From 64d4fcb9b0087890fa76dc92f40958bb5d89f72f Mon Sep 17 00:00:00 2001 From: edward_xu Date: Sat, 30 May 2026 14:40:13 +0800 Subject: [PATCH 5/5] revert useless change --- Modules/_decimal/_decimal.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index a92d64421d19126..0960502d699d6d0 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -1945,13 +1945,11 @@ PyDec_SetCurrentContext(PyObject *self, PyObject *v) static PyObject * init_current_context(decimal_state *state) { - mpd_context_t* ctx; PyObject *tl_context = context_copy(state, state->default_context_template); if (tl_context == NULL) { return NULL; } - ctx = CTX(tl_context); - ctx->status = 0; + CTX(tl_context)->status = 0; PyObject *tok = PyContextVar_Set(state->current_context_var, tl_context); if (tok == NULL) { @@ -2012,8 +2010,7 @@ PyDec_SetCurrentContext(PyObject *self, PyObject *v) if (v == NULL) { return NULL; } - mpd_context_t* ctx = CTX(v); - ctx->status = 0; + CTX(v)->status = 0; } else { Py_INCREF(v);