Skip to content

Fuzzing Crash: Validity patch fails in ALP decompression (non-nullable base with nullable patches) #5766

@github-actions

Description

@github-actions

Fuzzing Crash Report

Analysis

Crash Location: vortex-array/src/validity.rs:269 (in Validity::patch function)

Error Message:

Can't patch a non-nullable validity with nullable validity

Stack Trace:

   3: patch
             at ./vortex-array/src/validity.rs:269:17
   4: patch
             at ./vortex-array/src/arrays/primitive/array/patch.rs:21:56
   5: decompress_unchunked
             at ./encodings/alp/src/alp/decompress.rs:188:17
   6: decompress_into_array
             at ./encodings/alp/src/alp/decompress.rs:46:9
   7: canonicalize
             at ./encodings/alp/src/alp/array.rs:470:30
   8: to_canonical<vortex_alp::alp::array::ALPVTable>
             at ./vortex-array/src/array/mod.rs:611:25
   9: to_primitive<dyn vortex_array::array::Array>
             at ./vortex-array/src/canonical.rs:354:14
  10: sort_canonical_array
             at ./fuzz/src/array/sort.rs:38:41
  11: run_fuzz_action
             at ./fuzz/src/array/mod.rs:549:34

Root Cause:

The fuzzer discovered a bug in the ALP decompression logic where patching fails due to validity incompatibility. The issue occurs when:

  1. An ALP-encoded ChunkedArray with F32 Nullable dtype is decompressed
  2. The base decoded array has AllValid validity (non-nullable)
  3. The patches array has nullable validity
  4. When attempting to apply patches at encodings/alp/src/alp/decompress.rs:188, the code calls decoded.patch(&patches)
  5. This calls Validity::patch() which panics because it doesn't allow patching a non-nullable validity with nullable validity

Looking at vortex-array/src/validity.rs:266-273:

match (&self, patches) {
    (Validity::NonNullable, Validity::NonNullable) => return Validity::NonNullable,
    (Validity::NonNullable, _) => {
        vortex_panic!("Can't patch a non-nullable validity with nullable validity")  // Line 269
    },
    (_, Validity::NonNullable) => {
        vortex_panic!("Can't patch a nullable validity with non-nullable validity")
    },
    ...
}

The issue is that Validity::AllValid is treated as non-nullable, but when the patches have nullable values, the patching operation should be allowed and should produce a nullable result.

The operation sequence that triggered this:

  1. Initial ChunkedArray with F32 Nullable dtype, containing 2 chunks
  2. Compress operation
  3. SearchSorted operation
  4. FillNull operation (which triggers sort → canonicalization → ALP decompression → crash)
