Skip to content

Commit a40538f

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 a40538f

10 files changed

Lines changed: 482 additions & 114 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: 109 additions & 100 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());
@@ -315,6 +319,84 @@ Status ReadFieldsSubset(int64_t offset, int32_t metadata_length,
315319
return Status::OK();
316320
}
317321

322+
struct ReadMessageState {
323+
std::unique_ptr<Message> result;
324+
std::shared_ptr<MessageDecoderListener> listener;
325+
std::shared_ptr<MessageDecoder> decoder;
326+
327+
ReadMessageState()
328+
: listener(std::make_shared<AssignMessageDecoderListener>(&result)),
329+
decoder(std::make_shared<MessageDecoder>(listener)) {}
330+
331+
// ReadMessageState points into itself, so it shouldn't be moved
332+
ReadMessageState(ReadMessageState&&) = delete;
333+
ReadMessageState& operator=(ReadMessageState&&) = delete;
334+
};
335+
336+
// A common continuation callback for ReadMessage and ReadMessageAsync overloads
337+
static Result<std::unique_ptr<Message>> ReadMessageContinued(
338+
int64_t offset, int32_t metadata_length, std::optional<int64_t> body_length,
339+
std::shared_ptr<Buffer> metadata, io::RandomAccessFile* file,
340+
const FieldsLoaderFunction& fields_loader, ReadMessageState* state) {
341+
MessageDecoder* decoder = state->decoder.get();
342+
if (body_length.has_value()) {
343+
// If body length was known, the given buffer should contain exactly the metadata
344+
// followed by the body.
345+
DCHECK_EQ(metadata->size(), metadata_length + *body_length);
346+
}
347+
ARROW_RETURN_NOT_OK(decoder->Consume(SliceBuffer(metadata, 0, metadata_length)));
348+
if (decoder->buffered_size() > 0) {
349+
return Status::Invalid("Message metadata too long by ", decoder->buffered_size(),
350+
" bytes");
351+
}
352+
switch (decoder->state()) {
353+
case MessageDecoder::State::INITIAL:
354+
return std::move(state->result);
355+
case MessageDecoder::State::METADATA_LENGTH:
356+
return Status::Invalid("metadata length is missing. File offset: ", offset,
357+
", metadata length: ", metadata_length);
358+
case MessageDecoder::State::METADATA:
359+
return Status::Invalid("flatbuffer size ", decoder->next_required_size(),
360+
" invalid. File offset: ", offset,
361+
", metadata length: ", metadata_length);
362+
case MessageDecoder::State::BODY: {
363+
std::shared_ptr<Buffer> body;
364+
if (fields_loader) {
365+
// Selective field loading: allocate a body buffer and read only the
366+
// requested field ranges into it.
367+
ARROW_ASSIGN_OR_RAISE(
368+
body, AllocateBuffer(decoder->next_required_size(), default_memory_pool()));
369+
RETURN_NOT_OK(ReadFieldsSubset(offset, file, fields_loader,
370+
SliceBuffer(metadata, 0, metadata_length),
371+
decoder->next_required_size(), body));
372+
} else if (body_length.has_value()) {
373+
// Body was already read as part of the combined IO; just slice it out.
374+
if (*body_length != decoder->next_required_size()) {
375+
// The streaming decoder got out of sync with the actual advertised
376+
// metadata and body size, which signals an invalid IPC file.
377+
return Status::IOError("Invalid IPC file: advertised body size is ",
378+
*body_length, ", but message decoder expects to read ",
379+
decoder->next_required_size(), " bytes instead");
380+
}
381+
body = SliceBuffer(metadata, metadata_length,
382+
std::min(*body_length, metadata->size() - metadata_length));
383+
} else {
384+
// Body length was unknown; do a separate IO to read the body.
385+
ARROW_ASSIGN_OR_RAISE(
386+
body, file->ReadAt(offset + metadata_length, decoder->next_required_size(),
387+
/*allow_short_read=*/false));
388+
}
389+
390+
RETURN_NOT_OK(decoder->Consume(body));
391+
return std::move(state->result);
392+
}
393+
case MessageDecoder::State::EOS:
394+
return Status::Invalid("Unexpected empty message in IPC file format");
395+
default:
396+
return Status::Invalid("Unexpected state: ", state->decoder->state());
397+
}
398+
}
399+
318400
} // namespace
319401

320402
Result<std::unique_ptr<Message>> ReadMessage(std::shared_ptr<Buffer> metadata,
@@ -330,6 +412,10 @@ Result<std::unique_ptr<Message>> ReadMessage(std::shared_ptr<Buffer> metadata,
330412
}
331413

332414
ARROW_RETURN_NOT_OK(decoder.Consume(metadata));
415+
if (decoder.buffered_size() > 0) {
416+
return Status::Invalid("Message metadata too long by ", decoder.buffered_size(),
417+
" bytes");
418+
}
333419

334420
switch (decoder.state()) {
335421
case MessageDecoder::State::INITIAL:
@@ -368,69 +454,20 @@ Result<std::unique_ptr<Message>> ReadMessage(std::shared_ptr<Buffer> metadata,
368454
static Result<std::unique_ptr<Message>> ReadMessageInternal(
369455
int64_t offset, int32_t metadata_length, std::optional<int64_t> body_length,
370456
io::RandomAccessFile* file, const FieldsLoaderFunction& fields_loader) {
371-
std::unique_ptr<Message> result;
372-
auto listener = std::make_shared<AssignMessageDecoderListener>(&result);
373-
MessageDecoder decoder(listener);
457+
ReadMessageState state;
374458

375-
if (metadata_length < decoder.next_required_size()) {
459+
if (metadata_length < state.decoder->next_required_size()) {
376460
return Status::Invalid("metadata_length should be at least ",
377-
decoder.next_required_size());
461+
state.decoder->next_required_size());
378462
}
379-
380463
// When body_length is known, read metadata + body in one IO call.
381464
// Otherwise, read only metadata first.
382465
ARROW_ASSIGN_OR_RAISE(std::shared_ptr<Buffer> metadata,
383466
file->ReadAt(offset, metadata_length + body_length.value_or(0),
384467
/*allow_short_read=*/false));
385468

386-
ARROW_RETURN_NOT_OK(decoder.Consume(SliceBuffer(metadata, 0, metadata_length)));
387-
388-
switch (decoder.state()) {
389-
case MessageDecoder::State::INITIAL:
390-
return result;
391-
case MessageDecoder::State::METADATA_LENGTH:
392-
return Status::Invalid("metadata length is missing. File offset: ", offset,
393-
", metadata length: ", metadata_length);
394-
case MessageDecoder::State::METADATA:
395-
return Status::Invalid("flatbuffer size ", decoder.next_required_size(),
396-
" invalid. File offset: ", offset,
397-
", metadata length: ", metadata_length);
398-
case MessageDecoder::State::BODY: {
399-
std::shared_ptr<Buffer> body;
400-
if (fields_loader) {
401-
// Selective field loading: allocate a body buffer and read only the
402-
// requested field ranges into it.
403-
ARROW_ASSIGN_OR_RAISE(
404-
body, AllocateBuffer(decoder.next_required_size(), default_memory_pool()));
405-
RETURN_NOT_OK(ReadFieldsSubset(offset, metadata_length, file, fields_loader,
406-
SliceBuffer(metadata, 0, metadata_length),
407-
decoder.next_required_size(), body));
408-
} else if (body_length.has_value()) {
409-
// Body was already read as part of the combined IO; just slice it out.
410-
if (*body_length != decoder.next_required_size()) {
411-
// The streaming decoder got out of sync with the actual advertised
412-
// metadata and body size, which signals an invalid IPC file.
413-
return Status::IOError("Invalid IPC file: advertised body size is ",
414-
*body_length, ", but message decoder expects to read ",
415-
decoder.next_required_size(), " bytes instead");
416-
}
417-
body = SliceBuffer(metadata, metadata_length,
418-
std::min(*body_length, metadata->size() - metadata_length));
419-
} else {
420-
// Body length was unknown; do a separate IO to read the body.
421-
ARROW_ASSIGN_OR_RAISE(
422-
body, file->ReadAt(offset + metadata_length, decoder.next_required_size(),
423-
/*allow_short_read=*/false));
424-
}
425-
426-
RETURN_NOT_OK(decoder.Consume(body));
427-
return result;
428-
}
429-
case MessageDecoder::State::EOS:
430-
return Status::Invalid("Unexpected empty message in IPC file format");
431-
default:
432-
return Status::Invalid("Unexpected state: ", decoder.state());
433-
}
469+
return ReadMessageContinued(offset, metadata_length, body_length, metadata, file,
470+
fields_loader, &state);
434471
}
435472

436473
Result<std::unique_ptr<Message>> ReadMessage(int64_t offset, int32_t metadata_length,
@@ -452,14 +489,9 @@ Future<std::shared_ptr<Message>> ReadMessageAsync(int64_t offset, int32_t metada
452489
int64_t body_length,
453490
io::RandomAccessFile* file,
454491
const io::IOContext& context) {
455-
struct State {
456-
std::unique_ptr<Message> result;
457-
std::shared_ptr<MessageDecoderListener> listener;
458-
std::shared_ptr<MessageDecoder> decoder;
459-
};
460-
auto state = std::make_shared<State>();
461-
state->listener = std::make_shared<AssignMessageDecoderListener>(&state->result);
462-
state->decoder = std::make_shared<MessageDecoder>(state->listener);
492+
// Make a std::shared_ptr so as to have a stable ReadMessageState pointer,
493+
// since state->listener will point back to state->result.
494+
auto state = std::make_shared<ReadMessageState>();
463495

464496
if (metadata_length < state->decoder->next_required_size()) {
465497
return Status::Invalid("metadata_length should be at least ",
@@ -469,35 +501,8 @@ Future<std::shared_ptr<Message>> ReadMessageAsync(int64_t offset, int32_t metada
469501
->ReadAsync(context, offset, metadata_length + body_length,
470502
/*allow_short_read=*/false)
471503
.Then([=](std::shared_ptr<Buffer> metadata) -> Result<std::shared_ptr<Message>> {
472-
DCHECK_EQ(metadata->size(), metadata_length + body_length);
473-
ARROW_RETURN_NOT_OK(
474-
state->decoder->Consume(SliceBuffer(metadata, 0, metadata_length)));
475-
switch (state->decoder->state()) {
476-
case MessageDecoder::State::INITIAL:
477-
return std::move(state->result);
478-
case MessageDecoder::State::METADATA_LENGTH:
479-
return Status::Invalid("metadata length is missing. File offset: ", offset,
480-
", metadata length: ", metadata_length);
481-
case MessageDecoder::State::METADATA:
482-
return Status::Invalid("flatbuffer size ",
483-
state->decoder->next_required_size(),
484-
" invalid. File offset: ", offset,
485-
", metadata length: ", metadata_length);
486-
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());
492-
}
493-
RETURN_NOT_OK(state->decoder->Consume(body));
494-
return std::move(state->result);
495-
}
496-
case MessageDecoder::State::EOS:
497-
return Status::Invalid("Unexpected empty message in IPC file format");
498-
default:
499-
return Status::Invalid("Unexpected state: ", state->decoder->state());
500-
}
504+
return ReadMessageContinued(offset, metadata_length, body_length, metadata, file,
505+
/*fields_loader=*/{}, state.get());
501506
});
502507
}
503508

@@ -755,6 +760,8 @@ class MessageDecoder::MessageDecoderImpl {
755760

756761
int64_t next_required_size() const { return next_required_size_ - buffered_size_; }
757762

763+
int64_t buffered_size() const { return buffered_size_; }
764+
758765
MessageDecoder::State state() const { return state_; }
759766

760767
private:
@@ -1020,6 +1027,8 @@ Status MessageDecoder::Consume(std::shared_ptr<Buffer> buffer) {
10201027

10211028
int64_t MessageDecoder::next_required_size() const { return impl_->next_required_size(); }
10221029

1030+
int64_t MessageDecoder::buffered_size() const { return impl_->buffered_size(); }
1031+
10231032
MessageDecoder::State MessageDecoder::state() const { return impl_->state(); }
10241033

10251034
// ----------------------------------------------------------------------

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)