Skip to content

Fuzzing Crash: List take creates validity array with incorrect size #5743

@github-actions

Description

@github-actions

Fuzzing Crash Report

Analysis

Crash Location: vortex-array/src/arrays/list/array.rs:224 (in validate function)

Error Message:

validity with size 26 does not match array size 40

Stack Trace:

   0: std::backtrace_rs::backtrace::libunwind::trace
   1: std::backtrace_rs::backtrace::trace_unsynchronized
   2: <std::backtrace::Backtrace>::create
   3: validate
             at ./vortex-array/src/arrays/list/array.rs:224:13
   4: try_new
             at ./vortex-array/src/arrays/list/array.rs:112:9
   5: _take<u64, u16, u16>
             at ./vortex-array/src/arrays/list/compute/take.rs:120:8
   6: take
             at ./vortex-array/src/arrays/list/compute/take.rs:48:21
   7: invoke<vortex_array::arrays::list::vtable::ListVTable>
             at ./vortex-array/src/compute/take.rs:247:17
   8: take_impl
             at ./vortex-array/src/compute/take.rs:178:38
   9: invoke
             at ./vortex-array/src/compute/take.rs:92:27
  10: invoke
             at ./vortex-array/src/compute/mod.rs:149:34
  11: take
             at ./vortex-array/src/compute/take.rs:60:10
  12: {closure#0}
             at ./vortex-array/src/arrays/struct_/compute/take.rs:42:30
  [... iterator frames ...]
  27: take
             at ./vortex-array/src/arrays/struct_/compute/take.rs:43:18
  28: invoke<vortex_array::arrays::struct_::vtable::StructVTable>
             at ./vortex-array/src/compute/take.rs:247:17
  29: take_impl
             at ./vortex-array/src/compute/take.rs:178:38
  30: invoke
             at ./vortex-array/src/compute/take.rs:92:27
  31: invoke
             at ./vortex-array/src/compute/mod.rs:149:34
  32: take
             at ./vortex-array/src/compute/take.rs:60:10
  33: run_fuzz_action
             at ./fuzz/src/array/mod.rs:545:33

Root Cause:

The fuzzer detected a bug in the list take operation at vortex-array/src/arrays/list/compute/take.rs:120. When performing a take operation on a ListArray that is nested within a StructArray, the validity array for the resulting ListArray has an incorrect size.

The issue occurs at line 120-127 of vortex-array/src/arrays/list/compute/take.rs:

Ok(ListArray::try_new(
    new_elements,
    new_offsets,
    indices_array
        .validity()
        .clone()
        .and(array.validity().clone()),
)?
.to_array())

The problem is with how the validity is computed. The code combines:

  1. indices_array.validity() - validity from the take indices array
  2. array.validity() - validity from the original list array

However, the resulting validity array ends up with size 26 (likely the size of the indices array), while the new_offsets array has size 41 (40 elements + 1 for the terminal offset), resulting in an array with logical length 40.

The validation at vortex-array/src/arrays/list/array.rs:224 correctly catches this:

vortex_ensure!(
    validity_len == offsets.len() - 1,
    "validity with size {validity_len} does not match array size {}",
    offsets.len() - 1
);

The crash input was:

  • A StructArray (len=9) containing a ListViewArray field
  • The ListViewArray contained Decimal elements
  • A Take operation with 37 indices (BitPackedArray)
  • When taking the struct, it recursively takes the list field
  • The list take produces 40 valid elements but a validity array of size 26
Debug Output
FuzzArrayAction {
    array: StructArray {
        len: 9,
        dtype: Struct(
            StructFields {
                names: FieldNames(
                    [
                        FieldName(
                            "",
                        ),
                    ],
                ),
                dtypes: [
                    FieldDType {
                        inner: Owned(
                            List(
                                Decimal(
                                    DecimalDType {
                                        precision: 10,
                                        scale: 5,
                                    },
                                    Nullable,
                                ),
                                Nullable,
                            ),
                        ),
                    },
                ],
            },
            Nullable,
        ),
        fields: [
            ListViewArray {
                dtype: List(
                    Decimal(
                        DecimalDType {
                            precision: 10,
                            scale: 5,
                        },
                        Nullable,
                    ),
                    Nullable,
                ),
                elements: DecimalArray {
                    dtype: Decimal(
                        DecimalDType {
                            precision: 10,
                            scale: 5,
                        },
                        Nullable,
                    ),
                    values: Buffer<u8> {
                        length: 72,
                        alignment: Alignment(
                            8,
                        ),
                        as_slice: [27, 4, 114, 241, 253, 255, 255, 255, 18, 52, 11, 54, 2, 0, 0, 0, ...],
                    },
                    values_type: I64,
                    validity: AllValid,
                },
                offsets: PrimitiveArray {
                    dtype: Primitive(
                        U16,
                        NonNullable,
                    ),
                    buffer: Buffer<u8> {
                        length: 18,
                        alignment: Alignment(
                            2,
                        ),
                        as_slice: [0, 0, 9, 0, 9, 0, 9, 0, 9, 0, 9, 0, 9, 0, 9, 0, ...],
                    },
                    validity: NonNullable,
                },
                sizes: PrimitiveArray {
                    dtype: Primitive(
                        U16,
                        NonNullable,
                    ),
                    buffer: Buffer<u8> {
                        length: 18,
                        alignment: Alignment(
                            2,
                        ),
                        as_slice: [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...],
                    },
                    validity: NonNullable,
                },
                is_zero_copy_to_list: true,
                validity: Array(
                    BoolArray {
                        dtype: Bool(
                            NonNullable,
                        ),
                        bits: BitBuffer {
                            buffer: Buffer<u8> {
                                length: 2,
                                alignment: Alignment(
                                    1,
                                ),
                                as_slice: [1, 0],
                            },
                            offset: 0,
                            len: 9,
                        },
                        validity: NonNullable,
                    },
                ),
            },
        ],
        validity: AllValid,
    },
    actions: [
        (
            Take(
                BitPackedArray {
                    offset: 0,
                    len: 37,
                    dtype: Primitive(
                        U64,
                        Nullable,
                    ),
                    bit_width: 0,
                    packed: Buffer<u8> {
                        length: 0,
                        alignment: Alignment(
                            8,
                        ),
                        as_slice: [],
                    },
                    patches: None,
                    validity: Array(
                        BoolArray {
                            dtype: Bool(
                                NonNullable,
                            ),
                            bits: BitBuffer {
                                buffer: Buffer<u8> {
                                    length: 5,
                                    alignment: Alignment(
                                        1,
                                    ),
                                    as_slice: [0, 0, 0, 0, 8],
                                },
                                offset: 0,
                                len: 37,
                            },
                            validity: NonNullable,
                        },
                    ),
                },
            ),
        ),
    ],
}

Summary

Reproduction

  1. Download the crash artifact:

  2. Reproduce locally:

# The artifact contains array_ops/crash-f25afd5a1813b335d85d4b020cd6675e4ed1165d
cargo +nightly fuzz run --sanitizer=none array_ops array_ops/crash-f25afd5a1813b335d85d4b020cd6675e4ed1165d
  1. Get full backtrace:
RUST_BACKTRACE=full cargo +nightly fuzz run --sanitizer=none array_ops array_ops/crash-f25afd5a1813b335d85d4b020cd6675e4ed1165d

Auto-created by fuzzing workflow with Claude analysis

Metadata

Metadata

Assignees

Labels

bugA bug issuefuzzerIssues detected by the fuzzer

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions