From 48af47dcdd591d8deb0bd4d14652780d165ca879 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson Date: Tue, 1 Sep 2026 10:13:30 +0200 Subject: [PATCH] Handle PG_INT32_MIN negation overflow in right() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A negative n means "return all but the first |n| characters", so text_right() negates n before clipping. Negating PG_INT32_MIN overflows; with -fwrapv the result is PG_INT32_MIN again, still negative, and pg_mbcharcliplen() then returns an offset of zero, so the whole string is returned where the correct answer is an empty string: SELECT right('abcdef', (-2147483648)::int4); -- 'abcdef', want '' SELECT right('abcdef', -2147483647); -- '', correct Clamp to PG_INT32_MAX instead. Any n whose absolute value is at least the string's length skips all of it, and a text value cannot be longer than PG_INT32_MAX, so this gives the same answer for every other input. Erroring out, as text_format_string_conversion() does for a width of INT_MIN, would not be correct here: unlike a format width, an out-of-range skip count has a well-defined result. Using pg_neg_s32_overflow() would be a slightly more optimal fix but as it's only available in PostgreSQL 18 and later the decision was taken to apply the same fix to all backbranches. Backpatch to all supported versions. Author: Ewan Young Reviewed-by: Daniel Gustafsson Reviewed-by: Dagfinn Ilmari Mannsåker Reviewed-by: David Rowley Reviewed-by: Chao Li Discussion: https://postgr.es/m/CAON2xHNnBz-AcPJgDmd5_39+8qR5AUKEZk4X3ZM-0zdsATn8kQ@mail.gmail.com Backpatch-through: 14 --- src/backend/utils/adt/varlena.c | 12 +++++++++++- src/test/regress/expected/text.out | 8 ++++++++ src/test/regress/sql/text.sql | 3 +++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c index 3c6ea7e995f..2ef7b44f71f 100644 --- a/src/backend/utils/adt/varlena.c +++ b/src/backend/utils/adt/varlena.c @@ -5540,7 +5540,17 @@ text_right(PG_FUNCTION_ARGS) int off; if (n < 0) - n = -n; + { + /* + * Negating PG_INT32_MIN would overflow, so clamp instead. Any n whose + * absolute value is at least the string's length skips the whole + * string, and len can't exceed PG_INT32_MAX, so this is equivalent. + */ + if (unlikely(n == PG_INT32_MIN)) + n = PG_INT32_MAX; + else + n = -n; + } else n = pg_mbstrlen_with_len(p, len) - n; off = pg_mbcharcliplen(p, len, n); diff --git a/src/test/regress/expected/text.out b/src/test/regress/expected/text.out index 4c65b238e76..53f050bbd32 100644 --- a/src/test/regress/expected/text.out +++ b/src/test/regress/expected/text.out @@ -116,6 +116,14 @@ select i, left('ahoj', i), right('ahoj', i) from generate_series(-5, 5) t(i) ord 5 | ahoj | ahoj (11 rows) +-- the most negative value must skip the whole string, same as any other n +-- whose absolute value exceeds its length +select left('ahoj', (-2147483648)::int4), right('ahoj', (-2147483648)::int4); + left | right +------+------- + | +(1 row) + select quote_literal(''); quote_literal --------------- diff --git a/src/test/regress/sql/text.sql b/src/test/regress/sql/text.sql index 540e551254d..3a9be863bb7 100644 --- a/src/test/regress/sql/text.sql +++ b/src/test/regress/sql/text.sql @@ -37,6 +37,9 @@ select concat_ws('',10,20,null,30); select concat_ws(NULL,10,20,null,30) is null; select reverse('abcde'); select i, left('ahoj', i), right('ahoj', i) from generate_series(-5, 5) t(i) order by i; +-- the most negative value must skip the whole string, same as any other n +-- whose absolute value exceeds its length +select left('ahoj', (-2147483648)::int4), right('ahoj', (-2147483648)::int4); select quote_literal(''); select quote_literal('abc'''); select quote_literal(e'\\');