Debug Output
FuzzArrayAction {
    array: ChunkedArray {
        dtype: Primitive(
            F32,
            Nullable,
        ),
        len: 10,
        chunk_offsets: PrimitiveArray {
            dtype: Primitive(
                U64,
                NonNullable,
            ),
            buffer: Buffer<u8> {
                length: 24,
                alignment: Alignment(
                    8,
                ),
                as_slice: [0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, ...],
            },
            validity: NonNullable,
            stats_set: ArrayStats {
                inner: RwLock {
                    data: StatsSet {
                        values: [],
                    },
                },
            },
        },
        chunks: [
            PrimitiveArray {
                dtype: Primitive(
                    F32,
                    Nullable,
                ),
                buffer: Buffer<u8> {
                    length: 40,
                    alignment: Alignment(
                        4,
                    ),
                    as_slice: [255, 255, 71, 65, 69, 69, 69, 69, 122, 123, 123, 123, 123, 123, 123, 71, ...],
                },
                validity: AllValid,
                stats_set: ArrayStats {
                    inner: RwLock {
                        data: StatsSet {
                            values: [],
                        },
                    },
                },
            },
            PrimitiveArray {
                dtype: Primitive(
                    F32,
                    Nullable,
                ),
                buffer: Buffer<u8> {
                    length: 0,
                    alignment: Alignment(
                        4,
                    ),
                    as_slice: [],
                },
                validity: AllValid,
                stats_set: ArrayStats {
                    inner: RwLock {
                        data: StatsSet {
                            values: [],
                        },
                    },
                },
            },
        ],
        stats_set: ArrayStats {
            inner: RwLock {
                data: StatsSet {
                    values: [],
                },
            },
        },
    },
    actions: [
        (
            Compress(
                Default,
            ),
            Array(
                ChunkedArray {
                    dtype: Primitive(
                        F32,
                        Nullable,
                    ),
                    len: 10,
                    chunk_offsets: PrimitiveArray {
                        dtype: Primitive(
                            U64,
                            NonNullable,
                        ),
                        buffer: Buffer<u8> {
                            length: 24,
                            alignment: Alignment(
                                8,
                            ),
                            as_slice: [0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, ...],
                        },
                        validity: NonNullable,
                        stats_set: ArrayStats {
                            inner: RwLock {
                                data: StatsSet {
                                    values: [],
                                },
                            },
                        },
                    },
                    chunks: [
                        PrimitiveArray {
                            dtype: Primitive(
                                F32,
                                Nullable,
                            ),
                            buffer: Buffer<u8> {
                                length: 40,
                                alignment: Alignment(
                                    4,
                                ),
                                as_slice: [255, 255, 71, 65, 69, 69, 69, 69, 122, 123, 123, 123, 123, 123, 123, 71, ...],
                            },
                            validity: AllValid,
                            stats_set: ArrayStats {
                                inner: RwLock {
                                    data: StatsSet {
                                    values: [],
                                    },
                                },
                            },
                        },
                        PrimitiveArray {
                            dtype: Primitive(
                                F32,
                                Nullable,
                            ),
                            buffer: Buffer<u8> {
                                length: 0,
                                alignment: Alignment(
                                    4,
                                ),
                                as_slice: [],
                            },
                            validity: AllValid,
                            stats_set: ArrayStats {
                                inner: RwLock {
                                    data: StatsSet {
                                        values: [],
                                    },
                                },
                            },
                        },
                    ],
                    stats_set: ArrayStats {
                        inner: RwLock {
                            data: StatsSet {
                                values: [],
                            },
                        },
                    },
                },
            ),
        ),
        (
            SearchSorted(
                Scalar {
                    dtype: Primitive(
                        F32,
                        Nullable,
                    ),
                    value: ScalarValue(
                        Primitive(
                            F32(
                                1.3057709e36,
                            ),
                        ),
                    ),
                },
                Left,
            ),
            Search(
                Found(
                    9,
                ),
            ),
        ),
        (
            FillNull(
                Scalar {
                    dtype: Primitive(
                        F32,
                        NonNullable,
                    ),
                    value: ScalarValue(
                        Primitive(
                            F32(
                                7.4028235e-39,
                            ),
                        ),
                    ),
                },
            ),
            Array(
                ChunkedArray {
                    dtype: Primitive(
                        F32,
                        NonNullable,
                    ),
                    len: 10,
                    chunk_offsets: PrimitiveArray {
                        dtype: Primitive(
                            U64,
                            NonNullable,
                        ),
                        buffer: Buffer<u8> {
                            length: 24,
                            alignment: Alignment(
                                8,
                            ),
                            as_slice: [0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, ...],
                        },
                        validity: NonNullable,
                        stats_set: ArrayStats {
                            inner: RwLock {
                                data: StatsSet {
                                    values: [],
                                },
                            },
                        },
                    },
                    chunks: [
                        PrimitiveArray {
                            dtype: Primitive(
                                F32,
                                NonNullable,
                            ),
                            buffer: Buffer<u8> {
                                length: 40,
                                alignment: Alignment(
                                    4,
                                ),
                                as_slice: [255, 255, 71, 65, 69, 69, 69, 69, 122, 123, 123, 123, 123, 123, 123, 71, ...],
                            },
                            validity: NonNullable,
                            stats_set: ArrayStats {
                                inner: RwLock {
                                    data: StatsSet {
                                        values: [],
                                    },
                                },
                            },
                        },
                        PrimitiveArray {
                            dtype: Primitive(
                                F32,
                                NonNullable,
                            ),
                            buffer: Buffer<u8> {
                                length: 0,
                                alignment: Alignment(
                                    4,
                                ),
                                as_slice: [],
                            },
                            validity: NonNullable,
                            stats_set: ArrayStats {
                                inner: RwLock {
                                    data: StatsSet {
                                        values: [],
                                    },
                                },
                            },
                        },
                    ],
                    stats_set: ArrayStats {
                        inner: RwLock {
                            data: StatsSet {
                                values: [],
                            },
                        },
                    },
                },
            ),
        ),
    ],
}

Summary

Reproduction

  1. Download the crash artifact:

  2. Reproduce locally:

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

Auto-created by fuzzing workflow with Claude analysis

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions