diff --git a/daemons/execd/remoted_pidone.c b/daemons/execd/remoted_pidone.c index 70fee47645..4a16144931 100644 --- a/daemons/execd/remoted_pidone.c +++ b/daemons/execd/remoted_pidone.c @@ -1,299 +1,302 @@ /* * Copyright 2017-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include "pacemaker-execd.h" static pid_t main_pid = 0; static void sigdone(void) { crm_exit(CRM_EX_OK); } static void sigreap(void) { pid_t pid = 0; int status; do { /* * Opinions seem to differ as to what to put here: * -1, any child process * 0, any child process whose process group ID is equal to that of the calling process */ pid = waitpid(-1, &status, WNOHANG); if (pid == main_pid) { /* Exit when pacemaker-remote exits and use the same return code */ if (WIFEXITED(status)) { crm_exit(WEXITSTATUS(status)); } crm_exit(CRM_EX_ERROR); } } while (pid > 0); } static struct { int sig; void (*handler)(void); } sigmap[] = { { SIGCHLD, sigreap }, { SIGINT, sigdone }, }; /*! * \internal * \brief Check whether a string is a valid environment variable name * * \param[in] name String to check * * \return \c true if \p name is a valid name, or \c false otherwise * \note It's reasonable to impose limitations on environment variable names * beyond what C or setenv() does: We only allow names that contain only * [a-zA-Z0-9_] characters and do not start with a digit. */ static bool valid_env_var_name(const gchar *name) { if (!isalpha(*name) && (*name != '_')) { // Invalid first character return false; } // The rest of the characters must be alphanumeric or underscores for (name++; isalnum(*name) || (*name == '_'); name++); return *name == '\0'; } /*! * \internal * \brief Read one environment variable assignment and set the value * * Empty lines and trailing comments are ignored. This function handles * backslashes, single quotes, and double quotes in a manner similar to a POSIX * shell. * * This function has at least two limitations compared to a shell: * * An assignment must be contained within a single line. * * Only one assignment per line is supported. * * It would be possible to get rid of these limitations, but it doesn't seem * worth the trouble of implementation and testing. * * \param[in] line Line containing an environment variable assignment statement */ static void load_env_var_line(const char *line) { gint argc = 0; gchar **argv = NULL; GError *error = NULL; gchar *name = NULL; gchar *value = NULL; int rc = pcmk_rc_ok; const char *reason = NULL; const char *value_to_set = NULL; /* g_shell_parse_argv() does the following in a manner similar to the shell: * * tokenizes the value * * strips a trailing '#' comment if one exists * * handles backslashes, single quotes, and double quotes */ // Ensure the line contains zero or one token besides an optional comment if (!g_shell_parse_argv(line, &argc, NULL, &error)) { // Empty line (or only space/comment) means nothing to do and no error - if (error->code != G_SHELL_ERROR_EMPTY_STRING) { + if (!g_error_matches(error, G_SHELL_ERROR, + G_SHELL_ERROR_EMPTY_STRING)) { reason = error->message; } goto done; } if (argc != 1) { // "argc != 1" for sanity; should imply "argc > 1" by now reason = "line contains garbage"; goto done; } rc = pcmk__scan_nvpair(line, &name, &value); if (rc != pcmk_rc_ok) { reason = pcmk_rc_str(rc); goto done; } // Leading whitespace is allowed and ignored. A quoted name is invalid. g_strchug(name); if (!valid_env_var_name(name)) { reason = "invalid environment variable name"; goto done; } /* Parse the value as the shell would do (stripping outermost quotes, etc.). * Also sanity-check that the value either is empty or consists of one * token. Anything malformed should have been caught by now. */ if (!g_shell_parse_argv(value, &argc, &argv, &error)) { // Parse error should mean value is empty - CRM_CHECK(error->code == G_SHELL_ERROR_EMPTY_STRING, goto done); + CRM_CHECK(g_error_matches(error, G_SHELL_ERROR, + G_SHELL_ERROR_EMPTY_STRING), + goto done); value_to_set = ""; } else { // value wasn't empty, so it should contain one token CRM_CHECK(argc == 1, goto done); value_to_set = argv[0]; } // Don't overwrite (bundle options take precedence) setenv(name, value_to_set, 0); done: if (reason != NULL) { crm_warn("Failed to perform environment variable assignment '%s': %s", line, reason); } g_strfreev(argv); g_clear_error(&error); g_free(name); g_free(value); } #define CONTAINER_ENV_FILE "/etc/pacemaker/pcmk-init.env" static void load_env_vars(void) { /* We haven't forked or initialized logging yet, so don't leave any file * descriptors open, and don't log -- silently ignore errors. */ FILE *fp = fopen(CONTAINER_ENV_FILE, "r"); char *line = NULL; size_t buf_size = 0; if (fp == NULL) { return; } while (getline(&line, &buf_size, fp) != -1) { load_env_var_line(line); errno = 0; } // getline() returns -1 on EOF (expected) or error if (errno != 0) { int rc = errno; crm_err("Error while reading environment variables from " CONTAINER_ENV_FILE ": %s", pcmk_rc_str(rc)); } fclose(fp); free(line); } void remoted_spawn_pidone(int argc, char **argv) { sigset_t set; /* This environment variable exists for two purposes: * - For testing, setting it to "full" enables full PID 1 behavior even * when PID is not 1 * - Setting to "vars" enables just the loading of environment variables * from /etc/pacemaker/pcmk-init.env, which could be useful for testing or * containers with a custom PID 1 script that launches the remote * executor. */ const char *pid1 = PCMK_VALUE_DEFAULT; if (getpid() != 1) { pid1 = pcmk__env_option(PCMK__ENV_REMOTE_PID1); if (!pcmk__str_any_of(pid1, "full", "vars", NULL)) { // Default, unset, or invalid return; } } /* When a container is launched, it may be given specific environment * variables, which for Pacemaker bundles are given in the bundle * configuration. However, that does not allow for host-specific values. * To allow for that, look for a special file containing a shell-like syntax * of name/value pairs, and export those into the environment. */ load_env_vars(); if (strcmp(pid1, "vars") == 0) { return; } /* Containers can be expected to have /var/log, but they may not have * /var/log/pacemaker, so use a different default if no value has been * explicitly configured in the container's environment. */ if (pcmk__env_option(PCMK__ENV_LOGFILE) == NULL) { pcmk__set_env_option(PCMK__ENV_LOGFILE, "/var/log/pcmk-init.log", true); } sigfillset(&set); sigprocmask(SIG_BLOCK, &set, 0); main_pid = fork(); switch (main_pid) { case 0: sigprocmask(SIG_UNBLOCK, &set, NULL); setsid(); setpgid(0, 0); // Child remains as pacemaker-remoted return; case -1: crm_err("fork failed: %s", pcmk_rc_str(errno)); } /* Parent becomes the reaper of zombie processes */ /* Safe to initialize logging now if needed */ /* Differentiate the parent from the child, which does the real * pacemaker-remoted work, in the output of the `ps` command. * * strncpy() pads argv[0] with '\0' after copying "pcmk-init" if there is * more space to fill. In practice argv[0] should always be longer than * "pcmk-init", but use strlen() for safety to ensure null termination. * * Zero out the other argv members. */ strncpy(argv[0], "pcmk-init", strlen(argv[0])); for (int i = 1; i < argc; i++) { memset(argv[i], '\0', strlen(argv[i])); } while (1) { int sig = 0; sigwait(&set, &sig); for (int i = 0; i < PCMK__NELEM(sigmap); i++) { if (sigmap[i].sig == sig) { sigmap[i].handler(); break; } } } } diff --git a/lib/common/cmdline.c b/lib/common/cmdline.c index 7a66b26be0..5b6b1654ed 100644 --- a/lib/common/cmdline.c +++ b/lib/common/cmdline.c @@ -1,379 +1,344 @@ /* * Copyright 2019-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include static gboolean bump_verbosity(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { pcmk__common_args_t *common_args = (pcmk__common_args_t *) data; common_args->verbosity++; return TRUE; } pcmk__common_args_t * pcmk__new_common_args(const char *summary) { pcmk__common_args_t *args = NULL; args = calloc(1, sizeof(pcmk__common_args_t)); if (args == NULL) { crm_exit(CRM_EX_OSERR); } // cppcheck-suppress nullPointerOutOfMemory args->summary = strdup(summary); // cppcheck-suppress nullPointerOutOfMemory if (args->summary == NULL) { free(args); args = NULL; crm_exit(CRM_EX_OSERR); } return args; } static void free_common_args(gpointer data) { pcmk__common_args_t *common_args = (pcmk__common_args_t *) data; free(common_args->summary); free(common_args->output_ty); free(common_args->output_dest); if (common_args->output_as_descr != NULL) { free(common_args->output_as_descr); } free(common_args); } GOptionContext * pcmk__build_arg_context(pcmk__common_args_t *common_args, const char *fmts, GOptionGroup **output_group, const char *param_string) { GOptionContext *context; GOptionGroup *main_group; GOptionEntry main_entries[3] = { { "version", '$', 0, G_OPTION_ARG_NONE, &(common_args->version), N_("Display software version and exit"), NULL }, { "verbose", 'V', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, bump_verbosity, N_("Increase debug output (may be specified multiple times)"), NULL }, { NULL } }; main_group = g_option_group_new(NULL, "Application Options:", NULL, common_args, free_common_args); g_option_group_add_entries(main_group, main_entries); context = g_option_context_new(param_string); g_option_context_set_summary(context, common_args->summary); g_option_context_set_description(context, "Report bugs to " PCMK__BUG_URL "\n"); g_option_context_set_main_group(context, main_group); if (fmts != NULL) { GOptionEntry output_entries[3] = { { "output-as", 0, 0, G_OPTION_ARG_STRING, &(common_args->output_ty), NULL, N_("FORMAT") }, { "output-to", 0, 0, G_OPTION_ARG_STRING, &(common_args->output_dest), N_( "Specify file name for output (or \"-\" for stdout)"), N_("DEST") }, { NULL } }; if (*output_group == NULL) { *output_group = g_option_group_new("output", N_("Output Options:"), N_("Show output help"), NULL, NULL); } common_args->output_as_descr = crm_strdup_printf("Specify output format as one of: %s", fmts); output_entries[0].description = common_args->output_as_descr; g_option_group_add_entries(*output_group, output_entries); g_option_context_add_group(context, *output_group); } // main_group is now owned by context, we don't free it here return context; } void pcmk__free_arg_context(GOptionContext *context) { if (context == NULL) { return; } g_option_context_free(context); } void pcmk__add_main_args(GOptionContext *context, const GOptionEntry entries[]) { GOptionGroup *main_group = g_option_context_get_main_group(context); g_option_group_add_entries(main_group, entries); } void pcmk__add_arg_group(GOptionContext *context, const char *name, const char *header, const char *desc, const GOptionEntry entries[]) { GOptionGroup *group = NULL; group = g_option_group_new(name, header, desc, NULL, NULL); g_option_group_add_entries(group, entries); g_option_context_add_group(context, group); // group is now owned by context, we don't free it here } -static gchar * -string_replace(gchar *str, const gchar *sub, const gchar *repl) -{ - /* This function just replaces all occurrences of a substring - * with some other string. It doesn't handle cases like overlapping, - * so don't get clever with it. - * - * FIXME: When glib >= 2.68 is supported, we can get rid of this - * function and use g_string_replace instead. - */ - gchar **split = g_strsplit(str, sub, 0); - gchar *retval = g_strjoinv(repl, split); - - g_strfreev(split); - return retval; -} - gchar * pcmk__quote_cmdline(gchar **argv) { - GString *gs = NULL; + GString *cmdline = NULL; - if (argv == NULL || argv[0] == NULL) { + if (argv == NULL) { return NULL; } - gs = g_string_sized_new(100); - for (int i = 0; argv[i] != NULL; i++) { - if (i > 0) { - g_string_append_c(gs, ' '); - } + gint argc = 0; + + /* Quote the argument if it's unparsable as-is (empty, all whitespace, + * or having mismatched quotes), or if it contains more than one token + */ + if (!g_shell_parse_argv(argv[i], &argc, NULL, NULL) || (argc > 1)) { + gchar *quoted = g_shell_quote(argv[i]); + + pcmk__add_word(&cmdline, 128, quoted); + g_free(quoted); - if (strchr(argv[i], ' ') == NULL) { - /* The arg does not contain a space. */ - g_string_append(gs, argv[i]); - } else if (strchr(argv[i], '\'') == NULL) { - /* The arg contains a space, but not a single quote. */ - pcmk__g_strcat(gs, "'", argv[i], "'", NULL); } else { - /* The arg contains both a space and a single quote, which needs to - * be replaced with an escaped version. We do this instead of counting - * on libxml to handle the escaping for various reasons: - * - * (1) This keeps the string as valid shell. - * (2) We don't want to use XML entities in formats besides XML and HTML. - * (3) The string we are feeding to libxml is something like: "a b 'c d' e". - * It won't escape the single quotes around 'c d' here because there is - * no need to escape quotes inside a different form of quote. If we - * change the string to "a b 'c'd' e", we haven't changed anything - it's - * still single quotes inside double quotes. - * - * On the other hand, if we replace the single quote with "'", then - * we have introduced an ampersand which libxml will escape. This leaves - * us with "&apos;" which is not what we want. - * - * It's simplest to just escape with a backslash. - */ - gchar *repl = string_replace(argv[i], "'", "\\\'"); - pcmk__g_strcat(gs, "'", repl, "'", NULL); - g_free(repl); + pcmk__add_word(&cmdline, 128, argv[i]); } } - return g_string_free(gs, FALSE); + if (cmdline == NULL) { + return NULL; + } + return g_string_free(cmdline, FALSE); } gchar ** pcmk__cmdline_preproc(char *const *argv, const char *special) { GPtrArray *arr = NULL; bool saw_dash_dash = false; bool copy_option = false; if (argv == NULL) { return NULL; } if (g_get_prgname() == NULL && argv && *argv) { gchar *basename = g_path_get_basename(*argv); g_set_prgname(basename); g_free(basename); } arr = g_ptr_array_new(); for (int i = 0; argv[i] != NULL; i++) { /* If this is the first time we saw "--" in the command line, set * a flag so we know to just copy everything after it over. We also * want to copy the "--" over so whatever actually parses the command * line when we're done knows where arguments end. */ if (saw_dash_dash == false && strcmp(argv[i], "--") == 0) { saw_dash_dash = true; } if (saw_dash_dash == true) { g_ptr_array_add(arr, g_strdup(argv[i])); continue; } if (copy_option == true) { g_ptr_array_add(arr, g_strdup(argv[i])); copy_option = false; continue; } /* This is just a dash by itself. That could indicate stdin/stdout, or * it could be user error. Copy it over and let glib figure it out. */ if (pcmk__str_eq(argv[i], "-", pcmk__str_casei)) { g_ptr_array_add(arr, g_strdup(argv[i])); continue; } /* "-INFINITY" is almost certainly meant as a string, not as an option * list */ if (strcmp(argv[i], "-INFINITY") == 0) { g_ptr_array_add(arr, g_strdup(argv[i])); continue; } /* This is a short argument, or perhaps several. Iterate over it * and explode them out into individual arguments. */ if (g_str_has_prefix(argv[i], "-") && !g_str_has_prefix(argv[i], "--")) { /* Skip over leading dash */ const char *ch = argv[i]+1; /* This looks like the start of a number, which means it is a negative * number. It's probably the argument to the preceeding option, but * we can't know that here. Copy it over and let whatever handles * arguments next figure it out. */ if (*ch != '\0' && *ch >= '1' && *ch <= '9') { bool is_numeric = true; while (*ch != '\0') { if (!isdigit(*ch)) { is_numeric = false; break; } ch++; } if (is_numeric) { g_ptr_array_add(arr, g_strdup_printf("%s", argv[i])); continue; } else { /* This argument wasn't entirely numeric. Reset ch to the * beginning so we can process it one character at a time. */ ch = argv[i]+1; } } while (*ch != '\0') { /* This is a special short argument that takes an option. getopt * allows values to be interspersed with a list of arguments, but * glib does not. Grab both the argument and its value and * separate them into a new argument. */ if (special != NULL && strchr(special, *ch) != NULL) { /* The argument does not occur at the end of this string of * arguments. Take everything through the end as its value. */ if (*(ch+1) != '\0') { fprintf(stderr, "Deprecated argument format '-%c%s' used.\n", *ch, ch+1); fprintf(stderr, "Please use '-%c %s' instead. " "Support will be removed in a future release.\n", *ch, ch+1); g_ptr_array_add(arr, g_strdup_printf("-%c", *ch)); g_ptr_array_add(arr, g_strdup(ch+1)); break; /* The argument occurs at the end of this string. Hopefully * whatever comes next in argv is its value. It may not be, * but that is not for us to decide. */ } else { g_ptr_array_add(arr, g_strdup_printf("-%c", *ch)); copy_option = true; ch++; } /* This is a regular short argument. Just copy it over. */ } else { g_ptr_array_add(arr, g_strdup_printf("-%c", *ch)); ch++; } } /* This is a long argument, or an option, or something else. * Copy it over - everything else is copied, so this keeps it easy for * the caller to know what to do with the memory when it's done. */ } else { g_ptr_array_add(arr, g_strdup(argv[i])); } } g_ptr_array_add(arr, NULL); return (char **) g_ptr_array_free(arr, FALSE); } G_GNUC_PRINTF(3, 4) gboolean pcmk__force_args(GOptionContext *context, GError **error, const char *format, ...) { int len = 0; char *buf = NULL; gchar **extra_args = NULL; va_list ap; gboolean retval = TRUE; va_start(ap, format); len = vasprintf(&buf, format, ap); pcmk__assert(len > 0); va_end(ap); if (!g_shell_parse_argv(buf, NULL, &extra_args, error)) { g_strfreev(extra_args); free(buf); return FALSE; } retval = g_option_context_parse_strv(context, &extra_args, error); g_strfreev(extra_args); free(buf); return retval; } diff --git a/lib/common/tests/cmdline/pcmk__quote_cmdline_test.c b/lib/common/tests/cmdline/pcmk__quote_cmdline_test.c index 42bd8cad68..bd0817909e 100644 --- a/lib/common/tests/cmdline/pcmk__quote_cmdline_test.c +++ b/lib/common/tests/cmdline/pcmk__quote_cmdline_test.c @@ -1,56 +1,86 @@ /* - * Copyright 2022 the Pacemaker project contributors + * Copyright 2022-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include +/* g_shell_quote currently always uses single quotes. However, the documentation + * says "The quoting style used is undefined (single or double quotes may be + * used)." + */ +static void +assert_quote_cmdline(const char **argv, const gchar *expected_single, + const gchar *expected_double) +{ + gchar *processed = pcmk__quote_cmdline((gchar **) argv); + + assert_true(pcmk__str_any_of(processed, expected_single, expected_double, + NULL)); + g_free(processed); +} + static void -empty_input(void **state) { +empty_input(void **state) +{ assert_null(pcmk__quote_cmdline(NULL)); } static void -no_spaces(void **state) { - const char *argv[] = { "crm_resource", "-r", "rsc1", "--meta", "-p", "comment", "-v", "hello", "--output-as=xml", NULL }; - const gchar *expected = "crm_resource -r rsc1 --meta -p comment -v hello --output-as=xml"; +no_spaces(void **state) +{ + const char *argv[] = { + "crm_resource", "-r", "rsc1", "--meta", "-p", "comment", + "-v", "hello", "--output-as=xml", NULL, + }; - gchar *processed = pcmk__quote_cmdline((gchar **) argv); - assert_string_equal(processed, expected); - g_free(processed); + assert_quote_cmdline(argv, + "crm_resource -r rsc1 --meta -p comment " + "-v hello --output-as=xml", + "crm_resource -r rsc1 --meta -p comment " + "-v hello --output-as=xml"); } static void -spaces_no_quote(void **state) { - const char *argv[] = { "crm_resource", "-r", "rsc1", "--meta", "-p", "comment", "-v", "hello world", "--output-as=xml", NULL }; - const gchar *expected = "crm_resource -r rsc1 --meta -p comment -v 'hello world' --output-as=xml"; +spaces_no_quote(void **state) +{ + const char *argv[] = { + "crm_resource", "-r", "rsc1", "--meta", "-p", "comment", + "-v", "hello world", "--output-as=xml", NULL, + }; - gchar *processed = pcmk__quote_cmdline((gchar **) argv); - assert_string_equal(processed, expected); - g_free(processed); + assert_quote_cmdline(argv, + "crm_resource -r rsc1 --meta -p comment " + "-v 'hello world' --output-as=xml", + "crm_resource -r rsc1 --meta -p comment " + "-v \"hello world\" --output-as=xml"); } static void spaces_with_quote(void **state) { - const char *argv[] = { "crm_resource", "-r", "rsc1", "--meta", "-p", "comment", "-v", "here's johnny", "--output-as=xml", NULL }; - const gchar *expected = "crm_resource -r rsc1 --meta -p comment -v 'here\\\'s johnny' --output-as=xml"; + const char *argv[] = { + "crm_resource", "-r", "rsc1", "--meta", "-p", "comment", + "-v", "here's johnny", "--output-as=xml", NULL, + }; - gchar *processed = pcmk__quote_cmdline((gchar **) argv); - assert_string_equal(processed, expected); - g_free(processed); + assert_quote_cmdline(argv, + "crm_resource -r rsc1 --meta -p comment " + "-v 'here'\\''s johnny' --output-as=xml", + "crm_resource -r rsc1 --meta -p comment " + "-v \"here's johnny\" --output-as=xml"); } PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(empty_input), cmocka_unit_test(no_spaces), cmocka_unit_test(spaces_no_quote), cmocka_unit_test(spaces_with_quote))