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

wgtmac pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new 1b1232bd fix(avro): key projection cache on Avro node to avoid OOB 
read (#1022)
1b1232bd is described below

commit 1b1232bdd0379ee10039dae3170df41ca93181fe
Author: Guotao Yu <[email protected]>
AuthorDate: Thu Oct 22 11:01:13 2026 +0800

    fix(avro): key projection cache on Avro node to avoid OOB read (#1022)
    
    The avro-to-projection cache was keyed on projections.data() only. All
    empty-struct projections share the same (null) data(), so a second empty
    struct with more Avro fields reused a cached vector sized for the first
    and read out of bounds. Include the Avro node in the key and reject
    out-of-range projected field indexes.
---
 src/iceberg/avro/avro_direct_decoder.cc         | 20 ++++++----
 src/iceberg/avro/avro_direct_decoder_internal.h | 18 ++++++++-
 src/iceberg/test/avro_data_test.cc              | 50 +++++++++++++++++++++++++
 src/iceberg/test/avro_test.cc                   | 49 ++++++++++++++++++++++++
 4 files changed, 128 insertions(+), 9 deletions(-)

diff --git a/src/iceberg/avro/avro_direct_decoder.cc 
b/src/iceberg/avro/avro_direct_decoder.cc
index 23bb8ca7..465bc4b2 100644
--- a/src/iceberg/avro/avro_direct_decoder.cc
+++ b/src/iceberg/avro/avro_direct_decoder.cc
@@ -160,7 +160,8 @@ Status DecodeStructToBuilder(const ::avro::NodePtr& 
avro_node, ::avro::Decoder&
 
   // Build a map from Avro field index to projection index (cached per struct 
schema)
   // -1 means the field should be skipped
-  const FieldProjection* cache_key = projections.data();
+  // Empty projections all share one data() pointer, so the Avro node is part 
of the key.
+  const DecodeContext::ProjectionCacheKey cache_key{projections.data(), 
avro_node.get()};
   auto cache_it = ctx.avro_to_projection_cache.find(cache_key);
   std::vector<int>* avro_to_projection;
 
@@ -168,18 +169,23 @@ Status DecodeStructToBuilder(const ::avro::NodePtr& 
avro_node, ::avro::Decoder&
     // Use cached mapping
     avro_to_projection = &cache_it->second;
   } else {
-    // Build and cache the mapping
-    auto [inserted_it, inserted] = ctx.avro_to_projection_cache.emplace(
-        cache_key, std::vector<int>(avro_node->leaves(), -1));
-    avro_to_projection = &inserted_it->second;
-
+    // Build and validate the mapping before caching so a failure leaves no 
partial entry
+    std::vector<int> mapping(avro_node->leaves(), -1);
     for (size_t proj_idx = 0; proj_idx < projections.size(); ++proj_idx) {
       const auto& field_projection = projections[proj_idx];
       if (field_projection.kind == FieldProjection::Kind::kProjected) {
         size_t avro_field_index = std::get<size_t>(field_projection.from);
-        (*avro_to_projection)[avro_field_index] = static_cast<int>(proj_idx);
+        if (avro_field_index >= mapping.size()) {
+          return InvalidArgument(
+              "Projected field index {} out of range for Avro record with {} 
fields",
+              avro_field_index, mapping.size());
+        }
+        mapping[avro_field_index] = static_cast<int>(proj_idx);
       }
     }
+    avro_to_projection =
+        &ctx.avro_to_projection_cache.emplace(cache_key, std::move(mapping))
+             .first->second;
   }
 
   // Read all Avro fields in order (must maintain decoder position)
diff --git a/src/iceberg/avro/avro_direct_decoder_internal.h 
b/src/iceberg/avro/avro_direct_decoder_internal.h
index d067b550..1e3505fc 100644
--- a/src/iceberg/avro/avro_direct_decoder_internal.h
+++ b/src/iceberg/avro/avro_direct_decoder_internal.h
@@ -19,6 +19,12 @@
 
 #pragma once
 
+#include <cstddef>
+#include <functional>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
 #include <arrow/array/builder_base.h>
 #include <avro/Decoder.hh>
 #include <avro/Node.hh>
@@ -41,9 +47,17 @@ struct DecodeContext {
   // Scratch buffer for binary/fixed/uuid/decimal data (reused across rows)
   std::vector<uint8_t> bytes_scratch;
   // Cache for avro field index to projection index mapping
-  // Key: pointer to projections array (identifies struct schema)
+  // Key: {projections array, Avro record node}; empty structs share a null 
data()
   // Value: vector mapping avro field index -> projection index (-1 if not 
projected)
-  std::unordered_map<const FieldProjection*, std::vector<int>> 
avro_to_projection_cache;
+  using ProjectionCacheKey = std::pair<const FieldProjection*, const 
::avro::Node*>;
+  struct ProjectionCacheKeyHash {
+    size_t operator()(const ProjectionCacheKey& key) const noexcept {
+      return std::hash<const FieldProjection*>{}(key.first) * 31 +
+             std::hash<const ::avro::Node*>{}(key.second);
+    }
+  };
+  std::unordered_map<ProjectionCacheKey, std::vector<int>, 
ProjectionCacheKeyHash>
+      avro_to_projection_cache;
 };
 
 /// \brief Directly decode Avro data to Arrow array builders without 
GenericDatum
diff --git a/src/iceberg/test/avro_data_test.cc 
b/src/iceberg/test/avro_data_test.cc
index 90ed4d03..9ec0221d 100644
--- a/src/iceberg/test/avro_data_test.cc
+++ b/src/iceberg/test/avro_data_test.cc
@@ -39,6 +39,7 @@
 #include <gtest/gtest.h>
 
 #include "iceberg/avro/avro_data_util_internal.h"
+#include "iceberg/avro/avro_direct_decoder_internal.h"
 #include "iceberg/avro/avro_direct_encoder_internal.h"
 #include "iceberg/avro/avro_schema_util_internal.h"
 #include "iceberg/expression/literal.h"
@@ -1900,6 +1901,55 @@ const std::vector<RoundTripParam> kRoundTripTestCases = {
     },
 };
 
