@@ -2047,8 +2047,40 @@ arraydescr_dealloc(PyArray_Descr *self)
20472047 Py_XDECREF (lself -> fields );
20482048 if (lself -> subarray ) {
20492049 Py_XDECREF (lself -> subarray -> shape );
2050- Py_DECREF (lself -> subarray -> base );
2050+ /*
2051+ * A subarray dtype's base may itself be a subarray dtype, so
2052+ * decref'ing the base here can re-enter this function, one C
2053+ * stack frame per nesting level. Unwind the chain iteratively
2054+ * instead; at refcount 1 this dealloc holds the only
2055+ * reference (descriptors support neither weakrefs nor GC), so
2056+ * stealing the link is unobservable and each node still runs
2057+ * its own, now shallow, dealloc.
2058+ *
2059+ * If descriptors ever get the Py_TPFLAGS_HAVE_GC flag, we can
2060+ * use CPython's stack protection via the trashcan macros
2061+ * instead.
2062+ */
2063+ PyArray_Descr * base = lself -> subarray -> base ;
20512064 PyArray_free (lself -> subarray );
2065+ /*
2066+ * The Py_REFCNT(..) == 1 check is intentional. This happens in
2067+ * a deallocator for a type that doesn't support weakrefs and
2068+ * isn't a GC type, so it's impossible to get here with a refcount
2069+ * of 1 without us being the only owner. We can't use
2070+ * PyUnstable_Object_IsUniquelyReferenced because that excludes
2071+ * objects on remote threads.
2072+ */
2073+ while (base != NULL && Py_REFCNT (base ) == 1 && PyDataType_HASSUBARRAY (base )) {
2074+ _PyArray_LegacyDescr * lbase = (_PyArray_LegacyDescr * )base ;
2075+ // steal reference owned by lbase and stash it in base
2076+ // (Py_CLEAR without a DECREF)
2077+ base = lbase -> subarray -> base ;
2078+ lbase -> subarray -> base = NULL ;
2079+ // lbase no longer owns a reference to base, so base's deallocator
2080+ // doesn't fire
2081+ Py_DECREF (lbase );
2082+ }
2083+ Py_XDECREF (base );
20522084 }
20532085 Py_XDECREF (lself -> metadata );
20542086 NPY_AUXDATA_FREE (lself -> c_metadata );
0 commit comments