From: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Date: Sat, 31 Oct 2026 01:40:00 +0500
Subject: [PATCH] Fix float to_char V scaling overflow

float4_to_char() and float8_to_char() scaled a V format with pow().
The factor becomes Inf past FLT_MAX_10_EXP or DBL_MAX_10_EXP, and the
multiply turns a finite value into Inf and zero into NaN.

Scale with numeric_power() and numeric_mul().  float8 reports a
product that does not fit via float_overflow_error().  float4 reports
it via numeric_float4().

BUG #19810
Author: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Reported-by: chunling qin <303677365@qq.com>
Discussion: https://www.postgresql.org/message-id/19810-e14756b84da7afc8@postgresql.org
---
diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c
index 8b5f99e6a82..78aacd349cb 100644
--- a/src/backend/utils/adt/formatting.c
+++ b/src/backend/utils/adt/formatting.c
@@ -77,6 +77,7 @@
 #include "utils/builtins.h"
 #include "utils/date.h"
 #include "utils/datetime.h"
+#include "utils/float.h"
 #include "utils/formatting.h"
 #include "utils/memutils.h"
 #include "utils/numeric.h"
@@ -6679,6 +6680,28 @@ int8_to_char(PG_FUNCTION_ARGS)
 	PG_RETURN_TEXT_P((text *) result.data);
 }
 
+/*
+ * 10^multi is computed in numeric.  A float power of ten is Inf past
+ * FLT_MAX_10_EXP or DBL_MAX_10_EXP.
+ */
+static Numeric
+scale_numeric_v(Numeric nval, int multi, int32 round_digits)
+{
+	Numeric		a = int64_to_numeric(10);
+	Numeric		b = int64_to_numeric(multi);
+	Numeric		x;
+
+	x = DatumGetNumeric(DirectFunctionCall2(numeric_power,
+											NumericGetDatum(a),
+											NumericGetDatum(b)));
+	nval = DatumGetNumeric(DirectFunctionCall2(numeric_mul,
+											   NumericGetDatum(nval),
+											   NumericGetDatum(x)));
+	return DatumGetNumeric(DirectFunctionCall2(numeric_round,
+											   NumericGetDatum(nval),
+											   Int32GetDatum(round_digits)));
+}
+
 /*
  * FLOAT4 to_char()
  */
@@ -6743,9 +6766,16 @@ float4_to_char(PG_FUNCTION_ARGS)
 
 		if (IS_MULTI(&Num))
 		{
-			float		multi = pow((double) 10, (double) Num.multi);
+			if (!isnan(val) && !isinf(val))
+			{
+				Numeric		nval;
 
-			val = value * multi;
+				nval = DatumGetNumeric(DirectFunctionCall1(float4_numeric,
+														   Float4GetDatum(val)));
+				nval = scale_numeric_v(nval, Num.multi, Num.post);
+				val = DatumGetFloat4(DirectFunctionCall1(numeric_float4,
+														 NumericGetDatum(nval)));
+			}
 			Num.pre += Num.multi;
 		}
 
@@ -6855,9 +6885,18 @@ float8_to_char(PG_FUNCTION_ARGS)
 
 		if (IS_MULTI(&Num))
 		{
-			double		multi = pow((double) 10, (double) Num.multi);
-
-			val = value * multi;
+			if (!isnan(val) && !isinf(val))
+			{
+				Numeric		nval;
+
+				nval = DatumGetNumeric(DirectFunctionCall1(float8_numeric,
+														   Float8GetDatum(val)));
+				nval = scale_numeric_v(nval, Num.multi, Num.post);
+				val = DatumGetFloat8(DirectFunctionCall1(numeric_float8_no_overflow,
+														 NumericGetDatum(nval)));
+				if (isinf(val) || isnan(val))
+					float_overflow_error();
+			}
 			Num.pre += Num.multi;
 		}
 
diff --git a/src/test/regress/expected/numeric.out b/src/test/regress/expected/numeric.out
index 6269d5bb397..74ed7431b2f 100644
--- a/src/test/regress/expected/numeric.out
+++ b/src/test/regress/expected/numeric.out
@@ -2208,6 +2208,45 @@ SELECT to_char(1234.56::float8, '99999V99');
    123456
 (1 row)
 
+-- V on float4/float8 keeps a representable product and errors on overflow
+SELECT to_char(1e-300::float8, 'FM9V' || repeat('9', 309));
+  to_char   
+------------
+ 1000000000
+(1 row)
+
+SELECT to_char((-1e-300)::float8, 'FM9V' || repeat('9', 309));
+   to_char   
+-------------
+ -1000000000
+(1 row)
+
+SELECT to_char(0::float8, 'FM9V' || repeat('9', 309));
+ to_char 
+---------
+ 0
+(1 row)
+
+SELECT to_char(1e308::float8, 'FM9V9999999');
+ERROR:  value out of range: overflow
+SELECT to_char((-1e308)::float8, 'FM9V9999999');
+ERROR:  value out of range: overflow
+SELECT to_char(1e308::float8, 'FM9V999999');
+ERROR:  value out of range: overflow
+SELECT to_char(0::float4, 'FM9V' || repeat('9', 39));
+ to_char 
+---------
+ 0
+(1 row)
+
+SELECT to_char(1e-30::float4, 'FM9V' || repeat('9', 39));
+  to_char   
+------------
+ 1000000000
+(1 row)
+
+SELECT to_char(1::float4, 'FM9V' || repeat('9', 39));
+ERROR:  "1000000000000000000000000000000000000000" is out of range for type real
 -- Check parsing of literal text in a format string
 SELECT to_char('100'::numeric, 'foo999');
  to_char 
diff --git a/src/test/regress/sql/numeric.sql b/src/test/regress/sql/numeric.sql
index e63b21e4b6b..12168047316 100644
--- a/src/test/regress/sql/numeric.sql
+++ b/src/test/regress/sql/numeric.sql
@@ -1052,6 +1052,17 @@ SELECT to_char(1234.56::numeric, '99999V99');
 SELECT to_char(1234.56::float4, '99999V99');
 SELECT to_char(1234.56::float8, '99999V99');
 
+-- V on float4/float8 keeps a representable product and errors on overflow
+SELECT to_char(1e-300::float8, 'FM9V' || repeat('9', 309));
+SELECT to_char((-1e-300)::float8, 'FM9V' || repeat('9', 309));
+SELECT to_char(0::float8, 'FM9V' || repeat('9', 309));
+SELECT to_char(1e308::float8, 'FM9V9999999');
+SELECT to_char((-1e308)::float8, 'FM9V9999999');
+SELECT to_char(1e308::float8, 'FM9V999999');
+SELECT to_char(0::float4, 'FM9V' || repeat('9', 39));
+SELECT to_char(1e-30::float4, 'FM9V' || repeat('9', 39));
+SELECT to_char(1::float4, 'FM9V' || repeat('9', 39));
+
 -- Check parsing of literal text in a format string
 SELECT to_char('100'::numeric, 'foo999');
 SELECT to_char('100'::numeric, 'f\oo999');
