From 929272e58bf26c60e217722e099f2e4a0aea88f5 Mon Sep 17 00:00:00 2001 From: ligade Date: Fri, 26 Jun 2026 22:19:00 +0530 Subject: [PATCH 1/2] gh-152235: Defer GC tracking until set_init completes on tp_new path set_new() used make_new_set(), which GC-tracked the empty set before set_init() populated it from the iterable. That left the same half-built window the vectorcall path already closed, so concurrent GC / get_objects() could observe an inconsistent set and crash on edge cases. Allocate untracked in set_new() and call _PyObject_GC_TRACK() only after set_init() succeeds (skipping if already tracked for re-init). --- Objects/setobject.c | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/Objects/setobject.c b/Objects/setobject.c index d8c38ff1c1d899a..522a7c65bfc4d8f 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -1483,7 +1483,11 @@ frozenset_vectorcall(PyObject *type, PyObject * const*args, static PyObject * set_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { - return make_new_set(type, NULL); + /* Leave GC-untracked until set_init() finishes filling (gh-152235). + * The vectorcall path uses make_new_set() and tracks at the end; the + * classic tp_new/tp_init path must do the same so a half-built set is + * never visible to gc.get_objects() / another thread during __init__. */ + return make_new_set_untracked(type, NULL); } #ifdef Py_GIL_DISABLED @@ -2750,6 +2754,7 @@ set_init(PyObject *so, PyObject *args, PyObject *kwds) { PySetObject *self = _PySet_CAST(so); PyObject *iterable = NULL; + int rv; if (!_PyArg_NoKeywords("set", kwds)) return -1; @@ -2759,19 +2764,30 @@ set_init(PyObject *so, PyObject *args, PyObject *kwds) if (_PyObject_IsUniquelyReferenced((PyObject *)self) && self->fill == 0) { self->hash = -1; if (iterable == NULL) { - return 0; + rv = 0; + } + else { + rv = set_update_local(self, iterable); } - return set_update_local(self, iterable); } - Py_BEGIN_CRITICAL_SECTION(self); - if (self->fill) - set_clear_internal((PyObject*)self); - self->hash = -1; - Py_END_CRITICAL_SECTION(); + else { + Py_BEGIN_CRITICAL_SECTION(self); + if (self->fill) + set_clear_internal((PyObject*)self); + self->hash = -1; + Py_END_CRITICAL_SECTION(); - if (iterable == NULL) - return 0; - return set_update_internal(self, iterable); + if (iterable == NULL) + rv = 0; + else + rv = set_update_internal(self, iterable); + } + + /* Track only once fully built (pairs with set_new leaving it untracked). */ + if (rv == 0 && !_PyObject_GC_IS_TRACKED(so)) { + _PyObject_GC_TRACK(so); + } + return rv; } static PyObject* From 43a0b439616d900958c1cd158a2053c7f3bcffc5 Mon Sep 17 00:00:00 2001 From: ligade Date: Fri, 26 Jun 2026 23:22:18 +0530 Subject: [PATCH 2/2] gh-148653: Reject hashing incompletely initialized tuples marshal.loads can expose a partially filled FLAG_REF tuple via TYPE_REF into a set, which called PyObject_Hash(NULL) and SIGSEGV'd. Guard tuple_hash NULL slots with ValueError instead. --- Objects/tupleobject.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index bb5e18cb790acf0..cbbbe7fb9bf5311 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -382,6 +382,14 @@ tuple_hash(PyObject *op) PyObject **item = v->ob_item; acc = _PyTuple_HASH_XXPRIME_5; for (Py_ssize_t i = 0; i < len; i++) { + /* Guard against incompletely initialized tuples (e.g. a + * TYPE_REF during marshal.loads before all slots are filled). + * Without this, PyObject_Hash(NULL) SIGSEGVs (gh-148653). */ + if (item[i] == NULL) { + PyErr_SetString(PyExc_ValueError, + "cannot hash incompletely initialized tuple"); + return -1; + } Py_uhash_t lane = PyObject_Hash(item[i]); if (lane == (Py_uhash_t)-1) { return -1;