Skip to content

Fuzzing Crash: Sum operation returns null on i256 overflow for DecimalArray (precision=76) #6352

@github-actions

Description

@github-actions

Fuzzing Crash Report

Analysis

Crash Location: fuzz/fuzz_targets/array_ops.rs:18 (panicked via assert_scalar_eq at fuzz/src/array/mod.rs:750)

Error Message:

Scalar mismatch: expected decimal256(10335731459288881875449326843340756734051447447459996202947475384727173595149, precision=76, scale=75), got null in step 2

Stack Trace:

   0: std::backtrace_rs::backtrace::libunwind::trace
   1: std::backtrace_rs::backtrace::trace_unsynchronized
   2: <std::backtrace::Backtrace>::create
   3: assert_scalar_eq
             at ./fuzz/src/array/mod.rs:750:13
   4: run_fuzz_action
             at ./fuzz/src/array/mod.rs:627:17
   5: __libfuzzer_sys_run
             at ./fuzz/fuzz_targets/array_ops.rs:14:11

Root Cause:

The fuzzer discovered an overflow issue in the sum operation for DecimalArray with very high precision (76). The sequence is:

  1. Start with a ChunkedArray containing DecimalArrays with dtype decimal(76, 75)
  2. Apply a mask operation (step 1) - reduces array to 9 values with some values masked out
  3. Apply a sum operation (step 2) - tries to sum the remaining decimal values

The sum operation detects an i256 overflow during the checked addition and returns Scalar::null instead of the actual sum value (see vortex-array/src/arrays/decimal/compute/sum.rs:64,88,104,118 - when CheckedAdd::checked_add returns None, it returns Scalar::null).

However, the expected behavior from the fuzzer's perspective is that the sum should either:

  • Return the correct sum value if it fits in i256, OR
  • Panic/error gracefully if overflow is detected

But currently it silently returns null, which causes a scalar mismatch when compared to the expected value computed by a reference implementation.

Array Structure:

  • ChunkedArray (length 12) with dtype: Decimal(precision=76, scale=75, Nullable)
  • 2 chunks: DecimalArray with 4 elements, DecimalArray with 8 elements
  • Values stored as I256 type
  • After mask: 9 valid values remain (mask: [248, 15] = bits 11111000 11110000 reversed = 9 true bits out of 12)
  • Sum of these 9 large i256 values causes overflow

This is related to issue #5820 (DecimalArray logical/physical type mismatch) but represents a different failure mode - overflow handling in aggregation operations.

