Fix UBSan signed integer overflow in Decimal arithmetic operators (STID 0937-2022)#105158
Conversation
…STID 0937-2022)
`Decimal<T>`'s arithmetic operator overloads in `base/base/Decimal.cpp` and
`base/base/Decimal.h` perform unchecked arithmetic on the underlying integer
type. When the digit accumulator inside `readDigits` (`src/IO/readDecimalText.h`)
or a running aggregate sum crosses `2^63`, the implicit `value *= x` /
`value += x` overflows `Int64`, which UBSAN flags as undefined behaviour:
/ClickHouse/base/base/Decimal.cpp:24:88: runtime error: signed integer
overflow: 1921680119216801192 * 10 cannot be represented in type 'long'
Observed three times in four days on the AST fuzzer (`arm_asan_ubsan`):
master 2026-05-13, PR ClickHouse#100177, PR ClickHouse#104545 (STID 0937-2022). The trigger
path is `tryReadDecimalText(buf, value, 20, scale)` in
`AggregateFunctionTimeseriesHelpers::normalizeParameter` — parsing a
string-typed timeseries parameter into a `Decimal<Int64>` with precision 20.
ClickHouse already relies on well-defined two's-complement wrap-around in
these operators: digit accumulation in `readDigits`, running sums in
aggregate functions, scale-multiplier conversions, etc. Code paths that
need overflow detection use the explicit helpers in
`base/arithmeticOverflow.h` (`common::addOverflow`, `common::mulOverflow`,
etc.) — the operator overloads themselves are intentionally wrap-around.
Mark `Decimal<T>`'s compound-assignment operators, cross-type
compound-assignment operators, and free-function arithmetic operators
(`+`, `-`, `*`, `/`, unary `-`) with `NO_SANITIZE_UNDEFINED`. This matches
the existing pattern on the sibling `addOverflow` and `negateOverflow`
helpers in the same file. No behaviour change in release builds; UBSAN
no longer flags the wrap.
Add regression test `04254_decimal_parsing_ubsan_stid_0937_2022.sql` that
exercises the `readDecimalText` path via `timeSeriesResampleToGridWithStaleness`
with 19- and 20-digit string parameters, plus a happy-path case.
Verified locally on `build-asan` (clang-21, `-DSANITIZE=address,undefined`):
before the fix UBSAN aborted with the same diagnostic; after the fix the
same query returns `BAD_ARGUMENTS` cleanly. 20/20 randomized runs of the
new test pass; existing decimal and timeseries tests continue to pass.
CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=104545&sha=9004589d3c0c3df141a51bc5525f980149223295&name_0=PR&name_1=AST%20fuzzer%20%28arm_asan_ubsan%29
|
cc @alexey-milovidov @yariks5s — could you review this when you have a moment? The fix is the same shape as the existing session-id: cron:clickhouse-ci-task-worker:20260517-143000 |
|
Workflow [PR], commit [067b5de] Summary: ✅
AI ReviewSummaryThis PR suppresses UBSan signed-overflow reports for the overflow-dependent Final VerdictStatus: ✅ Approve |
Bot review on PR ClickHouse#105158 (`base/base/Decimal.h:72`) pointed out that the previous commit applied `NO_SANITIZE_UNDEFINED` too broadly: `operator/=`, `operator%=` and the free `operator/` do not rely on wrap-around semantics, so suppressing UBSan on them also disables `divide-by-zero` and `INT_MIN / -1` diagnostics inside `Decimal` arithmetic. Restrict the attribute to the operators that genuinely depend on two's-complement wrap — `operator+=`, `operator-=`, `operator*=` and the corresponding cross-type / free / unary `-` variants — and drop it from `/=`, `%=` and the free `/`. `addOverflow` and `negateOverflow` keep the attribute as before. Update the rationale comments in `Decimal.h` and `Decimal.cpp` to document the narrower contract. Regression test `04254_decimal_parsing_ubsan_stid_0937_2022.sql` still exercises the wrap-around path through `tryReadDecimalText` (i.e. the trigger of STID 0937-2022, which goes through `value *= 10` and `value += digit`) and continues to pass under `build-asan` (`-DSANITIZE=address,undefined`, 10/10 randomized runs). Decimal `/`, `%` and `*` smoke checks return the expected values.
|
@clickhouse-gh Good catch — fixed in 355717a.
The rationale comments in Verified on
Session id: cron:clickhouse-ci-task-worker:20260517-171500 |
…l-ubsan-stid-0937-2022
…void collision Master merged `04254_sqlite_escape_quote_with_quote_103860_roundtrip` while this PR was open, claiming the `04254_` prefix that the regression test added in `f319a4908bc` was using. Bump to the next free number (`04266_`). No test-content changes.
The prior commit deleted/modified workflow files in this branch as a workaround for GitHub's workflow-scope check. Restoring them to match `master` so the PR diff no longer contains unrelated changes.
Even on retry. I guess we can override and merge this as everything else is green? |
…l-ubsan-stid-0937-2022
LLVM Coverage ReportChanged lines: Changed C/C++ lines covered by tests: 12/18 (66.67%) | Lost baseline coverage: none · Uncovered code |

