Skip to content

[C++] Take on binary/string arrays silently overflows int32 offsets, producing corrupt arrays and segfaults #50840

Description

@pearu

Describe the bug, including details regarding any error messages, version, and platform.

compute::Take on a binary-like array (string/binary, i.e. 32-bit offsets) silently overflows the output offsets buffer when the selected data exceeds INT32_MAX bytes. It returns Status::OK() with a corrupt array rather than raising, which leads to garbage values and segfaults downstream.

There is a guard for exactly this in VarBinarySelectionImpl::GenerateOutput, but it never fires on GCC/Clang because of a misplaced closing parenthesis.

Reproducer (Python)

This is how the problem typically surfaces — decoding a dictionary whose dense form exceeds 2 GiB:

import numpy as np, pyarrow as pa

arr = pa.DictionaryArray.from_arrays(
    np.zeros(50_000_000, dtype=np.int16),
    pa.array(["a" * 50]),
)
np.asarray(arr)     # 50_000_000 * 50 = 2.5e9 bytes > INT32_MAX

Result is either a nonsense error or a hard crash:

ArrowException: Unknown error: Wrapping <garbage bytes> failed
Segmentation fault (core dumped)

np.asarrayArray.to_numpy(zero_copy_only=False) sets decode_dictionaries=True, and arrow_to_pandas.cc DecodeDictionaries() casts to the dictionary's value type (string, 32-bit offsets) via compute::Cast, which dispatches to UnpackDictionaryTake.

Reproducer (C++, minimal)

The Python layer is incidental; Take alone is enough. 2048 × 1 MiB = 2 GiB of output, one value past the limit:

StringBuilder values_builder;
ASSERT_OK(values_builder.Append(std::string(1 << 20, 'x')));   // 1 MiB
ASSERT_OK_AND_ASSIGN(auto values, values_builder.Finish());

Int32Builder indices_builder;
ASSERT_OK(indices_builder.Reserve(2048));
for (int64_t i = 0; i < 2048; ++i) indices_builder.UnsafeAppend(0);
ASSERT_OK_AND_ASSIGN(auto indices, indices_builder.Finish());

ASSERT_OK_AND_ASSIGN(auto out, Take(Datum(*values), Datum(*indices)));  // succeeds!

Inspecting the result of the dictionary-decode form on current main (42694575d0):

decoded size = 2500000000 bytes (INT32_MAX = 2147483647)
RESULT: Cast returned OK (NO error raised)
  length              = 50000000
  negative offsets    = 7050328
  last offset         = -1794967296      <- 2500000000 - 2^32

Root cause

cpp/src/arrow/compute/kernels/vector_selection_internal.cc:520:

if (Adapter::is_take &&
    ARROW_PREDICT_FALSE(static_cast<int64_t>(offset) +
                        static_cast<int64_t>(val_size)) > kOffsetLimit) {
                                                      // ^ closes around the SUM
  return Status::Invalid("Take operation overflowed binary array capacity");
}

On GCC/Clang, ARROW_PREDICT_FALSE(x) expands to (__builtin_expect(!!(x), 0)) (cpp/src/arrow/util/macros.h:79). The !! collapses the sum to 0 or 1, which is then compared against kOffsetLimit (2147483646) — so the condition is always false and the guard is dead code.

Standalone confirmation:

sum          = 2147500000
kOffsetLimit = 2147483646
as-written guard fires : false
intended   guard fires : true

Under MSVC and the fallback definitions, ARROW_PREDICT_FALSE(x) is (x) (macros.h:105, macros.h:113), so the guard works correctly there. This affects GCC/Clang builds only — Linux and macOS, not Windows.

Introduced in c07486c29f (ARROW-5760, 2020-06-11), so it has been present since Arrow 1.0.0. It went unnoticed because there is no large-memory test covering Take in vector_selection_test.cc.

Expected behavior

Take should raise a proper error rather than returning a corrupt array. With the parenthesis corrected, the reproducer yields:

Invalid: Take operation overflowed binary array capacity

which propagates cleanly up through CastDecodeDictionaries → a normal Python exception, instead of a segfault.

Making the dictionary-decode case actually succeed (rather than raise) is a separate enhancement — the 32-bit string type genuinely cannot represent >2 GiB, so Take refusing is correct. I'll file that separately.

Platform

  • Reproduced on main @ 42694575d0, Linux x86_64, GCC 14.3.0
  • Python symptom also seen on pyarrow 24.0.0 and a 25.0.0 dev build

I have a fix and a LARGE_MEMORY_TEST regression test ready and will open a PR.

Component(s)

C++


🤖 Drafted by Claude Code (an AI agent) and reviewed & approved by pearu.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions