Skip to content

Commit 768ac65

Browse files
WZhuowgtmac
andauthored
fix(bucket): reject oversized input in BucketUtils::HashBytes (#961)
* fix(bucket): reject oversized input in BucketUtils::HashBytes HashBytes passed a size_t length straight to the vendored MurmurHash3_x86_32, whose length parameter is a signed 32-bit int. For inputs of 2^31 bytes or more the length wrapped negative, making the hash loop read far out of bounds (and nblocks * 4 signed-overflow UB); for inputs above 2^32 bytes a silently wrong prefix was hashed, breaking the bucket contract against other implementations. Every literal hash path (kDecimal/kString/kUuid/kBinary/kFixed) funnels through this one function, so guard it there: reject lengths above INT32_MAX with ICEBERG_CHECK_OR_DIE before the narrowing cast. The bound matches the limit implied by iceberg-java's byte arrays, so no legal bucket input is rejected. The vendored murmurhash3_internal.cc is left untouched (tagged third-party code) and the now-checked narrowing is made explicit with static_cast<int>. Add BucketUtilsTest.HashBytesRejectsOversizedInput, which fabricates an oversized span over a 1-byte buffer (never dereferenced, so no 2 GiB allocation is needed in CI). Without the guard the test segfaults. * test(bucket): guard raw byte input so the oversized-length test is well-formed The regression test built a std::span over INT32_MAX + 1 elements of a one-byte object, which violates the std::span constructor precondition and is undefined behavior even though the data is never read. Move the length guard into an internal HashBytes(const uint8_t*, size_t) overload and keep BucketUtils::HashBytes(std::span) as a thin wrapper, so the test can pass a null pointer with the oversized length instead. Also document that HashBytes and BucketIndex throw IcebergError for inputs larger than INT32_MAX, which the public headers did not mention. --------- Co-authored-by: Gang Wu <ustcwg@gmail.com>
1 parent d98d851 commit 768ac65

4 files changed

Lines changed: 92 additions & 1 deletion

File tree

‎src/iceberg/test/bucket_util_test.cc‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,16 @@
2020
#include "iceberg/util/bucket_util.h"
2121

2222
#include <chrono>
23+
#include <cstdint>
24+
#include <limits>
2325

26+
#include <gmock/gmock.h>
2427
#include <gtest/gtest.h>
2528

29+
#include "iceberg/exception.h"
2630
#include "iceberg/expression/literal.h"
2731
#include "iceberg/test/temporal_test_helper.h"
32+
#include "iceberg/util/bucket_util_internal.h"
2833
#include "iceberg/util/decimal.h"
2934
#include "iceberg/util/uuid.h"
3035

@@ -108,6 +113,19 @@ TEST(BucketUtilsTest, HashHelper) {
108113
EXPECT_EQ(BucketUtils::HashBytes(fixed), -188683207);
109114
}
110115

116+
TEST(BucketUtilsTest, HashBytesRejectsOversizedInput) {
117+
// Lengths above INT32_MAX would wrap the 32-bit signed length parameter of
118+
// MurmurHash3_x86_32 and read out of bounds. The guard rejects them before the
119+
// data is touched, so pass a null pointer instead of building a std::span over
120+
// more bytes than the object provides (which is undefined behavior) or
121+
// allocating 2 GiB.
122+
const auto oversized_length =
123+
static_cast<size_t>(std::numeric_limits<int32_t>::max()) + 1;
124+
ASSERT_THAT([&]() { internal::HashBytes(nullptr, oversized_length); },
125+
::testing::ThrowsMessage<IcebergError>(
126+
::testing::HasSubstr("exceeds the maximum supported length")));
127+
}
128+
111129
TEST(BucketUtilsTest, BucketTimestampNanosMatchesMicros) {
112130
constexpr int32_t kNumBuckets = 1000;
113131
const auto ts_micros = TemporalTestHelper::CreateTimestamp({.year = 2017,

‎src/iceberg/util/bucket_util.cc‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
1919

2020
#include "iceberg/util/bucket_util.h"
2121

22+
#include <limits>
2223
#include <utility>
2324

25+
#include "iceberg/exception.h"
2426
#include "iceberg/expression/literal.h"
27+
#include "iceberg/util/bucket_util_internal.h"
2528
#include "iceberg/util/endian.h"
2629
#include "iceberg/util/murmurhash3_internal.h"
2730
#include "iceberg/util/temporal_util.h"
@@ -110,11 +113,29 @@ int32_t HashLiteral<TypeId::kFixed>(const Literal& literal) {
110113
} // namespace
111114

112115
int32_t BucketUtils::HashBytes(std::span<const uint8_t> bytes) {
116+
return internal::HashBytes(bytes.data(), bytes.size());
117+
}
118+
119+
namespace internal {
120+
121+
int32_t HashBytes(const uint8_t* data, size_t len) {
122+
// MurmurHash3_x86_32 takes a signed 32-bit length. Passing a larger size_t
123+
// would wrap it negative and make the hash loop read far outside `data`
124+
// (and silently hash the wrong prefix for even larger inputs). Iceberg's
125+
// bucket transform is defined on byte sequences of at most 2^31 - 1 bytes
126+
// (Java arrays), so reject anything longer before narrowing.
127+
ICEBERG_CHECK_OR_DIE(
128+
len <= static_cast<size_t>(std::numeric_limits<int32_t>::max()),
129+
"BucketUtils::HashBytes: input length {} exceeds the maximum supported "
130+
"length {}",
131+
len, std::numeric_limits<int32_t>::max());
113132
int32_t hash_value = 0;
114-
MurmurHash3_x86_32(bytes.data(), bytes.size(), 0, &hash_value);
133+
MurmurHash3_x86_32(data, static_cast<int>(len), 0, &hash_value);
115134
return hash_value;
116135
}
117136

137+
} // namespace internal
138+
118139
int32_t BucketUtils::HashLong(int64_t value) {
119140
int32_t hash_value = 0;
120141
value = ToLittleEndian(value);

‎src/iceberg/util/bucket_util.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,19 @@ class ICEBERG_EXPORT BucketUtils {
5151
/// \brief Hash a byte array using MurmurHash3 and return a 32-bit hash value.
5252
/// \param bytes The input byte array to hash.
5353
/// \return A 32-bit hash value.
54+
/// \throws IcebergError if `bytes.size()` exceeds `INT32_MAX`. MurmurHash3 takes a
55+
/// signed 32-bit length, and the bucket transform is only defined for byte
56+
/// sequences up to that size, so longer inputs are rejected rather than
57+
/// truncated. Java cannot produce such a value because its arrays are
58+
/// int-indexed, so nothing iceberg-java could hash is rejected here.
5459
static int32_t HashBytes(std::span<const uint8_t> bytes);
5560

5661
/// \brief Compute the bucket index for a given literal and number of buckets.
5762
/// \param literal The input literal to hash.
5863
/// \param num_buckets The number of buckets to hash into.
5964
/// \return (murmur3_x86_32_hash(literal) & Integer.MAX_VALUE) % num_buckets
65+
/// \throws IcebergError if the literal is a string, binary, or fixed value whose
66+
/// byte length exceeds `INT32_MAX`; see HashBytes.
6067
static Result<int32_t> BucketIndex(const Literal& literal, int32_t num_buckets);
6168
};
6269

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#pragma once
21+
22+
#include <cstddef>
23+
#include <cstdint>
24+
25+
#include "iceberg/iceberg_export.h"
26+
27+
/// \file iceberg/util/bucket_util_internal.h
28+
/// \brief Internal entry points for the bucket transform.
29+
30+
namespace iceberg::internal {
31+
32+
/// \brief Hash `len` bytes starting at `data` using MurmurHash3.
33+
///
34+
/// The raw pointer and length are taken separately so that a length that does not
35+
/// fit the pointee object can be passed without forming an invalid `std::span`.
36+
/// `BucketUtils::HashBytes` forwards to this function.
37+
///
38+
/// \param data Pointer to the first byte to hash. It is only dereferenced once `len`
39+
/// is accepted, so it may be null when the call is expected to be rejected.
40+
/// \param len The number of bytes to hash.
41+
/// \return A 32-bit hash value.
42+
/// \throws IcebergError if `len` exceeds `INT32_MAX`; see `BucketUtils::HashBytes`.
43+
ICEBERG_EXPORT int32_t HashBytes(const uint8_t* data, size_t len);
44+
45+
} // namespace iceberg::internal

0 commit comments

Comments
 (0)