Skip to content

check transform mesh shape in _get_transform_mesh - #31894

Merged
ksunden merged 2 commits into
matplotlib:mainfrom
uwezkhan:image-resample-mesh-shape
Jul 10, 2026
Merged

check transform mesh shape in _get_transform_mesh#31894
ksunden merged 2 commits into
matplotlib:mainfrom
uwezkhan:image-resample-mesh-shape

Conversation

@uwezkhan

Copy link
Copy Markdown
Contributor

PR summary

_get_transform_mesh builds an input mesh sized to the output image, hands it to the supplied transform's inverted().transform(), and forwards whatever comes back to the resampler as a flat table of out_h * out_w coordinate pairs. The only check on the returned array is ndim == 2. A non-affine transform whose inverse returns fewer rows than requested, or a trailing dimension other than 2, leaves lookup_distortion in _image_resample.h indexing past the end of that buffer.

Before: an undersized mesh produces an out-of-bounds read during resampling, and with a large output array it segfaults. After: the row and column counts are checked against the requested size and a RuntimeError is raised, alongside the existing ndim check directly above it. The tradeoff is two integer comparisons per non-affine resample; keeping the check next to the mesh construction lets the resampler keep treating the buffer as exactly sized.

Minimal repro on the current tree:

import numpy as np
import matplotlib._image as _image
from matplotlib.transforms import Transform

class Bad(Transform):
    input_dims = output_dims = 2
    def inverted(self): return self
    def transform(self, v): return np.zeros((1, 2))  # should be out_h*out_w rows

_image.resample(np.zeros((10, 10, 4), np.uint8),
                np.zeros((2000, 2000, 4), np.uint8), Bad(),
                interpolation=_image._InterpolationType.NEAREST)  # SIGSEGV

AI Disclosure

PR checklist

@uwezkhan
uwezkhan force-pushed the image-resample-mesh-shape branch from 49a662c to 02b78f3 Compare June 29, 2026 07:53
@uwezkhan

Copy link
Copy Markdown
Contributor Author

The red Tests jobs were the test_mlab TestSpectral fixture errors from pytest 9.1.0, not from this change (test_image passed). That got sorted on main with the pytest unpin, so I rebased onto current main to pick it up and CI should be green now.

@uwezkhan

uwezkhan commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

gentle ping

@QuLogic QuLogic added this to the v3.11.1 milestone Jul 8, 2026

@eeshsaxena eeshsaxena left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed by reading (didn't build the extension locally), and the fix looks correct and worthwhile - this is really a memory-safety guard, not just a nicer error.

Tracing it through: mesh_dims is {dims[0]*dims[1], 2}, i.e. (out_h*out_w, 2), and the input mesh is built at exactly that size. The result is then forwarded via params.transform_mesh = transform_mesh.data() and consumed by the resampler as out_h*out_w coordinate pairs. The only prior validation was ndim == 2, so a (non-affine) inverse transform that returns e.g. (1, 2) passes that check but leaves the resampler reading past the end of the buffer. The new shape(0) != mesh_dims[0] || shape(1) != mesh_dims[1] check rejects exactly that case, and since a genuine 2-D -> 2-D transform must map N points to N points, there's no valid transform this would wrongly reject.

The message and the test line up too: the C++ raises "...should have shape ({}, {}) not ({}, {})" and the test's match="mesh array should have shape" is a substring, with BadMeshTransform.transform returning (1, 2) for a 9x9 output (81 != 1) to trip the new branch specifically.

Two small, optional thoughts:

  • Might be worth a one-line comment noting this guards against an out-of-bounds read downstream, since that's the real motivation.
  • Both the ndim and shape checks now format an error; not worth changing, just noting the shape check makes the failure mode much clearer than a segfault would have.

Nice catch on the buffer over-read.

@uwezkhan

Copy link
Copy Markdown
Contributor Author

Good summary, that matches the intent exactly. Added a one-line comment above the shape check noting it guards the out-of-bounds read in the resampler. Left the two error paths separate since the ndim failure can't report a meaningful shape, so merging them would only blur the messages.

@ksunden
ksunden merged commit d6c3639 into matplotlib:main Jul 10, 2026
40 of 42 checks passed
timhoffm added a commit that referenced this pull request Jul 11, 2026
…894-on-v3.11.x

Backport PR #31894 on branch v3.11.x (check transform mesh shape in _get_transform_mesh)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants