Skip to content

Commit a91b0b4

Browse files
titaiwangmsCopilot
andauthored
Add missing rank/length validation to pooling operators (fix OOB reads) (#29579)
## Summary The pooling operators (`MaxPool` / `AveragePool` / `LpPool` and the contrib `NhwcMaxPool`) did not fully validate attribute lengths and input ranks before indexing into per-spatial-dimension attribute vectors. On malformed models this could lead to out-of-bounds reads. This PR adds the missing input-validation guards so such models are rejected with clear errors instead of reading past the end of the attribute containers. Because all standard pooling execution providers (CPU / CUDA / ROCm / JS / WebGPU / XNNPACK / ACL) share `PoolBase` / `PoolAttributes`, the `pool_attributes.h` guards cover them all with a single change. `NhwcMaxPool` needs its own guard because it does not go through `PoolAttributes::SetOutputSize` and hand-writes its spatial loop. ## What changed, by file ### `onnxruntime/core/providers/cpu/nn/pool_attributes.h` - Validate `pads.size() == 2 * kernel_shape.size()` before indexing `pads` (F1). - Validate that the `kernel_shape` rank matches the input spatial rank in `InferOutputSize` before indexing `strides` / `kernel_shape` / `dilations` (F2). - Validate input rank `>= 2` in `SetOutputSize` before indexing the input shape (F5). - Remove the `int` truncation of the spatial dimension; use `int64` throughout (F3). - Wrap the residual output-size arithmetic in `SafeInt<int64_t>` to guard against overflow and enforce `out_size >= 0` (F4). - Give the `strides` length check an explicit error message (previously a bare condition). - Validate `MaxPool` `storage_order` is 0 (row-major) or 1 (column-major) at construction (F7). ### `onnxruntime/contrib_ops/cpu/quantization/nhwc_max_pool.cc` - `NhwcMaxPool` bypasses the shared `SetOutputSize` path and hand-writes its spatial loop, indexing `kernel_shape` / `strides` / `dilations` by dimension. Added a guard validating that the `kernel_shape` rank matches the input spatial rank before the loop. `Compute` returns `Status`, so this uses `ORT_RETURN_IF_NOT` (no-exception-build compatible), mirroring the existing guard in `fp16_pool.cc`. The `PoolAttributes` constructor already enforces that `strides` and `dilations` match `kernel_shape`, so guarding `kernel_shape` here covers all three indexed vectors. ### `onnxruntime/core/providers/cpu/fp16/fp16_pool.cc` - Fixed an incorrect loop bound in the malformed-`kernel_shape` diagnostic path: the error-message loop iterated with `TensorShape::Size()` (the element count, product of dims) while indexing the dimension array, which is bounded by the rank. Replaced it with `NumDimensions()` so the loop iterates the dims correctly. This `PoolFp16` kernel is ARM64-only (built under `MLAS_F16VEC_INTRINSICS_SUPPORTED`, i.e. non-Apple ARM64/ARM64EC) and the affected code runs only on the already-failing error path. Because the kernel is not compiled on x86 and the existing fp16 pool tests are gated on `USE_CUDA`/`USE_COREML` (exercising those EPs' kernels rather than this one), no x86 CI negative test can reach this path; the fix is validated by inspection. ### Tests Added 11 negative tests total, all using `kExpectFailure` and compatible with no-exception builds: - `pool_op_test.cc`: `MaxPool_PadsTooShort`, `AveragePool_PadsTooShort`, `LpPool_PadsTooShort`, `LpPool_KernelRankMismatch`, `AveragePool_NegativeOutputDim`, `MaxPool_PadsTooLong`, `MaxPool_StridesLengthMismatch`, `MaxPool_DilationsLengthMismatch`, `MaxPool_InputRankTooLow`, `MaxPool_InvalidStorageOrder`. - `nhwc_maxpool_op_test.cc`: `NhwcMaxPool KernelRankMismatch_S8`. ## Design note The `pads.size() == 2 * kernel_shape.size()` check is intentionally **unconditional** (not gated on `auto_pad`). Making it conditional on `auto_pad == NOTSET` would reopen the out-of-bounds indexing path for models that set `auto_pad` together with a malformed `pads` list. ## Testing All pooling and NHWC max-pool tests pass: **105 passed / 2 skipped (DML not built) / 0 failed**, with zero regression. ## Follow-ups (out of scope, tracked separately) - F6: `auto_pad` vs explicit `pads` mutual-exclusion per the ONNX spec — deferred due to backward-compatibility risk. - CUDA `narrow_cast<int>` truncation on `int64` dims — non-OOB. - Optional integer-domain ceil to remove a float path in `ComputeOutputSize`. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 010a8f0 commit a91b0b4

5 files changed

Lines changed: 282 additions & 8 deletions

File tree

onnxruntime/contrib_ops/cpu/quantization/nhwc_max_pool.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ Status NhwcMaxPool<T8Bits>::Compute(OpKernelContext* context) const {
3939

4040
const size_t spatial_dims = input_rank - 2;
4141

42+
// The spatial loop below indexes kernel_shape/strides/dilations by dim, so their length must
43+
// match the input spatial rank. The PoolAttributes constructor already enforces that strides and
44+
// dilations match kernel_shape, so guarding kernel_shape here covers all three.
45+
ORT_RETURN_IF_NOT(pool_attrs_.kernel_shape.size() == spatial_dims,
46+
"kernel_shape rank must match the input spatial rank. Got kernel_shape rank: ",
47+
pool_attrs_.kernel_shape.size(), ", input spatial rank: ", spatial_dims);
48+
4249
// Compute the output size and effective padding for this pooling operation.
4350
TensorShapeVector output_dims({N});
4451
TensorShapeVector pads = pool_attrs_.pads;

onnxruntime/core/providers/cpu/fp16/fp16_pool.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ Status PoolFp16::Compute(OpKernelContext* context) const {
7777
std::ostringstream ss;
7878
ss << "Invalid kernel shape. Input shape ";
7979
ss << (channels_last_ ? "(NHWC):[" : "(NCHW):[");
80-
for (int64_t i = 0; i < input_shape.Size(); i++) {
80+
// NumDimensions() is the number of dimensions; TensorShape::Size() is the element count
81+
// (product of dims), so iterating to Size() would index beyond the dimension array.
82+
for (size_t i = 0; i < input_shape.NumDimensions(); i++) {
8183
ss << input_shape[i] << ", ";
8284
}
8385
ss << "] Kernel shape:[";

onnxruntime/core/providers/cpu/nn/pool_attributes.h

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#pragma once
66

77
#include <cmath>
8+
#include "core/common/safeint.h"
89
#ifndef SHARED_PROVIDER
910
#include "core/common/common.h"
1011
#include "core/framework/op_node_proto_helper.h"
@@ -45,6 +46,9 @@ struct PoolAttributes {
4546
if (!info.GetAttrs("pads", pads).IsOK() || pads.empty()) {
4647
pads.resize(kernel_shape.size() * 2, 0);
4748
}
49+
ORT_ENFORCE(pads.size() == kernel_shape.size() * 2,
50+
"'pads' must have twice the kernel_shape rank (2 entries per spatial dim). Got pads size: ",
51+
pads.size(), ", expected: ", kernel_shape.size() * 2);
4852

4953
if (!info.GetAttrs("strides", strides).IsOK() || strides.empty()) {
5054
strides.resize(kernel_shape.size(), 1);
@@ -71,6 +75,8 @@ struct PoolAttributes {
7175
if (op_name == "MaxPool") {
7276
if (start_version >= 8) {
7377
ORT_ENFORCE(info.GetAttr("storage_order", &storage_order).IsOK());
78+
ORT_ENFORCE(storage_order == 0 || storage_order == 1,
79+
"storage_order must be 0 (row-major) or 1 (column-major). Got: ", storage_order);
7480
}
7581
}
7682

@@ -80,7 +86,8 @@ struct PoolAttributes {
8086
"Pad should be smaller than kernel.");
8187
}
8288

83-
ORT_ENFORCE(strides.size() == kernel_shape.size());
89+
ORT_ENFORCE(strides.size() == kernel_shape.size(),
90+
"Strides dimensions should match kernel shape");
8491
for (auto stride : strides) {
8592
ORT_ENFORCE(stride > 0, "All stride values must be positive, got: ", stride);
8693
}
@@ -108,6 +115,9 @@ struct PoolAttributes {
108115
int64_t output_channel,
109116
TensorShapeVector* actual_pads,
110117
bool is_nhwc = false) const {
118+
ORT_ENFORCE(input_shape.NumDimensions() >= 2,
119+
"Input must have at least 2 dimensions (N, C, ...spatial). Got rank: ",
120+
input_shape.NumDimensions());
111121
ORT_ENFORCE(input_shape.Size() > 0 || input_shape[0] == 0,
112122
"Invalid input shape. Only N can be zero. Got:", input_shape);
113123
TensorShapeVector output_dims;
@@ -126,14 +136,20 @@ struct PoolAttributes {
126136
TensorShapeVector* output_dims,
127137
TensorShapeVector* actual_pads,
128138
bool is_nhwc = false) const {
129-
ORT_ENFORCE(input_dims.size() >= 2);
139+
ORT_ENFORCE(input_dims.size() >= 2,
140+
"Input must have at least 2 dimensions (batch and channel) before the spatial dims. "
141+
"Got rank: ",
142+
input_dims.size());
130143
if (global_pooling) {
131144
output_dims->assign(input_dims.size() - 2, 1);
132145
} else {
146+
ORT_ENFORCE(input_dims.size() - 2 == kernel_shape.size(),
147+
"kernel_shape rank must match the input spatial rank. Input spatial rank: ",
148+
input_dims.size() - 2, ", kernel_shape rank: ", kernel_shape.size());
133149
for (size_t dim = 0; dim < input_dims.size() - 2; ++dim) {
134150
int64_t dim_size = 0;
135151
auto spatial_dim = is_nhwc ? input_dims[dim + 1] : input_dims[dim + 2];
136-
ComputeSizePadDilations(static_cast<int>(spatial_dim),
152+
ComputeSizePadDilations(spatial_dim,
137153
strides[dim],
138154
kernel_shape[dim],
139155
&actual_pads->at(dim),
@@ -153,6 +169,9 @@ struct PoolAttributes {
153169
int64_t dilation,
154170
int64_t* out_size) const {
155171
if (auto_pad != AutoPadType::NOTSET) {
172+
// TODO: Per the ONNX spec, auto_pad and explicit pads are mutually exclusive. ORT currently
173+
// accepts both and overwrites any explicit pads below when auto_pad is set, rather than
174+
// rejecting the model, to preserve backward compatibility with existing models.
156175
switch (auto_pad) {
157176
case AutoPadType::VALID:
158177
*pad_head = 0;
@@ -194,17 +213,25 @@ struct PoolAttributes {
194213
int64_t pad_head,
195214
int64_t pad_tail,
196215
int64_t dilation) const {
197-
int64_t numerator = in_size + pad_head + pad_tail - dilation * (kernel - 1) - 1;
198-
int64_t out_size = numerator / stride + 1;
216+
// SafeInt wraps the residual output-size arithmetic below (the numerator terms and the +1 /
217+
// ceil-mode add and subtract) to catch int64 overflow. It does not cover the auto_pad SAME_*
218+
// padding math in the caller or the numerator / stride division.
219+
int64_t numerator = SafeInt<int64_t>(in_size) + pad_head + pad_tail - SafeInt<int64_t>(dilation) * (kernel - 1) - 1;
220+
int64_t out_size = static_cast<int64_t>(SafeInt<int64_t>(numerator / stride) + 1);
199221

200222
if (ceil_mode == 1) {
201-
out_size = static_cast<int64_t>(std::ceil(static_cast<float>(numerator) / stride)) + 1;
223+
int64_t ceil_div = static_cast<int64_t>(std::ceil(static_cast<float>(numerator) / stride));
224+
out_size = static_cast<int64_t>(SafeInt<int64_t>(ceil_div) + 1);
202225
// Ensure that the last pooling starts inside the image (at least 1 pixel)
203226
// Reference: https://github.com/onnx/onnx/pull/5741
204-
if ((out_size - 1) * stride >= in_size + pad_head) {
227+
int64_t last_pool_start = static_cast<int64_t>((SafeInt<int64_t>(out_size) - 1) * stride);
228+
int64_t input_extent = static_cast<int64_t>(SafeInt<int64_t>(in_size) + pad_head);
229+
if (last_pool_start >= input_extent) {
205230
--out_size;
206231
}
207232
}
233+
ORT_ENFORCE(out_size >= 0,
234+
"Calculated output dimension is negative. Check kernel_shape, pads, strides and dilations.");
208235
return out_size;
209236
}
210237
#if defined(_MSC_VER) && !defined(__clang__)

onnxruntime/test/contrib_ops/nhwc_maxpool_op_test.cc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,5 +258,25 @@ TEST(NhwcMaxPoolContribOpTest, MaxPoolDilations_S8) {
258258
test.Run();
259259
}
260260

261+
// Verify that a kernel_shape whose rank does not match the input spatial rank is rejected before
262+
// the spatial loop in NhwcMaxPool::Compute (which indexes kernel_shape/strides/dilations by dim).
263+
// AddShapeToTensorData(false) omits the input shape from the graph so ONNX shape inference is
264+
// bypassed and the crafted rank mismatch reaches the kernel. Exclude compiling EPs (TRT, QNN) and
265+
// EPs with their own validation (DML) that produce different error messages.
266+
TEST(NhwcMaxPoolContribOpTest, KernelRankMismatch_S8) {
267+
OpTester test("NhwcMaxPool", 1, onnxruntime::kMSDomain);
268+
test.AddShapeToTensorData(false);
269+
270+
// Input spatial rank is 2 (input rank 4) while kernel_shape rank is 1.
271+
test.AddAttribute("kernel_shape", std::vector<int64_t>{3});
272+
273+
std::vector<int8_t> x_vals(1 * 8 * 8 * 4, 0);
274+
test.AddInput<int8_t>("x", {1, 8, 8, 4}, x_vals);
275+
test.AddOutput<int8_t>("y", {0}, {});
276+
277+
test.Run(OpTester::ExpectResult::kExpectFailure, "kernel_shape rank must match",
278+
{kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider});
279+
}
280+
261281
} // namespace test
262282
} // namespace onnxruntime

onnxruntime/test/providers/cpu/nn/pool_op_test.cc

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2247,6 +2247,224 @@ TEST(PoolTest, MaxPool_ZeroDilation) {
22472247
{kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider});
22482248
}
22492249

2250+
// Verify that a 'pads' attribute shorter than 2 * kernel_shape rank is rejected by PoolAttributes
2251+
// kernel validation. AddShapeToTensorData(false) omits input shape from the graph so ONNX shape
2252+
// inference is bypassed and the model reaches kernel construction where the ORT_ENFORCE fires.
2253+
// Exclude compiling EPs (TRT, QNN) and EPs with their own validation (DML) that produce
2254+
// different error messages.
2255+
TEST(PoolTest, MaxPool_PadsTooShort) {
2256+
OpTester test("MaxPool");
2257+
test.AddShapeToTensorData(false);
2258+
2259+
test.AddAttribute("auto_pad", "");
2260+
test.AddAttribute("strides", std::vector<int64_t>{1, 1});
2261+
test.AddAttribute("pads", std::vector<int64_t>{0, 0});
2262+
test.AddAttribute("kernel_shape", std::vector<int64_t>{2, 2});
2263+
2264+
std::vector<float> x_vals(1 * 1 * 8 * 8, 1.0f);
2265+
test.AddInput<float>("X", {1, 1, 8, 8}, x_vals);
2266+
test.AddOutput<float>("Y", {0}, {});
2267+
2268+
test.Run(OpTester::ExpectResult::kExpectFailure, "twice the kernel_shape rank",
2269+
{kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider});
2270+
}
2271+
2272+
TEST(PoolTest, AveragePool_PadsTooShort) {
2273+
OpTester test("AveragePool");
2274+
test.AddShapeToTensorData(false);
2275+
2276+
test.AddAttribute("auto_pad", "");
2277+
test.AddAttribute("strides", std::vector<int64_t>{1, 1});
2278+
test.AddAttribute("pads", std::vector<int64_t>{0, 0});
2279+
test.AddAttribute("kernel_shape", std::vector<int64_t>{2, 2});
2280+
test.AddAttribute("count_include_pad", static_cast<int64_t>(0));
2281+
2282+
std::vector<float> x_vals(1 * 1 * 8 * 8, 1.0f);
2283+
test.AddInput<float>("X", {1, 1, 8, 8}, x_vals);
2284+
test.AddOutput<float>("Y", {0}, {});
2285+
2286+
test.Run(OpTester::ExpectResult::kExpectFailure, "twice the kernel_shape rank",
2287+
{kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider});
2288+
}
2289+
2290+
TEST(PoolTest, LpPool_PadsTooShort) {
2291+
OpTester test("LpPool", 18);
2292+
test.AddShapeToTensorData(false);
2293+
2294+
test.AddAttribute("auto_pad", "");
2295+
test.AddAttribute("strides", std::vector<int64_t>{1, 1});
2296+
test.AddAttribute("pads", std::vector<int64_t>{0, 0});
2297+
test.AddAttribute("kernel_shape", std::vector<int64_t>{2, 2});
2298+
2299+
std::vector<float> x_vals(1 * 1 * 8 * 8, 1.0f);
2300+
test.AddInput<float>("X", {1, 1, 8, 8}, x_vals);
2301+
test.AddOutput<float>("Y", {0}, {});
2302+
2303+
test.Run(OpTester::ExpectResult::kExpectFailure, "twice the kernel_shape rank",
2304+
{kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider});
2305+
}
2306+
2307+
// Verify that a kernel_shape whose rank does not match the input spatial rank is rejected by
2308+
// PoolAttributes::InferOutputSize. LpPool (opset < 18) uses the generic Pool::Compute path, which
2309+
// has no earlier rank guard, so the request reaches the InferOutputSize validation directly.
2310+
// AddShapeToTensorData(false) omits the input shape so ONNX shape inference is bypassed.
2311+
// Exclude compiling EPs (TRT, QNN) and EPs with their own validation (DML) that produce
2312+
// different error messages.
2313+
TEST(PoolTest, LpPool_KernelRankMismatch) {
2314+
OpTester test("LpPool", 11);
2315+
test.AddShapeToTensorData(false);
2316+
2317+
test.AddAttribute("auto_pad", "");
2318+
test.AddAttribute("strides", std::vector<int64_t>{1, 1});
2319+
test.AddAttribute("pads", std::vector<int64_t>{0, 0, 0, 0});
2320+
test.AddAttribute("kernel_shape", std::vector<int64_t>{2, 2});
2321+
2322+
// Input spatial rank is 3 while kernel_shape rank is 2.
2323+
std::vector<float> x_vals(1 * 1 * 8 * 8 * 8, 1.0f);
2324+
test.AddInput<float>("X", {1, 1, 8, 8, 8}, x_vals);
2325+
test.AddOutput<float>("Y", {0}, {});
2326+
2327+
test.Run(OpTester::ExpectResult::kExpectFailure, "input spatial rank",
2328+
{kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider});
2329+
}
2330+
2331+
// Verify that attribute values producing a non-positive output dimension are rejected by
2332+
// PoolAttributes::ComputeOutputSize. A 3x3 kernel over a 1x1 spatial input with no padding makes
2333+
// the padded input smaller than the dilated kernel, so the computed output dimension is negative.
2334+
// AddShapeToTensorData(false) omits the input shape so ONNX shape inference is bypassed.
2335+
// Exclude compiling EPs (TRT, QNN) and EPs with their own validation (DML) that produce
2336+
// different error messages.
2337+
TEST(PoolTest, AveragePool_NegativeOutputDim) {
2338+
OpTester test("AveragePool");
2339+
test.AddShapeToTensorData(false);
2340+
2341+
test.AddAttribute("auto_pad", "");
2342+
test.AddAttribute("strides", std::vector<int64_t>{1, 1});
2343+
test.AddAttribute("pads", std::vector<int64_t>{0, 0, 0, 0});
2344+
test.AddAttribute("kernel_shape", std::vector<int64_t>{3, 3});
2345+
test.AddAttribute("count_include_pad", static_cast<int64_t>(0));
2346+
2347+
std::vector<float> x_vals(1 * 1 * 1 * 1, 1.0f);
2348+
test.AddInput<float>("X", {1, 1, 1, 1}, x_vals);
2349+
test.AddOutput<float>("Y", {0}, {});
2350+
2351+
test.Run(OpTester::ExpectResult::kExpectFailure, "output dimension is negative",
2352+
{kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider});
2353+
}
2354+
2355+
// Verify that a 'pads' attribute longer than 2 * kernel_shape rank is rejected by the
2356+
// PoolAttributes constructor. AddShapeToTensorData(false) omits the input shape so ONNX shape
2357+
// inference is bypassed and the model reaches kernel construction where the ORT_ENFORCE fires.
2358+
// Exclude compiling EPs (TRT, QNN) and EPs with their own validation (DML) that produce
2359+
// different error messages.
2360+
TEST(PoolTest, MaxPool_PadsTooLong) {
2361+
OpTester test("MaxPool");
2362+
test.AddShapeToTensorData(false);
2363+
2364+
test.AddAttribute("auto_pad", "");
2365+
test.AddAttribute("strides", std::vector<int64_t>{1, 1});
2366+
test.AddAttribute("pads", std::vector<int64_t>{0, 0, 0, 0, 0, 0});
2367+
test.AddAttribute("kernel_shape", std::vector<int64_t>{2, 2});
2368+
2369+
std::vector<float> x_vals(1 * 1 * 8 * 8, 1.0f);
2370+
test.AddInput<float>("X", {1, 1, 8, 8}, x_vals);
2371+
test.AddOutput<float>("Y", {0}, {});
2372+
2373+
test.Run(OpTester::ExpectResult::kExpectFailure, "twice the kernel_shape rank",
2374+
{kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider});
2375+
}
2376+
2377+
// Verify that a 'strides' attribute whose length does not match the kernel_shape rank is rejected
2378+
// by the PoolAttributes constructor. The assertion pins the explicit strides length message
2379+
// ("Strides dimensions should match kernel shape"). AddShapeToTensorData(false) bypasses shape
2380+
// inference. Exclude compiling EPs (TRT, QNN) and EPs with their own validation (DML) that produce
2381+
// different error messages.
2382+
TEST(PoolTest, MaxPool_StridesLengthMismatch) {
2383+
OpTester test("MaxPool");
2384+
test.AddShapeToTensorData(false);
2385+
2386+
test.AddAttribute("auto_pad", "");
2387+
test.AddAttribute("strides", std::vector<int64_t>{1, 1, 1});
2388+
test.AddAttribute("pads", std::vector<int64_t>{0, 0, 0, 0});
2389+
test.AddAttribute("kernel_shape", std::vector<int64_t>{2, 2});
2390+
2391+
std::vector<float> x_vals(1 * 1 * 8 * 8, 1.0f);
2392+
test.AddInput<float>("X", {1, 1, 8, 8}, x_vals);
2393+
test.AddOutput<float>("Y", {0}, {});
2394+
2395+
test.Run(OpTester::ExpectResult::kExpectFailure, "Strides dimensions should match kernel shape",
2396+
{kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider});
2397+
}
2398+
2399+
// Verify that a 'dilations' attribute whose length does not match the kernel_shape rank is
2400+
// rejected by the PoolAttributes constructor. AddShapeToTensorData(false) bypasses shape inference.
2401+
// Exclude compiling EPs (TRT, QNN) and EPs with their own validation (DML) that produce
2402+
// different error messages.
2403+
TEST(PoolTest, MaxPool_DilationsLengthMismatch) {
2404+
// 'dilations' is only a valid MaxPool attribute from opset 10 onward, so target that version.
2405+
OpTester test("MaxPool", 10);
2406+
test.AddShapeToTensorData(false);
2407+
2408+
test.AddAttribute("auto_pad", "");
2409+
test.AddAttribute("strides", std::vector<int64_t>{1, 1});
2410+
test.AddAttribute("pads", std::vector<int64_t>{0, 0, 0, 0});
2411+
test.AddAttribute("kernel_shape", std::vector<int64_t>{2, 2});
2412+
test.AddAttribute("dilations", std::vector<int64_t>{1, 1, 1});
2413+
2414+
std::vector<float> x_vals(1 * 1 * 8 * 8, 1.0f);
2415+
test.AddInput<float>("X", {1, 1, 8, 8}, x_vals);
2416+
test.AddOutput<float>("Y", {0}, {});
2417+
2418+
test.Run(OpTester::ExpectResult::kExpectFailure, "Dilations dimensions should match kernel shape",
2419+
{kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider});
2420+
}
2421+
2422+
// Verify that a low-rank input is rejected before pooling. The rank check in SetOutputSize
2423+
// (NumDimensions >= 2) is defense-in-depth for execution providers and direct callers, but the CPU
2424+
// pooling kernels reject inputs with rank < 3 earlier in Compute, so the tested path fires that
2425+
// earlier guard and this test pins its message ("Input dimension cannot be less than 3").
2426+
// AddShapeToTensorData(false) bypasses shape inference so the rank-2 input reaches the kernel.
2427+
// Exclude compiling EPs (TRT, QNN) and EPs with their own validation (DML).
2428+
TEST(PoolTest, MaxPool_InputRankTooLow) {
2429+
OpTester test("MaxPool");
2430+
test.AddShapeToTensorData(false);
2431+
2432+
test.AddAttribute("auto_pad", "");
2433+
test.AddAttribute("strides", std::vector<int64_t>{1, 1});
2434+
test.AddAttribute("pads", std::vector<int64_t>{0, 0, 0, 0});
2435+
test.AddAttribute("kernel_shape", std::vector<int64_t>{2, 2});
2436+
2437+
std::vector<float> x_vals(4 * 4, 1.0f);
2438+
test.AddInput<float>("X", {4, 4}, x_vals);
2439+
test.AddOutput<float>("Y", {0}, {});
2440+
2441+
test.Run(OpTester::ExpectResult::kExpectFailure, "Input dimension cannot be less than 3",
2442+
{kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider});
2443+
}
2444+
2445+
// Verify that a MaxPool 'storage_order' outside the valid set {0, 1} is rejected by the
2446+
// PoolAttributes constructor. storage_order was introduced in MaxPool opset 8, so target that
2447+
// version. AddShapeToTensorData(false) bypasses shape inference so the model reaches kernel
2448+
// construction where the ORT_ENFORCE fires. Exclude compiling EPs (TRT, QNN) and EPs with their
2449+
// own validation (DML) that produce different error messages.
2450+
TEST(PoolTest, MaxPool_InvalidStorageOrder) {
2451+
OpTester test("MaxPool", 8);
2452+
test.AddShapeToTensorData(false);
2453+
2454+
test.AddAttribute("auto_pad", "");
2455+
test.AddAttribute("strides", std::vector<int64_t>{1, 1});
2456+
test.AddAttribute("pads", std::vector<int64_t>{0, 0, 0, 0});
2457+
test.AddAttribute("kernel_shape", std::vector<int64_t>{2, 2});
2458+
test.AddAttribute("storage_order", static_cast<int64_t>(2));
2459+
2460+
std::vector<float> x_vals(1 * 1 * 8 * 8, 1.0f);
2461+
test.AddInput<float>("X", {1, 1, 8, 8}, x_vals);
2462+
test.AddOutput<float>("Y", {0}, {});
2463+
2464+
test.Run(OpTester::ExpectResult::kExpectFailure, "storage_order must be 0",
2465+
{kTensorrtExecutionProvider, kQnnExecutionProvider, kDmlExecutionProvider});
2466+
}
2467+
22502468
// DML EP validates stride/dilation in OperatorHelper.cpp (KernelHelper constructor) via
22512469
// ML_CHECK_VALID_ARGUMENT_MSG, but the descriptive message is lost when the exception crosses
22522470
// the COM/HRESULT boundary (CATCH_RETURN strips the message, THROW_IF_FAILED re-throws with

0 commit comments

Comments
 (0)