Allow ALTER SYSTEM to set unrecognized custom GUCs.
authorTom Lane <[email protected]>
Sat, 21 Oct 2023 17:35:19 +0000 (13:35 -0400)
committerTom Lane <[email protected]>
Sat, 21 Oct 2023 17:35:19 +0000 (13:35 -0400)
Previously, ALTER SYSTEM failed if the target GUC wasn't present in
the session's GUC hashtable.  That is a reasonable behavior for core
(single-part) GUC names, and for custom GUCs for which we have loaded
an extension that's reserved the prefix.  But it's unnecessarily
restrictive otherwise, and it also causes inconsistent behavior:
you can "ALTER SYSTEM SET foo.bar" only if you did "SET foo.bar"
earlier in the session.  That's fairly silly.

Hence, refactor things so that we can execute ALTER SYSTEM even
if the variable doesn't have a GUC hashtable entry, as long as the
name meets the custom-variable naming requirements and does not
have a reserved prefix.  (It's safe to do this even if the
variable belongs to an extension we currently don't have loaded.
A bad value will at worst cause a WARNING when the extension
does get loaded.)

Also, adjust GRANT ON PARAMETER to have the same opinions about
whether to allow an unrecognized GUC name, and to throw the
same errors if not (it previously used a one-size-fits-all
message for several distinguishable conditions).  By default,
only a superuser will be allowed to do ALTER SYSTEM SET on an
unrecognized name, but it's possible to GRANT the ability to
do it.

Patch by me, pursuant to a documentation complaint from
Gavin Panella.  Arguably this is a bug fix, but given the
lack of other complaints I'll refrain from back-patching.

Discussion: https://postgr.es/m/2617358.1697501956@sss.pgh.pa.us
Discussion: https://postgr.es/m/169746329791.169914.16613647309012285391@wrigleys.postgresql.org

src/backend/catalog/pg_parameter_acl.c
src/backend/utils/misc/guc.c
src/include/utils/guc.h
src/test/modules/unsafe_tests/expected/guc_privs.out
src/test/modules/unsafe_tests/sql/guc_privs.sql

index 073392e2c4fec77ac2c54d3b999b2dd0af21b097..f4bc10bafedcd50ba181909d1f74d3c1a545d1a5 100644 (file)
@@ -82,11 +82,7 @@ ParameterAclCreate(const char *parameter)
     * To prevent cluttering pg_parameter_acl with useless entries, insist
     * that the name be valid.
     */
-   if (!check_GUC_name_for_parameter_acl(parameter))
-       ereport(ERROR,
-               (errcode(ERRCODE_INVALID_NAME),
-                errmsg("invalid parameter name \"%s\"",
-                       parameter)));
+   check_GUC_name_for_parameter_acl(parameter);
 
    /* Convert name to the form it should have in pg_parameter_acl. */
    parname = convert_GUC_name_for_parameter_acl(parameter);
index c25c697a0691e4f580811f5b591f287dabe14621..39d3775e801b30558aba00c692e26a3f22233aae 100644 (file)
@@ -250,6 +250,8 @@ static void write_auto_conf_file(int fd, const char *filename, ConfigVariable *h
 static void replace_auto_config_value(ConfigVariable **head_p, ConfigVariable **tail_p,
                                      const char *name, const char *value);
 static bool valid_custom_variable_name(const char *name);
+static bool assignable_custom_variable_name(const char *name, bool skip_errors,
+                                           int elevel);
 static void do_serialize(char **destptr, Size *maxbytes,
                         const char *fmt,...) pg_attribute_printf(3, 4);
 static bool call_bool_check_hook(struct config_bool *conf, bool *newval,
@@ -1063,7 +1065,7 @@ add_guc_variable(struct config_generic *var, int elevel)
  *
  * It must be two or more identifiers separated by dots, where the rules
  * for what is an identifier agree with scan.l.  (If you change this rule,
- * adjust the errdetail in find_option().)
+ * adjust the errdetail in assignable_custom_variable_name().)
  */
 static bool
 valid_custom_variable_name(const char *name)
@@ -1098,6 +1100,71 @@ valid_custom_variable_name(const char *name)
    return saw_sep;
 }
 
