Subject: Assert in pageinspect with NULL pages

From: Daria Lepikhova <[email-masked]>
Date: Thu, 17 Feb 2022 13:46:40 +0500

Hi, hackers!

If we trying to call pageinspect functions for pages which are filled 
with nulls, we will get core dump. It happens with null pages for all 
indexes in pageinspect and for page_checksum. This problem was founded 
firstly by Anastasia Lubennikova, and now I continue this task.

For example, next script leads to fail.
CREATE TABLE test1 (
     x bigserial,
     y bigint DEFAULT 0
);
INSERT INTO test1(y) SELECT 0 FROM generate_series(1,1E6) AS x;
SELECT page_checksum(repeat(E'\\000', 8192)::bytea, 1);

server closed the connection unexpectedly
     This probably means the server terminated abnormally
     before or while processing the request.
fatal: connection to server was lost
LOG:  server process (PID 16465) was terminated by signal 6
DETAIL:  Failed process was running: select 
page_checksum(repeat(E'\\000', 8192)::bytea, 1);
LOG:  terminating any other active server processes
LOG:  all server processes terminated; reinitializing
LOG:  database system was interrupted; last known up at 2022-02-16 
14:03:16 +05
LOG:  database system was not properly shut down; automatic recovery in 
progress
LOG:  redo starts at 0/14F1B20
LOG:  invalid record length at 0/5D40CD8: wanted 24, got 0
LOG:  redo done at 0/5D40BC0 system usage: CPU: user: 0.98 s, system: 
0.02 s, elapsed: 1.01 s
LOG:  checkpoint starting: end-of-recovery immediate wait
LOG:  checkpoint complete: wrote 5500 buffers (33.6%); 0 WAL file(s) 
added, 0 removed, 4 recycled; write=0.064 s, sync=0.007 s, total=0.080 
s; sync files=45, longest=0.004 s, average=0.001 s; distance=74044 kB, 
estimate=74044 kB
LOG:  database system is ready to accept connections

Also it is possible to use
     select brin_metapage_info(repeat(E'\\000', 8192)::bytea);
or
     select bt_page_items(repeat(E'\\000', 8192)::bytea);
for getting fail.

I tried to make some additional checks for null pages into pageinspect' 
functions. The attached patch also contains tests for this case.
What do you think?

-- 
Daria Lepikhova
Postgres Professional
From 92d71d9b5c583f5f2733646a42c36e7e437ed0a1 Mon Sep 17 00:00:00 2001
From: "d.lepikhova" <[email-masked]>
Date: Tue, 15 Feb 2022 14:13:05 +0500
Subject: [PATCH] Add checks for null pages to pageinspect functions

---
 contrib/pageinspect/brinfuncs.c           | 12 ++++++++++++
 contrib/pageinspect/btreefuncs.c          | 12 ++++++++++++
 contrib/pageinspect/expected/brin.out     |  9 +++++++++
 contrib/pageinspect/expected/btree.out    |  3 +++
 contrib/pageinspect/expected/checksum.out |  3 +++
 contrib/pageinspect/expected/gin.out      |  7 +++++++
 contrib/pageinspect/expected/hash.out     |  9 +++++++++
 contrib/pageinspect/rawpage.c             | 12 ++++++++++++
 contrib/pageinspect/sql/brin.sql          |  6 ++++++
 contrib/pageinspect/sql/btree.sql         |  3 +++
 contrib/pageinspect/sql/checksum.sql      |  4 ++++
 contrib/pageinspect/sql/gin.sql           |  5 +++++
 contrib/pageinspect/sql/hash.sql          |  6 ++++++
 13 files changed, 91 insertions(+)

diff --git a/contrib/pageinspect/brinfuncs.c b/contrib/pageinspect/brinfuncs.c
index f1e64a39ef2..56129a40abb 100644
--- a/contrib/pageinspect/brinfuncs.c
+++ b/contrib/pageinspect/brinfuncs.c
@@ -63,6 +63,12 @@ brin_page_type(PG_FUNCTION_ARGS)
 				 errdetail("Expected size %d, got %d",
 						   BLCKSZ, raw_page_size)));
 
+	/* check that the page is initialized before accessing any fields */
+	if (PageIsNew(page))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("input page is empty")));
+
 	switch (BrinPageType(page))
 	{
 		case BRIN_PAGETYPE_META:
@@ -103,6 +109,12 @@ verify_brin_page(bytea *raw_page, uint16 type, const char *strtype)
 
 	page = VARDATA(raw_page);
 
+	/* check that the page is initialized before accessing any fields */
+	if (PageIsNew(page))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("input page is empty")));
+
 	/* verify the special space says this page is what we want */
 	if (BrinPageType(page) != type)
 		ereport(ERROR,
diff --git a/contrib/pageinspect/btreefuncs.c b/contrib/pageinspect/btreefuncs.c
index 03debe336ba..7af35da19f3 100644
--- a/contrib/pageinspect/btreefuncs.c
+++ b/contrib/pageinspect/btreefuncs.c
@@ -519,6 +519,12 @@ bt_page_items_internal(PG_FUNCTION_ARGS, enum pageinspect_version ext_version)
 		UnlockReleaseBuffer(buffer);
 		relation_close(rel, AccessShareLock);
 
+		/* check that the page is initialized before accessing any fields */
+		if (PageIsNew(uargs->page))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("input page is empty")));
+
 		uargs->offset = FirstOffsetNumber;
 
 		opaque = (BTPageOpaque) PageGetSpecialPointer(uargs->page);
