1 2# Copyright (c) 2021, PostgreSQL Global Development Group 3 4use strict; 5use warnings; 6 7use PostgresNode; 8use TestLib; 9 10use Fcntl qw(:seek); 11use Test::More tests => 63; 12 13my ($node, $port, %corrupt_page, %remove_relation); 14 15# Returns the filesystem path for the named relation. 16# 17# Assumes the test node is running 18sub relation_filepath 19{ 20 my ($dbname, $relname) = @_; 21 22 my $pgdata = $node->data_dir; 23 my $rel = 24 $node->safe_psql($dbname, qq(SELECT pg_relation_filepath('$relname'))); 25 die "path not found for relation $relname" unless defined $rel; 26 return "$pgdata/$rel"; 27} 28 29# Returns the name of the toast relation associated with the named relation. 30# 31# Assumes the test node is running 32sub relation_toast 33{ 34 my ($dbname, $relname) = @_; 35 36 my $rel = $node->safe_psql( 37 $dbname, qq( 38 SELECT c.reltoastrelid::regclass 39 FROM pg_catalog.pg_class c 40 WHERE c.oid = '$relname'::regclass 41 AND c.reltoastrelid != 0 42 )); 43 return $rel; 44} 45 46# Adds the relation file for the given (dbname, relname) to the list 47# to be corrupted by means of overwriting junk in the first page. 48# 49# Assumes the test node is running. 50sub plan_to_corrupt_first_page 51{ 52 my ($dbname, $relname) = @_; 53 my $relpath = relation_filepath($dbname, $relname); 54 $corrupt_page{$relpath} = 1; 55} 56 57# Adds the relation file for the given (dbname, relname) to the list 58# to be corrupted by means of removing the file.. 59# 60# Assumes the test node is running 61sub plan_to_remove_relation_file 62{ 63 my ($dbname, $relname) = @_; 64 my $relpath = relation_filepath($dbname, $relname); 65 $remove_relation{$relpath} = 1; 66} 67 68# For the given (dbname, relname), if a corresponding toast table 69# exists, adds that toast table's relation file to the list to be 70# corrupted by means of removing the file. 71# 72# Assumes the test node is running. 73sub plan_to_remove_toast_file 74{ 75 my ($dbname, $relname) = @_; 76 my $toastname = relation_toast($dbname, $relname); 77 plan_to_remove_relation_file($dbname, $toastname) if ($toastname); 78} 79 80# Corrupts the first page of the given file path 81sub corrupt_first_page 82{ 83 my ($relpath) = @_; 84 85 my $fh; 86 open($fh, '+<', $relpath) 87 or BAIL_OUT("open failed: $!"); 88 binmode $fh; 89 90 # Corrupt some line pointers. The values are chosen to hit the 91 # various line-pointer-corruption checks in verify_heapam.c 92 # on both little-endian and big-endian architectures. 93 seek($fh, 32, SEEK_SET) 94 or BAIL_OUT("seek failed: $!"); 95 syswrite( 96 $fh, 97 pack("L*", 98 0xAAA15550, 0xAAA0D550, 0x00010000, 0x00008000, 99 0x0000800F, 0x001e8000, 0xFFFFFFFF) 100 ) or BAIL_OUT("syswrite failed: $!"); 101 close($fh) 102 or BAIL_OUT("close failed: $!"); 103} 104 105# Stops the node, performs all the corruptions previously planned, and 106# starts the node again. 107# 108sub perform_all_corruptions() 109{ 110 $node->stop(); 111 for my $relpath (keys %corrupt_page) 112 { 113 corrupt_first_page($relpath); 114 } 115 for my $relpath (keys %remove_relation) 116 { 117 unlink($relpath); 118 } 119 $node->start; 120} 121 122# Test set-up 123$node = get_new_node('test'); 124$node->init; 125$node->append_conf('postgresql.conf', 'autovacuum=off'); 126$node->start; 127$port = $node->port; 128 129for my $dbname (qw(db1 db2 db3)) 130{ 131 # Create the database 132 $node->safe_psql('postgres', qq(CREATE DATABASE $dbname)); 133 134 # Load the amcheck extension, upon which pg_amcheck depends. Put the 135 # extension in an unexpected location to test that pg_amcheck finds it 136 # correctly. Create tables with names that look like pg_catalog names to 137 # check that pg_amcheck does not get confused by them. Create functions in 138 # schema public that look like amcheck functions to check that pg_amcheck 139 # does not use them. 140 $node->safe_psql( 141 $dbname, q( 142 CREATE SCHEMA amcheck_schema; 143 CREATE EXTENSION amcheck WITH SCHEMA amcheck_schema; 144 CREATE TABLE amcheck_schema.pg_database (junk text); 145 CREATE TABLE amcheck_schema.pg_namespace (junk text); 146 CREATE TABLE amcheck_schema.pg_class (junk text); 147 CREATE TABLE amcheck_schema.pg_operator (junk text); 148 CREATE TABLE amcheck_schema.pg_proc (junk text); 149 CREATE TABLE amcheck_schema.pg_tablespace (junk text); 150 151 CREATE FUNCTION public.bt_index_check(index regclass, 152 heapallindexed boolean default false) 153 RETURNS VOID AS $$ 154 BEGIN 155 RAISE EXCEPTION 'Invoked wrong bt_index_check!'; 156 END; 157 $$ LANGUAGE plpgsql; 158 159 CREATE FUNCTION public.bt_index_parent_check(index regclass, 160 heapallindexed boolean default false, 161 rootdescend boolean default false) 162 RETURNS VOID AS $$ 163 BEGIN 164 RAISE EXCEPTION 'Invoked wrong bt_index_parent_check!'; 165 END; 166 $$ LANGUAGE plpgsql; 167 168 CREATE FUNCTION public.verify_heapam(relation regclass, 169 on_error_stop boolean default false, 170 check_toast boolean default false, 171 skip text default 'none', 172 startblock bigint default null, 173 endblock bigint default null, 174 blkno OUT bigint, 175 offnum OUT integer, 176 attnum OUT integer, 177 msg OUT text) 178 RETURNS SETOF record AS $$ 179 BEGIN 180 RAISE EXCEPTION 'Invoked wrong verify_heapam!'; 181 END; 182 $$ LANGUAGE plpgsql; 183 )); 184 185 # Create schemas, tables and indexes in five separate 186 # schemas. The schemas are all identical to start, but 187 # we will corrupt them differently later. 188 # 189 for my $schema (qw(s1 s2 s3 s4 s5)) 190 { 191 $node->safe_psql( 192 $dbname, qq( 193 CREATE SCHEMA $schema; 194 CREATE SEQUENCE $schema.seq1; 195 CREATE SEQUENCE $schema.seq2; 196 CREATE TABLE $schema.t1 ( 197 i INTEGER, 198 b BOX, 199 ia int4[], 200 ir int4range, 201 t TEXT 202 ); 203 CREATE TABLE $schema.t2 ( 204 i INTEGER, 205 b BOX, 206 ia int4[], 207 ir int4range, 208 t TEXT 209 ); 210 CREATE VIEW $schema.t2_view AS ( 211 SELECT i*2, t FROM $schema.t2 212 ); 213 ALTER TABLE $schema.t2 214 ALTER COLUMN t 215 SET STORAGE EXTERNAL; 216 217 INSERT INTO $schema.t1 (i, b, ia, ir, t) 218 (SELECT gs::INTEGER AS i, 219 box(point(gs,gs+5),point(gs*2,gs*3)) AS b, 220 array[gs, gs + 1]::int4[] AS ia, 221 int4range(gs, gs+100) AS ir, 222 repeat('foo', gs) AS t 223 FROM generate_series(1,10000,3000) AS gs); 224 225 INSERT INTO $schema.t2 (i, b, ia, ir, t) 226 (SELECT gs::INTEGER AS i, 227 box(point(gs,gs+5),point(gs*2,gs*3)) AS b, 228 array[gs, gs + 1]::int4[] AS ia, 229 int4range(gs, gs+100) AS ir, 230 repeat('foo', gs) AS t 231 FROM generate_series(1,10000,3000) AS gs); 232 233 CREATE MATERIALIZED VIEW $schema.t1_mv AS SELECT * FROM $schema.t1; 234 CREATE MATERIALIZED VIEW $schema.t2_mv AS SELECT * FROM $schema.t2; 235 236 create table $schema.p1 (a int, b int) PARTITION BY list (a); 237 create table $schema.p2 (a int, b int) PARTITION BY list (a); 238 239 create table $schema.p1_1 partition of $schema.p1 for values in (1, 2, 3); 240 create table $schema.p1_2 partition of $schema.p1 for values in (4, 5, 6); 241 create table $schema.p2_1 partition of $schema.p2 for values in (1, 2, 3); 242 create table $schema.p2_2 partition of $schema.p2 for values in (4, 5, 6); 243 244 CREATE INDEX t1_btree ON $schema.t1 USING BTREE (i); 245 CREATE INDEX t2_btree ON $schema.t2 USING BTREE (i); 246 247 CREATE INDEX t1_hash ON $schema.t1 USING HASH (i); 248 CREATE INDEX t2_hash ON $schema.t2 USING HASH (i); 249 250 CREATE INDEX t1_brin ON $schema.t1 USING BRIN (i); 251 CREATE INDEX t2_brin ON $schema.t2 USING BRIN (i); 252 253 CREATE INDEX t1_gist ON $schema.t1 USING GIST (b); 254 CREATE INDEX t2_gist ON $schema.t2 USING GIST (b); 255 256 CREATE INDEX t1_gin ON $schema.t1 USING GIN (ia); 257 CREATE INDEX t2_gin ON $schema.t2 USING GIN (ia); 258 259 CREATE INDEX t1_spgist ON $schema.t1 USING SPGIST (ir); 260 CREATE INDEX t2_spgist ON $schema.t2 USING SPGIST (ir); 261 )); 262 } 263} 264 265# Database 'db1' corruptions 266# 267 268# Corrupt indexes in schema "s1" 269plan_to_remove_relation_file('db1', 's1.t1_btree'); 270plan_to_corrupt_first_page('db1', 's1.t2_btree'); 271 272# Corrupt tables in schema "s2" 273plan_to_remove_relation_file('db1', 's2.t1'); 274plan_to_corrupt_first_page('db1', 's2.t2'); 275 276# Corrupt tables, partitions, matviews, and btrees in schema "s3" 277plan_to_remove_relation_file('db1', 's3.t1'); 278plan_to_corrupt_first_page('db1', 's3.t2'); 279 280plan_to_remove_relation_file('db1', 's3.t1_mv'); 281plan_to_remove_relation_file('db1', 's3.p1_1'); 282 283plan_to_corrupt_first_page('db1', 's3.t2_mv'); 284plan_to_corrupt_first_page('db1', 's3.p2_1'); 285 286plan_to_remove_relation_file('db1', 's3.t1_btree'); 287plan_to_corrupt_first_page('db1', 's3.t2_btree'); 288 289# Corrupt toast table, partitions, and materialized views in schema "s4" 290plan_to_remove_toast_file('db1', 's4.t2'); 291 292# Corrupt all other object types in schema "s5". We don't have amcheck support 293# for these types, but we check that their corruption does not trigger any 294# errors in pg_amcheck 295plan_to_remove_relation_file('db1', 's5.seq1'); 296plan_to_remove_relation_file('db1', 's5.t1_hash'); 297plan_to_remove_relation_file('db1', 's5.t1_gist'); 298plan_to_remove_relation_file('db1', 's5.t1_gin'); 299plan_to_remove_relation_file('db1', 's5.t1_brin'); 300plan_to_remove_relation_file('db1', 's5.t1_spgist'); 301 302plan_to_corrupt_first_page('db1', 's5.seq2'); 303plan_to_corrupt_first_page('db1', 's5.t2_hash'); 304plan_to_corrupt_first_page('db1', 's5.t2_gist'); 305plan_to_corrupt_first_page('db1', 's5.t2_gin'); 306plan_to_corrupt_first_page('db1', 's5.t2_brin'); 307plan_to_corrupt_first_page('db1', 's5.t2_spgist'); 308 309 310# Database 'db2' corruptions 311# 312plan_to_remove_relation_file('db2', 's1.t1'); 313plan_to_remove_relation_file('db2', 's1.t1_btree'); 314 315 316# Leave 'db3' uncorrupted 317# 318 319# Standard first arguments to TestLib functions 320my @cmd = ('pg_amcheck', '-p', $port); 321 322# Regular expressions to match various expected output 323my $no_output_re = qr/^$/; 324my $line_pointer_corruption_re = qr/line pointer/; 325my $missing_file_re = qr/could not open file ".*": No such file or directory/; 326my $index_missing_relation_fork_re = 327 qr/index ".*" lacks a main relation fork/; 328 329# We have created test databases with tables populated with data, but have not 330# yet corrupted anything. As such, we expect no corruption and verify that 331# none is reported 332# 333$node->command_checks_all([ @cmd, '-d', 'db1', '-d', 'db2', '-d', 'db3' ], 334 0, [$no_output_re], [$no_output_re], 'pg_amcheck prior to corruption'); 335 336# Perform the corruptions we planned above using only a single database restart. 337# 338perform_all_corruptions(); 339 340 341# Checking databases with amcheck installed and corrupt relations, pg_amcheck 342# command itself should return exit status = 2, because tables and indexes are 343# corrupt, not exit status = 1, which would mean the pg_amcheck command itself 344# failed. Corruption messages should go to stdout, and nothing to stderr. 345# 346$node->command_checks_all( 347 [ @cmd, 'db1' ], 348 2, 349 [ 350 $index_missing_relation_fork_re, $line_pointer_corruption_re, 351 $missing_file_re, 352 ], 353 [$no_output_re], 354 'pg_amcheck all schemas, tables and indexes in database db1'); 355 356$node->command_checks_all( 357 [ @cmd, '-d', 'db1', '-d', 'db2', '-d', 'db3' ], 358 2, 359 [ 360 $index_missing_relation_fork_re, $line_pointer_corruption_re, 361 $missing_file_re, 362 ], 363 [$no_output_re], 364 'pg_amcheck all schemas, tables and indexes in databases db1, db2, and db3' 365); 366 367# Scans of indexes in s1 should detect the specific corruption that we created 368# above. For missing relation forks, we know what the error message looks 369# like. For corrupted index pages, the error might vary depending on how the 370# page was formatted on disk, including variations due to alignment differences 371# between platforms, so we accept any non-empty error message. 372# 373# If we don't limit the check to databases with amcheck installed, we expect 374# complaint on stderr, but otherwise stderr should be quiet. 375# 376$node->command_checks_all( 377 [ @cmd, '--all', '-s', 's1', '-i', 't1_btree' ], 378 2, 379 [$index_missing_relation_fork_re], 380 [ 381 qr/pg_amcheck: warning: skipping database "postgres": amcheck is not installed/ 382 ], 383 'pg_amcheck index s1.t1_btree reports missing main relation fork'); 384 385$node->command_checks_all( 386 [ @cmd, '-d', 'db1', '-s', 's1', '-i', 't2_btree' ], 387 2, 388 [qr/.+/], # Any non-empty error message is acceptable 389 [$no_output_re], 390 'pg_amcheck index s1.s2 reports index corruption'); 391 392# Checking db1.s1 with indexes excluded should show no corruptions because we 393# did not corrupt any tables in db1.s1. Verify that both stdout and stderr 394# are quiet. 395# 396$node->command_checks_all( 397 [ @cmd, '-t', 's1.*', '--no-dependent-indexes', 'db1' ], 398 0, [$no_output_re], [$no_output_re], 399 'pg_amcheck of db1.s1 excluding indexes'); 400 401# Checking db2.s1 should show table corruptions if indexes are excluded 402# 403$node->command_checks_all( 404 [ @cmd, '-t', 's1.*', '--no-dependent-indexes', 'db2' ], 405 2, [$missing_file_re], [$no_output_re], 406 'pg_amcheck of db2.s1 excluding indexes'); 407 408# In schema db1.s3, the tables and indexes are both corrupt. We should see 409# corruption messages on stdout, and nothing on stderr. 410# 411$node->command_checks_all( 412 [ @cmd, '-s', 's3', 'db1' ], 413 2, 414 [ 415 $index_missing_relation_fork_re, $line_pointer_corruption_re, 416 $missing_file_re, 417 ], 418 [$no_output_re], 419 'pg_amcheck schema s3 reports table and index errors'); 420 421# In schema db1.s4, only toast tables are corrupt. Check that under default 422# options the toast corruption is reported, but when excluding toast we get no 423# error reports. 424$node->command_checks_all([ @cmd, '-s', 's4', 'db1' ], 425 2, [$missing_file_re], [$no_output_re], 426 'pg_amcheck in schema s4 reports toast corruption'); 427 428$node->command_checks_all( 429 [ 430 @cmd, '--no-dependent-toast', '--exclude-toast-pointers', '-s', 's4', 431 'db1' 432 ], 433 0, 434 [$no_output_re], 435 [$no_output_re], 436 'pg_amcheck in schema s4 excluding toast reports no corruption'); 437 438# Check that no corruption is reported in schema db1.s5 439$node->command_checks_all([ @cmd, '-s', 's5', 'db1' ], 440 0, [$no_output_re], [$no_output_re], 441 'pg_amcheck over schema s5 reports no corruption'); 442 443# In schema db1.s1, only indexes are corrupt. Verify that when we exclude 444# the indexes, no corruption is reported about the schema. 445# 446$node->command_checks_all( 447 [ @cmd, '-s', 's1', '-I', 't1_btree', '-I', 't2_btree', 'db1' ], 448 0, 449 [$no_output_re], 450 [$no_output_re], 451 'pg_amcheck over schema s1 with corrupt indexes excluded reports no corruption' 452); 453 454# In schema db1.s1, only indexes are corrupt. Verify that when we provide only 455# table inclusions, and disable index expansion, no corruption is reported 456# about the schema. 457# 458$node->command_checks_all( 459 [ @cmd, '-t', 's1.*', '--no-dependent-indexes', 'db1' ], 460 0, 461 [$no_output_re], 462 [$no_output_re], 463 'pg_amcheck over schema s1 with all indexes excluded reports no corruption' 464); 465 466# In schema db1.s2, only tables are corrupt. Verify that when we exclude those 467# tables that no corruption is reported. 468# 469$node->command_checks_all( 470 [ @cmd, '-s', 's2', '-T', 't1', '-T', 't2', 'db1' ], 471 0, 472 [$no_output_re], 473 [$no_output_re], 474 'pg_amcheck over schema s2 with corrupt tables excluded reports no corruption' 475); 476 477# Check errors about bad block range command line arguments. We use schema s5 478# to avoid getting messages about corrupt tables or indexes. 479# 480command_fails_like( 481 [ @cmd, '-s', 's5', '--startblock', 'junk', 'db1' ], 482 qr/invalid start block/, 483 'pg_amcheck rejects garbage startblock'); 484 485command_fails_like( 486 [ @cmd, '-s', 's5', '--endblock', '1234junk', 'db1' ], 487 qr/invalid end block/, 488 'pg_amcheck rejects garbage endblock'); 489 490command_fails_like( 491 [ @cmd, '-s', 's5', '--startblock', '5', '--endblock', '4', 'db1' ], 492 qr/end block precedes start block/, 493 'pg_amcheck rejects invalid block range'); 494 495# Check bt_index_parent_check alternates. We don't create any index corruption 496# that would behave differently under these modes, so just smoke test that the 497# arguments are handled sensibly. 498# 499$node->command_checks_all( 500 [ @cmd, '-s', 's1', '-i', 't1_btree', '--parent-check', 'db1' ], 501 2, 502 [$index_missing_relation_fork_re], 503 [$no_output_re], 504 'pg_amcheck smoke test --parent-check'); 505 506$node->command_checks_all( 507 [ 508 @cmd, '-s', 's1', '-i', 't1_btree', '--heapallindexed', 509 '--rootdescend', 'db1' 510 ], 511 2, 512 [$index_missing_relation_fork_re], 513 [$no_output_re], 514 'pg_amcheck smoke test --heapallindexed --rootdescend'); 515 516$node->command_checks_all( 517 [ @cmd, '-d', 'db1', '-d', 'db2', '-d', 'db3', '-S', 's*' ], 518 0, [$no_output_re], [$no_output_re], 519 'pg_amcheck excluding all corrupt schemas'); 520