+/*
+ * Decide whether an unrecognized variable name is allowed to be SET.
+ *
+ * It must pass the syntactic rules of valid_custom_variable_name(),
+ * and it must not be in any namespace already reserved by an extension.
+ * (We make this separate from valid_custom_variable_name() because we don't
+ * apply the reserved-namespace test when reading configuration files.)
+ *
+ * If valid, return true.  Otherwise, return false if skip_errors is true,
+ * else throw a suitable error at the specified elevel (and return false
+ * if that's less than ERROR).
+ */
+static bool
+assignable_custom_variable_name(const char *name, bool skip_errors, int elevel)
+{
+   /* If there's no separator, it can't be a custom variable */
+   const char *sep = strchr(name, GUC_QUALIFIER_SEPARATOR);
+
+   if (sep != NULL)
+   {
+       size_t      classLen = sep - name;
+       ListCell   *lc;
+
+       /* The name must be syntactically acceptable ... */
+       if (!valid_custom_variable_name(name))
+       {
+           if (!skip_errors)
+               ereport(elevel,
+                       (errcode(ERRCODE_INVALID_NAME),
+                        errmsg("invalid configuration parameter name \"%s\"",
+                               name),
+                        errdetail("Custom parameter names must be two or more simple identifiers separated by dots.")));
+           return false;
+       }
+       /* ... and it must not match any previously-reserved prefix */
+       foreach(lc, reserved_class_prefix)
+       {
+           const char *rcprefix = lfirst(lc);
+
+           if (strlen(rcprefix) == classLen &&
+               strncmp(name, rcprefix, classLen) == 0)
+           {
+               if (!skip_errors)
+                   ereport(elevel,
+                           (errcode(ERRCODE_INVALID_NAME),
+                            errmsg("invalid configuration parameter name \"%s\"",
+                                   name),
+                            errdetail("\"%s\" is a reserved prefix.",
+                                      rcprefix)));
+               return false;
+           }
+       }
+       /* OK to create it */
+       return true;
+   }
+
+   /* Unrecognized single-part name */
+   if (!skip_errors)
+       ereport(elevel,
+               (errcode(ERRCODE_UNDEFINED_OBJECT),
+                errmsg("unrecognized configuration parameter \"%s\"",
+                       name)));
+   return false;
+}
+
 /*
  * Create and add a placeholder variable for a custom variable name.
  */
