From e46dc6155a478f7bfb06729f77f02b1d145d930a Mon Sep 17 00:00:00 2001
From: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Date: Sun, 30 Aug 2026 07:42:21 +0000
Subject: [PATCH v1 1/2] Prevent orphaned tablespace dependencies

DROP TABLESPACE checked pg_shdepend without first locking the tablespace
object.  A concurrent command could therefore add a shared dependency
after that check, allowing both commands to commit and leaving an object
that referenced a deleted tablespace.

Take an exclusive lock on the tablespace before checking its shared
dependencies.  This conflicts with the lock taken when recording a shared
dependency and makes either DROP or the dependency creator recheck after
waiting.

Add isolation coverage for both dependency-first and drop-first races.
---
 src/backend/commands/tablespace.c             |  5 ++
 .../tablespace-dependency-locking.out         | 36 ++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/tablespace-dependency-locking.spec  | 66 +++++++++++++++++++
 4 files changed, 108 insertions(+)
 create mode 100644 src/test/isolation/expected/tablespace-dependency-locking.out
 create mode 100644 src/test/isolation/specs/tablespace-dependency-locking.spec

diff --git a/src/backend/commands/tablespace.c b/src/backend/commands/tablespace.c
index e3c4a7fac87..99f6ecf2fd0 100644
--- a/src/backend/commands/tablespace.c
+++ b/src/backend/commands/tablespace.c
@@ -70,6 +70,7 @@
 #include "miscadmin.h"
 #include "postmaster/bgwriter.h"
 #include "storage/fd.h"
+#include "storage/lmgr.h"
 #include "storage/lwlock.h"
 #include "storage/procsignal.h"
 #include "storage/standby.h"
@@ -457,6 +458,10 @@ DropTableSpace(DropTableSpaceStmt *stmt)
 		aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_TABLESPACE,
 					   tablespacename);
 
+	/* Prevent new shared dependencies while we drop the tablespace. */
+	LockSharedObject(TableSpaceRelationId, tablespaceoid, 0,
+					 AccessExclusiveLock);
+
 	/* Check for pg_shdepend entries depending on this tablespace */
 	if (checkSharedDependencies(TableSpaceRelationId, tablespaceoid,
 								&detail, &detail_log))
diff --git a/src/test/isolation/expected/tablespace-dependency-locking.out b/src/test/isolation/expected/tablespace-dependency-locking.out
new file mode 100644
index 00000000000..74d2b718ab2
--- /dev/null
+++ b/src/test/isolation/expected/tablespace-dependency-locking.out
@@ -0,0 +1,36 @@
+Parsed test spec with 3 sessions
+
+starting permutation: s1_begin s1_create_table_in_tablespace s2_drop_tablespace s1_commit s1_drop_table s1_drop_tablespace
+step s1_begin: BEGIN;
+step s1_create_table_in_tablespace: 
+	CREATE TABLE tbl_tablespace (a int) PARTITION BY RANGE (a)
+		TABLESPACE regress_dependency_tablespace;
+
+step s2_drop_tablespace: DROP TABLESPACE regress_dependency_tablespace; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_tablespace: <... completed>
+ERROR:  tablespace "regress_dependency_tablespace" cannot be dropped because some objects depend on it
+step s1_drop_table: DROP TABLE tbl_tablespace;
+step s1_drop_tablespace: DROP TABLESPACE regress_dependency_tablespace;
+
+starting permutation: s1_begin s1_alter_tablespace s2_drop_tablespace s3_create_table_in_dropped_tablespace s1_rollback
+step s1_begin: BEGIN;
+step s1_alter_tablespace: 
+	ALTER TABLESPACE regress_dependency_tablespace
+		SET (random_page_cost = 1.1);
+
+step s2_drop_tablespace: DROP TABLESPACE regress_dependency_tablespace; <waiting ...>
+step s3_create_table_in_dropped_tablespace: 
+	DO $$
+	BEGIN
+		EXECUTE 'CREATE TABLE tbl_tablespace (a int) PARTITION BY RANGE (a)
+			TABLESPACE regress_dependency_tablespace';
+	EXCEPTION WHEN undefined_object THEN
+		RAISE NOTICE 'referenced tablespace was concurrently dropped';
+	END
+	$$;
+ <waiting ...>
+step s1_rollback: ROLLBACK;
+step s2_drop_tablespace: <... completed>
+s3: NOTICE:  referenced tablespace was concurrently dropped
+step s3_create_table_in_dropped_tablespace: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 1fcf4e63238..fc45d504d2b 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -128,5 +128,6 @@ test: matview-write-skew
 test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
+test: tablespace-dependency-locking
 test: pub-concurrent-drop
 test: drop-owned-grant
diff --git a/src/test/isolation/specs/tablespace-dependency-locking.spec b/src/test/isolation/specs/tablespace-dependency-locking.spec
new file mode 100644
index 00000000000..fa0b00aeb99
--- /dev/null
+++ b/src/test/isolation/specs/tablespace-dependency-locking.spec
@@ -0,0 +1,66 @@
+# Test that concurrent DROP TABLESPACE and CREATE TABLE do not leave behind
+# references to a non-existent tablespace.
+
+setup
+{
+	SET allow_in_place_tablespaces = true;
+}
+
+setup
+{
+	CREATE TABLESPACE regress_dependency_tablespace LOCATION '';
+}
+
+teardown
+{
+	DROP TABLESPACE IF EXISTS regress_dependency_tablespace;
+}
+
+session "s1"
+
+step "s1_begin" { BEGIN; }
+step "s1_create_table_in_tablespace"
+{
+	CREATE TABLE tbl_tablespace (a int) PARTITION BY RANGE (a)
+		TABLESPACE regress_dependency_tablespace;
+}
+step "s1_alter_tablespace"
+{
+	ALTER TABLESPACE regress_dependency_tablespace
+		SET (random_page_cost = 1.1);
+}
+step "s1_commit" { COMMIT; }
+step "s1_rollback" { ROLLBACK; }
+step "s1_drop_table" { DROP TABLE tbl_tablespace; }
+step "s1_drop_tablespace" { DROP TABLESPACE regress_dependency_tablespace; }
+
+teardown
+{
+	SET client_min_messages = warning;
+	DROP TABLE IF EXISTS tbl_tablespace;
+}
+
+session "s2"
+
+step "s2_drop_tablespace" { DROP TABLESPACE regress_dependency_tablespace; }
+
+session "s3"
+
+step "s3_create_table_in_dropped_tablespace"
+{
+	DO $$
+	BEGIN
+		EXECUTE 'CREATE TABLE tbl_tablespace (a int) PARTITION BY RANGE (a)
+			TABLESPACE regress_dependency_tablespace';
+	EXCEPTION WHEN undefined_object THEN
+		RAISE NOTICE 'referenced tablespace was concurrently dropped';
+	END
+	$$;
+}
+
+# create table - drop tablespace
+permutation "s1_begin" "s1_create_table_in_tablespace" "s2_drop_tablespace" "s1_commit" "s1_drop_table" "s1_drop_tablespace"
+
+# drop tablespace - create table; ALTER makes DROP wait while deleting the
+# catalog tuple, after DROP has checked for dependencies
+permutation "s1_begin" "s1_alter_tablespace" "s2_drop_tablespace" "s3_create_table_in_dropped_tablespace" "s1_rollback"
-- 
2.43.0

