fix(agg): keep sum/prod list reductions exact and UB-free for i64 - #423
fix(agg): keep sum/prod list reductions exact and UB-free for i64#423belowzeroff wants to merge 3 commits into
Conversation
Reducing a boxed (list ...) of integers mishandled large i64 values:
- sum: `isum += elems[i]->i64` (and the mixed-int fallback `isum += v`)
accumulated with signed arithmetic, which is UB on overflow. Confirmed by
UBSan on (sum (list 9223372036854775807 1)) at src/ops/agg.c:464.
- prod: read each i64 atom via `(int64_t)as_f64(atom)`, losing precision
above 2^53 (e.g. (prod (list 9007199254740993 1)) -> ...992) and hitting
a float->int cast UB when the value rounds to >= 2^63.
Accumulate integers via unsigned math (well-defined wraparound) and read i64
atoms directly, mirroring the unsigned idiom already used in prod's multiply
and the UB-free typed-vector reduction path. Adds regression coverage in
test/rfl/agg/{sum,prod_builtin}.rfl.
|
Reviewed (with an adversarial verification pass). The diff itself checks out — the unsigned-wrap idiom is sound, the tests' expected wrap values are correct, and wrap-landing-on- 1. 2. Parted path admits TIMESTAMP where flat rejects it ( 3. Table-scan scalar-sum kernels ( 4. Affine-sum fast path ( 5. Minor, in the touched function: the RAY_TIMESTAMP else-arm in Suggestion: extract a |
|
Follow-up on the Three variants of the reduction loop compiled side by side: s += d[i]; // signed (current UB form)
s = (int64_t)((uint64_t)s + (uint64_t)d[i]); // inline wrap expression (this PR's idiom)
s = wrap_add_i64(s, d[i]); // static inline helperAll three get The one real constraint: the helper must be Nuance for |
…ted TIMESTAMP Review follow-up: the boxed-list fix left the sibling sum accumulators in the same dispatch family carrying the identical signed-overflow UB. Extract wrap_add_i64 / wrap_mul_i64 (core/types.h; codegen-neutral — they inline to a bare add/mul and don't inhibit autovectorization) and apply them everywhere: - ray_sum_fn: the narrow/TIME vector arms and the boxed-list loop (collapsing the duplicated i64/other branches into one wrap-accumulate); drop the dead RAY_TIMESTAMP vector arm, which agg_type_admitted() rejects before dispatch. - ray_prod_fn and agg_parted_prod: same wrap idiom via the shared helper. - agg_parted_sum: wrap the segment accumulation and route admission through agg_type_admitted, so a parted TIMESTAMP column is rejected exactly like a flat one (previously it was silently summed and returned a timestamp). - group.c scalar_sum_i64_fn / scalar_sum_linear_i64_fn (the hot select-scan path, incl. its bias/coeff multiplies) and the lang/eval.c affine-sum fast path (sum(v + c) folding). Adds regression coverage for the select-scan (scalar, linear, by-group) and affine fast paths in test/rfl/agg/sum.rfl and test/rfl/arith/sum_affine.rfl.
|
Thanks for the thorough pass — you're right that the fix stopped at the boxed-list path while the siblings carried the same UB. Fixed all of them in 23132ec, via the Helper placement: put it in Point by point:
Tests: added select-scan (scalar, linear, by-group) coverage to Full suite green (3701/3702, 1 pre-existing skip, 0 failed), ASan/UBSan clean. |
|
Re-reviewed the head (23132ec + 0b05c1b) with a fresh ASan/UBSan build. Almost everything checks out — including the 0b05c1b decision to replace Three remaining items: 1. One merge site missed — reproduced UB. The intra-kernel accumulation in The PR's wrap tests use 2-row tables — below 2. The new parted tests pass for the wrong reason. In 3. Answer to your reachability question:
Suggest pinning those three in Side note, pre-existing (not this PR): plain |
Problem
Reducing a boxed
(list ...)of integers withsum/prodmishandles largei64values. Both are confirmed by UBSan (debug build):Root causes in the list (scalar) reduction path:
sumaccumulates with signed arithmetic —isum += elems[i]->i64(and the mixed-int fallbackisum += v). Signed overflow is undefined behaviour.prodreads every non-float element through(int64_t)as_f64(atom). Round-tripping ani64throughdoubleloses precision above 2^53, and the cast is UB when the value rounds to ≥ 2^63.The typed-vector reduction path already wraps safely, and
prod's multiply already uses unsigned math — only the boxed-list integer reads/accumulation were wrong, so a single-element list and a multi-element list of the same values disagreed.Fix
(int64_t)((uint64_t)acc + …)), giving well-defined wraparound instead of UB — matching the typed-vector path and the unsigned idiom already present inprod's multiply.i64atoms directly (elems[i]->i64) instead of viaas_f64, so results stay exact above 2^53 and never hit the float→int cast UB.This makes
sum/prodsymmetric and keeps single-element and multi-element list reductions consistent.Testing
make test— full suite green (3701/3702 pass, 1 pre-existing skip, 0 failed), UBSan/ASan clean.test/rfl/agg/sum.rfl:2*(2^63-1)wraps to-2; value at 2^53 + 1 stays exact.test/rfl/agg/prod_builtin.rfl: exact product above 2^53;(2^63-1)*4wraps to-4.