Skip to content

Commit d9884c5

Browse files
committed
EXP: [C++] Make IPC message decoding stricter
Error out when the advertised message metadata length is larger than expected.
1 parent f486053 commit d9884c5

9 files changed

Lines changed: 403 additions & 28 deletions

File tree

cpp/src/arrow/buffer.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Result<std::shared_ptr<Buffer>> SliceMutableBufferSafe(std::shared_ptr<Buffer> b
8787
return SliceMutableBuffer(std::move(buffer), offset, length);
8888
}
8989

90-
std::string Buffer::ToHexString() {
90+
std::string Buffer::ToHexString() const {
9191
return HexEncode(data(), static_cast<size_t>(size()));
9292
}
9393

cpp/src/arrow/buffer.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class ARROW_EXPORT Buffer {
121121

122122
/// \brief Construct a new std::string with a hexadecimal representation of the buffer.
123123
/// \return std::string
124-
std::string ToHexString();
124+
std::string ToHexString() const;
125125

126126
/// Return true if both buffers are the same size and contain the same bytes
127127
/// up to the number of compared bytes

cpp/src/arrow/ipc/message.cc

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -285,17 +285,21 @@ std::string FormatMessageType(MessageType type) {
285285

286286
namespace {
287287

288-
Status ReadFieldsSubset(int64_t offset, int32_t metadata_length,
289-
io::RandomAccessFile* file,
288+
Status ReadFieldsSubset(int64_t offset, io::RandomAccessFile* file,
290289
const FieldsLoaderFunction& fields_loader,
291290
const std::shared_ptr<Buffer>& metadata, int64_t required_size,
292291
std::shared_ptr<Buffer>& body) {
292+
DCHECK_GE(static_cast<size_t>(metadata->size()), sizeof(int32_t));
293+
const auto continuation = util::SafeLoadAs<int32_t>(metadata->data());
294+
// Either 8 bytes (32-bit continuation indicator + 32-bit little-endian length prefix)
295+
// or 4 bytes for legacy IPC without continuation indicator
296+
const auto continuation_size = (continuation == internal::kIpcContinuationToken)
297+
? 2 * sizeof(int32_t)
298+
: sizeof(int32_t);
299+
293300
const flatbuf::Message* message = nullptr;
294-
uint8_t continuation_metadata_size = sizeof(int32_t) + sizeof(int32_t);
295-
// skip 8 bytes (32-bit continuation indicator + 32-bit little-endian length prefix)
296-
RETURN_NOT_OK(internal::VerifyMessage(metadata->data() + continuation_metadata_size,
297-
metadata->size() - continuation_metadata_size,
298-
&message));
301+
RETURN_NOT_OK(internal::VerifyMessage(metadata->data() + continuation_size,
302+
metadata->size() - continuation_size, &message));
299303
auto batch = message->header_as_RecordBatch();
300304
if (batch == nullptr) {
301305
return Status::IOError(
@@ -305,8 +309,8 @@ Status ReadFieldsSubset(int64_t offset, int32_t metadata_length,
305309
RETURN_NOT_OK(fields_loader(batch, &io_recorded_random_access_file));
306310
const auto& read_ranges = io_recorded_random_access_file.GetReadRanges();
307311
for (const auto& range : read_ranges) {
308-
auto read_result = file->ReadAt(offset + metadata_length + range.offset, range.length,
309-
body->mutable_data() + range.offset);
312+
auto read_result = file->ReadAt(offset + metadata->size() + range.offset,
313+
range.length, body->mutable_data() + range.offset);
310314
if (!read_result.ok()) {
311315
return Status::IOError("Failed to read message body, error ",
312316
read_result.status().ToString());
@@ -330,6 +334,10 @@ Result<std::unique_ptr<Message>> ReadMessage(std::shared_ptr<Buffer> metadata,
330334
}
331335

332336
ARROW_RETURN_NOT_OK(decoder.Consume(metadata));
337+
if (decoder.buffered_size() > 0) {
338+
return Status::Invalid("Message metadata too long by ", decoder.buffered_size(),
339+
" bytes");
340+
}
333341

334342
switch (decoder.state()) {
335343
case MessageDecoder::State::INITIAL:
@@ -384,6 +392,10 @@ static Result<std::unique_ptr<Message>> ReadMessageInternal(
384392
/*allow_short_read=*/false));
385393

386394
ARROW_RETURN_NOT_OK(decoder.Consume(SliceBuffer(metadata, 0, metadata_length)));
395+
if (decoder.buffered_size() > 0) {
396+
return Status::Invalid("Message metadata too long by ", decoder.buffered_size(),
397+
" bytes");
398+
}
387399

388400
switch (decoder.state()) {
389401
case MessageDecoder::State::INITIAL:
@@ -402,7 +414,7 @@ static Result<std::unique_ptr<Message>> ReadMessageInternal(
402414
// requested field ranges into it.
403415
ARROW_ASSIGN_OR_RAISE(
404416
body, AllocateBuffer(decoder.next_required_size(), default_memory_pool()));
405-
RETURN_NOT_OK(ReadFieldsSubset(offset, metadata_length, file, fields_loader,
417+
RETURN_NOT_OK(ReadFieldsSubset(offset, file, fields_loader,
406418
SliceBuffer(metadata, 0, metadata_length),
407419
decoder.next_required_size(), body));
408420
} else if (body_length.has_value()) {
@@ -469,9 +481,14 @@ Future<std::shared_ptr<Message>> ReadMessageAsync(int64_t offset, int32_t metada
469481
->ReadAsync(context, offset, metadata_length + body_length,
470482
/*allow_short_read=*/false)
471483
.Then([=](std::shared_ptr<Buffer> metadata) -> Result<std::shared_ptr<Message>> {
484+
// TODO reconcile + factor out with blocking ReadMessage logic
472485
DCHECK_EQ(metadata->size(), metadata_length + body_length);
473486
ARROW_RETURN_NOT_OK(
474487
state->decoder->Consume(SliceBuffer(metadata, 0, metadata_length)));
488+
if (state->decoder->buffered_size() > 0) {
489+
return Status::Invalid("Message metadata too long by ",
490+
state->decoder->buffered_size(), " bytes");
491+
}
475492
switch (state->decoder->state()) {
476493
case MessageDecoder::State::INITIAL:
477494
return std::move(state->result);
@@ -484,12 +501,15 @@ Future<std::shared_ptr<Message>> ReadMessageAsync(int64_t offset, int32_t metada
484501
" invalid. File offset: ", offset,
485502
", metadata length: ", metadata_length);
486503
case MessageDecoder::State::BODY: {
487-
auto body = SliceBuffer(metadata, metadata_length, body_length);
488-
if (body->size() < state->decoder->next_required_size()) {
489-
return Status::IOError("Expected to be able to read ",
490-
state->decoder->next_required_size(),
491-
" bytes for message body, got ", body->size());
504+
if (body_length != state->decoder->next_required_size()) {
505+
// The streaming decoder got out of sync with the actual advertised
506+
// metadata and body size, which signals an invalid IPC file.
507+
return Status::IOError(
508+
"Invalid IPC file: advertised body size is ", body_length,
509+
", but message decoder expects to read ",
510+
state->decoder->next_required_size(), " bytes instead");
492511
}
512+
auto body = SliceBuffer(metadata, metadata_length, body_length);
493513
RETURN_NOT_OK(state->decoder->Consume(body));
494514
return std::move(state->result);
495515
}
@@ -755,6 +775,8 @@ class MessageDecoder::MessageDecoderImpl {
755775

756776
int64_t next_required_size() const { return next_required_size_ - buffered_size_; }
757777

778+
int64_t buffered_size() const { return buffered_size_; }
779+
758780
MessageDecoder::State state() const { return state_; }
759781

760782
private:
@@ -1020,6 +1042,8 @@ Status MessageDecoder::Consume(std::shared_ptr<Buffer> buffer) {
10201042

10211043
int64_t MessageDecoder::next_required_size() const { return impl_->next_required_size(); }
10221044

1045+
int64_t MessageDecoder::buffered_size() const { return impl_->buffered_size(); }
1046+
10231047
MessageDecoder::State MessageDecoder::state() const { return impl_->state(); }
10241048

10251049
// ----------------------------------------------------------------------

cpp/src/arrow/ipc/message.h

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,11 @@ class ARROW_EXPORT MessageDecoder {
419419
/// \return the current state
420420
State state() const;
421421

422+
/// \brief Return the number of bytes buffered in the decoder.
423+
///
424+
/// This method is mainly useful for testing and debugging.
425+
int64_t buffered_size() const;
426+
422427
private:
423428
class MessageDecoderImpl;
424429
std::unique_ptr<MessageDecoderImpl> impl_;
@@ -453,9 +458,12 @@ using FieldsLoaderFunction = std::function<Status(const void*, io::RandomAccessF
453458
///
454459
/// Read a length-prefixed message flatbuffer starting at the indicated file
455460
/// offset. If the message has a body with non-zero length, it will also be
456-
/// read
461+
/// read.
457462
///
458-
/// The metadata_length includes at least the length prefix and the flatbuffer
463+
/// The metadata_length includes the IPC encapsulation prefix and the
464+
/// Flatbuffers-serialized message.
465+
///
466+
/// This function should only be used when a RecordBatch message is expected.
459467
///
460468
/// \param[in] offset the position in the file where the message starts. The
461469
/// first 4 bytes after the offset are the message length
@@ -474,7 +482,8 @@ Result<std::unique_ptr<Message>> ReadMessage(
474482
/// Read a length-prefixed message flatbuffer starting at the indicated file
475483
/// offset.
476484
///
477-
/// The metadata_length includes at least the length prefix and the flatbuffer
485+
/// The metadata_length includes the IPC encapsulation prefix and the
486+
/// Flatbuffers-serialized message.
478487
///
479488
/// \param[in] offset the position in the file where the message starts. The
480489
/// first 4 bytes after the offset are the message length
@@ -578,6 +587,7 @@ Status DecodeMessage(MessageDecoder* decoder, io::InputStream* stream);
578587
/// \param[out] message_length the total size of the payload written including
579588
/// padding
580589
/// \return Status
590+
ARROW_EXPORT
581591
Status WriteMessage(const Buffer& message, const IpcWriteOptions& options,
582592
io::OutputStream* file, int32_t* message_length);
583593

0 commit comments

Comments
 (0)