This is an automated email from the ASF dual-hosted git repository.

rok pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/main by this push:
     new 72a970f713 GH-50304: [C++][IPC] Reject negative sparse tensor shape 
and non-zero length (#50305)
72a970f713 is described below

commit 72a970f713b334e9faa77113daf4dcf77c1bcdc2
Author: Rok Mihevc <[email protected]>
AuthorDate: Tue Jun 30 17:14:19 2026 +0200

    GH-50304: [C++][IPC] Reject negative sparse tensor shape and non-zero 
length (#50305)
    
    ### Rationale for this change
    
    `GetSparseTensorMetadata` and the sparse-tensor readers in 
`cpp/src/arrow/ipc/` currently accept several sizes from an IPC SparseTensor 
message without validation. We Want to introduce validations.
    
    `GetSparseTensorMetadata` accepts the per-dimension `TensorDim.size` and 
the tensor's `non_zero_length` from an IPC `SparseTensor` message without 
validation.
    
    ### What changes are included in this PR?
    
    - Reject a negative `size` and `non_zero_length` in 
`GetSparseTensorMetadata`.
    - Remove the now-redundant `non_zero_length < 0` check in 
`ReadSparseCSXIndex`
    
    ### Are these changes tested?
    
    Yes, see `cpp/src/arrow/ipc/tensor_test.cc`.
    
    ### Are there any user-facing changes?
    
    IPC messages are more scrutinized.
    
    * GitHub Issue: #50304
    
    Authored-by: Rok Mihevc <[email protected]>
    Signed-off-by: Rok Mihevc <[email protected]>
---
 cpp/src/arrow/ipc/metadata_internal.cc |  8 ++++
 cpp/src/arrow/ipc/reader.cc            | 39 +++++++++++------
 cpp/src/arrow/ipc/tensor_test.cc       | 77 ++++++++++++++++++++++++++++------
 cpp/src/arrow/sparse_tensor.cc         | 16 +++++++
 cpp/src/arrow/sparse_tensor.h          |  8 +++-
 5 files changed, 122 insertions(+), 26 deletions(-)

diff --git a/cpp/src/arrow/ipc/metadata_internal.cc 
b/cpp/src/arrow/ipc/metadata_internal.cc
index eef5cc221f..94a9569161 100644
--- a/cpp/src/arrow/ipc/metadata_internal.cc
+++ b/cpp/src/arrow/ipc/metadata_internal.cc
@@ -1524,6 +1524,7 @@ Status GetSparseCSFIndexMetadata(const 
flatbuf::SparseTensorIndexCSF* sparse_ind
 
   auto* fb_axis_order = sparse_index->axisOrder();
   auto* fb_indices_buffers = sparse_index->indicesBuffers();
+  // ValidateSparseCSFIndexMetadata already checks this, keep this check 
defensively.
   if (fb_axis_order == nullptr || fb_indices_buffers == nullptr ||
       fb_axis_order->size() != fb_indices_buffers->size()) {
     return Status::Invalid(
@@ -1559,6 +1560,9 @@ Status GetSparseTensorMetadata(const Buffer& metadata, 
std::shared_ptr<DataType>
       auto dim = sparse_tensor->shape()->Get(i);
 
       if (shape) {
+        if (dim->size() < 0) {
+          return Status::Invalid("Invalid sparse tensor dimension size: ", 
dim->size());
+        }
         shape->push_back(dim->size());
       }
 
@@ -1569,6 +1573,10 @@ Status GetSparseTensorMetadata(const Buffer& metadata, 
std::shared_ptr<DataType>
   }
 
   if (non_zero_length) {
+    if (sparse_tensor->non_zero_length() < 0) {
+      return Status::Invalid("Invalid sparse tensor non-zero length: ",
+                             sparse_tensor->non_zero_length());
+    }
     *non_zero_length = sparse_tensor->non_zero_length();
   }
 
diff --git a/cpp/src/arrow/ipc/reader.cc b/cpp/src/arrow/ipc/reader.cc
index 376d0fab4b..8ea04aaba6 100644
--- a/cpp/src/arrow/ipc/reader.cc
+++ b/cpp/src/arrow/ipc/reader.cc
@@ -2293,6 +2293,25 @@ Result<std::shared_ptr<Tensor>> ReadTensor(const 
Message& message) {
 
 namespace {
 
+Status ValidateSparseCSFIndexMetadata(const flatbuf::SparseTensorIndexCSF* 
sparse_index,
+                                      int64_t ndim) {
+  if (sparse_index == nullptr) {
+    return Status::Invalid("Missing CSF sparse index metadata");
+  }
+  auto* indptr_buffers = sparse_index->indptrBuffers();
+  auto* indices_buffers = sparse_index->indicesBuffers();
+  auto* axis_order = sparse_index->axisOrder();
+  if (ndim < 2 || indptr_buffers == nullptr || indices_buffers == nullptr ||
+      axis_order == nullptr || static_cast<int64_t>(indptr_buffers->size()) != 
ndim - 1 ||
+      static_cast<int64_t>(indices_buffers->size()) != ndim ||
+      static_cast<int64_t>(axis_order->size()) != ndim) {
+    return Status::Invalid(
+        "Inconsistent CSF sparse index: a CSF tensor must have at least 2 
dimensions "
+        "with indptr, indices and axis_order counts of ndim - 1, ndim and 
ndim");
+  }
+  return Status::OK();
+}
+
 Result<std::shared_ptr<SparseIndex>> ReadSparseCOOIndex(
     const flatbuf::SparseTensor* sparse_tensor, const std::vector<int64_t>& 
shape,
     int64_t non_zero_length, io::RandomAccessFile* file) {
@@ -2339,9 +2358,6 @@ Result<std::shared_ptr<SparseIndex>> ReadSparseCSXIndex(
   if (shape.size() != 2) {
     return Status::Invalid("Invalid shape length for a sparse matrix");
   }
-  if (non_zero_length < 0) {
-    return Status::Invalid("Invalid non-zero length for a sparse matrix");
-  }
 
   auto* sparse_index = sparse_tensor->sparseIndex_as_SparseMatrixIndexCSX();
 
@@ -2411,15 +2427,9 @@ Result<std::shared_ptr<SparseIndex>> ReadSparseCSFIndex(
     io::RandomAccessFile* file) {
   auto* sparse_index = sparse_tensor->sparseIndex_as_SparseTensorIndexCSF();
   const auto ndim = static_cast<int64_t>(shape.size());
+  RETURN_NOT_OK(ValidateSparseCSFIndexMetadata(sparse_index, ndim));
   auto* indptr_buffers = sparse_index->indptrBuffers();
   auto* indices_buffers = sparse_index->indicesBuffers();
-  if (ndim < 2 || indptr_buffers == nullptr || indices_buffers == nullptr ||
-      static_cast<int64_t>(indptr_buffers->size()) != ndim - 1 ||
-      static_cast<int64_t>(indices_buffers->size()) != ndim) {
-    return Status::Invalid(
-        "Inconsistent CSF sparse index: a CSF tensor must have at least 2 
dimensions "
-        "with indptr and indices buffer counts of ndim - 1 and ndim");
-  }
   std::vector<std::shared_ptr<Buffer>> indptr_data(ndim - 1);
   std::vector<std::shared_ptr<Buffer>> indices_data(ndim);
 
@@ -2526,6 +2536,9 @@ Result<size_t> 
GetSparseTensorBodyBufferCount(SparseTensorFormat::type format_id
       return 3;
 
     case SparseTensorFormat::CSF:
+      if (ndim < 2) {
+        return Status::Invalid("Invalid shape length for a sparse CSF tensor");
+      }
       return 2 * ndim;
 
     default:
@@ -2621,9 +2634,11 @@ Result<std::shared_ptr<SparseTensor>> 
ReadSparseTensorPayload(const IpcPayload&
       std::shared_ptr<DataType> indptr_type, indices_type;
       std::vector<int64_t> axis_order, indices_size;
 
+      auto fb_sparse_index = 
sparse_tensor->sparseIndex_as_SparseTensorIndexCSF();
+      RETURN_NOT_OK(ValidateSparseCSFIndexMetadata(fb_sparse_index,
+                                                   
static_cast<int64_t>(shape.size())));
       RETURN_NOT_OK(internal::GetSparseCSFIndexMetadata(
-          sparse_tensor->sparseIndex_as_SparseTensorIndexCSF(), &axis_order,
-          &indices_size, &indptr_type, &indices_type));
+          fb_sparse_index, &axis_order, &indices_size, &indptr_type, 
&indices_type));
       ARROW_CHECK_EQ(indptr_type, indices_type);
 
       const int64_t ndim = shape.size();
diff --git a/cpp/src/arrow/ipc/tensor_test.cc b/cpp/src/arrow/ipc/tensor_test.cc
index 4890dfc992..a9243e7799 100644
--- a/cpp/src/arrow/ipc/tensor_test.cc
+++ b/cpp/src/arrow/ipc/tensor_test.cc
@@ -510,17 +510,16 @@ namespace {
 // Build a SparseTensor IPC message carrying a CSF index with caller-controlled
 // buffer counts. This lets us exercise the reader's validation directly, since
 // such inconsistent indices can only be produced by hand-crafted flatbuffers.
-Result<std::shared_ptr<Message>> MakeCSFSparseTensorMessage(int num_dims,
-                                                            int 
num_indptr_buffers,
-                                                            int 
num_indices_buffers,
-                                                            int 
axis_order_size) {
+Result<std::shared_ptr<Message>> MakeCSFSparseTensorMessage(
+    const std::vector<int64_t>& shape, int num_indptr_buffers, int 
num_indices_buffers,
+    int axis_order_size, int64_t non_zero_length = 0) {
   flatbuffers::FlatBufferBuilder fbb;
 
   auto value_type = flatbuf::CreateInt(fbb, 64, /*is_signed=*/true);
 
   std::vector<flatbuffers::Offset<flatbuf::TensorDim>> dims;
-  for (int i = 0; i < num_dims; ++i) {
-    dims.push_back(flatbuf::CreateTensorDim(fbb, /*size=*/4, /*name=*/0));
+  for (int64_t dim_size : shape) {
+    dims.push_back(flatbuf::CreateTensorDim(fbb, dim_size, /*name=*/0));
   }
   auto fb_shape = fbb.CreateVector(dims);
 
@@ -540,7 +539,7 @@ Result<std::shared_ptr<Message>> 
MakeCSFSparseTensorMessage(int num_dims,
 
   flatbuf::Buffer data(0, 0);
   auto sparse_tensor = flatbuf::CreateSparseTensor(
-      fbb, flatbuf::Type_Int, value_type.Union(), fb_shape, 
/*non_zero_length=*/0,
+      fbb, flatbuf::Type_Int, value_type.Union(), fb_shape, non_zero_length,
       flatbuf::SparseTensorIndex::SparseTensorIndex_SparseTensorIndexCSF, 
csf.Union(),
       &data);
 
@@ -555,33 +554,85 @@ Result<std::shared_ptr<Message>> 
MakeCSFSparseTensorMessage(int num_dims,
   return std::shared_ptr<Message>(std::move(message));
 }
 
+IpcPayload MakeSparseTensorPayload(const std::shared_ptr<Message>& message,
+                                   int num_body_buffers) {
+  IpcPayload payload;
+  payload.metadata = message->metadata();
+  payload.body_buffers.assign(num_body_buffers, Buffer::FromString(""));
+  return payload;
+}
+
 }  // namespace
 
 TEST(TestSparseCSFIndex, RejectInconsistentBufferCounts) {
   // ndim == 1 is not a valid CSF index (it has no indptr buffers), and used to
   // reach SparseCSFIndex's constructor with an empty indptr vector.
-  ASSERT_OK_AND_ASSIGN(
-      auto message, MakeCSFSparseTensorMessage(/*num_dims=*/1, 
/*num_indptr_buffers=*/0,
-                                               /*num_indices_buffers=*/1,
-                                               /*axis_order_size=*/1));
+  ASSERT_OK_AND_ASSIGN(auto message,
+                       MakeCSFSparseTensorMessage(/*shape=*/{4}, 
/*num_indptr_buffers=*/0,
+                                                  /*num_indices_buffers=*/1,
+                                                  /*axis_order_size=*/1));
   ASSERT_RAISES(Invalid, ReadSparseTensor(*message));
 
   // Too many indices buffers for the declared number of dimensions, which used
   // to write past the end of the fixed-size index vectors.
   ASSERT_OK_AND_ASSIGN(
-      message, MakeCSFSparseTensorMessage(/*num_dims=*/2, 
/*num_indptr_buffers=*/1,
+      message, MakeCSFSparseTensorMessage(/*shape=*/{4, 4}, 
/*num_indptr_buffers=*/1,
                                           /*num_indices_buffers=*/3,
                                           /*axis_order_size=*/2));
   ASSERT_RAISES(Invalid, ReadSparseTensor(*message));
 
   // axisOrder and indicesBuffers lengths disagree (out-of-bounds read).
   ASSERT_OK_AND_ASSIGN(
-      message, MakeCSFSparseTensorMessage(/*num_dims=*/2, 
/*num_indptr_buffers=*/1,
+      message, MakeCSFSparseTensorMessage(/*shape=*/{4, 4}, 
/*num_indptr_buffers=*/1,
                                           /*num_indices_buffers=*/2,
                                           /*axis_order_size=*/3));
   ASSERT_RAISES(Invalid, ReadSparseTensor(*message));
 }
 
+TEST(TestSparseCSFIndex, RejectInconsistentPayloadBufferCounts) {
+  ASSERT_OK_AND_ASSIGN(auto message,
+                       MakeCSFSparseTensorMessage(/*shape=*/{4}, 
/*num_indptr_buffers=*/0,
+                                                  /*num_indices_buffers=*/1,
+                                                  /*axis_order_size=*/1));
+  ASSERT_RAISES(Invalid,
+                
internal::ReadSparseTensorPayload(MakeSparseTensorPayload(message, 2)));
+
+  ASSERT_OK_AND_ASSIGN(
+      message, MakeCSFSparseTensorMessage(/*shape=*/{4, 4}, 
/*num_indptr_buffers=*/1,
+                                          /*num_indices_buffers=*/3,
+                                          /*axis_order_size=*/3));
+  ASSERT_RAISES(Invalid,
+                
internal::ReadSparseTensorPayload(MakeSparseTensorPayload(message, 4)));
+}
+
+TEST(TestSparseCSXIndex, RejectIndptrLengthOverflow) {
+  auto empty = Buffer::FromString("");
+  ASSERT_RAISES(Invalid,
+                SparseCSRIndex::Make(int64(), 
{std::numeric_limits<int64_t>::max(), 1},
+                                     /*non_zero_length=*/0, empty, empty));
+  ASSERT_RAISES(Invalid,
+                SparseCSCIndex::Make(int64(), {1, 
std::numeric_limits<int64_t>::max()},
+                                     /*non_zero_length=*/0, empty, empty));
+}
+
+TEST(TestSparseTensor, RejectNegativeShapeAndNonZeroLength) {
+  // A negative non_zero_length must be rejected by GetSparseTensorMetadata,
+  // otherwise the negative size product bypasses the index buffer-size guards.
+  ASSERT_OK_AND_ASSIGN(
+      auto message, MakeCSFSparseTensorMessage(/*shape=*/{4, 4}, 
/*num_indptr_buffers=*/1,
+                                               /*num_indices_buffers=*/2,
+                                               /*axis_order_size=*/2,
+                                               /*non_zero_length=*/-1));
+  ASSERT_RAISES(Invalid, ReadSparseTensor(*message));
+
+  // A negative dimension size must likewise be rejected.
+  ASSERT_OK_AND_ASSIGN(
+      message, MakeCSFSparseTensorMessage(/*shape=*/{-1, 4}, 
/*num_indptr_buffers=*/1,
+                                          /*num_indices_buffers=*/2,
+                                          /*axis_order_size=*/2));
+  ASSERT_RAISES(Invalid, ReadSparseTensor(*message));
+}
+
 }  // namespace test
 }  // namespace ipc
 }  // namespace arrow
diff --git a/cpp/src/arrow/sparse_tensor.cc b/cpp/src/arrow/sparse_tensor.cc
index 0852a0cdb8..c23612e253 100644
--- a/cpp/src/arrow/sparse_tensor.cc
+++ b/cpp/src/arrow/sparse_tensor.cc
@@ -26,6 +26,7 @@
 #include "arrow/compare.h"
 #include "arrow/type_traits.h"
 #include "arrow/util/checked_cast.h"
+#include "arrow/util/int_util_overflow.h"
 #include "arrow/util/logging_internal.h"
 #include "arrow/visit_type_inline.h"
 
@@ -297,6 +298,21 @@ std::string SparseCOOIndex::ToString() const { return 
std::string("SparseCOOInde
 
 namespace internal {
 
+Result<int64_t> ComputeSparseCSXIndptrLength(SparseMatrixCompressedAxis 
compressed_axis,
+                                             const std::vector<int64_t>& 
shape) {
+  if (shape.size() != 2) {
+    return Status::Invalid("Invalid shape length for a sparse matrix");
+  }
+  const int64_t compressed_axis_size =
+      compressed_axis == SparseMatrixCompressedAxis::ROW ? shape[0] : shape[1];
+  const auto indptr_length =
+      AddWithOverflow<int64_t>({compressed_axis_size, 
static_cast<int64_t>(1)});
+  if (!indptr_length.has_value()) {
+    return Status::Invalid("shape is inconsistent to the size of indptr 
buffer");
+  }
+  return indptr_length.value();
+}
+
 Status ValidateSparseCSXIndex(const std::shared_ptr<DataType>& indptr_type,
                               const std::shared_ptr<DataType>& indices_type,
                               const std::vector<int64_t>& indptr_shape,
diff --git a/cpp/src/arrow/sparse_tensor.h b/cpp/src/arrow/sparse_tensor.h
index 5faae16bb2..a41024b741 100644
--- a/cpp/src/arrow/sparse_tensor.h
+++ b/cpp/src/arrow/sparse_tensor.h
@@ -203,6 +203,10 @@ enum class SparseMatrixCompressedAxis : char {
   COLUMN
 };
 
+ARROW_EXPORT
+Result<int64_t> ComputeSparseCSXIndptrLength(SparseMatrixCompressedAxis 
compressed_axis,
+                                             const std::vector<int64_t>& 
shape);
+
 ARROW_EXPORT
 Status ValidateSparseCSXIndex(const std::shared_ptr<DataType>& indptr_type,
                               const std::shared_ptr<DataType>& indices_type,
@@ -252,7 +256,9 @@ class SparseCSXIndex : public 
SparseIndexBase<SparseIndexType> {
       const std::shared_ptr<DataType>& indices_type, const 
std::vector<int64_t>& shape,
       int64_t non_zero_length, std::shared_ptr<Buffer> indptr_data,
       std::shared_ptr<Buffer> indices_data) {
-    std::vector<int64_t> indptr_shape({shape[0] + 1});
+    ARROW_ASSIGN_OR_RAISE(auto indptr_length,
+                          ComputeSparseCSXIndptrLength(COMPRESSED_AXIS, 
shape));
+    std::vector<int64_t> indptr_shape({indptr_length});
     std::vector<int64_t> indices_shape({non_zero_length});
     return Make(indptr_type, indices_type, indptr_shape, indices_shape, 
indptr_data,
                 indices_data);

Reply via email to