Skip to content

Commit e32d094

Browse files
Properly untrack Python GC objects during deallocation.
Add PyObject_GC_UnTrack() in deallocation functions for Python types that have PyTPFLAGS_HAVE_GC set, either explicitly or by inheriting from a type with GC set. Not untracking before clearing instance data introduces potential race conditions (if GC happens to run between the partial clearing and the actual deallocation) and produces a warning under Python 3.11. (The warning then triggered an assertion failure, which only showed up when building in Py_DEBUG mode; this therefor also fixes that assertion failure.) PiperOrigin-RevId: 579827001
1 parent d580fde commit e32d094

3 files changed

Lines changed: 36 additions & 6 deletions

File tree

python/descriptor.c

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,25 @@ static PyObject* PyUpb_DescriptorBase_CopyToProto(PyObject* _self,
197197
}
198198

199199
static void PyUpb_DescriptorBase_Dealloc(PyUpb_DescriptorBase* base) {
200+
// This deallocator can be called on different types (which, despite
201+
// 'Base' in the name of one of them, do not inherit from each other).
202+
// Some of these types are GC types (they have Py_TPFLAGS_HAVE_GC set),
203+
// which means Python's GC can visit them (via tp_visit and/or tp_clear
204+
// methods) at any time. This also means we *must* stop GC from tracking
205+
// instances of them before we start destructing the object. In Python
206+
// 3.11, failing to do so would raise a runtime warning.
207+
if (PyType_HasFeature(Py_TYPE(base), Py_TPFLAGS_HAVE_GC)) {
208+
PyObject_GC_UnTrack(base);
209+
}
200210
PyUpb_ObjCache_Delete(base->def);
201-
Py_XDECREF(base->message_meta);
202-
Py_DECREF(base->pool);
203-
Py_XDECREF(base->options);
211+
// In addition to being visited by GC, instances can also (potentially) be
212+
// accessed whenever arbitrary code is executed. Destructors can execute
213+
// arbitrary code, so any struct members we DECREF should be set to NULL
214+
// or a new value *before* calling Py_DECREF on them. The Py_CLEAR macro
215+
// (and Py_SETREF in Python 3.8+) takes care to do this safely.
216+
Py_CLEAR(base->message_meta);
217+
Py_CLEAR(base->pool);
218+
Py_CLEAR(base->options);
204219
PyUpb_Dealloc(base);
205220
}
206221

@@ -256,9 +271,13 @@ PyObject* PyUpb_Descriptor_GetClass(const upb_MessageDef* m) {
256271

257272
void PyUpb_Descriptor_SetClass(PyObject* py_descriptor, PyObject* meta) {
258273
PyUpb_DescriptorBase* base = (PyUpb_DescriptorBase*)py_descriptor;
259-
Py_XDECREF(base->message_meta);
260-
base->message_meta = meta;
261274
Py_INCREF(meta);
275+
// Py_SETREF replaces strong references without an intermediate invalid
276+
// object state, which code executed by base->message_meta's destructor
277+
// might see, but it's Python 3.8+.
278+
PyObject* tmp = base->message_meta;
279+
base->message_meta = meta;
280+
Py_XDECREF(tmp);
262281
}
263282

264283
// The LookupNested*() functions provide name lookup for entities nested inside

python/descriptor_pool.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ PyObject* PyUpb_DescriptorPool_Get(const upb_DefPool* symtab) {
9999
}
100100

101101
static void PyUpb_DescriptorPool_Dealloc(PyUpb_DescriptorPool* self) {
102+
PyObject_GC_UnTrack(self);
102103
PyUpb_DescriptorPool_Clear(self);
103104
upb_DefPool_Free(self->symtab);
104105
PyUpb_ObjCache_Delete(self->symtab);

python/message.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1850,7 +1850,15 @@ static PyObject* PyUpb_MessageMeta_New(PyTypeObject* type, PyObject* args,
18501850
static void PyUpb_MessageMeta_Dealloc(PyObject* self) {
18511851
PyUpb_MessageMeta* meta = PyUpb_GetMessageMeta(self);
18521852
PyUpb_ObjCache_Delete(meta->layout);
1853-
Py_DECREF(meta->py_message_descriptor);
1853+
// The MessageMeta type is a GC type, which means we should untrack the
1854+
// object before invalidating internal state (so that code executed by the
1855+
// GC doesn't see the invalid state). Unfortunately since we're calling
1856+
// cpython_bits.type_dealloc, which also untracks the object, we can't.
1857+
// Instead just make sure the internal state remains reasonable by using
1858+
// Py_CLEAR(), which sets the struct member to NULL. The tp_traverse and
1859+
// tp_clear methods, which are called by Python's GC, already allow for it
1860+
// to be NULL.
1861+
Py_CLEAR(meta->py_message_descriptor);
18541862
PyTypeObject* tp = Py_TYPE(self);
18551863
cpython_bits.type_dealloc(self);
18561864
Py_DECREF(tp);
@@ -1948,6 +1956,8 @@ static int PyUpb_MessageMeta_Traverse(PyObject* self, visitproc visit,
19481956
}
19491957

19501958
static int PyUpb_MessageMeta_Clear(PyObject* self, visitproc visit, void* arg) {
1959+
PyUpb_MessageMeta* meta = PyUpb_GetMessageMeta(self);
1960+
Py_CLEAR(meta->py_message_descriptor);
19511961
return cpython_bits.type_clear(self);
19521962
}
19531963

0 commit comments

Comments
 (0)