github-actions[bot] commented on code in PR #66820:
URL: https://github.com/apache/doris/pull/66820#discussion_r3793721406
##########
be/src/format/parquet/parquet_predicate.h:
##########
@@ -441,44 +441,76 @@ class ParquetPredicate {
static Status read_bloom_filter(const tparquet::ColumnMetaData&
column_meta_data,
io::FileReaderSPtr file_reader,
io::IOContext* io_ctx,
ColumnStat* ans_stat) {
- size_t size;
if (!column_meta_data.__isset.bloom_filter_offset) {
return Status::NotSupported("Can not use this parquet bloom
filter.");
}
+ if (column_meta_data.bloom_filter_offset < 0 ||
+ (column_meta_data.__isset.bloom_filter_length &&
+ column_meta_data.bloom_filter_length <= 0)) {
+ return Status::Corruption("Invalid Parquet bloom filter offset or
declared length");
+ }
- if (column_meta_data.__isset.bloom_filter_length &&
- column_meta_data.bloom_filter_length > 0) {
- size = column_meta_data.bloom_filter_length;
- } else {
- size = BLOOM_FILTER_MAX_HEADER_LENGTH;
+ const uint64_t bloom_offset =
static_cast<uint64_t>(column_meta_data.bloom_filter_offset);
+ if (bloom_offset >= file_reader->size()) {
+ return Status::Corruption("Parquet bloom filter offset exceeds
file size");
}
+ const size_t available = file_reader->size() - bloom_offset;
+ const size_t declared_available =
+ column_meta_data.__isset.bloom_filter_length
+ ?
std::min<size_t>(column_meta_data.bloom_filter_length, available)
+ : available;
+ const size_t header_read_size =
+ std::min<size_t>(declared_available,
BLOOM_FILTER_MAX_HEADER_LENGTH);
size_t bytes_read = 0;
- std::vector<uint8_t> header_buffer(size);
+ std::vector<uint8_t> header_buffer(header_read_size);
RETURN_IF_ERROR(file_reader->read_at(column_meta_data.bloom_filter_offset,
- Slice(header_buffer.data(),
size), &bytes_read,
- io_ctx));
+ Slice(header_buffer.data(),
header_buffer.size()),
+ &bytes_read, io_ctx));
tparquet::BloomFilterHeader t_bloom_filter_header;
uint32_t t_bloom_filter_header_size =
static_cast<uint32_t>(bytes_read);
- RETURN_IF_ERROR(deserialize_thrift_msg(header_buffer.data(),
&t_bloom_filter_header_size,
- true, &t_bloom_filter_header));
+ if (!deserialize_thrift_msg(header_buffer.data(),
&t_bloom_filter_header_size, true,
+ &t_bloom_filter_header)
+ .ok()) {
+ return Status::Corruption("Malformed Parquet bloom filter header");
+ }
// TODO the bloom filter could be encrypted, too, so need to double
check that this is NOT the case
if (!t_bloom_filter_header.algorithm.__isset.BLOCK ||
!t_bloom_filter_header.compression.__isset.UNCOMPRESSED ||
- !t_bloom_filter_header.hash.__isset.XXHASH) {
+ !t_bloom_filter_header.hash.__isset.XXHASH ||
t_bloom_filter_header.numBytes <= 0) {
return Status::NotSupported("Can not use this parquet bloom
filter.");
}
- ans_stat->bloom_filter =
std::make_unique<ParquetBlockSplitBloomFilter>();
+ const int64_t payload_size = t_bloom_filter_header.numBytes;
+ if (payload_size < segment_v2::BloomFilter::MINIMUM_BYTES ||
+ payload_size > segment_v2::BloomFilter::MAXIMUM_BYTES ||
payload_size % 32 != 0) {
+ return Status::Corruption("Invalid Parquet bloom filter payload
size {}", payload_size);
+ }
+ const uint64_t total_size =
+ static_cast<uint64_t>(t_bloom_filter_header_size) +
payload_size;
+ if (total_size > available) {
+ return Status::Corruption("Parquet bloom filter range exceeds file
size");
+ }
+ if (column_meta_data.__isset.bloom_filter_length &&
+ (static_cast<uint64_t>(column_meta_data.bloom_filter_length) <
total_size ||
+ static_cast<uint64_t>(column_meta_data.bloom_filter_length) >
available)) {
+ return Status::Corruption("Invalid Parquet bloom filter declared
length");
+ }
- std::vector<uint8_t> data_buffer(t_bloom_filter_header.numBytes);
+ // Validate the full split-block layout before allocating or adding
metadata-controlled
+ // offsets; the Bloom filter implementation assumes complete 32-byte
blocks.
+ std::vector<uint8_t> data_buffer(static_cast<size_t>(payload_size));
Review Comment:
[P1] Keep maximum-size Bloom filters inside the task memory budget
A valid `numBytes == MAXIMUM_BYTES` creates a 128 MiB `std::vector` here,
then `ParquetBlockSplitBloomFilter::init()` deep-copies it into another `new
char[128 MiB]`. Neither allocation uses Doris's allocator or `try_reserve()`,
and `_process_column_stat_filter()` can retain one 128 MiB filter per distinct
predicate column; several parallel scanners can therefore exceed the
query/workload-group limit and OOM the BE even though every individual filter
passes this new cap. Please reserve and allocator-track both temporary and
retained bytes (preferably transfer one tracked buffer instead of copying it),
and bound or avoid aggregate retention of very large filters. The format_v2
path has the same allocation pattern.
##########
be/src/util/thrift_util.h:
##########
@@ -134,15 +137,20 @@ Status deserialize_thrift_msg(const uint8_t* buf,
uint32_t* len, bool compact,
// transport. TMemoryBuffer is not const-safe, although we use it in
// a const-safe way, so we have to explicitly cast away the const.
auto conf = std::make_shared<apache::thrift::TConfiguration>();
- // On Thrift 0.14.0+, need use TConfiguration to raise the max message
size.
- // max message size is 100MB default, so make it unlimited.
- conf->setMaxMessageSize(std::numeric_limits<int>::max());
+ const int32_t size_limit =
+ *len == 0 ? 1
+ : static_cast<int32_t>(std::min<uint32_t>(
+ *len,
static_cast<uint32_t>(std::numeric_limits<int32_t>::max())));
Review Comment:
[P1] Bound decoded container allocation, not only element count
`container_limit = size_limit` still lets a modest input allocate far more
than its wire size. Thrift 0.16 generated list readers call `resize(count)`
immediately after `readListBegin()`, while compact and binary preflight treat
`T_STRUCT` as zero minimum wire bytes. Under the existing 100 MiB v2 footer
cap, metadata can declare nearly 100 million `SchemaElement`s (`count <= len`),
pass this guard, and resize the decoded vector to multiple GiB on the current
ABI before reading or validating the first element; padding makes the transport
window large enough, and failure afterward is too late. Please enforce a
decoded or reservation-aware container budget before generated resize, and add
compact and binary regressions where the count is within the input length but
decoded storage exceeds the allowed budget.
##########
be/src/format/parquet/parquet_predicate.h:
##########
@@ -441,44 +441,76 @@ class ParquetPredicate {
static Status read_bloom_filter(const tparquet::ColumnMetaData&
column_meta_data,
io::FileReaderSPtr file_reader,
io::IOContext* io_ctx,
ColumnStat* ans_stat) {
- size_t size;
if (!column_meta_data.__isset.bloom_filter_offset) {
return Status::NotSupported("Can not use this parquet bloom
filter.");
}
+ if (column_meta_data.bloom_filter_offset < 0 ||
+ (column_meta_data.__isset.bloom_filter_length &&
+ column_meta_data.bloom_filter_length <= 0)) {
+ return Status::Corruption("Invalid Parquet bloom filter offset or
declared length");
+ }
- if (column_meta_data.__isset.bloom_filter_length &&
- column_meta_data.bloom_filter_length > 0) {
- size = column_meta_data.bloom_filter_length;
- } else {
- size = BLOOM_FILTER_MAX_HEADER_LENGTH;
+ const uint64_t bloom_offset =
static_cast<uint64_t>(column_meta_data.bloom_filter_offset);
+ if (bloom_offset >= file_reader->size()) {
+ return Status::Corruption("Parquet bloom filter offset exceeds
file size");
}
+ const size_t available = file_reader->size() - bloom_offset;
+ const size_t declared_available =
+ column_meta_data.__isset.bloom_filter_length
+ ?
std::min<size_t>(column_meta_data.bloom_filter_length, available)
+ : available;
+ const size_t header_read_size =
+ std::min<size_t>(declared_available,
BLOOM_FILTER_MAX_HEADER_LENGTH);
size_t bytes_read = 0;
- std::vector<uint8_t> header_buffer(size);
+ std::vector<uint8_t> header_buffer(header_read_size);
RETURN_IF_ERROR(file_reader->read_at(column_meta_data.bloom_filter_offset,
- Slice(header_buffer.data(),
size), &bytes_read,
- io_ctx));
+ Slice(header_buffer.data(),
header_buffer.size()),
+ &bytes_read, io_ctx));
tparquet::BloomFilterHeader t_bloom_filter_header;
uint32_t t_bloom_filter_header_size =
static_cast<uint32_t>(bytes_read);
- RETURN_IF_ERROR(deserialize_thrift_msg(header_buffer.data(),
&t_bloom_filter_header_size,
- true, &t_bloom_filter_header));
+ if (!deserialize_thrift_msg(header_buffer.data(),
&t_bloom_filter_header_size, true,
+ &t_bloom_filter_header)
+ .ok()) {
+ return Status::Corruption("Malformed Parquet bloom filter header");
+ }
// TODO the bloom filter could be encrypted, too, so need to double
check that this is NOT the case
if (!t_bloom_filter_header.algorithm.__isset.BLOCK ||
!t_bloom_filter_header.compression.__isset.UNCOMPRESSED ||
- !t_bloom_filter_header.hash.__isset.XXHASH) {
+ !t_bloom_filter_header.hash.__isset.XXHASH ||
t_bloom_filter_header.numBytes <= 0) {
return Status::NotSupported("Can not use this parquet bloom
filter.");
}
- ans_stat->bloom_filter =
std::make_unique<ParquetBlockSplitBloomFilter>();
+ const int64_t payload_size = t_bloom_filter_header.numBytes;
+ if (payload_size < segment_v2::BloomFilter::MINIMUM_BYTES ||
+ payload_size > segment_v2::BloomFilter::MAXIMUM_BYTES ||
payload_size % 32 != 0) {
+ return Status::Corruption("Invalid Parquet bloom filter payload
size {}", payload_size);
+ }
+ const uint64_t total_size =
+ static_cast<uint64_t>(t_bloom_filter_header_size) +
payload_size;
+ if (total_size > available) {
+ return Status::Corruption("Parquet bloom filter range exceeds file
size");
+ }
+ if (column_meta_data.__isset.bloom_filter_length &&
+ (static_cast<uint64_t>(column_meta_data.bloom_filter_length) <
total_size ||
Review Comment:
[P1] Require the declared Bloom length to match the header
`bloom_filter_length` is the exact size of the serialized header plus
bitset, not just an upper bound. For example, metadata can declare `header_size
+ 64` while the header says `numBytes = 32`; this condition accepts it, reads
only the first block, and initializes a one-block filter. If the payload was
built as two blocks and a present value landed in block 1, `test_hash()`
recomputes its bucket for one block, sees the empty first block, and can
falsely prune the row group. Please reject any present declared length that is
not exactly `header_size + numBytes`, apply the same fix to
`validate_native_bloom_filter_layout()`, and cover the contradictory layout end
to end.
##########
be/test/format/parquet/parquet_thrift_test.cpp:
##########
@@ -71,6 +72,19 @@ class ParquetThriftReaderTest : public testing::Test {
void TearDown() override { TimezoneUtils::clear_timezone_caches(); }
};
Review Comment:
[P2] Cover the other newly bounded Thrift paths
This only exercises a compact-protocol container length, but the production
change separately constructs the binary protocol and enables both
`string_limit` and `container_limit` in both branches. A regression that drops
or misorders the binary limit, or either string limit, would therefore leave
this test green even though network-facing binary callers or Parquet string
fields could again allocate from a hostile length. Please add focused malformed
string/container cases for `compact=true` and `compact=false`, together with a
valid control, so every newly enabled allocation guard is covered.
##########
be/test/format/parquet/parquet_statistics_test.cpp:
##########
@@ -17,16 +17,111 @@
#include <gtest/gtest.h>
+#include <algorithm>
+#include <cstring>
+#include <memory>
#include <regex>
+#include <vector>
#include "format/parquet/parquet_predicate.h"
+#include "util/thrift_util.h"
namespace doris {
+namespace {
+
+class BloomFilterFileReader final : public io::FileReader {
+public:
+ explicit BloomFilterFileReader(std::vector<uint8_t> data) :
_data(std::move(data)) {}
+
+ Status close() override {
+ _closed = true;
+ return Status::OK();
+ }
+
+ const io::Path& path() const override { return _path; }
+ size_t size() const override { return _data.size(); }
+ bool closed() const override { return _closed; }
+ int64_t mtime() const override { return 0; }
+
+protected:
+ Status read_at_impl(size_t offset, Slice result, size_t* bytes_read,
+ const io::IOContext* io_ctx) override {
+ if (offset > _data.size()) {
+ return Status::IOError("Out of bounds");
+ }
+ *bytes_read = std::min(result.size, _data.size() - offset);
+ memcpy(result.data, _data.data() + offset, *bytes_read);
+ return Status::OK();
+ }
+
+private:
+ std::vector<uint8_t> _data;
+ io::Path _path = "parquet_bloom_filter_test";
+ bool _closed = false;
+};
+
+Status read_test_bloom_filter(int32_t header_payload_size, size_t
actual_payload_size,
+ int32_t declared_length_adjustment = 0) {
+ tparquet::BloomFilterAlgorithm algorithm;
+ algorithm.__set_BLOCK(tparquet::SplitBlockAlgorithm());
+ tparquet::BloomFilterHash hash;
+ hash.__set_XXHASH(tparquet::XxHash());
+ tparquet::BloomFilterCompression compression;
+ compression.__set_UNCOMPRESSED(tparquet::Uncompressed());
+ tparquet::BloomFilterHeader header;
+ header.__set_numBytes(header_payload_size);
+ header.__set_algorithm(algorithm);
+ header.__set_hash(hash);
+ header.__set_compression(compression);
+
+ std::vector<uint8_t> file_bytes;
+ ThriftSerializer serializer(/*compact=*/true, /*initial_buffer_size=*/64);
+ RETURN_IF_ERROR(serializer.serialize(&header, &file_bytes));
+ const size_t header_size = file_bytes.size();
+ file_bytes.resize(header_size + actual_payload_size);
+
+ tparquet::ColumnMetaData metadata;
+ metadata.__set_bloom_filter_offset(0);
+ metadata.__set_bloom_filter_length(static_cast<int32_t>(file_bytes.size())
+
+ declared_length_adjustment);
+ auto reader =
std::make_shared<BloomFilterFileReader>(std::move(file_bytes));
+ ParquetPredicate::ColumnStat stat;
+ return ParquetPredicate::read_bloom_filter(metadata, reader, nullptr,
&stat);
+}
+
+} // namespace
+
class ParquetStatisticsTest : public testing::Test {
public:
ParquetStatisticsTest() = default;
};
+TEST_F(ParquetStatisticsTest, reject_truncated_bloom_filter_payload) {
Review Comment:
[P2] Make this test reach the short-read check
This fake reports `_data.size()` as the file size, so with a 64-byte header
claim and only 32 payload bytes the production code returns from `total_size >
available` before it calls the payload `read_at()`. The test would still pass
if the new `bytes_read != data_buffer.size()` guard were removed. Please let
the fake advertise the full logical range while forcing a successful short
payload read, assert that no Bloom filter is installed, and keep the current
range-truncation scenario as a separate case.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]