Repository navigation
[C++][Python] pyarrow.Table.join() fatal crash for large_string keys near 2gb #54885
Description
Describe the bug, including details regarding any error messages, version, and platform.
Summary
pyarrow.Table.join() crashes my Python session when 1 or more large_string columns are used as keys, and the data in those columns nears 2gb.
Specifically, we can use the following inequality to define the bound of where this bug occurs
occurs when:
[num_rows]*[num_keys]*([key_length]+9) > [2 gigabytes]
In words: if the amount of key data plus the additional 9 bytes per key exceeds 2gb, a fatal crash occurs.
I focused my investigation on 'left anti' joins, where the right table is near boundary, in which case the left table's size seems irrelevant. Just before submission, I confirmed that this occurs with all join types when tested with a large right table.
Expected Behaviour
The table join function should support the use of large_string columns of any size as keys.
Actual Behaviour
The join function causes a fatal crash when large_string key columns exceed a certain size which is below, but near, 2gb.
Tested With
PyArrow versions tested:
24.0.125.0.026.0.0
Platform:
- Windows
- Linux
- Microsoft Fabric (Spark RT 2.0)
Minimal Reproducer
import faulthandler
import numpy as np
import pyarrow as pa
faulthandler.enable()
def make_join_test_data(
left_rows=1_000_000,
right_rows=50_000_000,
num_keys=3,
key_length=15,
):
def build_table(n_rows):
cols = {}
for k in range(num_keys):
cols[f"KEY{k+1}"] = pa.array(
[f"{i:0{key_length}d}" for i in range(n_rows)],
type=pa.large_string(),
)
cols["INTVAL"] = pa.array(np.arange(n_rows, dtype=np.int64))
cols["DOUBLEVAL"] = pa.array(np.arange(n_rows, dtype=np.float64))
return pa.table(cols)
left = build_table(left_rows)
right = build_table(right_rows)
return left, right
left, right = make_join_test_data(
right_rows=534_865 + 1, # remove the `+ 1` and it will work
left_rows=10_000,
num_keys=5,
key_length=794,
)
join_keys = [col for col in right.column_names if col.startswith("KEY")]
print(f"joining with {join_keys}")
left.join(right, keys=join_keys, join_type="left anti") # BUG: note that it fails with all join types
print("it worked")Output (pyarrow 25.0.0 cp314-cp314-linux_x86_64 )
joining with ['KEY1', 'KEY2', 'KEY3', 'KEY4', 'KEY5']
terminate called after throwing an instance of 'std::length_error'
what(): vector::_M_default_append
Fatal Python error: Aborted
Thread 0x00007fdc27af7740 [python] (most recent call first):
File "/opt/conda/lib/python3.14/site-packages/pyarrow/acero.py", line 253 in _perform_join
File "/home/jovyan/try_repro.py", line 39 in <module>
Current thread's C stack trace (most recent call first):
Binary file "python", at _Py_DumpStack+0x4a [0x56532cc3e4ba]
Binary file "python", at +0x18f928 [0x56532cc3e928]
Binary file "/lib/x86_64-linux-gnu/libc.so.6", at +0x45330 [0x7fdc27b41330]
Binary file "/lib/x86_64-linux-gnu/libc.so.6", at pthread_kill+0x11c [0x7fdc27b9ac0c]
Binary file "/lib/x86_64-linux-gnu/libc.so.6", at gsignal+0x1e [0x7fdc27b4127e]
Binary file "/lib/x86_64-linux-gnu/libc.so.6", at abort+0xdf [0x7fdc27b248ff]
Binary file "/opt/conda/lib/python3.14/site-packages/numpy/_core/../../../../libstdc++.so.6", at +0xb2cd2 [0x7fdc25b17cd2]
Binary file "/opt/conda/lib/python3.14/site-packages/numpy/_core/../../../../libstdc++.so.6", at +0xc5a1a [0x7fdc25b2aa1a]
Binary file "/opt/conda/lib/python3.14/site-packages/numpy/_core/../../../../libstdc++.so.6", at _ZSt10unexpectedv+0x0 [0x7fdc25b1784d]
Binary file "/opt/conda/lib/python3.14/site-packages/numpy/_core/../../../../libstdc++.so.6", at +0xc5cb7 [0x7fdc25b2acb7]
Binary file "/opt/conda/lib/python3.14/site-packages/numpy/_core/../../../../libstdc++.so.6", at _ZSt20__throw_length_errorPKc+0x38 [0x7fdc25b1abe1]
Binary file "/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow_compute.so.2500", at +0x11b30d [0x7fdc1800430d]
Binary file "/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow_compute.so.2500", at _ZN5arrow7compute8internal10RowEncoder15EncodeAndAppendERKNS0_8ExecSpanE+0x13a [0x7fdc188f627a]
Binary file "/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow_acero.so.2500", at _ZNK5arrow5acero22HashJoinDictBuildMulti11EncodeBatchEmRKNS0_20SchemaProjectionMapsINS0_18HashJoinProjectionEEERKNS_7compute9ExecBatchEPNS7_8internal10RowEncoderEPNS7_11ExecContextE+0x1a6 [0x7fdc00a10126]
Binary file "/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow_acero.so.2500", at +0x9e9cf [0x7fdc00a0a9cf]
Binary file "/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow_acero.so.2500", at _ZN5arrow5acero17TaskSchedulerImpl11ExecuteTaskEmilPb+0x69 [0x7fdc00a5abc9]
Binary file "/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow_acero.so.2500", at +0xfa151 [0x7fdc00a66151]
Binary file "/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow_acero.so.2500", at +0xc6d15 [0x7fdc00a32d15]
Binary file "/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow_acero.so.2500", at +0xbf557 [0x7fdc00a2b557]
Binary file "/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow.so.2500", at +0xa57be2 [0x7fdc195fbbe2]
Binary file "/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow.so.2500", at +0xa5499c [0x7fdc195f899c]
Binary file "/opt/conda/lib/python3.14/site-packages/numpy/_core/../../../../libstdc++.so.6", at +0xf5b86 [0x7fdc25b5ab86]
Binary file "/lib/x86_64-linux-gnu/libc.so.6", at +0x9cb84 [0x7fdc27b98b84]
Binary file "/lib/x86_64-linux-gnu/libc.so.6", at +0x129ecc [0x7fdc27c25ecc]
Extension modules: numpy._core._multiarray_umath, numpy.linalg._umath_linalg, pyarrow.lib, numpy.random._common, numpy.random.bit_generator, numpy.random._bounded_integers, numpy.random._pcg64, numpy.random._generator, numpy.random._mt19937, numpy.random._philox, numpy.random._sfc64, numpy.random.mtrand, _cyutility, pandas._libs._cyutility, pandas._libs.tslibs.ccalendar, pandas._libs.tslibs.np_datetime, pandas._libs.tslibs.dtypes, pandas._libs.tslibs.base, pandas._libs.tslibs.nattype, pandas._libs.tslibs.timezones, pandas._libs.properties, pandas._libs.tslibs.fields, pandas._libs.tslibs.timedeltas, pandas._libs.tslibs.tzconversion, pandas._libs.tslibs.timestamps, pandas._libs.tslibs.offsets, pandas._libs.tslibs.strptime, pandas._libs.tslibs.parsing, pandas._libs.tslibs.conversion, pandas._libs.tslibs.period, pandas._libs.tslibs.vectorized, pandas._libs.ops_dispatch, pandas._libs.missing, pandas._libs.hashtable, pandas._libs.algos, pandas._libs.interval, pandas._libs.lib, pyarrow._compute, pandas._libs.ops, numexpr.interpreter, bottleneck.move, bottleneck.nonreduce, bottleneck.nonreduce_axis, bottleneck.reduce, pandas._libs.hashing, pandas._libs.arrays, pandas._libs.tslib, pandas._libs.sparse, pandas._libs.internals, pandas._libs.indexing, pandas._libs.index, pandas._libs.writers, pandas._libs.join, pandas._libs.window.aggregations, pandas._libs.window.indexers, pandas._libs.reshape, pandas._libs.groupby, pandas._libs.json, pandas._libs.parsers, pandas._libs.testing, pyarrow._acero, pyarrow._fs, pyarrow._csv, pyarrow._json, pyarrow._substrait, pyarrow._dataset, pyarrow._dataset_orc, pyarrow._parquet, pyarrow._azurefs, pyarrow._hdfs, pyarrow._gcsfs, pyarrow._s3fs, pyarrow._parquet_encryption, pyarrow._dataset_parquet_encryption, pyarrow._dataset_parquet (total: 75)
Aborted
Related Issues
- [Python] Coredump when joining big large_strings #33151
- [Python][C++] Crash when performing
joinon a table larger than INT32_MAX bytes #36879
Investigation
I suspect there is an int32_t where there may need to be something larger. The stack trace leads to the C++ method arrow.compute.internal.RowEncoder.EncodeAndAppend() (found in row_encoder_internal.cc). This method makes use of a private member, offsets_, which is defined in row_encoder_internal.h and contains the following note
// offsets_ vector stores the starting position (offset) of each encoded row
// within the bytes_ vector. This allows for quick access to individual rows.
//
// The size would be num_rows + 1 if not empty, the last element is the total
// length of the bytes_ vector.
std::vector<int32_t> offsets_;This has me wondering, if the bytes vector exceeds a certain size, will the offset not be able to address it?
Additional Reproductions
I ran a number of different tests with varying numbers of keys, key length, rows of data, and different left table sizes. The following code documents that and makes it easy to continue testing.
"""pyarrow.Table.join fails under certain conditions.
pyarrow.Table.join fails when
- type is 'left anti'
- keys (1 or more) are `large_string`
- the volume of string key data + metadata exceeds a threshold of 2gb
The failure is not raised as an arrow exception but leads to 'Fatal Python error: Aborted'.
The C Stack Trace leads to C++ method `arrow.compute.internal.RowEncoder.EncodeAndAppend()`.
"""
import faulthandler
from pprint import pprint
import numpy as np
import pyarrow as pa
faulthandler.enable()
def print_table_summary(name, t, keys=None):
msg = f"\n{name}\n"
msg += f"rows : {t.num_rows:,}\n"
msg += f"columns : {t.num_columns}\n"
msg += f"nbytes : {t.nbytes} ({t.nbytes / 1024**3:.2f} GiB)\n"
key_bytes=sum([col.nbytes for col in t.select(keys).columns])
msg += f"key nbytes: {key_bytes} ({key_bytes / 1024**3:.2f} GiB)\n"
msg += f"chunks : {sum(len(c.chunks) for c in t.columns):,}\n"
for col_name in t.column_names:
col = t[col_name]
msg += f" {col_name:10}"
msg += f"{str(col.type):14}"
msg += f"chunks={len(col.chunks):<6,}"
msg += f" bytes={col.nbytes:-16}\n"
print(msg)
def make_join_test_data(
left_rows=1_000_000,
right_rows=50_000_000,
num_keys=3,
key_length=15,
final_key_length_adjustment=0,
):
"""Build tables as specified, each also includes 1 int and 1 double column."""
print("building test data with specs:")
pprint(locals())
print()
# determine expected outcome
_2gb = 2*1024*1024*1024
ptr_bytes=8
null_bytes=1
est_bytes = right_rows*num_keys*(key_length + ptr_bytes + null_bytes)
if final_key_length_adjustment != 0:
est_bytes += final_key_length_adjustment*num_keys
est_buffer_free = _2gb - est_bytes
if est_buffer_free <0:
print("Expected outcome: FAILURE")
print (" Estimated join bytes exceed 2gb by ", -1*est_buffer_free, " bytes")
else:
print("Expected outcome: SUCCESS")
print(" Join should pass with ", est_buffer_free, " bytes of extra space")
def build_table(n_rows):
cols = {}
temp_col=[f"{i:0{key_length}d}" for i in range(n_rows)]
temp_col[-1] = f"{(n_rows-1):0{key_length+final_key_length_adjustment}d}"
for k in range(num_keys):
cols[f"KEY{k+1}"] = pa.array(
temp_col,
type=pa.large_string(),
)
cols["INTVAL"] = pa.array(np.arange(n_rows, dtype=np.int64))
cols["DOUBLEVAL"] = pa.array(np.arange(n_rows, dtype=np.float64))
return pa.table(cols)
left = build_table(left_rows)
right = build_table(right_rows)
return left, right
# list of different shape tables at the point of failure
runs = [
{#0
"triggers_bug": False,
"args": {
"right_rows": 37_025_580,
"left_rows": 10_000,
"num_keys": 2,
"key_length": 20,
},
},
{#1
"triggers_bug": True,
"args": {
"right_rows": 37_025_580 + 1,
"left_rows": 10_000,
"num_keys": 2,
"key_length": 20,
},
},
{#2
"triggers_bug": False,
"args": {
"right_rows": 534_865,
"left_rows": 10_000,
"num_keys": 5,
"key_length": 794,
},
},
{#3
"triggers_bug": True,
"args": {
"right_rows": 534_865 + 1,
"left_rows": 10_000,
"num_keys": 5,
"key_length": 794,
},
},
{#4
"triggers_bug": False,
"args": {
"right_rows": 1_337_162,
"left_rows": 10_000,
"num_keys": 2,
"key_length": 794,
},
},
{#5
"triggers_bug": True,
"args": {
"right_rows": 1_337_162 + 1,
"left_rows": 10_000,
"num_keys": 2,
"key_length": 794,
},
},
{#6
"triggers_bug": False,
"args": {
"right_rows": 2_674_325,
"left_rows": 10_000,
"num_keys": 1,
"key_length": 794,
},
},
{#7
"triggers_bug": True,
"args": {
"right_rows": 2_674_325 + 1,
"left_rows": 10_000,
"num_keys": 1,
"key_length": 794,
},
},
{#8
"triggers_bug": False,
"args": {
"right_rows": 37_025_580,
"left_rows": 10_000,
"num_keys": 2,
"key_length": 20,
"final_key_length_adjustment": 3,
},
},
{#9
"triggers_bug": True,
"args": {
"right_rows": 37_025_580,
"left_rows": 10_000,
"num_keys": 2,
"key_length": 20,
"final_key_length_adjustment": 4,
},
},
{#10
"triggers_bug": False,
"args": {
"right_rows": 534_465,
"left_rows": 10_000,
"num_keys": 2,
"key_length": 2000,
},
},
{#11
"triggers_bug": True,
"args": {
"right_rows": 534_465 + 1,
"left_rows": 10_000,
"num_keys": 2,
"key_length": 2000,
},
},
]
#left, right = make_join_test_data(
# left_rows=10_000,
# right_rows=37_025_580+1,
# num_keys=2,
# key_length=20,
# #final_key_length_adjustment=3
#)
left, right = make_join_test_data(
**runs[4]['args']
)
join_keys = [col for col in right.column_names if col.startswith("KEY")]
print_table_summary("left", left, join_keys)
print_table_summary("right", right, join_keys)
print(f"joining with {join_keys}")
# .join doesn't like too much `large_string` data
left.join(
right,
keys=join_keys,
join_type="left anti",
)
print("it worked")Sample Output (runs[3])
building test data with specs:
{'final_key_length_adjustment': 0,
'key_length': 794,
'left_rows': 10000,
'num_keys': 5,
'right_rows': 534866}
Expected outcome: FAILURE
Estimated join bytes exceed 2gb by 3342 bytes
left
rows : 10,000
columns : 7
nbytes : 40260000 (0.04 GiB)
key nbytes: 40100000 (0.04 GiB)
chunks : 7
KEY1 large_string chunks=1 bytes= 8020000
KEY2 large_string chunks=1 bytes= 8020000
KEY3 large_string chunks=1 bytes= 8020000
KEY4 large_string chunks=1 bytes= 8020000
KEY5 large_string chunks=1 bytes= 8020000
INTVAL int64 chunks=1 bytes= 80000
DOUBLEVAL double chunks=1 bytes= 80000
right
rows : 534,866
columns : 7
nbytes : 2153370516 (2.01 GiB)
key nbytes: 2144812660 (2.00 GiB)
chunks : 7
KEY1 large_string chunks=1 bytes= 428962532
KEY2 large_string chunks=1 bytes= 428962532
KEY3 large_string chunks=1 bytes= 428962532
KEY4 large_string chunks=1 bytes= 428962532
KEY5 large_string chunks=1 bytes= 428962532
INTVAL int64 chunks=1 bytes= 4278928
DOUBLEVAL double chunks=1 bytes= 4278928
joining with ['KEY1', 'KEY2', 'KEY3', 'KEY4', 'KEY5']
Fatal Python error: Aborted
Thread 0x00000c14 (most recent call first):
.
.
.
Component(s)
C++, Python
Activity
[-]pyarrow.Table.join() fatal crash for `large_string` keys near 2gb[/-][+][C++][Python] pyarrow.Table.join() fatal crash for `large_string` keys near 2gb[/+]aryan9948 commentedon Oct 21, 2026
Hi, I'd like to investigate and contribute to this issue. I noticed the potential limitation around
int32_toffsets inRowEncoder. I'd like to reproduce the crash and investigate a fix with appropriate regression tests. Is anyone already working on this, and would you be open to me taking it on?zanmato1984 commentedon Oct 22, 2026
This is a signed 32-bit overflow in Basic Hash Join’s
RowEncoder. Schemas containinglarge_stringorlarge_binaryselect this implementation. Its cumulative build-side byte offsets overflow near 2 GiB, leading to the invalid vector resize.The extra 9 bytes per key are a null marker and an eight-byte length, explaining the reported boundary.
The large-offset cases in #33151 and #36879 share this cause. Keeping this issue open as the consolidated tracker.
joinon a table larger than INT32_MAX bytes #36879 as a duplicate of this issue