From 8d9fc7376ee8a10f949e62c2e0b9199e3dff48e3 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Sat, 26 Sep 2026 19:23:14 +0000
Subject: [PATCH v1 3/7] gin: reject invalid items in a decoded posting list

A corrupt page can hold a posting list that stays within bounds but decodes to
invalid items. Such as a first item or an accumulated value that yields offset
0, or an item that does not exceed its predecessor.

These were only Assert()ed, so they aborted an assert build and returned garbage
otherwise.  Reject them with an error instead.
---
 src/backend/access/gin/ginpostinglist.c | 35 +++++++++++++++++++++++--
 1 file changed, 33 insertions(+), 2 deletions(-)

diff --git a/src/backend/access/gin/ginpostinglist.c b/src/backend/access/gin/ginpostinglist.c
index f7dec6318c9..e66f69d04f7 100644
--- a/src/backend/access/gin/ginpostinglist.c
+++ b/src/backend/access/gin/ginpostinglist.c
@@ -287,6 +287,9 @@ ginPostingListDecodeAllSegments(GinPostingList *segment, int len, int *ndecoded_
 	ndecoded = 0;
 	while ((char *) segment < endseg)
 	{
+		OffsetNumber firstoff;
+		uint64		prev;
+
 		/*
 		 * Reject a segment that runs past the end of the posting list.
 		 * Compare sizes rather than forming segment +
@@ -300,6 +303,23 @@ ginPostingListDecodeAllSegments(GinPostingList *segment, int len, int *ndecoded_
 					(errcode(ERRCODE_DATA_CORRUPTED),
 					 errmsg("corrupted GIN posting list")));
 
+		/*
+		 * The first item's offset must fit in MaxHeapTuplesPerPageBits, as
+		 * itemptr_to_uint64() below requires.  That is tighter than
+		 * OffsetNumberIsValid(), which is why the range is open-coded here.
+		 * Read it with the No-Check accessor, since the checking one would
+		 * Assert() on the corrupt value being rejected.  The last clause keeps
+		 * items ascending across segment boundaries.
+		 */
+		firstoff = GinItemPointerGetOffsetNumber(&segment->first);
+		if (firstoff == InvalidOffsetNumber ||
+			firstoff >= (1 << MaxHeapTuplesPerPageBits) ||
+			(ndecoded > 0 &&
+			 ginCompareItemPointers(&segment->first, &result[ndecoded - 1]) <= 0))
+			ereport(ERROR,
+					(errcode(ERRCODE_DATA_CORRUPTED),
+					 errmsg("corrupted GIN posting list")));
+
 		/* enlarge output array if needed */
 		if (ndecoded >= nallocated)
 		{
@@ -308,12 +328,11 @@ ginPostingListDecodeAllSegments(GinPostingList *segment, int len, int *ndecoded_
 		}
 
 		/* copy the first item */
-		Assert(OffsetNumberIsValid(ItemPointerGetOffsetNumber(&segment->first)));
-		Assert(ndecoded == 0 || ginCompareItemPointers(&segment->first, &result[ndecoded - 1]) > 0);
 		result[ndecoded] = segment->first;
 		ndecoded++;
 
 		val = itemptr_to_uint64(&segment->first);
+		prev = val;
 		ptr = segment->bytes;
 		endptr = segment->bytes + segment->nbytes;
 		while (ptr < endptr)
@@ -327,6 +346,18 @@ ginPostingListDecodeAllSegments(GinPostingList *segment, int len, int *ndecoded_
 
 			val += decode_varbyte(&ptr, endptr);
 
+			/*
+			 * Reject offset 0 and a non-increasing item.  Neither appears in a
+			 * valid list, and uint64_to_itemptr() below Asserts on offset 0, so
+			 * this has to run before it.
+			 */
+			if ((val & ((1 << MaxHeapTuplesPerPageBits) - 1)) == 0 ||
+				val <= prev)
+				ereport(ERROR,
+						(errcode(ERRCODE_DATA_CORRUPTED),
+						 errmsg("corrupted GIN posting list")));
+			prev = val;
+
 			uint64_to_itemptr(val, &result[ndecoded]);
 			ndecoded++;
 		}
-- 
2.17.1

