Extend amcheck to check heap pages.

Mark Dilger, reviewed by Peter Geoghegan, Andres Freund, Álvaro Herrera,
Michael Paquier, Amul Sul, and by me. Some last-minute cosmetic
revisions by me.

Discussion: http://postgr.es/m/12ED3DA8-25F0-4B68-937D-D907CFBF08E7@enterprisedb.com
This commit is contained in:
Robert Haas 2020-10-22 08:44:18 -04:00
parent f8721bd752
commit 866e24d47d
12 changed files with 2299 additions and 9 deletions

View File

@ -3,13 +3,16 @@
MODULE_big = amcheck
OBJS = \
$(WIN32RES) \
verify_heapam.o \
verify_nbtree.o
EXTENSION = amcheck
DATA = amcheck--1.1--1.2.sql amcheck--1.0--1.1.sql amcheck--1.0.sql
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
TAP_TESTS = 1
ifdef USE_PGXS
PG_CONFIG = pg_config

View File

@ -0,0 +1,30 @@
/* 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
--
-- verify_heapam()
--
CREATE FUNCTION verify_heapam(relation regclass,
on_error_stop boolean default false,
check_toast boolean default false,
skip text default 'none',
startblock bigint default null,
endblock bigint default null,
blkno OUT bigint,
offnum OUT integer,
attnum 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,
boolean,
text,
bigint,
bigint)
FROM PUBLIC;

View File

@ -1,5 +1,5 @@
# amcheck extension
comment = 'functions for verifying relation integrity'
default_version = '1.2'
default_version = '1.3'
module_pathname = '$libdir/amcheck'
relocatable = true

View File

@ -0,0 +1,194 @@
CREATE TABLE heaptest (a integer, b text);
REVOKE ALL ON heaptest FROM PUBLIC;
-- Check that invalid skip option is rejected
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'rope');
ERROR: invalid skip option
HINT: Valid skip options are "all-visible", "all-frozen", and "none".
-- Check specifying invalid block ranges when verifying an empty table
SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 0, endblock := 0);
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 5, endblock := 8);
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
-- Check that valid options are not rejected nor corruption reported
-- for an empty table, and that skip enum-like parameter is case-insensitive
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'none');
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'all-frozen');
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'all-visible');
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'None');
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'All-Frozen');
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'All-Visible');
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'NONE');
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'ALL-FROZEN');
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'ALL-VISIBLE');
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
-- Add some data so subsequent tests are not entirely trivial
INSERT INTO heaptest (a, b)
(SELECT gs, repeat('x', gs)
FROM generate_series(1,50) gs);
-- Check that valid options are not rejected nor corruption reported
-- for a non-empty table
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'none');
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'all-frozen');
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'all-visible');
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 0, endblock := 0);
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
CREATE ROLE regress_heaptest_role;
-- verify permissions are checked (error due to function not callable)
SET ROLE regress_heaptest_role;
SELECT * FROM verify_heapam(relation := 'heaptest');
ERROR: permission denied for function verify_heapam
RESET ROLE;
GRANT EXECUTE ON FUNCTION verify_heapam(regclass, boolean, boolean, text, bigint, bigint) TO regress_heaptest_role;
-- verify permissions are now sufficient
SET ROLE regress_heaptest_role;
SELECT * FROM verify_heapam(relation := 'heaptest');
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
RESET ROLE;
-- Check specifying invalid block ranges when verifying a non-empty table.
SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 0, endblock := 10000);
ERROR: ending block number must be between 0 and 0
SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 10000, endblock := 11000);
ERROR: starting block number must be between 0 and 0
-- Vacuum freeze to change the xids encountered in subsequent tests
VACUUM FREEZE heaptest;
-- Check that valid options are not rejected nor corruption reported
-- for a non-empty frozen table
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'none');
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'all-frozen');
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'all-visible');
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 0, endblock := 0);
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
-- Check that partitioned tables (the parent ones) which don't have visibility
-- maps are rejected
CREATE TABLE test_partitioned (a int, b text default repeat('x', 5000))
PARTITION BY list (a);
SELECT * FROM verify_heapam('test_partitioned',
startblock := NULL,
endblock := NULL);
ERROR: "test_partitioned" is not a table, materialized view, or TOAST table
-- Check that valid options are not rejected nor corruption reported
-- for an empty partition table (the child one)
CREATE TABLE test_partition partition OF test_partitioned FOR VALUES IN (1);
SELECT * FROM verify_heapam('test_partition',
startblock := NULL,
endblock := NULL);
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
-- Check that valid options are not rejected nor corruption reported
-- for a non-empty partition table (the child one)
INSERT INTO test_partitioned (a) (SELECT 1 FROM generate_series(1,1000) gs);
SELECT * FROM verify_heapam('test_partition',
startblock := NULL,
endblock := NULL);
blkno | offnum | attnum | msg
-------+--------+--------+-----
(0 rows)
-- Check that indexes are rejected
CREATE INDEX test_index ON test_partition (a);
SELECT * FROM verify_heapam('test_index',
startblock := NULL,
endblock := NULL);
ERROR: "test_index" is not a table, materialized view, or TOAST table
-- Check that views are rejected
CREATE VIEW test_view AS SELECT 1;
SELECT * FROM verify_heapam('test_view',
startblock := NULL,
endblock := NULL);
ERROR: "test_view" is not a table, materialized view, or TOAST table
-- Check that sequences are rejected
CREATE SEQUENCE test_sequence;
SELECT * FROM verify_heapam('test_sequence',
startblock := NULL,
endblock := NULL);
ERROR: "test_sequence" is not a table, materialized view, or TOAST table
-- Check that foreign tables are rejected
CREATE FOREIGN DATA WRAPPER dummy;
CREATE SERVER dummy_server FOREIGN DATA WRAPPER dummy;
CREATE FOREIGN TABLE test_foreign_table () SERVER dummy_server;
SELECT * FROM verify_heapam('test_foreign_table',
startblock := NULL,
endblock := NULL);
ERROR: "test_foreign_table" is not a table, materialized view, or TOAST table
-- cleanup
DROP TABLE heaptest;
DROP TABLE test_partition;
DROP TABLE test_partitioned;
DROP OWNED BY regress_heaptest_role; -- permissions
DROP ROLE regress_heaptest_role;

View File

@ -0,0 +1,116 @@
CREATE TABLE heaptest (a integer, b text);
REVOKE ALL ON heaptest FROM PUBLIC;
-- Check that invalid skip option is rejected
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'rope');
-- Check specifying invalid block ranges when verifying an empty table
SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 0, endblock := 0);
SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 5, endblock := 8);
-- Check that valid options are not rejected nor corruption reported
-- for an empty table, and that skip enum-like parameter is case-insensitive
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'none');
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'all-frozen');
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'all-visible');
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'None');
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'All-Frozen');
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'All-Visible');
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'NONE');
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'ALL-FROZEN');
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'ALL-VISIBLE');
-- Add some data so subsequent tests are not entirely trivial
INSERT INTO heaptest (a, b)
(SELECT gs, repeat('x', gs)
FROM generate_series(1,50) gs);
-- Check that valid options are not rejected nor corruption reported
-- for a non-empty table
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'none');
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'all-frozen');
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'all-visible');
SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 0, endblock := 0);
CREATE ROLE regress_heaptest_role;
-- verify permissions are checked (error due to function not callable)
SET ROLE regress_heaptest_role;
SELECT * FROM verify_heapam(relation := 'heaptest');
RESET ROLE;
GRANT EXECUTE ON FUNCTION verify_heapam(regclass, boolean, boolean, text, bigint, bigint) TO regress_heaptest_role;
-- verify permissions are now sufficient
SET ROLE regress_heaptest_role;
SELECT * FROM verify_heapam(relation := 'heaptest');
RESET ROLE;
-- Check specifying invalid block ranges when verifying a non-empty table.
SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 0, endblock := 10000);
SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 10000, endblock := 11000);
-- Vacuum freeze to change the xids encountered in subsequent tests
VACUUM FREEZE heaptest;
-- Check that valid options are not rejected nor corruption reported
-- for a non-empty frozen table
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'none');
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'all-frozen');
SELECT * FROM verify_heapam(relation := 'heaptest', skip := 'all-visible');
SELECT * FROM verify_heapam(relation := 'heaptest', startblock := 0, endblock := 0);
-- Check that partitioned tables (the parent ones) which don't have visibility
-- maps are rejected
CREATE TABLE test_partitioned (a int, b text default repeat('x', 5000))
PARTITION BY list (a);
SELECT * FROM verify_heapam('test_partitioned',
startblock := NULL,
endblock := NULL);
-- Check that valid options are not rejected nor corruption reported
-- for an empty partition table (the child one)
CREATE TABLE test_partition partition OF test_partitioned FOR VALUES IN (1);
SELECT * FROM verify_heapam('test_partition',
startblock := NULL,
endblock := NULL);
-- Check that valid options are not rejected nor corruption reported
-- for a non-empty partition table (the child one)
INSERT INTO test_partitioned (a) (SELECT 1 FROM generate_series(1,1000) gs);
SELECT * FROM verify_heapam('test_partition',
startblock := NULL,
endblock := NULL);
-- Check that indexes are rejected
CREATE INDEX test_index ON test_partition (a);
SELECT * FROM verify_heapam('test_index',
startblock := NULL,
endblock := NULL);
-- Check that views are rejected
CREATE VIEW test_view AS SELECT 1;
SELECT * FROM verify_heapam('test_view',
startblock := NULL,
endblock := NULL);
-- Check that sequences are rejected
CREATE SEQUENCE test_sequence;
SELECT * FROM verify_heapam('test_sequence',
startblock := NULL,
endblock := NULL);
-- Check that foreign tables are rejected
CREATE FOREIGN DATA WRAPPER dummy;
CREATE SERVER dummy_server FOREIGN DATA WRAPPER dummy;
CREATE FOREIGN TABLE test_foreign_table () SERVER dummy_server;
SELECT * FROM verify_heapam('test_foreign_table',
startblock := NULL,
endblock := NULL);
-- cleanup
DROP TABLE heaptest;
DROP TABLE test_partition;
DROP TABLE test_partitioned;
DROP OWNED BY regress_heaptest_role; -- permissions
DROP ROLE regress_heaptest_role;

View File

@ -0,0 +1,242 @@
use strict;
use warnings;
use PostgresNode;
use TestLib;
use Test::More tests => 65;
my ($node, $result);
#
# 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 a table with data loaded but no corruption, freezing, etc.
#
fresh_test_table('test');
check_all_options_uncorrupted('test', 'plain');
#
# Check a corrupt table
#
fresh_test_table('test');
corrupt_first_page('test');
detects_corruption(
"verify_heapam('test')",
"plain corrupted table");
detects_corruption(
"verify_heapam('test', skip := 'all-visible')",
"plain corrupted table skipping all-visible");
detects_corruption(
"verify_heapam('test', skip := 'all-frozen')",
"plain corrupted table skipping all-frozen");
detects_corruption(
"verify_heapam('test', check_toast := false)",
"plain corrupted table skipping toast");
detects_corruption(
"verify_heapam('test', startblock := 0, endblock := 0)",
"plain corrupted table checking only block zero");
#
# Check a corrupt table with all-frozen data
#
fresh_test_table('test');
$node->safe_psql('postgres', q(VACUUM FREEZE test));
corrupt_first_page('test');
detects_corruption(
"verify_heapam('test')",
"all-frozen corrupted table");
detects_no_corruption(
"verify_heapam('test', skip := 'all-frozen')",
"all-frozen corrupted table skipping all-frozen");
#
# Check a corrupt table with corrupt page header
#
fresh_test_table('test');
corrupt_first_page_and_header('test');
detects_corruption(
"verify_heapam('test')",
"corrupted test table with bad page header");
#
# Check an uncorrupted table with corrupt toast page header
#
fresh_test_table('test');
my $toast = get_toast_for('test');
corrupt_first_page_and_header($toast);
detects_corruption(
"verify_heapam('test', check_toast := true)",
"table with corrupted toast page header checking toast");
detects_no_corruption(
"verify_heapam('test', check_toast := false)",
"table with corrupted toast page header skipping toast");
detects_corruption(
"verify_heapam('$toast')",
"corrupted toast page header");
#
# Check an uncorrupted table with corrupt toast
#
fresh_test_table('test');
$toast = get_toast_for('test');
corrupt_first_page($toast);
detects_corruption(
"verify_heapam('test', check_toast := true)",
"table with corrupted toast checking toast");
detects_no_corruption(
"verify_heapam('test', check_toast := false)",
"table with corrupted toast skipping toast");
detects_corruption(
"verify_heapam('$toast')",
"corrupted toast table");
#
# Check an uncorrupted all-frozen table with corrupt toast
#
fresh_test_table('test');
$node->safe_psql('postgres', q(VACUUM FREEZE test));
$toast = get_toast_for('test');
corrupt_first_page($toast);
detects_corruption(
"verify_heapam('test', check_toast := true)",
"all-frozen table with corrupted toast checking toast");
detects_no_corruption(
"verify_heapam('test', check_toast := false)",
"all-frozen table with corrupted toast skipping toast");
detects_corruption(
"verify_heapam('$toast')",
"corrupted toast table of all-frozen table");
# Returns the filesystem path for the named relation.
sub relation_filepath
{
my ($relname) = @_;
my $pgdata = $node->data_dir;
my $rel = $node->safe_psql('postgres',
qq(SELECT pg_relation_filepath('$relname')));
die "path not found for relation $relname" unless defined $rel;
return "$pgdata/$rel";
}
# Returns the fully qualified name of the toast table for the named relation
sub get_toast_for
{
my ($relname) = @_;
$node->safe_psql('postgres', qq(
SELECT 'pg_toast.' || t.relname
FROM pg_catalog.pg_class c, pg_catalog.pg_class t
WHERE c.relname = '$relname'
AND c.reltoastrelid = t.oid));
}
# (Re)create and populate a test table of the given name.
sub fresh_test_table
{
my ($relname) = @_;
$node->safe_psql('postgres', qq(
DROP TABLE IF EXISTS $relname CASCADE;
CREATE TABLE $relname (a integer, b text);
ALTER TABLE $relname SET (autovacuum_enabled=false);
ALTER TABLE $relname ALTER b SET STORAGE external;
INSERT INTO $relname (a, b)
(SELECT gs, repeat('b',gs*10) FROM generate_series(1,1000) gs);
));
}
# Stops the test node, corrupts the first page of the named relation, and
# restarts the node.
sub corrupt_first_page_internal
{
my ($relname, $corrupt_header) = @_;
my $relpath = relation_filepath($relname);
$node->stop;
my $fh;
open($fh, '+<', $relpath);
binmode $fh;
# If we corrupt the header, postgres won't allow the page into the buffer.
syswrite($fh, '\xFF\xFF\xFF\xFF', 8) if ($corrupt_header);
# Corrupt at least the line pointers. Exactly what this corrupts will
# depend on the page, as it may run past the line pointers into the user
# data. We stop short of writing 2048 bytes (2k), the smallest supported
# page size, as we don't want to corrupt the next page.
seek($fh, 32, 0);
syswrite($fh, '\x77\x77\x77\x77', 500);
close($fh);
$node->start;
}
sub corrupt_first_page
{
corrupt_first_page_internal($_[0], undef);
}
sub corrupt_first_page_and_header
{
corrupt_first_page_internal($_[0], 1);
}
sub detects_corruption
{
my ($function, $testname) = @_;
my $result = $node->safe_psql('postgres',
qq(SELECT COUNT(*) > 0 FROM $function));
is($result, 't', $testname);
}
sub detects_no_corruption
{
my ($function, $testname) = @_;
my $result = $node->safe_psql('postgres',
qq(SELECT COUNT(*) = 0 FROM $function));
is($result, 't', $testname);
}
# Check various options are stable (don't abort) and do not report corruption
# when running verify_heapam on an uncorrupted test table.
#
# The relname *must* be an uncorrupted table, or this will fail.
#
# The prefix is used to identify the test, along with the options,
# and should be unique.
sub check_all_options_uncorrupted
{
my ($relname, $prefix) = @_;
for my $stop (qw(true false))
{
for my $check_toast (qw(true false))
{
for my $skip ("'none'", "'all-frozen'", "'all-visible'")
{
for my $startblock (qw(NULL 0))
{
for my $endblock (qw(NULL 0))
{
my $opts = "on_error_stop := $stop, " .
"check_toast := $check_toast, " .
"skip := $skip, " .
"startblock := $startblock, " .
"endblock := $endblock";
detects_no_corruption(
"verify_heapam('$relname', $opts)",
"$prefix: $opts");
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -9,12 +9,11 @@
<para>
The <filename>amcheck</filename> module provides functions that allow you to
verify the logical consistency of the structure of relations. If the
structure appears to be valid, no error is raised.
verify the logical consistency of the structure of relations.
</para>
<para>
The functions verify various <emphasis>invariants</emphasis> in the
The B-Tree checking functions verify various <emphasis>invariants</emphasis> in the
structure of the representation of particular relations. The
correctness of the access method functions behind index scans and
other important operations relies on these invariants always
@ -24,7 +23,7 @@
collated lexical order). If that particular invariant somehow fails
to hold, we can expect binary searches on the affected page to
incorrectly guide index scans, resulting in wrong answers to SQL
queries.
queries. If the structure appears to be valid, no error is raised.
</para>
<para>
Verification is performed using the same procedures as those used by
@ -35,7 +34,22 @@
functions.
</para>
<para>
<filename>amcheck</filename> functions may only be used by superusers.
Unlike the B-Tree checking functions which report corruption by raising
errors, the heap checking function <function>verify_heapam</function> checks
a table and attempts to return a set of rows, one row per corruption
detected. Despite this, if facilities that
<function>verify_heapam</function> relies upon are themselves corrupted, the
function may be unable to continue and may instead raise an error.
</para>
<para>
Permission to execute <filename>amcheck</filename> functions may be granted
to non-superusers, but before granting such permissions careful consideration
should be given to data security and privacy concerns. Although the
corruption reports generated by these functions do not focus on the contents
of the corrupted data so much as on the structure of that data and the nature
of the corruptions found, an attacker who gains permission to execute these
functions, particularly if the attacker can also induce corruption, might be
able to infer something of the data itself from such messages.
</para>
<sect2>
@ -187,12 +201,221 @@ SET client_min_messages = DEBUG1;
</para>
</tip>
<variablelist>
<varlistentry>
<term>
<function>
verify_heapam(relation regclass,
on_error_stop boolean,
check_toast boolean,
skip cstring,
startblock bigint,
endblock bigint,
blkno OUT bigint,
offnum OUT integer,
attnum OUT integer,
msg OUT text)
returns record
</function>
</term>
<listitem>
<para>
Checks a table for structural corruption, where pages in the relation
contain data that is invalidly formatted, and for logical corruption,
where pages are structurally valid but inconsistent with the rest of the
database cluster. Example usage:
<screen>
test=# select * from verify_heapam('mytable', check_toast := true);
blkno | offnum | attnum | msg
-------+--------+--------+--------------------------------------------------------------------------------------------------
17 | 12 | | xmin 4294967295 precedes relation freeze threshold 17:1134217582
960 | 4 | | data begins at offset 152 beyond the tuple length 58
960 | 4 | | tuple data should begin at byte 24, but actually begins at byte 152 (3 attributes, no nulls)
960 | 5 | | tuple data should begin at byte 24, but actually begins at byte 27 (3 attributes, no nulls)
960 | 6 | | tuple data should begin at byte 24, but actually begins at byte 16 (3 attributes, no nulls)
960 | 7 | | tuple data should begin at byte 24, but actually begins at byte 21 (3 attributes, no nulls)
1147 | 2 | | number of attributes 2047 exceeds maximum expected for table 3
1147 | 10 | | tuple data should begin at byte 280, but actually begins at byte 24 (2047 attributes, has nulls)
1147 | 15 | | number of attributes 67 exceeds maximum expected for table 3
1147 | 16 | 1 | attribute 1 with length 4294967295 ends at offset 416848000 beyond total tuple length 58
1147 | 18 | 2 | final toast chunk number 0 differs from expected value 6
1147 | 19 | 2 | toasted value for attribute 2 missing from toast table
1147 | 21 | | tuple is marked as only locked, but also claims key columns were updated
1147 | 22 | | multitransaction ID 1775655 is from before relation cutoff 2355572
(14 rows)
</screen>
As this example shows, the Tuple ID (TID) of the corrupt tuple is given
in the (<literal>blkno</literal>, <literal>offnum</literal>) columns, and
for corruptions specific to a particular attribute in the tuple, the
<literal>attnum</literal> field shows which one.
</para>
<para>
Structural corruption can happen due to faulty storage hardware, or
relation files being overwritten or modified by unrelated software.
This kind of corruption can also be detected with
<link linkend="app-initdb-data-checksums"><application>data page
checksums</application></link>.
</para>
<para>
Relation pages which are correctly formatted, internally consistent, and
correct relative to their own internal checksums may still contain
logical corruption. As such, this kind of corruption cannot be detected
with <application>checksums</application>. Examples include toasted
values in the main table which lack a corresponding entry in the toast
table, and tuples in the main table with a Transaction ID that is older
than the oldest valid Transaction ID in the database or cluster.
</para>
<para>
Multiple causes of logical corruption have been observed in production
systems, including bugs in the <productname>PostgreSQL</productname>
server software, faulty and ill-conceived backup and restore tools, and
user error.
</para>
<para>
Corrupt relations are most concerning in live production environments,
precisely the same environments where high risk activities are least
welcome. For this reason, <function>verify_heapam</function> has been
designed to diagnose corruption without undue risk. It cannot guard
against all causes of backend crashes, as even executing the calling
query could be unsafe on a badly corrupted system. Access to <link
linkend="catalogs-overview">catalog tables</link> are performed and could
be problematic if the catalogs themselves are corrupted.
</para>
<para>
The design principle adhered to in <function>verify_heapam</function> is
that, if the rest of the system and server hardware are correct, under
default options, <function>verify_heapam</function> will not crash the
server due merely to structural or logical corruption in the target
table.
</para>
<para>
The <literal>check_toast</literal> attempts to reconcile the target
table against entries in its corresponding toast table. This option is
disabled by default and is known to be slow.
If the target relation's corresponding toast table or toast index is
corrupt, reconciling the target table against toast values could
conceivably crash the server, although in many cases this would
just produce an error.
</para>
<para>
The following optional arguments are recognized:
</para>
<variablelist>
<varlistentry>
<term>on_error_stop</term>
<listitem>
<para>
If true, corruption checking stops at the end of the first block on
which any corruptions are found.
</para>
<para>
Defaults to false.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>check_toast</term>
<listitem>
<para>
If true, toasted values are checked gainst the corresponding
TOAST table.
</para>
<para>
Defaults to false.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>skip</term>
<listitem>
<para>
If not <literal>none</literal>, corruption checking skips blocks that
are marked as all-visible or all-frozen, as given.
Valid options are <literal>all-visible</literal>,
<literal>all-frozen</literal> and <literal>none</literal>.
</para>
<para>
Defaults to <literal>none</literal>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>startblock</term>
<listitem>
<para>
If specified, corruption checking begins at the specified block,
skipping all previous blocks. It is an error to specify a
<literal>startblock</literal> outside the range of blocks in the
target table.
</para>
<para>
By default, does not skip any blocks.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>endblock</term>
<listitem>
<para>
If specified, corruption checking ends at the specified block,
skipping all remaining blocks. It is an error to specify an
<literal>endblock</literal> outside the range of blocks in the target
table.
</para>
<para>
By default, does not skip any blocks.
</para>
</listitem>
</varlistentry>
</variablelist>
<para>
For each corruption detected, <function>verify_heapam</function> returns
a row with the following columns:
</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>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>msg</term>
<listitem>
<para>
A human readable message describing the corruption in the page.
</para>
</listitem>
</varlistentry>
</variablelist>
</listitem>
</varlistentry>
</variablelist>
</sect2>
<sect2>
<title>Optional <parameter>heapallindexed</parameter> Verification</title>
<para>
When the <parameter>heapallindexed</parameter> argument to
When the <parameter>heapallindexed</parameter> argument to B-Tree
verification functions is <literal>true</literal>, an additional
phase of verification is performed against the table associated with
the target index relation. This consists of a <quote>dummy</quote>

View File

@ -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 disable one or both of these
* assertions, make corresponding changes there.
*/
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);

