From e8dfe0430fad922948f56889a84af41da532c853 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 19 Jul 2024 11:52:32 -0500
Subject: [PATCH] Add overflow checks to money type.

None of the arithmetic functions for the the money type handle
overflow.  This commit introduces several helper functions with
overflow checking and makes use of them in the money type's
arithmetic functions.

Fixes bug #18240.

Reported-by: Alexander Lakhin
Author: Joseph Koshakow
Discussion: https://postgr.es/m/18240-c5da758d7dc1ecf0%40postgresql.org
Discussion: https://postgr.es/m/CAAvxfHdBPOyEGS7s%2Bxf4iaW0-cgiq25jpYdWBqQqvLtLe_t6tw%40mail.gmail.com
Backpatch-through: 12
---
 src/backend/utils/adt/cash.c        | 174 +++++++++++++++-------------
 src/test/regress/expected/money.out |  19 +++
 src/test/regress/sql/money.sql      |  11 ++
 3 files changed, 124 insertions(+), 80 deletions(-)

diff --git a/src/backend/utils/adt/cash.c b/src/backend/utils/adt/cash.c
index d093ce80386..387bef50d62 100644
--- a/src/backend/utils/adt/cash.c
+++ b/src/backend/utils/adt/cash.c
@@ -26,6 +26,7 @@
 #include "libpq/pqformat.h"
 #include "utils/builtins.h"
 #include "utils/cash.h"
+#include "utils/float.h"
 #include "utils/int8.h"
 #include "utils/numeric.h"
 #include "utils/pg_locale.h"
@@ -87,6 +88,82 @@ num_word(Cash value)
 	return buf;
 }								/* num_word() */
 
