Skip to content

Commit 18ae6da

Browse files
edgchen1CopilotCopilot
authored
Validate per-column weight_scale/weight_zero_point shape in CPU QAttention; harden integer arithmetic in QAttention and AttentionBase (microsoft#28480)
### Description The CPU `QAttention` kernel did not validate the shape of per-column `weight_scale` and `weight_zero_point` inputs against the expected `3 * hidden_size`. A model could supply a per-column tensor smaller than expected, causing the GEMM dequantization loop to read past the end of the buffer (offsets up to `~3 * hidden_size - head_size`). This PR adds the missing shape validation and, while in the area, hardens integer arithmetic across `QAttention` and `AttentionBase` against malformed shape attributes / dimensions. ### Changes **`onnxruntime/contrib_ops/cpu/quantization/attention_quant.cc`** - Validate per-column `weight_scale` and `weight_zero_point` are 1-D with size `3 * hidden_size`; reject otherwise. - Use `narrow<int>` / `narrow<size_t>` when converting `int64_t` shape dims, so out-of-range values throw rather than silently truncating. - Use `SafeInt` for multiplications whose operands are not provably bounded by upstream validation (`loop_len`, `input_offset`, `qkv_offset`, the gemm allocation, and `packed_weights_data_size` in `PrePack`). - Refactor the gemm allocation and Q/K/V pointer arithmetic to share a single `SafeInt`-validated `batch_size * sequence_length * hidden_size` value. - Drop a few redundant `static_cast<int>`s in the per-iteration index math. - Remove the `hidden_size_x3 % 3 == 0` and `hidden_size % num_heads_ == 0` checks here; they are now enforced uniformly in `AttentionBase::CheckInputs` with clearer error messages. **`onnxruntime/contrib_ops/cpu/bert/attention_base.h`** - Replace `static_cast<int>` with `narrow<int>` for `num_heads_`, `rotary_embedding_`, the `parameters` struct outputs, and `GetPresent`'s `past_sequence_length`. Without this, any `int64_t` value outside the `int` range (e.g., a `num_heads` attribute of `2^31`, or a `past` sequence length of `2^31`) silently truncates to an unrelated `int` value that is then propagated to downstream kernels and used in arithmetic, enabling division by zero, sign flips, or out-of-bounds indexing. - Drop the `static_cast<int>` from the `past_dims[2]` / `past_dims[4]` shape comparisons so the equality check uses the full `int64_t` value; previously a `past` tensor whose dim's low 32 bits happened to match `num_heads_` (or `k_hidden_size / num_heads_`) would pass validation despite having the wrong physical shape. - In `CheckInputs`, when `require_same_hidden_size_` is true, reject `bias_dims[0]` not a multiple of 3 with a clear error (Q, K, V are packed and share a hidden size). - In `CheckInputs`, when `qkv_hidden_sizes` is not set, also reject `q_hidden_size % num_heads_ != 0` (mirrors the existing check on the `qkv_hidden_sizes` path). **`onnxruntime/test/contrib_ops/quantize_attention_op_test.cc`** - 4 regression tests for the per-column shape validation: - `InvalidWeightScalePerColumnShape` - `InvalidWeightScalePerColumnRank` - `InvalidWeightZeroPointPerColumnShape` - `InvalidWeightZeroPointPerColumnRank` - 3 regression tests for the divisibility / narrowing checks (sharing a `RunQAttentionExpectFailure` helper): - `InvalidBiasDimNotMultipleOfThree` - `InvalidHiddenSizeNotDivisibleByNumHeads` - `InvalidNumHeadsOverflowsInt` (`num_heads = INT_MAX + 1` triggers `gsl::narrowing_error`) ### Testing All `QAttention*` / `AttentionTest*` / `MultiHeadAttention*` tests (97/97) pass locally on CPU Release build. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent b36d8db commit 18ae6da

3 files changed

Lines changed: 239 additions & 42 deletions

File tree

onnxruntime/contrib_ops/cpu/bert/attention_base.h

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <array>
77
#include <vector>
88
#include "core/common/common.h"
9+
#include "core/common/narrow.h"
910
#include "core/providers/cpu/mlas_backend_kernel_selector_config_utils.h"
1011
#ifndef SHARED_PROVIDER
1112
#include "core/framework/op_kernel.h"
@@ -57,11 +58,11 @@ class AttentionBase {
5758
AttentionBase(const KernelInfoType& info, bool require_same_hidden_size) {
5859
int64_t num_heads = 0;
5960
ORT_ENFORCE(info.GetAttr("num_heads", &num_heads).IsOK() && num_heads > 0);
60-
num_heads_ = static_cast<int>(num_heads);
61+
num_heads_ = narrow<int>(num_heads);
6162

6263
is_unidirectional_ = info.template GetAttrOrDefault<int64_t>("unidirectional", 0) == 1;
6364
do_rotary_ = info.template GetAttrOrDefault<int64_t>("do_rotary", 0) == 1;
64-
rotary_embedding_ = static_cast<int>(info.template GetAttrOrDefault<int64_t>("rotary_embedding_dim", 0));
65+
rotary_embedding_ = narrow<int>(info.template GetAttrOrDefault<int64_t>("rotary_embedding_dim", 0));
6566
mask_filter_value_ = info.template GetAttrOrDefault<float>("mask_filter_value", -10000.0f);
6667
scale_ = info.template GetAttrOrDefault<float>("scale", 0.0f);
6768
if (!info.template GetAttrs<int64_t>("qkv_hidden_sizes", qkv_hidden_sizes_).IsOK()) {
@@ -222,6 +223,14 @@ inline Status AttentionBase::CheckInputs(const TensorShape& input_shape,
222223
"Input 'bias' dimension 0 should have same length as dimension 1 of input 'weights'");
223224
}
224225

226+
// Q, K, V are packed along bias_dims[0]. When their hidden sizes are required to be equal,
227+
// bias_dims[0] == 3 * hidden_size must be a multiple of 3.
228+
if (require_same_hidden_size_ && bias_dims[0] % 3 != 0) {
229+
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
230+
"Input 'bias' dimension 0 (", bias_dims[0],
231+
") must be a multiple of 3 (Q, K, V are packed and have equal hidden sizes).");
232+
}
233+
225234
int64_t q_hidden_size = bias_dims[0] / static_cast<int64_t>(3);
226235
int64_t k_hidden_size = q_hidden_size;
227236
int64_t v_hidden_size = k_hidden_size;
@@ -241,6 +250,10 @@ inline Status AttentionBase::CheckInputs(const TensorShape& input_shape,
241250
q_hidden_size = qkv_hidden_sizes_[0];
242251
k_hidden_size = qkv_hidden_sizes_[1];
243252
v_hidden_size = qkv_hidden_sizes_[2];
253+
} else if (q_hidden_size % num_heads_ != 0) {
254+
// Match the error message produced by the qkv_hidden_sizes path above.
255+
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
256+
"hidden_size should be divisible by num_heads:", q_hidden_size);
244257
}
245258

246259
int64_t kv_sequence_length = sequence_length;
@@ -282,14 +295,14 @@ inline Status AttentionBase::CheckInputs(const TensorShape& input_shape,
282295
"Inputs 'past' dimension 1 shall have same length as dimension 0 of input 0");
283296
}
284297

