revamp row-security tracking

Started by Nathan Bossartabout 1 year ago7 messages
#1Nathan Bossart
nathandbossart@gmail.com
1 attachment(s)

In light of CVE-2024-10976, which was fixed by commit cd7ab57, I'd like to
propose a bigger change to this area of the code that aims to future-proof
it a bit. Instead of requiring hackers to carefully cart around whether a
query references a table with RLS enabled, I think we should instead
accumulate such information globally and require higher-level routines like
fireRIRrules() and inline_set_returning_function() to inspect it as needed.

The attached patch accomplishes this by establishing a global queue of
row-security "nest levels" that the aforementioned higher-level callers can
use. Essentially, they will first call PushRowSecurityNestLevel(), then
any calls to get_row_security_policies() that encounter a table with
row-security enabled will mark the current nest level (and its parents).
Finally, PopRowSecurity() is used to retrieve whether row-security might
apply, and the query can be marked correctly. I've also attempted to
handle resetting this queue after an ERROR or failed SRF inlining, but I'm
not yet positive that I've got all the details right.

With this patch applied, we should be able to revert some of the
row-security tracking code, especially the stuff added by commit cd7ab57.

Thoughts?

--
nathan

Attachments:

v1-0001-revamp-row-security-tracking.patchtext/plain; charset=us-asciiDownload
From 7645f3a7215b052ab29be6d8e2b6e84954cb5caf Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Thu, 21 Nov 2024 11:57:43 -0600
Subject: [PATCH v1 1/1] revamp row-security tracking

---
 src/backend/access/transam/xact.c    |  6 ++
 src/backend/executor/functions.c     |  6 --
 src/backend/optimizer/util/clauses.c |  6 ++
 src/backend/rewrite/rewriteHandler.c | 77 ++++--------------------
 src/backend/rewrite/rowsecurity.c    | 88 +++++++++++++++++++++++++---
 src/include/rewrite/rowsecurity.h    |  6 +-
 src/tools/pgindent/typedefs.list     |  1 -
 7 files changed, 109 insertions(+), 81 deletions(-)

diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index b7ebcc2a55..440bf34906 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -51,6 +51,7 @@
 #include "replication/origin.h"
 #include "replication/snapbuild.h"
 #include "replication/syncrep.h"
+#include "rewrite/rowsecurity.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
 #include "storage/lmgr.h"
@@ -2473,6 +2474,7 @@ CommitTransaction(void)
 	AtEOXact_Snapshot(true, false);
 	AtEOXact_ApplyLauncher(true);
 	AtEOXact_LogicalRepWorkers(true);
+	AtEOXact_RowSecurity(true);
 	pgstat_report_xact_timestamp(0);
 
 	ResourceOwnerDelete(TopTransactionResourceOwner);
@@ -2766,6 +2768,7 @@ PrepareTransaction(void)
 	/* we treat PREPARE as ROLLBACK so far as waking workers goes */
 	AtEOXact_ApplyLauncher(false);
 	AtEOXact_LogicalRepWorkers(false);
+	AtEOXact_RowSecurity(true);
 	pgstat_report_xact_timestamp(0);
 
 	CurrentResourceOwner = NULL;
@@ -2985,6 +2988,7 @@ AbortTransaction(void)
 		AtEOXact_PgStat(false, is_parallel_worker);
 		AtEOXact_ApplyLauncher(false);
 		AtEOXact_LogicalRepWorkers(false);
+		AtEOXact_RowSecurity(false);
 		pgstat_report_xact_timestamp(0);
 	}
 
@@ -5197,6 +5201,7 @@ CommitSubTransaction(void)
 	AtEOSubXact_HashTables(true, s->nestingLevel);
 	AtEOSubXact_PgStat(true, s->nestingLevel);
 	AtSubCommit_Snapshot(s->nestingLevel);
