From 7be6fa2edeaf0da8c9c6f167fcbef4af56c56bed Mon Sep 17 00:00:00 2001
From: Maxim Orlov <orlovmg@gmail.com>
Date: Wed, 21 Oct 2026 16:25:03 +0300
Subject: [PATCH v1 1/3] Add 2pc race test between CheckPointTwoPhase() and
 FinishPreparedTransaction()

---
 src/backend/access/transam/twophase.c   |  5 +-
 src/test/recovery/t/061_2pc_lsn_race.pl | 93 +++++++++++++++++++++++++
 2 files changed, 97 insertions(+), 1 deletion(-)
 create mode 100644 src/test/recovery/t/061_2pc_lsn_race.pl

diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index 48e478a4ecb..2129c75336a 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -1536,7 +1536,9 @@ FinishPreparedTransaction(const char *gid, bool isCommit)
 	 * in WAL files if the LSN is after the last checkpoint record, or moved
 	 * to disk if for some reason they have lived for a long time.
 	 */
-	if (gxact->ondisk)
+	ondisk = gxact->ondisk;
+	INJECTION_POINT("twophase-finish-after-ondisk-read", NULL);
+	if (ondisk)
 		buf = ReadTwoPhaseFile(fxid, false);
 	else
 		XlogReadTwoPhaseData(gxact->prepare_start_lsn, &buf, NULL);
@@ -1872,6 +1874,7 @@ CheckPointTwoPhase(XLogRecPtr redo_horizon)
 			XlogReadTwoPhaseData(gxact->prepare_start_lsn, &buf, &len);
 			RecreateTwoPhaseFile(gxact->fxid, buf, len);
 			gxact->ondisk = true;
+			INJECTION_POINT("twophase-checkpoint-after-ondisk-set", NULL);
 			gxact->prepare_start_lsn = InvalidXLogRecPtr;
 			gxact->prepare_end_lsn = InvalidXLogRecPtr;
 			pfree(buf);
diff --git a/src/test/recovery/t/061_2pc_lsn_race.pl b/src/test/recovery/t/061_2pc_lsn_race.pl
new file mode 100644
index 00000000000..615fbbde8e5
--- /dev/null
+++ b/src/test/recovery/t/061_2pc_lsn_race.pl
@@ -0,0 +1,93 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+#
+# Reproduce a race between CheckPointTwoPhase() changing ondisk/LSNs and
+# FinishPreparedTransaction() reading those fields without syncing.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node1 = PostgreSQL::Test::Cluster->new('twophase_lsn_race');
+$node1->init;
+$node1->append_conf('postgresql.conf', q{
+	max_prepared_transactions = 10
+	log_checkpoints = on
+});
+$node1->start;
+
+if (!$node1->check_extension('injection_points'))
+{
+	plan skip_all => 'Extension injection_points not installed';
+}
+
+$node1->safe_psql('postgres', q{CREATE EXTENSION injection_points});
+$node1->safe_psql(
+	'postgres',
+	q{SELECT injection_points_attach('twophase-finish-after-ondisk-read',
+									 'wait')});
+$node1->safe_psql(
+	'postgres',
+	q{SELECT injection_points_attach('twophase-checkpoint-after-ondisk-set',
+									 'wait')});
+
+$node1->safe_psql('postgres', q{
+	BEGIN;
+	CREATE TABLE twophase_lsn_race_table (id integer);
+	INSERT INTO twophase_lsn_race_table VALUES (1);
+	PREPARE TRANSACTION 'twophase_lsn_race_gid';
+});
+
+# Pause COMMIT PREPARED after it has copied ondisk=false to its local variable.
+my $commit = $node1->background_psql('postgres');
+$commit->query_until(
+	qr/commit_started/, q{
+	\echo commit_started
+	COMMIT PREPARED 'twophase_lsn_race_gid';
+	\echo commit_finished
+});
+$node1->wait_for_event('client backend', 'twophase-finish-after-ondisk-read');
+
+# The checkpoint now sets ondisk=true and stops before clearing the LSNs.
+my $checkpoint = $node1->background_psql('postgres');
+$checkpoint->query_until(
+	qr/checkpoint_started/, q{
+	\echo checkpoint_started
+	CHECKPOINT;
+	\echo checkpoint_finished
+});
+$node1->wait_for_event('checkpointer', 'twophase-checkpoint-after-ondisk-set');
+
+# Finish the checkpoint first, so prepare_start_lsn is invalidated. Then
+# resume COMMIT PREPARED, which still has the earlier ondisk=false value.
+$node1->safe_psql('postgres',
+	q{SELECT injection_points_wakeup('twophase-checkpoint-after-ondisk-set')});
+$checkpoint->query_until(qr/checkpoint_finished/, '');
+
+$node1->safe_psql('postgres',
+	q{SELECT injection_points_wakeup('twophase-finish-after-ondisk-read')});
+
+$checkpoint->quit;
+$commit->quit;
+
+ok($node1->is_alive(), "server is still running");
+
+$node1->stop;
+
+my $logfile = slurp_file($node1->logfile());
+ok($logfile !~
+	qr/TRAP: failed Assert*/,
+	"no failed assertion");
+
+# Reproduce a race between CheckPointTwoPhase() changing ondisk/LSNs and
+# LookupGXact() reading those fields without syncing.
+# TODO: add here...
+
+done_testing();
-- 
2.55.0