@@ -1191,52 +1258,15 @@ find_option(const char *name, bool create_placeholders, bool skip_errors,
    if (create_placeholders)
    {
        /*
-        * Check if the name is valid, and if so, add a placeholder.  If it
-        * doesn't contain a separator, don't assume that it was meant to be a
-        * placeholder.
+        * Check if the name is valid, and if so, add a placeholder.
         */
-       const char *sep = strchr(name, GUC_QUALIFIER_SEPARATOR);
-
-       if (sep != NULL)
-       {
-           size_t      classLen = sep - name;
-           ListCell   *lc;
-
-           /* The name must be syntactically acceptable ... */
-           if (!valid_custom_variable_name(name))
-           {
-               if (!skip_errors)
-                   ereport(elevel,
-                           (errcode(ERRCODE_INVALID_NAME),
-                            errmsg("invalid configuration parameter name \"%s\"",
-                                   name),
-                            errdetail("Custom parameter names must be two or more simple identifiers separated by dots.")));
-               return NULL;
-           }
-           /* ... and it must not match any previously-reserved prefix */
-           foreach(lc, reserved_class_prefix)
-           {
-               const char *rcprefix = lfirst(lc);
-
-               if (strlen(rcprefix) == classLen &&
-                   strncmp(name, rcprefix, classLen) == 0)
-               {
-                   if (!skip_errors)
-                       ereport(elevel,
-                               (errcode(ERRCODE_INVALID_NAME),
-                                errmsg("invalid configuration parameter name \"%s\"",
-                                       name),
-                                errdetail("\"%s\" is a reserved prefix.",
-                                          rcprefix)));
-                   return NULL;
-               }
-           }
-           /* OK, create it */
+       if (assignable_custom_variable_name(name, skip_errors, elevel))
            return add_placeholder_variable(name, elevel);
-       }
+       else
+           return NULL;        /* error message, if any, already emitted */
    }
 
-   /* Unknown name */
+   /* Unknown name and we're not supposed to make a placeholder */
    if (!skip_errors)
        ereport(elevel,
                (errcode(ERRCODE_UNDEFINED_OBJECT),
@@ -1369,18 +1399,16 @@ convert_GUC_name_for_parameter_acl(const char *name)
 /*
  * Check whether we should allow creation of a pg_parameter_acl entry
  * for the given name.  (This can be applied either before or after
- * canonicalizing it.)
+ * canonicalizing it.)  Throws error if not.
  */
-bool
+void
 check_GUC_name_for_parameter_acl(const char *name)
 {
    /* OK if the GUC exists. */
-   if (find_option(name, false, true, DEBUG1) != NULL)
-       return true;
+   if (find_option(name, false, true, DEBUG5) != NULL)
+       return;
    /* Otherwise, it'd better be a valid custom GUC name. */
-   if (valid_custom_variable_name(name))
-       return true;
-   return false;
+   (void) assignable_custom_variable_name(name, false, ERROR);
 }
 
 /*
@@ -4515,52 +4543,64 @@ AlterSystemSetConfigFile(AlterSystemStmt *altersysstmt)
    {
        struct config_generic *record;
 
-       record = find_option(name, false, false, ERROR);
-       Assert(record != NULL);
-
-       /*
-        * Don't allow parameters that can't be set in configuration files to
-        * be set in PG_AUTOCONF_FILENAME file.
-        */
-       if ((record->context == PGC_INTERNAL) ||
-           (record->flags & GUC_DISALLOW_IN_FILE) ||
-           (record->flags & GUC_DISALLOW_IN_AUTO_FILE))
-           ereport(ERROR,
-                   (errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM),
-                    errmsg("parameter \"%s\" cannot be changed",
-                           name)));
-
-       /*
-        * If a value is specified, verify that it's sane.
-        */
-       if (value)
+       /* We don't want to create a placeholder if there's not one already */
+       record = find_option(name, false, true, DEBUG5);
+       if (record != NULL)
        {
-           union config_var_val newval;
-           void       *newextra = NULL;
-
-           /* Check that it's acceptable for the indicated parameter */
-           if (!parse_and_validate_value(record, name, value,
-                                         PGC_S_FILE, ERROR,
-                                         &newval, &newextra))
+           /*
+            * Don't allow parameters that can't be set in configuration files
+            * to be set in PG_AUTOCONF_FILENAME file.
+            */
+           if ((record->context == PGC_INTERNAL) ||
+               (record->flags & GUC_DISALLOW_IN_FILE) ||
+               (record->flags & GUC_DISALLOW_IN_AUTO_FILE))
                ereport(ERROR,
-                       (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-                        errmsg("invalid value for parameter \"%s\": \"%s\"",
-                               name, value)));
+                       (errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM),
+                        errmsg("parameter \"%s\" cannot be changed",
+                               name)));
+
+           /*
+            * If a value is specified, verify that it's sane.
+            */
+           if (value)
+           {
+               union config_var_val newval;
+               void       *newextra = NULL;
 
-           if (record->vartype == PGC_STRING && newval.stringval != NULL)
-               guc_free(newval.stringval);
-           guc_free(newextra);
+               if (!parse_and_validate_value(record, name, value,
+                                             PGC_S_FILE, ERROR,
+                                             &newval, &newextra))
+                   ereport(ERROR,
+                           (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                            errmsg("invalid value for parameter \"%s\": \"%s\"",
+                                   name, value)));
 
+               if (record->vartype == PGC_STRING && newval.stringval != NULL)
+                   guc_free(newval.stringval);
+               guc_free(newextra);
+           }
+       }
+       else
+       {
            /*
-            * We must also reject values containing newlines, because the
-            * grammar for config files doesn't support embedded newlines in
-            * string literals.
+            * Variable not known; check we'd be allowed to create it.  (We
+            * cannot validate the value, but that's fine.  A non-core GUC in
+            * the config file cannot cause postmaster start to fail, so we
+            * don't have to be too tense about possibly installing a bad
+            * value.)
             */
-           if (strchr(value, '\n'))
-               ereport(ERROR,
-                       (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-                        errmsg("parameter value for ALTER SYSTEM must not contain a newline")));
+           (void) assignable_custom_variable_name(name, false, ERROR);
        }
