v11-0002-Adding-function-verify_heapam-to-amcheck-module.patch

application/octet-stream

Filename: v11-0002-Adding-function-verify_heapam-to-amcheck-module.patch
Type: application/octet-stream
Part: 1
Message: Re: new heapcheck contrib module

Patch

Format: format-patch
Series: patch v11-0002
Subject: Adding function verify_heapam to amcheck module
File+
contrib/amcheck/amcheck--1.2--1.3.sql 54 0
contrib/amcheck/amcheck.h 5 0
contrib/amcheck/expected/check_heap.out 67 0
contrib/amcheck/expected/disallowed_reltypes.out 48 0
contrib/amcheck/Makefile 4 1
contrib/amcheck/sql/check_heap.sql 19 0
contrib/amcheck/sql/disallowed_reltypes.sql 48 0
contrib/amcheck/t/001_verify_heapam.pl 94 0
contrib/amcheck/verify_heapam.c 1153 0
doc/src/sgml/amcheck.sgml 105 1
src/backend/access/heap/hio.c 11 0
From a92f063a0deac08503d7f605207fca60542b325b Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Mon, 20 Jul 2020 12:37:48 -0700
Subject: [PATCH v11 2/3] Adding function verify_heapam to amcheck module

Adding new function verify_heapam for checking a heap relation and
associated toast relation, if any, to contrib/amcheck.
---
 contrib/amcheck/Makefile                      |    5 +-
 contrib/amcheck/amcheck--1.2--1.3.sql         |   54 +
 contrib/amcheck/amcheck.h                     |    5 +
 contrib/amcheck/expected/check_heap.out       |   67 +
 .../amcheck/expected/disallowed_reltypes.out  |   48 +
 contrib/amcheck/sql/check_heap.sql            |   19 +
 contrib/amcheck/sql/disallowed_reltypes.sql   |   48 +
 contrib/amcheck/t/001_verify_heapam.pl        |   94 ++
 contrib/amcheck/verify_heapam.c               | 1153 +++++++++++++++++
 doc/src/sgml/amcheck.sgml                     |  106 +-
 src/backend/access/heap/hio.c                 |   11 +
 11 files changed, 1608 insertions(+), 2 deletions(-)
 create mode 100644 contrib/amcheck/amcheck--1.2--1.3.sql
 create mode 100644 contrib/amcheck/amcheck.h
 create mode 100644 contrib/amcheck/expected/check_heap.out
 create mode 100644 contrib/amcheck/expected/disallowed_reltypes.out
 create mode 100644 contrib/amcheck/sql/check_heap.sql
 create mode 100644 contrib/amcheck/sql/disallowed_reltypes.sql
 create mode 100644 contrib/amcheck/t/001_verify_heapam.pl
 create mode 100644 contrib/amcheck/verify_heapam.c

diff --git a/contrib/amcheck/Makefile b/contrib/amcheck/Makefile
index b288c28fa0..27d38b2e86 100644
--- a/contrib/amcheck/Makefile
+++ b/contrib/amcheck/Makefile
@@ -3,13 +3,16 @@
 MODULE_big	= amcheck
 OBJS = \
 	$(WIN32RES) \
+	verify_heapam.o \
 	verify_nbtree.o
 
 EXTENSION = amcheck
 DATA = amcheck--1.2--1.3.sql amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
 PGFILEDESC = "amcheck - function for verifying relation integrity"
 
-REGRESS = check check_btree
+REGRESS = check check_btree check_heap disallowed_reltypes
+
+TAP_TESTS = 1
 
 ifdef USE_PGXS
 PG_CONFIG = pg_config
