Re: run GUC check hooks on RESET

Started by Robert Haasalmost 14 years ago8 messages
#1Robert Haas
robertmhaas@gmail.com

On Sat, Feb 11, 2012 at 1:36 PM, Kevin Grittner
<Kevin.Grittner@wicourts.gov> wrote:

"Kevin Grittner"  wrote:
Tom Lane wrote:

I agree it's a bug that you can do what Kevin's example shows.

I'll look at it and see if I can pull together a patch.

Attached.

Basically, if a GUC has a check function, this patch causes it to be
run on a RESET just like it is on a SET, to make sure that the
resulting value is valid to set within the context.  Some messages
needed adjustment.  While I was there, I made cod a little more
consistent among related GUCs.

I also added a little to the regression tests to cover this.

This patch makes me a little nervous, because the existing behavior
seems to have been coded for quite deliberately. Sadly, I don't see
any comments explaining why the RESET case was excluded originally.
On the other hand, I can't see what it would break, either. Have you
gone through all the check hooks and verified that we're not violating
any of their assumptions?

I assume that you're thinking we'd only fix this in master?

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#2Kevin Grittner
Kevin.Grittner@wicourts.gov
In reply to: Robert Haas (#1)

Robert Haas <robertmhaas@gmail.com> wrote:

On Sat, Feb 11, 2012 at 1:36 PM, Kevin Grittner
<Kevin.Grittner@wicourts.gov> wrote:

"Kevin Grittner" wrote:
Tom Lane wrote:

I agree it's a bug that you can do what Kevin's example shows.

I'll look at it and see if I can pull together a patch.

Attached.

Basically, if a GUC has a check function, this patch causes it to
be run on a RESET just like it is on a SET, to make sure that the
resulting value is valid to set within the context. Some
messages needed adjustment. While I was there, I made cod a
little more consistent among related GUCs.

I also added a little to the regression tests to cover this.

This patch makes me a little nervous, because the existing
behavior seems to have been coded for quite deliberately.

It does, although I'm not clear *why* it was. I suspect it may have
been based on an assumption that whatever value is in the reset_val
field had to have been already determined to be good, so it was a
waste of cycles to check it again -- without considering that the
validity of making a change might depend on context.

Sadly, I don't see any comments explaining why the RESET case was
excluded originally.

That is unfortunate. I guess it points out the value of adding a
comment to point out why we would want to check these values even on
a reset to a previously-used value.

On the other hand, I can't see what it would break, either. Have
you gone through all the check hooks and verified that we're not
violating any of their assumptions?

I studied the code enough to be convinced that the patch as it
stands can't break a check hook which only validates the value
and/or changes the value to a canonical form. There appear to be 34
check hooks, and I reviewed the 10 in the variable.c file, although
not at great depth. I could set aside some time this weekend to
look at all of them, in depth, if you think that is warranted. I do
think that a check hook would have to be doing something which is
probably more appropriate for an assign hook to cause trouble, but I
can't swear that that isn't happening without spending about a full
day in reviewing it.

I assume that you're thinking we'd only fix this in master?

Without this, I don't think it's possible for someone to enforce
protection of their data through SSI in an ironclad way. So there
is at least some case to be made to take it back as far as 9.1. I
don't think it makes sense to take it further, because read-only was
broken in other ways before 9.1, and I'm not aware of specific
threats further back. On the other hand, it is a change in behavior
with at least some chance to break code which is functioning as
intended, so it's a pretty marginal candidate for back-patching from
that point of view. I don't think a decision either way on that
would be crazy. Personally I would hope to see it included in a 9.1
patch, perhaps after some "settling time" on master, but totally
understand if the consensus is to just patch master.

-Kevin

#3Tom Lane
tgl@sss.pgh.pa.us
In reply to: Kevin Grittner (#2)

"Kevin Grittner" <Kevin.Grittner@wicourts.gov> writes:

Robert Haas <robertmhaas@gmail.com> wrote:

This patch makes me a little nervous, because the existing
behavior seems to have been coded for quite deliberately.

It does, although I'm not clear *why* it was. I suspect it may have
been based on an assumption that whatever value is in the reset_val
field had to have been already determined to be good, so it was a
waste of cycles to check it again -- without considering that the
validity of making a change might depend on context.

Yes, I'm inclined to think the same, although obviously we need to
review the patch carefully. The GUC code is a bit ticklish.

The main thing I would be worried about is whether you're sure that
you have separated the RESET-as-a-command case from the cases where
we actually are rolling back to a previous state.

regards, tom lane

#4Kevin Grittner
Kevin.Grittner@wicourts.gov
In reply to: Tom Lane (#3)

Tom Lane <tgl@sss.pgh.pa.us> wrote:

The main thing I would be worried about is whether you're sure
that you have separated the RESET-as-a-command case from the cases
where we actually are rolling back to a previous state.

I will double-check that, and make sure there is regression test
coverage of that case.

-Kevin

#5Robert Haas
robertmhaas@gmail.com
In reply to: Kevin Grittner (#2)

On Wed, Feb 15, 2012 at 4:47 PM, Kevin Grittner
<Kevin.Grittner@wicourts.gov> wrote:

That is unfortunate.  I guess it points out the value of adding a
comment to point out why we would want to check these values even on
a reset to a previously-used value.

+1 for such a comment.

I assume that you're thinking we'd only fix this in master?

Without this, I don't think it's possible for someone to enforce
protection of their data through SSI in an ironclad way.  So there
is at least some case to be made to take it back as far as 9.1.

I'm OK with that, but perhaps the only-tangentially-related changes
where you swap the order of certain error messages ought to be
separated out and committed only to master? That stuff doesn't seem
like material for a back-patch.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#6Kevin Grittner
Kevin.Grittner@wicourts.gov
In reply to: Robert Haas (#5)

Robert Haas <robertmhaas@gmail.com> wrote:

On Wed, Feb 15, 2012 at 4:47 PM, Kevin Grittner
<Kevin.Grittner@wicourts.gov> wrote:

That is unfortunate. I guess it points out the value of adding a
comment to point out why we would want to check these values even
on a reset to a previously-used value.

+1 for such a comment.

Will do.

I assume that you're thinking we'd only fix this in master?

Without this, I don't think it's possible for someone to enforce
protection of their data through SSI in an ironclad way. So
there is at least some case to be made to take it back as far as
9.1.

I'm OK with that, but perhaps the only-tangentially-related
changes where you swap the order of certain error messages ought
to be separated out and committed only to master? That stuff
doesn't seem like material for a back-patch.

Agreed. I'm not sure we want to change the message text at all in
9.1. Translations and all that.

-Kevin

#7Robert Haas
robertmhaas@gmail.com
In reply to: Kevin Grittner (#6)

On Wed, Feb 15, 2012 at 5:36 PM, Kevin Grittner
<Kevin.Grittner@wicourts.gov> wrote:

Agreed.  I'm not sure we want to change the message text at all in
9.1.  Translations and all that.

Agreed. I think we definitely don't want to do that.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#8Kevin Grittner
Kevin.Grittner@wicourts.gov
In reply to: Tom Lane (#3)
1 attachment(s)

Tom Lane <tgl@sss.pgh.pa.us> wrote:

The main thing I would be worried about is whether you're sure
that you have separated the RESET-as-a-command case from the cases
where we actually are rolling back to a previous state.

It looks good to me. I added a few regression tests for that.

Robert Haas <robertmhaas@gmail.com> wrote:

+1 for such a comment.

Added.

The attached patch includes these. If it seems close, I'd be happy
to come up with a version for the 9.1 branch. Basically it looks
like that means omitting the changes to variable.c (which only
changed message text and made the order of steps on related GUCs
more consistent), and capturing a different out file for the
expected directory.

-Kevin

Attachments:

check-reset-v2.patchtext/plain; name=check-reset-v2.patchDownload
*** a/src/backend/commands/variable.c
--- b/src/backend/commands/variable.c
***************
*** 600,616 **** check_XactIsoLevel(char **newval, void **extra, GucSource source)
  
  	if (newXactIsoLevel != XactIsoLevel)
  	{
! 		if (FirstSnapshotSet)
  		{
  			GUC_check_errcode(ERRCODE_ACTIVE_SQL_TRANSACTION);
! 			GUC_check_errmsg("SET TRANSACTION ISOLATION LEVEL must be called before any query");
  			return false;
  		}
! 		/* We ignore a subtransaction setting it to the existing value. */
! 		if (IsSubTransaction())
  		{
  			GUC_check_errcode(ERRCODE_ACTIVE_SQL_TRANSACTION);
! 			GUC_check_errmsg("SET TRANSACTION ISOLATION LEVEL must not be called in a subtransaction");
  			return false;
  		}
  		/* Can't go to serializable mode while recovery is still active */
--- 600,616 ----
  
  	if (newXactIsoLevel != XactIsoLevel)
  	{
! 		/* We ignore a subtransaction setting it to the existing value. */
! 		if (IsSubTransaction())
  		{
  			GUC_check_errcode(ERRCODE_ACTIVE_SQL_TRANSACTION);
! 			GUC_check_errmsg("cannot set transaction isolation level in a subtransaction");
  			return false;
  		}
! 		if (FirstSnapshotSet)
  		{
  			GUC_check_errcode(ERRCODE_ACTIVE_SQL_TRANSACTION);
! 			GUC_check_errmsg("transaction isolation level must be set before any query");
  			return false;
  		}
  		/* Can't go to serializable mode while recovery is still active */
***************
*** 665,677 **** check_transaction_deferrable(bool *newval, void **extra, GucSource source)
  	if (IsSubTransaction())
  	{
  		GUC_check_errcode(ERRCODE_ACTIVE_SQL_TRANSACTION);
! 		GUC_check_errmsg("SET TRANSACTION [NOT] DEFERRABLE cannot be called within a subtransaction");
  		return false;
  	}
  	if (FirstSnapshotSet)
  	{
  		GUC_check_errcode(ERRCODE_ACTIVE_SQL_TRANSACTION);
! 		GUC_check_errmsg("SET TRANSACTION [NOT] DEFERRABLE must be called before any query");
  		return false;
  	}
  
--- 665,677 ----
  	if (IsSubTransaction())
  	{
  		GUC_check_errcode(ERRCODE_ACTIVE_SQL_TRANSACTION);
! 		GUC_check_errmsg("cannot set transaction deferrable state within a subtransaction");
  		return false;
  	}
  	if (FirstSnapshotSet)
  	{
  		GUC_check_errcode(ERRCODE_ACTIVE_SQL_TRANSACTION);
! 		GUC_check_errmsg("transaction deferrable state must be set before any query");
  		return false;
  	}
  
*** a/src/backend/utils/misc/guc.c
--- b/src/backend/utils/misc/guc.c
***************
*** 5042,5047 **** config_enum_get_options(struct config_enum * record, const char *prefix,
--- 5042,5051 ----
   *
   * If value is NULL, set the option to its default value (normally the
   * reset_val, but if source == PGC_S_DEFAULT we instead use the boot_val).
+  * When we reset a value we can't assume that the old value is valid, but must
+  * call the check hook if present.  This is because the validity of a change
+  * might depend on context.  For example, transaction isolation may not be
+  * changed after the transaction has run a query, even by a RESET command.
   *
   * action indicates whether to set the value globally in the session, locally
   * to the current top transaction, or just for the duration of a function call.
***************
*** 5287,5302 **** set_config_option(const char *name, const char *value,
  								 name)));
  						return 0;
  					}
- 					if (!call_bool_check_hook(conf, &newval, &newextra,
- 											  source, elevel))
- 						return 0;
  				}
  				else if (source == PGC_S_DEFAULT)
  				{
  					newval = conf->boot_val;
- 					if (!call_bool_check_hook(conf, &newval, &newextra,
- 											  source, elevel))
- 						return 0;
  				}
  				else
  				{
--- 5291,5300 ----
***************
*** 5306,5311 **** set_config_option(const char *name, const char *value,
--- 5304,5313 ----
  					context = conf->gen.reset_scontext;
  				}
  
+ 				if (!call_bool_check_hook(conf, &newval, &newextra,
+ 										  source, elevel))
+ 					return 0;
+ 
  				if (prohibitValueChange)
  				{
  					if (*conf->variable != newval)
***************
*** 5391,5406 **** set_config_option(const char *name, const char *value,
  										newval, name, conf->min, conf->max)));
  						return 0;
  					}
- 					if (!call_int_check_hook(conf, &newval, &newextra,
- 											 source, elevel))
- 						return 0;
  				}
  				else if (source == PGC_S_DEFAULT)
  				{
  					newval = conf->boot_val;
- 					if (!call_int_check_hook(conf, &newval, &newextra,
- 											 source, elevel))
- 						return 0;
  				}
  				else
  				{
--- 5393,5402 ----
***************
*** 5410,5415 **** set_config_option(const char *name, const char *value,
--- 5406,5415 ----
  					context = conf->gen.reset_scontext;
  				}
  
+ 				if (!call_int_check_hook(conf, &newval, &newextra,
+ 										 source, elevel))
+ 					return 0;
+ 
  				if (prohibitValueChange)
  				{
  					if (*conf->variable != newval)
***************
*** 5492,5507 **** set_config_option(const char *name, const char *value,
  										newval, name, conf->min, conf->max)));
  						return 0;
  					}
- 					if (!call_real_check_hook(conf, &newval, &newextra,
- 											  source, elevel))
- 						return 0;
  				}
  				else if (source == PGC_S_DEFAULT)
  				{
  					newval = conf->boot_val;
- 					if (!call_real_check_hook(conf, &newval, &newextra,
- 											  source, elevel))
- 						return 0;
  				}
  				else
  				{
--- 5492,5501 ----
***************
*** 5511,5516 **** set_config_option(const char *name, const char *value,
--- 5505,5514 ----
  					context = conf->gen.reset_scontext;
  				}
  
+ 				if (!call_real_check_hook(conf, &newval, &newextra,
+ 										  source, elevel))
+ 					return 0;
+ 
  				if (prohibitValueChange)
  				{
  					if (*conf->variable != newval)
***************
*** 5591,5603 **** set_config_option(const char *name, const char *value,
  					 */
  					if (conf->gen.flags & GUC_IS_NAME)
  						truncate_identifier(newval, strlen(newval), true);
- 
- 					if (!call_string_check_hook(conf, &newval, &newextra,
- 												source, elevel))
- 					{
- 						free(newval);
- 						return 0;
- 					}
  				}
  				else if (source == PGC_S_DEFAULT)
  				{
--- 5589,5594 ----
***************
*** 5610,5635 **** set_config_option(const char *name, const char *value,
  					}
  					else
  						newval = NULL;
- 
- 					if (!call_string_check_hook(conf, &newval, &newextra,
- 												source, elevel))
- 					{
- 						free(newval);
- 						return 0;
- 					}
  				}
  				else
  				{
! 					/*
! 					 * strdup not needed, since reset_val is already under
! 					 * guc.c's control
! 					 */
! 					newval = conf->reset_val;
  					newextra = conf->reset_extra;
  					source = conf->gen.reset_source;
  					context = conf->gen.reset_scontext;
  				}
  
  				if (prohibitValueChange)
  				{
  					/* newval shouldn't be NULL, so we're a bit sloppy here */
--- 5601,5630 ----
  					}
  					else
  						newval = NULL;
  				}
  				else
  				{
! 					/* non-NULL reset_val must always get strdup'd */
! 					if (conf->reset_val != NULL)
! 					{
! 						newval = guc_strdup(elevel, conf->reset_val);
! 						if (newval == NULL)
! 							return 0;
! 					}
! 					else
! 						newval = NULL;
  					newextra = conf->reset_extra;
  					source = conf->gen.reset_source;
  					context = conf->gen.reset_scontext;
  				}
  
+ 				if (!call_string_check_hook(conf, &newval, &newextra,
+ 											source, elevel))
+ 				{
+ 					free(newval);
+ 					return 0;
+ 				}
+ 
  				if (prohibitValueChange)
  				{
  					/* newval shouldn't be NULL, so we're a bit sloppy here */
***************
*** 5721,5736 **** set_config_option(const char *name, const char *value,
  							pfree(hintmsg);
  						return 0;
  					}
- 					if (!call_enum_check_hook(conf, &newval, &newextra,
- 											  source, elevel))
- 						return 0;
  				}
  				else if (source == PGC_S_DEFAULT)
  				{
  					newval = conf->boot_val;
- 					if (!call_enum_check_hook(conf, &newval, &newextra,
- 											  source, elevel))
- 						return 0;
  				}
  				else
  				{
--- 5716,5725 ----
***************
*** 5740,5745 **** set_config_option(const char *name, const char *value,
--- 5729,5738 ----
  					context = conf->gen.reset_scontext;
  				}
  
+ 				if (!call_enum_check_hook(conf, &newval, &newextra,
+ 										  source, elevel))
+ 					return 0;
+ 
  				if (prohibitValueChange)
  				{
  					if (*conf->variable != newval)
*** a/src/test/regress/expected/transactions.out
--- b/src/test/regress/expected/transactions.out
***************
*** 53,58 **** SELECT * FROM writetest; -- ok
--- 53,90 ----
  SET TRANSACTION READ WRITE; --fail
  ERROR:  transaction read-write mode must be set before any query
  COMMIT;
+ SET default_transaction_read_only = FALSE;
+ SET default_transaction_isolation = 'read committed';
+ BEGIN;
+ SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY; -- ok
+ SELECT * FROM writetest; -- ok
+  a 
+ ---
+ (0 rows)
+ 
+ RESET transaction_read_only; --fail
+ ERROR:  transaction read-write mode must be set before any query
+ COMMIT;
+ BEGIN;
+ SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY; -- ok
+ SELECT * FROM writetest; -- ok
+  a 
+ ---
+ (0 rows)
+ 
+ RESET transaction_isolation; --fail
+ ERROR:  transaction isolation level must be set before any query
+ COMMIT;
+ BEGIN;
+ SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY; -- ok
+ SELECT * FROM writetest; -- ok
+  a 
+ ---
+ (0 rows)
+ 
+ ROLLBACK; -- ok
+ RESET default_transaction_read_only;
+ RESET default_transaction_isolation;
  BEGIN;
  SET TRANSACTION READ ONLY; -- ok
  SET TRANSACTION READ WRITE; -- ok
***************
*** 391,396 **** SELECT 1;			-- this should work
--- 423,480 ----
          1
  (1 row)
  
+ -- test rollbacks and resets of GUC in transactions
+ CREATE SCHEMA albion;
+ SET search_path = "$user",public,albion;
+ DROP SCHEMA albion;
+ SHOW search_path;
+        search_path       
+ -------------------------
+  "$user", public, albion
+ (1 row)
+ 
+ BEGIN;
+ CREATE SCHEMA camelot;
+ SET search_path = "$user",public,camelot;
+ DROP SCHEMA camelot;
+ SAVEPOINT bedivere;
+ CREATE SCHEMA avalon;
+ SET search_path = "$user",public,avalon;
+ DROP SCHEMA avalon;
+ SHOW search_path;
+        search_path       
+ -------------------------
+  "$user", public, avalon
+ (1 row)
+ 
+ ROLLBACK TO SAVEPOINT bedivere;
+ SHOW search_path;
+        search_path        
+ --------------------------
+  "$user", public, camelot
+ (1 row)
+ 
+ RESET search_path;
+ SHOW search_path;
+   search_path   
+ ----------------
+  "$user",public
+ (1 row)
+ 
+ ROLLBACK;
+ SHOW search_path;
+        search_path       
+ -------------------------
+  "$user", public, albion
+ (1 row)
+ 
+ RESET search_path;
+ SHOW search_path;
+   search_path   
+ ----------------
+  "$user",public
+ (1 row)
+ 
  -- check non-transactional behavior of cursors
  BEGIN;
  	DECLARE c CURSOR FOR SELECT unique2 FROM tenk1 ORDER BY unique2;
*** a/src/test/regress/sql/transactions.sql
--- b/src/test/regress/sql/transactions.sql
***************
*** 45,50 **** SELECT * FROM writetest; -- ok
--- 45,73 ----
  SET TRANSACTION READ WRITE; --fail
  COMMIT;
  
+ SET default_transaction_read_only = FALSE;
+ SET default_transaction_isolation = 'read committed';
+ 
+ BEGIN;
+ SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY; -- ok
+ SELECT * FROM writetest; -- ok
+ RESET transaction_read_only; --fail
+ COMMIT;
+ 
+ BEGIN;
+ SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY; -- ok
+ SELECT * FROM writetest; -- ok
+ RESET transaction_isolation; --fail
+ COMMIT;
+ 
+ BEGIN;
+ SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY; -- ok
+ SELECT * FROM writetest; -- ok
+ ROLLBACK; -- ok
+ 
+ RESET default_transaction_read_only;
+ RESET default_transaction_isolation;
+ 
  BEGIN;
  SET TRANSACTION READ ONLY; -- ok
  SET TRANSACTION READ WRITE; -- ok
***************
*** 252,257 **** BEGIN;
--- 275,303 ----
  COMMIT;
  SELECT 1;			-- this should work
  
+ -- test rollbacks and resets of GUC in transactions
+ CREATE SCHEMA albion;
+ SET search_path = "$user",public,albion;
+ DROP SCHEMA albion;
+ SHOW search_path;
+ BEGIN;
+ CREATE SCHEMA camelot;
+ SET search_path = "$user",public,camelot;
+ DROP SCHEMA camelot;
+ SAVEPOINT bedivere;
+ CREATE SCHEMA avalon;
+ SET search_path = "$user",public,avalon;
+ DROP SCHEMA avalon;
+ SHOW search_path;
+ ROLLBACK TO SAVEPOINT bedivere;
+ SHOW search_path;
+ RESET search_path;
+ SHOW search_path;
+ ROLLBACK;
+ SHOW search_path;
+ RESET search_path;
+ SHOW search_path;
+ 
  -- check non-transactional behavior of cursors
  BEGIN;
  	DECLARE c CURSOR FOR SELECT unique2 FROM tenk1 ORDER BY unique2;