A while ago [1] I proposed an enhancement to the way qual pushdown
safety is decided in RLS / security barrier views. Currently we just
test for the presence of leaky functions in the qual, but it is
possible to do better than that, by further testing if the leaky
function is actually being passed information that we don't want to be
leaked.

Attached is a patch that does that, allowing restriction clause quals
to be pushed down into subquery RTEs if they contain leaky functions,
provided that the arglists of those leaky functions contain no Vars
(such Vars would necessarily refer to output columns of the subquery,
which is the data that must not be leaked).

An example of the sort of query this will help optimise is:

SELECT * FROM table_with_rls WHERE created > now() - '1 hour'::interval;

where currently this qual cannot be pushed down because neither now()
nor timestamptz_mi_interval() are leakproof, but since they are not
being passed any data from the table, they can't actually leak
anything, so the qual can be safely pushed down, allowing indexes to
be used if available.

In fact the majority of builtin functions aren't marked leakproof, and
probably most user functions aren't either, so this could potentially
be useful in a wide range of real-world queries, where it is common to
write quals of the form <column> <operator> <expression>, and the
expression may contain leaky functions.

Regards,
Dean

[1] 
http://www.postgresql.org/message-id/caezatcwkcpfwilocnmfmszklgoabz7axkmgz-mk83gd3jg8...@mail.gmail.com
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
new file mode 100644
index 58d78e6..4433468
*** a/src/backend/optimizer/path/allpaths.c
--- b/src/backend/optimizer/path/allpaths.c
*************** targetIsInAllPartitionLists(TargetEntry
*** 1982,1988 ****
   * 2. If unsafeVolatile is set, the qual must not contain any volatile
   * functions.
   *
!  * 3. If unsafeLeaky is set, the qual must not contain any leaky functions.
   *
   * 4. The qual must not refer to the whole-row output of the subquery
   * (since there is no easy way to name that within the subquery itself).
--- 1982,1989 ----
   * 2. If unsafeVolatile is set, the qual must not contain any volatile
   * functions.
   *
!  * 3. If unsafeLeaky is set, the qual must not contain any leaky functions
!  * that might reveal values from the subquery as side effects.
   *
   * 4. The qual must not refer to the whole-row output of the subquery
   * (since there is no easy way to name that within the subquery itself).
*************** qual_is_pushdown_safe(Query *subquery, I
*** 2009,2015 ****
  
  	/* Refuse leaky quals if told to (point 3) */
  	if (safetyInfo->unsafeLeaky &&
! 		contain_leaky_functions(qual))
  		return false;
  
  	/*
--- 2010,2016 ----
  
  	/* Refuse leaky quals if told to (point 3) */
  	if (safetyInfo->unsafeLeaky &&
! 		contain_leaked_vars(qual))
  		return false;
  
  	/*
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
new file mode 100644
index b340b01..7f82d54
*** a/src/backend/optimizer/util/clauses.c
--- b/src/backend/optimizer/util/clauses.c
*************** static bool contain_mutable_functions_wa
*** 97,103 ****
  static bool contain_volatile_functions_walker(Node *node, void *context);
  static bool contain_volatile_functions_not_nextval_walker(Node *node, void *context);
  static bool contain_nonstrict_functions_walker(Node *node, void *context);
! static bool contain_leaky_functions_walker(Node *node, void *context);
  static Relids find_nonnullable_rels_walker(Node *node, bool top_level);
  static List *find_nonnullable_vars_walker(Node *node, bool top_level);
  static bool is_strict_saop(ScalarArrayOpExpr *expr, bool falseOK);
--- 97,103 ----
  static bool contain_volatile_functions_walker(Node *node, void *context);
  static bool contain_volatile_functions_not_nextval_walker(Node *node, void *context);
  static bool contain_nonstrict_functions_walker(Node *node, void *context);
! static bool contain_leaked_vars_walker(Node *node, void *context);
  static Relids find_nonnullable_rels_walker(Node *node, bool top_level);
  static List *find_nonnullable_vars_walker(Node *node, bool top_level);
  static bool is_strict_saop(ScalarArrayOpExpr *expr, bool falseOK);
*************** contain_nonstrict_functions_walker(Node
*** 1318,1343 ****
  }
  
  /*****************************************************************************
!  *		  Check clauses for non-leakproof functions
   *****************************************************************************/
  
  /*
!  * contain_leaky_functions
!  *		Recursively search for leaky functions within a clause.
   *
!  * Returns true if any function call with side-effect may be present in the
!  * clause.  Qualifiers from outside the a security_barrier view should not
!  * be pushed down into the view, lest the contents of tuples intended to be
!  * filtered out be revealed via side effects.
   */
  bool
! contain_leaky_functions(Node *clause)
  {
! 	return contain_leaky_functions_walker(clause, NULL);
  }
  
  static bool
! contain_leaky_functions_walker(Node *node, void *context)
  {
  	if (node == NULL)
  		return false;
--- 1318,1347 ----
  }
  
  /*****************************************************************************
!  *		  Check clauses for non-leakproof functions that might leak Vars
   *****************************************************************************/
  
  /*
!  * contain_leaked_vars
!  *		Recursively scan a clause to discover whether it contains any Var
!  *		nodes (of the current query level) that are passed as arguments to
!  *		leaky functions.
   *
!  * Returns true if any function call with side effects may be present in the
!  * clause and that function's arguments contain any Var nodes of the current
!  * query level.  Qualifiers from outside a security_barrier view that leak
!  * Var nodes in this way should not be pushed down into the view, lest the
!  * contents of tuples intended to be filtered out be revealed via the side
!  * effects.
   */
  bool
! contain_leaked_vars(Node *clause)
  {
! 	return contain_leaked_vars_walker(clause, NULL);
  }
  
  static bool
! contain_leaked_vars_walker(Node *node, void *context)
  {
  	if (node == NULL)
  		return false;
*************** contain_leaky_functions_walker(Node *nod
*** 1369,1375 ****
  			{
  				FuncExpr   *expr = (FuncExpr *) node;
  
! 				if (!get_func_leakproof(expr->funcid))
  					return true;
  			}
  			break;
--- 1373,1380 ----
  			{
  				FuncExpr   *expr = (FuncExpr *) node;
  
! 				if (!get_func_leakproof(expr->funcid) &&
! 					contain_var_clause((Node *) expr->args))
  					return true;
  			}
  			break;
*************** contain_leaky_functions_walker(Node *nod
*** 1381,1387 ****
  				OpExpr	   *expr = (OpExpr *) node;
  
  				set_opfuncid(expr);
! 				if (!get_func_leakproof(expr->opfuncid))
  					return true;
  			}
  			break;
--- 1386,1393 ----
  				OpExpr	   *expr = (OpExpr *) node;
  
  				set_opfuncid(expr);
! 				if (!get_func_leakproof(expr->opfuncid) &&
! 					contain_var_clause((Node *) expr->args))
  					return true;
  			}
  			break;
*************** contain_leaky_functions_walker(Node *nod
*** 1391,1397 ****
  				ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) node;
  
  				set_sa_opfuncid(expr);
! 				if (!get_func_leakproof(expr->opfuncid))
  					return true;
  			}
  			break;
--- 1397,1404 ----
  				ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) node;
  
  				set_sa_opfuncid(expr);
! 				if (!get_func_leakproof(expr->opfuncid) &&
! 					contain_var_clause((Node *) expr->args))
  					return true;
  			}
  			break;
