Repository navigation
Fix np.clip writing past the end of a caller-provided out= array - #10761
Conversation
np.clip never checked that an explicit out= matches the shape of the result. The loop runs over the result shape and writes into out regardless of its size, so a too-small out is written past its end and a wrong array is returned with no error. NumPy raises a ValueError, since it broadcasts the inputs onto the output but never stretches the output itself. Validate out against the result shape before writing, in a helper that replaces the repeated allocation line so every implementation branch is covered. A mismatch in the number of dimensions is reported at typing time instead, as out is never broadcast. Allocation on the out=None path is unchanged. Closes numba#10682 Assisted-by: Claude Code Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Assisted-by: Claude Code Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@nicoseijas thanks, can you update the comment to include which Model exactly was used via Claude code? Thank you. For reference, our AI tools policy is located here: https://numba.readthedocs.io/en/latest/reference/ai_tools_policy.html Please double check that this PR complies with those rules and comment to confirm, thank you! |
Hi, I've updated the comment in the PR description to name the model. Sorry about that |
esc
left a comment
There was a problem hiding this comment.
So, this looks mostly fine, issues that were ignored silently are now explicit exceptions:
However, one of the tests gives an unexpected error message. Can you explain why it doesn't fail silently?
esc@artemis [numba_3.14] [numba:fix-10682-clip-out-shape:★★★₃★] ~/git/numba python -m numba.runtests numba.tests.test_array_methods.TestArrayMethods.test_clip_out_bad_ndim
F
======================================================================
FAIL: test_clip_out_bad_ndim (numba.tests.test_array_methods.TestArrayMethods.test_clip_out_bad_ndim)
----------------------------------------------------------------------
numba.core.errors.TypingError: Failed in nopython mode pipeline (step: nopython frontend)
No implementation of function Function(<function clip at 0x103016980>) found for signature:
>>> clip(array(float64, 1d, C), float64, float64, array(float64, 2d, C))
There are 2 candidate implementations:
- Of which 2 did not match due to:
Overload in function 'np_clip': File: numba/np/arrayobj.py: Line 2673.
With argument(s): '(array(float64, 1d, C), float64, float64, array(float64, 2d, C))':
Rejected as the implementation raised a specific error:
TypingError: Failed in nopython mode pipeline (step: nopython frontend)
Cannot unify array(float64, 2d, C) and array(float64, 1d, C) for '$phi58.0.2', defined at /Users/esc/git/numba/numba/np/arrayobj.py (2711)
File "numba/np/arrayobj.py", line 2711:
def np_clip_ss(a, a_min, a_max, out=None):
<source elided>
# so broadcasting is not needed at all
ret = np.empty_like(a) if out is None else out
^
During: typing of assignment at /Users/esc/git/numba/numba/np/arrayobj.py (2711)
File "numba/np/arrayobj.py", line 2711:
def np_clip_ss(a, a_min, a_max, out=None):
<source elided>
# so broadcasting is not needed at all
ret = np.empty_like(a) if out is None else out
^
During: Pass nopython_type_inference
raised from /Users/esc/git/numba/numba/core/typeinfer.py:1083
During: resolving callee type: Function(<function clip at 0x103016980>)
During: typing of call at /Users/esc/git/numba/numba/tests/test_array_methods.py (249)
File "numba/tests/test_array_methods.py", line 249:
def np_clip(a, a_min, a_max, out=None):
return np.clip(a, a_min, a_max, out)
^
During: Pass nopython_type_inference
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/Users/esc/git/numba/numba/tests/test_array_methods.py", line 1870, in test_clip_out_bad_ndim
with self.assertRaisesRegex(TypingError, msg):
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
AssertionError: ".*The argument "out" must have the same number of dimensions.*" does not match "Failed in nopython mode pipeline (step: nopython frontend)
No implementation of function Function(<function clip at 0x103016980>) found for signature:
>>> clip(array(float64, 1d, C), float64, float64, array(float64, 2d, C))
There are 2 candidate implementations:
- Of which 2 did not match due to:
Overload in function 'np_clip': File: numba/np/arrayobj.py: Line 2673.
With argument(s): '(array(float64, 1d, C), float64, float64, array(float64, 2d, C))':
Rejected as the implementation raised a specific error:
TypingError: Failed in nopython mode pipeline (step: nopython frontend)
Cannot unify array(float64, 2d, C) and array(float64, 1d, C) for '$phi58.0.2', defined at /Users/esc/git/numba/numba/np/arrayobj.py (2711)
File "numba/np/arrayobj.py", line 2711:
def np_clip_ss(a, a_min, a_max, out=None):
<source elided>
# so broadcasting is not needed at all
ret = np.empty_like(a) if out is None else out
^
During: typing of assignment at /Users/esc/git/numba/numba/np/arrayobj.py (2711)
File "numba/np/arrayobj.py", line 2711:
def np_clip_ss(a, a_min, a_max, out=None):
<source elided>
# so broadcasting is not needed at all
ret = np.empty_like(a) if out is None else out
^
During: Pass nopython_type_inference
raised from /Users/esc/git/numba/numba/core/typeinfer.py:1083
During: resolving callee type: Function(<function clip at 0x103016980>)
During: typing of call at /Users/esc/git/numba/numba/tests/test_array_methods.py (249)
File "numba/tests/test_array_methods.py", line 249:
def np_clip(a, a_min, a_max, out=None):
return np.clip(a, a_min, a_max, out)
^
During: Pass nopython_type_inference"
----------------------------------------------------------------------
Ran 1 test in 0.290s
FAILED (failures=1)
Oh I see, this is an additional fix beyond the scope of #10682 ? |
Yes, sorry! I should have been more explicit. Correct, It's an additional scope. If it's against the project convention I can remove it |
No worries, that's fine. |
esc
left a comment
There was a problem hiding this comment.
I've reviewed this on my own and with Kilo (Kimi K3) and I'm pretty confident that it's fine.
I would like to approve this, since it clearly improves the situation. In addition I would like to post this short AI snippet about out=:
Non-blocking discussion item — this PR codifies a stricter out= rule than both NumPy and numba's own ufunc path.
NumPy does not require out.shape == broadcast(inputs); it requires out to be a valid broadcast target — extra leading dims are accepted and the result is written through:
import numpy as np
out = np.full((2, 5), -1.0)
np.clip(np.arange(5.0), 0.0, 3.0, out=out)
# inputs broadcast to (5,); out is (2,5) -> ACCEPTED, result tiled across both rows:
# [[0. 1. 2. 3. 3.]
# [0. 1. 2. 3. 3.]]Numba's ufunc machinery agrees with NumPy — np.add(a_1d, 1.0, out_2x5) compiles and runs correctly under @njit. Post-PR clip rejects the equivalent call shape with a typing-time error.
Comparison for a of shape (5,):
out shape |
NumPy | numba np.add (ufunc path) |
numba clip, pre-PR | numba clip, post-PR |
|---|---|---|---|---|
(5,) (exact) |
✓ | ✓ | ✓ | ✓ |
(2,5) (larger target) |
✓ | ✓ | ✗ opaque TypingError: No implementation of function clip |
✗ clear TypingError (ndim mismatch) |
(3,) (too small) |
✗ ValueError |
✗ | out (the bug this PR fixes) |
✗ clear ValueError |
This is not a regression: ndim-mismatched out never compiled before either — pre-PR it died with the opaque No implementation error, as a side effect of the IfExp in the typing code (np.empty_like(a) if out is None else out) forcing empty_like(a) and out to unify. The PR strictly improves the error for a call that was already unsupported.
That said, the exact-shape rule is now baked into _np_clip_prepare_out and an explicit typing check, which makes the divergence from NumPy/ufunc semantics more permanent than the accidental unification failure was — a future reader could mistake the check for a statement of intended semantics rather than a stopgap. Supporting broadcast-target out properly requires the same loop/allocation redesign as #10760 (the broadcast result shape can exceed a.shape).
Problem
np.clipwith an explicitout=never checked thatoutmatches the shape of the result. The loop runs over the shape of the result and writes intooutregardless of its size, so a too-smalloutis written past its end.NumPy raises a
ValueErrorfor the same call, because it broadcasts the inputs onto the output but never stretches the output itself.The returned array is also wrong, and nothing is raised. An
outthat is too large is accepted as well, where NumPy raises.This is the same class of defect as #9166, fixed for the ufunc path in #10671.
np.cliphas its own implementation and does not go through_build_array, so that fix does not cover it.Closes #10682.
What changed
outis now validated against the shape of the result before anything is written, and a mismatch raisesValueError. The check is in a small helper that replaces thenp.empty_like(a) if out is None else outline repeated across the implementation branches, so all of them are covered: both scalar bounds, either boundNone, either or both bounds arrays, and thenp.clip,out=keyword andndarray.clipspellings.A mismatch in the number of dimensions is reported at typing time instead, since
outis never broadcast. That case already failed to compile, but withNo implementation of function cliprather than anything pointing atout.Allocation is unchanged: when
outis None the result still comes fromnp.empty_like(a), so the layout of the returned array is the same as before.Verification
numba/tests/test_array_methods.py, covering every implementation branch through all four call spellings,outboth too small and too large, the dimension mismatch, and a check that a buffer adjacent to a too-smalloutis left untouched. All three fail onmainand pass with the change.numba.tests.test_array_methods(83 tests) andnumba.tests.test_ufuncs.TestUFuncs(87 tests) pass.flake8clean on both changed files.Notes for the reviewer
boundscheck=Truechanges the result. Onmainit does not raise at all: the indices are generated inside the@overloadimplementation, which the flag does not reach. Worth knowing when reading the original report, but out of scope here.np.cliphas a second, related defect on theout=Nonepath: the result is allocated withnp.empty_like(a)while the loop runs over the broadcast shape, which can be larger. Reported separately in np.clip returns an array with the wrong shape when a_min or a_max broadcasts to a larger result #10760 and deliberately left out of this PR, because fixing it changes the layout and shape of returned arrays.Assisted-by: Claude Code (Claude Opus 5, model ID claude-opus-5[1m])