filter_join_indices and its internal use by mixed joins index candidate pairs with 32-bit arithmetic and silently corrupt or read out of bounds when the equality-match candidate count exceeds 2^31, which column row limits do not prevent.
Candidate count is the hash-join match count, bounded only by memory (~8 bytes/pair), not by libcudf's int32 row limit: two ~47k-row tables sharing one distinct key already exceed 2^31 candidates. The left-join branch of the same file was deliberately made 64-bit for exactly this shape (comments at cpp/src/join/filter_join_indices/filter_join_indices.cu:237-288 cite "index vectors of size greater than integer limits"), but these sites were left 32-bit:
cpp/src/join/filter_join_indices/filter_join_indices_kernel.cuh:55: for (cudf::size_type i = tid; ...) truncates the int64 thread id; wrapped-negative values exit the loop via uint64 promotion, so candidate positions >= 2^31 are never evaluated and the tail of the uninitialized predicate_results buffer feeds downstream count/copy as garbage.
cpp/src/join/filter_join_indices/filter_join_indices.cu:179-180 (INNER count): static_cast<size_type>(left_indices.size()) wraps negative before iteration; count runs over a negative-distance range producing garbage counts.
cpp/src/join/filter_join_indices/filter_join_indices.cu:196 (INNER stencil): the counting_iterator<size_type> wraps at offset 2^31 inside CUB DeviceSelect::FlaggedIf; the lambda then indexes predicate_results_ptr[negative], an out-of-bounds read, and garbage flags can select more elements than the sized outputs, overrunning them.
- Same pattern in the FULL branch: lines 321-322, 334-335, 362.
No guard rejects oversized inputs on any of these paths; only cross join has a CUDF_EXPECTS on result rows (cpp/src/join/cross_join.cu:51). The default public path (mixed_inner_join with no output_size) goes through the INNER branch, so production workloads hit this without opting into anything. A >2^31 candidate span with a predicate that rejects most candidates triggers every site while keeping the final result small enough to look healthy.
The newer output-size kernel shows the intended pattern (auto i = tid deducing thread_index_type, host reduction into std::size_t). Suggested fix: propagate that pattern to the predicate kernel loop and all INNER/FULL counting/stencil sites, mirroring the existing 64-bit left-join branch, plus a CUDF_EXPECTS capping the post-filter result count at size_type max before materializing columns, matching cross join.
filter_join_indicesand its internal use by mixed joins index candidate pairs with 32-bit arithmetic and silently corrupt or read out of bounds when the equality-match candidate count exceeds2^31, which column row limits do not prevent.Candidate count is the hash-join match count, bounded only by memory (~8 bytes/pair), not by libcudf's int32 row limit: two ~47k-row tables sharing one distinct key already exceed 2^31 candidates. The left-join branch of the same file was deliberately made 64-bit for exactly this shape (comments at
cpp/src/join/filter_join_indices/filter_join_indices.cu:237-288cite "index vectors of size greater than integer limits"), but these sites were left 32-bit:cpp/src/join/filter_join_indices/filter_join_indices_kernel.cuh:55:for (cudf::size_type i = tid; ...)truncates the int64 thread id; wrapped-negative values exit the loop via uint64 promotion, so candidate positions >= 2^31 are never evaluated and the tail of the uninitializedpredicate_resultsbuffer feeds downstream count/copy as garbage.cpp/src/join/filter_join_indices/filter_join_indices.cu:179-180(INNER count):static_cast<size_type>(left_indices.size())wraps negative before iteration; count runs over a negative-distance range producing garbage counts.cpp/src/join/filter_join_indices/filter_join_indices.cu:196(INNER stencil): thecounting_iterator<size_type>wraps at offset 2^31 inside CUBDeviceSelect::FlaggedIf; the lambda then indexespredicate_results_ptr[negative], an out-of-bounds read, and garbage flags can select more elements than the sized outputs, overrunning them.No guard rejects oversized inputs on any of these paths; only cross join has a
CUDF_EXPECTSon result rows (cpp/src/join/cross_join.cu:51). The default public path (mixed_inner_joinwith nooutput_size) goes through the INNER branch, so production workloads hit this without opting into anything. A >2^31 candidate span with a predicate that rejects most candidates triggers every site while keeping the final result small enough to look healthy.The newer output-size kernel shows the intended pattern (
auto i = tiddeducingthread_index_type, host reduction intostd::size_t). Suggested fix: propagate that pattern to the predicate kernel loop and all INNER/FULL counting/stencil sites, mirroring the existing 64-bit left-join branch, plus aCUDF_EXPECTScapping the post-filter result count atsize_typemax before materializing columns, matching cross join.