From c49e8c7e4ef3fae1e1f0063226f7c61d69834cf1 Mon Sep 17 00:00:00 2001 From: pedrobtz Date: Wed, 21 Oct 2026 23:55:58 +0200 Subject: [PATCH] fix: Don't read offsets from a too-short offsets buffer in default validation Minimal validation allows a zero-length array to have an offsets buffer shorter than one offset. Default validation then read a full offset from it. Read offsets only when the buffer holds them. Co-Authored-By: Claude Opus 5.5 --- src/nanoarrow/common/array.c | 20 ++++++++++++++---- src/nanoarrow/common/array_test.cc | 33 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/nanoarrow/common/array.c b/src/nanoarrow/common/array.c index e4538955c..d41612879 100644 --- a/src/nanoarrow/common/array.c +++ b/src/nanoarrow/common/array.c @@ -1834,7 +1834,10 @@ static int ArrowArrayViewValidateDefault(struct ArrowArrayView* array_view, switch (array_view->storage_type) { case NANOARROW_TYPE_STRING: case NANOARROW_TYPE_BINARY: - if (array_view->buffer_views[1].size_bytes != 0) { + // A zero-length array may have an offsets buffer that is too short to + // hold one offset: only read offsets from a buffer that holds them + if (array_view->buffer_views[1].size_bytes >= + (offset_plus_length + 1) * (int64_t)sizeof(int32_t)) { first_offset = ArrowBufferViewGetInt32Unsafe(&array_view->buffer_views[1], array_view->offset); if (first_offset < 0) { @@ -1872,7 +1875,10 @@ static int ArrowArrayViewValidateDefault(struct ArrowArrayView* array_view, case NANOARROW_TYPE_LARGE_STRING: case NANOARROW_TYPE_LARGE_BINARY: - if (array_view->buffer_views[1].size_bytes != 0) { + // A zero-length array may have an offsets buffer that is too short to + // hold one offset: only read offsets from a buffer that holds them + if (array_view->buffer_views[1].size_bytes >= + (offset_plus_length + 1) * (int64_t)sizeof(int64_t)) { first_offset = ArrowBufferViewGetInt64Unsafe(&array_view->buffer_views[1], array_view->offset); if (first_offset < 0) { @@ -1923,7 +1929,10 @@ static int ArrowArrayViewValidateDefault(struct ArrowArrayView* array_view, case NANOARROW_TYPE_LIST: case NANOARROW_TYPE_MAP: - if (array_view->buffer_views[1].size_bytes != 0) { + // A zero-length array may have an offsets buffer that is too short to + // hold one offset: only read offsets from a buffer that holds them + if (array_view->buffer_views[1].size_bytes >= + (offset_plus_length + 1) * (int64_t)sizeof(int32_t)) { first_offset = ArrowBufferViewGetInt32Unsafe(&array_view->buffer_views[1], array_view->offset); if (first_offset < 0) { @@ -1953,7 +1962,10 @@ static int ArrowArrayViewValidateDefault(struct ArrowArrayView* array_view, break; case NANOARROW_TYPE_LARGE_LIST: - if (array_view->buffer_views[1].size_bytes != 0) { + // A zero-length array may have an offsets buffer that is too short to + // hold one offset: only read offsets from a buffer that holds them + if (array_view->buffer_views[1].size_bytes >= + (offset_plus_length + 1) * (int64_t)sizeof(int64_t)) { first_offset = ArrowBufferViewGetInt64Unsafe(&array_view->buffer_views[1], array_view->offset); if (first_offset < 0) { diff --git a/src/nanoarrow/common/array_test.cc b/src/nanoarrow/common/array_test.cc index 9324cdde6..cccf829d2 100644 --- a/src/nanoarrow/common/array_test.cc +++ b/src/nanoarrow/common/array_test.cc @@ -20,6 +20,7 @@ #include #include #include +#include #include #if defined(NANOARROW_BUILD_TESTS_WITH_ARROW) @@ -2839,6 +2840,38 @@ TEST(ArrayTest, ArrayViewTestMove) { ArrowArrayViewReset(&array_view2); } +TEST(ArrayTest, ArrayViewTestShortOffsetsBuffer) { + // A zero-length array passes minimal validation with an offsets buffer that is + // too short to hold one offset; default validation must not read an offset from it + struct ArrowError error; + for (enum ArrowType type : {NANOARROW_TYPE_STRING, NANOARROW_TYPE_LARGE_STRING, + NANOARROW_TYPE_LIST, NANOARROW_TYPE_LARGE_LIST}) { + nanoarrow::UniqueSchema schema; + ASSERT_EQ(ArrowSchemaInitFromType(schema.get(), type), NANOARROW_OK); + if (type == NANOARROW_TYPE_LIST || type == NANOARROW_TYPE_LARGE_LIST) { + ASSERT_EQ(ArrowSchemaSetType(schema->children[0], NANOARROW_TYPE_INT32), + NANOARROW_OK); + } + + nanoarrow::UniqueArrayView array_view; + ASSERT_EQ(ArrowArrayViewInitFromSchema(array_view.get(), schema.get(), &error), + NANOARROW_OK); + ArrowArrayViewSetLength(array_view.get(), 0); + + // One byte short of a single offset, in a heap buffer of exactly that size + int64_t offset_bytes = + ArrowArrayViewGetBufferElementSizeBits(array_view.get(), 1) / 8; + std::unique_ptr offsets(new uint8_t[offset_bytes - 1]()); + array_view->buffer_views[1].data.as_uint8 = offsets.get(); + array_view->buffer_views[1].size_bytes = offset_bytes - 1; + + EXPECT_EQ(ArrowArrayViewValidate(array_view.get(), NANOARROW_VALIDATION_LEVEL_DEFAULT, + &error), + NANOARROW_OK) + << ArrowTypeString(type) << ": " << error.message; + } +} + TEST(ArrayTest, ArrayViewTestString) { struct ArrowArrayView array_view; struct ArrowError error;