From: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Date: Tue, 11 Aug 2026 23:18:00 +0500
Subject: [PATCH] Fix int4 to_char V-multi overflow check

int4_to_char used an unchecked int32 multiply by pow(10, multi) for
the V format.  That could wrap on overflow and produce wrong digits.
Keep the product in int32 via int4mul/dtoi4, matching how
int8_to_char keeps it in int64 via int8mul/dtoi8.

Author: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Reported-by: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Backpatch-through: 14
---
diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c
index effad4c37dd..ef5ec3227a1 100644
--- a/src/backend/utils/adt/formatting.c
+++ b/src/backend/utils/adt/formatting.c
@@ -6617,13 +6617,15 @@ int4_to_char(PG_FUNCTION_ARGS)
 		if (IS_MULTI(&Num))
 		{
-			orgnum = DatumGetCString(DirectFunctionCall1(int4out,
-														 Int32GetDatum(value * ((int32) pow((double) 10, (double) Num.multi)))));
+			double		multi = pow((double) 10, (double) Num.multi);
+
+			value = DatumGetInt32(DirectFunctionCall2(int4mul,
+													  Int32GetDatum(value),
+													  DirectFunctionCall1(dtoi4,
+																		  Float8GetDatum(multi))));
 			Num.pre += Num.multi;
 		}
-		else
-		{
-			orgnum = DatumGetCString(DirectFunctionCall1(int4out,
-														 Int32GetDatum(value)));
-		}
+
+		orgnum = DatumGetCString(DirectFunctionCall1(int4out,
+													 Int32GetDatum(value)));
 
 		if (*orgnum == '-')
diff --git a/src/test/regress/expected/int4.out b/src/test/regress/expected/int4.out
index b1a15888ef8..f956164fefa 100644
--- a/src/test/regress/expected/int4.out
+++ b/src/test/regress/expected/int4.out
@@ -373,6 +373,33 @@
+-- check overflow of to_char() with V format
+SELECT to_char(2, '9V999999999'); -- 10^9
+   to_char   
+-------------
+  2000000000
+(1 row)
+
+SELECT to_char(3, '9V999999999'); -- 10^9
+ERROR:  integer out of range
+SELECT to_char(214748364, '999999999V9');
+   to_char   
+-------------
+  2147483640
+(1 row)
+
+SELECT to_char(2147483647, '9V9');
+ERROR:  integer out of range
+SELECT to_char(-2, '9V999999999'); -- 10^9
+   to_char   
+-------------
+ -2000000000
+(1 row)
+
+SELECT to_char((-2147483648)::int4, '9V9');
+ERROR:  integer out of range
+SELECT to_char(1, '9V9999999999'); -- 10^10
+ERROR:  integer out of range
 -- check rounding when casting from float
 SELECT x, x::int4 AS int4_value
 FROM (VALUES (-2.5::float8),
              (-1.5::float8),
              (-0.5::float8),
              (0.0::float8),
diff --git a/src/test/regress/sql/int4.sql b/src/test/regress/sql/int4.sql
index e9d89e8111f..57f0b92f730 100644
--- a/src/test/regress/sql/int4.sql
+++ b/src/test/regress/sql/int4.sql
@@ -129,6 +129,15 @@
+-- check overflow of to_char() with V format
+SELECT to_char(2, '9V999999999'); -- 10^9
+SELECT to_char(3, '9V999999999'); -- 10^9
+SELECT to_char(214748364, '999999999V9');
+SELECT to_char(2147483647, '9V9');
+SELECT to_char(-2, '9V999999999'); -- 10^9
+SELECT to_char((-2147483648)::int4, '9V9');
+SELECT to_char(1, '9V9999999999'); -- 10^10
+
 -- check rounding when casting from float
 SELECT x, x::int4 AS int4_value
 FROM (VALUES (-2.5::float8),
              (-1.5::float8),
              (-0.5::float8),
              (0.0::float8),
