Skip to content

fix(temporal): guard int64 overflow in DATE/TIMESTAMP decomposition - #386

Open
belowzeroff wants to merge 1 commit into
RayforceDB:devfrom
belowzeroff:fix/temporal-extract-overflow
Open

fix(temporal): guard int64 overflow in DATE/TIMESTAMP decomposition#386
belowzeroff wants to merge 1 commit into
RayforceDB:devfrom
belowzeroff:fix/temporal-extract-overflow

Conversation

@belowzeroff

Copy link
Copy Markdown
Contributor

Problem

The DATE/TIMESTAMP → microseconds conversion shared by the temporal extract and truncate paths overflowed int64 on extreme inputs (UBSan):

(dd (as 'DATE 2147483647))
src/ops/temporal.c:105: runtime error: signed integer overflow:
  2147483647 * 86400000000 cannot be represented in type 'long long'

(mm (as 'TIMESTAMP -9223372036854775807))
src/ops/temporal.c:109: runtime error: signed integer overflow:
  9223372036854775807 + 999 cannot be represented in type 'long long'
  • DATE is int32 days, so an extreme value × µs-per-day overflows.
  • The TIMESTAMP ns→µs floor negated the input (-((-raw)+999)/1000), so a value within 999 of INT64_MIN overflowed.

Both appear in all four decomposition kernels: the standalone ray_temporal_extract / ray_temporal_truncate and the DAG exec_extract / exec_date_trunc morsel kernels (reachable via (dd d) and via dotted-path col.yyyy / col.date in select).

Fix

  • Do the TIMESTAMP ns→µs floor with truncate-then-adjust so it never negates — overflow-free and exact at INT64_MIN.
  • A DATE so extreme its µs value is not representable (hundreds of millennia out) decodes to a null instead of reading overflow garbage, consistent with how these kernels already treat a null input.
  • Truncate additionally nulls a result whose bucketed µs would overflow the int64 nanosecond output.

Behaviour

Expression Before After
(yyyy (as 'DATE 2147483647)) UB 0Nl (null)
(mm (as 'TIMESTAMP -9223372036854775807)) UB 9
(yyyy 2024.03.15) 2024 2024 (unchanged)
select d.yyyy over [0 366 2147483647] UB [2000 2001 0Nl]
select d.date over the same UB [… … 0Np]

Ordinary values are unaffected. New coverage in temporal/extract_trunc_overflow.rfl exercises the standalone and both DAG kernels with extreme DATE, minimum-edge TIMESTAMP, and ordinary values. Full make test passes (3660/3661, 1 skipped, 0 failed) under the default ASan/UBSan build.

🤖 Generated with Claude Code

The DATE/TIMESTAMP → microseconds conversion shared by the temporal
extract and truncate paths overflowed int64 on extreme inputs (UBSan):

  * DATE is int32 days, so an extreme value × µs-per-day overflowed —
    (dd (as 'DATE 2147483647)) tripped `raw * 86400000000`.
  * The ns→µs floor for TIMESTAMP negated the input, so a value within
    999 of INT64_MIN overflowed `(-raw) + 999`.
  * The DAG date_trunc YEAR/MONTH arms re-multiply days_from_civil(...) —
    a day count floored down to the period start, up to a year beyond the
    input — by µs-per-day, so a DATE that cleared the µs bound still
    overflowed (d.year of (as 'DATE -106751991)).

Both conversions appear in all four decomposition kernels: the standalone
ray_temporal_extract / ray_temporal_truncate and the DAG exec_extract /
exec_date_trunc morsel kernels.

Do the TIMESTAMP ns→µs floor with truncate-then-adjust so it never
negates (overflow-free, exact at INT64_MIN). A DATE so extreme its µs
value is not representable decodes to a null instead of reading overflow
garbage, consistent with how these kernels already treat a null input.
The truncate DATE bound is the int64-NANOSECOND representable day range
(not just the µs one) so the YEAR/MONTH re-multiply cannot overflow, and
truncate additionally nulls a result whose bucketed µs would overflow the
ns output.

Adds temporal/extract_trunc_overflow.rfl covering the standalone kernels
(yyyy/dd/mm/hh and (date …)) and the DAG kernels (dotted col.field),
including the year/month re-multiply path, large-magnitude negative DATE,
and minimum-edge TIMESTAMP.
@belowzeroff
belowzeroff force-pushed the fix/temporal-extract-overflow branch from bfa94e1 to ae215c5 Compare August 7, 2026 11:12
@belowzeroff

Copy link
Copy Markdown
Contributor Author

Addressed the rayforce-audit findings in ae215c59.

Blocking (1) — incomplete truncate guard. Correct: the exec_date_trunc YEAR/MONTH arms re-multiply days_from_civil(...) * µs-per-day on a day count floored down to the period start (up to a year beyond the input), so a DATE that cleared the µs bound (INT64_MAX / µs-per-day) still overflowed — d.year of (as 'DATE -106751991) tripped it (-106752347 * 86400000000). Tightened the IN32 DATE guard to the int64-nanosecond representable day range (INT64_MAX / 1000 / DT_USEC_PER_DAY), matching the sibling rte_trunc_elem; the re-multiply and the final ×1000 now both stay in range.

(2) — vacuous assertion. Right, -2147483648 == NULL_I32, so it exited at the null-input early-out. Replaced with -200000000 (a real non-null negative that exercises the guard's negative branch).

(3) — untested standalone truncate / overstated header. Added (date <TIMESTAMP>) / (date <DATE>) cases (ordinary → day-truncated, out-of-ns-range → null) and the DAG d.year / d.month re-multiply path with a large-magnitude negative DATE. Corrected the header to describe exactly what is covered.

(4) — conservative low-edge guard. Kept the unconditional one-bucket headroom (avoids a branch in the hot kernel) and documented that it rounds an immaterial ~1-bucket band at the extreme low edge (~292 millennia before 2000) to null.

Full make test still green (3660/3661, 1 skipped, 0 failed) under ASan/UBSan; the d.year (as 'DATE -106751991) repro no longer reports UB.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant