Skip to content

Fix segfault in expand_dims for out of bounds negative axes - #4021

Merged
zcbenz merged 2 commits into
ml-explore:mainfrom
Gusanidas:fix-expand-dims-negative-axes
Aug 7, 2026
Merged

zcbenz merged 2 commits into
ml-explore:mainfrom
Gusanidas:fix-expand-dims-negative-axes

Conversation

@Gusanidas

Copy link
Copy Markdown
Contributor

Proposed changes

Closes #4011

mx.expand_dims(mx.zeros(()), axis=(-3, -2)) segfaults.

Cause

expand_dims(a, axes) normalized negative axes against the output rank and
then passed them to expand_dims_impl, which normalized them a second time
(-3 + 2 = -1, still negative and never range checked, then -1 + 2 = 1). An
out of bounds axis therefore turned silently into a valid but wrong one, and
the axes ended up as {1, 0} instead of sorted. ExpandDims::output_shape
assumes sorted, in bounds axes, so this inserted past the end of an empty shape
and crashed. out_ndim was also size_t, so the new_ax < 0 guard could
never fire.

Fix

Normalize, validate and sort the axes exactly once, in a single helper per op,
reusing the existing normalize_axis_index. Error wording now comes from that
helper; no tests asserted on the old strings.

squeeze had the same double normalization, but failed silently rather than
crashing: mx.squeeze(mx.zeros((1, 1, 1)), (-5,)) returned shape (1, 1)
instead of raising, because -5 mapped to axis 1. Fixed the same way, though
happy to split it out if you would prefer this PR stay scoped to the crash.

Testing

Extended the C++ test squeeze and expand case and added
test_squeeze_expand_invalid_axes in Python, covering out of bounds negative
axes for both ops plus the valid negative axis cases that must keep working.
Both segfault against unpatched ops.cpp and pass with the fix.

Full C++ and Python suites pass on a Metal build; the only failures are torch
MPS DLPack tests, which fail identically without this change.

Checklist

Put an x in the boxes that apply.

  • I have read the CONTRIBUTING document
  • I have run pre-commit run --all-files to format my code / installed pre-commit prior to committing changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the necessary documentation (if needed)

expand_dims(a, axes) normalized negative axes against the output rank and
then passed them to expand_dims_impl, which normalized them a second time.
A negative axis that was still negative after the first pass was silently
mapped to a valid but wrong axis, and the resulting axes were no longer
sorted. ExpandDims::output_shape assumes sorted in bounds axes, so
mx.expand_dims(mx.zeros(()), (-3, -2)) inserted past the end of the shape
and crashed.

Normalize, validate and sort the axes once via normalize_axis_index. The
same double normalization was present in squeeze, where it silently
squeezed the wrong axis instead of crashing.

@zcbenz zcbenz 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.

Nice fix, thanks!

@zcbenz
zcbenz merged commit f754f31 into ml-explore:main Aug 7, 2026
28 checks passed
Adityaj0 added a commit to Adityaj0/mlx that referenced this pull request Aug 10, 2026
…ong_axis

The bounds check in take_along_axis, scatter_axis (which backs put_along_axis
and scatter_add_axis) and linalg::cross was dead code:

    if (axis + a.ndim() < 0 || axis >= static_cast<int>(a.ndim()))

array::ndim() returns size_t, so `axis + a.ndim()` is evaluated in unsigned
arithmetic and is never negative. Only the positive half of the check, which
casts to int, actually ran. A negative out of bounds axis therefore flowed
through to broadcast_arrays as `axis - int(a.ndim())` and either raised an
internal "SmallVector out of range" error or, for larger magnitudes,
overwrote the stack and segfaulted inside broadcast_shapes.

Use normalize_axis_index, which does the comparison in int and is already
used for this purpose elsewhere in ops.cpp. It also normalizes, so the later
manual "allow negative axis" adjustments are no longer needed.

Same class of bug as ml-explore#4021.

Co-Authored-By: Claude Opus 5 <[email protected]>
Adityaj0 added a commit to Adityaj0/mlx that referenced this pull request Aug 10, 2026
…ong_axis

The bounds check in take_along_axis, scatter_axis (which backs put_along_axis
and scatter_add_axis) and linalg::cross was dead code:

    if (axis + a.ndim() < 0 || axis >= static_cast<int>(a.ndim()))

array::ndim() returns size_t, so `axis + a.ndim()` is evaluated in unsigned
arithmetic and is never negative. Only the positive half of the check, which
casts to int, actually ran. A negative out of bounds axis therefore flowed
through to broadcast_arrays as `axis - int(a.ndim())` and either raised an
internal "SmallVector out of range" error or, for larger magnitudes,
overwrote the stack and segfaulted inside broadcast_shapes.

Use normalize_axis_index, which does the comparison in int and is already
used for this purpose elsewhere in ops.cpp. It also normalizes, so the later
manual "allow negative axis" adjustments are no longer needed.

Same class of bug as ml-explore#4021.
@BrewTestBot BrewTestBot mentioned this pull request Aug 18, 2026
1 task done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] expand_dims() seg faults for invalid inputs.

2 participants