diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-30-14-30-00.gh-issue-148286.Wn5Tz4.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-30-14-30-00.gh-issue-148286.Wn5Tz4.rst new file mode 100644 index 00000000000000..5c467ffd8577d0 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-30-14-30-00.gh-issue-148286.Wn5Tz4.rst @@ -0,0 +1,5 @@ +Fix undefined behaviour in ``_PyObject_MergePerThreadRefcounts()`` on the +free-threaded build. The per-thread reference count delta being merged is +routinely negative, and it was shifted left by ``_Py_REF_SHARED_SHIFT``, +which is undefined behaviour for a negative value. The shift is now done in +the unsigned domain. diff --git a/Python/uniqueid.c b/Python/uniqueid.c index 64c3e6cfbbe825..e6e2c3eadcb434 100644 --- a/Python/uniqueid.c +++ b/Python/uniqueid.c @@ -181,8 +181,12 @@ _PyObject_MergePerThreadRefcounts(_PyThreadStateImpl *tstate) Py_ssize_t refcnt = tstate->refcounts.values[i]; if (refcnt != 0) { PyObject *obj = pool->table[i].obj; + /* `refcnt` is a per-thread delta, so it is routinely negative, + and shifting a negative value left is undefined behaviour. + Shift in the unsigned domain instead. */ _Py_atomic_add_ssize(&obj->ob_ref_shared, - refcnt << _Py_REF_SHARED_SHIFT); + (Py_ssize_t)((size_t)refcnt + << _Py_REF_SHARED_SHIFT)); tstate->refcounts.values[i] = 0; } }