Skip to content

Fuzzing Crash: DecimalArray fill_null panics on null scalar value #5807

@github-actions

Description

@github-actions

Fuzzing Crash Report

Analysis

Crash Location: vortex-array/src/arrays/decimal/compute/fill_null.rs:35 in the fill_null method

Error Message:

panicked at vortex-error/src/lib.rs:371:13:
top-level fill_null ensure non-null fill value

Stack Trace:

#0  fill_null at ./vortex-array/src/arrays/decimal/compute/fill_null.rs:35:26
#1  invoke<vortex_array::arrays::decimal::vtable::DecimalVTable> at ./vortex-array/src/compute/fill_null.rs:84:13
#2  invoke at ./vortex-array/src/compute/fill_null.rs:114:42
#3  invoke at ./vortex-array/src/compute/mod.rs:149:34
#4  fill_null at ./vortex-array/src/compute/fill_null.rs:54:10
#5  run_fuzz_action at ./fuzz/src/array/mod.rs:594:33
#6  __libfuzzer_sys_run at ./fuzz/fuzz_targets/array_ops.rs:14:11

Root Cause: The fill_null operation for DecimalArray assumes that the fill value scalar is non-null, but the fuzzer discovered a case where a scalar with NonNullable dtype actually contains a null value. At line 35, the code calls:

let fill_value = fill_value
    .as_decimal()
    .decimal_value()
    .and_then(|v| v.cast::<T>())
    .vortex_expect("top-level fill_null ensure non-null fill value");

The .decimal_value() method returns None when the scalar value is null, causing the panic. This reveals an inconsistency where a scalar can have a NonNullable dtype but still contain a null value, or a validation gap in the fill_null entry point that should reject null fill values.

Debug Output
Output of `std::fmt::Debug`:

FuzzArrayAction {
    array: DecimalArray {
        dtype: Decimal(
            DecimalDType {
                precision: 19,
                scale: -61,
            },
            NonNullable,
        ),
        values: Buffer<u8> {
            length: 16,
            alignment: Alignment(
                16,
            ),
            as_slice: [60, 59, 83, 23, 252, 220, 7, 68, 0, 0, 0, 0, 0, 0, 0, 0],
        },
        values_type: I128,
        validity: NonNullable,
        stats_set: ArrayStats {
            inner: RwLock {
                data: StatsSet {
                    values: [],
                },
            },
        },
    },
    actions: [
        ...
        (
            FillNull(
                Scalar {
                    dtype: Decimal(
                        DecimalDType {
                            precision: 19,
                            scale: -61,
                        },
                        NonNullable,
                    ),
                    value: ScalarValue(
                        Decimal(
                            I128(
                                -9999999999999999999,
                            ),
                        ),
                    ),
                },
            ),
            ...
        ),
    ],
}

Summary

Reproduction

  1. Download the crash artifact:

  2. Reproduce locally:

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

Auto-created by fuzzing workflow with Claude analysis

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions