From 6c927a21130f04f6e58e460ec40f2441f2200c2a Mon Sep 17 00:00:00 2001 From: Vignesh C Date: Tue, 28 Jul 2026 15:24:39 +0530 Subject: [PATCH v1] Add a TAP test to reproduce pg_dump sequence read race Add a regression test that reproduces a race between pg_dump and concurrent ALTER SEQUENCE commands. pg_dump reads each sequence's current last_value and is_called via pg_get_sequence_data(). If a definition-changing ALTER SEQUENCE commits after pg_get_sequence_data() has opened the sequence relation but before it reads the on-disk tuple, the ALTER rewrites the sequence to a new relfilenode. As a result, pg_get_sequence_data() can fail with a "could not read blocks" error. --- src/backend/commands/sequence.c | 7 ++ src/bin/pg_dump/Makefile | 4 + src/bin/pg_dump/meson.build | 2 + .../007_pg_dump_sequence_relfilenode_race.pl | 103 ++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 src/bin/pg_dump/t/007_pg_dump_sequence_relfilenode_race.pl diff --git a/src/backend/commands/sequence.c b/src/backend/commands/sequence.c index 551667650ba..f710afa7bd1 100644 --- a/src/backend/commands/sequence.c +++ b/src/backend/commands/sequence.c @@ -43,6 +43,7 @@ #include "storage/smgr.h" #include "utils/acl.h" #include "utils/builtins.h" +#include "utils/injection_point.h" #include "utils/lsyscache.h" #include "utils/pg_lsn.h" #include "utils/resowner.h" @@ -1813,6 +1814,12 @@ pg_get_sequence_data(PG_FUNCTION_ARGS) seqrel = try_relation_open(relid, AccessShareLock); + /* + * Injection point to exercise the window between opening the sequence + * relation here and reading its on-disk tuple below. + */ + INJECTION_POINT("pg-get-sequence-data-after-open", NULL); + /* * Return all NULLs for missing sequences, sequences for which we lack * privileges, other sessions' temporary sequences, and unlogged sequences diff --git a/src/bin/pg_dump/Makefile b/src/bin/pg_dump/Makefile index 79073b0a0ea..5f297a4f75c 100644 --- a/src/bin/pg_dump/Makefile +++ b/src/bin/pg_dump/Makefile @@ -21,6 +21,10 @@ export LZ4 export ZSTD export with_icu +# Some of the TAP tests need the injection_points extension. +EXTRA_INSTALL = src/test/modules/injection_points +export enable_injection_points + override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS) LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport) diff --git a/src/bin/pg_dump/meson.build b/src/bin/pg_dump/meson.build index 79bd5036841..eb31f7b5c42 100644 --- a/src/bin/pg_dump/meson.build +++ b/src/bin/pg_dump/meson.build @@ -95,6 +95,7 @@ tests += { 'LZ4': program_lz4.found() ? program_lz4.full_path() : '', 'ZSTD': program_zstd.found() ? program_zstd.full_path() : '', 'with_icu': icu.found() ? 'yes' : 'no', + 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no', }, 'tests': [ 't/001_basic.pl', @@ -103,6 +104,7 @@ tests += { 't/004_pg_dump_parallel.pl', 't/005_pg_dump_filterfile.pl', 't/006_pg_dump_compress.pl', + 't/007_pg_dump_sequence_relfilenode_race.pl', 't/010_dump_connstr.pl', ], }, diff --git a/src/bin/pg_dump/t/007_pg_dump_sequence_relfilenode_race.pl b/src/bin/pg_dump/t/007_pg_dump_sequence_relfilenode_race.pl new file mode 100644 index 00000000000..a84a1e35fc5 --- /dev/null +++ b/src/bin/pg_dump/t/007_pg_dump_sequence_relfilenode_race.pl @@ -0,0 +1,103 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group +# +# Test for a race between pg_dump and ALTER SEQUENCE. +# +# pg_dump obtains each sequence's live last_value/is_called via +# pg_get_sequence_data(). A definition-changing ALTER SEQUENCE (here, +# MINVALUE) rewrites the sequence to a new relfilenode. +# +# If pg_get_sequence_data() opened the sequence using only AccessShareLock, +# which does not conflict with ALTER SEQUENCE's ShareRowExclusiveLock, +# ALTER SEQUENCE could commit after the relation was opened but before its +# on-disk tuple was read. The subsequent read would fail with: +# +# could not read blocks 0..0 in file "base/.../...": read only 0 of 8192 bytes +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $tempdir = PostgreSQL::Test::Utils::tempdir; + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->start; + +# This test depends on the injection_points extension to pause pg_dump's +# read of the sequence. +if (!$node->check_extension('injection_points')) +{ + plan skip_all => 'Extension injection_points not installed'; +} + +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +# A single, ordinary sequence is enough; pg_get_sequence_data() is invoked +# once per sequence by pg_dump's data-gathering query. +$node->safe_psql( + 'postgres', qq( + CREATE SEQUENCE regress_dump_seq; + SELECT nextval('regress_dump_seq'); +)); + +# Verify that ALTER SEQUENCE performed a rewrite. +my $old_relfilenode = $node->safe_psql('postgres', + "SELECT relfilenode FROM pg_class WHERE oid = 'regress_dump_seq'::regclass;" +); + +# Arrange for pg_get_sequence_data() to block right after it has opened the +# sequence relation, before it reads the on-disk tuple. +$node->safe_psql('postgres', + "SELECT injection_points_attach('pg-get-sequence-data-after-open', 'wait');" +); + +# Run pg_dump in the background; it will block at the injection point while +# gathering the sequence's data. +my ($stdout, $stderr) = ('', ''); +my $timer = IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default); +my $pgdump = IPC::Run::start( + [ + 'pg_dump', + '--dbname' => $node->connstr('postgres'), + '--file' => "$tempdir/dump.sql", + ], + '>' => \$stdout, + '2>' => \$stderr, + $timer); + +# Wait until pg_dump is parked inside pg_get_sequence_data(), holding +# AccessShareLock on the sequence. +$node->wait_for_event('client backend', 'pg-get-sequence-data-after-open'); + +# While pg_dump has the sequence relation open, rewrite the sequence. +# ALTER SEQUENCE ... MINVALUE rewrites the sequence to a new relfilenode. +$node->safe_psql('postgres', + "ALTER SEQUENCE regress_dump_seq MINVALUE -100;"); + +# Let pg_dump continue. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('pg-get-sequence-data-after-open');"); + +$pgdump->finish; +$timer->reset; + +# pg_dump will fail with "could not read blocks ...". +unlike( + $stderr, + qr/could not read blocks 0\.\.0 in file ".*": read only 0 of 8192 bytes/, + 'pg_dump does not fail after concurrent sequence rewrite'); + +my $new_relfilenode = $node->safe_psql('postgres', + "SELECT relfilenode FROM pg_class WHERE oid = 'regress_dump_seq'::regclass;" +); +isnt($new_relfilenode, $old_relfilenode, + 'ALTER SEQUENCE ... MINVALUE rewrote the sequence to a new relfilenode'); + +$node->safe_psql('postgres', + "SELECT injection_points_detach('pg-get-sequence-data-after-open');"); + +$node->stop('fast'); + +done_testing(); -- 2.50.1 (Apple Git-155)