+	AtEOXact_RowSecurity(true);
 
 	/*
 	 * We need to restore the upper transaction's read-only state, in case the
@@ -5362,6 +5367,7 @@ AbortSubTransaction(void)
 		AtEOSubXact_HashTables(false, s->nestingLevel);
 		AtEOSubXact_PgStat(false, s->nestingLevel);
 		AtSubAbort_Snapshot(s->nestingLevel);
+		AtEOXact_RowSecurity(false);
 	}
 
 	/*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
index 3988066330..692854e2b3 100644
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1972,12 +1972,6 @@ tlist_coercion_finished:
 		rtr->rtindex = 1;
 		newquery->jointree = makeFromExpr(list_make1(rtr), NULL);
 
-		/*
-		 * Make sure the new query is marked as having row security if the
-		 * original one does.
-		 */
-		newquery->hasRowSecurity = parse->hasRowSecurity;
-
 		/* Replace original query in the correct element of the query list */
 		lfirst(parse_cell) = newquery;
 	}
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 8e39795e24..775c79eca5 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -43,6 +43,7 @@
 #include "parser/parse_func.h"
 #include "rewrite/rewriteHandler.h"
 #include "rewrite/rewriteManip.h"
+#include "rewrite/rowsecurity.h"
 #include "tcop/tcopprot.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
@@ -5082,6 +5083,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
 	List	   *raw_parsetree_list;
 	List	   *querytree_list;
 	Query	   *querytree;
+	int			rowsec_level;
 
 	Assert(rte->rtekind == RTE_FUNCTION);
 
@@ -5169,6 +5171,8 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
 		return NULL;
 	}
 
+	rowsec_level = PushRowSecurityNestLevel();
+
 	/*
 	 * Make a temporary memory context, so that we don't leak all the stuff
 	 * that parsing might create.
@@ -5333,6 +5337,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
 	 * We must also notice if the inserted query adds a dependency on the
 	 * calling role due to RLS quals.
 	 */
+	querytree->hasRowSecurity = PopRowSecurityNestLevel(false, rowsec_level);
 	if (querytree->hasRowSecurity)
 		root->glob->dependsOnRole = true;
 
@@ -5340,6 +5345,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
 
 	/* Here if func is not inlinable: release temp memory and return NULL */
 fail:
+	PopRowSecurityNestLevel(true, rowsec_level);
 	MemoryContextSwitchTo(oldcxt);
 	MemoryContextDelete(mycxt);
 	error_context_stack = sqlerrcontext.previous;
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 063afd4933..56c6ddf86d 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -58,12 +58,6 @@ typedef struct acquireLocksOnSubLinks_context
 	bool		for_execute;	/* AcquireRewriteLocks' forExecute param */
 } acquireLocksOnSubLinks_context;
 
-typedef struct fireRIRonSubLink_context
-{
-	List	   *activeRIRs;
-	bool		hasRowSecurity;
-} fireRIRonSubLink_context;
-
 static bool acquireLocksOnSubLinks(Node *node,
 								   acquireLocksOnSubLinks_context *context);
 static Query *rewriteRuleAction(Query *parsetree,
@@ -1845,12 +1839,6 @@ ApplyRetrieveRule(Query *parsetree,
 	 */
 	rule_action = fireRIRrules(rule_action, activeRIRs);
 
-	/*
-	 * Make sure the query is marked as having row security if the view query
-	 * does.
-	 */
-	parsetree->hasRowSecurity |= rule_action->hasRowSecurity;
-
 	/*
 	 * Now, plug the view query in as a subselect, converting the relation's
 	 * original RTE to a subquery RTE.
@@ -1964,7 +1952,7 @@ markQueryForLocking(Query *qry, Node *jtnode,
  * the SubLink's subselect link with the possibly-rewritten subquery.
  */
 static bool
-fireRIRonSubLink(Node *node, fireRIRonSubLink_context *context)
+fireRIRonSubLink(Node *node, List *activeRIRs)
 {
 	if (node == NULL)
 		return false;
@@ -1974,12 +1962,7 @@ fireRIRonSubLink(Node *node, fireRIRonSubLink_context *context)
 
 		/* Do what we came for */
 		sub->subselect = (Node *) fireRIRrules((Query *) sub->subselect,
-											   context->activeRIRs);
-
-		/*
-		 * Remember if any of the sublinks have row security.
-		 */
-		context->hasRowSecurity |= ((Query *) sub->subselect)->hasRowSecurity;
+											   activeRIRs);
 
 		/* Fall through to process lefthand args of SubLink */
 	}
@@ -1989,7 +1972,7 @@ fireRIRonSubLink(Node *node, fireRIRonSubLink_context *context)
 	 * subselects of subselects for us.
 	 */
 	return expression_tree_walker(node, fireRIRonSubLink,
-								  (void *) context);
+								  (void *) activeRIRs);
 }
 
 