+TEST(DirectDecoderTest, ProjectionCacheKeyedPerAvroNodeForEmptyStructs) {
+  auto make_struct = [](int32_t first_id, int count) {
+    std::vector<SchemaField> fields;
+    for (int i = 0; i < count; ++i) {
+      fields.push_back(SchemaField::MakeRequired(first_id + i, "f" + 
std::to_string(i),
+                                                 std::make_shared<IntType>()));
+    }
+    return std::make_shared<StructType>(std::move(fields));
+  };
+  Schema file_schema({SchemaField::MakeRequired(1, "narrow", make_struct(10, 
1)),
+                      SchemaField::MakeRequired(2, "wide", make_struct(20, 
6))});
+  Schema read_schema({SchemaField::MakeRequired(1, "narrow", make_struct(10, 
0)),
+                      SchemaField::MakeRequired(2, "wide", make_struct(20, 
0))});
+
+  ::avro::NodePtr avro_node;
+  ASSERT_THAT(ToAvroNodeVisitor{}.Visit(file_schema, &avro_node), IsOk());
+  ICEBERG_UNWRAP_OR_FAIL(auto projection,
+                         Project(read_schema, avro_node, 
/*prune_source=*/false));
+
+  auto out = ::avro::memoryOutputStream();
+  auto encoder = ::avro::binaryEncoder();
+  encoder->init(*out);
+  for (int i = 0; i < 7; ++i) {
+    encoder->encodeInt(i);
+  }
+  encoder->flush();
+  auto in = ::avro::memoryInputStream(*out);
+  auto decoder = ::avro::binaryDecoder();
+  decoder->init(*in);
+
+  ArrowSchema arrow_c_schema;
+  ASSERT_THAT(ToArrowSchema(read_schema, &arrow_c_schema), IsOk());
+  auto arrow_schema = ::arrow::ImportSchema(&arrow_c_schema).ValueOrDie();
+  auto builder =
+      
::arrow::MakeBuilder(std::make_shared<::arrow::StructType>(arrow_schema->fields()))
+          .ValueOrDie();
+
+  DecodeContext ctx;
+  ASSERT_THAT(DecodeAvroToBuilder(avro_node, *decoder, projection, read_schema,
+                                  /*metadata_context=*/{}, builder.get(), ctx),
+              IsOk());
+
+  std::vector<size_t> mapping_sizes;
+  for (const auto& [key, mapping] : ctx.avro_to_projection_cache) {
+    mapping_sizes.push_back(mapping.size());
+  }
+  EXPECT_THAT(mapping_sizes, ::testing::UnorderedElementsAre(1, 6, 2));
+}
+
 INSTANTIATE_TEST_SUITE_P(AllTypes, AvroRoundTripConversionTest,
                          ::testing::ValuesIn(kRoundTripTestCases),
                          [](const ::testing::TestParamInfo<RoundTripParam>& 
info) {
diff --git a/src/iceberg/test/avro_test.cc b/src/iceberg/test/avro_test.cc
index a631b4a1..a1611641 100644
--- a/src/iceberg/test/avro_test.cc
+++ b/src/iceberg/test/avro_test.cc
@@ -18,6 +18,7 @@
  */
 
 #include <array>
+#include <format>
 #include <initializer_list>
 #include <optional>
 #include <sstream>
@@ -399,6 +400,54 @@ TEST_F(AvroReaderTest, ReadWithBatchSize) {
   ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader));
 }
 
