]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Avoid orphaned objects dependencies
authorHeikki Linnakangas <heikki.linnakangas@iki.fi>
Wed, 27 May 2026 15:35:58 +0000 (18:35 +0300)
committerHeikki Linnakangas <heikki.linnakangas@iki.fi>
Wed, 27 May 2026 15:37:12 +0000 (18:37 +0300)
Concurrent DDL can leave behind objects referencing other objects that
no longer exist. This can happen if an object is dropped, while a new
object that depends on it is created concurrently. For example:

session 1: BEGIN; CREATE FUNCTION myschema.myfunc() ...;
session 2: DROP SCHEMA myschema;
session 1: COMMIT;

DROP SCHEMA does check that there are no objects dependending on the
schema being dropped, but it does not see objects being concurrently
created by other sessions. Even if it did, this scenario would still
fail:

session 1: BEGIN: DROP SCHEMA myschema;
session 2: CREATE FUNCTION myschema.myfunc() ...;
session 1: COMMIT;

When the DROP SCHEMA runs, the schema was empty, but the new function
is created in it before the dropping transaction completes. The CREATE
FUNCTION does not see that the schema is concurrently being dropped.

In both of these scenarios, the function is left behind in the schema
that no longer exists.

To fix, acquire AccessShareLock on all referenced objects when
recording dependencies. This conflicts with the AccessExclusiveLock
taken by DROP, preventing the race. After acquiring the lock, verify
that the object still exists, and if it was dropped concurrently,
report an error. We already had such a mechanism for shared
dependencies, but for some reason we didn't do it for in-database
dependendies.

Ideally the locks would be acquired much earlier when creating a new
object, but that will require modifying a lot of callers. This check
while recording the dependency is a nice wholesale protection, and
even if we change all the CREATE commands to acquire locks earlier,
it's still good to have this as a backstop to catch any cases where we
forgot to do so.

The patch adds a few tests for some cases that left behind orphaned
objects before this. It also adds a test for roles, which already had
such protection, although that test is partially disabled because the
error message includes an OID which is not predictable.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Discussion: https://postgr.es/m/ZiYjn0eVc7pxVY45@ip-10-97-1-34.eu-west-3.compute.internal
Backpatch-through: 14

src/backend/catalog/pg_depend.c
src/test/isolation/expected/ddl-dependency-locking.out [new file with mode: 0644]
src/test/isolation/isolation_schedule
src/test/isolation/specs/ddl-dependency-locking.spec [new file with mode: 0644]
src/test/regress/expected/alter_table.out

index b3d1c2fba9909edeeec2ccf9f11f6cd8f8ee0b99..d5c1a781d81da7c33d3567534a3436cde212995b 100644 (file)
 #include "catalog/pg_type.h"
 #include "commands/extension.h"
 #include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
 #include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 
 static bool isObjectPinned(const ObjectAddress *object);
