Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion numpy/_core/src/multiarray/descriptor.c
Original file line number Diff line number Diff line change
Expand Up @@ -2047,8 +2047,40 @@ arraydescr_dealloc(PyArray_Descr *self)
Py_XDECREF(lself->fields);
if (lself->subarray) {
Py_XDECREF(lself->subarray->shape);
Py_DECREF(lself->subarray->base);
/*
* A subarray dtype's base may itself be a subarray dtype, so
* decref'ing the base here can re-enter this function, one C
* stack frame per nesting level. Unwind the chain iteratively
* instead; at refcount 1 this dealloc holds the only
* reference (descriptors support neither weakrefs nor GC), so
* stealing the link is unobservable and each node still runs
* its own, now shallow, dealloc.
*
* If descriptors ever get the Py_TPFLAGS_HAVE_GC flag, we can
* use CPython's stack protection via the trashcan macros
* instead.
*/
PyArray_Descr *base = lself->subarray->base;
PyArray_free(lself->subarray);
/*
* The Py_REFCNT(..) == 1 check is intentional. This happens in
* a deallocator for a type that doesn't support weakrefs and
* isn't a GC type, so it's impossible to get here with a refcount
* of 1 without us being the only owner. We can't use
* PyUnstable_Object_IsUniquelyReferenced because that excludes
* objects on remote threads.
*/
while (base != NULL && Py_REFCNT(base) == 1 && PyDataType_HASSUBARRAY(base)) {
_PyArray_LegacyDescr *lbase = (_PyArray_LegacyDescr *)base;
// steal reference owned by lbase and stash it in base
// (Py_CLEAR without a DECREF)
base = lbase->subarray->base;
lbase->subarray->base = NULL;
// lbase no longer owns a reference to base, so base's deallocator
// doesn't fire
Py_DECREF(lbase);
}
Py_XDECREF(base);
}
Py_XDECREF(lself->metadata);
NPY_AUXDATA_FREE(lself->c_metadata);
Expand Down
27 changes: 27 additions & 0 deletions numpy/_core/tests/test_dtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from numpy.testing import (
HAS_REFCOUNT,
IS_64BIT,
IS_WASM,
assert_,
assert_array_equal,
assert_equal,
Expand Down Expand Up @@ -897,6 +898,32 @@ def test_tuple_recursion(self):
with contextlib.suppress(RecursionError):
np.dtype(d)

@pytest.mark.thread_unsafe(reason="Sets global threading stack size")
@pytest.mark.skipif(IS_WASM, reason="wasm doesn't have support for threads")
def test_deep_subarray_dtype_dealloc(self):
import threading

import numpy as np

def build_and_drop():
d = np.dtype(np.int32)
for _ in range(200000):
d = np.dtype((d, (1,)))
# hold a second reference so teardown exercises stopping
# and resuming
held = d.base
del d
del held

# small stack size to fail reliably if deallocation is recusive
old_stack_size = threading.stack_size(1024 * 1024)
try:
t = threading.Thread(target=build_and_drop)
t.start()
t.join()
finally:
threading.stack_size(old_stack_size)

@requires_deep_recursion
def test_dict_recursion(self):
d = {"names": ['self'], "formats": [None], "offsets": [0]}
Expand Down
Loading