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 a7d0bfa5ad7 GH-50840: [C++] Fix dead overflow guard in Take on
binary-like arrays (#50841)
a7d0bfa5ad7 is described below
commit a7d0bfa5ad71af8500f64a0e187ab9523e5da0d5
Author: Pearu Peterson <[email protected]>
AuthorDate: Mon Aug 10 23:28:27 2026 +0300
GH-50840: [C++] Fix dead overflow guard in Take on binary-like arrays
(#50841)
### Rationale for this change
`compute::Take` on `string`/`binary` arrays silently overflows the int32
offsets buffer when the selected data exceeds `INT32_MAX` bytes, returning
`Status::OK()` with a corrupt array. Downstream this produces garbage values
and segfaults — most visibly in pyarrow, where `np.asarray()` on a dictionary
array whose dense form exceeds 2 GiB crashes the interpreter.
A guard for this already exists, but a misplaced closing parenthesis makes
it dead code on GCC and clang:
```cpp
ARROW_PREDICT_FALSE(static_cast<int64_t>(offset) +
static_cast<int64_t>(val_size)) > kOffsetLimit
```
expands to `(__builtin_expect(!!(offset + val_size), 0)) > kOffsetLimit`.
The `!!` collapses the sum to 0 or 1, which is never greater than
`kOffsetLimit` (2147483646), so the branch is never taken. MSVC and the
fallback definitions expand `ARROW_PREDICT_FALSE(x)` to `(x)`, so those builds
were unaffected.
Present since `c07486c29f` (ARROW-5760, 2020-06-11). See #50840 for full
analysis.
### What changes are included in this PR?
- Move the closing parenthesis so the comparison happens inside
`ARROW_PREDICT_FALSE`, in `VarBinarySelectionImpl::GenerateOutput`.
- Add `TestTakeKernel.TakeBinaryOffsetOverflow`, a `LARGE_MEMORY_TEST`
covering the overflow.
Deliberately minimal: it does not attempt to make the oversized
dictionary-decode case *succeed*. A 32-bit `string` cannot represent >2 GiB, so
`Take` refusing is the correct behaviour; making the pyarrow conversion work is
a separate enhancement.
### Are these changes tested?
Yes, and the test was verified to distinguish both states:
| check | result |
|---|---|
| new test **with** fix | PASS (1.08 s, ~2 GiB peak) |
| new test **without** fix | FAIL — `Expected: has substring "...overflowed
binary array capacity" / Actual: "OK"` |
| `arrow-compute-vector-selection-test`, `ARROW_LARGE_MEMORY_TESTS=ON` |
169/169 pass |
| clang-format 18.1.8 | clean |
The test uses 2048 × 1 MiB = 2 GiB, one value past the limit — ~2 GiB peak
and ~1 s, rather than the multi-GB/multi-minute shape of the original
reproducer.
Separately, I confirmed the end-to-end path on `main` @ `42694575d0`:
before the fix `Cast(dictionary<int16,string> -> string)` on a 2.5 GB decode
returns OK with 7,050,328 negative offsets and a final offset of −1794967296 (=
2500000000 − 2³²); after the fix it returns `Invalid: Take operation overflowed
binary array capacity`.
Note that `LARGE_MEMORY_TEST` compiles to `DISABLED_*` unless
`ARROW_LARGE_MEMORY_TESTS=ON`, which in CI only happens in the "AMD64 Ubuntu
Large Memory Tests" job of `cpp_extra.yml` — nightly, or on PRs labelled `CI:
Extra: C++`. I don't have permission to add that label; a committer may want
to, so the new test is exercised before merge.
### Are there any user-facing changes?
Yes. `Take` (and anything built on it, including dictionary decoding and
`DictionaryArray` → numpy/pandas conversion) now raises `Invalid: Take
operation overflowed binary array capacity` where it previously returned
corrupt data or crashed. Code that unknowingly relied on the corrupt result
will now see an error — which is the intent.
**This PR contains a "Critical Fix".** It fixes both a bug that caused
incorrect or invalid data to be produced — silently corrupt offset buffers,
returned as a valid array with `Status::OK()` — and a bug that causes a crash
even when the API contract is upheld, since those offsets lead to out-of-bounds
reads and segfaults on ordinary `Take` usage.
### AI usage
Per the [AI-generated code
guidance](https://arrow.apache.org/docs/dev/developers/overview.html#ai-generated-code):
the diagnosis, the one-line fix, and the test were produced with Claude Code,
and reviewed and verified by me. Correctness was checked by (1) compiling the
macro expansion standalone to confirm the guard never fires as written, (2)
running the new test against both the fixed and unfixed kernel to confirm it
distinguishes them, and (3) reproducing the corrupt offsets and [...]
---
_🤖 Drafted by Claude Code (an AI agent) and reviewed & approved by pearu._
* GitHub Issue: #50840
Authored-by: Pearu Peterson <[email protected]>
Signed-off-by: Rossi Sun <[email protected]>
---
.../compute/kernels/vector_selection_internal.cc | 6 +++---
.../arrow/compute/kernels/vector_selection_test.cc | 23 ++++++++++++++++++++++
2 files changed, 26 insertions(+), 3 deletions(-)
diff --git a/cpp/src/arrow/compute/kernels/vector_selection_internal.cc
b/cpp/src/arrow/compute/kernels/vector_selection_internal.cc
index 7fe8d9b8866..ab58d935054 100644
--- a/cpp/src/arrow/compute/kernels/vector_selection_internal.cc
+++ b/cpp/src/arrow/compute/kernels/vector_selection_internal.cc
@@ -517,9 +517,9 @@ struct VarBinarySelectionImpl : public
Selection<VarBinarySelectionImpl<Type>, T
// Use static property to prune this code from the filter path in
// optimized builds
- if (Adapter::is_take &&
- ARROW_PREDICT_FALSE(static_cast<int64_t>(offset) +
- static_cast<int64_t>(val_size)) >
kOffsetLimit) {
+ if (Adapter::is_take &&
ARROW_PREDICT_FALSE(static_cast<int64_t>(offset) +
+
static_cast<int64_t>(val_size) >
+ kOffsetLimit)) {
return Status::Invalid("Take operation overflowed binary array
capacity");
}
offset += val_size;
diff --git a/cpp/src/arrow/compute/kernels/vector_selection_test.cc
b/cpp/src/arrow/compute/kernels/vector_selection_test.cc
index 5fa2d6824dc..c7972098539 100644
--- a/cpp/src/arrow/compute/kernels/vector_selection_test.cc
+++ b/cpp/src/arrow/compute/kernels/vector_selection_test.cc
@@ -1706,6 +1706,29 @@ TEST_F(TestTakeKernelFSB, TakeFixedSizeBinary) {
TakeCAC(type, {kABNullDE, kABC}, "[4,
10]").Value(&chunked_arr));
}
+// GH-50840: taking more data than a 32-bit offset can address must raise
+// instead of silently overflowing the offsets buffer.
+TEST_F(TestTakeKernel, LARGE_MEMORY_TEST(TakeBinaryOffsetOverflow)) {
+ // 2048 * 1 MiB = 2 GiB of output, one value past the int32 offset limit.
+ constexpr int64_t kValueSize = 1 << 20;
+ constexpr int64_t kNumIndices = 2048;
+
+ StringBuilder values_builder;
+ ASSERT_OK(values_builder.Append(std::string(kValueSize, 'x')));
+ ASSERT_OK_AND_ASSIGN(auto values, values_builder.Finish());
+
+ Int32Builder indices_builder;
+ ASSERT_OK(indices_builder.Reserve(kNumIndices));
+ for (int64_t i = 0; i < kNumIndices; ++i) {
+ indices_builder.UnsafeAppend(0);
+ }
+ ASSERT_OK_AND_ASSIGN(auto indices, indices_builder.Finish());
+
+ EXPECT_RAISES_WITH_MESSAGE_THAT(
+ Invalid, ::testing::HasSubstr("Take operation overflowed binary array
capacity"),
+ TakeAAA(*values, *indices));
+}
+
using ListAndListViewArrowTypes =
::testing::Types<ListType, LargeListType, ListViewType, LargeListViewType>;