Skip to content

Commit

Permalink
Allow ALTER SYSTEM to set unrecognized custom GUCs.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
tglsfdc committed Oct 21, 2023
1 parent 36a14af commit 2d870b4
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 96 deletions.
6 changes: 1 addition & 5 deletions src/backend/catalog/pg_parameter_acl.c
Expand Up @@ -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);
Expand Down
218 changes: 129 additions & 89 deletions src/backend/utils/misc/guc.c
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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);
}

/*
Expand Down Expand Up @@ -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")));
}

/*
Expand Down
2 changes: 1 addition & 1 deletion src/include/utils/guc.h
Expand Up @@ -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);
Expand Down
26 changes: 25 additions & 1 deletion src/test/modules/unsafe_tests/expected/guc_privs.out
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/test/modules/unsafe_tests/sql/guc_privs.sql
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 2d870b4

Please sign in to comment.