From: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Date: Sat, 8 Aug 2026 12:40:00 +0500
Subject: [PATCH] Fix temp cleanup hang under SERIALIZABLE READ ONLY DEFERRABLE (BUG #19441)

RemoveTempRelationsCallback() pushes an active snapshot so toast fetches
during backend-exit temp drops succeed.  Using GetTransactionSnapshot()
honors SERIALIZABLE READ ONLY DEFERRABLE session defaults and waits in
GetSafeSnapshot() for concurrent read/write serializable transactions.
A still-prepared serializable xact never finishes that wait, and during
proc_exit interrupts are held off, so the backend hangs until the
prepared transaction is resolved.

Switch to GetCatalogSnapshot(), which is sufficient for this catalog
cleanup and never enters the deferrable safe-snapshot wait.

Bug: #19441
Author: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Reported-by: Alexander Lakhin <exclusion@gmail.com>
Discussion: https://postgr.es/m/19441-ec29f3b1363b4a68@postgresql.org
---
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 56b87d878e8..165a87e6397 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -4700,7 +4700,15 @@ RemoveTempRelationsCallback(int code, Datum arg)
 		/* Need to ensure we have a usable transaction. */
 		AbortOutOfAnyTransaction();
 		StartTransactionCommand();
-		PushActiveSnapshot(GetTransactionSnapshot());
+
+		/*
+		 * Need an active snapshot for toast fetches during deletion.  Do not
+		 * use GetTransactionSnapshot(): under SERIALIZABLE READ ONLY
+		 * DEFERRABLE it may wait in GetSafeSnapshot(), and proc_exit holds
+		 * off interrupts so that wait cannot be cancelled.  A catalog
+		 * snapshot is enough and avoids that path.
+		 */
+		PushActiveSnapshot(GetCatalogSnapshot(RelationRelationId));
 
 		RemoveTempRelations(myTempNamespace);
 
diff --git a/src/test/modules/test_misc/t/015_temp_schema_exit_deferrable.pl b/src/test/modules/test_misc/t/015_temp_schema_exit_deferrable.pl
new file mode 100644
index 00000000000..07de36630a6
--- /dev/null
+++ b/src/test/modules/test_misc/t/015_temp_schema_exit_deferrable.pl
@@ -0,0 +1,58 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Backend exit must finish temp-schema cleanup even when the session default
+# is SERIALIZABLE READ ONLY DEFERRABLE and a prepared serializable transaction
+# is still around.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('deferrable_temp_exit');
+$node->init;
+$node->append_conf('postgresql.conf', 'max_prepared_transactions = 1');
+$node->start;
+
+my $psql1 = $node->background_psql('postgres');
+
+$psql1->query_safe(
+	q{
+CREATE TEMPORARY TABLE tt (i int);
+SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE;
+});
+
+my $pid = $psql1->query_safe(q{SELECT pg_backend_pid();});
+chomp $pid;
+like($pid, qr/^\d+$/, "backend pid $pid");
+
+# Overlapping read/write serializable xact that outlives session 1.
+$node->safe_psql(
+	'postgres',
+	q{
+CREATE TABLE t (i int);
+BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
+INSERT INTO t VALUES (1);
+PREPARE TRANSACTION 'pt';
+});
+
+# Disconnect: RemoveTempRelationsCallback runs during backend exit.
+$psql1->quit;
+
+ok( $node->poll_query_until(
+		'postgres',
+		"SELECT count(*) = 0 FROM pg_stat_activity WHERE pid = $pid"),
+	'backend exited despite prepared serializable xact');
+
+is( $node->safe_psql(
+		'postgres',
+		q{SELECT count(*) FROM pg_class WHERE relname = 'tt' AND relpersistence = 't'}
+	),
+	'0',
+	'temporary table cleaned up on exit');
+
+$node->safe_psql('postgres', q{ROLLBACK PREPARED 'pt';});
+$node->safe_psql('postgres', q{DROP TABLE t;});
+
+done_testing();
