Skip to content

Commit 589a1ff

Browse files
committed
GH-49261: [C++][CI] Use differential fuzzing on IPC file fuzzer
1 parent ebaaf07 commit 589a1ff

5 files changed

Lines changed: 95 additions & 21 deletions

File tree

cpp/src/arrow/ipc/feather.cc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ using internal::checked_cast;
5757
class ExtensionType;
5858

5959
namespace ipc {
60+
61+
using internal::kArrowMagicBytes;
62+
6063
namespace feather {
6164

6265
namespace {
@@ -787,8 +790,8 @@ Result<std::shared_ptr<Reader>> Reader::Open(
787790
// IPC Read options are ignored for ReaderV1
788791
RETURN_NOT_OK(result->Open(source));
789792
return result;
790-
} else if (memcmp(buffer->data(), internal::kArrowMagicBytes,
791-
strlen(internal::kArrowMagicBytes)) == 0) {
793+
} else if (std::string_view(buffer->data_as<char>(), kArrowMagicBytes.size()) ==
794+
kArrowMagicBytes) {
792795
std::shared_ptr<ReaderV2> result = std::make_shared<ReaderV2>();
793796
RETURN_NOT_OK(result->Open(source, options));
794797
return result;

cpp/src/arrow/ipc/metadata_internal.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <cstring>
2424
#include <memory>
2525
#include <string>
26+
#include <string_view>
2627
#include <utility>
2728
#include <vector>
2829

@@ -78,7 +79,7 @@ flatbuf::MetadataVersion MetadataVersionToFlatbuffer(MetadataVersion version);
7879
// Whether the type has a validity bitmap in the given IPC version
7980
bool HasValidityBitmap(Type::type type_id, MetadataVersion version);
8081

81-
static constexpr const char* kArrowMagicBytes = "ARROW1";
82+
constexpr const std::string_view kArrowMagicBytes = "ARROW1";
8283

8384
struct FieldMetadata {
8485
int64_t length;

cpp/src/arrow/ipc/read_write_test.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3045,7 +3045,7 @@ void GetReadRecordBatchReadRanges(
30453045

30463046
auto read_ranges = tracked->get_read_ranges();
30473047

3048-
const int32_t magic_size = static_cast<int>(strlen(ipc::internal::kArrowMagicBytes));
3048+
const int32_t magic_size = static_cast<int>(ipc::internal::kArrowMagicBytes.size());
30493049
// read magic and footer length IO
30503050
auto file_end_size = magic_size + sizeof(int32_t);
30513051
auto footer_length_offset = buffer->size() - file_end_size;

cpp/src/arrow/ipc/reader.cc

Lines changed: 85 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
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+
26922717
IpcReadOptions 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;

cpp/src/arrow/ipc/writer.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,7 +1493,7 @@ class PayloadFileWriter : public internal::IpcPayloadWriter, protected StreamBoo
14931493
RETURN_NOT_OK(UpdatePosition());
14941494

14951495
// It is only necessary to align to 8-byte boundary at the start of the file
1496-
RETURN_NOT_OK(Write(kArrowMagicBytes, strlen(kArrowMagicBytes)));
1496+
RETURN_NOT_OK(Write(kArrowMagicBytes.data(), kArrowMagicBytes.size()));
14971497
RETURN_NOT_OK(Align());
14981498

14991499
return Status::OK();
@@ -1521,7 +1521,7 @@ class PayloadFileWriter : public internal::IpcPayloadWriter, protected StreamBoo
15211521
RETURN_NOT_OK(Write(&footer_length, sizeof(int32_t)));
15221522

15231523
// Write magic bytes to end file
1524-
return Write(kArrowMagicBytes, strlen(kArrowMagicBytes));
1524+
return Write(kArrowMagicBytes.data(), kArrowMagicBytes.size());
15251525
}
15261526

15271527
protected:

0 commit comments

Comments
 (0)