@@ -2006,6 +1989,7 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 	int			origResultRelation = parsetree->resultRelation;
 	int			rt_index;
 	ListCell   *lc;
+	int			rowsec_level = PushRowSecurityNestLevel();
 
 	/*
 	 * Expand SEARCH and CYCLE clauses in CTEs.
@@ -2050,13 +2034,6 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 		if (rte->rtekind == RTE_SUBQUERY)
 		{
 			rte->subquery = fireRIRrules(rte->subquery, activeRIRs);
-
-			/*
-			 * While we are here, make sure the query is marked as having row
-			 * security if any of its subqueries do.
-			 */
-			parsetree->hasRowSecurity |= rte->subquery->hasRowSecurity;
-
 			continue;
 		}
 
@@ -2170,12 +2147,6 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 
 		cte->ctequery = (Node *)
 			fireRIRrules((Query *) cte->ctequery, activeRIRs);
-
-		/*
-		 * While we are here, make sure the query is marked as having row
-		 * security if any of its CTEs do.
-		 */
-		parsetree->hasRowSecurity |= ((Query *) cte->ctequery)->hasRowSecurity;
 	}
 
 	/*
@@ -2183,22 +2154,9 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 	 * the rtable and cteList.
 	 */
 	if (parsetree->hasSubLinks)
-	{
-		fireRIRonSubLink_context context;
-
-		context.activeRIRs = activeRIRs;
-		context.hasRowSecurity = false;
-
-		query_tree_walker(parsetree, fireRIRonSubLink, (void *) &context,
+		query_tree_walker(parsetree, fireRIRonSubLink, (void *) activeRIRs,
 						  QTW_IGNORE_RC_SUBQUERIES);
 
-		/*
-		 * Make sure the query is marked as having row security if any of its
-		 * sublinks do.
-		 */
-		parsetree->hasRowSecurity |= context.hasRowSecurity;
-	}
-
 	/*
 	 * Apply any row-level security policies.  We do this last because it
 	 * requires special recursion detection if the new quals have sublink
@@ -2212,7 +2170,6 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 		Relation	rel;
 		List	   *securityQuals;
 		List	   *withCheckOptions;
-		bool		hasRowSecurity;
 		bool		hasSubLinks;
 
 		++rt_index;
@@ -2230,14 +2187,13 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 		 */
 		get_row_security_policies(parsetree, rte, rt_index,
 								  &securityQuals, &withCheckOptions,
-								  &hasRowSecurity, &hasSubLinks);
+								  &hasSubLinks);
 
 		if (securityQuals != NIL || withCheckOptions != NIL)
 		{
 			if (hasSubLinks)
 			{
 				acquireLocksOnSubLinks_context context;
-				fireRIRonSubLink_context fire_context;
 
 				/*
 				 * Recursively process the new quals, checking for infinite
@@ -2268,21 +2224,11 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 				 * Now that we have the locks on anything added by
 				 * get_row_security_policies, fire any RIR rules for them.
 				 */
-				fire_context.activeRIRs = activeRIRs;
-				fire_context.hasRowSecurity = false;
-
 				expression_tree_walker((Node *) securityQuals,
-									   fireRIRonSubLink, (void *) &fire_context);
+									   fireRIRonSubLink, (void *) activeRIRs);
 
 				expression_tree_walker((Node *) withCheckOptions,
-									   fireRIRonSubLink, (void *) &fire_context);
-
-				/*
-				 * We can ignore the value of fire_context.hasRowSecurity
-				 * since we only reach this code in cases where hasRowSecurity
-				 * is already true.
-				 */
-				Assert(hasRowSecurity);
+									   fireRIRonSubLink, (void *) activeRIRs);
 
 				activeRIRs = list_delete_last(activeRIRs);
 			}
@@ -2301,17 +2247,16 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 		}
 
 		/*
-		 * Make sure the query is marked correctly if row-level security
-		 * applies, or if the new quals had sublinks.
+		 * Make sure the query is marked correctly if the new quals had
+		 * sublinks.
 		 */
