This is an automated email from the ASF dual-hosted git repository.
zanmato1984 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/main by this push:
new a769c291e0 GH-45086: [C++] Fix heap buffer overflow in
FillNullForward/Backward … (#50843)
a769c291e0 is described below
commit a769c291e01093b73d03a075179cf7a09bf92ad8
Author: Tony Roberts <[email protected]>
AuthorDate: Tue Sep 1 03:23:43 2026 +0100
GH-45086: [C++] Fix heap buffer overflow in FillNullForward/Backward …
(#50843)
### Rationale for this change
Fixes issue #45086 by fixing a heap heap buffer overflow in
FillNullForward/Backward on chunked boolean arrays.
### What changes are included in this PR?
FillNullForwardChunked and FillNullBackwardChunked sized each output
chunk's data buffer as `type->byte_width() * chunk->length()`. For
BooleanType, byte_width() returns 0 (bit_width() / 8, truncated by
integer division), so the buffer was allocated with 0 bytes while the
chunk's declared length was unchanged, and filling it wrote real bit
data past the end of the allocation.
~~Add DataType::bytes_required(num_elements), a virtual method alongside
byte_width()/bit_width() that correctly rounds up for bit-packed types,
and use it at both call sites instead of the byte_width()-based
calculation. Add regression tests exercising fill-null-forward and
fill-null-backward on a chunked boolean array with a chunk large enough
to reproduce the crash.~~
This change uses `arrow::util::internal::PreallocateFixedWidthArrayData`
to allocate the correct sized buffer.
### Are these changes tested?
Yes, and new unit tests have been added.
### Are there any user-facing changes?
No ~~breaking changes. A new method,
DataType::bytes_required(num_elements), was added.~~
### AI Disclosure
Claude was used to help fix this issue, but all the code has been
reviewed and tested locally (on Windows only, built using gcc).
* GitHub Issue: #45086
---------
Co-authored-by: Claude Sonnet 5 <[email protected]>
---
cpp/src/arrow/compute/kernels/vector_replace.cc | 24 ++++-----
.../arrow/compute/kernels/vector_replace_test.cc | 60 ++++++++++++++++++++++
2 files changed, 71 insertions(+), 13 deletions(-)
diff --git a/cpp/src/arrow/compute/kernels/vector_replace.cc
b/cpp/src/arrow/compute/kernels/vector_replace.cc
index 6a9abfc039..313fbd53c8 100644
--- a/cpp/src/arrow/compute/kernels/vector_replace.cc
+++ b/cpp/src/arrow/compute/kernels/vector_replace.cc
@@ -22,6 +22,7 @@
#include "arrow/compute/kernels/util_internal.h"
#include "arrow/compute/registry_internal.h"
#include "arrow/util/bitmap_ops.h"
+#include "arrow/util/fixed_width_internal.h"
#include "arrow/util/logging_internal.h"
namespace arrow {
@@ -422,12 +423,10 @@ struct ReplaceMaskChunked {
ExecResult chunk_result;
if (is_fixed_width(out->type()->id())) {
auto chunk_out = std::make_shared<ArrayData>(chunk->type(),
chunk->length());
- chunk_out->buffers.resize(2);
- ARROW_ASSIGN_OR_RAISE(chunk_out->buffers[0],
- ctx->AllocateBitmap(chunk->length()));
- const int64_t slot_width = out->type()->byte_width();
- ARROW_ASSIGN_OR_RAISE(chunk_out->buffers[1],
- ctx->Allocate(slot_width * chunk->length()));
+ ArrayData* chunk_out_arr = chunk_out.get();
+ RETURN_NOT_OK(util::internal::PreallocateFixedWidthArrayData(
+ ctx, chunk->length(), /*source=*/*chunk->data(),
+ /*allocate_validity=*/true, chunk_out_arr));
chunk_result.value = chunk_out;
}
if (batch[1].is_scalar()) {
@@ -696,10 +695,9 @@ struct FillNullForwardChunked {
for (const std::shared_ptr<Array>& chunk : values.chunks()) {
if (is_fixed_width(out->type()->id())) {
ArrayData* output = out->mutable_array();
- ARROW_ASSIGN_OR_RAISE(output->buffers[0],
ctx->AllocateBitmap(chunk->length()));
- ARROW_ASSIGN_OR_RAISE(
- output->buffers[1],
- ctx->Allocate(out->type()->byte_width() * chunk->length()));
+ RETURN_NOT_OK(util::internal::PreallocateFixedWidthArrayData(
+ ctx, chunk->length(), /*source=*/*chunk->data(),
+ /*allocate_validity=*/true, output));
}
ExecResult chunk_result;
chunk_result.value = out->array();
@@ -780,9 +778,9 @@ struct FillNullBackwardChunked {
const auto& chunk = chunks[i];
if (is_fixed_width(out->type()->id())) {
ArrayData* output = out->mutable_array();
- auto data_bytes = output->type->byte_width() * chunk->length();
- ARROW_ASSIGN_OR_RAISE(output->buffers[0],
ctx->AllocateBitmap(chunk->length()));
- ARROW_ASSIGN_OR_RAISE(output->buffers[1], ctx->Allocate(data_bytes));
+ RETURN_NOT_OK(util::internal::PreallocateFixedWidthArrayData(
+ ctx, chunk->length(), /*source=*/*chunk->data(),
+ /*allocate_validity=*/true, output));
}
ExecResult chunk_result;
chunk_result.value = out->array();
diff --git a/cpp/src/arrow/compute/kernels/vector_replace_test.cc
b/cpp/src/arrow/compute/kernels/vector_replace_test.cc
index 9dc8e70ab6..dc63bae39a 100644
--- a/cpp/src/arrow/compute/kernels/vector_replace_test.cc
+++ b/cpp/src/arrow/compute/kernels/vector_replace_test.cc
@@ -545,6 +545,24 @@ TEST_F(TestReplaceBoolean, ReplaceWithMask) {
}
}
+// Regression test: ReplaceMaskChunked (the ChunkedArray path of
replace_with_mask)
+// sized each output chunk's data buffer via byte_width(), which is 0 for
boolean
+// (bit-packed), the same GH-45086 buffer-overflow pattern fixed elsewhere in
this
+// file. A chunk needs to be large enough to write past the buffer's small
built-in
+// padding to reliably reproduce the crash.
+TEST_F(TestReplaceBoolean, ReplaceWithMaskChunkedArray) {
+ constexpr int64_t kChunkLength = 4096;
+ auto all_false = ConstantArrayGenerator::Boolean(kChunkLength,
/*value=*/false);
+ auto all_true = ConstantArrayGenerator::Boolean(kChunkLength,
/*value=*/true);
+ auto input = std::make_shared<ChunkedArray>(
+ ArrayVector{all_false, all_false, all_false}, boolean());
+ auto expected = std::make_shared<ChunkedArray>(
+ ArrayVector{all_true, all_true, all_true}, boolean());
+
+ this->Assert(ReplaceWithMask, Datum(input), this->mask_scalar(true),
+ this->scalar("true"), Datum(expected));
+}
+
TEST_F(TestReplaceNull, ReplaceWithMask) {
std::vector<ReplaceWithMaskCase> cases = {
{this->array("[]"), this->mask_scalar(false), this->array("[]"),
this->array("[]")},
@@ -1117,6 +1135,13 @@ class TestFillNullType : public
TestReplaceKernel<NullType> {
std::shared_ptr<DataType> type() override { return
default_type_instance<NullType>(); }
};
+class TestFillNullBoolean : public TestReplaceKernel<BooleanType> {
+ protected:
+ std::shared_ptr<DataType> type() override {
+ return TypeTraits<BooleanType>::type_singleton();
+ }
+};
+
TYPED_TEST_SUITE(TestFillNullNumeric, NumericBasedTypes);
TYPED_TEST_SUITE(TestFillNullDecimal, DecimalArrowTypes);
TYPED_TEST_SUITE(TestFillNullBinary, BaseBinaryArrowTypes);
@@ -2092,6 +2117,41 @@ TYPED_TEST(TestFillNullBinary, FillBackwardChunkedArray)
{
R"(["qup"])", R"(["qup", "mnz"])"}));
}
+// Regression test for GH-45086: FillNullForwardChunked/FillNullBackwardChunked
+// size each output chunk's data buffer as `type->byte_width() *
chunk->length()`.
+// For BooleanType, byte_width() returns 0 (it is bit-packed, not
byte-addressable),
+// so the buffer was allocated with 0 bytes while the chunk's declared length
stayed
+// the same, and filling the chunk wrote past the end of the (near-)empty
buffer.
+// The corruption/crash only reliably manifests once a chunk is large enough to
+// write past the buffer's small built-in padding, hence the large pad length
here.
+TEST_F(TestFillNullBoolean, FillNullForwardChunkedArray) {
+ constexpr int64_t kPadLength = 4096;
+ ASSERT_OK_AND_ASSIGN(auto null_pad, MakeArrayOfNull(boolean(), kPadLength));
+ auto all_true = ConstantArrayGenerator::Boolean(kPadLength, /*value=*/true);
+ auto single_true = ConstantArrayGenerator::Boolean(1, /*value=*/true);
+
+ auto input = std::make_shared<ChunkedArray>(
+ ArrayVector{null_pad, single_true, null_pad}, boolean());
+ auto expected = std::make_shared<ChunkedArray>(
+ ArrayVector{null_pad, single_true, all_true}, boolean());
+
+ this->AssertFillNullChunkedArray(FillNullForward, input, expected);
+}
+
+TEST_F(TestFillNullBoolean, FillNullBackwardChunkedArray) {
+ constexpr int64_t kPadLength = 4096;
+ ASSERT_OK_AND_ASSIGN(auto null_pad, MakeArrayOfNull(boolean(), kPadLength));
+ auto all_true = ConstantArrayGenerator::Boolean(kPadLength, /*value=*/true);
+ auto single_true = ConstantArrayGenerator::Boolean(1, /*value=*/true);
+
+ auto input = std::make_shared<ChunkedArray>(
+ ArrayVector{null_pad, single_true, null_pad}, boolean());
+ auto expected = std::make_shared<ChunkedArray>(
+ ArrayVector{all_true, single_true, null_pad}, boolean());
+
+ this->AssertFillNullChunkedArray(FillNullBackward, input, expected);
+}
+
TEST_F(TestFillNullType, TestFillOnNullType) {
this->AssertFillNullArray(FillNullForward, this->array(R"([null, null, null,
null])"),
this->array(R"([null, null, null, null])"));