From c4fb4499a8156830d9636191422ee3e7b89d5b7c Mon Sep 17 00:00:00 2001 From: Bharath Rupireddy Date: Sat, 26 Sep 2026 22:38:08 +0000 Subject: [PATCH v1] Fix DROP DATABASE FORCE failing on parallel autovacuum workers. Both an autovacuum worker and the parallel workers it launches run as the bootstrap superuser, so that is not what separates them. TerminateOtherDBBackends() looks at the role a process published in its PGPROC, and only the workers published one. An autovacuum worker gets its user from InitializeSessionUserIdStandalone(), which sets AuthenticatedUserId directly and never calls SetAuthenticatedUserId(), so MyProc->roleId stays InvalidOid and superuser_arg() on it is false. Its parallel workers take the same user through ParallelWorkerMain(), which does call SetAuthenticatedUserId(), so they publish the bootstrap superuser and the check refuses them. As a result, a role with pg_signal_backend that is not a superuser got "permission denied to terminate process" from DROP DATABASE WITH (FORCE) while a parallel autovacuum on that database was in its index phase, which on a large table is where the vacuum spends its time. The same role can terminate the autovacuum worker itself, and DROP DATABASE without FORCE succeeds with the same vacuum running. A parallel worker of a VACUUM command is not affected, since its leader is a user session and the worker publishes the same role as its leader. Autovacuum is the only leader that publishes no role while its workers publish one. Fix this by treating a process whose lock group leader is an autovacuum worker the way the autovacuum worker itself is treated. Add a test to the test_autovacuum module, which holds the parallel workers at a new injection point. Oversight in commit 1ff3180ca01. Backpatch to 19, where parallel autovacuum was introduced. Reported-by: Claude Code Author: Bharath Rupireddy Discussion: https://postgr.es/m/<> Backpatch-through: 19 --- src/backend/commands/vacuumparallel.c | 4 ++ src/backend/storage/ipc/procarray.c | 23 +++++++- .../t/001_parallel_autovacuum.pl | 52 +++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c index 4532da60c84..67630236f81 100644 --- a/src/backend/commands/vacuumparallel.c +++ b/src/backend/commands/vacuumparallel.c @@ -46,6 +46,7 @@ #include "storage/bufmgr.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -1316,6 +1317,9 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc) /* Prepare to track buffer usage during parallel execution */ InstrStartParallelQuery(); + /* Used by tests to pause a worker while it is attached to the leader */ + INJECTION_POINT("parallel-vacuum-worker-start", NULL); + /* Process indexes to perform vacuum/cleanup */ parallel_vacuum_process_safe_indexes(&pvs); diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c index b7e03134ed8..5836ed4d1cf 100644 --- a/src/backend/storage/ipc/procarray.c +++ b/src/backend/storage/ipc/procarray.c @@ -3900,14 +3900,33 @@ TerminateOtherDBBackends(Oid databaseId) if (proc != NULL) { - if (superuser_arg(proc->roleId) && !superuser()) + PGPROC *leader = proc->lockGroupLeader; + Oid roleId = proc->roleId; + + /* + * An autovacuum worker and the parallel workers it launches + * all run as the bootstrap superuser, but only the workers + * publish that role here. An autovacuum worker never goes + * through SetAuthenticatedUserId(), so its roleId stays + * InvalidOid, while a parallel worker calls it with the + * authenticated user of its leader. The checks below would + * then refuse a worker whose leader they accept, so check + * such a worker the way its leader is checked. The leader's + * PGPROC is not recycled until its last member has exited, so + * the pointer read here is the real leader. + */ + if (leader != NULL && leader != proc && + leader->backendType == B_AUTOVAC_WORKER) + roleId = InvalidOid; + + if (superuser_arg(roleId) && !superuser()) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("permission denied to terminate process"), errdetail("Only roles with the %s attribute may terminate processes of roles with the %s attribute.", "SUPERUSER", "SUPERUSER"))); - if (!has_privs_of_role(GetUserId(), proc->roleId) && + if (!has_privs_of_role(GetUserId(), roleId) && !has_privs_of_role(GetUserId(), ROLE_PG_SIGNAL_BACKEND)) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), diff --git a/src/test/modules/test_autovacuum/t/001_parallel_autovacuum.pl b/src/test/modules/test_autovacuum/t/001_parallel_autovacuum.pl index 33c86bbdc94..d1dfce8ad9b 100644 --- a/src/test/modules/test_autovacuum/t/001_parallel_autovacuum.pl +++ b/src/test/modules/test_autovacuum/t/001_parallel_autovacuum.pl @@ -253,5 +253,57 @@ $node->safe_psql('postgres', $node->safe_psql('postgres', "SELECT injection_points_detach('autovacuum-worker-cost-balanced')"); +# Test 4: +# Check that DROP DATABASE WITH (FORCE) can terminate the parallel workers of +# an autovacuum. They publish the bootstrap superuser as their role while +# their leader publishes none, so a non-superuser owner of the database was +# refused for the whole index phase of the vacuum. + +# Leave both worker slots to the parallel autovacuum below. +$node->safe_psql('postgres', + 'ALTER TABLE test_autovac SET (autovacuum_enabled = false)'); +$node->safe_psql('regress_db2', + 'ALTER TABLE filler SET (autovacuum_enabled = false)'); + +# Hand regress_db2 to a non-superuser that may terminate other sessions. +$node->safe_psql( + 'postgres', qq{ + CREATE ROLE regress_dbowner LOGIN; + GRANT pg_signal_backend TO regress_dbowner; + ALTER DATABASE regress_db2 OWNER TO regress_dbowner; +}); + +# Hold the parallel workers while they are attached to their leader. +$node->safe_psql('postgres', + "SELECT injection_points_attach('parallel-vacuum-worker-start', 'wait')"); + +# A table whose autovacuum vacuums indexes in parallel. +$node->safe_psql( + 'regress_db2', qq{ + CREATE TABLE dropdb_force (a int, b int, c int) + WITH (autovacuum_parallel_workers = 2, + autovacuum_vacuum_threshold = 1, + autovacuum_vacuum_scale_factor = 0); + INSERT INTO dropdb_force SELECT g, g, g FROM generate_series(1, 1000) g; + CREATE INDEX ON dropdb_force (a); + CREATE INDEX ON dropdb_force (b); + CREATE INDEX ON dropdb_force (c); + DELETE FROM dropdb_force; +}); + +$node->wait_for_event('parallel worker', 'parallel-vacuum-worker-start'); + +my ($ret, $out, $err) = $node->psql( + 'postgres', + 'DROP DATABASE regress_db2 WITH (FORCE)', + connstr => $node->connstr('postgres') . ' user=regress_dbowner'); + +is($ret, 0, 'DROP DATABASE WITH (FORCE) ends parallel autovacuum workers'); +is($err, '', 'no error from DROP DATABASE WITH (FORCE)'); + +# The command ended the held workers, so there is nothing left to wake up. +$node->safe_psql('postgres', + "SELECT injection_points_detach('parallel-vacuum-worker-start')"); + $node->stop; done_testing(); -- 2.47.3