-		if (hasRowSecurity)
-			parsetree->hasRowSecurity = true;
 		if (hasSubLinks)
 			parsetree->hasSubLinks = true;
 
 		table_close(rel, NoLock);
 	}
 
+	parsetree->hasRowSecurity = PopRowSecurityNestLevel(false, rowsec_level);
 	return parsetree;
 }
 
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
index 59fd305dd7..de3fb9325f 100644
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -45,6 +45,7 @@
 #include "rewrite/rewriteManip.h"
 #include "rewrite/rowsecurity.h"
 #include "utils/acl.h"
+#include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/rls.h"
 
@@ -74,6 +75,11 @@ static void add_with_check_options(Relation rel,
 
 static bool check_role_for_policy(ArrayType *policy_roles, Oid user_id);
 
+static void SetRowSecurityForCurrentNestLevel(void);
+
+static Bitmapset *row_sec_bms;
+static int	row_sec_nest_level = -1;
+
 /*
  * hooks to allow extensions to add their own security policies
  *
@@ -90,14 +96,14 @@ row_security_policy_hook_type row_security_policy_hook_restrictive = NULL;
  * Get any row security quals and WithCheckOption checks that should be
  * applied to the specified RTE.
  *
- * In addition, hasRowSecurity is set to true if row-level security is enabled
+ * In addition, we mark the current nest level if row security is enabled
  * (even if this RTE doesn't have any row security quals), and hasSubLinks is
  * set to true if any of the quals returned contain sublinks.
  */
 void
 get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index,
 						  List **securityQuals, List **withCheckOptions,
-						  bool *hasRowSecurity, bool *hasSubLinks)
+						  bool *hasSubLinks)
 {
 	Oid			user_id;
 	int			rls_status;
@@ -110,7 +116,6 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index,
 	/* Defaults for the return values */
 	*securityQuals = NIL;
 	*withCheckOptions = NIL;
-	*hasRowSecurity = false;
 	*hasSubLinks = false;
 
 	Assert(rte->rtekind == RTE_RELATION);
@@ -135,8 +140,8 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index,
 
 	/*
 	 * RLS_NONE_ENV means we are not doing any RLS now, but that may change
-	 * with changes to the environment, so we mark it as hasRowSecurity to
-	 * force a re-plan when the environment changes.
+	 * with changes to the environment, so we mark it as having row-security
+	 * to force a re-plan when the environment changes.
 	 */
 	if (rls_status == RLS_NONE_ENV)
 	{
@@ -145,7 +150,7 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index,
 		 * replanned if the environment changes (GUCs, role), but we are not
 		 * adding anything here.
 		 */
-		*hasRowSecurity = true;
+		SetRowSecurityForCurrentNestLevel();
 
 		return;
 	}
@@ -526,7 +531,7 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index,
 	 * Mark this query as having row security, so plancache can invalidate it
 	 * when necessary (eg: role changes)
 	 */
-	*hasRowSecurity = true;
+	SetRowSecurityForCurrentNestLevel();
 }
 
 /*
@@ -930,3 +935,72 @@ check_role_for_policy(ArrayType *policy_roles, Oid user_id)
 
 	return false;
 }
+
+/*
+ * The following functions are used to track whether any calls to
+ * get_row_security_policies() set hasRowSecurity between two points of code.
+ * This can be used to ensure that a Query is correctly marked as having
+ * row-level security dependencies without needing callers to preserve it in
+ * all cases.
+ *
+ * Before any potential calls to get_row_security_policies() that you care
+ * about, call PushRowSecurityNestLevel() to register your current level of
+ * recursion.  Then, once any potential calls to get_row_security_policies()
+ * are done, call PopRowSecurityNestLevel() and save its result in your Query
+ * object's hasRowSecurity flag.
+ *
+ * The value returned by PushRowSecurityNestLevel() should be passed to the
+ * matching call to PopRowSecurityNestLevel().  This is used to verify that we
+ * aren't stranding any nest levels or popping extra ones inadvertently.
+ */
+
+int
+PushRowSecurityNestLevel(void)
+{
+	return ++row_sec_nest_level;
+}
+
+bool
+PopRowSecurityNestLevel(bool discard, int nest_level)
+{
+	bool		ret = bms_is_member(row_sec_nest_level, row_sec_bms);
+
+	if (unlikely(nest_level != row_sec_nest_level))
+		elog(ERROR, "row security nest level mismatch");
+
+	if (ret)
+	{
+		MemoryContext oldcontext = MemoryContextSwitchTo(CurTransactionContext);
+
+		if (!discard && row_sec_nest_level > 0)
+			row_sec_bms = bms_add_member(row_sec_bms, row_sec_nest_level - 1);
+		row_sec_bms = bms_del_member(row_sec_bms, row_sec_nest_level);
+
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	row_sec_nest_level--;
+	return ret;
+}
+
+void
+AtEOXact_RowSecurity(bool isCommit)
+{
+	Assert(!isCommit || row_sec_nest_level == -1);
+
+	row_sec_bms = NULL;
+	row_sec_nest_level = -1;
+}
+
+static void
+SetRowSecurityForCurrentNestLevel(void)
+{
+	MemoryContext oldcontext = MemoryContextSwitchTo(CurTransactionContext);
+
+	if (unlikely(row_sec_nest_level < 0))
+		elog(ERROR, "no row security nest level was pushed");
+
+	row_sec_bms = bms_add_member(row_sec_bms, row_sec_nest_level);
+
+	MemoryContextSwitchTo(oldcontext);
+}
diff --git a/src/include/rewrite/rowsecurity.h b/src/include/rewrite/rowsecurity.h
index 1717ed4e5e..023f7f4091 100644
--- a/src/include/rewrite/rowsecurity.h
+++ b/src/include/rewrite/rowsecurity.h
@@ -44,6 +44,10 @@ extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook_restri
 extern void get_row_security_policies(Query *root,
 									  RangeTblEntry *rte, int rt_index,
 									  List **securityQuals, List **withCheckOptions,
-									  bool *hasRowSecurity, bool *hasSubLinks);
+									  bool *hasSubLinks);
+
+extern int	PushRowSecurityNestLevel(void);
+extern bool PopRowSecurityNestLevel(bool discard, int nest_level);
+extern void AtEOXact_RowSecurity(bool isCommit);
 
 #endif							/* ROWSECURITY_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 08521d51a9..d7fd8507ba 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3475,7 +3475,6 @@ fill_string_relopt
 finalize_primnode_context
 find_dependent_phvs_context
 find_expr_references_context
-fireRIRonSubLink_context
 fix_join_expr_context
 fix_scan_expr_context
 fix_upper_expr_context
-- 
2.39.5 (Apple Git-154)

#2Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Nathan Bossart (#1)
Re: revamp row-security tracking

On Thu, 21 Nov 2024 at 18:00, Nathan Bossart <nathandbossart@gmail.com> wrote:

In light of CVE-2024-10976, which was fixed by commit cd7ab57, I'd like to
propose a bigger change to this area of the code that aims to future-proof
it a bit. Instead of requiring hackers to carefully cart around whether a
query references a table with RLS enabled, I think we should instead
accumulate such information globally and require higher-level routines like
fireRIRrules() and inline_set_returning_function() to inspect it as needed.

The attached patch accomplishes this by establishing a global queue of
row-security "nest levels" that the aforementioned higher-level callers can
use.

I'm not convinced that this is an improvement.

The code in check_sql_fn_retval() is building a Query struct from
scratch, so it seems perfectly natural for it to be responsible for
setting all the required fields, based on the information it has
available. With this patch, check_sql_fn_retval() is returning a
potentially incorrectly marked Query at the end of the querytree list,
which the caller is responsible for fixing up, which doesn't seem
ideal.

I'm also not a fan of using global variables in this way, or the
resulting need to hook into the transaction management system to tidy
up. The end result is that the places where the flag is set are moved
further away from where RLS policies are applied, which IMO makes the
code much harder to follow.

There is exactly one place where RLS policies are applied, and it
seems much more natural for it to have responsibility for setting this
flag. I think that a slightly neater way for it to handle that would
be to modify fireRIRrules(), adding an extra parameter "bool
*hasRowSecurity" that it would set to true if RLS is enabled for the
query it is rewriting. Doing that forces all callers to think about
whether or not that affects some outer query. For example,
ApplyRetrieveRule() would then do:

