Skip to content

Fix np.clip writing past the end of a caller-provided out= array - #10761

Merged
esc merged 2 commits into
numba:mainfrom
nicoseijas:fix-10682-clip-out-shape
Aug 19, 2026
Merged

esc merged 2 commits into
numba:mainfrom
nicoseijas:fix-10682-clip-out-shape

Conversation

@nicoseijas

@nicoseijas nicoseijas commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Problem

np.clip with an explicit out= never checked that out matches the shape of the result. The loop runs over the shape of the result and writes into out regardless of its size, so a too-small out is written past its end.
NumPy raises a ValueError for the same call, because it broadcasts the inputs onto the output but never stretches the output itself.

a = np.arange(5.0)
buf = np.full(8, 999.0)

@njit
def clip_out(a, a_min, a_max, out):
    return np.clip(a, a_min, a_max, out)

clip_out(a, 0.0, 3.0, buf[:3])
# buf is now [0. 1. 2. 3. 3. 999. 999. 999.]
#                     ^^^^^^ written outside the 3-element `out`

The returned array is also wrong, and nothing is raised. An out that 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.clip has its own implementation and does not go through _build_array, so that fix does not cover it.

Closes #10682.

What changed

out is now validated against the shape of the result before anything is written, and a mismatch raises ValueError. The check is in a small helper that replaces the np.empty_like(a) if out is None else out line repeated across the implementation branches, so all of them are covered: both scalar bounds, either bound None, either or both bounds arrays, and the np.clip, out= keyword and ndarray.clip spellings.

A mismatch in the number of dimensions is reported at typing time instead, since out is never broadcast. That case already failed to compile, but with No implementation of function clip rather than anything pointing at out.

Allocation is unchanged: when out is None the result still comes from np.empty_like(a), so the layout of the returned array is the same as before.

Verification

  • Three regression tests in numba/tests/test_array_methods.py, covering every implementation branch through all four call spellings, out both too small and too large, the dimension mismatch, and a check that a buffer adjacent to a too-small out is left untouched. All three fail on main and pass with the change.
  • numba.tests.test_array_methods (83 tests) and numba.tests.test_ufuncs.TestUFuncs (87 tests) pass.
  • flake8 clean on both changed files.
  • Run on Windows, Python 3.13, NumPy 2.4.6, llvmlite 0.50.0dev0.

Notes for the reviewer

  • The issue reports that boundscheck=True changes the result. On main it does not raise at all: the indices are generated inside the @overload implementation, which the flag does not reach. Worth knowing when reading the original report, but out of scope here.
  • This touches the same lines as Fix np.clip type promotion to match NumPy behavior (Fixes #10000) #10594. The changes are independent and the textual conflict is small, but whichever lands second will need a rebase.
  • np.clip has a second, related defect on the out=None path: the result is allocated with np.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])

nicoseijas and others added 2 commits August 5, 2026 11:54
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>
@esc

esc commented Aug 5, 2026

Copy link
Copy Markdown
Member

@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!

@nicoseijas

Copy link
Copy Markdown
Contributor Author

@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 esc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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)

@esc

esc commented Aug 19, 2026

Copy link
Copy Markdown
Member

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?

Oh I see, this is an additional fix beyond the scope of #10682 ?

@nicoseijas

Copy link
Copy Markdown
Contributor Author

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?

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

@esc

esc commented Aug 19, 2026

Copy link
Copy Markdown
Member

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?

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 esc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 ✗ ⚠️ compiled and wrote past the end of 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).

@esc esc added 5 - Ready to merge Review and testing done, is ready to merge and removed 4 - Waiting on reviewer Waiting for reviewer to respond to author labels Aug 19, 2026
@esc
esc merged commit e43bb95 into numba:main Aug 19, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

5 - Ready to merge Review and testing done, is ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

np.clip with a too-small out= silently writes out of bounds under njit

2 participants