From a8546ea133ffbe4927af8ddfbd21eacd9af3225f Mon Sep 17 00:00:00 2001
From: Joseph Koshakow <koshy44@gmail.com>
Date: Thu, 13 Jun 2024 22:39:25 -0400
Subject: [PATCH 2/2] Handle overflow in money arithmetic

---
 src/backend/utils/adt/cash.c        | 106 +++++++++++++++++++++-------
 src/test/regress/expected/money.out |  90 +++++++++++++++++++++++
 src/test/regress/sql/money.sql      |  47 ++++++++++++
 3 files changed, 219 insertions(+), 24 deletions(-)

diff --git a/src/backend/utils/adt/cash.c b/src/backend/utils/adt/cash.c
index f6f095a57b..abc3cc8fac 100644
--- a/src/backend/utils/adt/cash.c
+++ b/src/backend/utils/adt/cash.c
@@ -86,6 +86,50 @@ num_word(Cash value)
 	return buf;
 }								/* num_word() */
 
+static Cash
+cash_mul_flt_internal(Cash c, float8 f)
+{
+	float8		fresult;
+
+	if (unlikely(isinf(f) || isnan(f)))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("invalid float value")));
+
+	fresult = rint(f * c);
+
+	if (unlikely(!FLOAT8_FITS_IN_INT64(fresult)))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
+
+	return (Cash) fresult;
+}
+
+static Cash
+cash_div_flt_internal(Cash c, float8 f)
+{
+	float8		fresult;
+
+	if (unlikely(f == 0.0))
+		ereport(ERROR,
+				(errcode(ERRCODE_DIVISION_BY_ZERO),
+				 errmsg("division by zero")));
+	if (unlikely(isinf(f) || isnan(f)))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("invalid float value")));
+
+	fresult = rint(c / f);
+
+	if (unlikely(!FLOAT8_FITS_IN_INT64(fresult)))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
+
+	return (Cash) fresult;
+}
+
 /* cash_in()
  * Convert a string to a cash data type.
  * Format is [$]###[,]###[.##]
@@ -617,7 +661,10 @@ cash_pl(PG_FUNCTION_ARGS)
 	Cash		c2 = PG_GETARG_CASH(1);
 	Cash		result;
 
-	result = c1 + c2;
+	if (pg_add_s64_overflow(c1, c2, &result))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
 
 	PG_RETURN_CASH(result);
 }
@@ -633,7 +680,10 @@ cash_mi(PG_FUNCTION_ARGS)
 	Cash		c2 = PG_GETARG_CASH(1);
 	Cash		result;
 
-	result = c1 - c2;
+	if (pg_sub_s64_overflow(c1, c2, &result))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
 
 	PG_RETURN_CASH(result);
 }
@@ -669,7 +719,7 @@ cash_mul_flt8(PG_FUNCTION_ARGS)
 	float8		f = PG_GETARG_FLOAT8(1);
 	Cash		result;
 
-	result = rint(c * f);
+	result = cash_mul_flt_internal(c, f);
 	PG_RETURN_CASH(result);
 }
 
@@ -684,7 +734,7 @@ flt8_mul_cash(PG_FUNCTION_ARGS)
 	Cash		c = PG_GETARG_CASH(1);
 	Cash		result;
 
-	result = rint(f * c);
+	result = cash_mul_flt_internal(c, f);
 	PG_RETURN_CASH(result);
 }
 
@@ -699,12 +749,7 @@ cash_div_flt8(PG_FUNCTION_ARGS)
 	float8		f = PG_GETARG_FLOAT8(1);
 	Cash		result;
 
-	if (f == 0.0)
-		ereport(ERROR,
-				(errcode(ERRCODE_DIVISION_BY_ZERO),
-				 errmsg("division by zero")));
-
-	result = rint(c / f);
+	result = cash_div_flt_internal(c, (float8) f);
 	PG_RETURN_CASH(result);
 }
 
@@ -719,7 +764,7 @@ cash_mul_flt4(PG_FUNCTION_ARGS)
 	float4		f = PG_GETARG_FLOAT4(1);
 	Cash		result;
 
-	result = rint(c * (float8) f);
+	result = cash_mul_flt_internal(c, f);
 	PG_RETURN_CASH(result);
 }
 
@@ -734,7 +779,7 @@ flt4_mul_cash(PG_FUNCTION_ARGS)
 	Cash		c = PG_GETARG_CASH(1);
 	Cash		result;
 
-	result = rint((float8) f * c);
+	result = cash_mul_flt_internal(c, f);
 	PG_RETURN_CASH(result);
 }
 
@@ -750,12 +795,7 @@ cash_div_flt4(PG_FUNCTION_ARGS)
 	float4		f = PG_GETARG_FLOAT4(1);
 	Cash		result;
 
-	if (f == 0.0)
-		ereport(ERROR,
-				(errcode(ERRCODE_DIVISION_BY_ZERO),
-				 errmsg("division by zero")));
-
-	result = rint(c / (float8) f);
+	result = cash_div_flt_internal(c, (float8) f);
 	PG_RETURN_CASH(result);
 }
 
@@ -770,7 +810,10 @@ cash_mul_int8(PG_FUNCTION_ARGS)
 	int64		i = PG_GETARG_INT64(1);
 	Cash		result;
 
-	result = c * i;
+	if (pg_mul_s64_overflow(c, i, &result))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
 	PG_RETURN_CASH(result);
 }
 
@@ -785,7 +828,10 @@ int8_mul_cash(PG_FUNCTION_ARGS)
 	Cash		c = PG_GETARG_CASH(1);
 	Cash		result;
 
-	result = i * c;
+	if (pg_mul_s64_overflow(i, c, &result))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
 	PG_RETURN_CASH(result);
 }
 
@@ -820,7 +866,10 @@ cash_mul_int4(PG_FUNCTION_ARGS)
 	int32		i = PG_GETARG_INT32(1);
 	Cash		result;
 
-	result = c * i;
+	if (pg_mul_s64_overflow(c, (int64) i, &result))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
 	PG_RETURN_CASH(result);
 }
 
@@ -835,7 +884,10 @@ int4_mul_cash(PG_FUNCTION_ARGS)
 	Cash		c = PG_GETARG_CASH(1);
 	Cash		result;
 
-	result = i * c;
+	if (pg_mul_s64_overflow((int64) i, c, &result))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
 	PG_RETURN_CASH(result);
 }
 
@@ -872,7 +924,10 @@ cash_mul_int2(PG_FUNCTION_ARGS)
 	int16		s = PG_GETARG_INT16(1);
 	Cash		result;
 
-	result = c * s;
+	if (pg_mul_s64_overflow(c, (int64) s, &result))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
 	PG_RETURN_CASH(result);
 }
 
@@ -886,7 +941,10 @@ int2_mul_cash(PG_FUNCTION_ARGS)
 	Cash		c = PG_GETARG_CASH(1);
 	Cash		result;
 
-	result = s * c;
+	if (pg_mul_s64_overflow((int64) s, c, &result))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
 	PG_RETURN_CASH(result);
 }
 
diff --git a/src/test/regress/expected/money.out b/src/test/regress/expected/money.out
index 7fd4e31804..d1212fdf43 100644
--- a/src/test/regress/expected/money.out
+++ b/src/test/regress/expected/money.out
@@ -528,3 +528,93 @@ SELECT '-92233720368547758.08'::money::numeric;
  -92233720368547758.08
 (1 row)
 
+-- Test overflow checks
+SELECT '92233720368547758.07'::money + '0.01'::money;
+ERROR:  money out of range
+SELECT '-92233720368547758.08'::money - '0.01'::money;
+ERROR:  money out of range
+SELECT '92233720368547758.07'::money * 2::int8;
+ERROR:  money out of range
+SELECT '-92233720368547758.08'::money * 2::int8;
+ERROR:  money out of range
+SELECT 2::int8 * '92233720368547758.07'::money ;
+ERROR:  money out of range
+SELECT 2::int8 * '-92233720368547758.08'::money;
+ERROR:  money out of range
+SELECT '92233720368547758.07'::money * 2::int4;
+ERROR:  money out of range
+SELECT '-92233720368547758.08'::money * 2::int4;
+ERROR:  money out of range
+SELECT 2::int4 * '92233720368547758.07'::money ;
+ERROR:  money out of range
+SELECT 2::int4 * '-92233720368547758.08'::money;
+ERROR:  money out of range
+SELECT '92233720368547758.07'::money * 2::int2;
+ERROR:  money out of range
+SELECT '-92233720368547758.08'::money * 2::int2;
+ERROR:  money out of range
+SELECT 2::int2 * '92233720368547758.07'::money ;
+ERROR:  money out of range
+SELECT 2::int2 * '-92233720368547758.08'::money;
+ERROR:  money out of range
+SELECT '92233720368547758.07'::money * 2::float8;
+ERROR:  money out of range
+SELECT '-92233720368547758.08'::money * 2::float8;
+ERROR:  money out of range
+SELECT 2::float8 * '92233720368547758.07'::money ;
+ERROR:  money out of range
+SELECT 2::float8 * '-92233720368547758.08'::money;
+ERROR:  money out of range
+SELECT '92233720368547758.07'::money * 2::float4;
+ERROR:  money out of range
+SELECT '-92233720368547758.08'::money * 2::float4;
+ERROR:  money out of range
+SELECT 2::float4 * '92233720368547758.07'::money ;
+ERROR:  money out of range
+SELECT 2::float4 * '-92233720368547758.08'::money;
+ERROR:  money out of range
+SELECT '1'::money / 1.175494e-38::float8;
+ERROR:  money out of range
+SELECT '-1'::money / 1.175494e-38::float8;
+ERROR:  money out of range
+SELECT '1'::money / 1.175494e-38::float4;
+ERROR:  money out of range
+SELECT '-1'::money / 1.175494e-38::float4;
+ERROR:  money out of range
+-- Test invalid multipliers/divisors
+SELECT '42'::money * 'inf'::float8;
+ERROR:  invalid float value
+SELECT '42'::money * '-inf'::float8;
+ERROR:  invalid float value
+SELECT '42'::money * 'nan'::float8;
+ERROR:  invalid float value
+SELECT 'inf'::float8 * '42'::money;
+ERROR:  invalid float value
+SELECT '-inf'::float8 * '42'::money;
+ERROR:  invalid float value
+SELECT 'nan'::float8 * '42'::money;
+ERROR:  invalid float value
+SELECT '42'::money / 'inf'::float8;
+ERROR:  invalid float value
+SELECT '42'::money / '-inf'::float8;
+ERROR:  invalid float value
+SELECT '42'::money / 'nan'::float8;
+ERROR:  invalid float value
+SELECT '42'::money * 'inf'::float4;
+ERROR:  invalid float value
+SELECT '42'::money * '-inf'::float4;
+ERROR:  invalid float value
+SELECT '42'::money * 'nan'::float4;
+ERROR:  invalid float value
+SELECT 'inf'::float4 * '42'::money;
+ERROR:  invalid float value
+SELECT '-inf'::float4 * '42'::money;
+ERROR:  invalid float value
+SELECT 'nan'::float4 * '42'::money;
+ERROR:  invalid float value
+SELECT '42'::money / 'inf'::float4;
+ERROR:  invalid float value
+SELECT '42'::money / '-inf'::float4;
+ERROR:  invalid float value
+SELECT '42'::money / 'nan'::float4;
+ERROR:  invalid float value
diff --git a/src/test/regress/sql/money.sql b/src/test/regress/sql/money.sql
index 81c92dd960..4422d2816b 100644
--- a/src/test/regress/sql/money.sql
+++ b/src/test/regress/sql/money.sql
@@ -135,3 +135,50 @@ SELECT '12345678901234567'::money::numeric;
 SELECT '-12345678901234567'::money::numeric;
 SELECT '92233720368547758.07'::money::numeric;
 SELECT '-92233720368547758.08'::money::numeric;
+
+-- Test overflow checks
+SELECT '92233720368547758.07'::money + '0.01'::money;
+SELECT '-92233720368547758.08'::money - '0.01'::money;
+SELECT '92233720368547758.07'::money * 2::int8;
+SELECT '-92233720368547758.08'::money * 2::int8;
+SELECT 2::int8 * '92233720368547758.07'::money ;
+SELECT 2::int8 * '-92233720368547758.08'::money;
+SELECT '92233720368547758.07'::money * 2::int4;
+SELECT '-92233720368547758.08'::money * 2::int4;
+SELECT 2::int4 * '92233720368547758.07'::money ;
+SELECT 2::int4 * '-92233720368547758.08'::money;
+SELECT '92233720368547758.07'::money * 2::int2;
+SELECT '-92233720368547758.08'::money * 2::int2;
+SELECT 2::int2 * '92233720368547758.07'::money ;
+SELECT 2::int2 * '-92233720368547758.08'::money;
+SELECT '92233720368547758.07'::money * 2::float8;
+SELECT '-92233720368547758.08'::money * 2::float8;
+SELECT 2::float8 * '92233720368547758.07'::money ;
+SELECT 2::float8 * '-92233720368547758.08'::money;
+SELECT '92233720368547758.07'::money * 2::float4;
+SELECT '-92233720368547758.08'::money * 2::float4;
+SELECT 2::float4 * '92233720368547758.07'::money ;
+SELECT 2::float4 * '-92233720368547758.08'::money;
+SELECT '1'::money / 1.175494e-38::float8;
+SELECT '-1'::money / 1.175494e-38::float8;
+SELECT '1'::money / 1.175494e-38::float4;
+SELECT '-1'::money / 1.175494e-38::float4;
+-- Test invalid multipliers/divisors
+SELECT '42'::money * 'inf'::float8;
+SELECT '42'::money * '-inf'::float8;
+SELECT '42'::money * 'nan'::float8;
+SELECT 'inf'::float8 * '42'::money;
+SELECT '-inf'::float8 * '42'::money;
+SELECT 'nan'::float8 * '42'::money;
+SELECT '42'::money / 'inf'::float8;
+SELECT '42'::money / '-inf'::float8;
+SELECT '42'::money / 'nan'::float8;
+SELECT '42'::money * 'inf'::float4;
+SELECT '42'::money * '-inf'::float4;
+SELECT '42'::money * 'nan'::float4;
+SELECT 'inf'::float4 * '42'::money;
+SELECT '-inf'::float4 * '42'::money;
+SELECT 'nan'::float4 * '42'::money;
+SELECT '42'::money / 'inf'::float4;
+SELECT '42'::money / '-inf'::float4;
+SELECT '42'::money / 'nan'::float4;
-- 
2.34.1