+static inline Cash
+cash_pl_cash(Cash c1, Cash c2)
+{
+	Cash		res;
+
+	if (unlikely(pg_add_s64_overflow(c1, c2, &res)))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
+
+	return res;
+}
+
+static inline Cash
+cash_mi_cash(Cash c1, Cash c2)
+{
+	Cash		res;
+
+	if (unlikely(pg_sub_s64_overflow(c1, c2, &res)))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
+
+	return res;
+}
+
+static inline Cash
+cash_mul_float8(Cash c, float8 f)
+{
+	float8		res = rint(float8_mul((float8) c, f));
+
+	if (unlikely(isnan(res) || !FLOAT8_FITS_IN_INT64(res)))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
+
+	return (Cash) res;
+}
+
+static inline Cash
+cash_div_float8(Cash c, float8 f)
+{
+	float8		res = rint(float8_div((float8) c, f));
+
+	if (unlikely(isnan(res) || !FLOAT8_FITS_IN_INT64(res)))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
+
+	return (Cash) res;
+}
+
+static inline Cash
+cash_mul_int64(Cash c, int64 i)
+{
+	Cash		res;
+
+	if (unlikely(pg_mul_s64_overflow(c, i, &res)))
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("money out of range")));
+
+	return res;
+}
+
+static inline Cash
+cash_div_int64(Cash c, int64 i)
+{
+	if (unlikely(i == 0))
+		ereport(ERROR,
+				(errcode(ERRCODE_DIVISION_BY_ZERO),
+				 errmsg("division by zero")));
+
+	return c / i;
+}
+
 /* cash_in()
  * Convert a string to a cash data type.
  * Format is [$]###[,]###[.##]
@@ -612,11 +689,8 @@ cash_pl(PG_FUNCTION_ARGS)
 {
 	Cash		c1 = PG_GETARG_CASH(0);
 	Cash		c2 = PG_GETARG_CASH(1);
-	Cash		result;
-
-	result = c1 + c2;
 
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_pl_cash(c1, c2));
 }
 
 
@@ -628,11 +702,8 @@ cash_mi(PG_FUNCTION_ARGS)
 {
 	Cash		c1 = PG_GETARG_CASH(0);
 	Cash		c2 = PG_GETARG_CASH(1);
-	Cash		result;
-
-	result = c1 - c2;
 
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mi_cash(c1, c2));
 }
 
 
@@ -664,10 +735,8 @@ cash_mul_flt8(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	float8		f = PG_GETARG_FLOAT8(1);
-	Cash		result;
 
-	result = rint(c * f);
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_float8(c, f));
 }
 
 
@@ -679,10 +748,8 @@ flt8_mul_cash(PG_FUNCTION_ARGS)
 {
 	float8		f = PG_GETARG_FLOAT8(0);
 	Cash		c = PG_GETARG_CASH(1);
-	Cash		result;
 
-	result = rint(f * c);
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_float8(c, f));
 }
 
 
@@ -694,15 +761,8 @@ cash_div_flt8(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	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);
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_div_float8(c, f));
 }
 
 
@@ -714,10 +774,8 @@ cash_mul_flt4(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	float4		f = PG_GETARG_FLOAT4(1);
-	Cash		result;
 
-	result = rint(c * (float8) f);
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_float8(c, (float8) f));
 }
 
 
@@ -729,10 +787,8 @@ flt4_mul_cash(PG_FUNCTION_ARGS)
 {
 	float4		f = PG_GETARG_FLOAT4(0);
 	Cash		c = PG_GETARG_CASH(1);
-	Cash		result;
 
-	result = rint((float8) f * c);
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_float8(c, (float8) f));
 }
 
 
@@ -745,15 +801,8 @@ cash_div_flt4(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	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);
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_div_float8(c, (float8) f));
 }
 
 
@@ -765,10 +814,8 @@ cash_mul_int8(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	int64		i = PG_GETARG_INT64(1);
-	Cash		result;
 
-	result = c * i;
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_int64(c, i));
 }
 
 
@@ -780,10 +827,8 @@ int8_mul_cash(PG_FUNCTION_ARGS)
 {
 	int64		i = PG_GETARG_INT64(0);
 	Cash		c = PG_GETARG_CASH(1);
-	Cash		result;
 
-	result = i * c;
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_int64(c, i));
 }
 
 /* cash_div_int8()
@@ -794,16 +839,8 @@ cash_div_int8(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	int64		i = PG_GETARG_INT64(1);
-	Cash		result;
-
-	if (i == 0)
-		ereport(ERROR,
-				(errcode(ERRCODE_DIVISION_BY_ZERO),
-				 errmsg("division by zero")));
 
-	result = c / i;
-
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_div_int64(c, i));
 }
 
 
@@ -815,10 +852,8 @@ cash_mul_int4(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	int32		i = PG_GETARG_INT32(1);
-	Cash		result;
 
-	result = c * i;
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_int64(c, (int64) i));
 }
 
 
@@ -830,10 +865,8 @@ int4_mul_cash(PG_FUNCTION_ARGS)
 {
 	int32		i = PG_GETARG_INT32(0);
 	Cash		c = PG_GETARG_CASH(1);
-	Cash		result;
 
-	result = i * c;
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_int64(c, (int64) i));
 }
 
 
@@ -846,16 +879,8 @@ cash_div_int4(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	int32		i = PG_GETARG_INT32(1);
-	Cash		result;
-
-	if (i == 0)
-		ereport(ERROR,
-				(errcode(ERRCODE_DIVISION_BY_ZERO),
-				 errmsg("division by zero")));
-
-	result = c / i;
 
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_div_int64(c, (int64) i));
 }
 
 
@@ -867,10 +892,8 @@ cash_mul_int2(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	int16		s = PG_GETARG_INT16(1);
-	Cash		result;
 
-	result = c * s;
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_int64(c, (int64) s));
 }
 
 /* int2_mul_cash()
@@ -881,10 +904,8 @@ int2_mul_cash(PG_FUNCTION_ARGS)
 {
 	int16		s = PG_GETARG_INT16(0);
 	Cash		c = PG_GETARG_CASH(1);
-	Cash		result;
 
-	result = s * c;
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_mul_int64(c, (int64) s));
 }
 
 /* cash_div_int2()
@@ -896,15 +917,8 @@ cash_div_int2(PG_FUNCTION_ARGS)
 {
 	Cash		c = PG_GETARG_CASH(0);
 	int16		s = PG_GETARG_INT16(1);
-	Cash		result;
 
-	if (s == 0)
-		ereport(ERROR,
-				(errcode(ERRCODE_DIVISION_BY_ZERO),
-				 errmsg("division by zero")));
-
-	result = c / s;
-	PG_RETURN_CASH(result);
+	PG_RETURN_CASH(cash_div_int64(c, (int64) s));
 }
 
 /* cashlarger()
diff --git a/src/test/regress/expected/money.out b/src/test/regress/expected/money.out
index fc71a72fed3..90140f66767 100644
--- a/src/test/regress/expected/money.out
+++ b/src/test/regress/expected/money.out
@@ -503,3 +503,22 @@ SELECT '-92233720368547758.08'::money::numeric;
  -92233720368547758.08
 (1 row)
 
+-- 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::float8;
+ERROR:  money out of range
+SELECT '-1'::money / 1.175494e-38::float4;
+ERROR:  money out of range
+SELECT '92233720368547758.07'::money * 2::int4;
+ERROR:  money out of range
+SELECT '1'::money / 0::int2;
+ERROR:  division by zero
+SELECT '42'::money * 'inf'::float8;
+ERROR:  money out of range
+SELECT '42'::money * '-inf'::float8;
+ERROR:  money out of range
+SELECT '42'::money * 'nan'::float4;
+ERROR:  money out of range
diff --git a/src/test/regress/sql/money.sql b/src/test/regress/sql/money.sql
index 5e746286c90..1eb471d1e4e 100644
--- a/src/test/regress/sql/money.sql
+++ b/src/test/regress/sql/money.sql
@@ -129,3 +129,14 @@ SELECT '12345678901234567'::money::numeric;
 SELECT '-12345678901234567'::money::numeric;
 SELECT '92233720368547758.07'::money::numeric;
 SELECT '-92233720368547758.08'::money::numeric;
+
+-- overflow checks
+SELECT '92233720368547758.07'::money + '0.01'::money;
+SELECT '-92233720368547758.08'::money - '0.01'::money;
+SELECT '92233720368547758.07'::money * 2::float8;
+SELECT '-1'::money / 1.175494e-38::float4;
+SELECT '92233720368547758.07'::money * 2::int4;
+SELECT '1'::money / 0::int2;
+SELECT '42'::money * 'inf'::float8;
+SELECT '42'::money * '-inf'::float8;
+SELECT '42'::money * 'nan'::float4;
-- 
2.39.5