+static void dependencyLockAndCheckObject(Oid classId, Oid objectId);
 
 
 /*
@@ -108,6 +112,13 @@ recordMultipleDependencies(const ObjectAddress *depender,
                if (isObjectPinned(referenced))
                        continue;
 
+               /*
+                * Make sure the new referenced object doesn't go away while we record
+                * the dependency.  DROP routines should lock the object exclusively
+                * before they check dependencies.
+                */
+               dependencyLockAndCheckObject(referenced->classId, referenced->objectId);
+
                if (slot_init_count < max_slots)
                {
                        slot[slot_stored_count] = MakeSingleTupleTableSlot(RelationGetDescr(dependDesc),
@@ -506,6 +517,13 @@ changeDependencyFor(Oid classId, Oid objectId,
                return 1;
        }
 
+       /*
+        * Make sure the new referenced object doesn't go away while we record the
+        * dependency.
+        */
+       if (!newIsPinned)
+               dependencyLockAndCheckObject(refClassId, newRefObjectId);
+
        depRel = table_open(DependRelationId, RowExclusiveLock);
 
        /* There should be existing dependency record(s), so search. */
@@ -713,6 +731,119 @@ isObjectPinned(const ObjectAddress *object)
 }
 
 
+/*
+ * dependencyLockAndCheckObject
+ *
+ * Lock the object that we are about to record a dependency on.  After it's
+ * locked, verify that it hasn't been dropped while we weren't looking.  If it
+ * has been dropped, throw an an error.
+ *
+ * If the caller already holds a lock that conflicts with DROP
+ * (AccessShareLock or stronger), this does nothing.  Callers should acquire
+ * locks already when they look up the dependent objects, but many callers
+ * currently do not.  This is a backstop to make sure that we don't record a
+ * bogus reference permanently in the catalogs in that case.  In the future,
+ * after we have tightened up all the callers to acquire locks earlier, this
+ * could just verify that the object is already locked and throw an error if
+ * not.
+ */
+static void
+dependencyLockAndCheckObject(Oid classId, Oid objectId)
+{
+       /*
+        * Pinned objects cannot be dropped concurrently, and callers checked this
+        * already.
+        */
+       Assert(!IsPinnedObject(classId, objectId));
+
+       if (classId != RelationRelationId)
+       {
+               LOCKTAG         tag;
+               int                     cache;
+               Relation        rel;
+               SysScanDesc scan;
+               ScanKeyData skey;
+               HeapTuple       tuple;
+
+               SET_LOCKTAG_OBJECT(tag,
+                                                  MyDatabaseId,
+                                                  classId,
+                                                  objectId,
+                                                  0);
+
+               if (LockOrStrongerHeldByMe(&tag, AccessShareLock))
+                       return;
+
+               /* Assume we should lock the whole object not a sub-object */
+               LockDatabaseObject(classId, objectId, 0, AccessShareLock);
+
+               /*
+                * Check that the object still exists.  If the catalog has a suitable
+                * syscache, check that first.
+                */
+               cache = get_object_catcache_oid(classId);
+               if (cache != -1)
+               {
+                       if (SearchSysCacheExists1(cache, ObjectIdGetDatum(objectId)))
+                               return;
+               }
+
+               /*
+                * If it's not found in the syscache, or there's no suitable syscache
+                * we can use, scan the catalog table using SnapshotSelf.  This
+                * handles the case that it's an object we just created (for example,
+                * if it's a composite type created as part of creating a table).
+                */
+               rel = table_open(classId, AccessShareLock);
+
+               ScanKeyInit(&skey,
+                                       get_object_attnum_oid(classId),
+                                       BTEqualStrategyNumber, F_OIDEQ,
+                                       ObjectIdGetDatum(objectId));
+
+               scan = systable_beginscan(rel, get_object_oid_index(classId),
+                                                                 true, SnapshotSelf, 1, &skey);
+
+               tuple = systable_getnext(scan);
+               if (!HeapTupleIsValid(tuple))
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_UNDEFINED_OBJECT),
+                                        errmsg("dependent %s was concurrently dropped",
+                                                       get_object_class_descr(classId))));
+
+               systable_endscan(scan);
+               table_close(rel, AccessShareLock);
+       }
+       else
+       {
+               /*
+                * Same logic for pg_class entries, but locking relations is handled
+                * by different functions.
+                *
+                * Callers are more careful with locking relations than other objects,
+                * so we should already have a lock on the relation, or on another
+                * object that indirectly prevents the relation from being dropped.
+                * For example, we might have a strong lock on a table while adding
+                * dependency to its index.  However, we cannot detect the indirectly
+                * protected case here easily.  To err on the safe side, acquire a
+                * lock directly on the relation if we're not holding one already.
+                */
+
+               /* all shared relations are pinned */
+               Assert(!IsSharedRelation(objectId));
+
+               if (CheckRelationOidLockedByMe(objectId, AccessShareLock, true))
+                       return;
+               LockRelationOid(objectId, AccessShareLock);
+
+               if (SearchSysCacheExists1(RELOID, ObjectIdGetDatum(objectId)))
+                       return;
+               ereport(ERROR,
+                               (errcode(ERRCODE_UNDEFINED_OBJECT),
+                                errmsg("dependent relation was concurrently dropped")));
+       }
+}
+
 /*
  * Various special-purpose lookups and manipulations of pg_depend.
  */