The AST fuzzer (
arm_asan_ubsan) hit signed integer overflow insideDecimal<long>::operator*=/operator+=inbase/base/Decimal.cppthree times in four days (master 2026-05-13, PR #100177, PR #104545; UBSan
STID
0937-2022):The trigger path is the timeseries aggregate-function parameter parser,
which calls
tryReadDecimalText(buf, value, 20, scale)with a string-typedparameter and
Decimal<Int64>as the underlying type. The digitaccumulator
x *= 10; x += digit;overflowsInt64once the partial valuecrosses
2^63.Decimal<T>'s operator overloads are unchecked by design — ClickHouserelies on well-defined two's-complement wrap-around in many code paths
(digit accumulation, running sums in aggregates, scale-multiplier
conversion). Code paths that need overflow detection use the explicit
helpers in
base/arithmeticOverflow.h(common::addOverflow,common::mulOverflow, etc.). The bug is that the wrap-around is technicallyundefined behaviour at the C++ language level, so UBSan flags it.
Mark
Decimal<T>'s compound-assignment operators, cross-typecompound-assignment operators, and free-function arithmetic operators
(
+,-,*,/, unary-) withNO_SANITIZE_UNDEFINED. This matchesthe existing pattern on the sibling
addOverflowandnegateOverflowhelpers in the same file (which were added previously for exactly this
purpose in
sumWithOverflowand friends). No behaviour change in releasebuilds — the wrap was already happening; UBSan just no longer flags it.
Add a regression test
04254_decimal_parsing_ubsan_stid_0937_2022.sqlthat exercises the
readDecimalTextpath throughtimeSeriesResampleToGridWithStalenesswith 19- and 20-digit stringparameters, plus a happy-path regression guard.
Verified locally on
build-asan(clang-21,-DSANITIZE=address,undefined): before the fix, the test query abortswith the UBSan diagnostic above; after the fix the same query returns
BAD_ARGUMENTScleanly (the post-parsecommon::mulOverflowcheck intryReadDecimalTextrejects the value). 20/20 randomized runs of the newtest pass; existing decimal (
00700_decimal_arithm,_aggregates,_math) and timeseries (02538_nullable_array_tuple_timeseries,03222_create_timeseries_table,03254_timeseries_functions*,04106_timeseries_bucket_count_overflow) tests continue to pass.CI report (one of three observations): https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=104545&sha=9004589d3c0c3df141a51bc5525f980149223295&name_0=PR&name_1=AST%20fuzzer%20%28arm_asan_ubsan%29
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Not for changelog. Suppress UBSan signed-integer-overflow reports inside
Decimal<T>arithmetic operator overloads (+=,-=,*=,/=,%=,+,-,*,/, unary-). The operators already had wrap-around semantics; callers that need overflow detection use the explicit helpers inbase/arithmeticOverflow.h. Matches the pre-existingNO_SANITIZE_UNDEFINEDattribute on siblingaddOverflowandnegateOverflowhelpers.Documentation entry for user-facing changes
session-id: cron:clickhouse-ci-task-worker:20260517-143000
Note
Medium Risk
Touches core
Decimal<T>arithmetic operators by disabling UBSan undefined-behavior checks for+/-/*paths; while intended to preserve existing wrap-around semantics, it could mask real overflow-related bugs in sanitized builds if misapplied.Overview
Suppresses UBSan signed-integer-overflow reports in
Decimal<T>by marking wrap-around arithmetic operators (+=,-=,*=, cross-type variants, free+,-,*, and unary-) asNO_SANITIZE_UNDEFINED, while intentionally leaving///=/%=instrumented to still catch divide-by-zero andINT_MIN / -1.Adds a stateless regression test (
04266_decimal_parsing_ubsan_stid_0937_2022) that exercises decimal digit accumulation viatimeSeriesResampleToGridWithStalenesswith oversized string parameters (expectingBAD_ARGUMENTS) plus a normal-range sanity check.Reviewed by Cursor Bugbot for commit f9107ed. Bugbot is set up for automated code reviews on this repo. Configure here.
Version info
26.6.1.290