@@ -615,6 +621,12 @@ bt_page_items_bytea(PG_FUNCTION_ARGS)
 
 		uargs->page = VARDATA(raw_page);
 
+		/* check that the page is initialized before accessing any fields */
+		if (PageIsNew(uargs->page))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("input page is empty")));
+
 		uargs->offset = FirstOffsetNumber;
 
 		opaque = (BTPageOpaque) PageGetSpecialPointer(uargs->page);
diff --git a/contrib/pageinspect/expected/brin.out b/contrib/pageinspect/expected/brin.out
index 71eb190380c..8e309a2a5d1 100644
--- a/contrib/pageinspect/expected/brin.out
+++ b/contrib/pageinspect/expected/brin.out
@@ -48,4 +48,13 @@ SELECT * FROM brin_page_items(get_raw_page('test1_a_idx', 2), 'test1_a_idx')
           1 |      0 |      1 | f        | f        | f           | {1 .. 1}
 (1 row)
 
+-- Check that all functions fail gracefully with an empty page imput
+select brin_page_type(repeat(E'\\000', 8192)::bytea);
+ERROR:  input page is empty
+select brin_metapage_info(repeat(E'\\000', 8192)::bytea);
+ERROR:  input page is empty
+select brin_revmap_data(repeat(E'\\000', 8192)::bytea);
+ERROR:  input page is empty
+select brin_page_items(repeat(E'\\000', 8192)::bytea, 'test1_a_idx');
+ERROR:  input page is empty
 DROP TABLE test1;
diff --git a/contrib/pageinspect/expected/btree.out b/contrib/pageinspect/expected/btree.out
index c60bc88560c..cc6c4181c85 100644
--- a/contrib/pageinspect/expected/btree.out
+++ b/contrib/pageinspect/expected/btree.out
@@ -70,4 +70,7 @@ tids       |
 
 SELECT * FROM bt_page_items(get_raw_page('test1_a_idx', 2));
 ERROR:  block number 2 is out of range for relation "test1_a_idx"
+-- Check that bt_page_items() fails gracefully with an empty page input
+select bt_page_items(repeat(E'\\000', 8192)::bytea);
+ERROR:  input page is empty
 DROP TABLE test1;
diff --git a/contrib/pageinspect/expected/checksum.out b/contrib/pageinspect/expected/checksum.out
index a85388e158e..ff07a879216 100644
--- a/contrib/pageinspect/expected/checksum.out
+++ b/contrib/pageinspect/expected/checksum.out
@@ -38,3 +38,6 @@ SELECT blkno,
    100 |        1139 |       28438 |        3648 |        -30881 |        -16305 |        -27349
 (3 rows)
 
+ --Check that page_checksum() with an empty page fails gracefully
+ select page_checksum(repeat(E'\\000', 8192)::bytea, 1);
+ERROR:  input page is empty
diff --git a/contrib/pageinspect/expected/gin.out b/contrib/pageinspect/expected/gin.out
index ef7570b9723..ef0fdb59565 100644
--- a/contrib/pageinspect/expected/gin.out
+++ b/contrib/pageinspect/expected/gin.out
@@ -35,4 +35,11 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
 -[ RECORD 1 ]
 ?column? | t
 
+-- Check that all functions fail gracefully with an empty page imput
+select gin_metapage_info(repeat(E'\\000', 8192)::bytea);
+ERROR:  input page is empty
+select gin_page_opaque_info(repeat(E'\\000', 8192)::bytea);
+ERROR:  input page is empty
+select gin_leafpage_items(repeat(E'\\000', 8192)::bytea);
+ERROR:  input page is empty
 DROP TABLE test1;
diff --git a/contrib/pageinspect/expected/hash.out b/contrib/pageinspect/expected/hash.out
index bd0628d0136..271f33bb481 100644
--- a/contrib/pageinspect/expected/hash.out
+++ b/contrib/pageinspect/expected/hash.out
@@ -163,4 +163,13 @@ SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 4));
 
 SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 5));
 ERROR:  page is not a hash bucket or overflow page
+-- Check that all functions fail gracefully with an empty page imput
+select hash_page_type(repeat(E'\\000', 8192)::bytea);
+ERROR:  input page is empty
+select hash_metapage_info(repeat(E'\\000', 8192)::bytea);
+ERROR:  input page is empty
+select hash_page_stats(repeat(E'\\000', 8192)::bytea);
+ERROR:  input page is empty
+select hash_page_items(repeat(E'\\000', 8192)::bytea);
+ERROR:  input page is empty
 DROP TABLE test_hash;
