From 2fe0dd7806e5cd604758af99ef167f164d17a15e Mon Sep 17 00:00:00 2001
From: Egor Ivkov <e.ivkov@arenadata.io>
Date: Wed, 30 Sep 2026 00:13:24 +0300
Subject: [PATCH v2] fix: return error on shift count out of range

Previous behavior of int4shr/int4shl/.. was an UB in cases where shift
exceeds bitness of the integer being shifted. x86 handled this by
applying a mask (e.g. & 31) while ARM will produce 0 and RISC-V will
crash. So besides being an UB it was inconsistent across platforms.

The proposed solution is to report an error to the user: "shift count
out of range".
---
 src/backend/utils/adt/int.c        | 20 ++++++++++++++++++++
 src/backend/utils/adt/int8.c       | 10 ++++++++++
 src/test/regress/expected/int2.out | 25 +++++++++++++++++++++++++
 src/test/regress/expected/int4.out | 27 +++++++++++++++++++++++++++
 src/test/regress/expected/int8.out | 25 +++++++++++++++++++++++++
 src/test/regress/sql/int2.sql      |  8 ++++++++
 src/test/regress/sql/int4.sql      |  9 +++++++++
 src/test/regress/sql/int8.sql      |  8 ++++++++
 8 files changed, 132 insertions(+)

diff --git a/src/backend/utils/adt/int.c b/src/backend/utils/adt/int.c
index 68fecbfe2ed..82a0e56dfe0 100644
--- a/src/backend/utils/adt/int.c
+++ b/src/backend/utils/adt/int.c
@@ -1451,6 +1451,11 @@ int4shl(PG_FUNCTION_ARGS)
 	int32		arg1 = PG_GETARG_INT32(0);
 	int32		arg2 = PG_GETARG_INT32(1);

+	if (arg2 < 0 || arg2 > 31)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("integer shift count out of range")));
+
 	PG_RETURN_INT32(arg1 << arg2);
 }

@@ -1460,6 +1465,11 @@ int4shr(PG_FUNCTION_ARGS)
 	int32		arg1 = PG_GETARG_INT32(0);
 	int32		arg2 = PG_GETARG_INT32(1);

+	if (arg2 < 0 || arg2 > 31)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("integer shift count out of range")));
+
 	PG_RETURN_INT32(arg1 >> arg2);
 }

@@ -1513,6 +1523,11 @@ int2shl(PG_FUNCTION_ARGS)
 	int16		arg1 = PG_GETARG_INT16(0);
 	int32		arg2 = PG_GETARG_INT32(1);

+	if (arg2 < 0 || arg2 > 31)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("integer shift count out of range")));
+
 	PG_RETURN_INT16(arg1 << arg2);
 }

@@ -1522,6 +1537,11 @@ int2shr(PG_FUNCTION_ARGS)
 	int16		arg1 = PG_GETARG_INT16(0);
 	int32		arg2 = PG_GETARG_INT32(1);

+	if (arg2 < 0 || arg2 > 31)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("integer shift count out of range")));
+
 	PG_RETURN_INT16(arg1 >> arg2);
 }

diff --git a/src/backend/utils/adt/int8.c b/src/backend/utils/adt/int8.c
index 1a8bddd6bb1..a18d9945205 100644
--- a/src/backend/utils/adt/int8.c
+++ b/src/backend/utils/adt/int8.c
@@ -1257,6 +1257,11 @@ int8shl(PG_FUNCTION_ARGS)
 	int64		arg1 = PG_GETARG_INT64(0);
 	int32		arg2 = PG_GETARG_INT32(1);

+	if (arg2 < 0 || arg2 > 63)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("integer shift count out of range")));
+
 	PG_RETURN_INT64(arg1 << arg2);
 }

@@ -1266,6 +1271,11 @@ int8shr(PG_FUNCTION_ARGS)
 	int64		arg1 = PG_GETARG_INT64(0);
 	int32		arg2 = PG_GETARG_INT32(1);

+	if (arg2 < 0 || arg2 > 63)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("integer shift count out of range")));
+
 	PG_RETURN_INT64(arg1 >> arg2);
 }

diff --git a/src/test/regress/expected/int2.out b/src/test/regress/expected/int2.out
index 4e03a5faee0..8e0b0c7f103 100644
--- a/src/test/regress/expected/int2.out
+++ b/src/test/regress/expected/int2.out
@@ -297,6 +297,31 @@ SELECT ((-1::int2<<15)+1::int2)::text;
  -32767
 (1 row)

+-- check sane handling of out-of-range shift counts
+SELECT (-1::int2) << 15;
+ ?column?
+----------
+   -32768
+(1 row)
+
+SELECT (-1::int2) >> 15;
+ ?column?
+----------
+       -1
+(1 row)
+
+SELECT 1::int2 << 31;
+ ?column?
+----------
+        0
+(1 row)
+
+SELECT 1::int2 << 32;
+ERROR:  integer shift count out of range
+SELECT 1::int2 >> 32;
+ERROR:  integer shift count out of range
+SELECT 1::int2 << -1;
+ERROR:  integer shift count out of range
 -- check sane handling of INT16_MIN overflow cases
 SELECT (-32768)::int2 * (-1)::int2;
 ERROR:  smallint out of range
