Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUG: Raise recursion error during dimension discovery #17786

Merged
merged 1 commit into from Dec 18, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 34 additions & 5 deletions numpy/core/src/multiarray/ctors.c
Expand Up @@ -741,7 +741,13 @@ discover_dimensions(PyObject *obj, int *maxndim, npy_intp *d, int check_it,
if (!PySequence_Check(obj) ||
PySequence_Length(obj) < 0) {
*maxndim = 0;
PyErr_Clear();
if (PyErr_Occurred()) {
if (PyErr_ExceptionMatches(PyExc_RecursionError) ||
PyErr_ExceptionMatches(PyExc_MemoryError)) {
return -1;
}
PyErr_Clear();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using PyErr_WriteUnraisable(NULL) in place here (and everywhere) might help smoke out the stack overflow

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I did that in most places, but not all. Somehow the places I considered irrelevant (because execution should hit another later) are relevant (or I missed one). I guess creating a nested array with a depth related to the recursion limit, is somehow just too much... Failing hard would likely solve it (i.e. in my python "crash" example, you still need the try/except RecursionError:)

I will first give it one more shot on master later (which is simpler), making the equivalent paths there more strict.

}
return 0;
}

Expand Down Expand Up @@ -780,7 +786,14 @@ discover_dimensions(PyObject *obj, int *maxndim, npy_intp *d, int check_it,
return 0;
}
else if (PyErr_Occurred()) {
/* TODO[gh-14801]: propagate crashes during attribute access? */
/*
* Clear all but clearly fatal errors (previously cleared all).
* 1.20+ does not clear any errors here.
*/
if (PyErr_ExceptionMatches(PyExc_RecursionError) ||
PyErr_ExceptionMatches(PyExc_MemoryError)) {
return -1;
}
PyErr_Clear();
}

Expand Down Expand Up @@ -1740,7 +1753,8 @@ PyArray_GetArrayParamsFromObject_int(PyObject *op,
else {
*out_dtype = NULL;
if (PyArray_DTypeFromObject(op, NPY_MAXDIMS, out_dtype) < 0) {
if (PyErr_ExceptionMatches(PyExc_MemoryError)) {
if (PyErr_ExceptionMatches(PyExc_MemoryError) ||
PyErr_ExceptionMatches(PyExc_RecursionError)) {
return -1;
}
/* Return NPY_OBJECT for most exceptions */
Expand Down Expand Up @@ -2404,7 +2418,14 @@ PyArray_FromInterface(PyObject *origin)
"__array_interface__");
if (iface == NULL) {
if (PyErr_Occurred()) {
PyErr_Clear(); /* TODO[gh-14801]: propagate crashes during attribute access? */
/*
* Clear all but clearly fatal errors (previously cleared all).
* 1.20+ does not clear any errors here.
*/
if (PyErr_ExceptionMatches(PyExc_RecursionError) ||
PyErr_ExceptionMatches(PyExc_MemoryError)) {
return NULL;
}
}
return Py_NotImplemented;
}
Expand Down Expand Up @@ -2672,7 +2693,15 @@ PyArray_FromArrayAttr(PyObject *op, PyArray_Descr *typecode, PyObject *context)
array_meth = PyArray_LookupSpecial_OnInstance(op, "__array__");
if (array_meth == NULL) {
if (PyErr_Occurred()) {
PyErr_Clear(); /* TODO[gh-14801]: propagate crashes during attribute access? */
/*
* Clear all but clearly fatal errors (previously cleared all).
* 1.20+ does not clear any errors here.
*/
if (PyErr_ExceptionMatches(PyExc_RecursionError) ||
PyErr_ExceptionMatches(PyExc_MemoryError)) {
return NULL;
}
PyErr_Clear();
}
return Py_NotImplemented;
}
Expand Down
31 changes: 31 additions & 0 deletions numpy/core/tests/test_multiarray.py
Expand Up @@ -818,6 +818,37 @@ def __array__(self, dtype=None):

assert_raises(ValueError, np.array, x())

@pytest.mark.parametrize("error", [RecursionError, MemoryError])
@pytest.mark.parametrize("attribute",
["__array_interface__", "__array__", "__array_struct__"])
def test_bad_array_like_attributes(self, attribute, error):
# Check that errors during attribute retrieval are raised unless
# they are Attribute errors.

class BadInterface:
def __getattr__(self, attr):
if attr == attribute:
raise error
super().__getattr__(attr)

with pytest.raises(error):
np.array(BadInterface())

@pytest.mark.parametrize("error", [RecursionError, MemoryError])
def test_bad_array_like_bad_length(self, error):
# RecursionError and MemoryError are considered "critical" in
# sequences. We could expand this more generally though. (NumPy 1.20)
class BadSequence:
def __len__(self):
raise error
def __getitem__(self):
# must have getitem to be a Sequence
return 1

with pytest.raises(error):
np.array(BadSequence())


def test_from_string(self):
types = np.typecodes['AllInteger'] + np.typecodes['Float']
nstr = ['123', '123']
Expand Down