Clarification on Role Access Rights to Table Indexes
Hi PostgreSQL Community,
I am currently exploring the behavior of pg_prewarm and encountered an
issue related to role
access rights that I was hoping you could help clarify.
Here is the scenario I observed:
postgres=# CREATE ROLE alpha;
CREATE ROLE
postgres=# GRANT SELECT ON pg_class TO alpha;
GRANT
postgres=# SET ROLE alpha;
SET
postgres=> SELECT pg_prewarm('pg_class');
pg_prewarm
------------
14
(1 row)
postgres=> SELECT pg_prewarm('pg_class_oid_index');
ERROR: permission denied for index pg_class_oid_index
postgres=> RESET ROLE;
RESET
postgres=# GRANT SELECT ON pg_class_oid_index TO alpha;
ERROR: "pg_class_oid_index" is an index
Based on this, I have few questions:
1. Can a role have access rights to a table without having access to its
index?
2. If yes, how can we explicitly grant access to the index?
3. If no, and the role inherently gets access to the index when granted
access to the table, why
does the pg_prewarm call fail [1]https://github.com/postgres/postgres/blob/master/contrib/pg_prewarm/pg_prewarm.c#L108-L110 in the above scenario?
[1]: https://github.com/postgres/postgres/blob/master/contrib/pg_prewarm/pg_prewarm.c#L108-L110
https://github.com/postgres/postgres/blob/master/contrib/pg_prewarm/pg_prewarm.c#L108-L110
Regards,
Ayush Vatsa
SDE AWS
On Monday, February 17, 2025, Ayush Vatsa <ayushvatsa1810@gmail.com> wrote:
postgres=# CREATE ROLE alpha;
CREATE ROLE
postgres=# GRANT SELECT ON pg_class TO alpha;
This is pointless, everyone (i.e. the PUBLIC pseudo-role) can already read
pg_class.
1. Can a role have access rights to a table without having access to its
index?
Roles don’t directly interact with indexes in PostgreSQL so this doesn’t
even make sense. But if you need a yes/no answer, then yes.
3. If no, and the role inherently gets access to the index when granted
access to the table, why
does the pg_prewarm call fail [1] in the above scenario?[1] https://github.com/postgres/postgres/blob/master/contrib
/pg_prewarm/pg_prewarm.c#L108-L110
It fails because AFAICS there is no way for it to work on an index, only
tables.
David J.
Ayush Vatsa <ayushvatsa1810@gmail.com> writes:
postgres=> SELECT pg_prewarm('pg_class_oid_index');
ERROR: permission denied for index pg_class_oid_index
You'd really have to take that up with the author of pg_prewarm.
It's not apparent to me why checking SQL access permissions is
the right mechanism for limiting use of pg_prewarm. It seems
like ownership of the table would be more appropriate, or maybe
access to one of the built-in roles like pg_maintain.
1. Can a role have access rights to a table without having access to its
index?
Indexes do not have access rights of their own, which is why
access rights are a poor gating mechanism for something that
needs to be applicable to indexes. Ownership could work,
because we make indexes inherit their table's ownership.
regards, tom lane
This is pointless, everyone (i.e. the PUBLIC pseudo-role) can already
read pg_class.
True, Just checked that.
It fails because AFAICS there is no way for it to work on an index, only
tables.
pg_prewarm extension works on index if we have right (SELECT) privileges
postgres=# CREATE TABLE x(id INT);
CREATE TABLE
postgres=# CREATE INDEX idx ON x(id);
CREATE INDEX
postgres=# INSERT INTO x SELECT * FROM generate_series(1,10000);
INSERT 0 10000
postgres=# SELECT pg_prewarm('x');
pg_prewarm
------------
45
(1 row)
postgres=# SELECT pg_prewarm('idx');
pg_prewarm
------------
30
(1 row)
It seems like ownership of the table would be more appropriate, or maybe
access to one of the built-in roles like pg_maintain.
True, adding Robert Haas (author) to this thread for his opinion.
Regards,
Ayush Vatsa
SDE AWS
On Monday, February 17, 2025, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Ayush Vatsa <ayushvatsa1810@gmail.com> writes:
postgres=> SELECT pg_prewarm('pg_class_oid_index');
ERROR: permission denied for index pg_class_oid_indexYou'd really have to take that up with the author of pg_prewarm.
This is our contrib module so this seems like the expected place to ask
such a question. It’s neither a bug nor a topic for -hackers. FTR, Robert
Haas is the author from 2013. Not sure he monitors -general though.
David J.
"David G. Johnston" <david.g.johnston@gmail.com> writes:
On Monday, February 17, 2025, Tom Lane <tgl@sss.pgh.pa.us> wrote:
You'd really have to take that up with the author of pg_prewarm.
This is our contrib module so this seems like the expected place to ask
such a question. It’s neither a bug nor a topic for -hackers. FTR, Robert
Haas is the author from 2013. Not sure he monitors -general though.
Ah, you are right, I was thinking it was a third-party extension.
If we're talking about changing the behavior of a contrib module,
I think -hackers would be the appropriate location for that.
And it does seem like this deserves a fresh look. As it stands,
a superuser can prewarm an index (because she bypasses all
privilege checks including this one), but nobody else can.
Seems weird.
regards, tom lane
As it stands, a superuser can prewarm an index (because she bypasses all
privilege checks including this one), but nobody else can.
That's not fully true. Any role can prewarm an index if the role has the
correct privileges.
postgres=# GRANT CREATE ON SCHEMA PUBLIC TO alpha;
GRANT
postgres=# SET ROLE alpha;
SET
postgres=> CREATE TABLE tab(id INT);
CREATE TABLE
postgres=> CREATE INDEX tab_idx ON tab(id);
CREATE INDEX
postgres=> SELECT pg_prewarm('tab_idx');
pg_prewarm
------------
1
(1 row)
Don't know what stopped it from prewarming the catalog table index.
Maybe it's checking who is the owner of the table. Although in code
it's just checking the ACL_SELECT [1]https://github.com/postgres/postgres/blob/master/contrib/pg_prewarm/pg_prewarm.c#L108-L110. I will be debugging more here
and maybe create a patch for the same.
postgres=# RESET ROLE;
RESET
postgres=# CREATE TABLE superuser_tab(id INT);
CREATE TABLE
postgres=# CREATE INDEX idx_superuser_tab ON superuser_tab(id);
CREATE INDEX
postgres=# GRANT SELECT ON superuser_tab TO alpha;
GRANT
postgres=# SET ROLE alpha;
SET
postgres=> SELECT pg_prewarm('superuser_tab');
pg_prewarm
------------
0
(1 row)
postgres=> SELECT pg_prewarm('idx_superuser_tab');
ERROR: permission denied for index idx_superuser_tab
postgres=> RESET ROLE;
RESET
postgres=# ALTER TABLE superuser_tab OWNER TO alpha;
ALTER TABLE
postgres=# SET ROLE alpha;
SET
postgres=> SELECT pg_prewarm('idx_superuser_tab');
pg_prewarm
------------
1
(1 row)
But I agree we should just check the table privileges even in the case of
indexes to decide whether to prewarm or not, as
*indexes don't have any privilegesof their own.*
I think -hackers would be the appropriate location for that.
I am shifting this to -hackers mailing list instead of general.
[1]: https://github.com/postgres/postgres/blob/master/contrib/pg_prewarm/pg_prewarm.c#L108-L110
https://github.com/postgres/postgres/blob/master/contrib/pg_prewarm/pg_prewarm.c#L108-L110
Regards,
Ayush Vatsa
SDE AWS
Ayush Vatsa <ayushvatsa1810@gmail.com> writes:
As it stands, a superuser can prewarm an index (because she bypasses all
privilege checks including this one), but nobody else can.
That's not fully true. Any role can prewarm an index if the role has the
correct privileges.
Ah, right. An index will have null pg_class.relacl, which'll be
interpreted as "owner has all rights", so it will work for the
table owner too. Likely this explains the lack of prior complaints.
It's still a poor design IMO.
regards, tom lane
On Mon, 2025-02-17 at 23:31 +0530, Ayush Vatsa wrote:
postgres=> SELECT pg_prewarm('pg_class_oid_index');
ERROR: permission denied for index pg_class_oid_index
postgres=> RESET ROLE;
RESETpostgres=# GRANT SELECT ON pg_class_oid_index TO alpha;
ERROR: "pg_class_oid_index" is an index
Based on this, I have few questions:
1. Can a role have access rights to a table without having access to its index?
2. If yes, how can we explicitly grant access to the index?
3. If no, and the role inherently gets access to the index when granted access to the table, why
does the pg_prewarm call fail [1] in the above scenario?
I have seen a complaint about this bug before:
https://dba.stackexchange.com/a/344603/176905
Yours,
Laurenz Albe
--
*E-Mail Disclaimer*
Der Inhalt dieser E-Mail ist ausschliesslich fuer den
bezeichneten Adressaten bestimmt. Wenn Sie nicht der vorgesehene Adressat
dieser E-Mail oder dessen Vertreter sein sollten, so beachten Sie bitte,
dass jede Form der Kenntnisnahme, Veroeffentlichung, Vervielfaeltigung oder
Weitergabe des Inhalts dieser E-Mail unzulaessig ist. Wir bitten Sie, sich
in diesem Fall mit dem Absender der E-Mail in Verbindung zu setzen.
*CONFIDENTIALITY NOTICE & DISCLAIMER
*This message and any attachment are
confidential and may be privileged or otherwise protected from disclosure
and solely for the use of the person(s) or entity to whom it is intended.
If you have received this message in error and are not the intended
recipient, please notify the sender immediately and delete this message and
any attachment from your system. If you are not the intended recipient, be
advised that any use of this message is prohibited and may be unlawful, and
you must not copy this message or attachment or disclose the contents to
any other person.
On Mon, Feb 17, 2025 at 2:57 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Ayush Vatsa <ayushvatsa1810@gmail.com> writes:
As it stands, a superuser can prewarm an index (because she bypasses all
privilege checks including this one), but nobody else can.That's not fully true. Any role can prewarm an index if the role has the
correct privileges.Ah, right. An index will have null pg_class.relacl, which'll be
interpreted as "owner has all rights", so it will work for the
table owner too. Likely this explains the lack of prior complaints.
It's still a poor design IMO.
I'm not sure if I'd call that a "design". Sounds like I just made a
mistake here.
--
Robert Haas
EDB: http://www.enterprisedb.com
I'm not sure if I'd call that a "design". Sounds like I just made a
mistake here.
Thanks Robert for confirming, let me submit a patch to fix the same.
Regards
Ayush Vatsa
ADE AWS
Ayush Vatsa <ayushvatsa1810@gmail.com> writes:
Thanks Robert for confirming, let me submit a patch to fix the same.
Well, the first thing you need is consensus on what the behavior
should be instead.
I have a very vague recollection that we concluded that SELECT
privilege was a reasonable check because if you have that you
could manually prewarm by reading the table. That would lead
to the conclusion that the minimal fix is to look at the owning
table's privileges instead of the index's own privileges.
Or we could switch to using ownership, which'd keep the code
simple but some users might complain it's too restrictive.
While I mentioned built-in roles earlier, I now think those mostly
carry more privilege than should be required here, given the analogy
to SELECT.
regards, tom lane
On Mon, Feb 17, 2025 at 3:02 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Ayush Vatsa <ayushvatsa1810@gmail.com> writes:
Thanks Robert for confirming, let me submit a patch to fix the same.
Well, the first thing you need is consensus on what the behavior
should be instead.I have a very vague recollection that we concluded that SELECT
privilege was a reasonable check because if you have that you
could manually prewarm by reading the table. That would lead
to the conclusion that the minimal fix is to look at the owning
table's privileges instead of the index's own privileges.
I feel like if you can blow up the cache by loading an entire table into
memory with just select privilege on the table we should be ok with
allowing the same person to name an index on the same table and load it
into the cache too.
David J.
On Mon, Feb 17, 2025 at 5:18 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:
I have a very vague recollection that we concluded that SELECT
privilege was a reasonable check because if you have that you
could manually prewarm by reading the table. That would lead
to the conclusion that the minimal fix is to look at the owning
table's privileges instead of the index's own privileges.I feel like if you can blow up the cache by loading an entire table into memory with just select privilege on the table we should be ok with allowing the same person to name an index on the same table and load it into the cache too.
+1.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
On Mon, Feb 17, 2025 at 5:18 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:I have a very vague recollection that we concluded that SELECT
privilege was a reasonable check because if you have that you
could manually prewarm by reading the table. That would lead
to the conclusion that the minimal fix is to look at the owning
table's privileges instead of the index's own privileges.
I feel like if you can blow up the cache by loading an entire table into memory with just select privilege on the table we should be ok with allowing the same person to name an index on the same table and load it into the cache too.
+1.
Is that a +1 for the specific design of "check SELECT on the index's
table", or just a +1 for changing something here?
regards, tom lane
On Tue, Feb 18, 2025 at 11:02 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Is that a +1 for the specific design of "check SELECT on the index's
table", or just a +1 for changing something here?
That is a +1 for the specific design of "check SELECT on the index's
table". I don't want to be closed-minded: if you have some strong
reason for believing that's the wrong thing to do, I'm all ears.
However, I'm presently of the view that it is exactly the right thing
to do, to the point where I don't currently understand why there's
anything to think about here.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
That is a +1 for the specific design of "check SELECT on the index's
table". I don't want to be closed-minded: if you have some strong
reason for believing that's the wrong thing to do, I'm all ears.
However, I'm presently of the view that it is exactly the right thing
to do, to the point where I don't currently understand why there's
anything to think about here.
I have no objection to it, but I wasn't as entirely convinced
as you are that it's the only plausible answer.
One specific thing I'm slightly worried about is that a naive
implementation would probably cause this function to lock the
table after the index, risking deadlock against queries that
take the locks in the more conventional order. I don't recall
what if anything we've done about that in other places
(-ENOCAFFEINE).
regards, tom lane
On Tue, Feb 18, 2025 at 11:30 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I have no objection to it, but I wasn't as entirely convinced
as you are that it's the only plausible answer.
Hmm, OK.
One specific thing I'm slightly worried about is that a naive
implementation would probably cause this function to lock the
table after the index, risking deadlock against queries that
take the locks in the more conventional order. I don't recall
what if anything we've done about that in other places
(-ENOCAFFEINE).
Yeah, that seems like a good thing to worry about from an
implementation point of view but it doesn't seem like a reason to
question the basic design choice. In general, if you can use a table,
you also get to use its indexes, so that interpretation seems natural
to me here, also. Now, if somebody finds a problem with requiring only
SELECT permission, I could see changing the requirements for both
tables and indexes, but I find it harder to imagine that we'd want
those things to work differently from each other. Of course I'm
willing to be convinced that there's a good reason for them to be
different; I just can't currently imagine what it might be.
--
Robert Haas
EDB: http://www.enterprisedb.com
Hello Everyone,
It seems there's a general consensus that we should maintain a
original design to support pg_prewarm, with a minor adjustment:
when querying indexes, we should verify the privileges of the parent table.
I’ve attached a patch for this, which includes some test cases as well.
Let me know if it needs any changes.
Regards,
Ayush Vatsa
SDE AWS
Attachments:
v1-0001-Improve-ACL-checks-in-pg_prewarm-for-indexes.patchapplication/octet-stream; name=v1-0001-Improve-ACL-checks-in-pg_prewarm-for-indexes.patchDownload
From 8f8606c007fdb58403792d36a62c6739341b0549 Mon Sep 17 00:00:00 2001
From: Ayush Vatsa <ayuvatsa@amazon.com>
Date: Mon, 17 Feb 2025 20:44:56 +0000
Subject: [PATCH v1] Improve ACL checks in pg_prewarm for indexes
When pg_prewarm is called on an index, perform the ACL check on its
underlying table instead of the index itself. Indexes do not have
independent access rights and depend on their associated table for
access control.
---
contrib/pg_prewarm/pg_prewarm.c | 29 ++++++++++++++++++++++++---
contrib/pg_prewarm/t/001_basic.pl | 33 ++++++++++++++++++++++++++++++-
2 files changed, 58 insertions(+), 4 deletions(-)
diff --git a/contrib/pg_prewarm/pg_prewarm.c b/contrib/pg_prewarm/pg_prewarm.c
index a2f0ac4af0..b2846cee2e 100644
--- a/contrib/pg_prewarm/pg_prewarm.c
+++ b/contrib/pg_prewarm/pg_prewarm.c
@@ -16,6 +16,7 @@
#include <unistd.h>
#include "access/relation.h"
+#include "catalog/index.h"
#include "fmgr.h"
#include "miscadmin.h"
#include "storage/bufmgr.h"
@@ -55,6 +56,7 @@ Datum
pg_prewarm(PG_FUNCTION_ARGS)
{
Oid relOid;
+ Oid tableOid;
text *forkName;
text *type;
int64 first_block;
@@ -105,9 +107,30 @@ pg_prewarm(PG_FUNCTION_ARGS)
/* Open relation and check privileges. */
rel = relation_open(relOid, AccessShareLock);
- aclresult = pg_class_aclcheck(relOid, GetUserId(), ACL_SELECT);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind), get_rel_name(relOid));
+
+ /**
+ * Check access permissions for pg_prewarm. If the relation is an index,
+ * perform the ACL check on its underlying table since indexes do not have
+ * their own access rights.
+ */
+ if (rel->rd_rel->relkind == RELKIND_INDEX)
+ {
+ /*
+ * If it's an index, get the table OID and perform ACL check on the
+ * table.
+ */
+ tableOid = IndexGetRelation(relOid, false);
+ aclresult = pg_class_aclcheck(tableOid, GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, get_relkind_objtype(RELKIND_RELATION), get_rel_name(tableOid));
+ }
+ else
+ {
+ /* If it's not an index, perform ACL check on the relation itself. */
+ aclresult = pg_class_aclcheck(relOid, GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind), get_rel_name(relOid));
+ }
/* Check that the fork exists. */
if (!smgrexists(RelationGetSmgr(rel), forkNumber))
diff --git a/contrib/pg_prewarm/t/001_basic.pl b/contrib/pg_prewarm/t/001_basic.pl
index 0a8259d367..651f4ec371 100644
--- a/contrib/pg_prewarm/t/001_basic.pl
+++ b/contrib/pg_prewarm/t/001_basic.pl
@@ -23,7 +23,9 @@ $node->start;
$node->safe_psql("postgres",
"CREATE EXTENSION pg_prewarm;\n"
. "CREATE TABLE test(c1 int);\n"
- . "INSERT INTO test SELECT generate_series(1, 100);");
+ . "INSERT INTO test SELECT generate_series(1, 100);\n"
+ . "CREATE INDEX test_idx ON test(c1);\n"
+ . "CREATE ROLE test_user WITH LOGIN;");
# test read mode
my $result =
@@ -42,6 +44,35 @@ ok( ( $stdout =~ qr/^[1-9][0-9]*$/
or $stderr =~ qr/prefetch is not supported by this build/),
'prefetch mode succeeded');
+# grant SELECT permission on the table to test_user
+$node->safe_psql("postgres", "GRANT SELECT ON test TO test_user;");
+
+# test pg_prewarm on the table/index as test_user (should succeed)
+$result =
+ $node->safe_psql("postgres", "SELECT pg_prewarm('test', 'buffer');", extra_params => [ '--username' => "test_user" ]);
+like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm on table succeeds with SELECT permission on table');
+
+$result =
+ $node->safe_psql("postgres", "SELECT pg_prewarm('test_idx', 'buffer');", extra_params => [ '--username' => "test_user" ]);
+like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm on index succeeds with SELECT permission on table');
+
+# revoke SELECT permission on the table from test_user
+$node->safe_psql("postgres", "REVOKE SELECT ON test FROM test_user;");
+
+# test pg_prewarm on the table/index as test_user (should fail)
+($cmdret, $stdout, $stderr) =
+ $node->psql("postgres", "SELECT pg_prewarm('test', 'buffer');", extra_params => [ '--username' => "test_user" ]);
+ok( $stderr =~ /permission denied for table test/,
+ "error message indicates user doesn't have sufficient privileges for the parent table");
+
+($cmdret, $stdout, $stderr) =
+ $node->psql("postgres", "SELECT pg_prewarm('test_idx', 'buffer');", extra_params => [ '--username' => "test_user" ]);
+ok( $stderr =~ /permission denied for table test/,
+ "error message indicates user doesn't have sufficient privileges for the parent table");
+
+# clean up: drop the test_user role
+$node->safe_psql("postgres", "DROP ROLE test_user;");
+
# test autoprewarm_dump_now()
$result = $node->safe_psql("postgres", "SELECT autoprewarm_dump_now();");
like($result, qr/^[1-9][0-9]*$/, 'autoprewarm_dump_now succeeded');
--
2.47.1
Added the CF entry for the same -
https://commitfest.postgresql.org/patch/5583/
Regards,
Ayush Vatsa
SDE AWS
Show quoted text
On Wed, Feb 19, 2025 at 03:53:48PM +0530, Ayush Vatsa wrote:
It seems there's a general consensus that we should maintain a
original design to support pg_prewarm, with a minor adjustment:
when querying indexes, we should verify the privileges of the parent table.I�ve attached a patch for this, which includes some test cases as well.
Let me know if it needs any changes.
+ tableOid = IndexGetRelation(relOid, false);
+ aclresult = pg_class_aclcheck(tableOid, GetUserId(), ACL_SELECT);
I'm wondering whether setting missing_ok to true is correct here. IIUC we
should have an AccessShareLock on the index, but I don't know if that's
enough protection. The only other similar coding pattern I'm aware of is
RangeVarCallbackForReindexIndex(), which sets missing_ok to false and
attempts to gracefully handle a missing table. Of course, maybe that's
wrong, too.
Perhaps it's all close enough in practice. If we get it wrong, you might
get a slightly less helpful error message when the table is concurrently
dropped, which isn't so bad.
--
nathan
I'm wondering whether setting missing_ok to true is correct here. IIUC we
should have an AccessShareLock on the index, but I don't know if that's
enough protection.
Since we are already opening the relation with rel = relation_open(relOid,
AccessShareLock);,
if relOid does not exist, it will throw an error. If it does exist, we
acquire an AccessShareLock,
preventing it from being dropped.
By the time we reach IndexGetRelation(), we can be confident that relOid
exists and is
protected by the lock. Given this, it makes sense to keep missing_ok = false
here.
Let me know if you agree or if you see any scenario where
missing_ok = true would be preferable—I can update the condition
accordingly.
Thanks!
Ayush Vatsa
SDE AWS
On Sat, Mar 08, 2025 at 08:34:40PM +0530, Ayush Vatsa wrote:
I'm wondering whether setting missing_ok to true is correct here. IIUC we
should have an AccessShareLock on the index, but I don't know if that's
enough protection.Since we are already opening the relation with rel = relation_open(relOid,
AccessShareLock);,
if relOid does not exist, it will throw an error. If it does exist, we
acquire an AccessShareLock,
preventing it from being dropped.By the time we reach IndexGetRelation(), we can be confident that relOid
exists and is
protected by the lock. Given this, it makes sense to keep missing_ok = false
here.Let me know if you agree or if you see any scenario where
missing_ok = true would be preferable-I can update the condition
accordingly.
Right, we will have a lock on the index, but my concern is that we won't
have a lock on its table. I was specifically concerned that a concurrent
DROP TABLE could cause IndexGetRelation() to fail, i.e., emit a gross
"cache lookup failed" error. From a quick test and skim of the relevant
code, I think your patch is fine, though. IndexGetRelation() retrieves the
table OID from pg_index, so the OID should definitely be valid. And IIUC
DROP TABLE first acquires a lock on the table and its dependent objects
(e.g., indexes) before any actual deletions, so AFAICT there's no problem
with using it in pg_class_aclcheck() and get_rel_name(), either.
--
nathan
From a quick test and skim of the relevant
code, I think your patch is fine, though
Thanks for reviewing.
And IIUC
DROP TABLE first acquires a lock on the table and its dependent objects
(e.g., indexes) before any actual deletions, so AFAICT there's no problem
with using it in pg_class_aclcheck() and get_rel_name(), either.
True, I have also verified that from [1]https://github.com/postgres/postgres/blob/master/src/backend/catalog/dependency.c#L398-L430, hence I think we are safe here.
Maybe we can move ahead with the patch if we can see no other concerns.
[1]: https://github.com/postgres/postgres/blob/master/src/backend/catalog/dependency.c#L398-L430
https://github.com/postgres/postgres/blob/master/src/backend/catalog/dependency.c#L398-L430
Thanks,
Ayush Vatsa
SDE AWS
On Sun, Mar 09, 2025 at 03:01:41AM +0530, Ayush Vatsa wrote:
Maybe we can move ahead with the patch if we can see no other concerns.
I think we should allow some time in case others want to review the patch.
I do see a concern upthread about increased deadlock risk [0]/messages/by-id/1246906.1739896202@sss.pgh.pa.us, but your
patch doesn't lock the table, but unless I'm wrong [1]/messages/by-id/Z8yxsm9ZWVkHlPbV@nathan (which is always
possible), it doesn't need to lock it.
Anyway, here is a tidied up patch.
[0]: /messages/by-id/1246906.1739896202@sss.pgh.pa.us
[1]: /messages/by-id/Z8yxsm9ZWVkHlPbV@nathan
--
nathan
Attachments:
v2-0001-pg_prewarm-For-indexes-check-privileges-on-table.patchtext/plain; charset=us-asciiDownload
From 326b4bfbb67485f40d77eaf97ea78bfef49b02f3 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Sat, 8 Mar 2025 15:45:32 -0600
Subject: [PATCH v2 1/1] pg_prewarm: For indexes, check privileges on table.
Author: Ayush Vatsa <ayushvatsa1810@gmail.com>
Discussion: https://postgr.es/m/CACX%2BKaMz2ZoOojh0nQ6QNBYx8Ak1Dkoko%3DD4FSb80BYW%2Bo8CHQ%40mail.gmail.com
Backpatch-through: 13
---
contrib/pg_prewarm/pg_prewarm.c | 13 +++++++++++--
contrib/pg_prewarm/t/001_basic.pl | 29 ++++++++++++++++++++++++++++-
2 files changed, 39 insertions(+), 3 deletions(-)
diff --git a/contrib/pg_prewarm/pg_prewarm.c b/contrib/pg_prewarm/pg_prewarm.c
index a2f0ac4af0c..57bd1527a50 100644
--- a/contrib/pg_prewarm/pg_prewarm.c
+++ b/contrib/pg_prewarm/pg_prewarm.c
@@ -16,6 +16,7 @@
#include <unistd.h>
#include "access/relation.h"
+#include "catalog/index.h"
#include "fmgr.h"
#include "miscadmin.h"
#include "storage/bufmgr.h"
@@ -55,6 +56,7 @@ Datum
pg_prewarm(PG_FUNCTION_ARGS)
{
Oid relOid;
+ Oid permOid;
text *forkName;
text *type;
int64 first_block;
@@ -103,9 +105,16 @@ pg_prewarm(PG_FUNCTION_ARGS)
forkString = text_to_cstring(forkName);
forkNumber = forkname_to_number(forkString);
- /* Open relation and check privileges. */
+ /*
+ * Open relation and check privileges. Indexes don't have their own
+ * privileges, so we check privileges on the table instead in that case.
+ */
rel = relation_open(relOid, AccessShareLock);
- aclresult = pg_class_aclcheck(relOid, GetUserId(), ACL_SELECT);
+ if (rel->rd_rel->relkind == RELKIND_INDEX)
+ permOid = IndexGetRelation(relOid, false);
+ else
+ permOid = relOid;
+ aclresult = pg_class_aclcheck(permOid, GetUserId(), ACL_SELECT);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind), get_rel_name(relOid));
diff --git a/contrib/pg_prewarm/t/001_basic.pl b/contrib/pg_prewarm/t/001_basic.pl
index 0a8259d3678..a77ab67d29e 100644
--- a/contrib/pg_prewarm/t/001_basic.pl
+++ b/contrib/pg_prewarm/t/001_basic.pl
@@ -23,7 +23,9 @@ $node->start;
$node->safe_psql("postgres",
"CREATE EXTENSION pg_prewarm;\n"
. "CREATE TABLE test(c1 int);\n"
- . "INSERT INTO test SELECT generate_series(1, 100);");
+ . "INSERT INTO test SELECT generate_series(1, 100);\n"
+ . "CREATE INDEX test_idx ON test(c1);\n"
+ . "CREATE ROLE test_user LOGIN;");
# test read mode
my $result =
@@ -42,6 +44,31 @@ ok( ( $stdout =~ qr/^[1-9][0-9]*$/
or $stderr =~ qr/prefetch is not supported by this build/),
'prefetch mode succeeded');
+# test_user should be unable to prewarm table/index without privileges
+($cmdret, $stdout, $stderr) =
+ $node->psql(
+ "postgres", "SELECT pg_prewarm('test');",
+ extra_params => [ '--username' => 'test_user' ]);
+ok($stderr =~ /permission denied for table test/, 'pg_prewarm failed as expected');
+($cmdret, $stdout, $stderr) =
+ $node->psql(
+ "postgres", "SELECT pg_prewarm('test_idx');",
+ extra_params => [ '--username' => 'test_user' ]);
+ok($stderr =~ /permission denied for index test_idx/, 'pg_prewarm failed as expected');
+
+# test_user should be able to prewarm table/index with privileges
+$node->safe_psql("postgres", "GRANT SELECT ON test TO test_user;");
+$result =
+ $node->safe_psql(
+ "postgres", "SELECT pg_prewarm('test');",
+ extra_params => [ '--username' => 'test_user' ]);
+like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm succeeded as expected');
+$result =
+ $node->safe_psql(
+ "postgres", "SELECT pg_prewarm('test_idx');",
+ extra_params => [ '--username' => 'test_user' ]);
+like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm succeeded as expected');
+
# test autoprewarm_dump_now()
$result = $node->safe_psql("postgres", "SELECT autoprewarm_dump_now();");
like($result, qr/^[1-9][0-9]*$/, 'autoprewarm_dump_now succeeded');
--
2.39.5 (Apple Git-154)
Nathan Bossart <nathandbossart@gmail.com> writes:
I do see a concern upthread about increased deadlock risk [0], but your
patch doesn't lock the table, but unless I'm wrong [1] (which is always
possible), it doesn't need to lock it.
It bothers me a bit that this proposes to do something as complicated
as pg_class_aclcheck on a table we have no lock on. As you say, the
lock we hold on the index would prevent DROP TABLE, but that doesn't
mean we won't have any issues with other DDL on the table. Still,
taking a lock would be bad because of the deadlock hazard, and I
think the potential for conflicts with concurrent DDL is nonzero in
a lot of other places. So I don't have any concrete reason to object.
ReindexIndex() faces this same problem and solves it with some
very complex code that manages to get the table's lock first.
But I see that it's also doing pg_class_aclcheck on a table
it hasn't locked yet, so I don't think that adopting its approach
would do anything useful for us here.
regards, tom lane
On Sat, Mar 08, 2025 at 05:17:40PM -0500, Tom Lane wrote:
It bothers me a bit that this proposes to do something as complicated
as pg_class_aclcheck on a table we have no lock on. As you say, the
lock we hold on the index would prevent DROP TABLE, but that doesn't
mean we won't have any issues with other DDL on the table. Still,
taking a lock would be bad because of the deadlock hazard, and I
think the potential for conflicts with concurrent DDL is nonzero in
a lot of other places. So I don't have any concrete reason to object.ReindexIndex() faces this same problem and solves it with some
very complex code that manages to get the table's lock first.
But I see that it's also doing pg_class_aclcheck on a table
it hasn't locked yet, so I don't think that adopting its approach
would do anything useful for us here.
I noticed that amcheck's bt_index_check_internal() handles this problem,
and I think that approach could be adapted here:
relkind = get_rel_relkind(relOid);
if (relkind == RELKIND_INDEX || relkind == RELKIND_PARTITIONED_INDEX)
{
permOid = IndexGetRelation(relOid, true);
if (OidIsValid(permOid))
LockRelationOid(permOid, AccessShareLock);
else
fail = true;
}
else
permOid = relOid;
rel = relation_open(relOid, AccessShareLock);
if (fail ||
(permOid != relOid && permOid != IndexGetRelation(relOid, false)))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_TABLE),
errmsg("could not find parent table of index \"%s\"",
RelationGetRelationName(rel))));
aclresult = pg_class_aclcheck(permOid, GetUserId(), ACL_SELECT);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind), get_rel_name(relOid));
if (permOid != relOid)
UnlockRelationOid(permOid, AccessShareLock);
stats_lock_check_privileges() does something similar, but it's not as
cautious about the "heapid != IndexGetRelation(indrelid, false)" race
condition. Maybe RangeVarCallbackForReindexIndex() should be smarter about
this, too. That being said, this is a fair amount of complexity to handle
something that is in theory extremely rare...
--
nathan
Nathan Bossart <nathandbossart@gmail.com> writes:
On Sat, Mar 08, 2025 at 05:17:40PM -0500, Tom Lane wrote:
ReindexIndex() faces this same problem and solves it with some
very complex code that manages to get the table's lock first.
I noticed that amcheck's bt_index_check_internal() handles this problem,
...
stats_lock_check_privileges() does something similar, but it's not as
cautious about the "heapid != IndexGetRelation(indrelid, false)" race
condition.
Egad, we've already got three inconsistent implementations of this
functionality? I think the first step must be to unify them into
a common implementation, if at all possible.
regards, tom lane
On Sun, Mar 09, 2025 at 11:48:05AM -0400, Tom Lane wrote:
Nathan Bossart <nathandbossart@gmail.com> writes:
On Sat, Mar 08, 2025 at 05:17:40PM -0500, Tom Lane wrote:
ReindexIndex() faces this same problem and solves it with some
very complex code that manages to get the table's lock first.I noticed that amcheck's bt_index_check_internal() handles this problem,
...
stats_lock_check_privileges() does something similar, but it's not as
cautious about the "heapid != IndexGetRelation(indrelid, false)" race
condition.Egad, we've already got three inconsistent implementations of this
functionality? I think the first step must be to unify them into
a common implementation, if at all possible.
Agreed. I worry that trying to unify each bespoke implementation into a
single function might result in an unwieldy mess, but I'll give it a
shot...
--
nathan
On Sun, 9 Mar 2025 at 03:27, Nathan Bossart <nathandbossart@gmail.com> wrote:
On Sun, Mar 09, 2025 at 03:01:41AM +0530, Ayush Vatsa wrote:
Maybe we can move ahead with the patch if we can see no other concerns.
I think we should allow some time in case others want to review the patch.
I do see a concern upthread about increased deadlock risk [0], but your
patch doesn't lock the table, but unless I'm wrong [1] (which is always
possible), it doesn't need to lock it.Anyway, here is a tidied up patch.
I noticed that Tom Lane's comment from [1]/messages/by-id/279947.1741535285@sss.pgh.pa.us is not addressed. I'm
changing the commitfest entry status to Waiting on Author, Please
address them and update the status to Needs Review.
[1]: /messages/by-id/279947.1741535285@sss.pgh.pa.us
Regards,
Vignesh
On Mon, Mar 10, 2025 at 10:15:19AM -0500, Nathan Bossart wrote:
On Sun, Mar 09, 2025 at 11:48:05AM -0400, Tom Lane wrote:
Nathan Bossart <nathandbossart@gmail.com> writes:
On Sat, Mar 08, 2025 at 05:17:40PM -0500, Tom Lane wrote:
ReindexIndex() faces this same problem and solves it with some
very complex code that manages to get the table's lock first.I noticed that amcheck's bt_index_check_internal() handles this problem,
...
stats_lock_check_privileges() does something similar, but it's not as
cautious about the "heapid != IndexGetRelation(indrelid, false)" race
condition.Egad, we've already got three inconsistent implementations of this
functionality? I think the first step must be to unify them into
a common implementation, if at all possible.Agreed. I worry that trying to unify each bespoke implementation into a
single function might result in an unwieldy mess, but I'll give it a
shot...
I tried to unify these, but each one seems to be just different enough to
make it not worth the trouble. Instead, I took a look at each
implementation:
* amcheck's amcheck_lock_relation_and_check() seems correct to me.
* stats_lock_check_privileges() appears to be missing the second
IndexGetRelation() check after locking the table and index, so I added
that in 0001. Since this code is new to v18, I proposed to back-patch 0001
there.
* RangeVarCallbackForReindexIndex() was checking privileges on the table
before locking it, so I reversed it in 0002. Interestingly, this caused
test errors because LockRelationOid() checks for invalidation messages, so
the pg_class_aclcheck() call started failing with unhelpful errors due to
concurrently dropped relations. To deal with that, I switched to
pg_class_aclcheck_ext() so that we can handle missing relations.
Furthermore, I noticed that this callback seems to assume that as long as
the index does not change between calls, its table won't, either. That's
probably always true in practice, but even if it's completely true, I see
no reason to rely on it. So, I simplified the code to unconditionally
unlock any previously-locked table and to lock whatever IndexGetRelation()
returns. This could probably be back-patched, but in the absence of any
reports or any reproducible bugs, I don't think we should.
* 0003 fixes pg_prewarm's privilege checks by following a similar pattern.
This probably ought to get back-patched to all supported versions.
--
nathan
Attachments:
v3-0001-fix-priv-checks-in-stats-code.patchtext/plain; charset=us-asciiDownload
From 0e70810f8ef47a0e4e22a70a5c9a2a7776611950 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 24 Sep 2025 09:24:28 -0500
Subject: [PATCH v3 1/3] fix priv checks in stats code
---
src/backend/statistics/stat_utils.c | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index ef7e5168bed..8b8203a58e3 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -189,6 +189,17 @@ stats_lock_check_privileges(Oid reloid)
Assert(index->rd_index && index->rd_index->indrelid == table_oid);
+ /*
+ * Since we did the IndexGetRelation() call above without any lock,
+ * it's barely possible that a race against an index drop/recreation
+ * could have netted us the wrong table.
+ */
+ if (table_oid != IndexGetRelation(index_oid, true))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_TABLE),
+ errmsg("could not find parent table of index \"%s\"",
+ RelationGetRelationName(index))));
+
/* retain lock on index */
relation_close(index, NoLock);
}
--
2.39.5 (Apple Git-154)
v3-0002-fix-priv-checks-in-index-code.patchtext/plain; charset=us-asciiDownload
From 28fa582f8372a6d73adb79f4d28fe997a08a23e0 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 24 Sep 2025 09:24:36 -0500
Subject: [PATCH v3 2/3] fix priv checks in index code
---
src/backend/commands/indexcmds.c | 51 +++++++++++++++-----------------
1 file changed, 24 insertions(+), 27 deletions(-)
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index ca2bde62e82..e4d05b5f8e6 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -2976,6 +2976,8 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation,
struct ReindexIndexCallbackState *state = arg;
LOCKMODE table_lockmode;
Oid table_oid;
+ AclResult aclresult;
+ bool is_missing = false;
/*
* Lock level here should match table lock in reindex_index() for
@@ -2985,12 +2987,8 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation,
table_lockmode = (state->params.options & REINDEXOPT_CONCURRENTLY) != 0 ?
ShareUpdateExclusiveLock : ShareLock;
- /*
- * If we previously locked some other index's heap, and the name we're
- * looking up no longer refers to that relation, release the now-useless
- * lock.
- */
- if (relId != oldRelId && OidIsValid(oldRelId))
+ /* Unlock any previously locked heap. */
+ if (OidIsValid(state->locked_table_oid))
{
UnlockRelationOid(state->locked_table_oid, table_lockmode);
state->locked_table_oid = InvalidOid;
@@ -3014,30 +3012,29 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not an index", relation->relname)));
- /* Check permissions */
+ /*
+ * If the OID isn't valid, it means the index was concurrently dropped,
+ * which is not a problem for us; just return normally.
+ */
table_oid = IndexGetRelation(relId, true);
- if (OidIsValid(table_oid))
- {
- AclResult aclresult;
-
- aclresult = pg_class_aclcheck(table_oid, GetUserId(), ACL_MAINTAIN);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_INDEX, relation->relname);
- }
+ if (!OidIsValid(table_oid))
+ return;
/* Lock heap before index to avoid deadlock. */
- if (relId != oldRelId)
- {
- /*
- * If the OID isn't valid, it means the index was concurrently
- * dropped, which is not a problem for us; just return normally.
- */
- if (OidIsValid(table_oid))
- {
- LockRelationOid(table_oid, table_lockmode);
- state->locked_table_oid = table_oid;
- }
- }
+ LockRelationOid(table_oid, table_lockmode);
+ state->locked_table_oid = table_oid;
+
+ /*
+ * Check permissions. Since LockRelationOid() checks for invalidation
+ * messages, the table might go missing here, too. As before, this isn't
+ * our problem; just return normally.
+ */
+ aclresult = pg_class_aclcheck_ext(table_oid, GetUserId(),
+ ACL_MAINTAIN, &is_missing);
+ if (is_missing)
+ return;
+ else if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_INDEX, relation->relname);
}
/*
--
2.39.5 (Apple Git-154)
v3-0003-fix-priv-checks-in-pg_prewarm.patchtext/plain; charset=us-asciiDownload
From 84159c5fffd04ea20090e3cae5b9eb783820dd06 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 24 Sep 2025 09:47:02 -0500
Subject: [PATCH v3 3/3] fix priv checks in pg_prewarm
---
contrib/pg_prewarm/pg_prewarm.c | 34 +++++++++++++++++++++++++++++--
contrib/pg_prewarm/t/001_basic.pl | 29 +++++++++++++++++++++++++-
2 files changed, 60 insertions(+), 3 deletions(-)
diff --git a/contrib/pg_prewarm/pg_prewarm.c b/contrib/pg_prewarm/pg_prewarm.c
index b968933ea8b..810a291204b 100644
--- a/contrib/pg_prewarm/pg_prewarm.c
+++ b/contrib/pg_prewarm/pg_prewarm.c
@@ -16,9 +16,11 @@
#include <unistd.h>
#include "access/relation.h"
+#include "catalog/index.h"
#include "fmgr.h"
#include "miscadmin.h"
#include "storage/bufmgr.h"
+#include "storage/lmgr.h"
#include "storage/read_stream.h"
#include "storage/smgr.h"
#include "utils/acl.h"
@@ -71,6 +73,8 @@ pg_prewarm(PG_FUNCTION_ARGS)
char *ttype;
PrewarmType ptype;
AclResult aclresult;
+ char relkind;
+ Oid privOid;
/* Basic sanity checking. */
if (PG_ARGISNULL(0))
@@ -107,8 +111,31 @@ pg_prewarm(PG_FUNCTION_ARGS)
forkNumber = forkname_to_number(forkString);
/* Open relation and check privileges. */
+ relkind = get_rel_relkind(relOid);
+ if (relkind == RELKIND_INDEX ||
+ relkind == RELKIND_PARTITIONED_INDEX)
+ {
+ privOid = IndexGetRelation(relOid, false);
+ LockRelationOid(privOid, AccessShareLock);
+ }
+ else
+ privOid = relOid;
+
rel = relation_open(relOid, AccessShareLock);
- aclresult = pg_class_aclcheck(relOid, GetUserId(), ACL_SELECT);
+
+ /*
+ * If we did the IndexGetRelation() call above, it's barely possible that
+ * a race against an index drop/recreation could have netted us the wrong
+ * table.
+ */
+ if (privOid != relOid &&
+ privOid != IndexGetRelation(relOid, true))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_TABLE),
+ errmsg("could not find parent table of index \"%s\"",
+ RelationGetRelationName(rel))));
+
+ aclresult = pg_class_aclcheck(privOid, GetUserId(), ACL_SELECT);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind), get_rel_name(relOid));
@@ -233,8 +260,11 @@ pg_prewarm(PG_FUNCTION_ARGS)
read_stream_end(stream);
}
- /* Close relation, release lock. */
+ /* Close relation, release locks. */
relation_close(rel, AccessShareLock);
+ if (privOid != relOid)
+ UnlockRelationOid(privOid, AccessShareLock);
+
PG_RETURN_INT64(blocks_done);
}
diff --git a/contrib/pg_prewarm/t/001_basic.pl b/contrib/pg_prewarm/t/001_basic.pl
index 0a8259d3678..a77ab67d29e 100644
--- a/contrib/pg_prewarm/t/001_basic.pl
+++ b/contrib/pg_prewarm/t/001_basic.pl
@@ -23,7 +23,9 @@ $node->start;
$node->safe_psql("postgres",
"CREATE EXTENSION pg_prewarm;\n"
. "CREATE TABLE test(c1 int);\n"
- . "INSERT INTO test SELECT generate_series(1, 100);");
+ . "INSERT INTO test SELECT generate_series(1, 100);\n"
+ . "CREATE INDEX test_idx ON test(c1);\n"
+ . "CREATE ROLE test_user LOGIN;");
# test read mode
my $result =
@@ -42,6 +44,31 @@ ok( ( $stdout =~ qr/^[1-9][0-9]*$/
or $stderr =~ qr/prefetch is not supported by this build/),
'prefetch mode succeeded');
+# test_user should be unable to prewarm table/index without privileges
+($cmdret, $stdout, $stderr) =
+ $node->psql(
+ "postgres", "SELECT pg_prewarm('test');",
+ extra_params => [ '--username' => 'test_user' ]);
+ok($stderr =~ /permission denied for table test/, 'pg_prewarm failed as expected');
+($cmdret, $stdout, $stderr) =
+ $node->psql(
+ "postgres", "SELECT pg_prewarm('test_idx');",
+ extra_params => [ '--username' => 'test_user' ]);
+ok($stderr =~ /permission denied for index test_idx/, 'pg_prewarm failed as expected');
+
+# test_user should be able to prewarm table/index with privileges
+$node->safe_psql("postgres", "GRANT SELECT ON test TO test_user;");
+$result =
+ $node->safe_psql(
+ "postgres", "SELECT pg_prewarm('test');",
+ extra_params => [ '--username' => 'test_user' ]);
+like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm succeeded as expected');
+$result =
+ $node->safe_psql(
+ "postgres", "SELECT pg_prewarm('test_idx');",
+ extra_params => [ '--username' => 'test_user' ]);
+like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm succeeded as expected');
+
# test autoprewarm_dump_now()
$result = $node->safe_psql("postgres", "SELECT autoprewarm_dump_now();");
like($result, qr/^[1-9][0-9]*$/, 'autoprewarm_dump_now succeeded');
--
2.39.5 (Apple Git-154)
Nathan Bossart <nathandbossart@gmail.com> writes:
* RangeVarCallbackForReindexIndex() was checking privileges on the table
before locking it, so I reversed it in 0002.
Don't we do that intentionally, to make sure someone can't cause DOS
on a table they have no privileges on?
regards, tom lane
On Wed, Sep 24, 2025 at 12:13:34PM -0400, Tom Lane wrote:
Nathan Bossart <nathandbossart@gmail.com> writes:
* RangeVarCallbackForReindexIndex() was checking privileges on the table
before locking it, so I reversed it in 0002.Don't we do that intentionally, to make sure someone can't cause DOS
on a table they have no privileges on?
Ah, right. I switched it back in v4.
--
nathan
Attachments:
v4-0001-fix-priv-checks-in-stats-code.patchtext/plain; charset=us-asciiDownload
From 9cf6e507808cbd4a5c8b93422881ec078ce66f60 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 24 Sep 2025 09:24:28 -0500
Subject: [PATCH v4 1/3] fix priv checks in stats code
---
src/backend/statistics/stat_utils.c | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index ef7e5168bed..8b8203a58e3 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -189,6 +189,17 @@ stats_lock_check_privileges(Oid reloid)
Assert(index->rd_index && index->rd_index->indrelid == table_oid);
+ /*
+ * Since we did the IndexGetRelation() call above without any lock,
+ * it's barely possible that a race against an index drop/recreation
+ * could have netted us the wrong table.
+ */
+ if (table_oid != IndexGetRelation(index_oid, true))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_TABLE),
+ errmsg("could not find parent table of index \"%s\"",
+ RelationGetRelationName(index))));
+
/* retain lock on index */
relation_close(index, NoLock);
}
--
2.39.5 (Apple Git-154)
v4-0002-fix-priv-checks-in-index-code.patchtext/plain; charset=us-asciiDownload
From 3f6d161c46456b95dfae949ecbe44bd028b6a37d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 24 Sep 2025 09:24:36 -0500
Subject: [PATCH v4 2/3] fix priv checks in index code
---
src/backend/commands/indexcmds.c | 41 ++++++++++++--------------------
1 file changed, 15 insertions(+), 26 deletions(-)
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index ca2bde62e82..2a6e58eb0de 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -2976,6 +2976,7 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation,
struct ReindexIndexCallbackState *state = arg;
LOCKMODE table_lockmode;
Oid table_oid;
+ AclResult aclresult;
/*
* Lock level here should match table lock in reindex_index() for
@@ -2985,12 +2986,8 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation,
table_lockmode = (state->params.options & REINDEXOPT_CONCURRENTLY) != 0 ?
ShareUpdateExclusiveLock : ShareLock;
- /*
- * If we previously locked some other index's heap, and the name we're
- * looking up no longer refers to that relation, release the now-useless
- * lock.
- */
- if (relId != oldRelId && OidIsValid(oldRelId))
+ /* Unlock any previously locked heap. */
+ if (OidIsValid(state->locked_table_oid))
{
UnlockRelationOid(state->locked_table_oid, table_lockmode);
state->locked_table_oid = InvalidOid;
@@ -3014,30 +3011,22 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not an index", relation->relname)));
- /* Check permissions */
+ /*
+ * If the OID isn't valid, it means the index was concurrently dropped,
+ * which is not a problem for us; just return normally.
+ */
table_oid = IndexGetRelation(relId, true);
- if (OidIsValid(table_oid))
- {
- AclResult aclresult;
+ if (!OidIsValid(table_oid))
+ return;
- aclresult = pg_class_aclcheck(table_oid, GetUserId(), ACL_MAINTAIN);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_INDEX, relation->relname);
- }
+ /* Check permissions */
+ aclresult = pg_class_aclcheck(table_oid, GetUserId(), ACL_MAINTAIN);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_INDEX, relation->relname);
/* Lock heap before index to avoid deadlock. */
- if (relId != oldRelId)
- {
- /*
- * If the OID isn't valid, it means the index was concurrently
- * dropped, which is not a problem for us; just return normally.
- */
- if (OidIsValid(table_oid))
- {
- LockRelationOid(table_oid, table_lockmode);
- state->locked_table_oid = table_oid;
- }
- }
+ LockRelationOid(table_oid, table_lockmode);
+ state->locked_table_oid = table_oid;
}
/*
--
2.39.5 (Apple Git-154)
v4-0003-fix-priv-checks-in-pg_prewarm.patchtext/plain; charset=us-asciiDownload
From 2c1da2d3db13603da4b2cded4a2dcf0d86614ca0 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 24 Sep 2025 09:47:02 -0500
Subject: [PATCH v4 3/3] fix priv checks in pg_prewarm
---
contrib/pg_prewarm/pg_prewarm.c | 34 +++++++++++++++++++++++++++++--
contrib/pg_prewarm/t/001_basic.pl | 29 +++++++++++++++++++++++++-
2 files changed, 60 insertions(+), 3 deletions(-)
diff --git a/contrib/pg_prewarm/pg_prewarm.c b/contrib/pg_prewarm/pg_prewarm.c
index b968933ea8b..810a291204b 100644
--- a/contrib/pg_prewarm/pg_prewarm.c
+++ b/contrib/pg_prewarm/pg_prewarm.c
@@ -16,9 +16,11 @@
#include <unistd.h>
#include "access/relation.h"
+#include "catalog/index.h"
#include "fmgr.h"
#include "miscadmin.h"
#include "storage/bufmgr.h"
+#include "storage/lmgr.h"
#include "storage/read_stream.h"
#include "storage/smgr.h"
#include "utils/acl.h"
@@ -71,6 +73,8 @@ pg_prewarm(PG_FUNCTION_ARGS)
char *ttype;
PrewarmType ptype;
AclResult aclresult;
+ char relkind;
+ Oid privOid;
/* Basic sanity checking. */
if (PG_ARGISNULL(0))
@@ -107,8 +111,31 @@ pg_prewarm(PG_FUNCTION_ARGS)
forkNumber = forkname_to_number(forkString);
/* Open relation and check privileges. */
+ relkind = get_rel_relkind(relOid);
+ if (relkind == RELKIND_INDEX ||
+ relkind == RELKIND_PARTITIONED_INDEX)
+ {
+ privOid = IndexGetRelation(relOid, false);
+ LockRelationOid(privOid, AccessShareLock);
+ }
+ else
+ privOid = relOid;
+
rel = relation_open(relOid, AccessShareLock);
- aclresult = pg_class_aclcheck(relOid, GetUserId(), ACL_SELECT);
+
+ /*
+ * If we did the IndexGetRelation() call above, it's barely possible that
+ * a race against an index drop/recreation could have netted us the wrong
+ * table.
+ */
+ if (privOid != relOid &&
+ privOid != IndexGetRelation(relOid, true))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_TABLE),
+ errmsg("could not find parent table of index \"%s\"",
+ RelationGetRelationName(rel))));
+
+ aclresult = pg_class_aclcheck(privOid, GetUserId(), ACL_SELECT);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind), get_rel_name(relOid));
@@ -233,8 +260,11 @@ pg_prewarm(PG_FUNCTION_ARGS)
read_stream_end(stream);
}
- /* Close relation, release lock. */
+ /* Close relation, release locks. */
relation_close(rel, AccessShareLock);
+ if (privOid != relOid)
+ UnlockRelationOid(privOid, AccessShareLock);
+
PG_RETURN_INT64(blocks_done);
}
diff --git a/contrib/pg_prewarm/t/001_basic.pl b/contrib/pg_prewarm/t/001_basic.pl
index 0a8259d3678..a77ab67d29e 100644
--- a/contrib/pg_prewarm/t/001_basic.pl
+++ b/contrib/pg_prewarm/t/001_basic.pl
@@ -23,7 +23,9 @@ $node->start;
$node->safe_psql("postgres",
"CREATE EXTENSION pg_prewarm;\n"
. "CREATE TABLE test(c1 int);\n"
- . "INSERT INTO test SELECT generate_series(1, 100);");
+ . "INSERT INTO test SELECT generate_series(1, 100);\n"
+ . "CREATE INDEX test_idx ON test(c1);\n"
+ . "CREATE ROLE test_user LOGIN;");
# test read mode
my $result =
@@ -42,6 +44,31 @@ ok( ( $stdout =~ qr/^[1-9][0-9]*$/
or $stderr =~ qr/prefetch is not supported by this build/),
'prefetch mode succeeded');
+# test_user should be unable to prewarm table/index without privileges
+($cmdret, $stdout, $stderr) =
+ $node->psql(
+ "postgres", "SELECT pg_prewarm('test');",
+ extra_params => [ '--username' => 'test_user' ]);
+ok($stderr =~ /permission denied for table test/, 'pg_prewarm failed as expected');
+($cmdret, $stdout, $stderr) =
+ $node->psql(
+ "postgres", "SELECT pg_prewarm('test_idx');",
+ extra_params => [ '--username' => 'test_user' ]);
+ok($stderr =~ /permission denied for index test_idx/, 'pg_prewarm failed as expected');
+
+# test_user should be able to prewarm table/index with privileges
+$node->safe_psql("postgres", "GRANT SELECT ON test TO test_user;");
+$result =
+ $node->safe_psql(
+ "postgres", "SELECT pg_prewarm('test');",
+ extra_params => [ '--username' => 'test_user' ]);
+like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm succeeded as expected');
+$result =
+ $node->safe_psql(
+ "postgres", "SELECT pg_prewarm('test_idx');",
+ extra_params => [ '--username' => 'test_user' ]);
+like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm succeeded as expected');
+
# test autoprewarm_dump_now()
$result = $node->safe_psql("postgres", "SELECT autoprewarm_dump_now();");
like($result, qr/^[1-9][0-9]*$/, 'autoprewarm_dump_now succeeded');
--
2.39.5 (Apple Git-154)
On Wed, Sep 24, 2025 at 11:52:09AM -0500, Nathan Bossart wrote:
Ah, right. I switched it back in v4.
Unless more feedback materializes, I'm planning to commit these sometime
next week.
--
nathan
On Wed, 2025-09-24 at 11:52 -0500, Nathan Bossart wrote:
On Wed, Sep 24, 2025 at 12:13:34PM -0400, Tom Lane wrote:
Nathan Bossart <nathandbossart@gmail.com> writes:
* RangeVarCallbackForReindexIndex() was checking privileges on
the table
before locking it, so I reversed it in 0002.Don't we do that intentionally, to make sure someone can't cause
DOS
on a table they have no privileges on?Ah, right. I switched it back in v4.
v4-0001 looks good to me.
Just to make sure I understand: the actual problem would only happen
with OID wraparound, right?
Regards,
Jeff Davis
On Wed, 2025-10-08 at 20:06 -0700, Jeff Davis wrote:
On Wed, 2025-09-24 at 11:52 -0500, Nathan Bossart wrote:
On Wed, Sep 24, 2025 at 12:13:34PM -0400, Tom Lane wrote:
Nathan Bossart <nathandbossart@gmail.com> writes:
* RangeVarCallbackForReindexIndex() was checking privileges on
the table
before locking it, so I reversed it in 0002.Don't we do that intentionally, to make sure someone can't cause
DOS
on a table they have no privileges on?Ah, right. I switched it back in v4.
v4-0001 looks good to me.
Actually, now I'm unsure. v4-0001 is taking a lock on the table before
checking privileges, whereas v4-0002 is going to some effort to avoid
that. Is that because the latter is taking a ShareLock?
Regards,
Jeff Davis
On Wed, Oct 08, 2025 at 08:28:01PM -0700, Jeff Davis wrote:
Actually, now I'm unsure. v4-0001 is taking a lock on the table before
checking privileges, whereas v4-0002 is going to some effort to avoid
that. Is that because the latter is taking a ShareLock?
I was confused by this, too. We seem to go to great lengths to avoid
taking a lock before checking permissions in RangeVarGetRelidExtended(),
but in pg_prewarm() and this stats code, we are taking the lock first.
pg_prewarm() can't use RangeVarGetRelid because you give it the OID, but
I'm not seeing why stat_utils.c can't use it. We should probably fix this.
I wouldn't be surprised if there are other examples.
--
nathan
On Thu, Oct 09, 2025 at 10:39:32AM -0500, Nathan Bossart wrote:
On Wed, Oct 08, 2025 at 08:28:01PM -0700, Jeff Davis wrote:
Actually, now I'm unsure. v4-0001 is taking a lock on the table before
checking privileges, whereas v4-0002 is going to some effort to avoid
that. Is that because the latter is taking a ShareLock?I was confused by this, too. We seem to go to great lengths to avoid
taking a lock before checking permissions in RangeVarGetRelidExtended(),
but in pg_prewarm() and this stats code, we are taking the lock first.
pg_prewarm() can't use RangeVarGetRelid because you give it the OID, but
I'm not seeing why stat_utils.c can't use it. We should probably fix this.
I wouldn't be surprised if there are other examples.
I spent some time trying to change pg_prewarm() to check permissions before
locking and came up with the attached. There are certainly issues with the
patch, but this at least demonstrates the complexity required. I'm tempted
to say that this is more trouble than it's worth, but it does feel a little
weird to leave it as-is.
There's a similar pattern in get_rel_from_relname() in dblink.c, which also
seems to only be used with an AccessShareLock (like pg_prewarm). My best
guess from reading lots of code, commit messages, and old e-mails in the
archives is that the original check-privileges-before-locking work was
never completed.
I'm currently leaning towards continuing with v4 of the patch set. 0001
and 0003 are a little weird in that a concurrent change could lead to a
"could not find parent table" ERROR, but IIUC that is an extremely remote
possibility.
--
nathan
Attachments:
0001-pg_prewarm-privilege-test.patchtext/plain; charset=us-asciiDownload
From df9b604c0d8c3c90df2026406e6ec0302f22454c Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Thu, 9 Oct 2025 15:49:18 -0500
Subject: [PATCH 1/1] pg_prewarm privilege test
---
contrib/pg_prewarm/pg_prewarm.c | 83 +++++++++++++++++++++++++++++--
contrib/pg_prewarm/t/001_basic.pl | 36 +++++++++++++-
2 files changed, 113 insertions(+), 6 deletions(-)
diff --git a/contrib/pg_prewarm/pg_prewarm.c b/contrib/pg_prewarm/pg_prewarm.c
index b968933ea8b..57b720cfbed 100644
--- a/contrib/pg_prewarm/pg_prewarm.c
+++ b/contrib/pg_prewarm/pg_prewarm.c
@@ -16,13 +16,17 @@
#include <unistd.h>
#include "access/relation.h"
+#include "catalog/index.h"
#include "fmgr.h"
#include "miscadmin.h"
#include "storage/bufmgr.h"
+#include "storage/lmgr.h"
#include "storage/read_stream.h"
+#include "storage/sinval.h"
#include "storage/smgr.h"
#include "utils/acl.h"
#include "utils/builtins.h"
+#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -42,6 +46,74 @@ typedef enum
static PGIOAlignedBlock blockbuffer;
+static void
+check_privs_and_lock_rel(Oid relid, Oid *heaprel)
+{
+ uint64 inval_count;
+ Oid privOid = InvalidOid;
+
+ do
+ {
+ AclResult aclresult;
+ bool is_missing = false;
+ char relkind;
+
+ /*
+ * Remember this value so that, after locking the relation(s), we can
+ * check whether any invalidation messages have been processed that
+ * might require a do-over.
+ */
+ inval_count = SharedInvalidMessageCounter;
+
+ /*
+ * Unlock any previously-locked relations. We could try to hold onto
+ * these through iterations, but it's simpler to just start fresh each
+ * time.
+ */
+ if (OidIsValid(privOid))
+ {
+ if (privOid != relid)
+ UnlockRelationOid(privOid, AccessShareLock);
+ UnlockRelationOid(relid, AccessShareLock);
+ }
+
+ /*
+ * For indexes, privilege checks must happen on the heap relation.
+ */
+ relkind = get_rel_relkind(relid);
+ if (relkind == RELKIND_INDEX || relkind == RELKIND_PARTITIONED_INDEX)
+ privOid = IndexGetRelation(relid, false);
+ else
+ privOid = relid;
+
+ /*
+ * Check privileges. If the relation has gone missing, there's
+ * nothing for us to do. We'll either retry in the next loop
+ * iteration, or the caller will fail when it tries to open the
+ * relation.
+ */
+ aclresult = pg_class_aclcheck_ext(privOid, GetUserId(),
+ ACL_SELECT, &is_missing);
+ if (!is_missing && aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult,
+ get_relkind_objtype(relkind),
+ get_rel_name(relid));
+
+ /*
+ * Lock the relation(s). If relid is an index, make sure we lock its
+ * heap first. Note that we rely on LockRelationOid() to call
+ * AcceptInvalidationMessages().
+ */
+ if (privOid != relid)
+ LockRelationOid(privOid, AccessShareLock);
+ LockRelationOid(relid, AccessShareLock);
+
+ } while (inval_count != SharedInvalidMessageCounter);
+
+ if (privOid != relid)
+ *heaprel = privOid;
+}
+
/*
* pg_prewarm(regclass, mode text, fork text,
* first_block int8, last_block int8)
@@ -70,7 +142,7 @@ pg_prewarm(PG_FUNCTION_ARGS)
char *forkString;
char *ttype;
PrewarmType ptype;
- AclResult aclresult;
+ Oid heaprel = InvalidOid;
/* Basic sanity checking. */
if (PG_ARGISNULL(0))
@@ -107,10 +179,8 @@ pg_prewarm(PG_FUNCTION_ARGS)
forkNumber = forkname_to_number(forkString);
/* Open relation and check privileges. */
- rel = relation_open(relOid, AccessShareLock);
- aclresult = pg_class_aclcheck(relOid, GetUserId(), ACL_SELECT);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind), get_rel_name(relOid));
+ check_privs_and_lock_rel(relOid, &heaprel);
+ rel = relation_open(relOid, NoLock);
/* Check that the relation has storage. */
if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind))
@@ -236,5 +306,8 @@ pg_prewarm(PG_FUNCTION_ARGS)
/* Close relation, release lock. */
relation_close(rel, AccessShareLock);
+ if (OidIsValid(heaprel))
+ UnlockRelationOid(heaprel, AccessShareLock);
+
PG_RETURN_INT64(blocks_done);
}
diff --git a/contrib/pg_prewarm/t/001_basic.pl b/contrib/pg_prewarm/t/001_basic.pl
index 0a8259d3678..5d7010eb44e 100644
--- a/contrib/pg_prewarm/t/001_basic.pl
+++ b/contrib/pg_prewarm/t/001_basic.pl
@@ -23,7 +23,9 @@ $node->start;
$node->safe_psql("postgres",
"CREATE EXTENSION pg_prewarm;\n"
. "CREATE TABLE test(c1 int);\n"
- . "INSERT INTO test SELECT generate_series(1, 100);");
+ . "INSERT INTO test SELECT generate_series(1, 100);\n"
+ . "CREATE INDEX test_idx ON test(c1);\n"
+ . "CREATE ROLE test_user LOGIN;");
# test read mode
my $result =
@@ -42,6 +44,38 @@ ok( ( $stdout =~ qr/^[1-9][0-9]*$/
or $stderr =~ qr/prefetch is not supported by this build/),
'prefetch mode succeeded');
+# test_user should be unable to prewarm table/index without privileges
+($cmdret, $stdout, $stderr) =
+ $node->psql(
+ "postgres", "SELECT pg_prewarm('test');",
+ extra_params => [ '--username' => 'test_user' ]);
+ok($stderr =~ /permission denied for table test/, 'pg_prewarm failed as expected');
+($cmdret, $stdout, $stderr) =
+ $node->psql(
+ "postgres", "SELECT pg_prewarm('test_idx');",
+ extra_params => [ '--username' => 'test_user' ]);
+ok($stderr =~ /permission denied for index test_idx/, 'pg_prewarm failed as expected');
+
+# test_user should be able to prewarm table/index with privileges
+$node->safe_psql("postgres", "GRANT SELECT ON test TO test_user;");
+$result =
+ $node->safe_psql(
+ "postgres", "SELECT pg_prewarm('test');",
+ extra_params => [ '--username' => 'test_user' ]);
+like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm succeeded as expected');
+$result =
+ $node->safe_psql(
+ "postgres", "SELECT pg_prewarm('test_idx');",
+ extra_params => [ '--username' => 'test_user' ]);
+like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm succeeded as expected');
+
+# prewarm fails for nonexistent table
+($cmdret, $stdout, $stderr) =
+ $node->psql(
+ "postgres", "SELECT pg_prewarm(0);",
+ extra_params => [ '--username' => 'test_user' ]);
+ok($stderr =~ /could not open relation with OID 0/, 'pg_prewarm failed as expected');
+
# test autoprewarm_dump_now()
$result = $node->safe_psql("postgres", "SELECT autoprewarm_dump_now();");
like($result, qr/^[1-9][0-9]*$/, 'autoprewarm_dump_now succeeded');
--
2.39.5 (Apple Git-154)
On Thu, Oct 09, 2025 at 04:18:03PM -0500, Nathan Bossart wrote:
There's a similar pattern in get_rel_from_relname() in dblink.c, which also
seems to only be used with an AccessShareLock (like pg_prewarm). My best
guess from reading lots of code, commit messages, and old e-mails in the
archives is that the original check-privileges-before-locking work was
never completed.
I added an 0004 that changes dblink to use RangeVarGetRelidExtended().
I'm currently leaning towards continuing with v4 of the patch set. 0001
and 0003 are a little weird in that a concurrent change could lead to a
"could not find parent table" ERROR, but IIUC that is an extremely remote
possibility.
After sleeping on it, I still think this is the right call. In any case,
I've spent way too much time on this stuff, so I plan to commit the
attached soon.
--
nathan
Attachments:
v5-0001-fix-priv-checks-in-stats-code.patchtext/plain; charset=us-asciiDownload
From 252991d120be8b50c5c3898deee8a723e3c02c02 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 24 Sep 2025 09:24:28 -0500
Subject: [PATCH v5 1/4] fix priv checks in stats code
---
src/backend/statistics/stat_utils.c | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index ef7e5168bed..8b8203a58e3 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -189,6 +189,17 @@ stats_lock_check_privileges(Oid reloid)
Assert(index->rd_index && index->rd_index->indrelid == table_oid);
+ /*
+ * Since we did the IndexGetRelation() call above without any lock,
+ * it's barely possible that a race against an index drop/recreation
+ * could have netted us the wrong table.
+ */
+ if (table_oid != IndexGetRelation(index_oid, true))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_TABLE),
+ errmsg("could not find parent table of index \"%s\"",
+ RelationGetRelationName(index))));
+
/* retain lock on index */
relation_close(index, NoLock);
}
--
2.39.5 (Apple Git-154)
v5-0002-fix-priv-checks-in-index-code.patchtext/plain; charset=us-asciiDownload
From 700ef73e8c373c57ab1b502d04176b4445442fb1 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 24 Sep 2025 09:24:36 -0500
Subject: [PATCH v5 2/4] fix priv checks in index code
---
src/backend/commands/indexcmds.c | 41 ++++++++++++--------------------
1 file changed, 15 insertions(+), 26 deletions(-)
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index ca2bde62e82..2a6e58eb0de 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -2976,6 +2976,7 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation,
struct ReindexIndexCallbackState *state = arg;
LOCKMODE table_lockmode;
Oid table_oid;
+ AclResult aclresult;
/*
* Lock level here should match table lock in reindex_index() for
@@ -2985,12 +2986,8 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation,
table_lockmode = (state->params.options & REINDEXOPT_CONCURRENTLY) != 0 ?
ShareUpdateExclusiveLock : ShareLock;
- /*
- * If we previously locked some other index's heap, and the name we're
- * looking up no longer refers to that relation, release the now-useless
- * lock.
- */
- if (relId != oldRelId && OidIsValid(oldRelId))
+ /* Unlock any previously locked heap. */
+ if (OidIsValid(state->locked_table_oid))
{
UnlockRelationOid(state->locked_table_oid, table_lockmode);
state->locked_table_oid = InvalidOid;
@@ -3014,30 +3011,22 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not an index", relation->relname)));
- /* Check permissions */
+ /*
+ * If the OID isn't valid, it means the index was concurrently dropped,
+ * which is not a problem for us; just return normally.
+ */
table_oid = IndexGetRelation(relId, true);
- if (OidIsValid(table_oid))
- {
- AclResult aclresult;
+ if (!OidIsValid(table_oid))
+ return;
- aclresult = pg_class_aclcheck(table_oid, GetUserId(), ACL_MAINTAIN);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_INDEX, relation->relname);
- }
+ /* Check permissions */
+ aclresult = pg_class_aclcheck(table_oid, GetUserId(), ACL_MAINTAIN);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_INDEX, relation->relname);
/* Lock heap before index to avoid deadlock. */
- if (relId != oldRelId)
- {
- /*
- * If the OID isn't valid, it means the index was concurrently
- * dropped, which is not a problem for us; just return normally.
- */
- if (OidIsValid(table_oid))
- {
- LockRelationOid(table_oid, table_lockmode);
- state->locked_table_oid = table_oid;
- }
- }
+ LockRelationOid(table_oid, table_lockmode);
+ state->locked_table_oid = table_oid;
}
/*
--
2.39.5 (Apple Git-154)
v5-0003-fix-priv-checks-in-pg_prewarm.patchtext/plain; charset=us-asciiDownload
From d197dec65e1a41c3f0a74b256eb7f9a2ed7c00c1 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 24 Sep 2025 09:47:02 -0500
Subject: [PATCH v5 3/4] fix priv checks in pg_prewarm
---
contrib/pg_prewarm/pg_prewarm.c | 34 +++++++++++++++++++++++++++++--
contrib/pg_prewarm/t/001_basic.pl | 29 +++++++++++++++++++++++++-
2 files changed, 60 insertions(+), 3 deletions(-)
diff --git a/contrib/pg_prewarm/pg_prewarm.c b/contrib/pg_prewarm/pg_prewarm.c
index b968933ea8b..810a291204b 100644
--- a/contrib/pg_prewarm/pg_prewarm.c
+++ b/contrib/pg_prewarm/pg_prewarm.c
@@ -16,9 +16,11 @@
#include <unistd.h>
#include "access/relation.h"
+#include "catalog/index.h"
#include "fmgr.h"
#include "miscadmin.h"
#include "storage/bufmgr.h"
+#include "storage/lmgr.h"
#include "storage/read_stream.h"
#include "storage/smgr.h"
#include "utils/acl.h"
@@ -71,6 +73,8 @@ pg_prewarm(PG_FUNCTION_ARGS)
char *ttype;
PrewarmType ptype;
AclResult aclresult;
+ char relkind;
+ Oid privOid;
/* Basic sanity checking. */
if (PG_ARGISNULL(0))
@@ -107,8 +111,31 @@ pg_prewarm(PG_FUNCTION_ARGS)
forkNumber = forkname_to_number(forkString);
/* Open relation and check privileges. */
+ relkind = get_rel_relkind(relOid);
+ if (relkind == RELKIND_INDEX ||
+ relkind == RELKIND_PARTITIONED_INDEX)
+ {
+ privOid = IndexGetRelation(relOid, false);
+ LockRelationOid(privOid, AccessShareLock);
+ }
+ else
+ privOid = relOid;
+
rel = relation_open(relOid, AccessShareLock);
- aclresult = pg_class_aclcheck(relOid, GetUserId(), ACL_SELECT);
+
+ /*
+ * If we did the IndexGetRelation() call above, it's barely possible that
+ * a race against an index drop/recreation could have netted us the wrong
+ * table.
+ */
+ if (privOid != relOid &&
+ privOid != IndexGetRelation(relOid, true))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_TABLE),
+ errmsg("could not find parent table of index \"%s\"",
+ RelationGetRelationName(rel))));
+
+ aclresult = pg_class_aclcheck(privOid, GetUserId(), ACL_SELECT);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind), get_rel_name(relOid));
@@ -233,8 +260,11 @@ pg_prewarm(PG_FUNCTION_ARGS)
read_stream_end(stream);
}
- /* Close relation, release lock. */
+ /* Close relation, release locks. */
relation_close(rel, AccessShareLock);
+ if (privOid != relOid)
+ UnlockRelationOid(privOid, AccessShareLock);
+
PG_RETURN_INT64(blocks_done);
}
diff --git a/contrib/pg_prewarm/t/001_basic.pl b/contrib/pg_prewarm/t/001_basic.pl
index 0a8259d3678..a77ab67d29e 100644
--- a/contrib/pg_prewarm/t/001_basic.pl
+++ b/contrib/pg_prewarm/t/001_basic.pl
@@ -23,7 +23,9 @@ $node->start;
$node->safe_psql("postgres",
"CREATE EXTENSION pg_prewarm;\n"
. "CREATE TABLE test(c1 int);\n"
- . "INSERT INTO test SELECT generate_series(1, 100);");
+ . "INSERT INTO test SELECT generate_series(1, 100);\n"
+ . "CREATE INDEX test_idx ON test(c1);\n"
+ . "CREATE ROLE test_user LOGIN;");
# test read mode
my $result =
@@ -42,6 +44,31 @@ ok( ( $stdout =~ qr/^[1-9][0-9]*$/
or $stderr =~ qr/prefetch is not supported by this build/),
'prefetch mode succeeded');
+# test_user should be unable to prewarm table/index without privileges
+($cmdret, $stdout, $stderr) =
+ $node->psql(
+ "postgres", "SELECT pg_prewarm('test');",
+ extra_params => [ '--username' => 'test_user' ]);
+ok($stderr =~ /permission denied for table test/, 'pg_prewarm failed as expected');
+($cmdret, $stdout, $stderr) =
+ $node->psql(
+ "postgres", "SELECT pg_prewarm('test_idx');",
+ extra_params => [ '--username' => 'test_user' ]);
+ok($stderr =~ /permission denied for index test_idx/, 'pg_prewarm failed as expected');
+
+# test_user should be able to prewarm table/index with privileges
+$node->safe_psql("postgres", "GRANT SELECT ON test TO test_user;");
+$result =
+ $node->safe_psql(
+ "postgres", "SELECT pg_prewarm('test');",
+ extra_params => [ '--username' => 'test_user' ]);
+like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm succeeded as expected');
+$result =
+ $node->safe_psql(
+ "postgres", "SELECT pg_prewarm('test_idx');",
+ extra_params => [ '--username' => 'test_user' ]);
+like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm succeeded as expected');
+
# test autoprewarm_dump_now()
$result = $node->safe_psql("postgres", "SELECT autoprewarm_dump_now();");
like($result, qr/^[1-9][0-9]*$/, 'autoprewarm_dump_now succeeded');
--
2.39.5 (Apple Git-154)
v5-0004-avoid-locking-before-privilege-checks-in-dblink.patchtext/plain; charset=us-asciiDownload
From 8a096e9bf7154df9393f6920579996d7dfe8958e Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 10 Oct 2025 09:37:51 -0500
Subject: [PATCH v5 4/4] avoid locking before privilege checks in dblink
---
contrib/dblink/dblink.c | 29 +++++++++++++++++++----------
1 file changed, 19 insertions(+), 10 deletions(-)
diff --git a/contrib/dblink/dblink.c b/contrib/dblink/dblink.c
index 0cf4c27f2e9..1e7696beb50 100644
--- a/contrib/dblink/dblink.c
+++ b/contrib/dblink/dblink.c
@@ -2460,6 +2460,21 @@ get_tuple_of_interest(Relation rel, int *pkattnums, int pknumatts, char **src_pk
return NULL;
}
+static void
+RangeVarCallbackForDblink(const RangeVar *relation,
+ Oid relId, Oid oldRelId, void *arg)
+{
+ AclResult aclresult;
+
+ if (!OidIsValid(relId))
+ return;
+
+ aclresult = pg_class_aclcheck(relId, GetUserId(), *((AclMode *) arg));
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, get_relkind_objtype(get_rel_relkind(relId)),
+ relation->relname);
+}
+
/*
* Open the relation named by relname_text, acquire specified type of lock,
* verify we have specified permissions.
@@ -2469,19 +2484,13 @@ static Relation
get_rel_from_relname(text *relname_text, LOCKMODE lockmode, AclMode aclmode)
{
RangeVar *relvar;
- Relation rel;
- AclResult aclresult;
+ Oid relid;
relvar = makeRangeVarFromNameList(textToQualifiedNameList(relname_text));
- rel = table_openrv(relvar, lockmode);
-
- aclresult = pg_class_aclcheck(RelationGetRelid(rel), GetUserId(),
- aclmode);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind),
- RelationGetRelationName(rel));
+ relid = RangeVarGetRelidExtended(relvar, lockmode, 0,
+ RangeVarCallbackForDblink, &aclmode);
- return rel;
+ return table_open(relid, NoLock);
}
/*
--
2.39.5 (Apple Git-154)
On Fri, 2025-10-10 at 11:26 -0500, Nathan Bossart wrote:
On Thu, Oct 09, 2025 at 04:18:03PM -0500, Nathan Bossart wrote:
There's a similar pattern in get_rel_from_relname() in dblink.c,
which also
seems to only be used with an AccessShareLock (like pg_prewarm).
My best
guess from reading lots of code, commit messages, and old e-mails
in the
archives is that the original check-privileges-before-locking work
was
never completed.
Interesting, thank you for the analysis.
I'm currently leaning towards continuing with v4 of the patch set.
0001
and 0003 are a little weird in that a concurrent change could lead
to a
"could not find parent table" ERROR, but IIUC that is an extremely
remote
possibility.After sleeping on it, I still think this is the right call. In any
case,
I've spent way too much time on this stuff, so I plan to commit the
attached soon.
I'm OK with that. v5-0001 is an improvement over the current situation.
Regards,
Jeff Davis
On Wed, 2025-09-24 at 12:13 -0400, Tom Lane wrote:
Don't we do that intentionally, to make sure someone can't cause DOS
on a table they have no privileges on?
Is this only a problem for strong locks (ShareLock or greater)?
Strong locks are a problem when you have a pattern like a long running
query that holds an AccessShareLock, and then an unprivileged user
requests an AccessExclusiveLock, forcing other queries to queue up
behind it, and the queue doesn't clear until the long running query
finishes.
But weaker locks don't seem to have that problem, right?
Regards,
Jeff Davis
On Fri, Oct 10, 2025 at 11:31:03AM -0700, Jeff Davis wrote:
On Fri, 2025-10-10 at 11:26 -0500, Nathan Bossart wrote:
After sleeping on it, I still think this is the right call. In any
case, I've spent way too much time on this stuff, so I plan to commit
the attached soon.I'm OK with that. v5-0001 is an improvement over the current situation.
Okay, I lied. I spent even more time on these patches and came up with the
attached. Here's a summary of what's going on:
* 0001 moves the code for stats clearing/setting to use
RangeVarGetRelidExtended(). The existing code looks up the relation, locks
it, and then checks permissions. There's no guarantee that the relation
you looked up didn't concurrently change before locking, and locking before
privilege checks is troublesome from a DOS perspective. One downside of
using RangeVarGetRelidExtended() is that we can't use AccessShareLock for
regular indexes, but I'm not sure that's really a problem since we're
already using ShareUpdateExclusiveLock for everything else. The
RangeVarGetRelidExtended() callback is similar to the one modified by 0002.
This should be back-patched to v18.
* 0002 fixes the RangeVarGetRelidExtended() callback for REINDEX INDEX to
handle unlikely scenarios involving OID reuse (e.g., lookup returns the
same index OID for a different table). I did confirm there was a bug here
by concurrently re-creating an index with the same OID for a heap with a
different OID (via the pg_upgrade support functions). In previous versions
of this patch, I tried to fix this by unconditionally unlocking the heap at
the beginning of the callback, but upon further inspection, I noticed that
creates deadlock hazards because we might've already locked the index. (We
need to lock the heap first.) In v6, I've just added an ERROR for these
extremely unlikely scenarios. I've also replaced all early returns in this
function with ERRORs (except for the invalid relId case). AFAICT the extra
checks are unecessary, and even if they were necessary, I think they break
some of the code related to heap locking in subtle ways. Some callbacks do
these extra checks, and others do not, and AFAIK there haven't been any
reported problems either way. 0002 should be back-patched to v13, but it
will look a little different on v16 and newer, i.e., before MAINTAIN was
added.
* 0003 fixes the privilege checks in pg_prewarm by using a similar approach
to amcheck_lock_relation_and_check(). This seems correct to me, but it
does add more locking. This should be back-patched to v13.
* 0004 is a small patch to teach dblink to use RangeVarGetRelidExtended().
I believe this code predates that function. I don't intend to back-patch
this one.
--
nathan
Attachments:
v6-0001-fix-priv-checks-in-stats-code.patchtext/plain; charset=us-asciiDownload
From a198750398236642f724a012ad52cdf6b29c9e6d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 10 Oct 2025 15:50:06 -0500
Subject: [PATCH v6 1/4] fix priv checks in stats code
---
src/backend/statistics/attribute_stats.c | 16 ++-
src/backend/statistics/relation_stats.c | 8 +-
src/backend/statistics/stat_utils.c | 154 +++++++++++----------
src/include/statistics/stat_utils.h | 5 +-
src/test/regress/expected/stats_import.out | 6 +-
5 files changed, 101 insertions(+), 88 deletions(-)
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 1db6a7f784c..d827c9b29d7 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -19,8 +19,10 @@
#include "access/heapam.h"
#include "catalog/indexing.h"
+#include "catalog/namespace.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_operator.h"
+#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "statistics/statistics.h"
#include "statistics/stat_utils.h"
@@ -143,6 +145,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
char *attname;
AttrNumber attnum;
bool inherited;
+ Oid locked_table_oid = InvalidOid;
Relation starel;
HeapTuple statup;
@@ -182,8 +185,6 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG));
relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG));
- reloid = stats_lookup_relid(nspname, relname);
-
if (RecoveryInProgress())
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
@@ -191,7 +192,9 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
errhint("Statistics cannot be modified during recovery.")));
/* lock before looking up attribute */
- stats_lock_check_privileges(reloid);
+ reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1),
+ ShareUpdateExclusiveLock, 0,
+ RangeVarCallbackForStats, &locked_table_oid);
/* user can specify either attname or attnum, but not both */
if (!PG_ARGISNULL(ATTNAME_ARG))
@@ -917,6 +920,7 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
char *attname;
AttrNumber attnum;
bool inherited;
+ Oid locked_table_oid = InvalidOid;
stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG);
stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG);
@@ -926,15 +930,15 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG));
relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG));
- reloid = stats_lookup_relid(nspname, relname);
-
if (RecoveryInProgress())
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("recovery is in progress"),
errhint("Statistics cannot be modified during recovery.")));
- stats_lock_check_privileges(reloid);
+ reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1),
+ ShareUpdateExclusiveLock, 0,
+ RangeVarCallbackForStats, &locked_table_oid);
attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG));
attnum = get_attnum(reloid, attname);
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index a59f0c519a4..95226dcd1e1 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -20,6 +20,7 @@
#include "access/heapam.h"
#include "catalog/indexing.h"
#include "catalog/namespace.h"
+#include "nodes/makefuncs.h"
#include "statistics/stat_utils.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -82,6 +83,7 @@ relation_statistics_update(FunctionCallInfo fcinfo)
Datum values[4] = {0};
bool nulls[4] = {0};
int nreplaces = 0;
+ Oid locked_table_oid = InvalidOid;
stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG);
@@ -89,15 +91,15 @@ relation_statistics_update(FunctionCallInfo fcinfo)
nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG));
- reloid = stats_lookup_relid(nspname, relname);
-
if (RecoveryInProgress())
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("recovery is in progress"),
errhint("Statistics cannot be modified during recovery.")));
- stats_lock_check_privileges(reloid);
+ reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1),
+ ShareUpdateExclusiveLock, 0,
+ RangeVarCallbackForStats, &locked_table_oid);
if (!PG_ARGISNULL(RELPAGES_ARG))
{
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index ef7e5168bed..71e2f0f4f02 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -16,9 +16,11 @@
#include "postgres.h"
+#include "access/htup_details.h"
#include "access/relation.h"
#include "catalog/index.h"
#include "catalog/namespace.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_database.h"
#include "funcapi.h"
#include "miscadmin.h"
@@ -29,6 +31,7 @@
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
+#include "utils/syscache.h"
/*
* Ensure that a given argument is not null.
@@ -119,53 +122,85 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
}
/*
- * Lock relation in ShareUpdateExclusive mode, check privileges, and close the
- * relation (but retain the lock).
- *
* A role has privileges to set statistics on the relation if any of the
* following are true:
* - the role owns the current database and the relation is not shared
* - the role has the MAINTAIN privilege on the relation
*/
void
-stats_lock_check_privileges(Oid reloid)
+RangeVarCallbackForStats(const RangeVar *relation,
+ Oid relId, Oid oldRelId, void *arg)
{
- Relation table;
- Oid table_oid = reloid;
- Oid index_oid = InvalidOid;
- LOCKMODE index_lockmode = NoLock;
+ Oid *locked_table_oid = (Oid *) arg;
+ Oid table_oid = relId;
+ HeapTuple tuple;
+ Form_pg_class form;
+ char relkind;
/*
- * For indexes, we follow the locking behavior in do_analyze_rel() and
- * check_lock_if_inplace_updateable_rel(), which is to lock the table
- * first in ShareUpdateExclusive mode and then the index in AccessShare
- * mode.
- *
- * Partitioned indexes are treated differently than normal indexes in
- * check_lock_if_inplace_updateable_rel(), so we take a
- * ShareUpdateExclusive lock on both the partitioned table and the
- * partitioned index.
+ * If we previously locked some other index's heap, and the name we're
+ * looking up no longer refers to that relation, release the now-useless
+ * lock.
*/
- switch (get_rel_relkind(reloid))
+ if (relId != oldRelId && OidIsValid(*locked_table_oid))
{
- case RELKIND_INDEX:
- index_oid = reloid;
- table_oid = IndexGetRelation(index_oid, false);
- index_lockmode = AccessShareLock;
- break;
- case RELKIND_PARTITIONED_INDEX:
- index_oid = reloid;
- table_oid = IndexGetRelation(index_oid, false);
- index_lockmode = ShareUpdateExclusiveLock;
- break;
- default:
- break;
+ UnlockRelationOid(*locked_table_oid, ShareUpdateExclusiveLock);
+ *locked_table_oid = InvalidOid;
+ }
+
+ /* If the relation does not exist, there's nothing more to do. */
+ if (!OidIsValid(relId))
+ return;
+
+ /*
+ * If the relation does exist, check whether it's an index. But note that
+ * the relation might have been dropped betewen the time we did the name
+ * lookup and now. In that case, there's nothing to do.
+ */
+ relkind = get_rel_relkind(relId);
+ if (relkind == RELKIND_INDEX ||
+ relkind == RELKIND_PARTITIONED_INDEX)
+ table_oid = IndexGetRelation(relId, false);
+
+ /*
+ * If retrying yields the same OID, there are a couple of unlikely
+ * scenarios we need to handle.
+ */
+ if (relId == oldRelId)
+ {
+ /*
+ * If a previous lookup found an index, but the current lookup did
+ * not, release the lock on the index's table.
+ */
+ if (table_oid == relId && OidIsValid(*locked_table_oid))
+ {
+ UnlockRelationOid(*locked_table_oid, ShareUpdateExclusiveLock);
+ *locked_table_oid = InvalidOid;
+ }
+
+ /*
+ * If the current lookup found an index but we did not previously lock
+ * the right table (or any table at all), fail.
+ * RangeVarGetRelidExtended() will have already locked the index in
+ * this case, and it won't retry again, so we can't lock the newly
+ * discovered table OID without risking deadlock. Also, while this
+ * corner case is indeed possible, it is extremely unlikely to happen
+ * in practice, so it's probably not worth any more effort than this.
+ */
+ if (table_oid != relId && table_oid != *locked_table_oid)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("index \"%s\" was concurrently dropped",
+ relation->relname)));
}
- table = relation_open(table_oid, ShareUpdateExclusiveLock);
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(table_oid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for OID %u", table_oid);
+ form = (Form_pg_class) GETSTRUCT(tuple);
/* the relkinds that can be used with ANALYZE */
- switch (table->rd_rel->relkind)
+ switch (form->relkind)
{
case RELKIND_RELATION:
case RELKIND_MATVIEW:
@@ -176,65 +211,38 @@ stats_lock_check_privileges(Oid reloid)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot modify statistics for relation \"%s\"",
- RelationGetRelationName(table)),
- errdetail_relkind_not_supported(table->rd_rel->relkind)));
- }
-
- if (OidIsValid(index_oid))
- {
- Relation index;
-
- Assert(index_lockmode != NoLock);
- index = relation_open(index_oid, index_lockmode);
-
- Assert(index->rd_index && index->rd_index->indrelid == table_oid);
-
- /* retain lock on index */
- relation_close(index, NoLock);
+ NameStr(form->relname)),
+ errdetail_relkind_not_supported(form->relkind)));
}
- if (table->rd_rel->relisshared)
+ if (form->relisshared)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot modify statistics for shared relation")));
+ /* Check permissions */
if (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()))
{
- AclResult aclresult = pg_class_aclcheck(RelationGetRelid(table),
+ AclResult aclresult = pg_class_aclcheck(table_oid,
GetUserId(),
ACL_MAINTAIN);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult,
- get_relkind_objtype(table->rd_rel->relkind),
- NameStr(table->rd_rel->relname));
+ get_relkind_objtype(form->relkind),
+ NameStr(form->relname));
}
- /* retain lock on table */
- relation_close(table, NoLock);
-}
+ ReleaseSysCache(tuple);
-/*
- * Lookup relation oid from schema and relation name.
- */
-Oid
-stats_lookup_relid(const char *nspname, const char *relname)
-{
- Oid nspoid;
- Oid reloid;
-
- nspoid = LookupExplicitNamespace(nspname, false);
- reloid = get_relname_relid(relname, nspoid);
- if (!OidIsValid(reloid))
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_TABLE),
- errmsg("relation \"%s.%s\" does not exist",
- nspname, relname)));
-
- return reloid;
+ /* Lock heap before index to avoid deadlock. */
+ if (relId != oldRelId && table_oid != relId)
+ {
+ LockRelationOid(table_oid, ShareUpdateExclusiveLock);
+ *locked_table_oid = table_oid;
+ }
}
-
/*
* Find the argument number for the given argument name, returning -1 if not
* found.
diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index 512eb776e0e..a8789407e53 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -30,9 +30,8 @@ extern bool stats_check_arg_pair(FunctionCallInfo fcinfo,
struct StatsArgInfo *arginfo,
int argnum1, int argnum2);
-extern void stats_lock_check_privileges(Oid reloid);
-
-extern Oid stats_lookup_relid(const char *nspname, const char *relname);
+extern void RangeVarCallbackForStats(const RangeVar *relation,
+ Oid relId, Oid oldRelid, void *arg);
extern bool stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
FunctionCallInfo positional_fcinfo,
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 9e615ccd0af..98ce7dc2841 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -120,9 +120,9 @@ WHERE relation = 'stats_import.test'::regclass AND
SELECT mode FROM pg_locks
WHERE relation = 'stats_import.test_i'::regclass AND
pid = pg_backend_pid() AND granted;
- mode
------------------
- AccessShareLock
+ mode
+--------------------------
+ ShareUpdateExclusiveLock
(1 row)
COMMIT;
--
2.39.5 (Apple Git-154)
v6-0002-fix-reindex-index-rangevar-callback.patchtext/plain; charset=us-asciiDownload
From 6915f1ec62b184974d3f1fdbf9ec9ef65636347e Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 13 Oct 2025 11:12:50 -0500
Subject: [PATCH v6 2/4] fix reindex index rangevar callback
---
src/backend/commands/indexcmds.c | 44 +++++++++++++++++---------------
1 file changed, 24 insertions(+), 20 deletions(-)
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index ca2bde62e82..5a42be0cabe 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -2976,6 +2976,7 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation,
struct ReindexIndexCallbackState *state = arg;
LOCKMODE table_lockmode;
Oid table_oid;
+ AclResult aclresult;
/*
* Lock level here should match table lock in reindex_index() for
@@ -3006,37 +3007,40 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation,
* lookup and now. In that case, there's nothing to do.
*/
relkind = get_rel_relkind(relId);
- if (!relkind)
- return;
if (relkind != RELKIND_INDEX &&
relkind != RELKIND_PARTITIONED_INDEX)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not an index", relation->relname)));
- /* Check permissions */
- table_oid = IndexGetRelation(relId, true);
- if (OidIsValid(table_oid))
- {
- AclResult aclresult;
+ /* Look up the index's table. */
+ table_oid = IndexGetRelation(relId, false);
- aclresult = pg_class_aclcheck(table_oid, GetUserId(), ACL_MAINTAIN);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_INDEX, relation->relname);
- }
+ /*
+ * In the unlikely event that, upon retry, we get the same index OID with
+ * a different table OID, fail. RangeVarGetRelidExtended() will have
+ * already locked the index in this case, and it won't retry again, so we
+ * can't lock the newly discovered table OID without risking deadlock.
+ * Also, while this corner case is indeed possible, it is extremely
+ * unlikely to happen in practice, so it's probably not worth any more
+ * effort than this.
+ */
+ if (relId == oldRelId && table_oid != state->locked_table_oid)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("index \"%s\" was concurrently dropped",
+ relation->relname)));
+
+ /* Check permissions */
+ aclresult = pg_class_aclcheck(table_oid, GetUserId(), ACL_MAINTAIN);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_INDEX, relation->relname);
/* Lock heap before index to avoid deadlock. */
if (relId != oldRelId)
{
- /*
- * If the OID isn't valid, it means the index was concurrently
- * dropped, which is not a problem for us; just return normally.
- */
- if (OidIsValid(table_oid))
- {
- LockRelationOid(table_oid, table_lockmode);
- state->locked_table_oid = table_oid;
- }
+ LockRelationOid(table_oid, table_lockmode);
+ state->locked_table_oid = table_oid;
}
}
--
2.39.5 (Apple Git-154)
v6-0003-fix-priv-checks-in-pg_prewarm.patchtext/plain; charset=us-asciiDownload
From 7b499fdddc29114fc2eb6065ab8f5912c3f4c5d0 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 24 Sep 2025 09:47:02 -0500
Subject: [PATCH v6 3/4] fix priv checks in pg_prewarm
---
contrib/pg_prewarm/pg_prewarm.c | 39 +++++++++++++++++++++++++++++--
contrib/pg_prewarm/t/001_basic.pl | 29 ++++++++++++++++++++++-
2 files changed, 65 insertions(+), 3 deletions(-)
diff --git a/contrib/pg_prewarm/pg_prewarm.c b/contrib/pg_prewarm/pg_prewarm.c
index b968933ea8b..eff1f40bfd0 100644
--- a/contrib/pg_prewarm/pg_prewarm.c
+++ b/contrib/pg_prewarm/pg_prewarm.c
@@ -16,9 +16,11 @@
#include <unistd.h>
#include "access/relation.h"
+#include "catalog/index.h"
#include "fmgr.h"
#include "miscadmin.h"
#include "storage/bufmgr.h"
+#include "storage/lmgr.h"
#include "storage/read_stream.h"
#include "storage/smgr.h"
#include "utils/acl.h"
@@ -71,6 +73,8 @@ pg_prewarm(PG_FUNCTION_ARGS)
char *ttype;
PrewarmType ptype;
AclResult aclresult;
+ char relkind;
+ Oid privOid;
/* Basic sanity checking. */
if (PG_ARGISNULL(0))
@@ -107,8 +111,36 @@ pg_prewarm(PG_FUNCTION_ARGS)
forkNumber = forkname_to_number(forkString);
/* Open relation and check privileges. */
+ relkind = get_rel_relkind(relOid);
+ if (relkind == RELKIND_INDEX ||
+ relkind == RELKIND_PARTITIONED_INDEX)
+ {
+ privOid = IndexGetRelation(relOid, true);
+ if (OidIsValid(privOid))
+ LockRelationOid(privOid, AccessShareLock);
+ }
+ else
+ privOid = relOid;
+
rel = relation_open(relOid, AccessShareLock);
- aclresult = pg_class_aclcheck(relOid, GetUserId(), ACL_SELECT);
+
+ /*
+ * If we did the IndexGetRelation() call above, it's barely possible that
+ * a race against an index drop/recreation could have netted us the wrong
+ * table. It's possible that such a race would change the outcome of
+ * get_rel_relkind(), too, but the worst case scenario is that we'll check
+ * privileges on an index instead of its parent table, which isn't too
+ * terrible.
+ */
+ if (!OidIsValid(privOid) ||
+ (privOid != relOid &&
+ privOid != IndexGetRelation(relOid, true)))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_TABLE),
+ errmsg("could not find parent table of index \"%s\"",
+ RelationGetRelationName(rel))));
+
+ aclresult = pg_class_aclcheck(privOid, GetUserId(), ACL_SELECT);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind), get_rel_name(relOid));
@@ -233,8 +265,11 @@ pg_prewarm(PG_FUNCTION_ARGS)
read_stream_end(stream);
}
- /* Close relation, release lock. */
+ /* Close relation, release locks. */
relation_close(rel, AccessShareLock);
+ if (privOid != relOid)
+ UnlockRelationOid(privOid, AccessShareLock);
+
PG_RETURN_INT64(blocks_done);
}
diff --git a/contrib/pg_prewarm/t/001_basic.pl b/contrib/pg_prewarm/t/001_basic.pl
index 0a8259d3678..a77ab67d29e 100644
--- a/contrib/pg_prewarm/t/001_basic.pl
+++ b/contrib/pg_prewarm/t/001_basic.pl
@@ -23,7 +23,9 @@ $node->start;
$node->safe_psql("postgres",
"CREATE EXTENSION pg_prewarm;\n"
. "CREATE TABLE test(c1 int);\n"
- . "INSERT INTO test SELECT generate_series(1, 100);");
+ . "INSERT INTO test SELECT generate_series(1, 100);\n"
+ . "CREATE INDEX test_idx ON test(c1);\n"
+ . "CREATE ROLE test_user LOGIN;");
# test read mode
my $result =
@@ -42,6 +44,31 @@ ok( ( $stdout =~ qr/^[1-9][0-9]*$/
or $stderr =~ qr/prefetch is not supported by this build/),
'prefetch mode succeeded');
+# test_user should be unable to prewarm table/index without privileges
+($cmdret, $stdout, $stderr) =
+ $node->psql(
+ "postgres", "SELECT pg_prewarm('test');",
+ extra_params => [ '--username' => 'test_user' ]);
+ok($stderr =~ /permission denied for table test/, 'pg_prewarm failed as expected');
+($cmdret, $stdout, $stderr) =
+ $node->psql(
+ "postgres", "SELECT pg_prewarm('test_idx');",
+ extra_params => [ '--username' => 'test_user' ]);
+ok($stderr =~ /permission denied for index test_idx/, 'pg_prewarm failed as expected');
+
+# test_user should be able to prewarm table/index with privileges
+$node->safe_psql("postgres", "GRANT SELECT ON test TO test_user;");
+$result =
+ $node->safe_psql(
+ "postgres", "SELECT pg_prewarm('test');",
+ extra_params => [ '--username' => 'test_user' ]);
+like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm succeeded as expected');
+$result =
+ $node->safe_psql(
+ "postgres", "SELECT pg_prewarm('test_idx');",
+ extra_params => [ '--username' => 'test_user' ]);
+like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm succeeded as expected');
+
# test autoprewarm_dump_now()
$result = $node->safe_psql("postgres", "SELECT autoprewarm_dump_now();");
like($result, qr/^[1-9][0-9]*$/, 'autoprewarm_dump_now succeeded');
--
2.39.5 (Apple Git-154)
v6-0004-avoid-locking-before-privilege-checks-in-dblink.patchtext/plain; charset=us-asciiDownload
From 5127522b53a9fa90af18bcd93365c25ebdab211a Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 10 Oct 2025 09:37:51 -0500
Subject: [PATCH v6 4/4] avoid locking before privilege checks in dblink
---
contrib/dblink/dblink.c | 29 +++++++++++++++++++----------
1 file changed, 19 insertions(+), 10 deletions(-)
diff --git a/contrib/dblink/dblink.c b/contrib/dblink/dblink.c
index 0cf4c27f2e9..1e7696beb50 100644
--- a/contrib/dblink/dblink.c
+++ b/contrib/dblink/dblink.c
@@ -2460,6 +2460,21 @@ get_tuple_of_interest(Relation rel, int *pkattnums, int pknumatts, char **src_pk
return NULL;
}
+static void
+RangeVarCallbackForDblink(const RangeVar *relation,
+ Oid relId, Oid oldRelId, void *arg)
+{
+ AclResult aclresult;
+
+ if (!OidIsValid(relId))
+ return;
+
+ aclresult = pg_class_aclcheck(relId, GetUserId(), *((AclMode *) arg));
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, get_relkind_objtype(get_rel_relkind(relId)),
+ relation->relname);
+}
+
/*
* Open the relation named by relname_text, acquire specified type of lock,
* verify we have specified permissions.
@@ -2469,19 +2484,13 @@ static Relation
get_rel_from_relname(text *relname_text, LOCKMODE lockmode, AclMode aclmode)
{
RangeVar *relvar;
- Relation rel;
- AclResult aclresult;
+ Oid relid;
relvar = makeRangeVarFromNameList(textToQualifiedNameList(relname_text));
- rel = table_openrv(relvar, lockmode);
-
- aclresult = pg_class_aclcheck(RelationGetRelid(rel), GetUserId(),
- aclmode);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind),
- RelationGetRelationName(rel));
+ relid = RangeVarGetRelidExtended(relvar, lockmode, 0,
+ RangeVarCallbackForDblink, &aclmode);
- return rel;
+ return table_open(relid, NoLock);
}
/*
--
2.39.5 (Apple Git-154)
Jeff Davis <pgsql@j-davis.com> writes:
On Wed, 2025-09-24 at 12:13 -0400, Tom Lane wrote:
Don't we do that intentionally, to make sure someone can't cause DOS
on a table they have no privileges on?
Is this only a problem for strong locks (ShareLock or greater)?
Strong locks are a problem when you have a pattern like a long running
query that holds an AccessShareLock, and then an unprivileged user
requests an AccessExclusiveLock, forcing other queries to queue up
behind it, and the queue doesn't clear until the long running query
finishes.
But weaker locks don't seem to have that problem, right?
I don't think so. Even AccessShareLock is enough to block another
session trying to acquire AccessExclusiveLock, and then not only
have you DoS'd that session, but everything else trying to access
the table will queue up behind the AccessExclusiveLock request.
So it's only not-a-problem if nothing anywhere in the system wants
non-sharable locks.
regards, tom lane
On Mon, 2025-10-13 at 17:21 -0400, Tom Lane wrote:
I don't think so. Even AccessShareLock is enough to block another
session trying to acquire AccessExclusiveLock, and then not only
have you DoS'd that session, but everything else trying to access
the table will queue up behind the AccessExclusiveLock request.
So it's only not-a-problem if nothing anywhere in the system wants
non-sharable locks.
I tried imagining how that could be a problem, but couldn't come up
with anything. If the privilege check is right after the lock, then
either:
(a) The malicious AccessShareLock is granted, then is quickly released
when the privilege check fails and the transaction aborts; or
(b) The malicious AccessShareLock is queued behind a legitimate
AccessExclusiveLock, in which case every other lock would be queued up
as well. As soon as the AccessExclusiveLock is released, the
AccessShareLock would be granted, but quickly released when the
privilege check fails.
For it to be a problem, the malicious lock needs to be strong enough to
conflict with a lock level weaker than itself, i.e. ShareLock or
stronger.
I'm not sure we save anything by being lazier for weaker lock levels,
so perhaps the point is irrelevant. But if I'm missing something,
please let me know.
Regards,
Jeff Davis
On Mon, 2025-10-13 at 14:30 -0500, Nathan Bossart wrote:
* 0001 moves the code for stats clearing/setting to use
RangeVarGetRelidExtended(). The existing code looks up the relation,
locks
it, and then checks permissions. There's no guarantee that the
relation
you looked up didn't concurrently change before locking, and locking
before
privilege checks is troublesome from a DOS perspective. One downside
of
using RangeVarGetRelidExtended() is that we can't use AccessShareLock
for
regular indexes, but I'm not sure that's really a problem since we're
already using ShareUpdateExclusiveLock for everything else. The
RangeVarGetRelidExtended() callback is similar to the one modified by
0002.
This should be back-patched to v18.
We tried to match the locking behavior for analyze. Originally, that's
because we were using in-place updates, which required those specific
kinds of locks. Now that the in-place code is gone, then I think it's
OK to use ShareUpdateExclusiveLock for indexes, too, but it is a
notable difference in behavior.
Including Corey in case he has comments.
As for the patch itself, it looks good to me. Stylistically I might
have kept the "index_oid" variable, which makes some of the tests a bit
clearer, but I don't have a strong opinion.
The unlikely scenarios are a bit confusing. I'd probably error for
either case. Also, the error message on the second scenario is wrong if
the previous lookup was a table, I think.
* 0002 fixes the RangeVarGetRelidExtended() callback for REINDEX
INDEX to
handle unlikely scenarios involving OID reuse (e.g., lookup returns
the
same index OID for a different table). I did confirm there was a bug
here
by concurrently re-creating an index with the same OID for a heap
with a
different OID (via the pg_upgrade support functions). In previous
versions
of this patch, I tried to fix this by unconditionally unlocking the
heap at
the beginning of the callback, but upon further inspection, I noticed
that
creates deadlock hazards because we might've already locked the
index. (We
need to lock the heap first.) In v6, I've just added an ERROR for
these
extremely unlikely scenarios. I've also replaced all early returns
in this
function with ERRORs (except for the invalid relId case).
+1 for throwing errors when we have race conditions combined with name
reuse. Looks fine to me.
* 0003 fixes the privilege checks in pg_prewarm by using a similar
approach
to amcheck_lock_relation_and_check(). This seems correct to me, but
it
does add more locking. This should be back-patched to v13.
IIUC this is locking before the privilege check. Is there a reason why
we think this is OK here (and in amcheck_lock_relation_and_check()) but
not for the stats?
* 0004 is a small patch to teach dblink to use
RangeVarGetRelidExtended().
I believe this code predates that function. I don't intend to back-
patch
this one.
Looks good.
Regards,
Jeff Davis
Thanks for reviewing.
On Mon, Oct 13, 2025 at 07:23:36PM -0700, Jeff Davis wrote:
The unlikely scenarios are a bit confusing. I'd probably error for
either case. Also, the error message on the second scenario is wrong if
the previous lookup was a table, I think.
Yeah, I think that's a better idea.
IIUC this is locking before the privilege check. Is there a reason why
we think this is OK here (and in amcheck_lock_relation_and_check()) but
not for the stats?
For amcheck, AFAICT there aren't actually any ACL checks within the code
because the function is restricted to superuser by default. For
pg_prewarm, I don't know. You do have to install the extension before
using it, but once installed, it's available to everyone by default. My
guess is that it just hasn't been a problem in the field.
Regardless, fixing the lock-before-privilege-checks behavior doesn't strike
me as a bug, so I think we ought to proceed with something like 0003 for
back-patching purposes and then to rework it further for v19. Does that
sound okay to you?
* 0004 is a small patch to teach dblink to use
RangeVarGetRelidExtended(). I believe this code predates that
function. I don't intend to back-patch this one.Looks good.
I'm going to go commit this one now to get it out of the way.
--
nathan
Jeff Davis <pgsql@j-davis.com> writes:
On Mon, 2025-10-13 at 17:21 -0400, Tom Lane wrote:
I don't think so. Even AccessShareLock is enough to block another
session trying to acquire AccessExclusiveLock, and then not only
have you DoS'd that session, but everything else trying to access
the table will queue up behind the AccessExclusiveLock request.
So it's only not-a-problem if nothing anywhere in the system wants
non-sharable locks.
I tried imagining how that could be a problem, but couldn't come up
with anything. If the privilege check is right after the lock, then
either:
(a) The malicious AccessShareLock is granted, then is quickly released
when the privilege check fails and the transaction aborts; or
(b) The malicious AccessShareLock is queued behind a legitimate
AccessExclusiveLock, in which case every other lock would be queued up
as well. As soon as the AccessExclusiveLock is released, the
AccessShareLock would be granted, but quickly released when the
privilege check fails.
For it to be a problem, the malicious lock needs to be strong enough to
conflict with a lock level weaker than itself, i.e. ShareLock or
stronger.
Robert might remember better, but I think the assumption behind
the current design of RangeVarGetRelidExtended is that it's not
okay to take a lock in the first place if you don't have the
privilege to do so. Your analysis here supposes that it's okay
to take a lock without privileges so long as you can't block someone
else for very long, where "very long" is not tightly defined but
hopefully isn't controllable by the malicious user. So that's
moving the goalposts somewhat, but you might get people to sign
onto it with more careful analysis of the worst-case delay.
(The thing I'd worry about is whether it's possible to block
execution of the privilege check, or even just make it slow.)
Given that definition, I think you're right that it's possible to
identify cases where lock-then-check can't cause meaningful DoS.
RangeVarGetRelidExtended has to cope with the general case, so
that's not an argument for simplifying it, but we might not need
equivalent complexity everywhere.
regards, tom lane
On Tue, 2025-10-14 at 11:05 -0500, Nathan Bossart wrote:
For
pg_prewarm, I don't know. You do have to install the extension
before
using it, but once installed, it's available to everyone by default.
My
guess is that it just hasn't been a problem in the field.
If we start with an OID, what's the right way to do these kinds of
checks? Could we do an ACL check, then lock it, then do an ACL check
again to catch OID wraparound?
Last-minute suggestions on 0003:
* Add a comment around the privOid check to explain that, if the
object is an index, we must check the privileges on the table instead.
* Clarify in the comment that the race against index drop/recreation
involves OID wraparound.
+1 to the patch and backpatch.
As a separate thought, I'm wondering if we should do more to enforce
the idea that we check the privileges and owner of an index's table,
and never the index itself. That's for another discussion, though.
Regardless, fixing the lock-before-privilege-checks behavior doesn't
strike
me as a bug, so I think we ought to proceed with something like 0003
for
back-patching purposes and then to rework it further for v19. Does
that
sound okay to you?
According to the current rules[1]/messages/by-id/976405.1760459426@sss.pgh.pa.us, it does seem to technically be a
bug, but as far as I can tell, not one of much consequence.
Regards,
Jeff Davis
[1]: /messages/by-id/976405.1760459426@sss.pgh.pa.us
/messages/by-id/976405.1760459426@sss.pgh.pa.us
I've committed 0004. Here is an updated patch set.
--
nathan
Attachments:
v7-0001-Fix-lookups-in-pg_-clear-restore-_-attribute-rela.patchtext/plain; charset=us-asciiDownload
From b19e661d5244582752a5fa61b2cc9e60a7825fc1 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 14 Oct 2025 14:42:37 -0500
Subject: [PATCH v7 1/3] Fix lookups in
pg_{clear,restore}_{attribute,relation}_stats().
Reviewed-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/Z8zwVmGzXyDdkAXj%40nathan
Backpatch-through: 18
---
src/backend/statistics/attribute_stats.c | 16 ++-
src/backend/statistics/relation_stats.c | 8 +-
src/backend/statistics/stat_utils.c | 152 +++++++++++----------
src/include/statistics/stat_utils.h | 8 +-
src/test/regress/expected/stats_import.out | 6 +-
5 files changed, 103 insertions(+), 87 deletions(-)
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 1db6a7f784c..c5df83282e0 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -19,8 +19,10 @@
#include "access/heapam.h"
#include "catalog/indexing.h"
+#include "catalog/namespace.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_operator.h"
+#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "statistics/statistics.h"
#include "statistics/stat_utils.h"
@@ -143,6 +145,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
char *attname;
AttrNumber attnum;
bool inherited;
+ Oid locked_table = InvalidOid;
Relation starel;
HeapTuple statup;
@@ -182,8 +185,6 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG));
relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG));
- reloid = stats_lookup_relid(nspname, relname);
-
if (RecoveryInProgress())
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
@@ -191,7 +192,9 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
errhint("Statistics cannot be modified during recovery.")));
/* lock before looking up attribute */
- stats_lock_check_privileges(reloid);
+ reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1),
+ ShareUpdateExclusiveLock, 0,
+ RangeVarCallbackForStats, &locked_table);
/* user can specify either attname or attnum, but not both */
if (!PG_ARGISNULL(ATTNAME_ARG))
@@ -917,6 +920,7 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
char *attname;
AttrNumber attnum;
bool inherited;
+ Oid locked_table = InvalidOid;
stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG);
stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG);
@@ -926,15 +930,15 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG));
relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG));
- reloid = stats_lookup_relid(nspname, relname);
-
if (RecoveryInProgress())
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("recovery is in progress"),
errhint("Statistics cannot be modified during recovery.")));
- stats_lock_check_privileges(reloid);
+ reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1),
+ ShareUpdateExclusiveLock, 0,
+ RangeVarCallbackForStats, &locked_table);
attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG));
attnum = get_attnum(reloid, attname);
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index a59f0c519a4..174da7d93a5 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -20,6 +20,7 @@
#include "access/heapam.h"
#include "catalog/indexing.h"
#include "catalog/namespace.h"
+#include "nodes/makefuncs.h"
#include "statistics/stat_utils.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -82,6 +83,7 @@ relation_statistics_update(FunctionCallInfo fcinfo)
Datum values[4] = {0};
bool nulls[4] = {0};
int nreplaces = 0;
+ Oid locked_table = InvalidOid;
stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG);
@@ -89,15 +91,15 @@ relation_statistics_update(FunctionCallInfo fcinfo)
nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG));
- reloid = stats_lookup_relid(nspname, relname);
-
if (RecoveryInProgress())
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("recovery is in progress"),
errhint("Statistics cannot be modified during recovery.")));
- stats_lock_check_privileges(reloid);
+ reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1),
+ ShareUpdateExclusiveLock, 0,
+ RangeVarCallbackForStats, &locked_table);
if (!PG_ARGISNULL(RELPAGES_ARG))
{
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index ef7e5168bed..5fd49e26132 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -16,9 +16,11 @@
#include "postgres.h"
+#include "access/htup_details.h"
#include "access/relation.h"
#include "catalog/index.h"
#include "catalog/namespace.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_database.h"
#include "funcapi.h"
#include "miscadmin.h"
@@ -29,6 +31,7 @@
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
+#include "utils/syscache.h"
/*
* Ensure that a given argument is not null.
@@ -119,53 +122,84 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
}
/*
- * Lock relation in ShareUpdateExclusive mode, check privileges, and close the
- * relation (but retain the lock).
- *
* A role has privileges to set statistics on the relation if any of the
* following are true:
* - the role owns the current database and the relation is not shared
* - the role has the MAINTAIN privilege on the relation
*/
void
-stats_lock_check_privileges(Oid reloid)
+RangeVarCallbackForStats(const RangeVar *relation,
+ Oid relId, Oid oldRelId, void *arg)
{
- Relation table;
- Oid table_oid = reloid;
- Oid index_oid = InvalidOid;
- LOCKMODE index_lockmode = NoLock;
+ Oid *locked_oid = (Oid *) arg;
+ Oid table_oid = relId;
+ HeapTuple tuple;
+ Form_pg_class form;
+ char relkind;
/*
- * For indexes, we follow the locking behavior in do_analyze_rel() and
- * check_lock_if_inplace_updateable_rel(), which is to lock the table
- * first in ShareUpdateExclusive mode and then the index in AccessShare
- * mode.
- *
- * Partitioned indexes are treated differently than normal indexes in
- * check_lock_if_inplace_updateable_rel(), so we take a
- * ShareUpdateExclusive lock on both the partitioned table and the
- * partitioned index.
+ * If we previously locked some other index's heap, and the name we're
+ * looking up no longer refers to that relation, release the now-useless
+ * lock.
*/
- switch (get_rel_relkind(reloid))
+ if (relId != oldRelId && OidIsValid(*locked_oid))
{
- case RELKIND_INDEX:
- index_oid = reloid;
- table_oid = IndexGetRelation(index_oid, false);
- index_lockmode = AccessShareLock;
- break;
- case RELKIND_PARTITIONED_INDEX:
- index_oid = reloid;
- table_oid = IndexGetRelation(index_oid, false);
- index_lockmode = ShareUpdateExclusiveLock;
- break;
- default:
- break;
+ UnlockRelationOid(*locked_oid, ShareUpdateExclusiveLock);
+ *locked_oid = InvalidOid;
+ }
+
+ /* If the relation does not exist, there's nothing more to do. */
+ if (!OidIsValid(relId))
+ return;
+
+ /* If the relation does exist, check whether it's an index. */
+ relkind = get_rel_relkind(relId);
+ if (relkind == RELKIND_INDEX ||
+ relkind == RELKIND_PARTITIONED_INDEX)
+ table_oid = IndexGetRelation(relId, false);
+
+ /*
+ * If retrying yields the same OID, there are a couple of extremely
+ * unlikely scenarios we need to handle.
+ */
+ if (relId == oldRelId)
+ {
+ /*
+ * If a previous lookup found an index, but the current lookup did
+ * not, the index was dropped and the OID was reused for something
+ * else between lookups. In theory, we could simply drop our lock on
+ * the index's relation and proceed, but in the interest of avoiding
+ * complexity, we just error.
+ */
+ if (table_oid == relId && OidIsValid(*locked_oid))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("index \"%s\" was concurrently dropped",
+ relation->relname)));
+
+ /*
+ * If the current lookup found an index but a previous lookup either
+ * did not find an index or found one with a different parent
+ * relation, the relation was dropped and the OID was reused for an
+ * index between lookups. RangeVarGetRelidExtended() will have
+ * already locked the index at this point, so we can't just lock the
+ * newly discovered parent table OID without risking deadlock. As
+ * above, we just error in this case.
+ */
+ if (table_oid != relId && table_oid != *locked_oid)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("index \"%s\" was concurrently created",
+ relation->relname)));
}
- table = relation_open(table_oid, ShareUpdateExclusiveLock);
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(table_oid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for OID %u", table_oid);
+ form = (Form_pg_class) GETSTRUCT(tuple);
/* the relkinds that can be used with ANALYZE */
- switch (table->rd_rel->relkind)
+ switch (form->relkind)
{
case RELKIND_RELATION:
case RELKIND_MATVIEW:
@@ -176,62 +210,36 @@ stats_lock_check_privileges(Oid reloid)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot modify statistics for relation \"%s\"",
- RelationGetRelationName(table)),
- errdetail_relkind_not_supported(table->rd_rel->relkind)));
+ NameStr(form->relname)),
+ errdetail_relkind_not_supported(form->relkind)));
}
- if (OidIsValid(index_oid))
- {
- Relation index;
-
- Assert(index_lockmode != NoLock);
- index = relation_open(index_oid, index_lockmode);
-
- Assert(index->rd_index && index->rd_index->indrelid == table_oid);
-
- /* retain lock on index */
- relation_close(index, NoLock);
- }
-
- if (table->rd_rel->relisshared)
+ if (form->relisshared)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot modify statistics for shared relation")));
+ /* Check permissions */
if (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()))
{
- AclResult aclresult = pg_class_aclcheck(RelationGetRelid(table),
+ AclResult aclresult = pg_class_aclcheck(table_oid,
GetUserId(),
ACL_MAINTAIN);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult,
- get_relkind_objtype(table->rd_rel->relkind),
- NameStr(table->rd_rel->relname));
+ get_relkind_objtype(form->relkind),
+ NameStr(form->relname));
}
- /* retain lock on table */
- relation_close(table, NoLock);
-}
+ ReleaseSysCache(tuple);
-/*
- * Lookup relation oid from schema and relation name.
- */
-Oid
-stats_lookup_relid(const char *nspname, const char *relname)
-{
- Oid nspoid;
- Oid reloid;
-
- nspoid = LookupExplicitNamespace(nspname, false);
- reloid = get_relname_relid(relname, nspoid);
- if (!OidIsValid(reloid))
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_TABLE),
- errmsg("relation \"%s.%s\" does not exist",
- nspname, relname)));
-
- return reloid;
+ /* Lock heap before index to avoid deadlock. */
+ if (relId != oldRelId && table_oid != relId)
+ {
+ LockRelationOid(table_oid, ShareUpdateExclusiveLock);
+ *locked_oid = table_oid;
+ }
}
diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index 512eb776e0e..f41b181d4d3 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -15,6 +15,9 @@
#include "fmgr.h"
+/* avoid including primnodes.h here */
+typedef struct RangeVar RangeVar;
+
struct StatsArgInfo
{
const char *argname;
@@ -30,9 +33,8 @@ extern bool stats_check_arg_pair(FunctionCallInfo fcinfo,
struct StatsArgInfo *arginfo,
int argnum1, int argnum2);
-extern void stats_lock_check_privileges(Oid reloid);
-
-extern Oid stats_lookup_relid(const char *nspname, const char *relname);
+extern void RangeVarCallbackForStats(const RangeVar *relation,
+ Oid relId, Oid oldRelid, void *arg);
extern bool stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
FunctionCallInfo positional_fcinfo,
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 9e615ccd0af..98ce7dc2841 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -120,9 +120,9 @@ WHERE relation = 'stats_import.test'::regclass AND
SELECT mode FROM pg_locks
WHERE relation = 'stats_import.test_i'::regclass AND
pid = pg_backend_pid() AND granted;
- mode
------------------
- AccessShareLock
+ mode
+--------------------------
+ ShareUpdateExclusiveLock
(1 row)
COMMIT;
--
2.39.5 (Apple Git-154)
v7-0002-Fix-lookup-code-for-REINDEX-INDEX.patchtext/plain; charset=us-asciiDownload
From 28c556c4d2356f18b35fc81fd9210675a09f204f Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 14 Oct 2025 15:55:35 -0500
Subject: [PATCH v7 2/3] Fix lookup code for REINDEX INDEX.
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Reviewed-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/Z8zwVmGzXyDdkAXj%40nathan
Backpatch-through: 13
---
src/backend/commands/indexcmds.c | 50 ++++++++++++++++----------------
1 file changed, 25 insertions(+), 25 deletions(-)
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index ca2bde62e82..5712fac3697 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -2976,6 +2976,7 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation,
struct ReindexIndexCallbackState *state = arg;
LOCKMODE table_lockmode;
Oid table_oid;
+ AclResult aclresult;
/*
* Lock level here should match table lock in reindex_index() for
@@ -3000,43 +3001,42 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation,
if (!OidIsValid(relId))
return;
- /*
- * If the relation does exist, check whether it's an index. But note that
- * the relation might have been dropped between the time we did the name
- * lookup and now. In that case, there's nothing to do.
- */
+ /* If the relation does exist, check whether it's an index. */
relkind = get_rel_relkind(relId);
- if (!relkind)
- return;
if (relkind != RELKIND_INDEX &&
relkind != RELKIND_PARTITIONED_INDEX)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not an index", relation->relname)));
- /* Check permissions */
- table_oid = IndexGetRelation(relId, true);
- if (OidIsValid(table_oid))
- {
- AclResult aclresult;
+ /* Look up the index's table. */
+ table_oid = IndexGetRelation(relId, false);
- aclresult = pg_class_aclcheck(table_oid, GetUserId(), ACL_MAINTAIN);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_INDEX, relation->relname);
- }
+ /*
+ * In the unlikely event that, upon retry, we get the same index OID with
+ * a different table OID, fail. RangeVarGetRelidExtended() will have
+ * already locked the index in this case, and it won't retry again, so we
+ * can't lock the newly discovered table OID without risking deadlock.
+ * Also, while this corner case is indeed possible, it is extremely
+ * unlikely to happen in practice, so it's probably not worth any more
+ * effort than this.
+ */
+ if (relId == oldRelId && table_oid != state->locked_table_oid)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("index \"%s\" was concurrently dropped",
+ relation->relname)));
+
+ /* Check permissions. */
+ aclresult = pg_class_aclcheck(table_oid, GetUserId(), ACL_MAINTAIN);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_INDEX, relation->relname);
/* Lock heap before index to avoid deadlock. */
if (relId != oldRelId)
{
- /*
- * If the OID isn't valid, it means the index was concurrently
- * dropped, which is not a problem for us; just return normally.
- */
- if (OidIsValid(table_oid))
- {
- LockRelationOid(table_oid, table_lockmode);
- state->locked_table_oid = table_oid;
- }
+ LockRelationOid(table_oid, table_lockmode);
+ state->locked_table_oid = table_oid;
}
}
--
2.39.5 (Apple Git-154)
v7-0003-Fix-privilege-checks-for-pg_prewarm-on-indexes.patchtext/plain; charset=us-asciiDownload
From 2b5004e14d5802fda3f51cbeaa0a41a84c633f62 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 14 Oct 2025 16:22:22 -0500
Subject: [PATCH v7 3/3] Fix privilege checks for pg_prewarm() on indexes.
Author: Ayush Vatsa <ayushvatsa1810@gmail.com>
Co-authored-by: Nathan Bossart <nathandbossart@gmail.com>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Reviewed-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/CACX%2BKaMz2ZoOojh0nQ6QNBYx8Ak1Dkoko%3DD4FSb80BYW%2Bo8CHQ%40mail.gmail.com
Backpatch-through: 13
---
contrib/pg_prewarm/pg_prewarm.c | 47 +++++++++++++++++++++++++++++--
contrib/pg_prewarm/t/001_basic.pl | 29 ++++++++++++++++++-
2 files changed, 72 insertions(+), 4 deletions(-)
diff --git a/contrib/pg_prewarm/pg_prewarm.c b/contrib/pg_prewarm/pg_prewarm.c
index b968933ea8b..5b519a2c854 100644
--- a/contrib/pg_prewarm/pg_prewarm.c
+++ b/contrib/pg_prewarm/pg_prewarm.c
@@ -16,9 +16,11 @@
#include <unistd.h>
#include "access/relation.h"
+#include "catalog/index.h"
#include "fmgr.h"
#include "miscadmin.h"
#include "storage/bufmgr.h"
+#include "storage/lmgr.h"
#include "storage/read_stream.h"
#include "storage/smgr.h"
#include "utils/acl.h"
@@ -71,6 +73,8 @@ pg_prewarm(PG_FUNCTION_ARGS)
char *ttype;
PrewarmType ptype;
AclResult aclresult;
+ char relkind;
+ Oid privOid;
/* Basic sanity checking. */
if (PG_ARGISNULL(0))
@@ -106,9 +110,43 @@ pg_prewarm(PG_FUNCTION_ARGS)
forkString = text_to_cstring(forkName);
forkNumber = forkname_to_number(forkString);
- /* Open relation and check privileges. */
+ /*
+ * Open relation and check privileges. If the relation is an index, we
+ * must check the privileges on its parent table instead.
+ */
+ relkind = get_rel_relkind(relOid);
+ if (relkind == RELKIND_INDEX ||
+ relkind == RELKIND_PARTITIONED_INDEX)
+ {
+ privOid = IndexGetRelation(relOid, true);
+
+ /* Lock table before index to avoid deadlock. */
+ if (OidIsValid(privOid))
+ LockRelationOid(privOid, AccessShareLock);
+ }
+ else
+ privOid = relOid;
+
rel = relation_open(relOid, AccessShareLock);
- aclresult = pg_class_aclcheck(relOid, GetUserId(), ACL_SELECT);
+
+ /*
+ * It's possible that the relation with OID "privOid" was dropped and the
+ * OID was reused before we locked it. If that happens, we could be left
+ * with the wrong parent table OID, in which case we must ERROR. It's
+ * possible that such a race would change the outcome of
+ * get_rel_relkind(), too, but the worst case scenario there is that we'll
+ * check privileges on the index instead of its parent table, which isn't
+ * too terrible.
+ */
+ if (!OidIsValid(privOid) ||
+ (privOid != relOid &&
+ privOid != IndexGetRelation(relOid, true)))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_TABLE),
+ errmsg("could not find parent table of index \"%s\"",
+ RelationGetRelationName(rel))));
+
+ aclresult = pg_class_aclcheck(privOid, GetUserId(), ACL_SELECT);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind), get_rel_name(relOid));
@@ -233,8 +271,11 @@ pg_prewarm(PG_FUNCTION_ARGS)
read_stream_end(stream);
}
- /* Close relation, release lock. */
+ /* Close relation, release locks. */
relation_close(rel, AccessShareLock);
+ if (privOid != relOid)
+ UnlockRelationOid(privOid, AccessShareLock);
+
PG_RETURN_INT64(blocks_done);
}
diff --git a/contrib/pg_prewarm/t/001_basic.pl b/contrib/pg_prewarm/t/001_basic.pl
index 0a8259d3678..a77ab67d29e 100644
--- a/contrib/pg_prewarm/t/001_basic.pl
+++ b/contrib/pg_prewarm/t/001_basic.pl
@@ -23,7 +23,9 @@ $node->start;
$node->safe_psql("postgres",
"CREATE EXTENSION pg_prewarm;\n"
. "CREATE TABLE test(c1 int);\n"
- . "INSERT INTO test SELECT generate_series(1, 100);");
+ . "INSERT INTO test SELECT generate_series(1, 100);\n"
+ . "CREATE INDEX test_idx ON test(c1);\n"
+ . "CREATE ROLE test_user LOGIN;");
# test read mode
my $result =
@@ -42,6 +44,31 @@ ok( ( $stdout =~ qr/^[1-9][0-9]*$/
or $stderr =~ qr/prefetch is not supported by this build/),
'prefetch mode succeeded');
+# test_user should be unable to prewarm table/index without privileges
+($cmdret, $stdout, $stderr) =
+ $node->psql(
+ "postgres", "SELECT pg_prewarm('test');",
+ extra_params => [ '--username' => 'test_user' ]);
+ok($stderr =~ /permission denied for table test/, 'pg_prewarm failed as expected');
+($cmdret, $stdout, $stderr) =
+ $node->psql(
+ "postgres", "SELECT pg_prewarm('test_idx');",
+ extra_params => [ '--username' => 'test_user' ]);
+ok($stderr =~ /permission denied for index test_idx/, 'pg_prewarm failed as expected');
+
+# test_user should be able to prewarm table/index with privileges
+$node->safe_psql("postgres", "GRANT SELECT ON test TO test_user;");
+$result =
+ $node->safe_psql(
+ "postgres", "SELECT pg_prewarm('test');",
+ extra_params => [ '--username' => 'test_user' ]);
+like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm succeeded as expected');
+$result =
+ $node->safe_psql(
+ "postgres", "SELECT pg_prewarm('test_idx');",
+ extra_params => [ '--username' => 'test_user' ]);
+like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm succeeded as expected');
+
# test autoprewarm_dump_now()
$result = $node->safe_psql("postgres", "SELECT autoprewarm_dump_now();");
like($result, qr/^[1-9][0-9]*$/, 'autoprewarm_dump_now succeeded');
--
2.39.5 (Apple Git-154)
On Tue, Oct 14, 2025 at 10:01:37AM -0700, Jeff Davis wrote:
If we start with an OID, what's the right way to do these kinds of
checks? Could we do an ACL check, then lock it, then do an ACL check
again to catch OID wraparound?
I tried something like this upthread [0]/messages/by-id/aOgmi6avE6qMw_6t@nathan. My feeling was that this was a
lot of complexity for not a lot of gain. Perhaps it's still worth doing,
though.
[0]: /messages/by-id/aOgmi6avE6qMw_6t@nathan
--
nathan
I just pushed 0001, and longfin and sifaka have very quickly reminded me
that we don't require C11 on v18. Will fix shortly...
--
nathan
I've committed everything except for 0003, which I probably won't get to
until Friday. Note that I decided against back-patching 0002 because of
the presumed rarity of and lack of reports for the bug.
--
nathan
On Wed, Oct 15, 2025 at 04:38:05PM -0500, Nathan Bossart wrote:
I've committed everything except for 0003, which I probably won't get to
until Friday. Note that I decided against back-patching 0002 because of
the presumed rarity of and lack of reports for the bug.
Everything tracked in this thread is now committed.
--
nathan