From cd1b1a438d30b3fbd2bf5d2779f17e86005f5684 Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Sat, 20 Jun 2026 00:30:50 +0900 Subject: [PATCH] gh-151722: Defer GC tracking of frozendict to end of construction --- ...-06-20-00-30-47.gh-issue-151722.RPMPIY.rst | 2 + Objects/dictobject.c | 48 +++++++++++++++---- 2 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-20-00-30-47.gh-issue-151722.RPMPIY.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-20-00-30-47.gh-issue-151722.RPMPIY.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-20-00-30-47.gh-issue-151722.RPMPIY.rst new file mode 100644 index 000000000000000..57b5dee7458ede5 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-20-00-30-47.gh-issue-151722.RPMPIY.rst @@ -0,0 +1,2 @@ +Defer GC tracking of :class:`frozendict` to end of construction. Patch by +Donghee Na. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index ac2f210d023487d..8bf2c6daa945c65 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -138,6 +138,7 @@ As a consequence of this, split keys have a maximum size of 16. // Forward declarations static PyObject* frozendict_new(PyTypeObject *type, PyObject *args, PyObject *kwds); +static PyObject* frozendict_new_untracked(PyTypeObject *type); static PyObject* dict_new(PyTypeObject *type, PyObject *args, PyObject *kwds); static int dict_merge(PyObject *a, PyObject *b, int override, PyObject **dupkey); static int dict_contains(PyObject *op, PyObject *key); @@ -5242,7 +5243,7 @@ static PyNumberMethods dict_as_number = { }; static PyObject * -dict_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +dict_new_untracked(PyTypeObject *type) { assert(type != NULL); assert(type->tp_alloc != NULL); @@ -5262,8 +5263,18 @@ dict_new(PyTypeObject *type, PyObject *args, PyObject *kwds) d->ma_keys = Py_EMPTY_KEYS; d->ma_values = NULL; ASSERT_CONSISTENT(d); - if (!_PyObject_GC_IS_TRACKED(d)) { - _PyObject_GC_TRACK(d); + return self; +} + +static PyObject * +dict_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + PyObject *self = dict_new_untracked(type); + if (self == NULL) { + return NULL; + } + if (!_PyObject_GC_IS_TRACKED(self)) { + _PyObject_GC_TRACK(self); } return self; } @@ -5323,7 +5334,9 @@ frozendict_vectorcall(PyObject *type, PyObject * const*args, return Py_NewRef(args[0]); } - PyObject *self = frozendict_new(_PyType_CAST(type), NULL, NULL); + /* Keep the frozendict untracked until it is fully built, so a half-built + object is never reachable from another thread. */ + PyObject *self = frozendict_new_untracked(_PyType_CAST(type)); if (self == NULL) { return NULL; } @@ -5343,6 +5356,10 @@ frozendict_vectorcall(PyObject *type, PyObject * const*args, } } } + /* Track only once fully built. */ + if (!_PyObject_GC_IS_TRACKED(self)) { + _PyObject_GC_TRACK(self); + } return self; } @@ -8361,17 +8378,28 @@ frozendict_hash(PyObject *op) } +/* Allocate an empty, GC-untracked frozendict. Staying untracked while it is + filled keeps a half-built frozendict so another thread can't observe it + changing. Callers must GC-track it once fully built. */ static PyObject * -frozendict_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +frozendict_new_untracked(PyTypeObject *type) { - PyObject *d = dict_new(type, args, kwds); + PyObject *d = dict_new_untracked(type); if (d == NULL) { return NULL; } assert(can_modify_dict(_PyAnyDict_CAST(d))); + _PyFrozenDictObject_CAST(d)->ma_hash = -1; + return d; +} - PyFrozenDictObject *self = _PyFrozenDictObject_CAST(d); - self->ma_hash = -1; +static PyObject * +frozendict_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + PyObject *d = frozendict_new_untracked(type); + if (d == NULL) { + return NULL; + } if (args != NULL) { if (dict_update_common(d, args, kwds, "frozendict") < 0) { @@ -8383,6 +8411,10 @@ frozendict_new(PyTypeObject *type, PyObject *args, PyObject *kwds) assert(kwds == NULL); } + /* Track only once fully built. */ + if (!_PyObject_GC_IS_TRACKED(d)) { + _PyObject_GC_TRACK(d); + } return d; }