diff --git a/contrib/amcheck/amcheck--1.2--1.3.sql b/contrib/amcheck/amcheck--1.2--1.3.sql
new file mode 100644
index 0000000000..df418a850b
--- /dev/null
+++ b/contrib/amcheck/amcheck--1.2--1.3.sql
@@ -0,0 +1,54 @@
+/* contrib/amcheck/amcheck--1.2--1.3.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION amcheck UPDATE TO '1.3'" to load this file. \quit
+
+-- In order to avoid issues with dependencies when updating amcheck to 1.3,
+-- create new, overloaded version of the 1.2 function signature
+
+--
+-- verify_heapam()
+--
+CREATE FUNCTION verify_heapam(rel regclass,
+							  on_error_stop boolean default false,
+							  skip cstring default 'none',
+							  startblock bigint default null,
+							  endblock bigint default null,
+							  blkno OUT bigint,
+							  offnum OUT integer,
+							  lp_off OUT smallint,
+							  lp_flags OUT smallint,
+							  lp_len OUT smallint,
+							  attnum OUT integer,
+							  chunk OUT integer,
+							  msg OUT text
+							  )
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'verify_heapam'
+LANGUAGE C;
+
+-- Don't want this to be available to public
+REVOKE ALL ON FUNCTION verify_heapam(regclass, boolean, cstring, bigint, bigint)
+FROM PUBLIC;
+
+--
+-- verify_btreeam()
+--
+CREATE FUNCTION verify_btreeam(rel regclass,
+							   blkno OUT bigint,
+							   msg OUT text)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'verify_btreeam'
+LANGUAGE C;
+
+CREATE FUNCTION verify_btreeam(rel regclass,
+							   on_error_stop boolean,
+							   blkno OUT bigint,
+							   msg OUT text)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'verify_btreeam'
+LANGUAGE C;
+
+-- Don't want this to be available to public
+REVOKE ALL ON FUNCTION verify_btreeam(regclass) FROM PUBLIC;
+REVOKE ALL ON FUNCTION verify_btreeam(regclass, boolean) FROM PUBLIC;
diff --git a/contrib/amcheck/amcheck.h b/contrib/amcheck/amcheck.h
new file mode 100644
index 0000000000..74edfc2f65
--- /dev/null
+++ b/contrib/amcheck/amcheck.h
@@ -0,0 +1,5 @@
+#include "postgres.h"
+
+Datum		verify_heapam(PG_FUNCTION_ARGS);
+Datum		bt_index_check(PG_FUNCTION_ARGS);
+Datum		bt_index_parent_check(PG_FUNCTION_ARGS);
diff --git a/contrib/amcheck/expected/check_heap.out b/contrib/amcheck/expected/check_heap.out
new file mode 100644
index 0000000000..4175bb2d37
--- /dev/null
+++ b/contrib/amcheck/expected/check_heap.out
@@ -0,0 +1,67 @@
+CREATE TABLE heaptest (a integer, b text);
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'rope');
+ERROR:  unrecognized parameter for 'skip': rope
+HINT:  please choose from 'all-visible', 'all-frozen', or 'none'
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'none');
+ blkno | offnum | lp_off | lp_flags | lp_len | attnum | chunk | msg 
+-------+--------+--------+----------+--------+--------+-------+-----
+(0 rows)
+
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'all-frozen');
+ blkno | offnum | lp_off | lp_flags | lp_len | attnum | chunk | msg 
+-------+--------+--------+----------+--------+--------+-------+-----
+(0 rows)
+
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'all-visible');
+ blkno | offnum | lp_off | lp_flags | lp_len | attnum | chunk | msg 
+-------+--------+--------+----------+--------+--------+-------+-----
+(0 rows)
+
+SELECT * FROM verify_heapam(rel := 'heaptest', startblock := 0, endblock := 0);
+ERROR:  starting block 0 is out of bounds for relation with no blocks
+INSERT INTO heaptest (a, b)
+	(SELECT gs, repeat('x', gs)
+		FROM generate_series(1,10000) gs);
+SELECT * FROM verify_heapam(rel := 'heaptest', startblock := 100000, endblock := 200000);
+ERROR:  block range 100000 .. 200000 is out of bounds for relation with block count 370
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'none');
+ blkno | offnum | lp_off | lp_flags | lp_len | attnum | chunk | msg 
+-------+--------+--------+----------+--------+--------+-------+-----
+(0 rows)
+
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'all-frozen');
+ blkno | offnum | lp_off | lp_flags | lp_len | attnum | chunk | msg 
+-------+--------+--------+----------+--------+--------+-------+-----
+(0 rows)
+
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'all-visible');
+ blkno | offnum | lp_off | lp_flags | lp_len | attnum | chunk | msg 
+-------+--------+--------+----------+--------+--------+-------+-----
+(0 rows)
+
+SELECT * FROM verify_heapam(rel := 'heaptest', startblock := 0, endblock := 0);
+ blkno | offnum | lp_off | lp_flags | lp_len | attnum | chunk | msg 
+-------+--------+--------+----------+--------+--------+-------+-----
+(0 rows)
+
+VACUUM FREEZE heaptest;
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'none');
+ blkno | offnum | lp_off | lp_flags | lp_len | attnum | chunk | msg 
+-------+--------+--------+----------+--------+--------+-------+-----
+(0 rows)
+
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'all-frozen');
+ blkno | offnum | lp_off | lp_flags | lp_len | attnum | chunk | msg 
+-------+--------+--------+----------+--------+--------+-------+-----
+(0 rows)
+
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'all-visible');
+ blkno | offnum | lp_off | lp_flags | lp_len | attnum | chunk | msg 
+-------+--------+--------+----------+--------+--------+-------+-----
+(0 rows)
+
+SELECT * FROM verify_heapam(rel := 'heaptest', startblock := 0, endblock := 0);
+ blkno | offnum | lp_off | lp_flags | lp_len | attnum | chunk | msg 
+-------+--------+--------+----------+--------+--------+-------+-----
+(0 rows)
+
diff --git a/contrib/amcheck/expected/disallowed_reltypes.out b/contrib/amcheck/expected/disallowed_reltypes.out
new file mode 100644
index 0000000000..892ae89652
--- /dev/null
+++ b/contrib/amcheck/expected/disallowed_reltypes.out
@@ -0,0 +1,48 @@
+--
+-- check that using the module's functions with unsupported relations will fail
+--
+-- partitioned tables (the parent ones) don't have visibility maps
+create table test_partitioned (a int, b text default repeat('x', 5000))
+			 partition by list (a);
+-- these should all fail
+select * from verify_heapam('test_partitioned',
+							on_error_stop := false,
+							skip := NULL,
+							startblock := NULL,
+							endblock := NULL);
+ERROR:  "test_partitioned" is not a table, materialized view, or TOAST table
+create table test_partition partition of test_partitioned for values in (1);
+create index test_index on test_partition (a);
+-- indexes do not, so these all fail
+select * from verify_heapam('test_index',
+							on_error_stop := false,
+							skip := NULL,
+							startblock := NULL,
+							endblock := NULL);
+ERROR:  "test_index" is not a table, materialized view, or TOAST table
+create view test_view as select 1;
+-- views do not have vms, so these all fail
+select * from verify_heapam('test_view',
+							on_error_stop := false,
+							skip := NULL,
+							startblock := NULL,
+							endblock := NULL);
+ERROR:  "test_view" is not a table, materialized view, or TOAST table
+create sequence test_sequence;
+-- sequences do not have vms, so these all fail
+select * from verify_heapam('test_sequence',
+							on_error_stop := false,
+							skip := NULL,
+							startblock := NULL,
+							endblock := NULL);
+ERROR:  "test_sequence" is not a table, materialized view, or TOAST table
+create foreign data wrapper dummy;
+create server dummy_server foreign data wrapper dummy;
+create foreign table test_foreign_table () server dummy_server;
+-- foreign tables do not have vms, so these all fail
+select * from verify_heapam('test_foreign_table',
+							on_error_stop := false,
+							skip := NULL,
+							startblock := NULL,
+							endblock := NULL);
+ERROR:  "test_foreign_table" is not a table, materialized view, or TOAST table
diff --git a/contrib/amcheck/sql/check_heap.sql b/contrib/amcheck/sql/check_heap.sql
new file mode 100644
index 0000000000..c75f5ff869
--- /dev/null
+++ b/contrib/amcheck/sql/check_heap.sql
@@ -0,0 +1,19 @@
+CREATE TABLE heaptest (a integer, b text);
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'rope');
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'none');
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'all-frozen');
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'all-visible');
+SELECT * FROM verify_heapam(rel := 'heaptest', startblock := 0, endblock := 0);
+INSERT INTO heaptest (a, b)
+	(SELECT gs, repeat('x', gs)
+		FROM generate_series(1,10000) gs);
+SELECT * FROM verify_heapam(rel := 'heaptest', startblock := 100000, endblock := 200000);
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'none');
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'all-frozen');
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'all-visible');
+SELECT * FROM verify_heapam(rel := 'heaptest', startblock := 0, endblock := 0);
+VACUUM FREEZE heaptest;
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'none');
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'all-frozen');
+SELECT * FROM verify_heapam(rel := 'heaptest', skip := 'all-visible');
+SELECT * FROM verify_heapam(rel := 'heaptest', startblock := 0, endblock := 0);
diff --git a/contrib/amcheck/sql/disallowed_reltypes.sql b/contrib/amcheck/sql/disallowed_reltypes.sql
new file mode 100644
index 0000000000..fc90e6ca33
--- /dev/null
+++ b/contrib/amcheck/sql/disallowed_reltypes.sql
@@ -0,0 +1,48 @@
+--
+-- check that using the module's functions with unsupported relations will fail
+--
+
+-- partitioned tables (the parent ones) don't have visibility maps
+create table test_partitioned (a int, b text default repeat('x', 5000))
+			 partition by list (a);
+-- these should all fail
+select * from verify_heapam('test_partitioned',
+							on_error_stop := false,
+							skip := NULL,
+							startblock := NULL,
+							endblock := NULL);
+
+create table test_partition partition of test_partitioned for values in (1);
+create index test_index on test_partition (a);
+-- indexes do not, so these all fail
+select * from verify_heapam('test_index',
+							on_error_stop := false,
+							skip := NULL,
+							startblock := NULL,
+							endblock := NULL);
+
+create view test_view as select 1;
+-- views do not have vms, so these all fail
+select * from verify_heapam('test_view',
+							on_error_stop := false,
+							skip := NULL,
+							startblock := NULL,
+							endblock := NULL);
+
+create sequence test_sequence;
+-- sequences do not have vms, so these all fail
+select * from verify_heapam('test_sequence',
+							on_error_stop := false,
+							skip := NULL,
+							startblock := NULL,
+							endblock := NULL);
+
+create foreign data wrapper dummy;
+create server dummy_server foreign data wrapper dummy;
+create foreign table test_foreign_table () server dummy_server;
+-- foreign tables do not have vms, so these all fail
+select * from verify_heapam('test_foreign_table',
+							on_error_stop := false,
+							skip := NULL,
+							startblock := NULL,
+							endblock := NULL);
diff --git a/contrib/amcheck/t/001_verify_heapam.pl b/contrib/amcheck/t/001_verify_heapam.pl
new file mode 100644
index 0000000000..c2d890bcd9
--- /dev/null
+++ b/contrib/amcheck/t/001_verify_heapam.pl
@@ -0,0 +1,94 @@
+use strict;
+use warnings;
+
+use PostgresNode;
+use TestLib;
+
+use Test::More tests => 48;
+
+my ($node, $result);
+
+# Check various options are stable (don't abort) when running verify_heapam on
+# the test table.  For uncorrupted tables, there isn't anything to check except
+# that it runs without crashing.
+sub check_all_options
+{
+	for my $stop (qw(NULL true false))
+	{
+		for my $skip ("'none'", "'all-frozen'", "'all-visible'")
+		{
+			my $check = "SELECT verify_heapam('test', $stop, $skip)";
+			$result = $node->safe_psql('postgres', "$check; SELECT 1");
+			is ($result, 1, "checked: $check");
+		}
+	}
+}
+
+# Stops the server and writes nulls in the first page of the table,
+# assuming page size is large enough for offset 1000..1016 to be
+# in the midst of the first page of data.
+sub corrupt_first_page
+{
+	my $pgdata = $node->data_dir;
+	my $rel = $node->safe_psql('postgres',
+							   qq(SELECT pg_relation_filepath('test')));
+	my $relpath = "$pgdata/$rel";
+	$node->stop;
+
+	my $fh;
+	open($fh, '+<', $relpath);
+	binmode $fh;
+	seek($fh, 1000, 0);
+	syswrite($fh, '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0', 16);
+	close($fh);
+
+	$node->start;
+}
+
+# Test set-up
+$node = get_new_node('test');
+$node->init;
+$node->append_conf('postgresql.conf', 'autovacuum=off');
+$node->start;
+$node->safe_psql('postgres', q(CREATE EXTENSION amcheck));
+
+# Check empty table
+$node->safe_psql('postgres', q(
+	CREATE TABLE test (a integer);
+	ALTER TABLE public.test SET (autovacuum_enabled=false);
+));
+check_all_options();
+
+# Check table with trivial data
+$node->safe_psql('postgres', q(INSERT INTO test VALUES (0)));
+check_all_options();
+
+# Check table with non-trivial data (more than a page worth) but
+# without any all-frozen or all-visible
+$node->safe_psql('postgres', q(
+INSERT INTO test SELECT generate_series(1,10000)));
+check_all_options();
+
+# Check table with all-visible data
+$node->safe_psql('postgres', q(VACUUM test));
+check_all_options();
+
+# Check table with all-frozen data
+$node->safe_psql('postgres', q(VACUUM FREEZE test));
+check_all_options();
+
+# Check table with corruption, no skipping
+corrupt_first_page();
+$result = $node->safe_psql('postgres', q(
+SELECT COUNT(*) > 0 FROM verify_heapam('test', on_error_stop := false, skip := NULL, startblock := NULL, endblock := NULL)));
+is($result, 't', 'corruption detected on first page');
+
+# Check table with corruption, skipping all-visible blocks
+$result = $node->safe_psql('postgres', q(
+SELECT COUNT(*) > 0 FROM verify_heapam('test', on_error_stop := false, skip := 'all-visible', startblock := NULL, endblock := NULL)));
+is($result, 'f', 'skipping all-visible first page');
+
+# Check table with corruption, skipping all-frozen blocks
+$result = $node->safe_psql('postgres', q(
+SELECT COUNT(*) > 0 FROM verify_heapam('test', on_error_stop := false, skip := 'all-frozen', startblock := NULL, endblock := NULL)));
+is($result, 'f', 'skipping all-frozen first page');
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
new file mode 100644
index 0000000000..007241d333
--- /dev/null
+++ b/contrib/amcheck/verify_heapam.c
@@ -0,0 +1,1153 @@
+/*-------------------------------------------------------------------------
+ *
+ * verify_heapam.c
+ *	  Functions to check postgresql heap relations for corruption
+ *
+ * Copyright (c) 2016-2020, PostgreSQL Global Development Group
+ *
+ *	  contrib/amcheck/verify_heapam.c
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/detoast.h"
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/heaptoast.h"
+#include "access/htup_details.h"
+#include "access/multixact.h"
+#include "access/toast_internals.h"
+#include "access/visibilitymap.h"
+#include "access/xact.h"
+#include "catalog/pg_am.h"
+#include "catalog/pg_type.h"
+#include "catalog/storage_xlog.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "storage/bufmgr.h"
+#include "storage/procarray.h"
+#include "storage/smgr.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/snapmgr.h"
+#include "utils/syscache.h"
+#include "amcheck.h"
+
+PG_FUNCTION_INFO_V1(verify_heapam);
+
+/* The number of columns in tuples returned by verify_heapam */
+#define HEAPCHECK_RELATION_COLS 8
+
+/*
+ * Struct holding the running context information during
+ * a lifetime of a verify_heapam execution.
+ */
+typedef struct HeapCheckContext
+{
+	/*
+	 * While verifying a table, we check whether any xid we encounter is
+	 * either too old or too new.  We could naively check that by taking the
+	 * XidGenLock each time and reading ShmemVariableCache.  We instead cache
+	 * the values and rely on the fact that we have the table locked
+	 * sufficiently that the oldest xid in the table cannot change
+	 * mid-verification, and although the newest xid in the table may advance,
+	 * it cannot retreat.  As such, whenever we encounter an xid older than
+	 * our cached oldest xid, we know it is invalid, and when we encounter an
+	 * xid newer than our cached newest xid, we recheck the
+	 * ShmemVariableCache.
+	 */
+	TransactionId nextKnownValidXid;
+	TransactionId oldestValidXid;
+
+	/* Values concerning the heap relation being checked */
+	Relation	rel;
+	TransactionId relfrozenxid;
+	TransactionId relminmxid;
+	Relation	toastrel;
+	Relation   *toast_indexes;
+	Relation	valid_toast_index;
+	int			num_toast_indexes;
+
+	/* Values for iterating over pages in the relation */
+	BlockNumber nblocks;
+	BlockNumber blkno;
+	BufferAccessStrategy bstrategy;
+	Buffer		buffer;
+	Page		page;
+
+	/* Values for iterating over tuples within a page */
+	OffsetNumber offnum;
+	ItemId		itemid;
+	uint16		lp_len;
+	HeapTupleHeader tuphdr;
+	int			natts;
+
+	/* Values for iterating over attributes within the tuple */
+	uint32		offset;			/* offset in tuple data */
+	AttrNumber	attnum;
+
+	/* Values for iterating over toast for the attribute */
+	int32		chunkno;
+	int32		attrsize;
+	int32		endchunk;
+	int32		totalchunks;
+
+	/* Whether verify_heapam has yet encountered any corrupt tuples */
+	bool		is_corrupt;
+
+	/* The descriptor and tuplestore for verify_heapam's result tuples */
+	TupleDesc	tupdesc;
+	Tuplestorestate *tupstore;
+}			HeapCheckContext;
+
+/* Internal implementation */
+static void check_relation_relkind_and_relam(Relation rel);
+
+static void confess(HeapCheckContext * ctx, char *msg);
+static TupleDesc verify_heapam_tupdesc(void);
+
+static bool TransactionIdValidInRel(TransactionId xid, HeapCheckContext * ctx);
+static bool tuple_is_visible(HeapTupleHeader tuphdr, HeapCheckContext * ctx);
+static void check_toast_tuple(HeapTuple toasttup, HeapCheckContext * ctx);
+static bool check_tuple_attribute(HeapCheckContext * ctx);
+static void check_tuple(HeapCheckContext * ctx);
+
+typedef enum SkipPages
+{
+	SKIP_ALL_FROZEN_PAGES,
+	SKIP_ALL_VISIBLE_PAGES,
+	SKIP_PAGES_NONE
+}			SkipPages;
+
+/*
+ * verify_heapam
+ *
+ *   Scan and report corruption in heap pages or in associated toast relation.
+ */
+Datum
+verify_heapam(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	MemoryContext oldcontext;
+	bool		randomAccess;
+	HeapCheckContext ctx;
+	FullTransactionId nextFullXid;
+	Buffer		vmbuffer = InvalidBuffer;
+	Oid			relid;
+	bool		fatal = false;
+	bool		on_error_stop;
+	SkipPages	skip_option = SKIP_PAGES_NONE;
+	int64		startblock;
+	int64		endblock;
+
+	/* check to see if caller supports us returning a tuplestore */
+	if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("set-valued function called in context that cannot "
+						"accept a set")));
+	if (!(rsinfo->allowedModes & SFRM_Materialize))
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("materialize mode required, but it is not allowed "
+						"in this context")));
+
+	/* check supplied arguments */
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("missing required parameter for 'rel'")));
+	relid = PG_GETARG_OID(0);
+	on_error_stop = PG_ARGISNULL(1) ? false : PG_GETARG_BOOL(1);
+	if (!PG_ARGISNULL(2))
+	{
+		const char *skip = PG_GETARG_CSTRING(2);
+
+		if (pg_strcasecmp(skip, "all-visible") == 0)
+			skip_option = SKIP_ALL_VISIBLE_PAGES;
+		else if (pg_strcasecmp(skip, "all-frozen") == 0)
+			skip_option = SKIP_ALL_FROZEN_PAGES;
+		else if (pg_strcasecmp(skip, "none") == 0)
+			skip_option = SKIP_PAGES_NONE;
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unrecognized parameter for 'skip': %s", skip),
+					 errhint("please choose from 'all-visible', 'all-frozen', or 'none'")));
+	}
+
+	memset(&ctx, 0, sizeof(HeapCheckContext));
+
+	/* The tupdesc and tuplestore must be created in ecxt_per_query_memory */
+	oldcontext = MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory);
+	randomAccess = (rsinfo->allowedModes & SFRM_Materialize_Random) != 0;
+	ctx.tupdesc = verify_heapam_tupdesc();
+	ctx.tupstore = tuplestore_begin_heap(randomAccess, false, work_mem);
+	rsinfo->returnMode = SFRM_Materialize;
+	rsinfo->setResult = ctx.tupstore;
+	rsinfo->setDesc = ctx.tupdesc;
+
+	MemoryContextSwitchTo(oldcontext);
+
+	/*
+	 * Open the relation.  We use ShareUpdateExclusive to prevent concurrent
+	 * vacuums from changing the relfrozenxid, relminmxid, or advancing the
+	 * global oldestXid to be newer than those.
+	 */
+	ctx.rel = relation_open(relid, ShareUpdateExclusiveLock);
+	check_relation_relkind_and_relam(ctx.rel);
+	ctx.nblocks = RelationGetNumberOfBlocks(ctx.rel);
+
+	/* Early exit if the relation is empty */
+	if (!ctx.nblocks)
+	{
+		/*
+		 * For consistency, we need to enforce that the startblock and
+		 * endblock are within the valid range if the user specified them.
+		 * Yet, for an empty table with no blocks, no specified block can be
+		 * in range.
+		 */
+		if (!PG_ARGISNULL(3))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("starting block " INT64_FORMAT
+							" is out of bounds for relation with no blocks",
+							PG_GETARG_INT64(3))));
+		if (!PG_ARGISNULL(4))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("ending block " INT64_FORMAT
+							" is out of bounds for relation with no blocks",
+							PG_GETARG_INT64(4))));
+		relation_close(ctx.rel, ShareUpdateExclusiveLock);
+		PG_RETURN_NULL();
+	}
+
+	ctx.bstrategy = GetAccessStrategy(BAS_BULKREAD);
+	ctx.buffer = InvalidBuffer;
+	ctx.page = NULL;
+
+	/* If we get this far, we know the relation has at least one block */
+	startblock = PG_ARGISNULL(3) ? 0 : PG_GETARG_INT64(3);
+	endblock = PG_ARGISNULL(4) ? ((int64) ctx.nblocks) - 1 : PG_GETARG_INT64(4);
+	if (startblock < 0 || endblock >= ctx.nblocks || startblock > endblock)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("block range " INT64_FORMAT " .. " INT64_FORMAT
+						" is out of bounds for relation with block count %u",
+						startblock, endblock, ctx.nblocks)));
+
+	/*
+	 * Open the toast relation, if any, also protected from concurrent
+	 * vacuums.
+	 */
+	if (ctx.rel->rd_rel->reltoastrelid)
+	{
+		int			offset;
+
+		/* Main relation has associated toast relation */
+		ctx.toastrel = table_open(ctx.rel->rd_rel->reltoastrelid,
+								  ShareUpdateExclusiveLock);
+		offset = toast_open_indexes(ctx.toastrel,
+									ShareUpdateExclusiveLock,
+									&(ctx.toast_indexes),
+									&(ctx.num_toast_indexes));
+		ctx.valid_toast_index = ctx.toast_indexes[offset];
+	}
+	else
+	{
+		/* Main relation has no associated toast relation */
+		ctx.toastrel = NULL;
+		ctx.toast_indexes = NULL;
+		ctx.num_toast_indexes = 0;
+	}
+
+	/*
+	 * Now that we have our relation(s) locked, oldestXid cannot advance
+	 * beyond the oldest valid xid in our table, nor can our relfrozenxid
+	 * advance.
+	 *
+	 * If relfrozenxid is normal, it contains the oldest valid xid we may
+	 * encounter in the table.  If not, the oldest xid for our database is the
+	 * oldest we should encounter.
+	 *
+	 * Bugs in pg_upgrade are reported (see commands/vacuum.c circa line 1572)
+	 * to have sometimes rendered the oldest xid value for a database invalid.
+	 * It seems unwise to report rows as corrupt for failing to be newer than
+	 * a value which itself may be corrupt.  We instead use the oldest xid for
+	 * the entire cluster, which must be at least as old as the oldest xid for
+	 * our database.
+	 *
+	 * If neither the value for the database nor the xids for any row are
+	 * corrupt, then this gives the right answer.  If the rows disagree with
+	 * the value for the database, how can we know which one is wrong?
+	 */
+	ctx.relfrozenxid = ctx.rel->rd_rel->relfrozenxid;
+	ctx.relminmxid = ctx.rel->rd_rel->relminmxid;
+
+	LWLockAcquire(XidGenLock, LW_SHARED);
+	nextFullXid = ShmemVariableCache->nextFullXid;
+	ctx.oldestValidXid = ShmemVariableCache->oldestXid;
+	LWLockRelease(XidGenLock);
+	ctx.nextKnownValidXid = XidFromFullTransactionId(nextFullXid);
+
+	if (TransactionIdIsNormal(ctx.relfrozenxid) &&
+		TransactionIdPrecedes(ctx.relfrozenxid, ctx.oldestValidXid))
+	{
+		confess(&ctx, psprintf("relfrozenxid %u precedes global oldest valid xid %u",
+							   ctx.relfrozenxid, ctx.oldestValidXid));
+		fatal = true;
+	}
+	else if (TransactionIdIsNormal(ctx.relminmxid) &&
+			 TransactionIdPrecedes(ctx.relminmxid, ctx.oldestValidXid))
+	{
+		confess(&ctx, psprintf("relminmxid %u precedes global oldest valid xid %u",
+							   ctx.relminmxid, ctx.oldestValidXid));
+		fatal = true;
+	}
+
+	if (fatal)
+	{
+		if (ctx.toast_indexes)
+			toast_close_indexes(ctx.toast_indexes, ctx.num_toast_indexes,
+								ShareUpdateExclusiveLock);
+		if (ctx.toastrel)
+			table_close(ctx.toastrel, ShareUpdateExclusiveLock);
+		relation_close(ctx.rel, ShareUpdateExclusiveLock);
+		PG_RETURN_NULL();
+	}
+
+	if (TransactionIdIsNormal(ctx.relfrozenxid))
+		ctx.oldestValidXid = ctx.relfrozenxid;
+
+	if (startblock < 0)
+		startblock = 0;
+	if (endblock < 0 || endblock > ctx.nblocks)
+		endblock = ctx.nblocks;
+
+	for (ctx.blkno = startblock; ctx.blkno <= endblock; ctx.blkno++)
+	{
+		int32		mapbits;
+		OffsetNumber maxoff;
+		PageHeader	ph;
+
+		/* Optionally skip over all-frozen or all-visible blocks */
+		if (skip_option != SKIP_PAGES_NONE)
+		{
+			bool		all_frozen,
+						all_visible;
+
+			mapbits = (int32) visibilitymap_get_status(ctx.rel, ctx.blkno,
+													   &vmbuffer);
+			all_frozen = mapbits & VISIBILITYMAP_ALL_VISIBLE;
+			all_visible = mapbits & VISIBILITYMAP_ALL_FROZEN;
+
+			if ((all_frozen && skip_option == SKIP_ALL_FROZEN_PAGES) ||
+				(all_visible && skip_option == SKIP_ALL_VISIBLE_PAGES))
+			{
+				continue;
+			}
+		}
+
+		/* Read and lock the next page. */
+		ctx.buffer = ReadBufferExtended(ctx.rel, MAIN_FORKNUM, ctx.blkno,
+										RBM_NORMAL, ctx.bstrategy);
+		LockBuffer(ctx.buffer, BUFFER_LOCK_SHARE);
+		ctx.page = BufferGetPage(ctx.buffer);
+		ph = (PageHeader) ctx.page;
+
+		/* We rely on this math property for the first iteration */
+		StaticAssertStmt(InvalidOffsetNumber + 1 == FirstOffsetNumber,
+						 "InvalidOffsetNumber increments to FirstOffsetNumber");
+
+		ctx.offnum = InvalidOffsetNumber;
+		ctx.itemid = NULL;
+		ctx.lp_len = 0;
+		ctx.tuphdr = NULL;
+		ctx.natts = 0;
+
+		/* Perform tuple checks */
+		maxoff = PageGetMaxOffsetNumber(ctx.page);
+		for (ctx.offnum = FirstOffsetNumber; ctx.offnum <= maxoff;
+			 ctx.offnum = OffsetNumberNext(ctx.offnum))
+		{
+			ctx.itemid = PageGetItemId(ctx.page, ctx.offnum);
+
+			/* Skip over unused/dead line pointers */
+			if (!ItemIdIsUsed(ctx.itemid) || ItemIdIsDead(ctx.itemid))
+				continue;
+
+			/*
+			 * If this line pointer has been redirected, check that it
+			 * redirects to a valid offset within the line pointer array.
+			 */
+			if (ItemIdIsRedirected(ctx.itemid))
+			{
+				OffsetNumber rdoffnum = ItemIdGetRedirect(ctx.itemid);
+				ItemId		rditem;
+
+				if (rdoffnum < FirstOffsetNumber || rdoffnum > maxoff)
+				{
+					confess(&ctx, psprintf(
+										   "line pointer redirection to item at offset number %u is outside valid bounds %u .. %u",
+										   (unsigned) rdoffnum, (unsigned) FirstOffsetNumber,
+										   (unsigned) maxoff));
+					continue;
+				}
+				rditem = PageGetItemId(ctx.page, rdoffnum);
+				if (!ItemIdIsUsed(rditem))
+					confess(&ctx, psprintf(
+										   "line pointer redirection to unused item at offset %u",
+										   (unsigned) rdoffnum));
+				continue;
+			}
+
+			/* Set up context information about this next tuple */
+			ctx.lp_len = ItemIdGetLength(ctx.itemid);
+			ctx.tuphdr = (HeapTupleHeader) PageGetItem(ctx.page, ctx.itemid);
+			ctx.natts = HeapTupleHeaderGetNatts(ctx.tuphdr);
+
+			/*
+			 * Reset information about individual attributes and related toast
+			 * values, so they show as NULL in the corruption report if we
+			 * record a corruption before beginning to iterate over the
+			 * attributes.
+			 */
+			ctx.attnum = -1;
+			ctx.chunkno = -1;
+
+			/* Ok, ready to check this next tuple */
+			check_tuple(&ctx);
+		}
+
+		/* clean up */
+		ctx.itemid = NULL;
+		ctx.lp_len = 0;
+		UnlockReleaseBuffer(ctx.buffer);
+
+		if (on_error_stop && ctx.is_corrupt)
+			break;
+	}
+
+	if (vmbuffer != InvalidBuffer)
+		ReleaseBuffer(vmbuffer);
+
+	/* Close the associated toast table and indexes, if any. */
+	if (ctx.rel->rd_rel->reltoastrelid)
+	{
+		toast_close_indexes(ctx.toast_indexes, ctx.num_toast_indexes,
+							ShareUpdateExclusiveLock);
+		table_close(ctx.toastrel, ShareUpdateExclusiveLock);
+	}
+
+	/* Close the main relation */
+	relation_close(ctx.rel, ShareUpdateExclusiveLock);
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * check_relation_relkind_and_relam
+ *
+ *   convenience routine to check that relation is of a supported relkind.
+ */
+static void
+check_relation_relkind_and_relam(Relation rel)
+{
+	if (rel->rd_rel->relkind != RELKIND_RELATION &&
+		rel->rd_rel->relkind != RELKIND_MATVIEW &&
+		rel->rd_rel->relkind != RELKIND_TOASTVALUE)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("\"%s\" is not a table, materialized view, or TOAST table",
+						RelationGetRelationName(rel))));
+	if (rel->rd_rel->relam != HEAP_TABLE_AM_OID)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("\"%s\" is not a heap",
+						RelationGetRelationName(rel))));
+}
+
+/*
+ * confess
+ *
+ *   Return a message about corruption, including information
+ *   about where in the relation the corruption was found.
+ *
+ *   The msg argument is pfree'd by this function.
+ */
+static void
+confess(HeapCheckContext * ctx, char *msg)
+{
+	Datum		values[HEAPCHECK_RELATION_COLS];
+	bool		nulls[HEAPCHECK_RELATION_COLS];
+	HeapTuple	tuple;
+	int16		lp_off = ItemIdGetOffset(ctx->itemid);
+	int16		lp_flags = ItemIdGetFlags(ctx->itemid);
+	int16		lp_len = ItemIdGetLength(ctx->itemid);
+
+	MemSet(values, 0, sizeof(values));
+	MemSet(nulls, 0, sizeof(nulls));
+	values[0] = Int64GetDatum(ctx->blkno);
+	values[1] = Int32GetDatum(ctx->offnum);
+	nulls[1] = (ctx->offnum < 0);
+	values[2] = Int16GetDatum(lp_off);
+	nulls[2] = (lp_off < 0);
+	values[3] = Int16GetDatum(lp_flags);
+	nulls[3] = (lp_flags < 0);
+	values[4] = Int16GetDatum(lp_len);
+	nulls[4] = (lp_len < 0);
+	values[5] = Int32GetDatum(ctx->attnum);
+	nulls[5] = (ctx->attnum < 0);
+	values[6] = Int32GetDatum(ctx->chunkno);
+	nulls[6] = (ctx->chunkno < 0);
+	values[7] = CStringGetTextDatum(msg);
+
+	/*
+	 * In principle, there is nothing to prevent a scan over a large, highly
+	 * corrupted table from using workmem worth of memory building up the
+	 * tuplestore.  That's ok, but if we also leak the msg argument memory
+	 * until the end of the query, we could exceed workmem by more than a
+	 * trivial amount.  Therefore, free the msg argument each time we are
+	 * called rather than waiting for our current memory context to be freed.
+	 */
+	pfree(msg);
+
+	tuple = heap_form_tuple(ctx->tupdesc, values, nulls);
+	tuplestore_puttuple(ctx->tupstore, tuple);
+	ctx->is_corrupt = true;
+}
+
+/*
+ * Helper function to construct the TupleDesc needed by verify_heapam.
+ */
+static TupleDesc
+verify_heapam_tupdesc(void)
+{
+	TupleDesc	tupdesc;
+	AttrNumber	a = 0;
+
+	tupdesc = CreateTemplateTupleDesc(HEAPCHECK_RELATION_COLS);
+	TupleDescInitEntry(tupdesc, ++a, "blkno", INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, ++a, "offnum", INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, ++a, "lp_off", INT2OID, -1, 0);
+	TupleDescInitEntry(tupdesc, ++a, "lp_flags", INT2OID, -1, 0);
+	TupleDescInitEntry(tupdesc, ++a, "lp_len", INT2OID, -1, 0);
+	TupleDescInitEntry(tupdesc, ++a, "attnum", INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, ++a, "chunk", INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, ++a, "msg", TEXTOID, -1, 0);
+	Assert(a == HEAPCHECK_RELATION_COLS);
+
+	return BlessTupleDesc(tupdesc);
+}
+
+static inline bool
+XidInValidRange(TransactionId xid, HeapCheckContext * ctx)
+{
+	return (TransactionIdPrecedesOrEquals(ctx->oldestValidXid, xid) &&
+			TransactionIdPrecedes(xid, ctx->nextKnownValidXid));
+}
+
+/*
+ * Given a TransactionId, attempt to interpret it as a valid
+ * FullTransactionId, neither in the future nor overlong in
+ * the past.  Stores the inferred FullTransactionId in *fxid.
+ *
+ * Returns whether the xid is newer than the oldest clog xid.
+ */
+static bool
+TransactionIdValidInRel(TransactionId xid, HeapCheckContext * ctx)
+{
+	/* Quick return for special oids */
+	switch (xid)
+	{
+		case InvalidTransactionId:
+			return false;
+		case BootstrapTransactionId:
+		case FrozenTransactionId:
+			return true;
+	}
+
+	/*
+	 * If this xid is within the last known valid range of xids, then it has
+	 * to be ok.  The oldest valid xid cannot advance, because we have too
+	 * strong a lock on the relation for that, and although the newest valid
+	 * xid may advance, that doesn't invalidate anything from the range we've
+	 * already identified.
+	 */
+	if (XidInValidRange(xid, ctx))
+		return true;
+
+	/* The latest valid xid may have advanced.  Recheck. */
+	ctx->nextKnownValidXid =
+		XidFromFullTransactionId(ReadNextFullTransactionId());
+	if (XidInValidRange(xid, ctx))
+		return true;
+
+	/* No good.  This xid is invalid. */
+	return false;
+}
+
+/*
+ * tuple_is_visible
+ *
+ *	Determine whether tuples are visible for verification.  Similar to
+ *  HeapTupleSatisfiesVacuum, but with critical differences.
+ *
+ *  1) Does not touch hint bits.  It seems imprudent to write hint bits
+ *     to a table during a corruption check.
+ *  2) Only makes a boolean determination of whether verification should
+ *     see the tuple, rather than doing extra work for vacuum-related
+ *     categorization.
+ *
+ *  The caller should already have checked that xmin and xmax are not out of
+ *  bounds for the relation.
+ */
+static bool
+tuple_is_visible(HeapTupleHeader tuphdr, HeapCheckContext * ctx)
+{
+	uint16		infomask = tuphdr->t_infomask;
+
+	if (!HeapTupleHeaderXminCommitted(tuphdr))
+	{
+		TransactionId raw_xmin = HeapTupleHeaderGetRawXmin(tuphdr);
+
+		if (HeapTupleHeaderXminInvalid(tuphdr))
+		{
+			return false;		/* HEAPTUPLE_DEAD */
+		}
+		/* Used by pre-9.0 binary upgrades */
+		else if (infomask & HEAP_MOVED_OFF ||
+				 infomask & HEAP_MOVED_IN)
+		{
+			TransactionId xvac = HeapTupleHeaderGetXvac(tuphdr);
+
+			if (TransactionIdIsCurrentTransactionId(xvac))
+				return true;	/* HEAPTUPLE_DELETE_IN_PROGRESS */
+			if (TransactionIdIsInProgress(xvac))
+				return true;	/* HEAPTUPLE_DELETE_IN_PROGRESS */
+
+			if (!TransactionIdValidInRel(xvac, ctx))
+			{
+				confess(ctx, psprintf("old-style VACUUM FULL transaction ID %u is invalid in this relation",
+									  xvac));
+				return false;
+			}
+			else if (TransactionIdDidCommit(xvac))
+				return false;	/* HEAPTUPLE_DEAD */
+		}
+		else if (TransactionIdIsCurrentTransactionId(raw_xmin))
+			return true;		/* insert or delete in progress */
+		else if (TransactionIdIsInProgress(raw_xmin))
+			return true;		/* HEAPTUPLE_INSERT_IN_PROGRESS */
+		else if (!TransactionIdDidCommit(raw_xmin))
+		{
+			return false;		/* HEAPTUPLE_DEAD */
+		}
+	}
+
+	if (!(infomask & HEAP_XMAX_INVALID) && !HEAP_XMAX_IS_LOCKED_ONLY(infomask))
+	{
+		if (infomask & HEAP_XMAX_IS_MULTI)
+		{
+			TransactionId xmax = HeapTupleGetUpdateXid(tuphdr);
+
+			/* not LOCKED_ONLY, so it has to have an xmax */
+			if (!TransactionIdIsValid(xmax))
+			{
+				confess(ctx, pstrdup(
+									 "heap tuple with XMAX_IS_MULTI is neither LOCKED_ONLY nor has a valid xmax"));
+				return false;
+			}
+			if (TransactionIdIsInProgress(xmax))
+				return true;	/* HEAPTUPLE_DELETE_IN_PROGRESS */
+
+			else if (TransactionIdDidCommit(xmax))
+			{
+				return false;	/* HEAPTUPLE_RECENTLY_DEAD or HEAPTUPLE_DEAD */
+			}
+			/* Ok, the tuple is live */
+		}
+		else if (!(infomask & HEAP_XMAX_COMMITTED))
+		{
+			if (TransactionIdIsInProgress(HeapTupleHeaderGetRawXmax(tuphdr)))
+				return true;	/* HEAPTUPLE_DELETE_IN_PROGRESS */
+			/* Ok, the tuple is live */
+		}
+		else
+			return false;		/* HEAPTUPLE_RECENTLY_DEAD or HEAPTUPLE_DEAD */
+	}
+	return true;
+}
+
+/*
+ * check_toast_tuple
+ *
+ *   Checks the current toast tuple as tracked in ctx for corruption.  Records
+ *   any corruption found in ctx->corruption.
+ */
+static void
+check_toast_tuple(HeapTuple toasttup, HeapCheckContext * ctx)
+{
+	int32		curchunk;
+	Pointer		chunk;
+	bool		isnull;
+	char	   *chunkdata;
+	int32		chunksize;
+	int32		expected_size;
+
+	/*
+	 * Have a chunk, extract the sequence number and the data
+	 */
+	curchunk = DatumGetInt32(fastgetattr(toasttup, 2,
+										 ctx->toastrel->rd_att, &isnull));
+	if (isnull)
+	{
+		confess(ctx,
+				pstrdup("toast chunk sequence number is null"));
+		return;
+	}
+	chunk = DatumGetPointer(fastgetattr(toasttup, 3,
+										ctx->toastrel->rd_att, &isnull));
+	if (isnull)
+	{
+		confess(ctx, pstrdup("toast chunk data is null"));
+		return;
+	}
+	if (!VARATT_IS_EXTENDED(chunk))
+	{
+		chunksize = VARSIZE(chunk) - VARHDRSZ;
+		chunkdata = VARDATA(chunk);
+	}
+	else if (VARATT_IS_SHORT(chunk))
+	{
+		/*
+		 * could happen due to heap_form_tuple doing its thing
+		 */
+		chunksize = VARSIZE_SHORT(chunk) - VARHDRSZ_SHORT;
+		chunkdata = VARDATA_SHORT(chunk);
+	}
+	else
+	{
+		/* should never happen */
+		uint32		header = ((varattrib_4b *) chunk)->va_4byte.va_header;
+
+		confess(ctx, psprintf(
+							  "corrupt extended toast chunk with sequence number %d has invalid varlena header %0x",
+							  curchunk, header));
+		return;
+	}
+
+	/*
+	 * Some checks on the data we've found
+	 */
+	if (curchunk != ctx->chunkno)
+	{
+		confess(ctx, psprintf(
+							  "toast chunk sequence number %u not the expected sequence number %u",
+							  curchunk, ctx->chunkno));
+		return;
+	}
+	if (curchunk > ctx->endchunk)
+	{
+		confess(ctx, psprintf(
+							  "toast chunk sequence number %u exceeds the end chunk sequence number %u",
+							  curchunk, ctx->endchunk));
+		return;
+	}
+
+	expected_size = curchunk < ctx->totalchunks - 1 ? TOAST_MAX_CHUNK_SIZE
+		: ctx->attrsize - ((ctx->totalchunks - 1) * TOAST_MAX_CHUNK_SIZE);
+	if (chunksize != expected_size)
+	{
+		confess(ctx, psprintf("toast chunk size %u differs from expected size %u",
+							  chunksize, expected_size));
+		return;
+	}
+
+	ctx->chunkno++;
+}
+
+/*
+ * check_tuple_attribute
+ *
+ *   Checks the current attribute as tracked in ctx for corruption.  Records
+ *   any corruption found in ctx->corruption.
+ *
+ *   This function follows the logic performed by heap_deform_tuple(), and in
+ *   the case of a toasted value, continues along the logic of
+ *   detoast_external_attr(), checking for any conditions that would result in
+ *   either of those functions Asserting or crashing the backend.  The checks
+ *   performed by Asserts present in those two functions are also performed
+ *   here.  In cases where those two functions are a bit cavalier in their
+ *   assumptions about data being correct, we perform additional checks not
+ *   present in either of those two functions.  Where some condition is checked
+ *   in both of those functions, we perform it here twice, as we parallel the
+ *   logical flow of those two functions.  The presence of duplicate checks
+ *   seems a reasonable price to pay for keeping this code tightly coupled with
+ *   the code it protects.
+ *
+ *   Returns true if the tuple attribute is sane enough for processing to
+ *   continue on to the next attribute, false otherwise.
+ */
+static bool
+check_tuple_attribute(HeapCheckContext * ctx)
+{
+	struct varatt_external toast_pointer;
+	ScanKeyData toastkey;
+	SysScanDesc toastscan;
+	SnapshotData SnapshotToast;
+	HeapTuple	toasttup;
+	bool		found_toasttup;
+	Datum		attdatum;
+	struct varlena *attr;
+	char	   *tp;				/* pointer to the tuple data */
+	uint16		infomask;
+	Form_pg_attribute thisatt;
+
+	infomask = ctx->tuphdr->t_infomask;
+	thisatt = TupleDescAttr(RelationGetDescr(ctx->rel), ctx->attnum);
+
+	tp = (char *) ctx->tuphdr + ctx->tuphdr->t_hoff;
+
+	if (ctx->tuphdr->t_hoff + ctx->offset > ctx->lp_len)
+	{
+		confess(ctx, psprintf(
+							  "tuple attribute should start at offset %u, but tuple length is only %u",
+							  ctx->tuphdr->t_hoff + ctx->offset, ctx->lp_len));
+		return false;
+	}
+
+	/* Skip null values */
+	if (infomask & HEAP_HASNULL && att_isnull(ctx->attnum, ctx->tuphdr->t_bits))
+		return true;
+
+	/* Skip non-varlena values, but update offset first */
+	if (thisatt->attlen != -1)
+	{
+		ctx->offset = att_align_nominal(ctx->offset, thisatt->attalign);
+		ctx->offset = att_addlength_pointer(ctx->offset, thisatt->attlen,
+											tp + ctx->offset);
+		return true;
+	}
+
+	/* Ok, we're looking at a varlena attribute. */
+	ctx->offset = att_align_pointer(ctx->offset, thisatt->attalign, -1,
+									tp + ctx->offset);
+
+	/* Get the (possibly corrupt) varlena datum */
+	attdatum = fetchatt(thisatt, tp + ctx->offset);
+
+	/*
+	 * We have the datum, but we cannot decode it carelessly, as it may still
+	 * be corrupt.
+	 */
+
+	/*
+	 * Check that VARTAG_SIZE won't hit a TrapMacro on a corrupt va_tag before
+	 * risking a call into att_addlength_pointer
+	 */
+	if (VARATT_IS_EXTERNAL(tp + ctx->offset))
+	{
+		uint8		va_tag = VARTAG_EXTERNAL(tp + ctx->offset);
+
+		if (va_tag != VARTAG_ONDISK)
+		{
+			confess(ctx, psprintf(
+								  "%s toast at offset %u is unexpected",
+								  va_tag == VARTAG_INDIRECT ? "indirect" :
+								  va_tag == VARTAG_EXPANDED_RO ? "expanded" :
+								  va_tag == VARTAG_EXPANDED_RW ? "expanded" :
+								  "unexpected",
+								  ctx->tuphdr->t_hoff + ctx->offset));
+			/* We can't know where the next attribute begins */
+			return false;
+		}
+	}
+
+	/* Ok, should be safe now */
+	ctx->offset = att_addlength_pointer(ctx->offset, thisatt->attlen,
+										tp + ctx->offset);
+
+	if (ctx->tuphdr->t_hoff + ctx->offset > ctx->lp_len)
+	{
+		confess(ctx, psprintf(
+							  "tuple attribute of length %u ends at offset %u, but tuple length is only %u",
+							  thisatt->attlen, ctx->tuphdr->t_hoff + ctx->offset,
+							  ctx->lp_len));
+		return false;
+	}
+
+	/*
+	 * heap_deform_tuple would be done with this attribute at this point,
+	 * having stored it in values[], and would continue to the next attribute.
+	 * We go further, because we need to check if the toast datum is corrupt.
+	 */
+
+	attr = (struct varlena *) DatumGetPointer(attdatum);
+
+	/*
+	 * Now we follow the logic of detoast_external_attr(), with the same
+	 * caveats about being paranoid about corruption.
+	 */
+
+	/* Skip values that are not external */
+	if (!VARATT_IS_EXTERNAL(attr))
+		return true;
+
+	/* It is external, and we're looking at a page on disk */
+
+	/* The tuple header better claim to contain toasted values */
+	if (!(infomask & HEAP_HASEXTERNAL))
+	{
+		confess(ctx, pstrdup(
+							 "attribute is external but tuple header flag HEAP_HASEXTERNAL not set"));
+		return true;
+	}
+
+	/* The relation better have a toast table */
+	if (!ctx->rel->rd_rel->reltoastrelid)
+	{
+		confess(ctx, pstrdup(
+							 "attribute is external but relation has no toast relation"));
+		return true;
+	}
+
+	/*
+	 * Must copy attr into toast_pointer for alignment considerations
+	 */
+	VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+
+	ctx->attrsize = toast_pointer.va_extsize;
+	ctx->endchunk = (ctx->attrsize - 1) / TOAST_MAX_CHUNK_SIZE;
+	ctx->totalchunks = ctx->endchunk + 1;
+
+	/*
+	 * Setup a scan key to find chunks in toast table with matching va_valueid
+	 */
+	ScanKeyInit(&toastkey,
+				(AttrNumber) 1,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(toast_pointer.va_valueid));
+
+	/*
+	 * Check if any chunks for this toasted object exist in the toast table,
+	 * accessible via the index.
+	 */
+	init_toast_snapshot(&SnapshotToast);
+	toastscan = systable_beginscan_ordered(ctx->toastrel,
+										   ctx->valid_toast_index,
+										   &SnapshotToast, 1,
+										   &toastkey);
+	ctx->chunkno = 0;
+
+	found_toasttup = false;
+	while ((toasttup =
+			systable_getnext_ordered(toastscan,
+									 ForwardScanDirection)) != NULL)
+	{
+		found_toasttup = true;
+		check_toast_tuple(toasttup, ctx);
+	}
+	if (ctx->chunkno != (ctx->endchunk + 1))
+		confess(ctx, psprintf(
+							  "final chunk number %u differs from expected value %u",
+							  ctx->chunkno, (ctx->endchunk + 1)));
+	if (!found_toasttup)
+		confess(ctx, pstrdup("toasted value missing from toast table"));
+	systable_endscan_ordered(toastscan);
+
+	return true;
+}
+
+/*
+ * check_tuple
+ *
+ *   Checks the current tuple as tracked in ctx for corruption.  Records any
+ *   corruption found in ctx->corruption.
+ */
+static void
+check_tuple(HeapCheckContext * ctx)
+{
+	TransactionId xmin;
+	TransactionId xmax;
+	bool		fatal = false;
+	uint16		infomask = ctx->tuphdr->t_infomask;
+
+	/*
+	 * If the line pointer for this tuple does not reserve enough space for a
+	 * complete tuple header, we dare not read the tuple header.
+	 */
+	if (ctx->lp_len < MAXALIGN(SizeofHeapTupleHeader))
+	{
+		confess(ctx, psprintf(
+							  "tuple's %u byte line pointer length is less than the %u byte minimum tuple header size",
+							  ctx->lp_len, (uint32) MAXALIGN(SizeofHeapTupleHeader)));
+		return;
+	}
+
+	/* Check relminmxid against mxid, if any */
+	xmax = HeapTupleHeaderGetRawXmax(ctx->tuphdr);
+	if (infomask & HEAP_XMAX_IS_MULTI &&
+		MultiXactIdPrecedes(xmax, ctx->relminmxid))
+	{
+		confess(ctx, psprintf(
+							  "tuple xmax %u precedes relminmxid %u",
+							  xmax, ctx->relminmxid));
+		fatal = true;
+	}
+
+	/* Check xmin against relfrozenxid */
+	xmin = HeapTupleHeaderGetXmin(ctx->tuphdr);
+	if (TransactionIdIsNormal(ctx->relfrozenxid) &&
+		TransactionIdIsNormal(xmin))
+	{
+		if (TransactionIdPrecedes(xmin, ctx->relfrozenxid))
+		{
+			confess(ctx, psprintf(
+								  "tuple xmin %u precedes relfrozenxid %u",
+								  xmin, ctx->relfrozenxid));
+			fatal = true;
+		}
+		else if (!TransactionIdValidInRel(xmin, ctx))
+		{
+			confess(ctx, psprintf(
+								  "tuple xmin %u follows last assigned xid %u",
+								  xmin, ctx->nextKnownValidXid));
+			fatal = true;
+		}
+	}
+
+	/* Check xmax against relfrozenxid */
+	if (TransactionIdIsNormal(ctx->relfrozenxid) &&
+		TransactionIdIsNormal(xmax))
+	{
+		if (TransactionIdPrecedes(xmax, ctx->relfrozenxid))
+		{
+			confess(ctx, psprintf(
+								  "tuple xmax %u precedes relfrozenxid %u",
+								  xmax, ctx->relfrozenxid));
+			fatal = true;
+		}
+		else if (!TransactionIdValidInRel(xmax, ctx))
+		{
+			confess(ctx, psprintf(
+								  "tuple xmax %u follows last assigned xid %u",
+								  xmax, ctx->nextKnownValidXid));
+			fatal = true;
+		}
+	}
+
+	/* Check for tuple header corruption */
+	if (ctx->tuphdr->t_hoff < SizeofHeapTupleHeader)
+	{
+		confess(ctx,
+				psprintf("tuple's header size is %u bytes which is less than the %u byte minimum valid header size",
+						 ctx->tuphdr->t_hoff,
+						 (unsigned) SizeofHeapTupleHeader));
+		fatal = true;
+	}
+	if (ctx->tuphdr->t_hoff > ctx->lp_len)
+	{
+		confess(ctx, psprintf(
+							  "tuple's %u byte header size exceeds the %u byte length of the entire tuple",
+							  ctx->tuphdr->t_hoff, ctx->lp_len));
+		fatal = true;
+	}
+	if (ctx->tuphdr->t_hoff != MAXALIGN(ctx->tuphdr->t_hoff))
+	{
+		confess(ctx, psprintf(
+							  "tuple's user data offset %u not maximally aligned to %u",
+							  ctx->tuphdr->t_hoff, (uint32) MAXALIGN(ctx->tuphdr->t_hoff)));
+		fatal = true;
+	}
+	if ((ctx->tuphdr->t_infomask & HEAP_XMAX_LOCK_ONLY) &&
+		(ctx->tuphdr->t_infomask2 & HEAP_KEYS_UPDATED))
+	{
+		confess(ctx,
+				psprintf("tuple xmax marked incompatibly as keys updated and locked only"));
+	}
+	if ((ctx->tuphdr->t_infomask & HEAP_XMAX_COMMITTED) &&
+		(ctx->tuphdr->t_infomask & HEAP_XMAX_IS_MULTI))
+	{
+		confess(ctx,
+				psprintf("tuple xmax marked incompatibly as committed and as a multitransaction ID"));
+	}
+
+	/*
+	 * If the tuple has nulls, check that the implied length of the variable
+	 * length nulls bitmap field t_bits does not overflow the allowed space.
+	 * We don't know if the corruption is in the t_hoff field or the infomask
+	 * bit HEAP_HASNULL.
+	 *
+	 * If the tuple does not have nulls, check that no space has been reserved
+	 * for the null bitmap.
+	 */
+	if ((infomask & HEAP_HASNULL) &&
+		(ctx->tuphdr->t_hoff != MAXALIGN(SizeofHeapTupleHeader + BITMAPLEN(ctx->natts))))
+	{
+		confess(ctx, psprintf(
+							  "tuple with null values has user data offset %u rather than the expected offset %u",
+							  ctx->tuphdr->t_hoff,
+							  (uint32) MAXALIGN(SizeofHeapTupleHeader + BITMAPLEN(ctx->natts))));
+		fatal = true;
+	}
+	else if (!(infomask & HEAP_HASNULL) &&
+			 (ctx->tuphdr->t_hoff != MAXALIGN(SizeofHeapTupleHeader)))
+	{
+		confess(ctx, psprintf(
+							  "tuple without null values has user data offset %u rather than the expected offset %u",
+							  ctx->tuphdr->t_hoff,
+							  (uint32) MAXALIGN(SizeofHeapTupleHeader)));
+		fatal = true;
+	}
+
+	/*
+	 * Cannot process tuple data if tuple header was corrupt, as the offsets
+	 * within the page cannot be trusted, leaving too much risk of reading
+	 * garbage if we continue.
+	 *
+	 * We also cannot process the tuple if the xmin or xmax were invalid
+	 * relative to relfrozenxid or relminmxid, as clog entries for the xids
+	 * may already be gone.
+	 */
+	if (fatal)
+		return;
+
+	/*
+	 * Skip tuples that are invisible, as we cannot assume the TupleDesc we
+	 * are using is appropriate.
+	 */
+	if (!tuple_is_visible(ctx->tuphdr, ctx))
+		return;
+
+	/*
+	 * If we get this far, the tuple is visible to us, so it must not be
+	 * incompatible with our relDesc.  The natts field could be legitimately
+	 * shorter than rel's natts, but it cannot be longer than rel's natts.
+	 */
+	if (RelationGetDescr(ctx->rel)->natts < ctx->natts)
+	{
+		confess(ctx, psprintf(
+							  "tuple has %u attributes in relation with only %u attributes",
+							  ctx->natts,
+							  RelationGetDescr(ctx->rel)->natts));
+		return;
+	}
+
+	/*
+	 * Iterate over the attributes looking for broken toast values. This
+	 * roughly follows the logic of heap_deform_tuple, except that it doesn't
+	 * bother building up isnull[] and values[] arrays, since nobody wants
+	 * them, and it unrolls anything that might trip over an Assert when
+	 * processing corrupt data.
+	 */
+	ctx->offset = 0;
+	for (ctx->attnum = 0; ctx->attnum < ctx->natts; ctx->attnum++)
+	{
+		if (!check_tuple_attribute(ctx))
+			break;
+	}
+}
diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml
index a9df2c1a9d..b8170bbfdf 100644
--- a/doc/src/sgml/amcheck.sgml
+++ b/doc/src/sgml/amcheck.sgml
@@ -69,7 +69,7 @@ AND c.relpersistence != 't'
 -- Function may throw an error when this is omitted:
 AND c.relkind = 'i' AND i.indisready AND i.indisvalid
 ORDER BY c.relpages DESC LIMIT 10;
