Skip to content

Commit 72a970f

Browse files
authored
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 <rok@mihevc.org> Signed-off-by: Rok Mihevc <rok@mihevc.org>
1 parent e92d5d7 commit 72a970f

5 files changed

Lines changed: 122 additions & 26 deletions

File tree

cpp/src/arrow/ipc/metadata_internal.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,6 +1524,7 @@ Status GetSparseCSFIndexMetadata(const flatbuf::SparseTensorIndexCSF* sparse_ind
15241524

15251525
auto* fb_axis_order = sparse_index->axisOrder();
15261526
auto* fb_indices_buffers = sparse_index->indicesBuffers();
1527+
// ValidateSparseCSFIndexMetadata already checks this, keep this check defensively.
15271528
if (fb_axis_order == nullptr || fb_indices_buffers == nullptr ||
15281529
fb_axis_order->size() != fb_indices_buffers->size()) {
15291530
return Status::Invalid(
@@ -1559,6 +1560,9 @@ Status GetSparseTensorMetadata(const Buffer& metadata, std::shared_ptr<DataType>
15591560
auto dim = sparse_tensor->shape()->Get(i);
15601561

15611562
if (shape) {
1563+
if (dim->size() < 0) {
1564+
return Status::Invalid("Invalid sparse tensor dimension size: ", dim->size());
1565+
}
15621566
shape->push_back(dim->size());
15631567
}
15641568

@@ -1569,6 +1573,10 @@ Status GetSparseTensorMetadata(const Buffer& metadata, std::shared_ptr<DataType>
15691573
}
15701574

15711575
if (non_zero_length) {
1576+
if (sparse_tensor->non_zero_length() < 0) {
1577+
return Status::Invalid("Invalid sparse tensor non-zero length: ",
1578+
sparse_tensor->non_zero_length());
1579+
}
15721580
*non_zero_length = sparse_tensor->non_zero_length();
15731581
}
15741582

cpp/src/arrow/ipc/reader.cc

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2293,6 +2293,25 @@ Result<std::shared_ptr<Tensor>> ReadTensor(const Message& message) {
22932293

22942294
namespace {
22952295

2296+
Status ValidateSparseCSFIndexMetadata(const flatbuf::SparseTensorIndexCSF* sparse_index,
2297+
int64_t ndim) {
2298+
if (sparse_index == nullptr) {
2299+
return Status::Invalid("Missing CSF sparse index metadata");
2300+
}
2301+
auto* indptr_buffers = sparse_index->indptrBuffers();
2302+
auto* indices_buffers = sparse_index->indicesBuffers();
2303+
auto* axis_order = sparse_index->axisOrder();
2304+
if (ndim < 2 || indptr_buffers == nullptr || indices_buffers == nullptr ||
2305+
axis_order == nullptr || static_cast<int64_t>(indptr_buffers->size()) != ndim - 1 ||
2306+
static_cast<int64_t>(indices_buffers->size()) != ndim ||
2307+
static_cast<int64_t>(axis_order->size()) != ndim) {
2308+
return Status::Invalid(
2309+
"Inconsistent CSF sparse index: a CSF tensor must have at least 2 dimensions "
2310+
"with indptr, indices and axis_order counts of ndim - 1, ndim and ndim");
2311+
}
2312+
return Status::OK();
2313+
}
2314+
22962315
Result<std::shared_ptr<SparseIndex>> ReadSparseCOOIndex(
22972316
const flatbuf::SparseTensor* sparse_tensor, const std::vector<int64_t>& shape,
22982317
int64_t non_zero_length, io::RandomAccessFile* file) {
@@ -2339,9 +2358,6 @@ Result<std::shared_ptr<SparseIndex>> ReadSparseCSXIndex(
23392358
if (shape.size() != 2) {
23402359
return Status::Invalid("Invalid shape length for a sparse matrix");
23412360
}
2342-
if (non_zero_length < 0) {
2343-
return Status::Invalid("Invalid non-zero length for a sparse matrix");
2344-
}
23452361

23462362
auto* sparse_index = sparse_tensor->sparseIndex_as_SparseMatrixIndexCSX();
23472363

@@ -2411,15 +2427,9 @@ Result<std::shared_ptr<SparseIndex>> ReadSparseCSFIndex(
24112427
io::RandomAccessFile* file) {
24122428
auto* sparse_index = sparse_tensor->sparseIndex_as_SparseTensorIndexCSF();
24132429
const auto ndim = static_cast<int64_t>(shape.size());
2430+
RETURN_NOT_OK(ValidateSparseCSFIndexMetadata(sparse_index, ndim));
24142431
auto* indptr_buffers = sparse_index->indptrBuffers();
24152432
auto* indices_buffers = sparse_index->indicesBuffers();
2416-
if (ndim < 2 || indptr_buffers == nullptr || indices_buffers == nullptr ||
2417-
static_cast<int64_t>(indptr_buffers->size()) != ndim - 1 ||
2418-
static_cast<int64_t>(indices_buffers->size()) != ndim) {
2419-
return Status::Invalid(
2420-
"Inconsistent CSF sparse index: a CSF tensor must have at least 2 dimensions "
2421-
"with indptr and indices buffer counts of ndim - 1 and ndim");
2422-
}
24232433
std::vector<std::shared_ptr<Buffer>> indptr_data(ndim - 1);
24242434
std::vector<std::shared_ptr<Buffer>> indices_data(ndim);
24252435

@@ -2526,6 +2536,9 @@ Result<size_t> GetSparseTensorBodyBufferCount(SparseTensorFormat::type format_id
25262536
return 3;
25272537

25282538
case SparseTensorFormat::CSF:
2539+
if (ndim < 2) {
2540+
return Status::Invalid("Invalid shape length for a sparse CSF tensor");
2541+
}
25292542
return 2 * ndim;
25302543

25312544
default:
@@ -2621,9 +2634,11 @@ Result<std::shared_ptr<SparseTensor>> ReadSparseTensorPayload(const IpcPayload&
26212634
std::shared_ptr<DataType> indptr_type, indices_type;
26222635
std::vector<int64_t> axis_order, indices_size;
26232636

2637+
auto fb_sparse_index = sparse_tensor->sparseIndex_as_SparseTensorIndexCSF();
2638+
RETURN_NOT_OK(ValidateSparseCSFIndexMetadata(fb_sparse_index,
2639+
static_cast<int64_t>(shape.size())));
26242640
RETURN_NOT_OK(internal::GetSparseCSFIndexMetadata(
2625-
sparse_tensor->sparseIndex_as_SparseTensorIndexCSF(), &axis_order,
2626-
&indices_size, &indptr_type, &indices_type));
2641+
fb_sparse_index, &axis_order, &indices_size, &indptr_type, &indices_type));
26272642
ARROW_CHECK_EQ(indptr_type, indices_type);
26282643

26292644
const int64_t ndim = shape.size();

cpp/src/arrow/ipc/tensor_test.cc

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -510,17 +510,16 @@ namespace {
510510
// Build a SparseTensor IPC message carrying a CSF index with caller-controlled
511511
// buffer counts. This lets us exercise the reader's validation directly, since
512512
// such inconsistent indices can only be produced by hand-crafted flatbuffers.
513-
Result<std::shared_ptr<Message>> MakeCSFSparseTensorMessage(int num_dims,
514-
int num_indptr_buffers,
515-
int num_indices_buffers,
516-
int axis_order_size) {
513+
Result<std::shared_ptr<Message>> MakeCSFSparseTensorMessage(
514+
const std::vector<int64_t>& shape, int num_indptr_buffers, int num_indices_buffers,
515+
int axis_order_size, int64_t non_zero_length = 0) {
517516
flatbuffers::FlatBufferBuilder fbb;
518517

519518
auto value_type = flatbuf::CreateInt(fbb, 64, /*is_signed=*/true);
520519

521520
std::vector<flatbuffers::Offset<flatbuf::TensorDim>> dims;
522-
for (int i = 0; i < num_dims; ++i) {
523-
dims.push_back(flatbuf::CreateTensorDim(fbb, /*size=*/4, /*name=*/0));
521+
for (int64_t dim_size : shape) {
522+
dims.push_back(flatbuf::CreateTensorDim(fbb, dim_size, /*name=*/0));
524523
}
525524
auto fb_shape = fbb.CreateVector(dims);
526525

@@ -540,7 +539,7 @@ Result<std::shared_ptr<Message>> MakeCSFSparseTensorMessage(int num_dims,
540539

541540
flatbuf::Buffer data(0, 0);
542541
auto sparse_tensor = flatbuf::CreateSparseTensor(
543-
fbb, flatbuf::Type_Int, value_type.Union(), fb_shape, /*non_zero_length=*/0,
542+
fbb, flatbuf::Type_Int, value_type.Union(), fb_shape, non_zero_length,
544543
flatbuf::SparseTensorIndex::SparseTensorIndex_SparseTensorIndexCSF, csf.Union(),
545544
&data);
546545

@@ -555,33 +554,85 @@ Result<std::shared_ptr<Message>> MakeCSFSparseTensorMessage(int num_dims,
555554
return std::shared_ptr<Message>(std::move(message));
556555
}
557556

557+
IpcPayload MakeSparseTensorPayload(const std::shared_ptr<Message>& message,
558+
int num_body_buffers) {
559+
IpcPayload payload;
560+
payload.metadata = message->metadata();
561+
payload.body_buffers.assign(num_body_buffers, Buffer::FromString(""));
562+
return payload;
563+
}
564+
558565
} // namespace
559566

560567
TEST(TestSparseCSFIndex, RejectInconsistentBufferCounts) {
561568
// ndim == 1 is not a valid CSF index (it has no indptr buffers), and used to
562569
// reach SparseCSFIndex's constructor with an empty indptr vector.
563-
ASSERT_OK_AND_ASSIGN(
564-
auto message, MakeCSFSparseTensorMessage(/*num_dims=*/1, /*num_indptr_buffers=*/0,
565-
/*num_indices_buffers=*/1,
566-
/*axis_order_size=*/1));
570+
ASSERT_OK_AND_ASSIGN(auto message,
571+
MakeCSFSparseTensorMessage(/*shape=*/{4}, /*num_indptr_buffers=*/0,
572+
/*num_indices_buffers=*/1,
573+
/*axis_order_size=*/1));
567574
ASSERT_RAISES(Invalid, ReadSparseTensor(*message));
568575

569576
// Too many indices buffers for the declared number of dimensions, which used
570577
// to write past the end of the fixed-size index vectors.
571578
ASSERT_OK_AND_ASSIGN(
572-
message, MakeCSFSparseTensorMessage(/*num_dims=*/2, /*num_indptr_buffers=*/1,
579+
message, MakeCSFSparseTensorMessage(/*shape=*/{4, 4}, /*num_indptr_buffers=*/1,
573580
/*num_indices_buffers=*/3,
574581
/*axis_order_size=*/2));
575582
ASSERT_RAISES(Invalid, ReadSparseTensor(*message));
576583

577584
// axisOrder and indicesBuffers lengths disagree (out-of-bounds read).
578585
ASSERT_OK_AND_ASSIGN(
579-
message, MakeCSFSparseTensorMessage(/*num_dims=*/2, /*num_indptr_buffers=*/1,
586+
message, MakeCSFSparseTensorMessage(/*shape=*/{4, 4}, /*num_indptr_buffers=*/1,
580587
/*num_indices_buffers=*/2,
581588
/*axis_order_size=*/3));
582589
ASSERT_RAISES(Invalid, ReadSparseTensor(*message));
583590
}
584591

592+
TEST(TestSparseCSFIndex, RejectInconsistentPayloadBufferCounts) {
593+
ASSERT_OK_AND_ASSIGN(auto message,
594+
MakeCSFSparseTensorMessage(/*shape=*/{4}, /*num_indptr_buffers=*/0,
595+
/*num_indices_buffers=*/1,
596+
/*axis_order_size=*/1));
597+
ASSERT_RAISES(Invalid,
598+
internal::ReadSparseTensorPayload(MakeSparseTensorPayload(message, 2)));
599+
600+
ASSERT_OK_AND_ASSIGN(
601+
message, MakeCSFSparseTensorMessage(/*shape=*/{4, 4}, /*num_indptr_buffers=*/1,
602+
/*num_indices_buffers=*/3,
603+
/*axis_order_size=*/3));
604+
ASSERT_RAISES(Invalid,
605+
internal::ReadSparseTensorPayload(MakeSparseTensorPayload(message, 4)));
606+
}
607+
608+
TEST(TestSparseCSXIndex, RejectIndptrLengthOverflow) {
609+
auto empty = Buffer::FromString("");
610+
ASSERT_RAISES(Invalid,
611+
SparseCSRIndex::Make(int64(), {std::numeric_limits<int64_t>::max(), 1},
612+
/*non_zero_length=*/0, empty, empty));
613+
ASSERT_RAISES(Invalid,
614+
SparseCSCIndex::Make(int64(), {1, std::numeric_limits<int64_t>::max()},
615+
/*non_zero_length=*/0, empty, empty));
616+
}
617+
618+
TEST(TestSparseTensor, RejectNegativeShapeAndNonZeroLength) {
619+
// A negative non_zero_length must be rejected by GetSparseTensorMetadata,
620+
// otherwise the negative size product bypasses the index buffer-size guards.
621+
ASSERT_OK_AND_ASSIGN(
622+
auto message, MakeCSFSparseTensorMessage(/*shape=*/{4, 4}, /*num_indptr_buffers=*/1,
623+
/*num_indices_buffers=*/2,
624+
/*axis_order_size=*/2,
625+
/*non_zero_length=*/-1));
626+
ASSERT_RAISES(Invalid, ReadSparseTensor(*message));
627+
628+
// A negative dimension size must likewise be rejected.
629+
ASSERT_OK_AND_ASSIGN(
630+
message, MakeCSFSparseTensorMessage(/*shape=*/{-1, 4}, /*num_indptr_buffers=*/1,
631+
/*num_indices_buffers=*/2,
632+
/*axis_order_size=*/2));
633+
ASSERT_RAISES(Invalid, ReadSparseTensor(*message));
634+
}
635+
585636
} // namespace test
586637
} // namespace ipc
587638
} // namespace arrow

cpp/src/arrow/sparse_tensor.cc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "arrow/compare.h"
2727
#include "arrow/type_traits.h"
2828
#include "arrow/util/checked_cast.h"
29+
#include "arrow/util/int_util_overflow.h"
2930
#include "arrow/util/logging_internal.h"
3031
#include "arrow/visit_type_inline.h"
3132

@@ -297,6 +298,21 @@ std::string SparseCOOIndex::ToString() const { return std::string("SparseCOOInde
297298

298299
namespace internal {
299300

301+
Result<int64_t> ComputeSparseCSXIndptrLength(SparseMatrixCompressedAxis compressed_axis,
302+
const std::vector<int64_t>& shape) {
303+
if (shape.size() != 2) {
304+
return Status::Invalid("Invalid shape length for a sparse matrix");
305+
}
306+
const int64_t compressed_axis_size =
307+
compressed_axis == SparseMatrixCompressedAxis::ROW ? shape[0] : shape[1];
308+
const auto indptr_length =
309+
AddWithOverflow<int64_t>({compressed_axis_size, static_cast<int64_t>(1)});
310+
if (!indptr_length.has_value()) {
311+
return Status::Invalid("shape is inconsistent to the size of indptr buffer");
312+
}
313+
return indptr_length.value();
314+
}
315+
300316
Status ValidateSparseCSXIndex(const std::shared_ptr<DataType>& indptr_type,
301317
const std::shared_ptr<DataType>& indices_type,
302318
const std::vector<int64_t>& indptr_shape,

cpp/src/arrow/sparse_tensor.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ enum class SparseMatrixCompressedAxis : char {
203203
COLUMN
204204
};
205205

206+
ARROW_EXPORT
207+
Result<int64_t> ComputeSparseCSXIndptrLength(SparseMatrixCompressedAxis compressed_axis,
208+
const std::vector<int64_t>& shape);
209+
206210
ARROW_EXPORT
207211
Status ValidateSparseCSXIndex(const std::shared_ptr<DataType>& indptr_type,
208212
const std::shared_ptr<DataType>& indices_type,
@@ -252,7 +256,9 @@ class SparseCSXIndex : public SparseIndexBase<SparseIndexType> {
252256
const std::shared_ptr<DataType>& indices_type, const std::vector<int64_t>& shape,
253257
int64_t non_zero_length, std::shared_ptr<Buffer> indptr_data,
254258
std::shared_ptr<Buffer> indices_data) {
255-
std::vector<int64_t> indptr_shape({shape[0] + 1});
259+
ARROW_ASSIGN_OR_RAISE(auto indptr_length,
260+
ComputeSparseCSXIndptrLength(COMPRESSED_AXIS, shape));
261+
std::vector<int64_t> indptr_shape({indptr_length});
256262
std::vector<int64_t> indices_shape({non_zero_length});
257263
return Make(indptr_type, indices_type, indptr_shape, indices_shape, indptr_data,
258264
indices_data);

0 commit comments

Comments
 (0)