Skip to content

panic: multiply overflow in generate_series(DATE, DATE, INTERVAL) for dates far from 1970 #22193

Description

@Dandandan

Describe the bug

generate_series(start_date, end_date, INTERVAL ...) panics with attempt to multiply with overflow when either date is far enough from the Unix epoch (roughly before year ~1670 or after year ~2270). The panic happens during planning (when the table-valued function converts Date32 days to timestamp nanoseconds).

To Reproduce

use datafusion::prelude::SessionContext;

#[tokio::main]
async fn main() {
    let ctx = SessionContext::new();
    let _ = ctx
        .sql("SELECT * FROM generate_series(DATE '0001-01-01', DATE '2000-01-01', INTERVAL '1' DAY) LIMIT 0")
        .await
        .unwrap()
        .create_physical_plan()
        .await;
}

Panic (debug-assertions on):

thread 'main' panicked at datafusion/functions-table/src/generate_series.rs:714:24:
attempt to multiply with overflow

Also reproduces with:

  • EXPLAIN SELECT * FROM generate_series(DATE '0001-01-01', DATE '2000-01-01', INTERVAL '1' DAY)
  • EXPLAIN SELECT * FROM generate_series(DATE '0996-09-01', DATE '1992-09-03', INTERVAL '1' DAY) (original fuzzer find)

Without debug-assertions the multiplication would wrap silently and produce a wildly wrong result, which is also bad.

Expected behavior

Return a planner error explaining the requested range is out of representable bounds (or use checked arithmetic and overflow → error). The public SQL API should never panic on user-supplied SQL.

Root cause

datafusion/functions-table/src/generate_series.rs, around lines 712-715:

// Convert Date32 (days since epoch) to timestamp nanoseconds
const NANOS_PER_DAY: i64 = 24 * 60 * 60 * 1_000_000_000;   // 86_400_000_000_000

let start_ts = start_date as i64 * NANOS_PER_DAY;          // ← panics
let end_ts   = end_date   as i64 * NANOS_PER_DAY;

Date32 is i32 days since 1970-01-01, so |start_date| can be up to ~2^31. Multiplying by 86_400_000_000_000 overflows i64 once |days| exceeds i64::MAX / 86_400_000_000_000 ≈ 106_751 days (~292 years). Any date before ~year 1670 or after ~year 2270 triggers it.

Suggested fix

Use checked_mul:

let start_ts = (start_date as i64).checked_mul(NANOS_PER_DAY)
    .ok_or_else(|| plan_datafusion_err!(
        "generate_series: start date {start_date} out of representable timestamp range"
    ))?;
let end_ts = (end_date as i64).checked_mul(NANOS_PER_DAY)
    .ok_or_else(|| plan_datafusion_err!(
        "generate_series: end date {end_date} out of representable timestamp range"
    ))?;

(Or convert via Timestamp(Microsecond, _) instead of nanoseconds — the representable range there is ±290k years, well beyond Date32.)

Additional context

Found by a cargo fuzz target (fuzz/fuzz_targets/sql_physical_plan.rs) seeded with SQL from datafusion/sqllogictest/test_files/. The fuzzer mutated a small year literal into 0996, which was enough to overflow.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions