Add missing rank/length validation to pooling operators (fix OOB reads)#29579
Add missing rank/length validation to pooling operators (fix OOB reads)#29579titaiwangms wants to merge 11 commits into
Conversation
Extend the pads length check (F1) with adjacent memory-safety and hardening fixes in the shared pool_attributes.h, all via constructor/function ORT_ENFORCE and SafeInt: - F2: InferOutputSize enforces input spatial rank == kernel_shape rank before indexing strides/kernel_shape/dilations. - F3: drop the static_cast<int> narrowing of the spatial dim; pass the int64_t through unchanged. - F4: compute the output-size numerator with SafeInt<int64_t> to guard overflow and enforce a non-negative output dimension. - F5: SetOutputSize enforces input rank >= 2 before accessing element 0. All PoolBase-derived EPs (CPU, CUDA, ROCm, JS, WebGPU, XNNPACK, ACL, fp16) inherit these checks. Add LpPool_KernelRankMismatch (F2) and AveragePool_NegativeOutputDim (F4) tests alongside the existing *_PadsTooShort tests. Agent-signed-off: Developer (7237dba3) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the pool_attributes.h validation that pairs with the already-committed tests: - F1: enforce pads length == 2 * kernel_shape rank before indexing pads[dim + K]. - F2: InferOutputSize enforces input spatial rank == kernel_shape rank. - F3: drop the static_cast<int> narrowing of the spatial dim. - F4: compute the output-size numerator with SafeInt<int64_t> and enforce a non-negative output dimension. - F5: SetOutputSize enforces input rank >= 2 before accessing element 0. All PoolBase-derived EPs (CPU, CUDA, ROCm, JS, WebGPU, XNNPACK, ACL, fp16) inherit these checks. Agent-signed-off: Developer (7237dba3) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Reword the F1 pads ORT_ENFORCE to print actual vs expected sizes, matching the F2/F5 message style. - Add a why-comment on the F4 SafeInt numerator explaining the int64 overflow guard. - Tighten the three *_PadsTooShort test assertions to the unique F1 substring 'twice the kernel_shape rank' so they provably pin F1 rather than any generic pads mention. Agent-signed-off: Developer (7237dba3) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Decision 3: wrap the residual output-size arithmetic in ComputeOutputSize with SafeInt<int64_t>, consistent with the F4 numerator: the numerator/stride + 1 step, the ceil_mode + 1 step, and the ceil_mode (out_size - 1) * stride and in_size + pad_head terms. Behavior for valid inputs is unchanged; only overflow is guarded. Decision 4: add negative unit tests modeled on the existing *_PadsTooShort style (AddShapeToTensorData(false), kExpectFailure, TRT/QNN/DML excluded): MaxPool_PadsTooLong (pins the pads length enforce), MaxPool_StridesLengthMismatch and MaxPool_DilationsLengthMismatch (pin the strides/dilations size enforces), and MaxPool_InputRankTooLow (the CPU kernels reject rank < 3 before SetOutputSize, so it pins that earlier guard; F5 is defense-in-depth for other callers). Agent-signed-off: Developer (7237dba3) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e messages Agent-signed-off: Developer (7237dba3) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Agent-signed-off: Developer (7237dba3) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR hardens pooling operator input/attribute validation to prevent out-of-bounds reads on malformed models by adding rank/length checks in the shared PoolAttributes path and in the contrib NhwcMaxPool kernel, and it adds negative tests to pin the new failure modes.
Changes:
- Added strict attribute-length and rank validation in
PoolAttributes(e.g.,padslength,kernel_shaperank vs input spatial rank), plus overflow-checked output-size arithmetic. - Added a kernel-rank guard in
contribNhwcMaxPoolprior to its per-spatial-dimension loop. - Added multiple negative tests covering malformed pooling attributes and rank mismatches.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| onnxruntime/core/providers/cpu/nn/pool_attributes.h | Adds missing pooling attribute/rank validation and safer output-size arithmetic. |
| onnxruntime/contrib_ops/cpu/quantization/nhwc_max_pool.cc | Adds kernel/input rank validation before indexing per-dimension attribute vectors. |
| onnxruntime/test/providers/cpu/nn/pool_op_test.cc | Adds negative tests for malformed pooling attributes and rank issues. |
| onnxruntime/test/contrib_ops/nhwc_maxpool_op_test.cc | Adds a negative test for NHWC max-pool kernel rank mismatch. |
Review synthesis (multi-reviewer)Verdict: solid, mergeable. Deep spec/math review validated the core design; no true Critical/Major issues in the changed code. Findings below are deduped and verified against the code. What's well done (verified)
In-scope, worth fixing before merge (Minor/Nit)
Follow-ups (out of scope — track separately, not blockers)
Note: an earlier reviewer flagged the negative-numerator integer-truncation-vs-floor path as a regression; on deeper review this is pre-existing behavior the PR does not change, and the new |
…des message Agent-signed-off: Developer (7237dba3) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Agent-signed-off: Developer (7237dba3) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Agent-signed-off: Developer (7237dba3) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…scription Agent-signed-off: Developer (7237dba3) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Agent-signed-off: Developer (7237dba3) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
The pooling operators (
MaxPool/AveragePool/LpPooland the contribNhwcMaxPool) 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, thepool_attributes.hguards cover them all with a single change.NhwcMaxPoolneeds its own guard because it does not go throughPoolAttributes::SetOutputSizeand hand-writes its spatial loop.What changed, by file
onnxruntime/core/providers/cpu/nn/pool_attributes.hpads.size() == 2 * kernel_shape.size()before indexingpads(F1).kernel_shaperank matches the input spatial rank inInferOutputSizebefore indexingstrides/kernel_shape/dilations(F2).>= 2inSetOutputSizebefore indexing the input shape (F5).inttruncation of the spatial dimension; useint64throughout (F3).SafeInt<int64_t>to guard against overflow and enforceout_size >= 0(F4).strideslength check an explicit error message (previously a bare condition).MaxPoolstorage_orderis 0 (row-major) or 1 (column-major) at construction (F7).onnxruntime/contrib_ops/cpu/quantization/nhwc_max_pool.ccNhwcMaxPoolbypasses the sharedSetOutputSizepath and hand-writes its spatial loop, indexingkernel_shape/strides/dilationsby dimension. Added a guard validating that thekernel_shaperank matches the input spatial rank before the loop.ComputereturnsStatus, so this usesORT_RETURN_IF_NOT(no-exception-build compatible), mirroring the existing guard infp16_pool.cc. ThePoolAttributesconstructor already enforces thatstridesanddilationsmatchkernel_shape, so guardingkernel_shapehere covers all three indexed vectors.onnxruntime/core/providers/cpu/fp16/fp16_pool.cckernel_shapediagnostic path: the error-messageloop iterated with
TensorShape::Size()(the element count, product of dims) while indexing thedimension array, which is bounded by the rank. Replaced it with
NumDimensions()so the loopiterates the dims correctly. This
PoolFp16kernel is ARM64-only (built underMLAS_F16VEC_INTRINSICS_SUPPORTED, i.e. non-Apple ARM64/ARM64EC) and the affected code runs onlyon 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 thanthis one), no x86 CI negative test can reach this path; the fix is validated by inspection.
Tests
Added 11 negative tests total, all using
kExpectFailureand 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 onauto_pad). Making it conditional onauto_pad == NOTSETwould reopen the out-of-bounds indexing path for models that setauto_padtogether with a malformedpadslist.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)
auto_padvs explicitpadsmutual-exclusion per the ONNX spec — deferred due to backward-compatibility risk.narrow_cast<int>truncation onint64dims — non-OOB.ComputeOutputSize.