Visitar URL original
Merge pull request #32158 from charris/backport-32133 · numpy/numpy@988d94d · GitHub
Skip to content

Commit 988d94d

Browse files
authored
Merge pull request #32158 from charris/backport-32133
BUG: avoid possible stack overflow in arraydescr_dealloc (#32133)
2 parents aa5d2bf + b2e4f97 commit 988d94d

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

‎numpy/_core/src/multiarray/descriptor.c‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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);

‎numpy/_core/tests/test_dtype.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from numpy.testing import (
2222
HAS_REFCOUNT,
2323
IS_64BIT,
24+
IS_WASM,
2425
assert_,
2526
assert_array_equal,
2627
assert_equal,
@@ -897,6 +898,32 @@ def test_tuple_recursion(self):
897898
with contextlib.suppress(RecursionError):
898899
np.dtype(d)
899900

901+
@pytest.mark.thread_unsafe(reason="Sets global threading stack size")
902+
@pytest.mark.skipif(IS_WASM, reason="wasm doesn't have support for threads")
903+
def test_deep_subarray_dtype_dealloc(self):
904+
import threading
905+
906+
import numpy as np
907+
908+
def build_and_drop():
909+
d = np.dtype(np.int32)
910+
for _ in range(200000):
911+
d = np.dtype((d, (1,)))
912+
# hold a second reference so teardown exercises stopping
913+
# and resuming
914+
held = d.base
915+
del d
916+
del held
917+
918+
# small stack size to fail reliably if deallocation is recusive
919+
old_stack_size = threading.stack_size(1024 * 1024)
920+
try:
921+
t = threading.Thread(target=build_and_drop)
922+
t.start()
923+
t.join()
924+
finally:
925+
threading.stack_size(old_stack_size)
926+
900927
@requires_deep_recursion
901928
def test_dict_recursion(self):
902929
d = {"names": ['self'], "formats": [None], "offsets": [0]}

0 commit comments

Comments
 (0)