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

pitrou 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 ab7a6af3eed GH-51649: [C++][Parquet] Tell encoding fuzzer to skip 
inputs with invalid parameters (#51650)
ab7a6af3eed is described below

commit ab7a6af3eed42f3429369082a067280b05f7c4dd
Author: Antoine Pitrou <[email protected]>
AuthorDate: Tue Oct 6 08:48:00 2026 +0200

    GH-51649: [C++][Parquet] Tell encoding fuzzer to skip inputs with invalid 
parameters (#51650)
    
    ### Rationale for this change
    
    `LLVMFuzzerTestOneInput` allows two return values: 0 and -1. While 0 allows 
for the input to be (potentially) saved in the corpus, -1 tells the fuzzer not 
to save it.
    
    In the words of the [fine 
documentation](https://llvm.org/docs/LibFuzzer.html#rejecting-unwanted-inputs):
    
    > It may be desirable to reject some inputs, i.e. to not add them to the 
corpus.
    > For example, when fuzzing an API consisting of parsing and other logic, 
one may want
    > to allow only those inputs into the corpus that parse successfully.
    > If the fuzz target returns -1 on a given input, libFuzzer will not add 
that
    > input to the corpus, regardless of what coverage it triggers.
    
    ### What changes are included in this PR?
    
    1. Add a `FuzzStatus` type that allows telling the fuzzing engine whether 
an input should be skipped (i.e. not saved in the corpus).
    2. Skip inputs where the parameter header contains invalid values in the 
Parquet encoding fuzzer
    
    ### Are these changes tested?
    
    Manually, by running the fuzzer from the command line and watching logs 
with `ARROW_FUZZING_VERBOSITY` enabled.
    
    Without a seed corpus and trying to generate 100k inputs 
(`./build/out/arrow/parquet-encoding-fuzz -seed=42 -runs=100000`), I get:
    * before:
    ```
    #100000 DONE   cov: 1377 ft: 2265 corp: 46/3357b lim: 778 exec/s: 7142 rss: 
201Mb
    Done 100000 runs in 14 second(s)
    ```
    * after:
    ```
    #100000 DONE   cov: 2051 ft: 3723 corp: 41/5725b lim: 832 exec/s: 50000 
rss: 259Mb
    Done 100000 runs in 2 second(s)
    ```
    
    It seems that this PR allows the fuzzer to proceed much faster when 
mutation generates such inputs. Despite running faster, it also reaches more 
coverage (see [doc](https://llvm.org/docs/LibFuzzer.html#output) about the 
`cov:` and `ft:` values).
    
    (*caveat:* these measurements is with a fixed random seed, and without a 
seed corpus)
    
    (of course, ideally the fuzzer would not produce such inputs at all, but 
ensuring that is [more 
involved](https://llvm.org/docs/LibFuzzer.html#user-supplied-mutators))
    
    ### Are there any user-facing changes?
    
    No.
    
    ### Was AI used for this PR?
    
    In accordance to the [AI generation 
guidelines](https://arrow.apache.org/docs/dev/developers/overview.html#ai-generated-code),
 please disclose below whether and how AI was used in this PR.
    
    **PR code and description written by:**
    
    - [x] Human
    - [ ] AI
    
    **Reviewed before submission by:**
    
    - [x] Human
    - [ ] AI
    - [ ] Not reviewed
    
    * GitHub Issue: #51649
    
    Authored-by: Antoine Pitrou <[email protected]>
    Signed-off-by: Antoine Pitrou <[email protected]>
---
 cpp/src/arrow/csv/fuzz.cc                       |  3 +-
 cpp/src/arrow/ipc/file_fuzz.cc                  |  3 +-
 cpp/src/arrow/ipc/stream_fuzz.cc                |  3 +-
 cpp/src/arrow/util/fuzz_internal.cc             | 22 +++++++++----
 cpp/src/arrow/util/fuzz_internal.h              | 29 ++++++++++++++++-
 cpp/src/parquet/arrow/encoding_fuzz.cc          |  3 +-
 cpp/src/parquet/arrow/fuzz.cc                   |  3 +-
 cpp/src/parquet/arrow/fuzz_encoding_internal.cc | 21 ++++++++++---
 cpp/src/parquet/arrow/fuzz_encoding_internal.h  |  4 ++-
 cpp/src/parquet/decoder.cc                      | 41 +++++++++++++++++++------
 cpp/src/parquet/encoding.h                      |  8 ++++-
 cpp/src/parquet/encoding_test.cc                |  2 ++
 12 files changed, 109 insertions(+), 33 deletions(-)

diff --git a/cpp/src/arrow/csv/fuzz.cc b/cpp/src/arrow/csv/fuzz.cc
index 39148f1aacd..2219efa2cb5 100644
--- a/cpp/src/arrow/csv/fuzz.cc
+++ b/cpp/src/arrow/csv/fuzz.cc
@@ -121,6 +121,5 @@ Status FuzzCsvReader(const uint8_t* data, int64_t size) {
 
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
   auto status = arrow::csv::FuzzCsvReader(data, static_cast<int64_t>(size));
-  arrow::internal::LogFuzzStatus(status, data, size);
-  return 0;
+  return arrow::internal::LogFuzzStatus(status, data, size);
 }
diff --git a/cpp/src/arrow/ipc/file_fuzz.cc b/cpp/src/arrow/ipc/file_fuzz.cc
index c3ce9c2dea6..2d2a924befc 100644
--- a/cpp/src/arrow/ipc/file_fuzz.cc
+++ b/cpp/src/arrow/ipc/file_fuzz.cc
@@ -24,6 +24,5 @@
 
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
   auto status = arrow::ipc::internal::FuzzIpcFile(data, 
static_cast<int64_t>(size));
-  arrow::internal::LogFuzzStatus(status, data, static_cast<int64_t>(size));
-  return 0;
+  return arrow::internal::LogFuzzStatus(status, data, 
static_cast<int64_t>(size));
 }
diff --git a/cpp/src/arrow/ipc/stream_fuzz.cc b/cpp/src/arrow/ipc/stream_fuzz.cc
index 04b12863bed..8aaef771260 100644
--- a/cpp/src/arrow/ipc/stream_fuzz.cc
+++ b/cpp/src/arrow/ipc/stream_fuzz.cc
@@ -24,6 +24,5 @@
 
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
   auto status = arrow::ipc::internal::FuzzIpcStream(data, 
static_cast<int64_t>(size));
-  arrow::internal::LogFuzzStatus(status, data, static_cast<int64_t>(size));
-  return 0;
+  return arrow::internal::LogFuzzStatus(status, data, 
static_cast<int64_t>(size));
 }
diff --git a/cpp/src/arrow/util/fuzz_internal.cc 
b/cpp/src/arrow/util/fuzz_internal.cc
index 28d210333dd..813652f49ae 100644
--- a/cpp/src/arrow/util/fuzz_internal.cc
+++ b/cpp/src/arrow/util/fuzz_internal.cc
@@ -34,7 +34,7 @@ MemoryPool* fuzzing_memory_pool() {
   return pool.get();
 }
 
-void LogFuzzStatus(const Status& st, const uint8_t* data, int64_t size) {
+int LogFuzzStatus(const FuzzStatus& st, const uint8_t* data, int64_t size) {
   static const int kVerbosity = []() {
     auto maybe_env_value =
         GetEnvVarInteger("ARROW_FUZZING_VERBOSITY", /*min_value=*/0, 
/*max_value=*/1);
@@ -48,13 +48,23 @@ void LogFuzzStatus(const Status& st, const uint8_t* data, 
int64_t size) {
     return 0;
   }();
 
-  if (!st.ok() && kVerbosity >= 1) {
-    ARROW_LOG(WARNING) << "Fuzzing input with size=" << size
-                       << " failed: " << st.ToString();
-  } else if (st.IsOutOfMemory()) {
+  const auto& reason = FuzzReason(st);
+  bool skip_input = std::holds_alternative<SkipFuzzInput>(st);
+  if (skip_input) {
+    if (kVerbosity >= 1) {
+      ARROW_LOG(WARNING) << "Skipping input with size=" << size << ": "
+                         << reason.ToString();
+    }
+  } else if (reason.IsOutOfMemory()) {
     ARROW_LOG(WARNING) << "Fuzzing input with size=" << size
-                       << " hit allocation failure: " << st.ToString();
+                       << " hit allocation failure: " << reason.ToString();
+  } else {
+    if (!reason.ok() && kVerbosity >= 1) {
+      ARROW_LOG(WARNING) << "Fuzzing input with size=" << size
+                         << " failed: " << reason.ToString();
+    }
   }
+  return skip_input ? -1 : 0;
 }
 
 }  // namespace arrow::internal
diff --git a/cpp/src/arrow/util/fuzz_internal.h 
b/cpp/src/arrow/util/fuzz_internal.h
index 5280b7ec1ff..685570e5cf7 100644
--- a/cpp/src/arrow/util/fuzz_internal.h
+++ b/cpp/src/arrow/util/fuzz_internal.h
@@ -18,12 +18,33 @@
 #pragma once
 
 #include <cstdint>
+#include <variant>
 
+#include "arrow/status.h"
 #include "arrow/type_fwd.h"
 #include "arrow/util/macros.h"
 
 namespace arrow::internal {
 
+// A tag used to signal the fuzzing engine that this input should not be saved
+// into the corpus.
+struct SkipFuzzInput {
+  Status reason;
+};
+
+// The Status alternative holds a regular fuzzing success or failure,
+// while the SkipFuzzInput alternative holds a setup failure (e.g. invalid
+// fuzzing parameters encoded in the payload).
+using FuzzStatus = std::variant<Status, SkipFuzzInput>;
+
+inline const Status& FuzzReason(const FuzzStatus& st) {
+  struct Visitor {
+    const Status& operator()(const SkipFuzzInput& v) { return v.reason; }
+    const Status& operator()(const Status& v) { return v; }
+  };
+  return std::visit(Visitor{}, st);
+}
+
 // The default rss_limit_mb on OSS-Fuzz is 2560 MB and we want to fail 
allocations
 // before that limit is reached, otherwise the fuzz target gets killed 
(GH-48105).
 constexpr int64_t kFuzzingMemoryLimit = 2200LL * 1000 * 1000;
@@ -32,6 +53,12 @@ constexpr int64_t kFuzzingMemoryLimit = 2200LL * 1000 * 1000;
 ARROW_EXPORT MemoryPool* fuzzing_memory_pool();
 
 /// Optionally log the outcome of fuzzing an input
-ARROW_EXPORT void LogFuzzStatus(const Status&, const uint8_t* data, int64_t 
size);
+///
+/// Returns the integer code to return from LLVMFuzzerTestOneInput.
+ARROW_EXPORT int LogFuzzStatus(const FuzzStatus&, const uint8_t* data, int64_t 
size);
+
+inline int LogFuzzStatus(const Status& status, const uint8_t* data, int64_t 
size) {
+  return LogFuzzStatus(FuzzStatus{status}, data, size);
+}
 
 }  // namespace arrow::internal
diff --git a/cpp/src/parquet/arrow/encoding_fuzz.cc 
b/cpp/src/parquet/arrow/encoding_fuzz.cc
index 64095795e28..3a968a3df87 100644
--- a/cpp/src/parquet/arrow/encoding_fuzz.cc
+++ b/cpp/src/parquet/arrow/encoding_fuzz.cc
@@ -22,6 +22,5 @@
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
   auto status =
       parquet::fuzzing::internal::FuzzEncoding(data, 
static_cast<int64_t>(size));
-  arrow::internal::LogFuzzStatus(status, data, static_cast<int64_t>(size));
-  return 0;
+  return arrow::internal::LogFuzzStatus(status, data, 
static_cast<int64_t>(size));
 }
diff --git a/cpp/src/parquet/arrow/fuzz.cc b/cpp/src/parquet/arrow/fuzz.cc
index c2d8021a5a5..9cf26eb1f2b 100644
--- a/cpp/src/parquet/arrow/fuzz.cc
+++ b/cpp/src/parquet/arrow/fuzz.cc
@@ -21,6 +21,5 @@
 
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
   auto status = parquet::fuzzing::internal::FuzzReader(data, 
static_cast<int64_t>(size));
-  arrow::internal::LogFuzzStatus(status, data, static_cast<int64_t>(size));
-  return 0;
+  return arrow::internal::LogFuzzStatus(status, data, 
static_cast<int64_t>(size));
 }
diff --git a/cpp/src/parquet/arrow/fuzz_encoding_internal.cc 
b/cpp/src/parquet/arrow/fuzz_encoding_internal.cc
index a0077391167..9b8917897c0 100644
--- a/cpp/src/parquet/arrow/fuzz_encoding_internal.cc
+++ b/cpp/src/parquet/arrow/fuzz_encoding_internal.cc
@@ -23,8 +23,10 @@
 #include <functional>
 #include <limits>
 #include <new>
+#include <optional>
 #include <sstream>
 #include <string_view>
+#include <typeinfo>
 
 #include "arrow/array.h"
 #include "arrow/array/builder_binary.h"
@@ -52,6 +54,7 @@ using ::arrow::MemoryPool;
 using ::arrow::Result;
 using ::arrow::Status;
 using ::arrow::TypedBufferBuilder;
+using ::arrow::internal::FuzzStatus;
 using ::parquet::arrow::FileReader;
 
 ColumnDescriptor MakeColumnDescriptor(Type::type type, int type_length) {
@@ -140,6 +143,10 @@ std::string FuzzEncodingHeader::Serialize() const {
                             
static_cast<Encoding::type>(ph.roundtrip_encoding_id),
                             static_cast<Type::type>(ph.type_id), 
ph.type_length,
                             ph.num_values);
+  if (!IsEncodingSupported(header.type, header.source_encoding) ||
+      !IsEncodingSupported(header.type, header.roundtrip_encoding)) {
+    return invalid_payload();
+  }
   if ((header.type == Type::FIXED_LEN_BYTE_ARRAY) ? (header.type_length <= 0)
                                                   : (header.type_length != 
-1)) {
     return invalid_payload();
@@ -492,13 +499,17 @@ struct TypedFuzzEncoding {
 
 }  // namespace
 
-Status FuzzEncoding(const uint8_t* data, int64_t size) {
+FuzzStatus FuzzEncoding(const uint8_t* data, int64_t size) {
   constexpr auto kInt32Max = std::numeric_limits<int32_t>::max();
 
-  ARROW_ASSIGN_OR_RAISE(const auto parse_result,
-                        FuzzEncodingHeader::Parse(std::span(data, size)));
-  const auto header = parse_result.first;
-  const auto encoded_data = parse_result.second;
+  auto maybe_parse_result = FuzzEncodingHeader::Parse(std::span(data, size));
+  if (!maybe_parse_result.ok()) {
+    // If the fuzz encoding header is invalid, we won't save this input
+    // in the corpus, because it didn't exercise anything interesting.
+    return ::arrow::internal::SkipFuzzInput{maybe_parse_result.status()};
+  }
+  const auto header = maybe_parse_result->first;
+  const auto encoded_data = maybe_parse_result->second;
   if (encoded_data.size() > static_cast<size_t>(kInt32Max)) {
     // Unlikely but who knows?
     return Status::Invalid("Fuzz payload too large");
diff --git a/cpp/src/parquet/arrow/fuzz_encoding_internal.h 
b/cpp/src/parquet/arrow/fuzz_encoding_internal.h
index 92d6b2834cc..8ce17eadc41 100644
--- a/cpp/src/parquet/arrow/fuzz_encoding_internal.h
+++ b/cpp/src/parquet/arrow/fuzz_encoding_internal.h
@@ -27,6 +27,7 @@
 #include "arrow/result.h"
 #include "arrow/status.h"
 #include "arrow/type_fwd.h"
+#include "arrow/util/fuzz_internal.h"
 #include "arrow/util/macros.h"
 #include "parquet/platform.h"
 #include "parquet/types.h"
@@ -76,7 +77,8 @@ struct FuzzEncodingHeader {
 };
 
 /// Fuzz a payload encoded as explained in FuzzEncodingHeader
-PARQUET_EXPORT ::arrow::Status FuzzEncoding(const uint8_t* data, int64_t size);
+PARQUET_EXPORT ::arrow::internal::FuzzStatus FuzzEncoding(const uint8_t* data,
+                                                          int64_t size);
 
 PARQUET_EXPORT ColumnDescriptor MakeColumnDescriptor(Type::type type,
                                                      int type_length = -1);
diff --git a/cpp/src/parquet/decoder.cc b/cpp/src/parquet/decoder.cc
index 47f2b46ebaa..c63d01bb16d 100644
--- a/cpp/src/parquet/decoder.cc
+++ b/cpp/src/parquet/decoder.cc
@@ -2557,27 +2557,50 @@ std::unique_ptr<Decoder> MakeDictDecoder(Type::type 
type_num,
 
 }  // namespace detail
 
-std::vector<Encoding::type> SupportedEncodings(Type::type physical_type) {
+namespace {
+
+// Discriminant allows definition of distinct functions for different 
initializers.
+template <auto Discriminant, typename T>
+const std::vector<T>& ConstVectorRef(std::initializer_list<T> init) {
+  static const auto vec = std::vector<T>(std::move(init));
+  return vec;
+}
+
+const std::vector<Encoding::type>& SupportedEncodingsRef(Type::type 
physical_type) {
   switch (physical_type) {
     case Type::BOOLEAN:
-      return {Encoding::PLAIN, Encoding::RLE};
+      return ConstVectorRef<Type::BOOLEAN>({Encoding::PLAIN, Encoding::RLE});
     case Type::INT32:
     case Type::INT64:
-      return {Encoding::PLAIN, Encoding::DELTA_BINARY_PACKED,
-              Encoding::BYTE_STREAM_SPLIT};
+      return ConstVectorRef<Type::INT32>(
+          {Encoding::PLAIN, Encoding::DELTA_BINARY_PACKED, 
Encoding::BYTE_STREAM_SPLIT});
     case Type::INT96:
-      return {Encoding::PLAIN};
+      return ConstVectorRef<Type::INT96>({Encoding::PLAIN});
     case Type::FLOAT:
     case Type::DOUBLE:
-      return {Encoding::PLAIN, Encoding::BYTE_STREAM_SPLIT};
+      return ConstVectorRef<Type::FLOAT>({Encoding::PLAIN, 
Encoding::BYTE_STREAM_SPLIT});
     case Type::FIXED_LEN_BYTE_ARRAY:
-      return {Encoding::PLAIN, Encoding::BYTE_STREAM_SPLIT, 
Encoding::DELTA_BYTE_ARRAY};
+      return ConstVectorRef<Type::FIXED_LEN_BYTE_ARRAY>(
+          {Encoding::PLAIN, Encoding::BYTE_STREAM_SPLIT, 
Encoding::DELTA_BYTE_ARRAY});
     case Type::BYTE_ARRAY:
-      return {Encoding::PLAIN, Encoding::DELTA_LENGTH_BYTE_ARRAY,
-              Encoding::DELTA_BYTE_ARRAY};
+      return ConstVectorRef<Type::BYTE_ARRAY>({Encoding::PLAIN,
+                                               
Encoding::DELTA_LENGTH_BYTE_ARRAY,
+                                               Encoding::DELTA_BYTE_ARRAY});
     default:
       throw ParquetException("Invalid physical type");
   }
 }
 
+}  // namespace
+
+const std::vector<Encoding::type>& SupportedEncodings(Type::type 
physical_type) {
+  return SupportedEncodingsRef(physical_type);
+}
+
+bool IsEncodingSupported(Type::type physical_type, Encoding::type encoding) {
+  const auto& supported_encodings = SupportedEncodingsRef(physical_type);
+  return std::find(supported_encodings.begin(), supported_encodings.end(), 
encoding) !=
+         supported_encodings.end();
+}
+
 }  // namespace parquet
diff --git a/cpp/src/parquet/encoding.h b/cpp/src/parquet/encoding.h
index 9a0cc55aa18..157d67264c5 100644
--- a/cpp/src/parquet/encoding.h
+++ b/cpp/src/parquet/encoding.h
@@ -482,6 +482,12 @@ std::unique_ptr<typename EncodingTraits<DType>::Decoder> 
MakeTypedDecoder(
 ///
 /// Only non-dictionary encodings are returned.
 PARQUET_EXPORT
-std::vector<Encoding::type> SupportedEncodings(Type::type physical_type);
+const std::vector<Encoding::type>& SupportedEncodings(Type::type 
physical_type);
+
+/// Return whether the encoding is supported for the given physical type
+///
+/// Dictionary encodings return false.
+PARQUET_EXPORT
+bool IsEncodingSupported(Type::type physical_type, Encoding::type encoding);
 
 }  // namespace parquet
diff --git a/cpp/src/parquet/encoding_test.cc b/cpp/src/parquet/encoding_test.cc
index 74b422cb606..6aa9256aa8a 100644
--- a/cpp/src/parquet/encoding_test.cc
+++ b/cpp/src/parquet/encoding_test.cc
@@ -77,8 +77,10 @@ void TestSupportedEncodingsConsistentWith(
       if (std::find(supported_encodings.begin(), supported_encodings.end(), 
encoding) !=
           supported_encodings.end()) {
         ASSERT_NO_THROW(func(type, encoding, descr));
+        ASSERT_TRUE(IsEncodingSupported(type, encoding));
       } else {
         ASSERT_THROW(func(type, encoding, descr), ParquetException);
+        ASSERT_FALSE(IsEncodingSupported(type, encoding));
       }
     }
   }

Reply via email to