diff --git a/src/test/regress/expected/int4.out b/src/test/regress/expected/int4.out
index fca591fad84..e70bd953277 100644
--- a/src/test/regress/expected/int4.out
+++ b/src/test/regress/expected/int4.out
@@ -349,6 +349,33 @@ SELECT ((-1::int4<<31)+1)::text;
  -2147483647
 (1 row)

+-- check sane handling of out-of-range shift counts
+SELECT (-1::int4) << 31;
+  ?column?
+-------------
+ -2147483648
+(1 row)
+
+SELECT (-1::int4) >> 31;
+ ?column?
+----------
+       -1
+(1 row)
+
+SELECT 1::int4 << 31;
+  ?column?
+-------------
+ -2147483648
+(1 row)
+
+SELECT 1::int4 << 32;
+ERROR:  integer shift count out of range
+SELECT 1::int4 >> 32;
+ERROR:  integer shift count out of range
+SELECT 1::int4 << -1;
+ERROR:  integer shift count out of range
+SELECT 1::int4 >> -1;
+ERROR:  integer shift count out of range
 -- check sane handling of INT_MIN overflow cases
 SELECT (-2147483648)::int4 * (-1)::int4;
 ERROR:  integer out of range
diff --git a/src/test/regress/expected/int8.out b/src/test/regress/expected/int8.out
index 392f3bf655c..f7cbebb8063 100644
--- a/src/test/regress/expected/int8.out
+++ b/src/test/regress/expected/int8.out
@@ -887,6 +887,31 @@ SELECT ((-1::int8<<63)+1)::text;
  -9223372036854775807
 (1 row)

+-- check sane handling of out-of-range shift counts
+SELECT (-1::int8) << 63;
+       ?column?
+----------------------
+ -9223372036854775808
+(1 row)
+
+SELECT (-1::int8) >> 63;
+ ?column?
+----------
+       -1
+(1 row)
+
+SELECT 1::int8 << 63;
+       ?column?
+----------------------
+ -9223372036854775808
+(1 row)
+
+SELECT 1::int8 << 64;
+ERROR:  integer shift count out of range
+SELECT 1::int8 >> 64;
+ERROR:  integer shift count out of range
+SELECT 1::int8 << -1;
+ERROR:  integer shift count out of range
 -- check sane handling of INT64_MIN overflow cases
 SELECT (-9223372036854775808)::int8 * (-1)::int8;
 ERROR:  bigint out of range
diff --git a/src/test/regress/sql/int2.sql b/src/test/regress/sql/int2.sql
index df1e46d4e2e..0c099360f8b 100644
--- a/src/test/regress/sql/int2.sql
+++ b/src/test/regress/sql/int2.sql
@@ -91,6 +91,14 @@ SELECT i.f1, i.f1 / int4 '2' AS x FROM INT2_TBL i;
 SELECT (-1::int2<<15)::text;
 SELECT ((-1::int2<<15)+1::int2)::text;

+-- check sane handling of out-of-range shift counts
+SELECT (-1::int2) << 15;
+SELECT (-1::int2) >> 15;
+SELECT 1::int2 << 31;
+SELECT 1::int2 << 32;
+SELECT 1::int2 >> 32;
+SELECT 1::int2 << -1;
+
 -- check sane handling of INT16_MIN overflow cases
 SELECT (-32768)::int2 * (-1)::int2;
 SELECT (-32768)::int2 / (-1)::int2;
diff --git a/src/test/regress/sql/int4.sql b/src/test/regress/sql/int4.sql
index d27f5500e01..e8bf7553642 100644
--- a/src/test/regress/sql/int4.sql
+++ b/src/test/regress/sql/int4.sql
@@ -118,6 +118,15 @@ SELECT (2 + 2) / 2 AS two;
 SELECT (-1::int4<<31)::text;
 SELECT ((-1::int4<<31)+1)::text;

+-- check sane handling of out-of-range shift counts
+SELECT (-1::int4) << 31;
+SELECT (-1::int4) >> 31;
+SELECT 1::int4 << 31;
+SELECT 1::int4 << 32;
+SELECT 1::int4 >> 32;
+SELECT 1::int4 << -1;
+SELECT 1::int4 >> -1;
+
 -- check sane handling of INT_MIN overflow cases
 SELECT (-2147483648)::int4 * (-1)::int4;
 SELECT (-2147483648)::int4 / (-1)::int4;
diff --git a/src/test/regress/sql/int8.sql b/src/test/regress/sql/int8.sql
index 53359f06ade..9de4caa39aa 100644
--- a/src/test/regress/sql/int8.sql
+++ b/src/test/regress/sql/int8.sql
@@ -205,6 +205,14 @@ SELECT * FROM generate_series('+4567890123456789'::int8, '+4567890123456799'::in
 SELECT (-1::int8<<63)::text;
 SELECT ((-1::int8<<63)+1)::text;

+-- check sane handling of out-of-range shift counts
+SELECT (-1::int8) << 63;
+SELECT (-1::int8) >> 63;
+SELECT 1::int8 << 63;
+SELECT 1::int8 << 64;
+SELECT 1::int8 >> 64;
+SELECT 1::int8 << -1;
+
 -- check sane handling of INT64_MIN overflow cases
 SELECT (-9223372036854775808)::int8 * (-1)::int8;
 SELECT (-9223372036854775808)::int8 / (-1)::int8;
--
2.53.0