- bt_index_check |             relname             | relpages 
+ bt_index_check |             relname             | relpages
 ----------------+---------------------------------+----------
                 | pg_depend_reference_index       |       43
                 | pg_depend_depender_index        |       40
@@ -165,6 +165,110 @@ ORDER BY c.relpages DESC LIMIT 10;
      </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term>
+     <function>
+      verify_heapam(relation regclass,
+                    on_error_stop boolean,
+                    skip_all_frozen boolean,
+                    skip_all_visible boolean,
+                    blkno OUT bigint,
+                    offnum OUT integer,
+                    lp_off OUT smallint,
+                    lp_flags OUT smallint,
+                    lp_len OUT smallint,
+                    attnum OUT integer,
+                    chunk OUT integer,
+                    msg OUT text)
+      returns record
+     </function>
+    </term>
+    <listitem>
+     <para>
+      Checks for "logical" corruption, where the page is valid but inconsistent
+      with the rest of the database cluster. This can happen due to faulty or
+      ill-conceived backup and restore tools, or bad storage, or user error, or
+      bugs in the server itself.  It checks xmin and xmax values against
+      relfrozenxid and relminmxid, and also validates TOAST pointers.
+     </para>
+
+     <para>
+      For each block in the relation where corruption is detected, or for just
+      the first block if on_error_stop is true, for each corruption detected,
+      returns one row containing the following fields:
+     </para>
+     <variablelist>
+      <varlistentry>
+       <term>blkno</term>
+       <listitem>
+        <para>
+         The number of the block containing the corrupt page.
+        </para>
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term>offnum</term>
+       <listitem>
+        <para>
+         The OffsetNumber of the corrupt tuple.
+        </para>
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term>lp_off</term>
+       <listitem>
+        <para>
+         The offset into the page of the line pointer for the corrupt tuple.
+        </para>
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term>lp_flags</term>
+       <listitem>
+        <para>
+         The flags in the line pointer for the corrupt tuple.
+        </para>
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term>lp_len</term>
+       <listitem>
+        <para>
+         The length of the corrupt tuple as recorded in the line pointer.
+        </para>
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term>attnum</term>
+       <listitem>
+        <para>
+         The attribute number of the corrupt column in the tuple, if the
+         corruption is specific to a column and not the tuple as a whole.
+        </para>
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term>chunk</term>
+       <listitem>
+        <para>
+         The chunk number of the corrupt toasted attribute, if the corruption
+         is specific to a toasted value.
+        </para>
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term>msg</term>
+       <listitem>
+        <para>
+         A human readable message describing the corruption in the page.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
   </variablelist>
   <tip>
    <para>