rule_action = fireRIRrules(rule_action, activeRIRs,
&parsetree->hasRowSecurity);

rather than having a separate second step to update the flag on
"parsetree", and similarly for fireRIRrules()'s recursive calls to
itself. If, in the future, it becomes necessary to invoke
fireRIRrules() on more parts of a Query, it's then much more likely
that the new code won't forget to update the parent query's flag.

Regards,
Dean

#3Nathan Bossart
nathandbossart@gmail.com
In reply to: Dean Rasheed (#2)
1 attachment(s)
Re: revamp row-security tracking

On Fri, Nov 29, 2024 at 10:01:41AM +0000, Dean Rasheed wrote:

On Thu, 21 Nov 2024 at 18:00, Nathan Bossart <nathandbossart@gmail.com> wrote:

The attached patch accomplishes this by establishing a global queue of
row-security "nest levels" that the aforementioned higher-level callers can
use.

I'm not convinced that this is an improvement.

Thanks for reviewing.

The code in check_sql_fn_retval() is building a Query struct from
scratch, so it seems perfectly natural for it to be responsible for
setting all the required fields, based on the information it has
available. With this patch, check_sql_fn_retval() is returning a
potentially incorrectly marked Query at the end of the querytree list,
which the caller is responsible for fixing up, which doesn't seem
ideal.

While it is indeed natural for the code that builds a Query to be
responsible for setting it correctly, unfortunately there's no backstop if
someone forgets to do so (as was the case in the recent CVE). I don't
think my v1 patch would necessarily prevent all such problems, but I do
think it would help prevent some.

There is exactly one place where RLS policies are applied, and it
seems much more natural for it to have responsibility for setting this
flag. I think that a slightly neater way for it to handle that would
be to modify fireRIRrules(), adding an extra parameter "bool
*hasRowSecurity" that it would set to true if RLS is enabled for the
query it is rewriting. Doing that forces all callers to think about
whether or not that affects some outer query. For example,
ApplyRetrieveRule() would then do:

rule_action = fireRIRrules(rule_action, activeRIRs,
&parsetree->hasRowSecurity);

rather than having a separate second step to update the flag on
"parsetree", and similarly for fireRIRrules()'s recursive calls to
itself. If, in the future, it becomes necessary to invoke
fireRIRrules() on more parts of a Query, it's then much more likely
that the new code won't forget to update the parent query's flag.

I've attempted this in the attached v2 patch. I do think this is an
improvement over the status quo, but I worry that it doesn't go far enough.

--
nathan

Attachments:

v2-0001-revamp-row-security-tracking.patchtext/plain; charset=us-asciiDownload
From 0c81e04c3e62cd178dff9fc5b5f671e41fd39f28 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 2 Dec 2024 10:24:49 -0600
Subject: [PATCH v2 1/1] revamp row security tracking

---
 src/backend/rewrite/rewriteHandler.c | 42 +++++++++++++++++-----------
 1 file changed, 25 insertions(+), 17 deletions(-)

diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index ab2e2cd647..c9ad429703 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -94,7 +94,7 @@ static void markQueryForLocking(Query *qry, Node *jtnode,
 								bool pushedDown);
 static List *matchLocks(CmdType event, Relation relation,
 						int varno, Query *parsetree, bool *hasUpdate);
-static Query *fireRIRrules(Query *parsetree, List *activeRIRs);
+static Query *fireRIRrules(Query *parsetree, List *activeRIRs, bool *hasRowSecurity);
 static Bitmapset *adjust_view_column_set(Bitmapset *cols, List *targetlist);
 
 
@@ -1730,6 +1730,7 @@ ApplyRetrieveRule(Query *parsetree,
 	RangeTblEntry *rte;
 	RowMarkClause *rc;
 	int			numCols;
+	bool		hasRowSecurity = false;
 
 	if (list_length(rule->actions) != 1)
 		elog(ERROR, "expected just one rule action");
@@ -1843,13 +1844,13 @@ ApplyRetrieveRule(Query *parsetree,
 	/*
 	 * Recursively expand any view references inside the view.
 	 */
-	rule_action = fireRIRrules(rule_action, activeRIRs);
+	rule_action = fireRIRrules(rule_action, activeRIRs, &hasRowSecurity);
 
 	/*
 	 * Make sure the query is marked as having row security if the view query
 	 * does.
 	 */
-	parsetree->hasRowSecurity |= rule_action->hasRowSecurity;
+	parsetree->hasRowSecurity |= hasRowSecurity;
 
 	/*
 	 * Now, plug the view query in as a subselect, converting the relation's
@@ -1971,15 +1972,17 @@ fireRIRonSubLink(Node *node, fireRIRonSubLink_context *context)
 	if (IsA(node, SubLink))
 	{
 		SubLink    *sub = (SubLink *) node;
+		bool		hasRowSecurity = false;
 
 		/* Do what we came for */
 		sub->subselect = (Node *) fireRIRrules((Query *) sub->subselect,
-											   context->activeRIRs);
+											   context->activeRIRs,
+											   &hasRowSecurity);
 
 		/*
 		 * Remember if any of the sublinks have row security.
 		 */
-		context->hasRowSecurity |= ((Query *) sub->subselect)->hasRowSecurity;
+		context->hasRowSecurity |= hasRowSecurity;
 
 		/* Fall through to process lefthand args of SubLink */
 	}
@@ -2000,7 +2003,7 @@ fireRIRonSubLink(Node *node, fireRIRonSubLink_context *context)
  * rules for, used to detect/reject recursion.
  */
 static Query *
-fireRIRrules(Query *parsetree, List *activeRIRs)
+fireRIRrules(Query *parsetree, List *activeRIRs, bool *hasRowSecurity)
 {
 	int			origResultRelation = parsetree->resultRelation;
 	int			rt_index;
@@ -2048,13 +2051,15 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 		 */
 		if (rte->rtekind == RTE_SUBQUERY)
 		{
-			rte->subquery = fireRIRrules(rte->subquery, activeRIRs);
+			bool		hasRowSecurity_internal = false;
+
+			rte->subquery = fireRIRrules(rte->subquery, activeRIRs, &hasRowSecurity_internal);
 
 			/*
 			 * While we are here, make sure the query is marked as having row
 			 * security if any of its subqueries do.
 			 */
-			parsetree->hasRowSecurity |= rte->subquery->hasRowSecurity;
+			parsetree->hasRowSecurity |= hasRowSecurity_internal;
 
 			continue;
 		}
@@ -2166,15 +2171,16 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 	foreach(lc, parsetree->cteList)
 	{
 		CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
+		bool		hasRowSecurity_internal = false;
 
 		cte->ctequery = (Node *)
-			fireRIRrules((Query *) cte->ctequery, activeRIRs);
+			fireRIRrules((Query *) cte->ctequery, activeRIRs, &hasRowSecurity_internal);
 
 		/*
 		 * While we are here, make sure the query is marked as having row
 		 * security if any of its CTEs do.
 		 */
-		parsetree->hasRowSecurity |= ((Query *) cte->ctequery)->hasRowSecurity;
+		parsetree->hasRowSecurity |= hasRowSecurity_internal;
 	}
 
 	/*
@@ -2211,7 +2217,7 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 		Relation	rel;
 		List	   *securityQuals;
 		List	   *withCheckOptions;
-		bool		hasRowSecurity;
+		bool		hasRowSecurity_internal = false;
 		bool		hasSubLinks;
 
 		++rt_index;
@@ -2229,7 +2235,7 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 		 */
 		get_row_security_policies(parsetree, rte, rt_index,
 								  &securityQuals, &withCheckOptions,
-								  &hasRowSecurity, &hasSubLinks);
+								  &hasRowSecurity_internal, &hasSubLinks);
 
 		if (securityQuals != NIL || withCheckOptions != NIL)
 		{
@@ -2278,10 +2284,10 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 
 				/*
 				 * We can ignore the value of fire_context.hasRowSecurity
-				 * since we only reach this code in cases where hasRowSecurity
-				 * is already true.
+				 * since we only reach this code in cases where
+				 * hasRowSecurity_internal is already true.
 				 */
-				Assert(hasRowSecurity);
+				Assert(hasRowSecurity_internal);
 
 				activeRIRs = list_delete_last(activeRIRs);
 			}
@@ -2303,7 +2309,7 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 		 * Make sure the query is marked correctly if row-level security
 		 * applies, or if the new quals had sublinks.
 		 */
-		if (hasRowSecurity)
+		if (hasRowSecurity_internal)
 			parsetree->hasRowSecurity = true;
 		if (hasSubLinks)
 			parsetree->hasSubLinks = true;
@@ -2311,6 +2317,7 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 		table_close(rel, NoLock);
 	}
 