+
+       /*
+        * We must also reject values containing newlines, because the grammar
+        * for config files doesn't support embedded newlines in string
+        * literals.
+        */
+       if (value && strchr(value, '\n'))
+           ereport(ERROR,
+                   (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                    errmsg("parameter value for ALTER SYSTEM must not contain a newline")));
    }
 
    /*
index e89083ee0e6e30fb8dfd04b11cd4d9a7f258e49d..902e57c0ca31a2f3706ef15e4456f458fcd20397 100644 (file)
@@ -363,7 +363,7 @@ extern const char *GetConfigOptionResetString(const char *name);
 extern int GetConfigOptionFlags(const char *name, bool missing_ok);
 extern void ProcessConfigFile(GucContext context);
 extern char *convert_GUC_name_for_parameter_acl(const char *name);
-extern bool check_GUC_name_for_parameter_acl(const char *name);
+extern void check_GUC_name_for_parameter_acl(const char *name);
 extern void InitializeGUCOptions(void);
 extern bool SelectConfigFiles(const char *userDoption, const char *progname);
 extern void ResetAllOptions(void);
index f43a1da214e0f650f002501b9fa22009f260b066..6c0ad898341ff0543ff4a532052171c3a4f3ab48 100644 (file)
@@ -220,9 +220,31 @@ SELECT 1 FROM pg_parameter_acl WHERE parname = 'none.such';
 ----------
 (0 rows)
 
+-- Superuser should be able to ALTER SYSTEM SET a non-existent custom GUC.
+ALTER SYSTEM SET none.such = 'whiz bang';
+-- None of the above should have created a placeholder GUC for none.such.
+SHOW none.such;  -- error
+ERROR:  unrecognized configuration parameter "none.such"
+-- However, if we reload ...
+SELECT pg_reload_conf();
+ pg_reload_conf 
+----------------
+ t
+(1 row)
+
+-- and start a new session to avoid race condition ...
+\c -
+SET SESSION AUTHORIZATION regress_admin;
+-- then it should be there.
+SHOW none.such;
+ none.such 
+-----------
+ whiz bang
+(1 row)
+
 -- Can't grant on a non-existent core GUC.
 GRANT ALL ON PARAMETER no_such_guc TO regress_host_resource_admin;  -- fail
-ERROR:  invalid parameter name "no_such_guc"
+ERROR:  unrecognized configuration parameter "no_such_guc"
 -- Initially there are no privileges and no catalog entry for this GUC.
 SELECT has_parameter_privilege('regress_host_resource_admin', 'enable_material', 'SET');
  has_parameter_privilege 
@@ -459,6 +481,8 @@ SELECT set_config ('temp_buffers', '8192', false); -- ok
 ALTER SYSTEM RESET autovacuum_work_mem;  -- ok, privileges have been granted
 ALTER SYSTEM RESET ALL;  -- fail, insufficient privileges
 ERROR:  permission denied to perform ALTER SYSTEM RESET ALL
+ALTER SYSTEM SET none.such2 = 'whiz bang';  -- fail, not superuser
+ERROR:  permission denied to set parameter "none.such2"
 ALTER ROLE regress_host_resource_admin SET lc_messages = 'POSIX';  -- fail
 ERROR:  permission denied to set parameter "lc_messages"
 ALTER ROLE regress_host_resource_admin SET max_stack_depth = '1MB';  -- ok
index 7a4fb24b9d1bda5a6a247359e5aa23e86ed87559..9bcbbfa9040cfae91140f6bad972316613a146c6 100644 (file)
@@ -98,6 +98,19 @@ GRANT ALL ON PARAMETER none.such TO regress_host_resource_admin;
 SELECT 1 FROM pg_parameter_acl WHERE parname = 'none.such';
 REVOKE ALL ON PARAMETER "None.Such" FROM regress_host_resource_admin;
 SELECT 1 FROM pg_parameter_acl WHERE parname = 'none.such';
+
+-- Superuser should be able to ALTER SYSTEM SET a non-existent custom GUC.
+ALTER SYSTEM SET none.such = 'whiz bang';
+-- None of the above should have created a placeholder GUC for none.such.
+SHOW none.such;  -- error
+-- However, if we reload ...
+SELECT pg_reload_conf();
+-- and start a new session to avoid race condition ...
+\c -
+SET SESSION AUTHORIZATION regress_admin;
+-- then it should be there.
+SHOW none.such;
+
 -- Can't grant on a non-existent core GUC.
 GRANT ALL ON PARAMETER no_such_guc TO regress_host_resource_admin;  -- fail
 
@@ -190,6 +203,7 @@ ALTER SYSTEM RESET lc_messages;  -- fail, insufficient privileges
 SELECT set_config ('temp_buffers', '8192', false); -- ok
 ALTER SYSTEM RESET autovacuum_work_mem;  -- ok, privileges have been granted
 ALTER SYSTEM RESET ALL;  -- fail, insufficient privileges
+ALTER SYSTEM SET none.such2 = 'whiz bang';  -- fail, not superuser
 ALTER ROLE regress_host_resource_admin SET lc_messages = 'POSIX';  -- fail
 ALTER ROLE regress_host_resource_admin SET max_stack_depth = '1MB';  -- ok
 SELECT setconfig FROM pg_db_role_setting