285-
if (static_cast<int>(past_dims[2]) != num_heads_) {
298+
if (past_dims[2] != num_heads_) {
286299
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
287300
"Inputs 'past' dimension 2 shall have length of num_heads", num_heads_);
288301
}
289302

290-
if (static_cast<int>(past_dims[4]) != k_hidden_size / num_heads_) {
303+
if (past_dims[4] != k_hidden_size / num_heads_) {
291304
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
292-
"Inputs 'past' dimension 2 shall have length of ", k_hidden_size / num_heads_);
305+
"Inputs 'past' dimension 4 shall have length of ", k_hidden_size / num_heads_);
293306
}
294307

295308
if (!past_present_share_buffer_) {
@@ -348,17 +361,17 @@ inline Status AttentionBase::CheckInputs(const TensorShape& input_shape,
348361

349362
if (parameters != nullptr) {
350363
AttentionParameters* output_parameters = reinterpret_cast<AttentionParameters*>(parameters);
351-
output_parameters->batch_size = static_cast<int>(batch_size);
352-
output_parameters->sequence_length = static_cast<int>(sequence_length);
353-
output_parameters->past_sequence_length = static_cast<int>(past_sequence_length);
354-
output_parameters->kv_sequence_length = static_cast<int>(kv_sequence_length);
355-
output_parameters->total_sequence_length = static_cast<int>(total_sequence_length);
356-
output_parameters->max_sequence_length = static_cast<int>(max_sequence_length);
357-
output_parameters->input_hidden_size = static_cast<int>(input_hidden_size);
358-
output_parameters->hidden_size = static_cast<int>(q_hidden_size);
359-
output_parameters->v_hidden_size = static_cast<int>(v_hidden_size);
360-
output_parameters->head_size = static_cast<int>(q_hidden_size) / num_heads_;
361-
output_parameters->v_head_size = static_cast<int>(v_hidden_size) / num_heads_;
364+
output_parameters->batch_size = narrow<int>(batch_size);
365+
output_parameters->sequence_length = narrow<int>(sequence_length);
366+
output_parameters->past_sequence_length = narrow<int>(past_sequence_length);
367+
output_parameters->kv_sequence_length = narrow<int>(kv_sequence_length);
368+
output_parameters->total_sequence_length = narrow<int>(total_sequence_length);
369+
output_parameters->max_sequence_length = narrow<int>(max_sequence_length);
370+
output_parameters->input_hidden_size = narrow<int>(input_hidden_size);
371+
output_parameters->hidden_size = narrow<int>(q_hidden_size);
372+
output_parameters->v_hidden_size = narrow<int>(v_hidden_size);
373+
output_parameters->head_size = narrow<int>(q_hidden_size) / num_heads_;
374+
output_parameters->v_head_size = narrow<int>(v_hidden_size) / num_heads_;
362375
output_parameters->num_heads = num_heads_;
363376
output_parameters->is_unidirectional = is_unidirectional_;
364377
output_parameters->past_present_share_buffer = (past_present_share_buffer_ != 0 && past != nullptr);
@@ -398,7 +411,7 @@ inline Tensor* AttentionBase::GetPresent(TOpKernelContext* context,
398411
int head_size,
399412
int kv_sequence_length,
400413
int& past_sequence_length) const {
401-
past_sequence_length = (nullptr != past) ? static_cast<int>(past->Shape().GetDims()[3]) : 0;
414+
past_sequence_length = (nullptr != past) ? narrow<int>(past->Shape().GetDims()[3]) : 0;
402415
std::array<int64_t, 5> present_dims{2, batch_size, num_heads_,
403416
static_cast<int64_t>(kv_sequence_length) + past_sequence_length, head_size};
404417

onnxruntime/contrib_ops/cpu/quantization/attention_quant.cc

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "core/util/math.h"
88
#include "core/util/qmath.h"
99
#include "core/util/math_cpuonly.h"
10+
#include "core/common/narrow.h"
1011
#include "core/common/safeint.h"
1112
#include "core/platform/threadpool.h"
1213
#include "core/mlas/inc/mlas.h"
@@ -71,8 +72,8 @@ Status QAttention<T>::PrePack(const Tensor& weights, int input_idx, AllocatorPtr
7172
return Status::OK();
7273
}
7374

74-
const size_t input_hidden_size = static_cast<size_t>(weights_dims[0]);
75-
const size_t hidden_size_x3 = static_cast<size_t>(weights_dims[1]);
75+
const size_t input_hidden_size = narrow<size_t>(weights_dims[0]);
76+
const size_t hidden_size_x3 = narrow<size_t>(weights_dims[1]);
7677
const size_t hidden_size = hidden_size_x3 / 3;
7778
const size_t head_size = hidden_size / num_heads_;
7879

@@ -89,8 +90,8 @@ Status QAttention<T>::PrePack(const Tensor& weights, int input_idx, AllocatorPtr
8990
return Status::OK();
9091
}
9192

92-
const size_t loop_len = 3 * static_cast<size_t>(num_heads_);
93-
size_t packed_weights_data_size = packed_weights_size_ * loop_len;
93+
const size_t loop_len = SafeInt<size_t>(3) * num_heads_;
94+
size_t packed_weights_data_size = SafeInt<size_t>(packed_weights_size_) * loop_len;
9495

9596
packed_weights_ = IAllocator::MakeUniquePtr<void>(alloc, packed_weights_data_size, true);
9697
std::byte* packed_weights_data = static_cast<std::byte*>(packed_weights_.get());
@@ -171,12 +172,6 @@ Status QAttention<T>::Compute(OpKernelContext* context) const {
171172
T input_scale = *(input_scale_tensor->Data<T>());
172173

173174
bool is_weight_scale_per_column = !IsScalarOr1ElementVector(weight_scale_tensor);
174-
const T* weight_scale_data = weight_scale_tensor->Data<T>();
175-
176-
std::vector<T> dequant_scales(weight_scale_data, weight_scale_data + weight_scale_tensor->Shape().Size());
177-
std::for_each(dequant_scales.begin(), dequant_scales.end(), [&input_scale](float& dequant_scale) {
178-
return dequant_scale *= input_scale;
179-
});
180175

181176
uint8_t input_zero_point = 0;
182177
if (i_zp_tensor != nullptr) {
@@ -194,14 +189,42 @@ Status QAttention<T>::Compute(OpKernelContext* context) const {
194189
}
195190

196191
const auto& shape = input->Shape();
197-
const int batch_size = static_cast<int>(shape[0]);
198-
const int sequence_length = static_cast<int>(shape[1]);
199-
const int input_hidden_size = static_cast<int>(shape[2]);
200-
201-
const auto hidden_size_x3 = weights_shape.GetDims()[1];
202-
const int hidden_size = static_cast<int>(hidden_size_x3) / 3;
192+
const int batch_size = narrow<int>(shape[0]);
193+
const int sequence_length = narrow<int>(shape[1]);
194+
const int input_hidden_size = narrow<int>(shape[2]);
195+
196+
const int hidden_size_x3 = narrow<int>(weights_shape.GetDims()[1]);
197+
// AttentionBase::CheckInputs verifies that weights_dims[1] (== bias_dims[0]) is a multiple of 3
198+
// and that hidden_size = bias_dims[0] / 3 is divisible by num_heads_.
199+
const int hidden_size = hidden_size_x3 / 3;
203200
const int head_size = hidden_size / num_heads_;
204201

202+
// Validate per-column 'weight_scale' / 'weight_zero_point' shapes against the expected
203+
// 3 * hidden_size. Without this check, a malicious or malformed model can supply a
204+
// smaller per-column tensor and cause an out-of-bounds read in the GEMM loop below
205+
// (which indexes scales/zero-points using offsets up to ~3 * hidden_size - head_size).
206+
if (is_weight_scale_per_column) {
207+
ORT_RETURN_IF_NOT(weight_scale_tensor->Shape().NumDimensions() == 1 &&
208+
weight_scale_tensor->Shape().Size() == hidden_size_x3,
209+
"Input 'weight_scale' must be a scalar or a 1D tensor of size 3 * hidden_size (= ",
210+
hidden_size_x3, "), got shape ", weight_scale_tensor->Shape().ToString());
211+
}
212+
213+
if (is_weight_zp_per_column) {
214+
ORT_RETURN_IF_NOT(w_zp_tensor->Shape().NumDimensions() == 1 &&
215+
w_zp_tensor->Shape().Size() == hidden_size_x3,
216+
"Input 'weight_zero_point' must be a scalar or a 1D tensor of size 3 * hidden_size (= ",
217+
hidden_size_x3, "), got shape ", w_zp_tensor->Shape().ToString());
218+
}
219+
220+
// Build the dequantization scales after shape validation so that malformed
221+
// inputs are rejected before any allocation/copy work.
222+
const T* weight_scale_data = weight_scale_tensor->Data<T>();
223+
std::vector<T> dequant_scales(weight_scale_data, weight_scale_data + weight_scale_tensor->Shape().Size());
224+
std::for_each(dequant_scales.begin(), dequant_scales.end(), [&input_scale](T& dequant_scale) {
225+
return dequant_scale *= input_scale;
226+
});
227+
205228
std::vector<int64_t> output_shape(3);
206229
output_shape[0] = shape[0];
207230
output_shape[1] = shape[1];
@@ -216,16 +239,17 @@ Status QAttention<T>::Compute(OpKernelContext* context) const {
216239
auto* tp = context->GetOperatorThreadPool();
217240
// STEP.1: gemm_data(BS, 3NH) = Scale(input(BS, D) x weights(D, 3NH)) + bias(3NH)
218241
// D is hidden dimension of input, where input_hidden_size (D) could be larger than hidden_size (NH) when model is pruned.
219-
auto gemm_data = allocator->Alloc(SafeInt<size_t>(batch_size) * sequence_length * 3 * hidden_size * element_size);
242+
const auto batch_size_x_sequence_length_x_hidden_size = SafeInt<size_t>(batch_size) * sequence_length * hidden_size;
243+
void* gemm_data = allocator->Alloc(batch_size_x_sequence_length_x_hidden_size * 3 * element_size);
220244
BufferUniquePtr gemm_buffer(gemm_data, BufferDeleter(std::move(allocator)));
221245

222246
auto Q = reinterpret_cast<T*>(gemm_data);
223-
auto K = Q + static_cast<int64_t>(batch_size) * sequence_length * hidden_size;
224-
auto V = K + static_cast<int64_t>(batch_size) * sequence_length * hidden_size;
247+
auto K = Q + static_cast<size_t>(batch_size_x_sequence_length_x_hidden_size);
248+
auto V = K + static_cast<size_t>(batch_size_x_sequence_length_x_hidden_size);
225249
T* QKV[3] = {Q, K, V};
226250

227251
{
228-
const int loop_len = 3 * batch_size * num_heads_;
252+
const int loop_len = SafeInt<int>(3) * batch_size * num_heads_;
229253
const auto* input_data = input->Data<uint8_t>();
230254
const auto* bias_data = bias->Data<T>();
231255

@@ -243,16 +267,17 @@ Status QAttention<T>::Compute(OpKernelContext* context) const {
243267
scale_bias_procs.reserve(loop_len);
244268

245269
for (int i = 0; i < loop_len; i++) {
246-
const int batch_index = static_cast<int>((i / 3) / num_heads_);
247-
const int head_index = static_cast<int>((i / 3) % num_heads_);
248-
const int qkv_index = static_cast<int>(i % 3);
270+
const int batch_index = (i / 3) / num_heads_;
271+
const int head_index = (i / 3) % num_heads_;
272+
const int qkv_index = i % 3;
249273

250-
int input_offset = batch_index * sequence_length * input_hidden_size;
274+
int input_offset = SafeInt<int>(batch_index) * sequence_length * input_hidden_size;
251275
int weights_offset = qkv_index * hidden_size + head_index * head_size;
252276
int weights_scale_offset = is_weight_scale_per_column ? weights_offset : 0;
253277
int weights_zp_offset = is_weight_zp_per_column ? weights_offset : 0;
254278
float* qkv_dest = QKV[qkv_index];
255-
int qkv_offset = (batch_index * num_heads_ + head_index) * (sequence_length * head_size);
279+
int qkv_offset = (SafeInt<int>(batch_index) * num_heads_ + head_index) *
280+
(SafeInt<int>(sequence_length) * head_size);
256281

257282
// original transposed iteration
258283
// A: input (BxSxD) (B.)S x D S x D

0 commit comments

Comments
 (0)