From 1a84c4a5d89ecc8e13e7c499f1624ed1ea7df8e6 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Thu, 5 Nov 2026 21:28:40 +0530
Subject: [PATCH] Add isolation test for lock-skip statistics and privileges.

A role that has no privileges to VACUUM or ANALYZE a table must not be
able to change that table's statistics.  Skips caused by a lock held by
someone else are currently reported before vacuum_is_permitted_for_relation()
is reached, so such a role can increase lock_skipped_vacuum_count and
lock_skipped_analyze_count, and the corresponding last_lock_skipped_* times,
of a table it cannot touch.

The test locks a table in ACCESS EXCLUSIVE mode in one session, runs
VACUUM (SKIP_LOCKED) and ANALYZE (SKIP_LOCKED) on it from a role without
privileges in another one, and expects both counters to stay at zero.  A
control permutation runs the same statements as the table owner and expects
both counters to be one.

The first permutation fails until the skip is reported only for a caller
that is permitted to process the table.
---
 .../expected/vacuum-skip-locked-perms.out     | 41 +++++++++++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/vacuum-skip-locked-perms.spec       | 45 +++++++++++++++++++
 3 files changed, 87 insertions(+)
 create mode 100644 src/test/isolation/expected/vacuum-skip-locked-perms.out
 create mode 100644 src/test/isolation/specs/vacuum-skip-locked-perms.spec

diff --git a/src/test/isolation/expected/vacuum-skip-locked-perms.out b/src/test/isolation/expected/vacuum-skip-locked-perms.out
new file mode 100644
index 00000000000..a5f88b65da2
--- /dev/null
+++ b/src/test/isolation/expected/vacuum-skip-locked-perms.out
@@ -0,0 +1,41 @@
+Parsed test spec with 2 sessions
+
+starting permutation: lock s2_unpriv s2_vacuum s2_analyze s2_reset commit check_stat
+step lock: 
+	BEGIN;
+	LOCK TABLE vsl_tab IN ACCESS EXCLUSIVE MODE;
+
+step s2_unpriv: SET ROLE regress_vsl_unpriv;
+s2: WARNING:  skipping vacuum of "vsl_tab" --- lock not available
+step s2_vacuum: VACUUM (SKIP_LOCKED) vsl_tab;
+s2: WARNING:  skipping analyze of "vsl_tab" --- lock not available
+step s2_analyze: ANALYZE (SKIP_LOCKED) vsl_tab;
+step s2_reset: RESET ROLE;
+step commit: 
+	COMMIT;
+
+step check_stat: SELECT pg_stat_get_lock_skipped_vacuum_count('vsl_tab'::regclass) AS vac_skips, pg_stat_get_lock_skipped_analyze_count('vsl_tab'::regclass) AS anl_skips;
+vac_skips|anl_skips
+---------+---------
+        0|        0
+(1 row)
+
+
+starting permutation: lock s2_vacuum s2_analyze commit check_stat
+step lock: 
+	BEGIN;
+	LOCK TABLE vsl_tab IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "vsl_tab" --- lock not available
+step s2_vacuum: VACUUM (SKIP_LOCKED) vsl_tab;
+s2: WARNING:  skipping analyze of "vsl_tab" --- lock not available
+step s2_analyze: ANALYZE (SKIP_LOCKED) vsl_tab;
+step commit: 
+	COMMIT;
+
+step check_stat: SELECT pg_stat_get_lock_skipped_vacuum_count('vsl_tab'::regclass) AS vac_skips, pg_stat_get_lock_skipped_analyze_count('vsl_tab'::regclass) AS anl_skips;
+vac_skips|anl_skips
+---------+---------
+        1|        1
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index bf9a2037c70..7c6f4099b28 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -107,6 +107,7 @@ test: timeouts
 test: vacuum-concurrent-drop
 test: vacuum-conflict
 test: vacuum-skip-locked
+test: vacuum-skip-locked-perms
 test: stats
 test: horizons
 test: predicate-hash
diff --git a/src/test/isolation/specs/vacuum-skip-locked-perms.spec b/src/test/isolation/specs/vacuum-skip-locked-perms.spec
new file mode 100644
index 00000000000..366b43c4b89
--- /dev/null
+++ b/src/test/isolation/specs/vacuum-skip-locked-perms.spec
@@ -0,0 +1,45 @@
+# Test that lock-skip statistics are not recorded for a role that lacks the
+# privileges to VACUUM or ANALYZE the table.
+#
+# A role that cannot vacuum or analyze a table must not be able to change that
+# table's statistics, so a skip caused by a lock held by someone else must not
+# be counted when the caller would not have been permitted to process the table
+# anyway.  The second permutation is the control: the same skips are counted
+# for a role that is allowed to process the table.
+
+setup
+{
+	CREATE ROLE regress_vsl_unpriv;
+	CREATE TABLE vsl_tab (a int);
+	ALTER TABLE vsl_tab SET (autovacuum_enabled = false);
+}
+
+teardown
+{
+	DROP TABLE vsl_tab;
+	DROP ROLE regress_vsl_unpriv;
+}
+
+session s1
+step lock
+{
+	BEGIN;
+	LOCK TABLE vsl_tab IN ACCESS EXCLUSIVE MODE;
+}
+step commit
+{
+	COMMIT;
+}
+step check_stat { SELECT pg_stat_get_lock_skipped_vacuum_count('vsl_tab'::regclass) AS vac_skips, pg_stat_get_lock_skipped_analyze_count('vsl_tab'::regclass) AS anl_skips; }
+
+session s2
+step s2_unpriv	{ SET ROLE regress_vsl_unpriv; }
+step s2_reset	{ RESET ROLE; }
+step s2_vacuum	{ VACUUM (SKIP_LOCKED) vsl_tab; }
+step s2_analyze	{ ANALYZE (SKIP_LOCKED) vsl_tab; }
+
+# The role has no privileges on the table: nothing is counted.
+permutation lock s2_unpriv s2_vacuum s2_analyze s2_reset commit check_stat
+
+# Control: the owner (the superuser running the test) is counted.
+permutation lock s2_vacuum s2_analyze commit check_stat
-- 
2.55.0

