From 86a060174fcf858379c67a826b94306fb9f55e68 Mon Sep 17 00:00:00 2001 From: titaiwangms Date: Thu, 25 Jun 2026 01:06:12 +0000 Subject: [PATCH 1/3] Validate GridSample input spatial dimensions are non-empty (CPU and CUDA kernels) Add ORT_RETURN_IF_NOT guards that every input spatial dimension is greater than zero in GridSample::Compute (CPU) and GridSample::ComputeInternal (CUDA), covering 2D and 3D-spatial inputs in both NCHW and NHWC layouts. The output is sized by the grid, so a zero-size input spatial dimension would otherwise skip the empty-output early return and reach interpolation with an invalid extent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- onnxruntime/core/providers/cpu/tensor/grid_sample.cc | 8 ++++++++ onnxruntime/core/providers/cuda/tensor/grid_sample.cc | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/onnxruntime/core/providers/cpu/tensor/grid_sample.cc b/onnxruntime/core/providers/cpu/tensor/grid_sample.cc index e80ecd8fee7fe..7fe1b423a128c 100644 --- a/onnxruntime/core/providers/cpu/tensor/grid_sample.cc +++ b/onnxruntime/core/providers/cpu/tensor/grid_sample.cc @@ -379,6 +379,14 @@ Status GridSample::Compute(OpKernelContext* context) const { ORT_ENFORCE(input_dims.NumDimensions() == 4 || input_dims.NumDimensions() == 5, "Only 4-D or 5-D tensor is supported"); + // Spatial dimensions must be non-empty for sampling: the output is sized by the grid, so a zero-size + // input spatial dimension would otherwise lead to invalid index computations during interpolation. + for (size_t i = 2; i < input_dims.NumDimensions(); ++i) { + ORT_RETURN_IF_NOT(input_dims[i] > 0, + "Input spatial dimensions must be non-empty for sampling. Dimension ", i, + " has size ", input_dims[i]); + } + auto N = input_dims[0]; auto C = input_dims[1]; ORT_ENFORCE(grid_dims[0] == N, "Grid batch size ", grid_dims[0], " does not match input batch size ", N); diff --git a/onnxruntime/core/providers/cuda/tensor/grid_sample.cc b/onnxruntime/core/providers/cuda/tensor/grid_sample.cc index d97d5fcbb0b5b..e23b5bad573f7 100755 --- a/onnxruntime/core/providers/cuda/tensor/grid_sample.cc +++ b/onnxruntime/core/providers/cuda/tensor/grid_sample.cc @@ -124,6 +124,17 @@ Status GridSample::ComputeInternal(OpKernelContext* context) const { return Status(common::ONNXRUNTIME, common::NOT_IMPLEMENTED, "Cubic mode is only supported in 4-D cases."); } + // Spatial dimensions must be non-empty for sampling: the output is sized by the grid, so a zero-size + // input spatial dimension would otherwise lead to invalid index computations during interpolation. + // Spatial dims are [1, rank-1) for NHWC (N,...,C) and [2, rank) for NCHW (N,C,...). + const size_t spatial_begin = IsNHWC ? 1 : 2; + const size_t spatial_end = IsNHWC ? dims_input.size() - 1 : dims_input.size(); + for (size_t i = spatial_begin; i < spatial_end; ++i) { + ORT_RETURN_IF_NOT(dims_input[i] > 0, + "Input spatial dimensions must be non-empty for sampling. Dimension ", i, + " has size ", dims_input[i]); + } + using Ch = Channels; TensorShapeVector dims_output(dims_input.size()); From cc7f3101998b53415bbe808057339e376a9b566d Mon Sep 17 00:00:00 2001 From: titaiwangms Date: Thu, 25 Jun 2026 01:05:39 +0000 Subject: [PATCH 2/3] Validate GridSample input spatial dimensions are non-empty GridSample sizes its output from the grid, so a zero-size input spatial dimension skips the empty-output early return and reaches the clamp/reflect index computation with an invalid extent. Add an ORT_RETURN_IF_NOT check that every input spatial dimension is greater than zero in both the CPU Compute and the CUDA ComputeInternal (covering 2D and 3D-spatial inputs, NCHW and NHWC layouts), with a consistent diagnostic message. Add an expect-failure OpTester case that fans across the registered CPU and CUDA execution providers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Agent-signed-off: Developer (cc69a935) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cpu/tensor/grid_sample_test_custom.cc | 248 +++++++++++++++++- 1 file changed, 247 insertions(+), 1 deletion(-) diff --git a/onnxruntime/test/providers/cpu/tensor/grid_sample_test_custom.cc b/onnxruntime/test/providers/cpu/tensor/grid_sample_test_custom.cc index fe4f551046474..43bc761891bbb 100644 --- a/onnxruntime/test/providers/cpu/tensor/grid_sample_test_custom.cc +++ b/onnxruntime/test/providers/cpu/tensor/grid_sample_test_custom.cc @@ -63,6 +63,17 @@ void RunCpuOnly(T& test) { RunTests(test, GetCpuExecutionProviders()); } +template +void RunTestsExpectFailure(T& test, const std::string& error_msg, + std::vector>&& execution_providers) { + for (size_t idx = 0; idx < execution_providers.size(); ++idx) { + test.Config(BaseTester::ExpectResult::kExpectFailure, error_msg) + .ConfigEp(std::move(execution_providers[idx])) + .RunWithConfig(); + } + execution_providers.clear(); +} + // Execution providers targeted by the float->int64 cast hardening in the CUDA GridSample // kernels (onnxruntime/core/providers/cuda/tensor/grid_sample_impl.cu). CoreML and WebGPU // are intentionally excluded here: their integer-conversion semantics differ from the @@ -602,10 +613,245 @@ TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_nearest_reflection_mixed RunCpuOnly(test); } +// ----------------------------------------------------------------------------------------- +// Non-empty input spatial-dimension validation tests: a zero-size spatial dim must be +// rejected before sampling (guard fires host-side across CPU/CUDA/WebGPU). +// ----------------------------------------------------------------------------------------- +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_nearest_border_empty_spatial_rejected) { + // An input with a zero-size spatial dimension is rejected: the output is sized by the grid, + // so a non-empty grid skips the empty-output early return, and spatial dimensions must be + // non-empty for sampling to produce valid sample indices. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("nearest")); + test.AddAttribute("padding_mode", std::string("border")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 0, 5}; + std::initializer_list X_data{}; + std::initializer_list Grid_shape{1, 2, 2, 2}; + std::initializer_list Grid_data{ + TypeParam(0.0f), TypeParam(0.0f), TypeParam(-1.0f), TypeParam(-1.0f), + TypeParam(1.0f), TypeParam(1.0f), TypeParam(0.5f), TypeParam(-0.5f)}; + std::initializer_list Y_shape{1, 1, 2, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + test.AddOutput("Y", Y_shape, + {TypeParam(0.0f), TypeParam(0.0f), TypeParam(0.0f), TypeParam(0.0f)}); + // The zero-spatial-dim guard currently covers the CPU and CUDA kernels; other EPs (WebGPU/CoreML) + // are out of scope for this test, so restrict the expect-failure run to CPU + CUDA only. + RunTestsExpectFailure(test, "Input spatial dimensions must be non-empty for sampling", + GetCpuAndCudaExecutionProviders()); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_5D_nearest_border_empty_spatial_rejected) { + // Same rejection as the 4-D case, but for a 5-D input with a zero-size spatial dimension. This keeps + // the 5-D branch of the guard (and the CUDA NCHW/NHWC layouts) covered: the output is sized by the + // grid, so a non-empty grid skips the empty-output early return, and spatial dimensions must be + // non-empty for sampling to produce valid sample indices. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("nearest")); + test.AddAttribute("padding_mode", std::string("border")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 0, 2, 2}; + std::initializer_list X_data{}; + std::initializer_list Grid_shape{1, 1, 1, 2, 3}; + std::initializer_list Grid_data{ + TypeParam(0.0f), TypeParam(0.0f), TypeParam(0.0f), + TypeParam(1.0f), TypeParam(1.0f), TypeParam(1.0f)}; + std::initializer_list Y_shape{1, 1, 1, 1, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + test.AddOutput("Y", Y_shape, {TypeParam(0.0f), TypeParam(0.0f)}); + // Restrict the expect-failure run to CPU + CUDA only, matching the 4-D case above. + RunTestsExpectFailure(test, "Input spatial dimensions must be non-empty for sampling", + GetCpuAndCudaExecutionProviders()); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_nearest_border_empty_last_spatial_rejected) { + // Zero-size trailing spatial dimension (W = 0), confirming the guard loop catches a non-first + // spatial dimension, not just the leading one. This runs on the CUDA NHWC provider as well: for a + // logical NCHW {1, 1, 5, 0} input the internal NHWC layout transform places the zero at a spatial + // index in the (N, H, W, C) layout, so it exercises the layout-aware spatial range on both kernels. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("nearest")); + test.AddAttribute("padding_mode", std::string("border")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 5, 0}; + std::initializer_list X_data{}; + std::initializer_list Grid_shape{1, 2, 2, 2}; + std::initializer_list Grid_data{ + TypeParam(0.0f), TypeParam(0.0f), TypeParam(-1.0f), TypeParam(-1.0f), + TypeParam(1.0f), TypeParam(1.0f), TypeParam(0.5f), TypeParam(-0.5f)}; + std::initializer_list Y_shape{1, 1, 2, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + test.AddOutput("Y", Y_shape, + {TypeParam(0.0f), TypeParam(0.0f), TypeParam(0.0f), TypeParam(0.0f)}); + RunTestsExpectFailure(test, "Input spatial dimensions must be non-empty for sampling", + GetCpuAndCudaExecutionProviders()); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_nearest_border_all_spatial_zero_rejected) { + // Both spatial dimensions are zero ({1, 1, 0, 0}); the guard must reject on the first zero dimension + // without double-reporting or crashing. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("nearest")); + test.AddAttribute("padding_mode", std::string("border")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 0, 0}; + std::initializer_list X_data{}; + std::initializer_list Grid_shape{1, 2, 2, 2}; + std::initializer_list Grid_data{ + TypeParam(0.0f), TypeParam(0.0f), TypeParam(-1.0f), TypeParam(-1.0f), + TypeParam(1.0f), TypeParam(1.0f), TypeParam(0.5f), TypeParam(-0.5f)}; + std::initializer_list Y_shape{1, 1, 2, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + test.AddOutput("Y", Y_shape, + {TypeParam(0.0f), TypeParam(0.0f), TypeParam(0.0f), TypeParam(0.0f)}); + RunTestsExpectFailure(test, "Input spatial dimensions must be non-empty for sampling", + GetCpuAndCudaExecutionProviders()); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_5D_nearest_border_middle_spatial_zero_rejected) { + // 5-D input with a zero in a non-first spatial dimension ({1, 1, 2, 0, 3}), confirming the guard + // spans the full spatial range of a 5-D tensor rather than only checking the first spatial index. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("nearest")); + test.AddAttribute("padding_mode", std::string("border")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 2, 0, 3}; + std::initializer_list X_data{}; + std::initializer_list Grid_shape{1, 1, 1, 2, 3}; + std::initializer_list Grid_data{ + TypeParam(0.0f), TypeParam(0.0f), TypeParam(0.0f), + TypeParam(1.0f), TypeParam(1.0f), TypeParam(1.0f)}; + std::initializer_list Y_shape{1, 1, 1, 1, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + test.AddOutput("Y", Y_shape, {TypeParam(0.0f), TypeParam(0.0f)}); + RunTestsExpectFailure(test, "Input spatial dimensions must be non-empty for sampling", + GetCpuAndCudaExecutionProviders()); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_linear_zeros_empty_spatial_rejected) { + // The guard must fire before interpolation regardless of padding mode. This covers padding_mode + // "zeros" (the mode where reads outside the image are otherwise silently treated as 0). + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("linear")); + test.AddAttribute("padding_mode", std::string("zeros")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 0, 5}; + std::initializer_list X_data{}; + std::initializer_list Grid_shape{1, 2, 2, 2}; + std::initializer_list Grid_data{ + TypeParam(0.0f), TypeParam(0.0f), TypeParam(-1.0f), TypeParam(-1.0f), + TypeParam(1.0f), TypeParam(1.0f), TypeParam(0.5f), TypeParam(-0.5f)}; + std::initializer_list Y_shape{1, 1, 2, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + test.AddOutput("Y", Y_shape, + {TypeParam(0.0f), TypeParam(0.0f), TypeParam(0.0f), TypeParam(0.0f)}); + RunTestsExpectFailure(test, "Input spatial dimensions must be non-empty for sampling", + GetCpuAndCudaExecutionProviders()); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_linear_reflection_empty_spatial_rejected) { + // Padding mode "reflection" is the most sensitive to a zero-size spatial dimension because the + // reflect logic divides by (length - 1); the guard must reject the input before that computation. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("linear")); + test.AddAttribute("padding_mode", std::string("reflection")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 0, 5}; + std::initializer_list X_data{}; + std::initializer_list Grid_shape{1, 2, 2, 2}; + std::initializer_list Grid_data{ + TypeParam(0.0f), TypeParam(0.0f), TypeParam(-1.0f), TypeParam(-1.0f), + TypeParam(1.0f), TypeParam(1.0f), TypeParam(0.5f), TypeParam(-0.5f)}; + std::initializer_list Y_shape{1, 1, 2, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + test.AddOutput("Y", Y_shape, + {TypeParam(0.0f), TypeParam(0.0f), TypeParam(0.0f), TypeParam(0.0f)}); + RunTestsExpectFailure(test, "Input spatial dimensions must be non-empty for sampling", + GetCpuAndCudaExecutionProviders()); +} + +TYPED_TEST(GridSampleCustomTest, test_grid_sample_20_4D_nearest_border_nonzero_spatial_accepted) { + // Sanity check that the guard does not reject valid, non-empty spatial dimensions: a normal 2x2 + // input with align_corners=1 samples its four corners exactly and must succeed on both kernels. + OpTester test("GridSample", 20); + test.AddAttribute("mode", std::string("nearest")); + test.AddAttribute("padding_mode", std::string("border")); + test.AddAttribute("align_corners", int64_t{1}); + + std::initializer_list X_shape{1, 1, 2, 2}; + std::initializer_list X_data{TypeParam(1.0f), TypeParam(2.0f), TypeParam(3.0f), TypeParam(4.0f)}; + std::initializer_list Grid_shape{1, 1, 4, 2}; + // align_corners=1 on a 2x2 image maps normalized [-1, 1] -> pixel coords [0, 1]; sample all corners. + std::initializer_list Grid_data{ + TypeParam(-1.0f), TypeParam(-1.0f), // -> pixel(row 0, col 0) = 1 + TypeParam(1.0f), TypeParam(-1.0f), // -> pixel(row 0, col 1) = 2 + TypeParam(-1.0f), TypeParam(1.0f), // -> pixel(row 1, col 0) = 3 + TypeParam(1.0f), TypeParam(1.0f)}; // -> pixel(row 1, col 1) = 4 + std::initializer_list Y_shape{1, 1, 1, 4}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + test.AddOutput("Y", Y_shape, + {TypeParam(1.0f), TypeParam(2.0f), TypeParam(3.0f), TypeParam(4.0f)}); + RunTests(test, GetCpuAndCudaExecutionProviders()); +} + +#ifdef USE_WEBGPU +TYPED_TEST(GridSampleCustomTest, test_grid_sample_16_4D_webgpu_empty_spatial_rejected) { + // The WebGPU GridSample kernel is registered for opset 16-19 (NCHW, 4-D only) and now carries the + // same non-empty spatial-dimension guard as the CPU and CUDA kernels. Exercise it directly on the + // WebGPU provider with a zero-size spatial dimension; the guard rejects host-side before any shader + // dispatch, so this validates the WebGPU path without depending on a particular GPU adapter. + OpTester test("GridSample", 16); + test.AddAttribute("mode", std::string("nearest")); + test.AddAttribute("padding_mode", std::string("border")); + test.AddAttribute("align_corners", int64_t{0}); + + std::initializer_list X_shape{1, 1, 0, 5}; + std::initializer_list X_data{}; + std::initializer_list Grid_shape{1, 2, 2, 2}; + std::initializer_list Grid_data{ + TypeParam(0.0f), TypeParam(0.0f), TypeParam(-1.0f), TypeParam(-1.0f), + TypeParam(1.0f), TypeParam(1.0f), TypeParam(0.5f), TypeParam(-0.5f)}; + std::initializer_list Y_shape{1, 1, 2, 2}; + + test.AddInput("X", X_shape, X_data); + test.AddInput("Grid", Grid_shape, Grid_data); + test.AddOutput("Y", Y_shape, + {TypeParam(0.0f), TypeParam(0.0f), TypeParam(0.0f), TypeParam(0.0f)}); + + std::vector> execution_providers; + execution_providers.push_back(DefaultWebGpuExecutionProvider()); + RunTestsExpectFailure(test, "Input spatial dimensions must be non-empty for sampling", + std::move(execution_providers)); +} +#endif + // ----------------------------------------------------------------------------------------- // CUDA hardening regression tests. // -// These mirror the CPU-only cases above but also run on the CUDA execution provider (when +// These mirror the coordinate-hardening cases above but also run on the CUDA execution provider (when // built) to cover the equivalent float->int64 cast hardening in // onnxruntime/core/providers/cuda/tensor/grid_sample_impl.cu. They are float-only because // the CUDA GridSample kernel is registered for float only, and they use constant-valued From 2a3a06003e1a761c17654b37eaebeb60b987d0f8 Mon Sep 17 00:00:00 2001 From: titaiwangms Date: Wed, 1 Jul 2026 22:56:54 +0000 Subject: [PATCH 3/3] Extend GridSample spatial-dimension guard to WebGPU and broaden tests Apply the same non-empty input spatial-dimension check already present in the CPU and CUDA GridSample kernels to the WebGPU kernel (opset 16-19, NCHW 4-D): reject an input whose spatial dimensions contain a zero size before dispatch, using the same message, so the output-from-grid path cannot produce invalid sample indices. Broaden the regression tests to cover the cases requested in review: - a zero trailing spatial dimension ({1,1,5,0}), which also exercises the CUDA NHWC layout path, - all spatial dimensions zero ({1,1,0,0}), - a 5-D input with a zero in a non-first spatial dimension ({1,1,2,0,3}), - padding_mode "zeros" and "reflection" variants of the rejection, - a valid non-empty input that must still succeed (no false rejection), - a WebGPU-only opset-16 rejection test guarded by USE_WEBGPU that runs on the WebGPU provider where the new guard applies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- onnxruntime/core/providers/webgpu/tensor/grid_sample.cc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/onnxruntime/core/providers/webgpu/tensor/grid_sample.cc b/onnxruntime/core/providers/webgpu/tensor/grid_sample.cc index abf7df6f4b8a2..4e72a0951cc7c 100644 --- a/onnxruntime/core/providers/webgpu/tensor/grid_sample.cc +++ b/onnxruntime/core/providers/webgpu/tensor/grid_sample.cc @@ -204,6 +204,15 @@ Status GridSample::ComputeInternal(ComputeContext& context) const { ORT_RETURN_IF_NOT(grid_shape.NumDimensions() == 4, "grid must be 4-D"); ORT_RETURN_IF_NOT(grid_shape[3] == 2, "grid last dimension must be 2"); + // Spatial dimensions must be non-empty for sampling: the output is sized by the grid, so a zero-size + // input spatial dimension would otherwise lead to invalid index computations during interpolation. + // This kernel is NCHW and 4-D, so the spatial dimensions are indices [2, rank). + for (size_t i = 2; i < X_shape.NumDimensions(); ++i) { + ORT_RETURN_IF_NOT(X_shape[i] > 0, + "Input spatial dimensions must be non-empty for sampling. Dimension ", i, + " has size ", X_shape[i]); + } + const int64_t N = X_shape[0]; const int64_t C = X_shape[1]; const int64_t H_in = X_shape[2];