diff --git a/pgtype/numeric.go b/pgtype/numeric.go index caf5ff17b..40fa3c2ec 100644 --- a/pgtype/numeric.go +++ b/pgtype/numeric.go @@ -687,7 +687,7 @@ func (scanPlanBinaryNumericToNumericScanner) Scan(src []byte, dst any) error { reduced := &big.Int{} remainder := &big.Int{} - if exp >= 0 { + if exp >= 0 && accum.Sign() != 0 { for { reduced.DivMod(accum, big10, remainder) if remainder.Sign() != 0 { diff --git a/pgtype/numeric_test.go b/pgtype/numeric_test.go index 61faac5df..b796625c2 100644 --- a/pgtype/numeric_test.go +++ b/pgtype/numeric_test.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" "testing" + "time" pgx "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" @@ -147,6 +148,42 @@ func TestNumericCodecInfinity(t *testing.T) { }) } +// TestNumericBinaryDecodeUnnormalizedZero ensures the binary decoder does not +// hang on a zero value encoded with ndigits > 0. PostgreSQL always normalizes +// zero to ndigits == 0, but other servers speaking the same wire protocol +// (e.g. CockroachDB) and connection poolers may send an unnormalized zero. The +// trailing-zero reduction loop divided such a value by 10 forever because +// 0 % 10 == 0, spinning the CPU indefinitely. +func TestNumericBinaryDecodeUnnormalizedZero(t *testing.T) { + // ndigits=1, weight=0, sign=0 (positive), dscale=0, single base-10000 digit = 0. + src := []byte{ + 0x00, 0x01, // ndigits + 0x00, 0x00, // weight + 0x00, 0x00, // sign + 0x00, 0x00, // dscale + 0x00, 0x00, // digit[0] + } + + done := make(chan error, 1) + var n pgtype.Numeric + go func() { + plan := pgtype.NumericCodec{}.PlanScan(nil, pgtype.NumericOID, pgtype.BinaryFormatCode, &n) + require.NotNil(t, plan) + done <- plan.Scan(src, &n) + }() + + select { + case err := <-done: + require.NoError(t, err) + require.True(t, n.Valid) + require.False(t, n.NaN) + require.Equal(t, pgtype.Finite, n.InfinityModifier) + require.Equal(t, 0, n.Int.Sign()) + case <-time.After(5 * time.Second): + t.Fatal("decoding unnormalized zero did not terminate (infinite loop)") + } +} + func TestNumericFloat64Valuer(t *testing.T) { for i, tt := range []struct { n pgtype.Numeric