Skip to content

Fuzzing Crash: ListViewArray rebuild assertion failure - is_zero_copy_to_list() not preserved #6178

@github-actions

Description

@github-actions

Fuzzing Crash Report

Analysis

Crash Location: vortex-array/src/arrays/listview/conversion.rs:114 in the list_from_list_view function

Error Message:

assertion failed: zctl_array.is_zero_copy_to_list()

Stack Trace:

   0: __rustc::rust_begin_unwind
             at /rustc/474276961f48b0d05f4ea260ba400096b027584e/library/std/src/panicking.rs:689:5
   1: core::panicking::panic_fmt
             at /rustc/474276961f48b0d05f4ea260ba400096b027584e/library/core/src/panicking.rs:80:14
   2: core::panicking::panic
             at /rustc/474276961f48b0d05f4ea260ba400096b027584e/library/core/src/panicking.rs:150:5
   3: list_from_list_view
             at ./vortex-array/src/arrays/listview/conversion.rs:114:5
   4: compress_canonical
             at ./vortex-btrblocks/src/lib.rs:443:34
   5: compress
             at ./vortex-btrblocks/src/lib.rs:402:14
   6: compress_array
             at ./fuzz/src/array/mod.rs:524:14
   7: run_fuzz_action
             at ./fuzz/src/array/mod.rs:567:33

Root Cause:

The bug is in vortex-array/src/arrays/listview/rebuild.rs. The list_from_list_view function calls rebuild(ListViewRebuildMode::MakeExact) and asserts that the result has is_zero_copy_to_list() == true. However, the rebuild logic doesn't always preserve this property correctly.

The issue occurs in the rebuild_make_exact method (rebuild.rs:253-261):

fn rebuild_make_exact(&self) -> VortexResult<ListViewArray> {
    if self.is_zero_copy_to_list() {
        self.rebuild_trim_elements()  // Line 255
    } else {
        self.rebuild_zero_copy_to_list()
    }
}

When the array already has is_zero_copy_to_list() == true, it calls rebuild_trim_elements(). However, rebuild_trim_elements() (lines 194-251) can break the zero-copy property for edge cases like empty arrays or arrays with zero-length elements.

At line 249, rebuild_trim_elements preserves the original flag:

.with_zero_copy_to_list(self.is_zero_copy_to_list())

But when dealing with empty arrays (length 0), the calculations at lines 209-211 try to access self.offset_at(self.len() - 1) which is problematic:

let last_offset = self.offset_at(self.len() - 1);  // self.len() = 0, so this accesses index -1!
let last_size = self.size_at(self.len() - 1);

Array Structure from Debug Output:

  • Nested ListViewArray with List(List(Decimal(...))) dtype
  • Both outer and inner ListViewArray have empty buffers (length: 0)
  • Both have is_zero_copy_to_list: true set
  • The inner elements are a DecimalArray with empty values buffer
  • Empty offsets and sizes arrays for both levels

The crash occurs when BtrBlocks compression tries to convert this nested empty ListViewArray to a ListArray via list_from_list_view.

Debug Output
FuzzArrayAction {
    array: ListViewArray {
        dtype: List(
            List(
                Decimal(
                    DecimalDType {
                        precision: 22,
                        scale: -49,
                    },
                    Nullable,
                ),
                NonNullable,
            ),
            NonNullable,
        ),
        elements: ListViewArray {
            dtype: List(
                Decimal(
                    DecimalDType {
                        precision: 22,
                        scale: -49,
                    },
                    Nullable,
                ),
                NonNullable,
            ),
            elements: DecimalArray {
                dtype: Decimal(
                    DecimalDType {
                        precision: 22,
                        scale: -49,
                    },
                    Nullable,
                ),
                values: BufferHandle(
                    Host(
                        Buffer<u8> {
                            length: 0,
                            alignment: Alignment(
                                16,
                            ),
                            as_slice: [],
                        },
                    ),
                ),
                values_type: I128,
                validity: AllValid,
                stats_set: ArrayStats {
                    inner: RwLock {
                        data: StatsSet {
                            values: [],
                        },
                    },
                },
            },
            offsets: PrimitiveArray {
                dtype: Primitive(
                    U64,
                    NonNullable,
                ),
                buffer: BufferHandle(
                    Host(
                        Buffer<u8> {
                            length: 0,
                            alignment: Alignment(
                                8,
                            ),
                            as_slice: [],
                        },
                    ),
                ),
                validity: NonNullable,
                stats_set: ArrayStats {
                    inner: RwLock {
                        data: StatsSet {
                            values: [
                                (
                                    IsSorted,
                                    Exact(
                                        ScalarValue(
                                            Bool(
                                                true,
                                            ),
                                        ),
                                    ),
                                ),
                            ],
                        },
                    },
                },
            },
            sizes: PrimitiveArray {
                dtype: Primitive(
                    U64,
                    NonNullable,
                ),
                buffer: BufferHandle(
                    Host(
                        Buffer<u8> {
                            length: 0,
                            alignment: Alignment(
                                8,
                            ),
                            as_slice: [],
                        },
                    ),
                ),
                validity: NonNullable,
                stats_set: ArrayStats {
                    inner: RwLock {
                        data: StatsSet {
                            values: [],
                        },
                    },
                },
            },
            is_zero_copy_to_list: true,
            validity: NonNullable,
            stats_set: ArrayStats {
                inner: RwLock {
                    data: StatsSet {
                        values: [],
                    },
                },
            },
        },
        offsets: PrimitiveArray {
            dtype: Primitive(
                I16,
                NonNullable,
            ),
            buffer: BufferHandle(
                Host(
                    Buffer<u8> {
                        length: 0,
                        alignment: Alignment(
                            2,
                        ),
                        as_slice: [],
                    },
                ),
            ),
            validity: NonNullable,
            stats_set: ArrayStats {
                inner: RwLock {
                    data: StatsSet {
                        values: [
                            (
                                IsSorted,
                                Exact(
                                    ScalarValue(
                                        Bool(
                                            true,
                                        ),
                                    ),
                                ),
                            ),
                        ],
                    },
                },
            },
        },
        sizes: PrimitiveArray {
            dtype: Primitive(
                I16,
                NonNullable,
            ),
            buffer: BufferHandle(
                Host(
                    Buffer<u8> {
                        length: 0,
                        alignment: Alignment(
                            2,
                        ),
                        as_slice: [],
                    },
                ),
            ),
            validity: NonNullable,
            stats_set: ArrayStats {
                inner: RwLock {
                    data: StatsSet {
                        values: [],
                    },
                },
            },
        },
        is_zero_copy_to_list: true,
        validity: NonNullable,
        stats_set: ArrayStats {
            inner: RwLock {
                data: StatsSet {
                    values: [],
                },
            },
        },
    },
    actions: [
        Compress(Default)
    ]
}

Summary

Reproduction

  1. Download the crash artifact:

  2. Reproduce locally:

# The artifact contains array_ops/crash-608dbf18d00107ec105f15bdd29c17a20a5be099
cargo +nightly fuzz run -D --sanitizer=none array_ops array_ops/crash-608dbf18d00107ec105f15bdd29c17a20a5be099 -- -rss_limit_mb=0
  1. Get full backtrace:
RUST_BACKTRACE=full cargo +nightly fuzz run -D --sanitizer=none array_ops array_ops/crash-608dbf18d00107ec105f15bdd29c17a20a5be099 -- -rss_limit_mb=0

Auto-created by fuzzing workflow with Claude analysis

Metadata

Metadata

Assignees

No one assigned

    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