*************** contain_leaky_functions_walker(Node *nod
*** 1401,1415 ****
  				CoerceViaIO *expr = (CoerceViaIO *) node;
  				Oid			funcid;
  				Oid			ioparam;
  				bool		varlena;
  
  				getTypeInputInfo(exprType((Node *) expr->arg),
  								 &funcid, &ioparam);
! 				if (!get_func_leakproof(funcid))
! 					return true;
  
! 				getTypeOutputInfo(expr->resulttype, &funcid, &varlena);
! 				if (!get_func_leakproof(funcid))
  					return true;
  			}
  			break;
--- 1408,1428 ----
  				CoerceViaIO *expr = (CoerceViaIO *) node;
  				Oid			funcid;
  				Oid			ioparam;
+ 				bool		leaky;
  				bool		varlena;
  
  				getTypeInputInfo(exprType((Node *) expr->arg),
  								 &funcid, &ioparam);
! 				leaky = !get_func_leakproof(funcid);
  
! 				if (!leaky)
! 				{
! 					getTypeOutputInfo(expr->resulttype, &funcid, &varlena);
! 					leaky = !get_func_leakproof(funcid);
! 				}
! 
! 				if (leaky &&
! 					contain_var_clause((Node *) expr->arg))
  					return true;
  			}
  			break;
