From 310e38237e52b99b82045f2acabaa3751df0dbd3 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Tue, 18 Aug 2026 13:50:04 -0700
Subject: [PATCH v1] hashtext: fix fragile code.

Previously, in the path for non-deterministic collations, the code
assumed that bsize==rsize. That assumption seems to be true for ICU,
and all non-deterministic collations are ICU, so it's not known to be
an actual bug.

The only known place where bsize may not equal rsize is in the libc
provider, where strxfrm() can return an upper bound of the size needed
to store the result. That means the initial call to determine the
buffer size (with dest==NULL, n==0) could return a larger number than
the actual call with an adequate dest buffer. That's OK, because libc
locales are always deterministic.

Commit 679c5084cf2 partially fixed the assumption, but missed this
part. Fix it, and add a more prominent documentation note.

Backpatch-through: 16
---
 src/backend/access/hash/hashfunc.c     |  4 ++--
 src/backend/utils/adt/pg_locale.c      |  7 ++++---
 src/backend/utils/adt/pg_locale_libc.c | 10 ++++++++++
 src/backend/utils/adt/varchar.c        |  4 ++--
 4 files changed, 18 insertions(+), 7 deletions(-)

diff --git a/src/backend/access/hash/hashfunc.c b/src/backend/access/hash/hashfunc.c
index 575342a21b6..97c2c5a6a4c 100644
--- a/src/backend/access/hash/hashfunc.c
+++ b/src/backend/access/hash/hashfunc.c
@@ -310,7 +310,7 @@ hashtext(PG_FUNCTION_ARGS)
 		 * character in the hash, but it was done before and the behavior must
 		 * be preserved.
 		 */
-		result = hash_any((uint8_t *) buf, bsize + 1);
+		result = hash_any((uint8_t *) buf, rsize + 1);
 
 		pfree(buf);
 	}
@@ -365,7 +365,7 @@ hashtextextended(PG_FUNCTION_ARGS)
 		 * character in the hash, but it was done before and the behavior must
 		 * be preserved.
 		 */
-		result = hash_any_extended((uint8_t *) buf, bsize + 1,
+		result = hash_any_extended((uint8_t *) buf, rsize + 1,
 								   PG_GETARG_INT64(1));
 
 		pfree(buf);
diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 60e46a4fe02..e76f3fc8eec 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1506,9 +1506,10 @@ pg_strxfrm(char *dest, const char *src, size_t destsize, pg_locale_t locale)
  * pg_strxfrm_enabled() first, otherwise this function may return wrong
  * results or an error.
  *
- * Returns the number of bytes needed (or more) to store the transformed
- * string, excluding the terminating nul byte. If the value returned is
- * 'destsize' or greater, the resulting contents of 'dest' are undefined.
+ * Returns the number of bytes needed (NB: or more; see comments above
+ * strnxfrm_libc()) to store the transformed string, excluding the terminating
+ * nul byte. If the value returned is 'destsize' or greater, the resulting
+ * contents of 'dest' are undefined.
  */
 size_t
 pg_strnxfrm(char *dest, size_t destsize, const char *src, size_t srclen,
diff --git a/src/backend/utils/adt/pg_locale_libc.c b/src/backend/utils/adt/pg_locale_libc.c
index d1f55e145f5..d9a33db8de5 100644
--- a/src/backend/utils/adt/pg_locale_libc.c
+++ b/src/backend/utils/adt/pg_locale_libc.c
@@ -971,6 +971,11 @@ strcoll_libc(const char *arg1, const char *arg2, pg_locale_t locale)
  * strnxfrm_libc
  *
  * NUL-terminate src and pass to strxfrm_l().
+ *
+ * NB: it's possible for this function to return a different size needed for
+ * two calls with the same input string. If destsize is too small to hold the
+ * result, strxfrm() may return the upper bound of the size needed rather than
+ * the exact size needed.
  */
 static size_t
 strnxfrm_libc(char *dest, size_t destsize, const char *src, size_t srclen,
@@ -1001,6 +1006,11 @@ strnxfrm_libc(char *dest, size_t destsize, const char *src, size_t srclen,
 
 /*
  * strxfrm_libc
+ *
+ * NB: it's possible for this function to return a different size needed for
+ * two calls with the same input string. If destsize is too small to hold the
+ * result, strxfrm() may return the upper bound of the size needed rather than
+ * the exact size needed.
  */
 static size_t
 strxfrm_libc(char *dest, size_t destsize, const char *src, pg_locale_t locale)
diff --git a/src/backend/utils/adt/varchar.c b/src/backend/utils/adt/varchar.c
index 45b7ef185a1..be598cfaf23 100644
--- a/src/backend/utils/adt/varchar.c
+++ b/src/backend/utils/adt/varchar.c
@@ -1032,7 +1032,7 @@ hashbpchar(PG_FUNCTION_ARGS)
 		 * character in the hash, but it was done before and the behavior must
 		 * be preserved.
 		 */
-		result = hash_any((uint8_t *) buf, bsize + 1);
+		result = hash_any((uint8_t *) buf, rsize + 1);
 
 		pfree(buf);
 	}
@@ -1089,7 +1089,7 @@ hashbpcharextended(PG_FUNCTION_ARGS)
 		 * character in the hash, but it was done before and the behavior must
 		 * be preserved.
 		 */
-		result = hash_any_extended((uint8_t *) buf, bsize + 1,
+		result = hash_any_extended((uint8_t *) buf, rsize + 1,
 								   PG_GETARG_INT64(1));
 
 		pfree(buf);
-- 
2.43.0

