From b6a9184c9202a84c5fa925cb87924b47bbe8aeca Mon Sep 17 00:00:00 2001
From: Andreas Lind <andreaslindpetersen@gmail.com>
Date: Thu, 9 Jul 2026 15:06:39 +0200
Subject: [PATCH] Fix relcache's equalPolicy() to compare the
 permissive/restrictive flag

equalPolicy() is used by equalRSDesc(), which RelationRebuildRelation()
consults when rebuilding an open relation's relcache entry in place
(refcount > 0) to decide whether the entry's existing row-security
descriptor can be kept as-is or must be treated as changed. It
compared polcmd, hassublinks, policy_name, roles, qual, and
with_check_qual, but never permissive, so two policies that are
identical in every other respect but differ in their
PERMISSIVE/RESTRICTIVE designation were reported as equal, and the
stale descriptor (with the old designation) would be kept.

This is reachable without any unsupported catalog surgery: ALTER
POLICY has no way to change a policy's PERMISSIVE/RESTRICTIVE
designation, but DROP POLICY followed by CREATE POLICY of the same
name, roles, command, and quals, differing only in AS
PERMISSIVE/RESTRICTIVE, hits exactly this case, and would leave the
relcache out of sync with how the policy should now combine with
others (PERMISSIVE policies are ORed together; RESTRICTIVE policies
are ANDed with the rest).

Note that RelationRebuildRelation(), and thus this comparison, is only
reached while the relation is still open (refcount > 0) at the moment
its invalidation is locally processed; if nothing holds it open at
that point, RelationClearRelation() just deletes the entry outright
and it gets rebuilt from scratch on next access, sidestepping the
comparison entirely. Consequently this isn't reachable through a
straightforward sequence of ordinary SQL statements: closing and
reopening the relation between statements (the usual behavior) avoids
the bug, and a second session cannot hold the relation open across the
DROP/CREATE either, since both require AccessExclusiveLock. As with
ab6d1cd26eb, which fixed the same kind of omission in this function
for the USING qual, this is a fix for an internal cache-consistency
invariant rather than something with a simple externally observable
trigger, and is submitted without a reproduction script for that
reason.
---
 src/backend/utils/cache/relcache.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index fb4e042be8a..19c4ff6e75e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -980,6 +980,8 @@ equalPolicy(RowSecurityPolicy *policy1, RowSecurityPolicy *policy2)
 
 		if (policy1->polcmd != policy2->polcmd)
 			return false;
+		if (policy1->permissive != policy2->permissive)
+			return false;
 		if (policy1->hassublinks != policy2->hassublinks)
 			return false;
 		if (strcmp(policy1->policy_name, policy2->policy_name) != 0)
-- 
2.50.1 (Apple Git-155)