diff --git a/src/test/isolation/expected/ddl-dependency-locking.out b/src/test/isolation/expected/ddl-dependency-locking.out
new file mode 100644 (file)
index 0000000..3bf9e43
--- /dev/null
@@ -0,0 +1,137 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_create_function_in_schema s2_drop_schema s1_commit
+step s1_begin: BEGIN;
+step s1_create_function_in_schema: CREATE FUNCTION testschema.foo() RETURNS int AS 'select 1' LANGUAGE sql;
+step s2_drop_schema: DROP SCHEMA testschema; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_schema: <... completed>
+ERROR:  cannot drop schema testschema because other objects depend on it
+
+starting permutation: s2_begin s2_drop_schema s1_create_function_in_schema s2_commit
+step s2_begin: BEGIN;
+step s2_drop_schema: DROP SCHEMA testschema;
+step s1_create_function_in_schema: CREATE FUNCTION testschema.foo() RETURNS int AS 'select 1' LANGUAGE sql; <waiting ...>
+step s2_commit: COMMIT;
+step s1_create_function_in_schema: <... completed>
+ERROR:  dependent schema was concurrently dropped
+
+starting permutation: s1_begin s1_alter_function_schema s2_drop_alterschema s1_commit
+step s1_begin: BEGIN;
+step s1_alter_function_schema: ALTER FUNCTION public.falter() SET SCHEMA alterschema;
+step s2_drop_alterschema: DROP SCHEMA alterschema; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_alterschema: <... completed>
+ERROR:  cannot drop schema alterschema because other objects depend on it
+
+starting permutation: s2_begin s2_drop_alterschema s1_alter_function_schema s2_commit
+step s2_begin: BEGIN;
+step s2_drop_alterschema: DROP SCHEMA alterschema;
+step s1_alter_function_schema: ALTER FUNCTION public.falter() SET SCHEMA alterschema; <waiting ...>
+step s2_commit: COMMIT;
+step s1_alter_function_schema: <... completed>
+ERROR:  dependent schema was concurrently dropped
+
+starting permutation: s1_begin s1_create_function_with_argtype s2_drop_foo_type s1_commit
+step s1_begin: BEGIN;
+step s1_create_function_with_argtype: CREATE FUNCTION fooargtype(num foo) RETURNS int AS 'select 1' LANGUAGE sql;
+step s2_drop_foo_type: DROP TYPE public.foo; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_foo_type: <... completed>
+ERROR:  cannot drop type foo because other objects depend on it
+
+starting permutation: s2_begin s2_drop_foo_type s1_create_function_with_argtype s2_commit
+step s2_begin: BEGIN;
+step s2_drop_foo_type: DROP TYPE public.foo;
+step s1_create_function_with_argtype: CREATE FUNCTION fooargtype(num foo) RETURNS int AS 'select 1' LANGUAGE sql; <waiting ...>
+step s2_commit: COMMIT;
+step s1_create_function_with_argtype: <... completed>
+ERROR:  dependent type was concurrently dropped
+
+starting permutation: s1_begin s1_create_function_with_rettype s2_drop_foo_rettype s1_commit
+step s1_begin: BEGIN;
+step s1_create_function_with_rettype: CREATE FUNCTION footrettype() RETURNS id LANGUAGE sql RETURN 1;
+step s2_drop_foo_rettype: DROP DOMAIN id; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_foo_rettype: <... completed>
+ERROR:  cannot drop type id because other objects depend on it
+
+starting permutation: s2_begin s2_drop_foo_rettype s1_create_function_with_rettype s2_commit
+step s2_begin: BEGIN;
+step s2_drop_foo_rettype: DROP DOMAIN id;
+step s1_create_function_with_rettype: CREATE FUNCTION footrettype() RETURNS id LANGUAGE sql RETURN 1; <waiting ...>
+step s2_commit: COMMIT;
+step s1_create_function_with_rettype: <... completed>
+ERROR:  dependent type was concurrently dropped
+
+starting permutation: s1_begin s1_create_function_with_function s2_drop_function_f s1_commit
+step s1_begin: BEGIN;
+step s1_create_function_with_function: CREATE FUNCTION foofunc() RETURNS int LANGUAGE SQL RETURN f() + 1;
+step s2_drop_function_f: DROP FUNCTION f(); <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_function_f: <... completed>
+ERROR:  cannot drop function f() because other objects depend on it
+
+starting permutation: s2_begin s2_drop_function_f s1_create_function_with_function s2_commit
+step s2_begin: BEGIN;
+step s2_drop_function_f: DROP FUNCTION f();
+step s1_create_function_with_function: CREATE FUNCTION foofunc() RETURNS int LANGUAGE SQL RETURN f() + 1; <waiting ...>
+step s2_commit: COMMIT;
+step s1_create_function_with_function: <... completed>
+ERROR:  dependent function was concurrently dropped
+
+starting permutation: s1_begin s1_create_domain_with_domain s2_drop_domain_id s1_commit
+step s1_begin: BEGIN;
+step s1_create_domain_with_domain: CREATE DOMAIN idid as id;
+step s2_drop_domain_id: DROP DOMAIN id; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_domain_id: <... completed>
+ERROR:  cannot drop type id because other objects depend on it
+
+starting permutation: s2_begin s2_drop_domain_id s1_create_domain_with_domain s2_commit
+step s2_begin: BEGIN;
+step s2_drop_domain_id: DROP DOMAIN id;
+step s1_create_domain_with_domain: CREATE DOMAIN idid as id; <waiting ...>
+step s2_commit: COMMIT;
+step s1_create_domain_with_domain: <... completed>
+ERROR:  dependent type was concurrently dropped
+
+starting permutation: s1_begin s1_create_table_with_type s2_drop_footab_type s1_commit
+step s1_begin: BEGIN;
+step s1_create_table_with_type: CREATE TABLE tabtype(a footab);
+step s2_drop_footab_type: DROP TYPE public.footab; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_footab_type: <... completed>
+ERROR:  cannot drop type footab because other objects depend on it
+
+starting permutation: s2_begin s2_drop_footab_type s1_create_table_with_type s2_commit
+step s2_begin: BEGIN;
+step s2_drop_footab_type: DROP TYPE public.footab;
+step s1_create_table_with_type: CREATE TABLE tabtype(a footab); <waiting ...>
+step s2_commit: COMMIT;
+step s1_create_table_with_type: <... completed>
+ERROR:  dependent type was concurrently dropped
+
+starting permutation: s1_begin s1_create_server_with_fdw_wrapper s2_drop_fdw_wrapper s1_commit
+step s1_begin: BEGIN;
+step s1_create_server_with_fdw_wrapper: CREATE SERVER srv_fdw_wrapper FOREIGN DATA WRAPPER fdw_wrapper;
+step s2_drop_fdw_wrapper: DROP FOREIGN DATA WRAPPER fdw_wrapper RESTRICT; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_fdw_wrapper: <... completed>
+ERROR:  cannot drop foreign-data wrapper fdw_wrapper because other objects depend on it
+
+starting permutation: s2_begin s2_drop_fdw_wrapper s1_create_server_with_fdw_wrapper s2_commit
+step s2_begin: BEGIN;
+step s2_drop_fdw_wrapper: DROP FOREIGN DATA WRAPPER fdw_wrapper RESTRICT;
+step s1_create_server_with_fdw_wrapper: CREATE SERVER srv_fdw_wrapper FOREIGN DATA WRAPPER fdw_wrapper; <waiting ...>
+step s2_commit: COMMIT;
+step s1_create_server_with_fdw_wrapper: <... completed>
+ERROR:  dependent foreign-data wrapper was concurrently dropped
+
+starting permutation: s1_begin s1_alter_function_owner s2_drop_role s1_commit
+step s1_begin: BEGIN;
+step s1_alter_function_owner: ALTER FUNCTION public.falter() OWNER TO regress_dependency;
+step s2_drop_role: DROP ROLE regress_dependency; <waiting ...>
+step s1_commit: COMMIT;
+step s2_drop_role: <... completed>
+ERROR:  role "regress_dependency" cannot be dropped because some objects depend on it
index 9f49e72a5043697e7ce05c8b1330a48f76f83335..0b9e67347cc0693938bb5d16acc5d835ce13818e 100644 (file)
@@ -115,3 +115,4 @@ test: serializable-parallel
 test: serializable-parallel-2
 test: serializable-parallel-3
 test: matview-write-skew
