BUG: Segfault in nditer buffer dealloc for Object arrays #18469
Merged
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Backport of #18450.
I get a segfault in 1.20.0 and in master involving nditer and object arrays. It doesn't seem to happen in 1.18. The cause seems to be some combination of incomplete error handling in nditer ( known problem which @seberg seems to have been working on fixing a few months ago), as well as mis-detection of buffer non-initialization.
This PR has two changes:
npyiter_clear_buffers
to avoid clearing unused buffers, which is the main fix. I am not 100% on correctness since I'm not yet sure of the lifetime of validity of theNPY_OP_ITFLAG_USINGBUFFER
flag: Can it get set/unset between the call tonpyiter_copy_to_buffers
(where it is set) andnpyiter_clear_buffers
(which does the check)?PyErr_Occurred
in ufunc inner-loop code. While I think there is a problem with fall-through errors, I am not sure if this is the right fix: Are there performance implications? Also, if this is a correct change, I think I'd need to do it elsewhere in the file where nditer is used.Reproducing code example:
This code should return an error,
TypeError: unsupported operand type(s) for *: 'float' and 'NoneType'
, however it often segfaults. The segfault depends on malloc'ing some memory with uninitualized non-null bytes, so you may need to vary the number 17000 two or three times to trigger a different malloc. The second array should be a 0d (and not 1d) object array. The segfaulting code was recently touched in #17029 (seenpyiter_clear_buffers
in backtrace below).Debug Info + Discussion
What appears to happen in the example code:
None
is converted to a 0d object array. Buffers of 'O' dtype of size 8192 are created for both of the inputs and the one output. (sidenote: can we update nditer to avoid creating a 64k buffer for the 0d array (op 1) which we never even use?)innerloop
fails due to an invalid python operation (float*None), and sets PyErrnpyiter_buffered_iternext
) copies the output buffer to the output array (no problem)NPY_OP_ITFLAG_USINGBUFFER
for that op._aligned_contig_to_contig_cast
. After successfully doing the copy, this function checks ifPyErr_Occurred
, which incorrectly triggers due to the previously set error from innerloop.npyiter_clear_buffers
) and return failure.npyiter_clear_buffers
tries to zero-out op 1's buffer, which involves decref'ing each element since it is Object dtype. However, this buffer was never initialized, so depending on malloc has random addresses, so this often segfaults.GDB traceback:
Additionally, this is the output with
NPY_IT_DBG_TRACING
andNPY_UF_DBG_TRACING
turned on:It doesn't show it, but I in gdb I can see the segfault happens when clearing op 1's buffer
NumPy/Python version information:
1.20.0 and master