Skip to content

Commit 9de744a

Browse files
committed
fix: Reject out-of-range ArrayMap probe keys on 32-bit targets
ArrayMap bucket lookups computed `key.wrapping_sub(offset) as usize` and bounds-checked the result after the cast. On 32-bit targets (e.g. wasm32) the cast truncates the 64-bit ring distance, so an out-of-range probe key whose low 32 bits land inside the bucket array (e.g. probe key 2^32 + 3 against build keys 0..=10) wraps back into range and reports a false match, producing incorrect join results. Consolidate the four lookup sites (fill_data, both loops of lookup_and_get_indices, contain_keys) onto a key_to_index helper that bounds-checks in u64 before narrowing to usize, and add a regression test. No behavior change on 64-bit targets.
1 parent b8998c7 commit 9de744a

1 file changed

Lines changed: 80 additions & 13 deletions

File tree

datafusion/physical-plan/src/joins/array_map.rs

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,16 @@ impl ArrayMap {
157157
max_val.wrapping_sub(min_val)
158158
}
159159

160+
#[inline]
161+
fn key_to_index(key: u64, offset: u64, data_len: usize) -> Option<usize> {
162+
let idx = key.wrapping_sub(offset);
163+
if idx < data_len as u64 {
164+
Some(idx as usize)
165+
} else {
166+
None
167+
}
168+
}
169+
160170
/// Creates a new [`ArrayMap`] from the given array of join keys.
161171
///
162172
/// Note: This function processes only the non-null values in the input `array`,
@@ -207,10 +217,9 @@ impl ArrayMap {
207217
for (i, val) in arr.iter().enumerate().rev() {
208218
if let Some(val) = val {
209219
let key: u64 = val.as_();
210-
let idx = key.wrapping_sub(offset_val) as usize;
211-
if idx >= data.len() {
220+
let Some(idx) = Self::key_to_index(key, offset_val, data.len()) else {
212221
return internal_err!("failed build Array idx >= data.len()");
213-
}
222+
};
214223

215224
if data[idx] != 0 {
216225
if next.is_empty() {
@@ -264,6 +273,16 @@ impl ArrayMap {
264273
)
265274
}
266275

276+
/// Returns whether `key` (a raw probe value cast to `u64`) is present in the
277+
/// build side, i.e. maps to a non-empty bucket.
278+
#[inline]
279+
fn key_present(&self, key: u64) -> bool {
280+
let Some(idx) = Self::key_to_index(key, self.offset, self.data.len()) else {
281+
return false;
282+
};
283+
self.data[idx] != 0
284+
}
285+
267286
fn lookup_and_get_indices<T: ArrowNumericType>(
268287
&self,
269288
array: &ArrayRef,
@@ -294,11 +313,13 @@ impl ArrayMap {
294313
}
295314
// SAFETY: prob_idx is guaranteed to be within bounds by the loop range.
296315
let prob_val: u64 = unsafe { arr.value_unchecked(prob_idx) }.as_();
297-
let idx_in_build_side = prob_val.wrapping_sub(self.offset) as usize;
316+
let Some(idx_in_build_side) =
317+
Self::key_to_index(prob_val, self.offset, self.data.len())
318+
else {
319+
continue;
320+
};
298321

299-
if idx_in_build_side >= self.data.len()
300-
|| self.data[idx_in_build_side] == 0
301-
{
322+
if self.data[idx_in_build_side] == 0 {
302323
continue;
303324
}
304325
build_indices.push((self.data[idx_in_build_side] - 1) as u64);
@@ -345,10 +366,12 @@ impl ArrayMap {
345366

346367
// SAFETY: prob_idx is guaranteed to be within bounds by the loop range.
347368
let prob_val: u64 = unsafe { arr.value_unchecked(prob_side_idx) }.as_();
348-
let idx_in_build_side = prob_val.wrapping_sub(self.offset) as usize;
349-
if idx_in_build_side >= self.data.len()
350-
|| self.data[idx_in_build_side] == 0
351-
{
369+
let Some(idx_in_build_side) =
370+
Self::key_to_index(prob_val, self.offset, self.data.len())
371+
else {
372+
continue;
373+
};
374+
if self.data[idx_in_build_side] == 0 {
352375
continue;
353376
}
354377

@@ -402,8 +425,7 @@ impl ArrayMap {
402425
}
403426
// SAFETY: i is within bounds [0, arr.len())
404427
let key: u64 = unsafe { arr.value_unchecked(i) }.as_();
405-
let idx = key.wrapping_sub(self.offset) as usize;
406-
idx < self.data.len() && self.data[idx] != 0
428+
self.key_present(key)
407429
});
408430
Ok(BooleanArray::new(buffer, None))
409431
}
@@ -414,6 +436,7 @@ mod tests {
414436
use super::*;
415437
use arrow::array::Int32Array;
416438
use arrow::array::Int64Array;
439+
use arrow::array::UInt64Array;
417440
use std::sync::Arc;
418441

419442
#[test]
@@ -506,6 +529,50 @@ mod tests {
506529
Ok(())
507530
}
508531

532+
#[test]
533+
fn test_array_map_rejects_large_out_of_range_probe_key() -> Result<()> {
534+
let build: ArrayRef =
535+
Arc::new(UInt64Array::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));
536+
let map = ArrayMap::try_new(&build, 0, 10)?;
537+
538+
assert_eq!(ArrayMap::key_to_index(3, 0, 11), Some(3));
539+
540+
// Pick a key for which the computed bucket offset is larger than
541+
// u32::MAX but has low 32 bits equal to 3. It must be bounds-checked
542+
// before casting to usize, otherwise 32-bit targets can truncate it
543+
// into range.
544+
let out_of_range_key = (1_u64 << 32) + 3;
545+
assert_eq!(ArrayMap::key_to_index(out_of_range_key, 0, 11), None);
546+
547+
let probe = [Arc::new(UInt64Array::from(vec![
548+
Some(3),
549+
Some(out_of_range_key),
550+
Some(11),
551+
None,
552+
])) as ArrayRef];
553+
554+
let mut matched_probe_indices = vec![];
555+
let mut matched_build_indices = vec![];
556+
let next = map.get_matched_indices_with_limit_offset(
557+
&probe,
558+
10,
559+
(0, None),
560+
&mut matched_probe_indices,
561+
&mut matched_build_indices,
562+
)?;
563+
assert_eq!(matched_probe_indices, vec![0]);
564+
assert_eq!(matched_build_indices, vec![3]);
565+
assert!(next.is_none());
566+
567+
let contains = map.contain_keys(&probe)?;
568+
assert!(contains.value(0));
569+
assert!(!contains.value(1));
570+
assert!(!contains.value(2));
571+
assert!(!contains.value(3));
572+
573+
Ok(())
574+
}
575+
509576
#[test]
510577
fn test_array_map_i64_with_negative_and_positive_numbers() -> Result<()> {
511578
// Build array with a mix of negative and positive i64 values, no duplicates

0 commit comments

Comments
 (0)