From 6be1bf5c3d245dcd2f01f345c716ddf843cc0d13 Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Fri, 25 Sep 2026 17:29:58 +0000
Subject: [PATCH v1 4/7] pageinspect: add tests for corrupt GIN posting lists

Check that gin_leafpage_items() reports a corrupted GIN posting list rather
than aborting or decoding past the page, for an unterminated varbyte stream,
an item that decodes to offset 0, and a first item with offset 0.
---
 contrib/pageinspect/expected/gin.out | 22 ++++++++++++++++++++++
 contrib/pageinspect/sql/gin.sql      | 19 +++++++++++++++++++
 2 files changed, 41 insertions(+)

diff --git a/contrib/pageinspect/expected/gin.out b/contrib/pageinspect/expected/gin.out
index ff1da6a5a17..c1582cbea36 100644
--- a/contrib/pageinspect/expected/gin.out
+++ b/contrib/pageinspect/expected/gin.out
@@ -53,6 +53,28 @@ SELECT * FROM gin_page_opaque_info(get_raw_page('test1', 0));
 ERROR:  input page is not a valid GIN data leaf page
 SELECT * FROM gin_leafpage_items(get_raw_page('test1', 0));
 ERROR:  input page is not a valid GIN data leaf page
+-- corrupt posting list on the last leaf page.  The first GinPostingList
+-- begins 32 bytes into the page, so on any block size its first item's
+-- offset is at bytes 36-37 and its varbyte stream starts at byte 40
+-- (set_byte is 0-based, overlay 1-based).  Each of these must be reported,
+-- not decoded past.
+SELECT (pg_relation_size('test1_y_idx') /
+        current_setting('block_size')::bigint)::int - 1 AS ln \gset
+-- a varbyte stream that never terminates (seven continuation bytes)
+SELECT gin_leafpage_items(overlay(get_raw_page('test1_y_idx', :ln)
+                                  PLACING '\x80808080808080'::bytea FROM 41));
+ERROR:  corrupted GIN posting list
+-- an item that decodes to offset 0, with the first item's offset set to 1
+-- and the first delta to 2047, so the running value reaches 2048
+SELECT gin_leafpage_items(set_byte(set_byte(set_byte(set_byte(
+                                  get_raw_page('test1_y_idx', :ln),
+                                  36, 1), 37, 0), 40, 255), 41, 15));
+ERROR:  corrupted GIN posting list
+-- a segment whose first item has an invalid offset of 0
+SELECT gin_leafpage_items(set_byte(set_byte(
+                                  get_raw_page('test1_y_idx', :ln),
+                                  36, 0), 37, 0));
+ERROR:  corrupted GIN posting list
 \set VERBOSITY default
 -- Tests with all-zero pages.
 SHOW block_size \gset
diff --git a/contrib/pageinspect/sql/gin.sql b/contrib/pageinspect/sql/gin.sql
index b57466d7ebf..5f41ba54b34 100644
--- a/contrib/pageinspect/sql/gin.sql
+++ b/contrib/pageinspect/sql/gin.sql
@@ -30,6 +30,25 @@ SELECT gin_page_opaque_info('ccc'::bytea);
 SELECT * FROM gin_metapage_info(get_raw_page('test1', 0));
 SELECT * FROM gin_page_opaque_info(get_raw_page('test1', 0));
 SELECT * FROM gin_leafpage_items(get_raw_page('test1', 0));
+-- corrupt posting list on the last leaf page.  The first GinPostingList
+-- begins 32 bytes into the page, so on any block size its first item's
+-- offset is at bytes 36-37 and its varbyte stream starts at byte 40
+-- (set_byte is 0-based, overlay 1-based).  Each of these must be reported,
+-- not decoded past.
+SELECT (pg_relation_size('test1_y_idx') /
+        current_setting('block_size')::bigint)::int - 1 AS ln \gset
+-- a varbyte stream that never terminates (seven continuation bytes)
+SELECT gin_leafpage_items(overlay(get_raw_page('test1_y_idx', :ln)
+                                  PLACING '\x80808080808080'::bytea FROM 41));
+-- an item that decodes to offset 0, with the first item's offset set to 1
+-- and the first delta to 2047, so the running value reaches 2048
+SELECT gin_leafpage_items(set_byte(set_byte(set_byte(set_byte(
+                                  get_raw_page('test1_y_idx', :ln),
+                                  36, 1), 37, 0), 40, 255), 41, 15));
+-- a segment whose first item has an invalid offset of 0
+SELECT gin_leafpage_items(set_byte(set_byte(
+                                  get_raw_page('test1_y_idx', :ln),
+                                  36, 0), 37, 0));
 \set VERBOSITY default
 
 -- Tests with all-zero pages.
-- 
2.17.1

