From 6c927a21130f04f6e58e460ec40f2441f2200c2a Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
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)

