From d5f3a397e5b24185928b6b1fe4fa818b690e59e0 Mon Sep 17 00:00:00 2001 From: tompng Date: Sat, 5 Sep 2026 01:40:09 +0900 Subject: [PATCH 1/4] Reset exponent in VpSetZero, VpSetInf and VpSetNaN These macros left the exponent of the previous value. Zero, Infinity and NaN have no meaningful exponent and every reader (VpExponent10, VpAsgn, ...) already ignores it, but the internal inconsistency reached slice_copy in Newton-Raphson division: BigDecimal_fix of a tiny positive value (a quotient block that is zero) produced a zero with a negative exponent, and slice_copy underflowed the copy length and wrote outside of the quotient buffer. It corrupted the heap for e.g. BigDecimal(y * 10**1800 + 5).divmod(BigDecimal(y)) with y = 10**1000 + 7, and rake test with NEWTON_RAPHSON_DIVISION_THRESHOLD=1 crashed in GC about once in four runs. Co-Authored-By: Claude Fable 5.1 --- ext/bigdecimal/bigdecimal.c | 1 + ext/bigdecimal/bigdecimal.h | 10 +++++----- test/bigdecimal/test_vp_operation.rb | 11 +++++++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/ext/bigdecimal/bigdecimal.c b/ext/bigdecimal/bigdecimal.c index 99bd9f06..7563a409 100644 --- a/ext/bigdecimal/bigdecimal.c +++ b/ext/bigdecimal/bigdecimal.c @@ -5123,6 +5123,7 @@ VpNmlz(Real *a) NoVal: a->frac[0] = 0; a->Prec = 1; + a->exponent = 0; return 0; } diff --git a/ext/bigdecimal/bigdecimal.h b/ext/bigdecimal/bigdecimal.h index faa66264..46fb36b1 100644 --- a/ext/bigdecimal/bigdecimal.h +++ b/ext/bigdecimal/bigdecimal.h @@ -267,21 +267,21 @@ VP_EXPORT inline BDVALUE rbd_allocate_struct_zero_wrap(int sign, size_t const di #define VpIsPosZero(a) ((a)->sign==VP_SIGN_POSITIVE_ZERO) #define VpIsNegZero(a) ((a)->sign==VP_SIGN_NEGATIVE_ZERO) #define VpIsZero(a) (VpIsPosZero(a) || VpIsNegZero(a)) -#define VpSetPosZero(a) ((a)->frac[0]=0,(a)->Prec=1,(a)->sign=VP_SIGN_POSITIVE_ZERO) -#define VpSetNegZero(a) ((a)->frac[0]=0,(a)->Prec=1,(a)->sign=VP_SIGN_NEGATIVE_ZERO) +#define VpSetPosZero(a) ((a)->frac[0]=0,(a)->Prec=1,(a)->exponent=0,(a)->sign=VP_SIGN_POSITIVE_ZERO) +#define VpSetNegZero(a) ((a)->frac[0]=0,(a)->Prec=1,(a)->exponent=0,(a)->sign=VP_SIGN_NEGATIVE_ZERO) #define VpSetZero(a,s) (void)(((s)>0)?VpSetPosZero(a):VpSetNegZero(a)) /* NaN */ #define VpIsNaN(a) ((a)->sign==VP_SIGN_NaN) -#define VpSetNaN(a) ((a)->frac[0]=0,(a)->Prec=1,(a)->sign=VP_SIGN_NaN) +#define VpSetNaN(a) ((a)->frac[0]=0,(a)->Prec=1,(a)->exponent=0,(a)->sign=VP_SIGN_NaN) /* Infinity */ #define VpIsPosInf(a) ((a)->sign==VP_SIGN_POSITIVE_INFINITE) #define VpIsNegInf(a) ((a)->sign==VP_SIGN_NEGATIVE_INFINITE) #define VpIsInf(a) (VpIsPosInf(a) || VpIsNegInf(a)) #define VpIsDef(a) ( !(VpIsNaN(a)||VpIsInf(a)) ) -#define VpSetPosInf(a) ((a)->frac[0]=0,(a)->Prec=1,(a)->sign=VP_SIGN_POSITIVE_INFINITE) -#define VpSetNegInf(a) ((a)->frac[0]=0,(a)->Prec=1,(a)->sign=VP_SIGN_NEGATIVE_INFINITE) +#define VpSetPosInf(a) ((a)->frac[0]=0,(a)->Prec=1,(a)->exponent=0,(a)->sign=VP_SIGN_POSITIVE_INFINITE) +#define VpSetNegInf(a) ((a)->frac[0]=0,(a)->Prec=1,(a)->exponent=0,(a)->sign=VP_SIGN_NEGATIVE_INFINITE) #define VpSetInf(a,s) (void)(((s)>0)?VpSetPosInf(a):VpSetNegInf(a)) #define VpHasVal(a) (a->frac[0]) #define VpIsOne(a) ((a->Prec==1)&&(a->frac[0]==1)&&(a->exponent==1)) diff --git a/test/bigdecimal/test_vp_operation.rb b/test/bigdecimal/test_vp_operation.rb index ce690aee..eea63b5e 100644 --- a/test/bigdecimal/test_vp_operation.rb +++ b/test/bigdecimal/test_vp_operation.rb @@ -182,6 +182,17 @@ def test_vpdivd_large_prec_divisor assert_vpdivd_equal([divy2_2, x - y2 * divy2_2], [x, y2, 2]) end + def test_vpdivd_zero_quotient_block + # The last block of the quotient is zero and x * inv is far below 1. + # Copying that zero quotient wrote outside of the result buffer. + y = BigDecimal(7 * BASE + 1) + x = y * BASE**3 + 5 + assert_vpdivd_equal([BigDecimal(BASE**3), BigDecimal(5)], [x, y, 4]) + y = BigDecimal(10**1000 + 7) + x = y * BigDecimal("1e1800") + 5 + assert_equal([BigDecimal("1e1800"), BigDecimal(5)], x.divmod(y)) + end + def test_vpdivd_intermediate_zero x = BigDecimal('123456789.246913578000000000123456789') y = BigDecimal('123456789') From 9c9cf167dbc5413e50e2697d5e1961d4e6170a66 Mon Sep 17 00:00:00 2001 From: tompng Date: Sat, 5 Sep 2026 01:40:09 +0900 Subject: [PATCH 2/4] Copy only the words that exist in slice_copy The range can be entirely above the most significant word or below the least significant word of the source. Clamp both ends instead of adjusting the start afterwards, so that the copy never exceeds dest[0, length). Co-Authored-By: Claude Fable 5.1 --- ext/bigdecimal/div.h | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/ext/bigdecimal/div.h b/ext/bigdecimal/div.h index a171a471..80f7fea3 100644 --- a/ext/bigdecimal/div.h +++ b/ext/bigdecimal/div.h @@ -60,14 +60,10 @@ divmod_by_inv_mul(VALUE x, VALUE y, VALUE inv, VALUE *res_div, VALUE *res_mod) { static void slice_copy(DECDIG *dest, Real *src, size_t rshift, size_t length) { ssize_t start = src->exponent - (ssize_t)rshift - (ssize_t)length; - if (start >= (ssize_t)src->Prec) return; - if (start < 0) { - dest -= start; - length -= (size_t)(-start); - start = 0; - } - size_t max_length = (size_t)((ssize_t)src->Prec - start); - memcpy(dest, src->frac + start, Min(length, max_length) * sizeof(DECDIG)); + ssize_t from = Max(start, 0); + ssize_t to = Min(start + (ssize_t)length, (ssize_t)src->Prec); + if (from >= to) return; + memcpy(dest + (from - start), src->frac + from, (size_t)(to - from) * sizeof(DECDIG)); } /* Calculates divmod using Newton-Raphson method. From 5cfd5ca04f0e30f0d3cd73af09dc95cc50a1fccc Mon Sep 17 00:00:00 2001 From: tompng Date: Sat, 5 Sep 2026 01:56:26 +0900 Subject: [PATCH 3/4] Adjust the exponent of Newton division result at once VpDivdNewtonInner ignored the result of AddExponent and kept adding to the exponent after an overflow turned the quotient into Infinity, and also added to the exponent of a zero quotient or remainder. Apply the total shift in one call and skip it for zero. Co-Authored-By: Claude Fable 5.1 --- ext/bigdecimal/div.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ext/bigdecimal/div.h b/ext/bigdecimal/div.h index 80f7fea3..c2160a3d 100644 --- a/ext/bigdecimal/div.h +++ b/ext/bigdecimal/div.h @@ -158,11 +158,8 @@ VpDivdNewtonInner(VALUE args_ptr) r2 = GetBDValueMust(mod); VpAsgn(c, c2.real, VpGetSign(a) * VpGetSign(b)); VpAsgn(r, r2.real, VpGetSign(a)); - AddExponent(c, a->exponent); - AddExponent(c, -b->exponent); - AddExponent(c, -(ssize_t)div_prec); - AddExponent(r, a->exponent); - AddExponent(r, -(ssize_t)(base_prec + div_prec)); + if (!VpIsZero(c)) AddExponent(c, a->exponent - b->exponent - (ssize_t)div_prec); + if (!VpIsZero(r)) AddExponent(r, a->exponent - (ssize_t)(base_prec + div_prec)); RB_GC_GUARD(a2.bigdecimal); RB_GC_GUARD(b2.bigdecimal); RB_GC_GUARD(c2.bigdecimal); From 35a7cb10bcafcf7bf110c4ba68cf5c210219c80d Mon Sep 17 00:00:00 2001 From: tompng Date: Sat, 5 Sep 2026 01:56:26 +0900 Subject: [PATCH 4/4] Do not shift the exponent of zero in VpMidRound Rounding a small value with ROUND_CEILING or ROUND_FLOOR beyond its first digit can result in zero, and the exponent compensation for the rounding position was applied to that zero. Co-Authored-By: Claude Fable 5.1 --- ext/bigdecimal/bigdecimal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/bigdecimal/bigdecimal.c b/ext/bigdecimal/bigdecimal.c index 7563a409..6846bcb2 100644 --- a/ext/bigdecimal/bigdecimal.c +++ b/ext/bigdecimal/bigdecimal.c @@ -6008,7 +6008,7 @@ VpMidRound(Real *y, unsigned short f, ssize_t nf) y->frac[ix] = div; VpNmlz(y); } - if (exptoadd > 0) { + if (exptoadd > 0 && !VpIsZero(y)) { y->exponent += (SIGNED_VALUE)(exptoadd / BASE_FIG); exptoadd %= (ssize_t)BASE_FIG; for (i = 0; i < exptoadd; i++) {