Conversation
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.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The new regression test constructs a std::span with a count far beyond the referenced object size, which is undefined behavior even if the span is never dereferenced.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
This PR hardens BucketUtils::HashBytes by rejecting oversized byte inputs before calling the vendored MurmurHash3_x86_32 function, preventing signed-length wraparound that could otherwise lead to out-of-bounds reads and incorrect bucket hashes for very large inputs.
Changes:
- Add a size guard in
BucketUtils::HashBytesto reject inputs larger thanINT32_MAXbefore narrowing to theintlength parameter required byMurmurHash3_x86_32. - Make the narrowing cast explicit (
static_cast<int>(bytes.size())) after the guard. - Add a regression test intended to ensure oversized inputs are rejected without requiring a multi-GB allocation.
| File | Description |
|---|---|
| src/iceberg/util/bucket_util.cc | Adds an explicit maximum-length guard before calling MurmurHash3 with an int length. |
| src/iceberg/test/bucket_util_test.cc | Adds a regression test for rejecting inputs larger than INT32_MAX. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const uint8_t byte = 0; | ||
| const auto oversized_length = | ||
| static_cast<size_t>(std::numeric_limits<int32_t>::max()) + 1; | ||
| std::span<const uint8_t> oversized(&byte, oversized_length); | ||
| ASSERT_THAT([&]() { BucketUtils::HashBytes(oversized); }, |
|
The fix looks correct to me, the UB reported by copilot should be resolved though, thanks. |

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.
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.