*************** contain_leaky_functions_walker(Node *nod
*** 1419,1432 ****
  				ArrayCoerceExpr *expr = (ArrayCoerceExpr *) node;
  				Oid			funcid;
  				Oid			ioparam;
  				bool		varlena;
  
  				getTypeInputInfo(exprType((Node *) expr->arg),
  								 &funcid, &ioparam);
! 				if (!get_func_leakproof(funcid))
! 					return true;
! 				getTypeOutputInfo(expr->resulttype, &funcid, &varlena);
! 				if (!get_func_leakproof(funcid))
  					return true;
  			}
  			break;
--- 1432,1452 ----
  				ArrayCoerceExpr *expr = (ArrayCoerceExpr *) node;
  				Oid			funcid;
  				Oid			ioparam;
+ 				bool		leaky;
  				bool		varlena;
  
  				getTypeInputInfo(exprType((Node *) expr->arg),
  								 &funcid, &ioparam);
! 				leaky = !get_func_leakproof(funcid);
! 
! 				if (!leaky)
! 				{
! 					getTypeOutputInfo(expr->resulttype, &funcid, &varlena);
! 					leaky = !get_func_leakproof(funcid);
! 				}
! 
! 				if (leaky &&
! 					contain_var_clause((Node *) expr->arg))
  					return true;
  			}
  			break;
*************** contain_leaky_functions_walker(Node *nod
*** 1435,1446 ****
  			{
  				RowCompareExpr *rcexpr = (RowCompareExpr *) node;
  				ListCell   *opid;
  
! 				foreach(opid, rcexpr->opnos)
  				{
  					Oid			funcid = get_opcode(lfirst_oid(opid));
  
! 					if (!get_func_leakproof(funcid))
  						return true;
  				}
  			}
--- 1455,1472 ----
  			{
  				RowCompareExpr *rcexpr = (RowCompareExpr *) node;
  				ListCell   *opid;
+ 				ListCell   *larg;
+ 				ListCell   *rarg;
  
! 				forthree(opid, rcexpr->opnos,
! 						 larg, rcexpr->largs,
! 						 rarg, rcexpr->rargs)
  				{
  					Oid			funcid = get_opcode(lfirst_oid(opid));
  
! 					if (!get_func_leakproof(funcid) &&
! 						(contain_var_clause((Node *) lfirst(larg)) ||
! 						 contain_var_clause((Node *) lfirst(rarg))))
  						return true;
  				}
  			}
*************** contain_leaky_functions_walker(Node *nod
*** 1455,1461 ****
  			 */
  			return true;
  	}
! 	return expression_tree_walker(node, contain_leaky_functions_walker,
  								  context);
  }
  
--- 1481,1487 ----
  			 */
  			return true;
  	}
! 	return expression_tree_walker(node, contain_leaked_vars_walker,
  								  context);
  }
  
