diff --git a/src/iceberg/arrow_c_data_util.cc b/src/iceberg/arrow_c_data_util.cc index a1d765d2c8..264517a428 100644 --- a/src/iceberg/arrow_c_data_util.cc +++ b/src/iceberg/arrow_c_data_util.cc @@ -341,8 +341,14 @@ Result ProjectBatchNanoarrow( internal::ArrowArrayViewGuard input_view_guard(&input_view); ICEBERG_NANOARROW_RETURN_UNEXPECTED_WITH_ERROR( ArrowArrayViewSetArray(&input_view, &input_batch, &error), error); + // FULL validation is required here: the append path below uses + // ArrowArrayViewGet{String,Bytes}Unsafe and raw list/map offset arithmetic, + // which nanoarrow only documents as memory-safe after FULL validation + // (DEFAULT does not check that every offsets-buffer entry is non-decreasing + // and in-bounds). This matches the FULL validation already performed on the + // equivalent untrusted-batch boundary in manifest_reader.cc. ICEBERG_NANOARROW_RETURN_UNEXPECTED_WITH_ERROR( - ArrowArrayViewValidate(&input_view, NANOARROW_VALIDATION_LEVEL_DEFAULT, &error), + ArrowArrayViewValidate(&input_view, NANOARROW_VALIDATION_LEVEL_FULL, &error), error); ArrowArray output_array; diff --git a/src/iceberg/test/arrow_c_data_util_test.cc b/src/iceberg/test/arrow_c_data_util_test.cc index c93a928a3d..5996bc1344 100644 --- a/src/iceberg/test/arrow_c_data_util_test.cc +++ b/src/iceberg/test/arrow_c_data_util_test.cc @@ -20,6 +20,7 @@ /// \file arrow_c_data_util_test.cc /// Verifies ProjectBatch behavior across registered implementations. +#include #include #include #include @@ -191,4 +192,42 @@ TEST(ProjectBatchTest, ProjectionRejectsNestedPruning) { EXPECT_THAT(projection, IsError(ErrorKind::kInvalidArgument)); } +TEST(ProjectBatchTest, RejectsNonMonotonicStringOffsets) { + auto schema = std::make_shared( + std::vector{SchemaField::MakeRequired(1, "value", string())}); + + // Arrow's builders always emit monotonic offsets, so a batch with a + // corrupt offsets buffer has to be hand-assembled via the nanoarrow C ABI. + // offsets = [0, 5, 2] with a 2-byte data buffer: DEFAULT validation only + // checks the first (0) and last (2) offsets, both of which are consistent + // with the 2-byte buffer, so it accepts this array. FULL validation also + // walks the offsets buffer and rejects it because 2 < 5. + ArrowArray root; + ASSERT_EQ(ArrowArrayInitFromType(&root, NANOARROW_TYPE_STRUCT), NANOARROW_OK); + ASSERT_EQ(ArrowArrayAllocateChildren(&root, 1), NANOARROW_OK); + + ArrowArray* value_array = root.children[0]; + ASSERT_EQ(ArrowArrayInitFromType(value_array, NANOARROW_TYPE_STRING), NANOARROW_OK); + const std::array raw_offsets = {0, 5, 2}; + ASSERT_EQ(ArrowBufferAppend(ArrowArrayBuffer(value_array, 1), raw_offsets.data(), + raw_offsets.size() * sizeof(int32_t)), + NANOARROW_OK); + ASSERT_EQ(ArrowBufferAppend(ArrowArrayBuffer(value_array, 2), "ab", 2), NANOARROW_OK); + value_array->length = 2; + value_array->null_count = 0; + ASSERT_EQ(ArrowArrayFinishBuildingDefault(value_array, nullptr), NANOARROW_OK); + + root.length = 2; + root.null_count = 0; + ASSERT_EQ(ArrowArrayFinishBuildingDefault(&root, nullptr), NANOARROW_OK); + + auto projection = ProjectionContext::Make(*schema, *schema, nullptr); + ASSERT_THAT(projection, IsOk()); + + // No rows are selected, so the malformed offsets must be caught by batch + // validation itself rather than by appending the corrupt row. + auto result = ProjectBatch(&root, /*row_indices=*/{}, projection.value()); + EXPECT_THAT(result, IsError(ErrorKind::kInvalidArrowData)); +} + } // namespace iceberg::internal