diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-11-03-13-44-26.gh-issue-159184.X_sOdY.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-03-13-44-26.gh-issue-159184.X_sOdY.rst new file mode 100644 index 00000000000000..651ac1d0b9bae9 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-03-13-44-26.gh-issue-159184.X_sOdY.rst @@ -0,0 +1,2 @@ +Fix a leak of NaN constants in code objects created by :func:`marshal.loads` +on the free-threaded build. diff --git a/Objects/codeobject.c b/Objects/codeobject.c index eb390ab24ec13e..2255220bec3d30 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -173,8 +173,16 @@ should_immortalize_constant(PyObject *v) _Py_IsImmortal(slice->stop) && _Py_IsImmortal(slice->step)); } - return (PyLong_CheckExact(v) || PyFloat_CheckExact(v) || - PyComplex_CheckExact(v) || PyBytes_CheckExact(v)); + // The hash of a NaN depends on its address, so the table would keep every + // NaN object it gets (and merging them would shrink frozensets). + if (PyFloat_CheckExact(v)) { + return !isnan(PyFloat_AS_DOUBLE(v)); + } + if (PyComplex_CheckExact(v)) { + Py_complex c = ((PyComplexObject *)v)->cval; + return !isnan(c.real) && !isnan(c.imag); + } + return PyLong_CheckExact(v) || PyBytes_CheckExact(v); } #endif