+TEST_F(AvroReaderTest, ProjectEmptyStructsOfDifferentWidths) {
+  auto make_struct = [](int32_t first_id, int count) {
+    std::vector<SchemaField> fields;
+    for (int i = 0; i < count; ++i) {
+      fields.push_back(SchemaField::MakeRequired(first_id + i, 
std::format("f{}", i),
+                                                 std::make_shared<IntType>()));
+    }
+    return std::make_shared<StructType>(std::move(fields));
+  };
+  auto write_schema = std::make_shared<Schema>(
+      std::vector<SchemaField>{SchemaField::MakeRequired(1, "narrow", 
make_struct(10, 1)),
+                               SchemaField::MakeRequired(2, "wide", 
make_struct(20, 6))});
+
+  ArrowSchema arrow_c_schema;
+  ASSERT_THAT(ToArrowSchema(*write_schema, &arrow_c_schema), IsOk());
+  auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie();
+  auto array =
+      ::arrow::json::ArrayFromJSONString(
+          arrow_schema, R"([[[1], [1, 2, 3, 4, 5, 6]], [[7], [7, 8, 9, 10, 11, 
12]]])")
+          .ValueOrDie();
+  struct ArrowArray arrow_array;
+  ASSERT_TRUE(::arrow::ExportArray(*array, &arrow_array).ok());
+
+  auto writer_result = WriterFactoryRegistry::Open(
+      FileFormatType::kAvro,
+      {.path = temp_avro_file_, .schema = write_schema, .io = file_io_});
+  ASSERT_THAT(writer_result, IsOk());
+  auto writer = std::move(writer_result.value());
+  ASSERT_THAT(writer->Write(&arrow_array), IsOk());
+  ASSERT_THAT(writer->Close(), IsOk());
+
+  auto read_schema = std::make_shared<Schema>(
+      std::vector<SchemaField>{SchemaField::MakeRequired(1, "narrow", 
make_struct(10, 0)),
+                               SchemaField::MakeRequired(2, "wide", 
make_struct(20, 0))});
+  ReaderProperties reader_properties;
+  reader_properties.Set(ReaderProperties::kAvroSkipDatum, true);
+  auto reader_result = ReaderFactoryRegistry::Open(
+      FileFormatType::kAvro, {.path = temp_avro_file_,
+                              .io = file_io_,
+                              .projection = read_schema,
+                              .properties = std::move(reader_properties)});
+  ASSERT_THAT(reader_result, IsOk());
+  auto reader = std::move(reader_result.value());
+
+  ASSERT_NO_FATAL_FAILURE(VerifyNextBatch(*reader, R"([[[], []], [[], []]])"));
+  ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader));
+}
+
 TEST_F(AvroReaderTest, BufferSizeConfiguration) {
   // Test default buffer size
   ReaderProperties properties1;

Reply via email to