Fix over-copy of packed sub-byte tensors in OrtApi::GetValue#29157
Fix over-copy of packed sub-byte tensors in OrtApi::GetValue#29157neilmsft wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes a memory-safety bug in OrtApi::GetValue when copying tensors with packed sub-byte element types (INT4/UINT4) out of tensor sequences, ensuring the copy size matches the tensor’s actual packed storage size.
Changes:
- Update
PopulateTensorWithDatato copytensor.SizeInBytes()(packing-aware) instead ofelement_type->Size() * num_elems. - Simplify
PopulateTensorWithDataby removing the unusedelem_sizeparameter. - Add a shared-lib C API test covering sequence
GetValue()round-trips for INT4/UINT4 tensors with odd element counts (packed byte edge case).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| onnxruntime/core/session/onnxruntime_c_api.cc | Fix tensor byte-copy sizing in GetValue() sequence element extraction for packed sub-byte tensors. |
| onnxruntime/test/shared_lib/test_nontensor_types.cc | Add regression test for GetValue() on sequences of INT4/UINT4 tensors with odd lengths (packed storage). |
Comments suppressed due to low confidence (1)
onnxruntime/core/session/onnxruntime_c_api.cc:2035
- In the string-tensor path,
std::copy(str_span.begin(), str_span.end(), dst)copiesnum_elemsstrings into a buffer sized forlenelements. The precondition only checksnum_elems < len, sonum_elems > lenwould write past the destination. Copy onlylenelements (and ignore extras) to keep this helper safe/consistent with the non-string path.
const std::string* strings = reinterpret_cast<const std::string*>(data_elem);
auto str_span = gsl::make_span(strings, num_elems);
auto* dst = tensor.MutableData<std::string>();
std::copy(str_span.begin(), str_span.end(), dst);
|
Issues not addressed by PR #29157.
Can we address at least some of those here or should we open another PR? |
For the first one: Fixed. Size estimators are now packing-aware ( ceil(num_elements / sub_elems_per_byte) * element_size ; 2 for int4/uint4/float, 4 for int2/uint2), applied to all three paths (generic, Unique , ConstantOfShape ). This matches the packing-aware post-exec Tensor::SizeInBytes() check. Added ConstantFoldingSubByteOutputSizeIsPacked covering fold/block cases. For the second: Fixed. Clarified the sizeof * count formula only holds for >= 1-byte types, sub-byte types return the smaller packed size. For the fourth: Fixed in onnxruntime_cxx_api.h - I noted element count ≠ byte size for sub-byte types. For the third: Deferring to a follow-up. As you said, it's a usability gap, not a correctness bug, and needs new public wrapper types plus reworking CreateTensor 's count * sizeof(T) sizing. |
| /// For packed sub-byte types (e.g. int4/uint4) this is the element count, which is NOT the same | ||
| /// as the storage size in bytes: multiple elements share a storage byte, so the raw buffer is | ||
| /// smaller than the element count. Use Ort::Value::GetTensorSizeInBytes() when sizing or bounds- |
There was a problem hiding this comment.
"which is NOT the same as the storage size in bytes"
nit: this is true for element types that are larger than one byte too. this sentence feels overly specific in this context - perhaps we can drop it. I think the recommendation in the next sentence to use GetTensorSizeInBytes() is still good to have.
There was a problem hiding this comment.
Done. dropped the sub-byte-specific sentence, kept the GetTensorSizeInBytes()
| int64_t num_elements, size_t element_size) { | ||
| const int64_t sub_elems_per_byte = GetNumSubElemsPerByteForConstantFolding(elem_type); | ||
| const int64_t num_storage_units = (num_elements + (sub_elems_per_byte - 1)) / sub_elems_per_byte; | ||
| return SafeInt<int64_t>(num_storage_units) * static_cast<int64_t>(element_size); |
There was a problem hiding this comment.
we don't need to cast the operand of a SafeInt multiply, SafeInt will handle checking it already.
| return SafeInt<int64_t>(num_storage_units) * static_cast<int64_t>(element_size); | |
| return SafeInt<int64_t>(num_storage_units) * element_size; |
There was a problem hiding this comment.
Done. The multiply no longer casts the operand.
| // Number of sub-byte elements packed into a single storage byte for packed sub-byte element | ||
| // types (e.g. int4/uint4 pack 2 elements per byte, int2/uint2 pack 4). Returns 1 for all other | ||
| // (>= 1 byte) element types. Keep in sync with the sub-byte types registered in data_types.cc. | ||
| static int64_t GetNumSubElemsPerByteForConstantFolding(ONNX_NAMESPACE::TensorProto_DataType elem_type) { | ||
| switch (elem_type) { | ||
| case ONNX_NAMESPACE::TensorProto_DataType_INT4: | ||
| case ONNX_NAMESPACE::TensorProto_DataType_UINT4: | ||
| case ONNX_NAMESPACE::TensorProto_DataType_FLOAT4E2M1: | ||
| return 2; | ||
| case ONNX_NAMESPACE::TensorProto_DataType_INT2: | ||
| case ONNX_NAMESPACE::TensorProto_DataType_UINT2: | ||
| return 4; | ||
| default: | ||
| return 1; | ||
| } | ||
| } |
There was a problem hiding this comment.
is it possible to reuse the Tensor class member functions, like Tensor::CalculateTensorStorageSize()?
that implementation already accounts for the sub-byte type sizes so we don't have to keep this in sync here.
| input_element_size); | ||
| } else { | ||
| // The remaining Unique outputs are int64 index/count tensors. | ||
| total_size += SafeInt<int64_t>(input_num_elements) * static_cast<int64_t>(sizeof(int64_t)); |
There was a problem hiding this comment.
| total_size += SafeInt<int64_t>(input_num_elements) * static_cast<int64_t>(sizeof(int64_t)); | |
| total_size += SafeInt<int64_t>(input_num_elements) * sizeof(int64_t); |
Summary
OrtApi::GetValue on a sequence of tensors copied elements using element_type->Size() * element_count bytes. For packed sub-byte types (INT4/UINT4, two elements per byte) this is ~2× the real storage size, causing a heap over-read of the source and overflow of the destination.
Fix
In PopulateTensorWithData (onnxruntime/core/session/onnxruntime_c_api.cc), copy the tensor's actual packed size via Tensor::SizeInBytes() instead of element_type->Size() * num_elems, and drop the now-unused elem_size parameter. SizeInBytes() is packing-aware: identical for ≥1-byte types (no behavior change) and correct for sub-byte types.
Testing