diff --git a/src/backend/access/heap/hio.c b/src/backend/access/heap/hio.c
index aa3f14c019..00de10b7c9 100644
--- a/src/backend/access/heap/hio.c
+++ b/src/backend/access/heap/hio.c
@@ -47,6 +47,17 @@ RelationPutHeapTuple(Relation relation,
 	 */
 	Assert(!token || HeapTupleHeaderIsSpeculative(tuple->t_data));
 
+	/*
+	 * Do not allow tuples with invalid combinations of hint bits to be placed
+	 * on a page.  These combinations are detected as corruption by the
+	 * contrib/amcheck logic, so if you decide to disable one or more of these
+	 * assertions, make corresponding changes to contrib/amcheck.
+	 */
+	Assert(!((tuple->t_data->t_infomask & HEAP_XMAX_LOCK_ONLY) &&
+			 (tuple->t_data->t_infomask2 & HEAP_KEYS_UPDATED)));
+	Assert(!((tuple->t_data->t_infomask & HEAP_XMAX_COMMITTED) &&
+			 (tuple->t_data->t_infomask & HEAP_XMAX_IS_MULTI)));
+
 	/* Add the tuple to the page */
 	pageHeader = BufferGetPage(buffer);
 
-- 
2.21.1 (Apple Git-122.3)