+test: ddl-dependency-locking
diff --git a/src/test/isolation/specs/ddl-dependency-locking.spec b/src/test/isolation/specs/ddl-dependency-locking.spec
new file mode 100644 (file)
index 0000000..de5bd88
--- /dev/null
@@ -0,0 +1,104 @@
+# Test that concurrent DROP and CREATE commands do not leave behind
+# references to non-existent objects.
+
+setup
+{
+       CREATE SCHEMA testschema;
+       CREATE SCHEMA alterschema;
+       CREATE TYPE public.foo as enum ('one', 'two');
+       CREATE TYPE public.footab as enum ('three', 'four');
+       CREATE DOMAIN id AS int;
+       CREATE FUNCTION f() RETURNS int LANGUAGE SQL RETURN 1;
+       CREATE FUNCTION public.falter() RETURNS int LANGUAGE SQL RETURN 1;
+       CREATE FOREIGN DATA WRAPPER fdw_wrapper;
+       CREATE ROLE regress_dependency;
+}
+
+teardown
+{
+       DROP FUNCTION IF EXISTS testschema.foo();
+       DROP FUNCTION IF EXISTS fooargtype(num foo);
+       DROP FUNCTION IF EXISTS footrettype();
+       DROP FUNCTION IF EXISTS foofunc();
+       DROP FUNCTION IF EXISTS public.falter();
+       DROP FUNCTION IF EXISTS alterschema.falter();
+       DROP DOMAIN IF EXISTS idid;
+       DROP SERVER IF EXISTS srv_fdw_wrapper;
+       DROP TABLE IF EXISTS tabtype;
+       DROP SCHEMA IF EXISTS testschema;
+       DROP SCHEMA IF EXISTS alterschema;
+       DROP TYPE IF EXISTS public.foo;
+       DROP TYPE IF EXISTS public.footab;
+       DROP DOMAIN IF EXISTS id;
+       DROP FUNCTION IF EXISTS f();
+       DROP FOREIGN DATA WRAPPER IF EXISTS fdw_wrapper;
+       DROP ROLE regress_dependency;
+}
+
+session "s1"
+
+step "s1_begin" { BEGIN; }
+step "s1_create_function_in_schema" { CREATE FUNCTION testschema.foo() RETURNS int AS 'select 1' LANGUAGE sql; }
+step "s1_create_function_with_argtype" { CREATE FUNCTION fooargtype(num foo) RETURNS int AS 'select 1' LANGUAGE sql; }
+step "s1_create_function_with_rettype" { CREATE FUNCTION footrettype() RETURNS id LANGUAGE sql RETURN 1; }
+step "s1_create_function_with_function" { CREATE FUNCTION foofunc() RETURNS int LANGUAGE SQL RETURN f() + 1; }
+step "s1_alter_function_owner" { ALTER FUNCTION public.falter() OWNER TO regress_dependency; }
+step "s1_alter_function_schema" { ALTER FUNCTION public.falter() SET SCHEMA alterschema; }
+step "s1_create_domain_with_domain" { CREATE DOMAIN idid as id; }
+step "s1_create_table_with_type" { CREATE TABLE tabtype(a footab); }
+step "s1_create_server_with_fdw_wrapper" { CREATE SERVER srv_fdw_wrapper FOREIGN DATA WRAPPER fdw_wrapper; }
+step "s1_commit" { COMMIT; }
+
+session "s2"
+
+step "s2_begin" { BEGIN; }
+step "s2_drop_schema" { DROP SCHEMA testschema; }
+step "s2_drop_alterschema" { DROP SCHEMA alterschema; }
+step "s2_drop_foo_type" { DROP TYPE public.foo; }
+step "s2_drop_foo_rettype" { DROP DOMAIN id; }
+step "s2_drop_footab_type" { DROP TYPE public.footab; }
+step "s2_drop_function_f" { DROP FUNCTION f(); }
+step "s2_drop_domain_id" { DROP DOMAIN id; }
+step "s2_drop_fdw_wrapper" { DROP FOREIGN DATA WRAPPER fdw_wrapper RESTRICT; }
+step "s2_drop_role" { DROP ROLE regress_dependency; }
+step "s2_commit" { COMMIT; }
+
+# create function - drop schema
+permutation "s1_begin" "s1_create_function_in_schema" "s2_drop_schema" "s1_commit"
+permutation "s2_begin" "s2_drop_schema" "s1_create_function_in_schema" "s2_commit"
+
+# alter function - drop schema
+permutation "s1_begin" "s1_alter_function_schema" "s2_drop_alterschema" "s1_commit"
+permutation "s2_begin" "s2_drop_alterschema" "s1_alter_function_schema" "s2_commit"
+
+# create function - drop argtype
+permutation "s1_begin" "s1_create_function_with_argtype" "s2_drop_foo_type" "s1_commit"
+permutation "s2_begin" "s2_drop_foo_type" "s1_create_function_with_argtype" "s2_commit"
+
+# create function - drop rettype
+permutation "s1_begin" "s1_create_function_with_rettype" "s2_drop_foo_rettype" "s1_commit"
+permutation "s2_begin" "s2_drop_foo_rettype" "s1_create_function_with_rettype" "s2_commit"
+
+# create function - drop function used in its body
+permutation "s1_begin" "s1_create_function_with_function" "s2_drop_function_f" "s1_commit"
+permutation "s2_begin" "s2_drop_function_f" "s1_create_function_with_function" "s2_commit"
+
+# create domain over domain - drop the base domain
+permutation "s1_begin" "s1_create_domain_with_domain" "s2_drop_domain_id" "s1_commit"
+permutation "s2_begin" "s2_drop_domain_id" "s1_create_domain_with_domain" "s2_commit"
+
+# create table - drop type used in column
+permutation "s1_begin" "s1_create_table_with_type" "s2_drop_footab_type" "s1_commit"
+permutation "s2_begin" "s2_drop_footab_type" "s1_create_table_with_type" "s2_commit"
+
+# create server - drop foreign data wrapper
+permutation "s1_begin" "s1_create_server_with_fdw_wrapper" "s2_drop_fdw_wrapper" "s1_commit"
+permutation "s2_begin" "s2_drop_fdw_wrapper" "s1_create_server_with_fdw_wrapper" "s2_commit"
+
+# create function - drop owner role
+permutation "s1_begin" "s1_alter_function_owner" "s2_drop_role" "s1_commit"
+
+# XXX: This permutation is disabled because the error message, "role
+# <OID> was concurrently dropped", contains an OID that is not stable.
+#
+# permutation "s2_begin" "s2_drop_role" "s1_alter_function_owner" "s2_commit"
index 5ecf000c70fd69e8e4c66b308095600eb138cb3d..3b545becf21bf0c3c0619fe681b5996214b0d7cd 100644 (file)
@@ -2845,11 +2845,12 @@ begin;
 alter table alterlock2
 add constraint alterlock2nv foreign key (f1) references alterlock (f1) NOT VALID;
 select * from my_locks order by 1;
-  relname   |     max_lockmode      
-------------+-----------------------
- alterlock  | ShareRowExclusiveLock
- alterlock2 | ShareRowExclusiveLock
-(2 rows)
+    relname     |     max_lockmode      
+----------------+-----------------------
+ alterlock      | ShareRowExclusiveLock
+ alterlock2     | ShareRowExclusiveLock
+ alterlock_pkey | AccessShareLock
+(3 rows)
 
 commit;
 begin;