From 23c04b263ef298a56dd8dc974c77fa2a86132b2f Mon Sep 17 00:00:00 2001
From: Ewan Young <kdbase.hack@gmail.com>
Date: Mon, 7 Sep 2026 23:22:25 +0800
Subject: [PATCH v5] Fix parallel GIN index build with keys larger than 65535
 bytes

During a parallel GIN build, each key is serialized into a GinTuple, a
transient representation used only while sorting.  _gin_build_tuple()
lays out the whole tuple -- the palloc size, the key memcpy, and the
offset of the posting list -- from a local variable holding the real
key length, but then stored that length in GinTuple.keylen, which was
uint16.  For a key longer than 65535 bytes the stored length was thus
silently truncated.

On read-back, GinTupleGetFirst() and _gin_parse_tuple_items()
recompute the posting-list offset from the truncated keylen and land
inside the key data, so ginPostingListDecodeAllSegments() decodes
garbage: an assertion failure with assertions enabled, and a read past
the end of the allocation without.  Only parallel builds are affected,
because only they materialize a GinTuple; a serial build of the same
data succeeds, as index_form_tuple() compresses large keys before the
GinMaxItemSize check.

Widen GinTuple.keylen to Size, which is what VARSIZE_ANY() returns and
what GinBuffer.keylen already uses, and use Size for the local in
_gin_build_tuple() too, which was an int that truncated VARSIZE_ANY()
the same way.

Widening keylen moves GinTuple.data. The key value is accessed in place
in data, so data must be MAXALIGN'ed; before, that was only true by
accident of the field layout. This branch predates the use of C11
alignas, so force the alignment the way PGAlignedBlock did: put keylen
in a union with double and int64 members, which puts data at a
MAXALIGN'ed offset on all platforms, including 32-bit ones where Size is
4 bytes and MAXIMUM_ALIGNOF is 8. A static assertion verifies the
resulting offset.

Bug: #19545
Reported-by: Yuelin Wang <1217816127@qq.com>
Discussion: https://postgr.es/m/19545-0f25b7e47351e8fc@postgresql.org
---
 src/backend/access/gin/gininsert.c | 12 ++++++------
 src/include/access/gin_tuple.h     | 21 +++++++++++++++++++--
 2 files changed, 25 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/gin/gininsert.c b/src/backend/access/gin/gininsert.c
index a575afacab3..fb660e4f0c4 100644
--- a/src/backend/access/gin/gininsert.c
+++ b/src/backend/access/gin/gininsert.c
@@ -1454,7 +1454,7 @@ GinBufferStoreTuple(GinBuffer *buffer, GinTuple *tup)
 	if (GinBufferIsEmpty(buffer))
 	{
 		buffer->category = tup->category;
-		buffer->keylen = tup->keylen;
+		buffer->keylen = tup->k.keylen;
 		buffer->attnum = tup->attrnum;
 
 		buffer->typlen = tup->typlen;
@@ -2238,7 +2238,7 @@ _gin_build_tuple(OffsetNumber attrnum, unsigned char category,
 	char	   *ptr;
 
 	Size		tuplen;
-	int			keylen;
+	Size		keylen;
 
 	dlist_mutable_iter iter;
 	dlist_head	segments;
@@ -2314,7 +2314,7 @@ _gin_build_tuple(OffsetNumber attrnum, unsigned char category,
 	tuple->tuplen = tuplen;
 	tuple->attrnum = attrnum;
 	tuple->category = category;
-	tuple->keylen = keylen;
+	tuple->k.keylen = keylen;
 	tuple->nitems = nitems;
 
 	/* key type info */
@@ -2387,7 +2387,7 @@ _gin_parse_tuple_key(GinTuple *a)
 
 	if (a->typbyval)
 	{
-		memcpy(&key, a->data, a->keylen);
+		memcpy(&key, a->data, a->k.keylen);
 		return key;
 	}
 
@@ -2406,8 +2406,8 @@ _gin_parse_tuple_items(GinTuple *a)
 	int			ndecoded;
 	ItemPointer items;
 
-	len = a->tuplen - SHORTALIGN(offsetof(GinTuple, data) + a->keylen);
-	ptr = (char *) a + SHORTALIGN(offsetof(GinTuple, data) + a->keylen);
+	len = a->tuplen - SHORTALIGN(offsetof(GinTuple, data) + a->k.keylen);
+	ptr = (char *) a + SHORTALIGN(offsetof(GinTuple, data) + a->k.keylen);
 
 	items = ginPostingListDecodeAllSegments((GinPostingList *) ptr, len, &ndecoded);
 
diff --git a/src/include/access/gin_tuple.h b/src/include/access/gin_tuple.h
index 702f7d12889..27da458650e 100644
--- a/src/include/access/gin_tuple.h
+++ b/src/include/access/gin_tuple.h
@@ -21,7 +21,21 @@ typedef struct GinTuple
 {
 	int			tuplen;			/* length of the whole tuple */
 	OffsetNumber attrnum;		/* attnum of index key */
-	uint16		keylen;			/* bytes in data for key value */
+
+	/*
+	 * The key value is accessed in place, so data (below) must be aligned
+	 * well enough for any key type.  We include both "double" and "int64" in
+	 * the union to ensure that the compiler knows it must be MAXALIGN'ed (cf.
+	 * configure's computation of MAXIMUM_ALIGNOF); together with the fields
+	 * that follow it, this puts data at a MAXALIGN'ed offset, which the
+	 * static assertion below verifies.
+	 */
+	union
+	{
+		Size		keylen;		/* bytes in data for key value */
+		double		force_align_d;
+		int64		force_align_i64;
+	}			k;
 	int16		typlen;			/* typlen for key */
 	bool		typbyval;		/* typbyval for key */
 	signed char category;		/* category: normal or NULL? */
@@ -29,12 +43,15 @@ typedef struct GinTuple
 	char		data[FLEXIBLE_ARRAY_MEMBER];
 } GinTuple;
 
+StaticAssertDecl(offsetof(GinTuple, data) % MAXIMUM_ALIGNOF == 0,
+				 "GinTuple.data must be MAXALIGN'ed");
+
 static inline ItemPointer
 GinTupleGetFirst(GinTuple *tup)
 {
 	GinPostingList *list;
 
-	list = (GinPostingList *) SHORTALIGN(tup->data + tup->keylen);
+	list = (GinPostingList *) SHORTALIGN(tup->data + tup->k.keylen);
 
 	return &list->first;
 }
-- 
2.47.3