diff --git a/src/include/optimizer/clauses.h b/src/include/optimizer/clauses.h
new file mode 100644
index 160045c..3d04ac2
*** a/src/include/optimizer/clauses.h
--- b/src/include/optimizer/clauses.h
*************** extern bool contain_mutable_functions(No
*** 63,69 ****
  extern bool contain_volatile_functions(Node *clause);
  extern bool contain_volatile_functions_not_nextval(Node *clause);
  extern bool contain_nonstrict_functions(Node *clause);
! extern bool contain_leaky_functions(Node *clause);
  
  extern Relids find_nonnullable_rels(Node *clause);
  extern List *find_nonnullable_vars(Node *clause);
--- 63,69 ----
  extern bool contain_volatile_functions(Node *clause);
  extern bool contain_volatile_functions_not_nextval(Node *clause);
  extern bool contain_nonstrict_functions(Node *clause);
! extern bool contain_leaked_vars(Node *clause);
  
  extern Relids find_nonnullable_rels(Node *clause);
  extern List *find_nonnullable_vars(Node *clause);
diff --git a/src/test/regress/expected/select_views.out b/src/test/regress/expected/select_views.out
new file mode 100644
index 82d510d..7f57526
*** a/src/test/regress/expected/select_views.out
--- b/src/test/regress/expected/select_views.out
*************** EXPLAIN (COSTS OFF) SELECT * FROM my_pro
*** 1349,1354 ****
--- 1349,1400 ----
  (4 rows)
  
  --
+ -- scenario: qualifiers can be pushed down if they contain leaky functions,
+ --           provided they aren't passed data from inside the view.
+ --
+ SELECT * FROM my_property_normal v
+ 		WHERE f_leak('passwd') AND f_leak(passwd);
+ NOTICE:  f_leak => passwd
+ NOTICE:  f_leak => passwd123
+ NOTICE:  f_leak => passwd
+ NOTICE:  f_leak => beafsteak
+ NOTICE:  f_leak => passwd
+ NOTICE:  f_leak => hamburger
+  cid |     name      |       tel        |  passwd   
+ -----+---------------+------------------+-----------
+  101 | regress_alice | +81-12-3456-7890 | passwd123
+ (1 row)
+ 
+ EXPLAIN (COSTS OFF) SELECT * FROM my_property_normal v
+ 		WHERE f_leak('passwd') AND f_leak(passwd);
+                                          QUERY PLAN                                          
+ ---------------------------------------------------------------------------------------------
+  Seq Scan on customer
+    Filter: (f_leak('passwd'::text) AND f_leak(passwd) AND (name = ("current_user"())::text))
+ (2 rows)
+ 
+ SELECT * FROM my_property_secure v
+ 		WHERE f_leak('passwd') AND f_leak(passwd);
+ NOTICE:  f_leak => passwd
+ NOTICE:  f_leak => passwd123
+ NOTICE:  f_leak => passwd
+ NOTICE:  f_leak => passwd
+  cid |     name      |       tel        |  passwd   
+ -----+---------------+------------------+-----------
+  101 | regress_alice | +81-12-3456-7890 | passwd123
+ (1 row)
+ 
+ EXPLAIN (COSTS OFF) SELECT * FROM my_property_secure v
+ 		WHERE f_leak('passwd') AND f_leak(passwd);
+                                    QUERY PLAN                                   
+ --------------------------------------------------------------------------------
+  Subquery Scan on v
+    Filter: f_leak(v.passwd)
+    ->  Seq Scan on customer
+          Filter: (f_leak('passwd'::text) AND (name = ("current_user"())::text))
+ (4 rows)
+ 
+ --
  -- scenario: if a qualifier references only one-side of a particular join-
  --           tree, it shall be distributed to the most deep scan plan as
  --           possible as we can.
diff --git a/src/test/regress/expected/select_views_1.out b/src/test/regress/expected/select_views_1.out
new file mode 100644
index ce22bfa..5275ef0
*** a/src/test/regress/expected/select_views_1.out
--- b/src/test/regress/expected/select_views_1.out
*************** EXPLAIN (COSTS OFF) SELECT * FROM my_pro
*** 1349,1354 ****
--- 1349,1400 ----
  (4 rows)
  
  --
+ -- scenario: qualifiers can be pushed down if they contain leaky functions,
+ --           provided they aren't passed data from inside the view.
+ --
+ SELECT * FROM my_property_normal v
+ 		WHERE f_leak('passwd') AND f_leak(passwd);
+ NOTICE:  f_leak => passwd
+ NOTICE:  f_leak => passwd123
+ NOTICE:  f_leak => passwd
+ NOTICE:  f_leak => beafsteak
+ NOTICE:  f_leak => passwd
+ NOTICE:  f_leak => hamburger
+  cid |     name      |       tel        |  passwd   
+ -----+---------------+------------------+-----------
+  101 | regress_alice | +81-12-3456-7890 | passwd123
+ (1 row)
+ 
+ EXPLAIN (COSTS OFF) SELECT * FROM my_property_normal v
+ 		WHERE f_leak('passwd') AND f_leak(passwd);
+                                          QUERY PLAN                                          
+ ---------------------------------------------------------------------------------------------
+  Seq Scan on customer
+    Filter: (f_leak('passwd'::text) AND f_leak(passwd) AND (name = ("current_user"())::text))
+ (2 rows)
+ 
+ SELECT * FROM my_property_secure v
+ 		WHERE f_leak('passwd') AND f_leak(passwd);
+ NOTICE:  f_leak => passwd
+ NOTICE:  f_leak => passwd123
+ NOTICE:  f_leak => passwd
+ NOTICE:  f_leak => passwd
+  cid |     name      |       tel        |  passwd   
+ -----+---------------+------------------+-----------
+  101 | regress_alice | +81-12-3456-7890 | passwd123
+ (1 row)
+ 
+ EXPLAIN (COSTS OFF) SELECT * FROM my_property_secure v
+ 		WHERE f_leak('passwd') AND f_leak(passwd);
+                                    QUERY PLAN                                   
+ --------------------------------------------------------------------------------
+  Subquery Scan on v
+    Filter: f_leak(v.passwd)
+    ->  Seq Scan on customer
+          Filter: (f_leak('passwd'::text) AND (name = ("current_user"())::text))
+ (4 rows)
+ 
+ --
  -- scenario: if a qualifier references only one-side of a particular join-
  --           tree, it shall be distributed to the most deep scan plan as
  --           possible as we can.
diff --git a/src/test/regress/sql/select_views.sql b/src/test/regress/sql/select_views.sql
new file mode 100644
index da35623..3b74ab9
*** a/src/test/regress/sql/select_views.sql
--- b/src/test/regress/sql/select_views.sql
*************** SELECT * FROM my_property_secure WHERE f
*** 96,101 ****
--- 96,115 ----
  EXPLAIN (COSTS OFF) SELECT * FROM my_property_secure WHERE f_leak(passwd);
  
  --
+ -- scenario: qualifiers can be pushed down if they contain leaky functions,
+ --           provided they aren't passed data from inside the view.
+ --
+ SELECT * FROM my_property_normal v
+ 		WHERE f_leak('passwd') AND f_leak(passwd);
+ EXPLAIN (COSTS OFF) SELECT * FROM my_property_normal v
+ 		WHERE f_leak('passwd') AND f_leak(passwd);
+ 
+ SELECT * FROM my_property_secure v
+ 		WHERE f_leak('passwd') AND f_leak(passwd);
+ EXPLAIN (COSTS OFF) SELECT * FROM my_property_secure v
+ 		WHERE f_leak('passwd') AND f_leak(passwd);
+ 
+ --
  -- scenario: if a qualifier references only one-side of a particular join-
  --           tree, it shall be distributed to the most deep scan plan as
  --           possible as we can.
-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to