+	*hasRowSecurity = parsetree->hasRowSecurity;
 	return parsetree;
 }
 
@@ -4463,8 +4470,9 @@ QueryRewrite(Query *parsetree)
 	foreach(l, querylist)
 	{
 		Query	   *query = (Query *) lfirst(l);
+		bool		hasRowSecurity pg_attribute_unused() = false;
 
-		query = fireRIRrules(query, NIL);
+		query = fireRIRrules(query, NIL, &hasRowSecurity);
 
 		query->queryId = input_query_id;
 
-- 
2.39.5 (Apple Git-154)

#4Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#3)
Re: revamp row-security tracking

Given there doesn't seem to be a huge amount of interest in this, I plan to
mark it as Withdrawn soon.

--
nathan

#5Tom Lane
tgl@sss.pgh.pa.us
In reply to: Nathan Bossart (#4)
Re: revamp row-security tracking

Nathan Bossart <nathandbossart@gmail.com> writes:

Given there doesn't seem to be a huge amount of interest in this, I plan to
mark it as Withdrawn soon.

I think you're being too impatient. It's still an interesting
topic, it just needs more thought to get to something committable.

I find this has-row-security marking problem to be comparable
to the has-sublinks marking problem. We've had tons of
bugs-of-omission with that too, and the present code feels
ugly and not any less prone to omissions than it ever was.

I wonder whether considering both problems together would yield any
insights, following Polya's dictum that "the more general problem may
be easier to solve".

One straightforward idea is to just not do the marking at all,
but rather require places that want to know these properties
to do a fresh search of the query tree when they want to know
it. That obviously has performance questions to answer, but
it's easier to give answers to performance questions than
"is this correct" questions.

regards, tom lane

#6Nathan Bossart
nathandbossart@gmail.com
In reply to: Tom Lane (#5)
Re: revamp row-security tracking

On Mon, Feb 17, 2025 at 01:08:29PM -0500, Tom Lane wrote:

I think you're being too impatient. It's still an interesting
topic, it just needs more thought to get to something committable.

Maybe I am. Thanks for chiming in.

I find this has-row-security marking problem to be comparable
to the has-sublinks marking problem. We've had tons of
bugs-of-omission with that too, and the present code feels
ugly and not any less prone to omissions than it ever was.

I wonder whether considering both problems together would yield any
insights, following Polya's dictum that "the more general problem may
be easier to solve".

One straightforward idea is to just not do the marking at all,
but rather require places that want to know these properties
to do a fresh search of the query tree when they want to know
it. That obviously has performance questions to answer, but
it's easier to give answers to performance questions than
"is this correct" questions.

That could be worth a try. The reason I started with the global queue idea
was that we seem to reliably discover relations with RLS enabled, we just
tend to miss propagating that information to the top-level query. We could
invent a separate query tree walker for discovering RLS, etc. to keep it
centralized. However, besides the performance questions, it would be
another separate piece of code to keep updated. Perhaps another variation
on this idea is to create a query walker that just looks for hasRowSecurity
flags throughout the tree (and to use that to mark the plan cache entry
appropriately).

--
nathan

#7Tom Lane
tgl@sss.pgh.pa.us
In reply to: Nathan Bossart (#6)
Re: revamp row-security tracking

Nathan Bossart <nathandbossart@gmail.com> writes:

Perhaps another variation
on this idea is to create a query walker that just looks for hasRowSecurity
flags throughout the tree (and to use that to mark the plan cache entry
appropriately).

That seems like a pretty plausible compromise position. So we'd
redefine Query.hasRowSecurity as summarizing the situation for only
the Query's own rtable entries, not recursively for sub-Queries.

regards, tom lane