diff --git a/contrib/pageinspect/rawpage.c b/contrib/pageinspect/rawpage.c
index 7e41af045f3..8a7bfccfcc3 100644
--- a/contrib/pageinspect/rawpage.c
+++ b/contrib/pageinspect/rawpage.c
@@ -230,6 +230,12 @@ get_page_from_raw(bytea *raw_page)
 
 	memcpy(page, VARDATA_ANY(raw_page), raw_page_size);
 
+	/* check that the page is initialized */
+	if (PageIsNew((Page) page))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("input page is empty")));
+
 	return page;
 }
 
@@ -375,6 +381,12 @@ page_checksum_internal(PG_FUNCTION_ARGS, enum pageinspect_version ext_version)
 
 	page = (PageHeader) VARDATA(raw_page);
 
+	/* check that the page is initialized */
+	if (PageIsNew((Page) page))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("input page is empty")));
+
 	PG_RETURN_INT16(pg_checksum_page((char *) page, blkno));
 }
 
diff --git a/contrib/pageinspect/sql/brin.sql b/contrib/pageinspect/sql/brin.sql
index 735bc3b6733..da810adf62b 100644
--- a/contrib/pageinspect/sql/brin.sql
+++ b/contrib/pageinspect/sql/brin.sql
@@ -15,4 +15,10 @@ SELECT * FROM brin_revmap_data(get_raw_page('test1_a_idx', 1)) LIMIT 5;
 SELECT * FROM brin_page_items(get_raw_page('test1_a_idx', 2), 'test1_a_idx')
     ORDER BY blknum, attnum LIMIT 5;
 
+-- Check that all functions fail gracefully with an empty page imput
+select brin_page_type(repeat(E'\\000', 8192)::bytea);
+select brin_metapage_info(repeat(E'\\000', 8192)::bytea);
+select brin_revmap_data(repeat(E'\\000', 8192)::bytea);
+select brin_page_items(repeat(E'\\000', 8192)::bytea, 'test1_a_idx');
+
 DROP TABLE test1;
diff --git a/contrib/pageinspect/sql/btree.sql b/contrib/pageinspect/sql/btree.sql
index 96359179597..d2d99196077 100644
--- a/contrib/pageinspect/sql/btree.sql
+++ b/contrib/pageinspect/sql/btree.sql
@@ -21,4 +21,7 @@ SELECT * FROM bt_page_items(get_raw_page('test1_a_idx', 0));
 SELECT * FROM bt_page_items(get_raw_page('test1_a_idx', 1));
 SELECT * FROM bt_page_items(get_raw_page('test1_a_idx', 2));
 
+-- Check that bt_page_items() fails gracefully with an empty page input
+select bt_page_items(repeat(E'\\000', 8192)::bytea);
+
 DROP TABLE test1;
diff --git a/contrib/pageinspect/sql/checksum.sql b/contrib/pageinspect/sql/checksum.sql
index b877db0611f..56c4f6b52ed 100644
--- a/contrib/pageinspect/sql/checksum.sql
+++ b/contrib/pageinspect/sql/checksum.sql
@@ -29,3 +29,7 @@ SELECT blkno,
     page_checksum(decode(repeat('e6d6', :block_size / 2), 'hex'), blkno) AS checksum_e6d6,
     page_checksum(decode(repeat('4a5e', :block_size / 2), 'hex'), blkno) AS checksum_4a5e
   FROM generate_series(0, 100, 50) AS a (blkno);
+
+ --Check that page_checksum() with an empty page fails gracefully
+ select page_checksum(repeat(E'\\000', 8192)::bytea, 1);
+
diff --git a/contrib/pageinspect/sql/gin.sql b/contrib/pageinspect/sql/gin.sql
index 423f5c57499..559c570a644 100644
--- a/contrib/pageinspect/sql/gin.sql
+++ b/contrib/pageinspect/sql/gin.sql
@@ -18,4 +18,9 @@ FROM gin_leafpage_items(get_raw_page('test1_y_idx',
                         (pg_relation_size('test1_y_idx') /
                          current_setting('block_size')::bigint)::int - 1));
 
+-- Check that all functions fail gracefully with an empty page imput
+select gin_metapage_info(repeat(E'\\000', 8192)::bytea);
+select gin_page_opaque_info(repeat(E'\\000', 8192)::bytea);
+select gin_leafpage_items(repeat(E'\\000', 8192)::bytea);
+
 DROP TABLE test1;
diff --git a/contrib/pageinspect/sql/hash.sql b/contrib/pageinspect/sql/hash.sql
index 64f33f1d52f..4ef23cb94fe 100644
--- a/contrib/pageinspect/sql/hash.sql
+++ b/contrib/pageinspect/sql/hash.sql
@@ -78,5 +78,11 @@ SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 3));
 SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 4));
 SELECT * FROM hash_page_items(get_raw_page('test_hash_a_idx', 5));
 
+-- Check that all functions fail gracefully with an empty page imput
+select hash_page_type(repeat(E'\\000', 8192)::bytea);
+select hash_metapage_info(repeat(E'\\000', 8192)::bytea);
+select hash_page_stats(repeat(E'\\000', 8192)::bytea);
+select hash_page_items(repeat(E'\\000', 8192)::bytea);
+
 
 DROP TABLE test_hash;
-- 
2.25.1