Skip to content

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

@github-actions

Description

@github-actions

Fuzzing Crash Report

Analysis

Crash Location: vortex-array/src/validity.rs:281 in the Validity::patch function

Error Message:

compare operation should succeed in fuzz test:
  Can't patch a nullable validity with non-nullable validity

Stack Trace:

   3: patch
             at ./vortex-array/src/validity.rs:281:17
   4: patch
             at ./vortex-array/src/arrays/primitive/array/patch.rs:25:56
   5: decompress_unchunked_core
             at ./encodings/alp/src/alp/decompress.rs:170:17
   6: execute_decompress
             at ./encodings/alp/src/alp/decompress.rs:89:9
   7: execute
             at ./encodings/alp/src/alp/array.rs:171:33
   8: execute_canonical<vortex_alp::alp::array::ALPVTable>
             at ./vortex-array/src/vtable/dyn_.rs:156:22
   9: execute
             at ./vortex-array/src/executor.rs:98:24
  10: execute<vortex_array::canonical::Canonical>
             at ./vortex-array/src/executor.rs:43:9
  11: execute
             at ./vortex-array/src/expr/vtable.rs:342:50
  12: execute<vortex_array::expr::vtable::ExecutionResult>
             at ./vortex-array/src/executor.rs:43:9
  13: execute
             at ./vortex-array/src/expr/exprs/cast.rs:87:53
  14: execute<vortex_array::expr::exprs::cast::Cast>
             at ./vortex-array/src/expr/vtable.rs:550:22

Root Cause:

The fuzzer discovered a bug in the ALP decompression logic where patching fails due to validity incompatibility. This is the inverse of issue #5766.

The issue occurs when:

  1. An ALP-encoded ChunkedArray with F32 NonNullable dtype is decompressed
  2. The base decoded array has nullable validity (either AllValid with nullable dtype or an explicit validity array)
  3. The patches array has NonNullable validity
  4. When attempting to apply patches at encodings/alp/src/alp/decompress.rs:170, the code calls decoded.patch(&patches)
  5. This calls Validity::patch() at line 25 of patch.rs, which then fails at validity.rs:281

Looking at vortex-array/src/validity.rs:275-282:

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

The panic happens during a compare operation in the fuzzer's file I/O test at file_io.rs:115-116. The compare operation triggers Arrow conversion, which executes the ALP-encoded array, triggering decompression with incompatible validities.

Array Structure:

  • ChunkedArray with dtype Primitive(F32, NonNullable), length 5
  • 2 chunks: PrimitiveArray(length=1) and PrimitiveArray(length=4)
  • Both chunks have NonNullable validity
  • After compression and file I/O, the decompression creates a mismatch

Relationship to Other Issues:

Debug Output
FuzzFileAction {
    array: ChunkedArray {
        dtype: Primitive(
            F32,
            NonNullable,
        ),
        len: 5,
        chunk_offsets: PrimitiveArray {
            dtype: Primitive(
                U64,
                NonNullable,
            ),
            buffer: BufferHandle(
                Host(
                    Buffer<u8> {
                        length: 24,
                        alignment: Alignment(
                            8,
                        ),
                        as_slice: [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, ...],
                    },
                ),
            ),
            validity: NonNullable,
            stats_set: ArrayStats {
                inner: RwLock {
                    data: StatsSet {
                        values: [],
                    },
                },
            },
        },
        chunks: [
            PrimitiveArray {
                dtype: Primitive(
                    F32,
                    NonNullable,
                ),
                buffer: BufferHandle(
                    Host(
                        Buffer<u8> {
                            length: 4,
                            alignment: Alignment(
                                4,
                            ),
                            as_slice: [1, 0, 193, 65],
                        },
                    ),
                ),
                validity: NonNullable,
                stats_set: ArrayStats {
                    inner: RwLock {
                        data: StatsSet {
                            values: [],
                        },
                    },
                },
            },
            PrimitiveArray {
                dtype: Primitive(
                    F32,
                    NonNullable,
                ),
                buffer: BufferHandle(
                    Host(
                        Buffer<u8> {
                            length: 16,
                            alignment: Alignment(
                                4,
                            ),
                            as_slice: [193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193],
                        },
                    ),
                ),
                validity: NonNullable,
                stats_set: ArrayStats {
                    inner: RwLock {
                        data: StatsSet {
                            values: [],
                        },
                    },
                },
            },
        ],
        stats_set: ArrayStats {
            inner: RwLock {
                data: StatsSet {
                    values: [],
                },
            },
        },
    },
    projection_expr: None,
    filter_expr: None,
    compressor_strategy: Default,
}

Summary

Reproduction

  1. Download the crash artifact:

  2. Reproduce locally:

# The artifact contains file_io/crash-29298272e502730f91c1ff3b4c9b7a8c35932aca
cargo +nightly fuzz run -D --sanitizer=none file_io file_io/crash-29298272e502730f91c1ff3b4c9b7a8c35932aca -- -rss_limit_mb=0
  1. Get full backtrace:
RUST_BACKTRACE=full cargo +nightly fuzz run -D --sanitizer=none file_io file_io/crash-29298272e502730f91c1ff3b4c9b7a8c35932aca -- -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