2222#include < cstring>
2323#include < memory>
2424#include < numeric>
25+ #include < optional>
2526#include < string>
2627#include < type_traits>
2728#include < unordered_map>
@@ -1864,34 +1865,36 @@ class RecordBatchFileReaderImpl : public RecordBatchFileReader {
18641865 }
18651866
18661867 Future<> ReadFooterAsync (arrow::internal::Executor* executor) {
1867- const int32_t magic_size = static_cast <int >(strlen ( kArrowMagicBytes ));
1868+ constexpr int32_t kMagicSize = static_cast <int >(kArrowMagicBytes . size ( ));
18681869
1869- if (footer_offset_ <= magic_size * 2 + 4 ) {
1870+ if (footer_offset_ <= kMagicSize * 2 + 4 ) {
18701871 return Status::Invalid (" File is too small: " , footer_offset_);
18711872 }
18721873
1873- int file_end_size = static_cast <int >(magic_size + sizeof (int32_t ));
1874+ int file_end_size = static_cast <int >(kMagicSize + sizeof (int32_t ));
18741875 auto self = std::dynamic_pointer_cast<RecordBatchFileReaderImpl>(shared_from_this ());
18751876 auto read_magic = file_->ReadAsync (footer_offset_ - file_end_size, file_end_size);
18761877 if (executor) read_magic = executor->Transfer (std::move (read_magic));
18771878 return read_magic
18781879 .Then ([=](const std::shared_ptr<Buffer>& buffer)
18791880 -> Future<std::shared_ptr<Buffer>> {
1880- const int64_t expected_footer_size = magic_size + sizeof (int32_t );
1881+ const int64_t expected_footer_size = kMagicSize + sizeof (int32_t );
18811882 if (buffer->size () < expected_footer_size) {
18821883 return Status::Invalid (" Unable to read " , expected_footer_size,
18831884 " from end of file" );
18841885 }
18851886
1886- if (memcmp (buffer->data () + sizeof (int32_t ), kArrowMagicBytes , magic_size)) {
1887+ const auto magic_start = buffer->data () + sizeof (int32_t );
1888+ if (std::string_view (reinterpret_cast <const char *>(magic_start), kMagicSize ) !=
1889+ kArrowMagicBytes ) {
18871890 return Status::Invalid (" Not an Arrow file" );
18881891 }
18891892
18901893 int32_t footer_length = bit_util::FromLittleEndian (
18911894 *reinterpret_cast <const int32_t *>(buffer->data ()));
18921895
18931896 if (footer_length <= 0 ||
1894- footer_length > self->footer_offset_ - magic_size * 2 - 4 ) {
1897+ footer_length > self->footer_offset_ - kMagicSize * 2 - 4 ) {
18951898 return Status::Invalid (" File is smaller than indicated metadata size" );
18961899 }
18971900
@@ -2689,6 +2692,28 @@ Status ValidateFuzzBatch(const RecordBatchWithMetadata& batch) {
26892692 return Status::OK ();
26902693}
26912694
2695+ Status CompareFuzzBatches (const RecordBatchWithMetadata& left,
2696+ const RecordBatchWithMetadata& right) {
2697+ bool ok = true ;
2698+ if ((left.batch != nullptr ) != (right.batch != nullptr )) {
2699+ ok = false ;
2700+ } else if (left.batch ) {
2701+ ok &= left.batch ->Equals (*right.batch , EqualOptions{}.nans_equal (true ));
2702+ }
2703+ return ok ? Status::OK () : Status::Invalid (" Batches unequal" );
2704+ }
2705+
2706+ Status CompareFuzzBatches (const std::vector<RecordBatchWithMetadata>& left,
2707+ const std::vector<RecordBatchWithMetadata>& right) {
2708+ if (left.size () != right.size ()) {
2709+ return Status::Invalid (" Not the same number of batches" );
2710+ }
2711+ for (size_t i = 0 ; i < left.size (); ++i) {
2712+ RETURN_NOT_OK (CompareFuzzBatches (left[i], right[i]));
2713+ }
2714+ return Status::OK ();
2715+ }
2716+
26922717IpcReadOptions FuzzingOptions () {
26932718 IpcReadOptions options;
26942719 options.memory_pool = ::arrow::internal::fuzzing_memory_pool ();
@@ -2723,7 +2748,29 @@ Status FuzzIpcFile(const uint8_t* data, int64_t size) {
27232748
27242749 Status final_status;
27252750
2726- auto do_read = [&](bool pre_buffer) {
2751+ // Try to read the IPC file as a stream to compare the results (differential fuzzing)
2752+ auto do_stream_read = [&]() -> Result<std::vector<RecordBatchWithMetadata>> {
2753+ io::BufferReader buffer_reader (buffer);
2754+ // Skip magic bytes at the beginning
2755+ RETURN_NOT_OK (
2756+ buffer_reader.Advance (bit_util::RoundUpToMultipleOf8 (kArrowMagicBytes .length ())));
2757+ ARROW_ASSIGN_OR_RAISE (auto batch_reader, RecordBatchStreamReader::Open (
2758+ &buffer_reader, FuzzingOptions ()));
2759+
2760+ std::vector<RecordBatchWithMetadata> batches;
2761+ while (true ) {
2762+ ARROW_ASSIGN_OR_RAISE (auto batch, batch_reader->ReadNext ());
2763+ if (!batch.batch && !batch.custom_metadata ) {
2764+ // EOS
2765+ break ;
2766+ }
2767+ batches.push_back (batch);
2768+ }
2769+ return batches;
2770+ };
2771+
2772+ auto do_file_read =
2773+ [&](bool pre_buffer) -> Result<std::vector<RecordBatchWithMetadata>> {
27272774 io::BufferReader buffer_reader (buffer);
27282775 ARROW_ASSIGN_OR_RAISE (auto batch_reader,
27292776 RecordBatchFileReader::Open (&buffer_reader, FuzzingOptions ()));
@@ -2733,20 +2780,43 @@ Status FuzzIpcFile(const uint8_t* data, int64_t size) {
27332780 }
27342781
27352782 const int n_batches = batch_reader->num_record_batches ();
2783+ std::vector<RecordBatchWithMetadata> batches;
2784+ // Delay error return until the end, as we want to access all record batches
2785+ Status st;
27362786 for (int i = 0 ; i < n_batches; ++i) {
27372787 RecordBatchWithMetadata batch;
2738- auto st = batch_reader->ReadRecordBatchWithCustomMetadata (i).Value (&batch);
2739- final_status &= st;
2740- if (!st.ok ()) {
2741- continue ;
2742- }
2743- final_status &= ValidateFuzzBatch (batch);
2788+ st &= batch_reader->ReadRecordBatchWithCustomMetadata (i).Value (&batch);
2789+ st &= ValidateFuzzBatch (batch);
2790+ batches.push_back (batch);
27442791 }
2745- return Status::OK ();
2792+ RETURN_NOT_OK (st);
2793+ return batches;
27462794 };
27472795
2796+ // Lazily-initialized if the IPC reader succeeds
2797+ std::optional<Result<std::vector<RecordBatchWithMetadata>>> maybe_stream_batches;
2798+
27482799 for (const bool pre_buffer : {false , true }) {
2749- final_status &= do_read (pre_buffer);
2800+ auto maybe_file_batches = do_file_read (pre_buffer);
2801+ final_status &= maybe_file_batches.status ();
2802+ if (maybe_file_batches.ok ()) {
2803+ // IPC file read successful: differential fuzzing with IPC stream reader,
2804+ // if possible.
2805+ // NOTE: some valid IPC files may not be readable as IPC streams,
2806+ // for example because of excess spacing between IPC messages.
2807+ // A regular IPC file writer would not produce them, but fuzzing might.
2808+ if (!maybe_stream_batches.has_value ()) {
2809+ maybe_stream_batches = do_stream_read ();
2810+ final_status &= maybe_stream_batches->status ();
2811+ }
2812+ if (maybe_stream_batches->ok ()) {
2813+ // XXX: in some rare cases, an IPC file might read unequal to the enclosed
2814+ // IPC stream, for example if the footer skips some batches or orders the
2815+ // batches differently. We should revisit this if the fuzzer generates such
2816+ // files.
2817+ ARROW_CHECK_OK (CompareFuzzBatches (*maybe_file_batches, **maybe_stream_batches));
2818+ }
2819+ }
27502820 }
27512821
27522822 return final_status;
0 commit comments