Debug Output
FuzzArrayAction {
    array: ChunkedArray {
        dtype: Decimal(
            DecimalDType {
                precision: 76,
                scale: 75,
            },
            Nullable,
        ),
        len: 12,
        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, 4, 0, 0, 0, 0, 0, 0, 0, ...],
                    },
                ),
            ),
            validity: NonNullable,
            stats_set: ArrayStats {
                inner: RwLock {
                    data: StatsSet {
                        values: [],
                    },
                },
            },
        },
        chunks: [
            DecimalArray {
                dtype: Decimal(
                    DecimalDType {
                        precision: 76,
                        scale: 75,
                    },
                    Nullable,
                ),
                values: BufferHandle(
                    Host(
                        Buffer<u8> {
                            length: 128,
                            alignment: Alignment(
                                16,
                            ),
                            as_slice: [3, 0, 0, 0, 0, 0, 0, 0, 0, 144, 236, 228, 101, 118, 200, 187, ...],
                        },
                    ),
                ),
                values_type: I256,
                validity: AllValid,
                stats_set: ArrayStats {
                    inner: RwLock {
                        data: StatsSet {
                            values: [],
                        },
                    },
                },
            },
            DecimalArray {
                dtype: Decimal(
                    DecimalDType {
                        precision: 76,
                        scale: 75,
                    },
                    Nullable,
                ),
                values: BufferHandle(
                    Host(
                        Buffer<u8> {
                            length: 256,
                            alignment: Alignment(
                                16,
                            ),
                            as_slice: [1, 0, 0, 0, 0, 0, 0, 0, 0, 208, 64, 171, 43, 14, 159, 225, ...],
                        },
                    ),
                ),
                values_type: I256,
                validity: AllValid,
                stats_set: ArrayStats {
                    inner: RwLock {
                        data: StatsSet {
                            values: [],
                        },
                    },
                },
            },
        ],
        stats_set: ArrayStats {
            inner: RwLock {
                data: StatsSet {
                    values: [],
                },
            },
        },
    },
    actions: [
        (
            Mask(
                Values(
                    MaskValues {
                        buffer: BitBuffer {
                            buffer: Buffer<u8> {
                                length: 2,
                                alignment: Alignment(
                                    1,
                                ),
                                as_slice: [248, 15],
                            },
                            offset: 0,
                            len: 12,
                        },
                        indices: OnceLock(
                            <uninit>,
                        ),
                        slices: OnceLock(
                            <uninit>,
                        ),
                        true_count: 9,
                        density: 0.75,
                    },
                ),
            ),
            Array(
                DecimalArray {
                    dtype: Decimal(
                        DecimalDType {
                            precision: 76,
                            scale: 75,
                        },
                        Nullable,
                    ),
                    values: BufferHandle(
                        Host(
                            Buffer<u8> {
                                length: 384,
                                alignment: Alignment(
                                    16,
                                ),
                                as_slice: [3, 0, 0, 0, 0, 0, 0, 0, 0, 144, 236, 228, 101, 118, 200, 187, ...],
                            },
                        ),
                    ),
                    values_type: I256,
                    validity: Array(
                        BoolArray {
                            dtype: Bool(
                                NonNullable,
                            ),
                            bits: BufferHandle(
                                Host(
                                    Buffer<u8> {
                                        length: 2,
                                        alignment: Alignment(
                                            1,
                                        ),
                                        as_slice: [7, 240],
                                    },
                                ),
                            ),
                            offset: 0,
                            len: 12,
                            validity: NonNullable,
                            stats_set: ArrayStats {
                                inner: RwLock {
                                    data: StatsSet {
                                        values: [
                                            (
                                                NullCount,
                                                Exact(
                                                    ScalarValue(
                                                        Primitive(
                                                            U64(
                                                                0,
                                                            ),
                                                        ),
                                                    ),
                                                ),
                                            ),
                                            (
                                                Min,
                                                Exact(
                                                    ScalarValue(
                                                        Bool(
                                                            false,
                                                        ),
                                                    ),
                                                ),
                                            ),
                                            (
                                                Max,
                                                Exact(
                                                    ScalarValue(
                                                        Bool(
                                                            true,
                                                        ),
                                                    ),
                                                ),
                                            ),
                                        ],
                                    },
                                },
                            },
                        },
                    ),
                    stats_set: ArrayStats {
                        inner: RwLock {
                            data: StatsSet {
                                values: [],
                            },
                        },
                    },
                },
            ),
        ),
        (
            Sum,
            Scalar(
                Scalar {
                    dtype: Decimal(
                        DecimalDType {
                            precision: 76,
                            scale: 75,
                        },
                        Nullable,
                    ),
                    value: ScalarValue(
                        Decimal(
                            I256(
                                i256(
                                    10335731459288881875449326843340756734051447447459996202947475384727173595149,
                                ),
                            ),
                        ),
                    ),
                },
            ),
        ),
        (
            Sum,
            Scalar(
                Scalar {
                    dtype: Decimal(
                        DecimalDType {
                            precision: 76,
                            scale: 75,
                        },
                        Nullable,
                    ),
                    value: ScalarValue(
                        Decimal(
                            I256(
                                i256(
                                    10335731459288881875449326843340756734051447447459996202947475384727173595149,
                                ),
                            ),
                        ),
                    ),
                },
            ),
        ),
    ],
}

Summary

Reproduction

  1. Download the crash artifact:

  2. Reproduce locally:

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