From 0b05cf02ce504890ece0261cc61246ec548eaae3 Mon Sep 17 00:00:00 2001 From: Tristan Partin Date: Thu, 30 Jul 2026 06:18:01 +0000 Subject: [PATCH v1 2/3] Fix hashchar() and hashcharextended() to not depend on char signedness hashchar() and hashcharextended() widened their "char" argument by casting it directly to int32: return hash_uint32((int32) PG_GETARG_CHAR(0)); DatumGetChar() returns a bare char, whose signedness is implementation-defined. A high-bit byte therefore sign-extends on platforms where char defaults to signed (e.g. x86-64, macOS) but not where it defaults to unsigned (e.g. aarch64 Linux), so the two functions returned different results for the same "char" value depending on which architecture computed them: SELECT hashchar(chr(128)::"char"); x86-64: 1361043915 (= hashint4(-128)) aarch64: 1807103465 (= hashint4( 128)) Fix by widening through GetDefaultCharSignedness(), the per-cluster flag already introduced by 44fe30fdab6 to give "char"-sensitive, disk-persisting computations a stable, platform-independent notion of char signedness (as already used by pg_trgm's trigram comparators). This makes the result depend on the cluster's recorded signedness rather than the signedness the server happens to be compiled with. Fixes: https://www.postgresql.org/message-id/19587-4416a590531c9c73%40postgresql.org Signed-off-by: Tristan Partin --- src/backend/access/hash/hashfunc.c | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/backend/access/hash/hashfunc.c b/src/backend/access/hash/hashfunc.c index 575342a21b6..245e313407b 100644 --- a/src/backend/access/hash/hashfunc.c +++ b/src/backend/access/hash/hashfunc.c @@ -26,6 +26,7 @@ #include "postgres.h" +#include "access/xlog.h" #include "common/hashfn.h" #include "utils/builtins.h" #include "utils/float.h" @@ -43,17 +44,37 @@ * routine is also used by dynahash tables. */ +/* + * Widen a "char" datum to int32 for hashing, honoring the cluster's recorded + * default char signedness (see GetDefaultCharSignedness()) rather than the + * signedness the server happens to be compiled with. + * + * A bare "char" has implementation-defined signedness, so casting it to int32 + * directly would sign-extend high-bit values on platforms where "char" defaults + * to signed (e.g. x86-64), but not on platforms where it defaults to unsigned + * (e.g. aarch64 Linux). That would make hashchar()'s result for such values + * depend on the architecture computing it, which is a problem for anything that + * persists the result, such as hash partitioning and hash indexes. See bug + * #19587. + */ +static inline int32 +char_hash_widen(const char ch) +{ + return GetDefaultCharSignedness() ? (int32) (signed char) ch : (int32) (unsigned char) ch; +} + /* Note: this is used for both "char" and boolean datatypes */ Datum hashchar(PG_FUNCTION_ARGS) { - return hash_uint32((int32) PG_GETARG_CHAR(0)); + return hash_uint32(char_hash_widen(PG_GETARG_CHAR(0))); } Datum hashcharextended(PG_FUNCTION_ARGS) { - return hash_uint32_extended((int32) PG_GETARG_CHAR(0), PG_GETARG_INT64(1)); + return hash_uint32_extended(char_hash_widen(PG_GETARG_CHAR(0)), + PG_GETARG_INT64(1)); } Datum -- Tristan Partin https://tristan.partin.io