View File

@ -735,6 +735,25 @@ ReadNextMultiXactId(void)
return mxid;
}
/*
* ReadMultiXactIdRange
* Get the range of IDs that may still be referenced by a relation.
*/
void
ReadMultiXactIdRange(MultiXactId *oldest, MultiXactId *next)
{
LWLockAcquire(MultiXactGenLock, LW_SHARED);
*oldest = MultiXactState->oldestMultiXactId;
*next = MultiXactState->nextMXact;
LWLockRelease(MultiXactGenLock);
if (*oldest < FirstMultiXactId)
*oldest = FirstMultiXactId;
if (*next < FirstMultiXactId)
*next = FirstMultiXactId;
}
/*
* MultiXactIdCreateFromMembers
* Make a new MultiXactId from the specified set of members

View File

@ -109,6 +109,7 @@ extern MultiXactId MultiXactIdCreateFromMembers(int nmembers,
MultiXactMember *members);
extern MultiXactId ReadNextMultiXactId(void);
extern void ReadMultiXactIdRange(MultiXactId *oldest, MultiXactId *next);
extern bool MultiXactIdIsRunning(MultiXactId multi, bool isLockOnly);
extern void MultiXactIdSetOldestMember(void);
extern int GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **xids,

View File

@ -1020,6 +1020,7 @@ HbaToken
HeadlineJsonState
HeadlineParsedText
HeadlineWordEntry
HeapCheckContext
HeapScanDesc
HeapTuple
HeapTupleData
@ -2290,6 +2291,7 @@ SimpleStringList
SimpleStringListCell
SingleBoundSortItem
Size
SkipPages
SlabBlock
SlabChunk
SlabContext
@ -2791,6 +2793,8 @@ XactCallback
XactCallbackItem
XactEvent
XactLockTableWaitInfo
XidBoundsViolation
XidCommitStatus
XidHorizonPrefetchState
XidStatus
XmlExpr