postgres.git / summary / log / commit / refs

commit    858fb27b69bc81b2ac2dab9e4db4df4056cc2fae
Author:   Daniel Gustafsson <dgustafsson@postgresql.org>
Date:     Tue Sep 01 08:13:30 2026 +0000

    Handle PG_INT32_MIN negation overflow in right()
    
    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 <kdbase.hack@gmail.com>
    Reviewed-by: Daniel Gustafsson <daniel@yesql.se>
    Reviewed-by: Dagfinn Ilmari Mannsåker <ilmari@ilmari.org>
    Reviewed-by: David Rowley <dgrowleyml@gmail.com>
    Reviewed-by: Chao Li <li.evan.chao@gmail.com>
    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 b4d1f65a20e..257019a3740 100644 --- a/src/backend/utils/adt/varlena.c +++ b/src/backend/utils/adt/varlena.c @@ -5673,7 +5673,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 b625b09f32d..43fa095862e 100644 --- a/src/test/regress/expected/text.out +++ b/src/test/regress/expected/text.out @@ -118,6 +118,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 56eee69abc6..779b858b29e 100644 --- a/src/test/regress/sql/text.sql +++ b/src/test/regress/sql/text.sql @@ -41,6 +41,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'\\'); [parent: f1c6f7ec95dc]