From 8b59b9dab54c094dfc9bafd9d9f2c18f25877f36 Mon Sep 17 00:00:00 2001 Message-Id: <8b59b9dab54c094dfc9bafd9d9f2c18f25877f36@dist-git> From: Paulo de Rezende Pinatti Date: Wed, 24 Jun 2020 13:16:17 +0200 Subject: [PATCH] util: Introduce a parser for kernel cmdline arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce two utility functions to parse a kernel command line string according to the kernel code parsing rules in order to enable the caller to perform operations such as verifying whether certain argument=value combinations are present or retrieving an argument's value. Signed-off-by: Paulo de Rezende Pinatti Signed-off-by: Boris Fiuczynski Reviewed-by: Erik Skultety (cherry picked from commit c5fffb959d93b83d87e70b21d19424e9722700b0) https://bugzilla.redhat.com/show_bug.cgi?id=1848997 https://bugzilla.redhat.com/show_bug.cgi?id=1850351 Conflicts: src/util/virutil.c - unrelated commits db72866310d and ab36f729470 were not backported Signed-off-by: Jiri Denemark Message-Id: <784fbc062d41f991b6321ac051b05e6c80a470cd.1592996194.git.jdenemar@redhat.com> Reviewed-by: Ján Tomko --- src/libvirt_private.syms | 2 + src/util/virutil.c | 185 +++++++++++++++++++++++++++++++++++++++ src/util/virutil.h | 34 +++++++ tests/utiltest.c | 136 ++++++++++++++++++++++++++++ 4 files changed, 357 insertions(+) diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms index a3fe49ae33..9e290c7bdf 100644 --- a/src/libvirt_private.syms +++ b/src/libvirt_private.syms @@ -3395,6 +3395,8 @@ virHexToBin; virHostGetDRMRenderNode; virHostHasIOMMU; virIndexToDiskName; +virKernelCmdlineMatchParam; +virKernelCmdlineNextParam; virMemoryLimitIsSet; virMemoryLimitTruncate; virMemoryMaxValue; diff --git a/src/util/virutil.c b/src/util/virutil.c index 261b2d2af6..17fd06dbb2 100644 --- a/src/util/virutil.c +++ b/src/util/virutil.c @@ -1673,3 +1673,188 @@ virHostGetDRMRenderNode(void) VIR_DIR_CLOSE(driDir); return ret; } + + +static const char *virKernelCmdlineSkipQuote(const char *cmdline, + bool *is_quoted) +{ + if (cmdline[0] == '"') { + *is_quoted = !(*is_quoted); + cmdline++; + } + return cmdline; +} + + +/** + * virKernelCmdlineFindEqual: + * @cmdline: target kernel command line string + * @is_quoted: indicates whether the string begins with quotes + * @res: pointer to the position immediately after the parsed parameter, + * can be used in subsequent calls to process further parameters until + * the end of the string. + * + * Iterate over the provided kernel command line string while honoring + * the kernel quoting rules and returns the index of the equal sign + * separating argument and value. + * + * Returns 0 for the cases where no equal sign is found or the argument + * itself begins with the equal sign (both cases indicating that the + * argument has no value). Otherwise, returns the index of the equal + * sign in the string. + */ +static size_t virKernelCmdlineFindEqual(const char *cmdline, + bool is_quoted, + const char **res) +{ + size_t i; + size_t equal_index = 0; + + for (i = 0; cmdline[i]; i++) { + if (!(is_quoted) && g_ascii_isspace(cmdline[i])) + break; + if (equal_index == 0 && cmdline[i] == '=') { + equal_index = i; + continue; + } + virKernelCmdlineSkipQuote(cmdline + i, &is_quoted); + } + *res = cmdline + i; + return equal_index; +} + + +static char* virKernelArgNormalize(const char *arg) +{ + return virStringReplace(arg, "_", "-"); +} + + +/** + * virKernelCmdlineNextParam: + * @cmdline: kernel command line string to be checked for next parameter + * @param: pointer to hold retrieved parameter, will be NULL if none found + * @val: pointer to hold retrieved value of @param + * + * Parse the kernel cmdline and store the next parameter in @param + * and the value of @param in @val which can be NULL if @param has + * no value. In addition returns the address right after @param=@value + * for possible further processing. + * + * Returns a pointer to address right after @param=@val in the + * kernel command line, will point to the string's end (NULL) + * in case no next parameter is found + */ +const char *virKernelCmdlineNextParam(const char *cmdline, + char **param, + char **val) +{ + const char *next; + int equal_index; + bool is_quoted = false; + *param = NULL; + *val = NULL; + + virSkipSpaces(&cmdline); + cmdline = virKernelCmdlineSkipQuote(cmdline, &is_quoted); + equal_index = virKernelCmdlineFindEqual(cmdline, is_quoted, &next); + + if (next == cmdline) + return next; + + /* param has no value */ + if (equal_index == 0) { + if (is_quoted && next[-1] == '"') + *param = g_strndup(cmdline, next - cmdline - 1); + else + *param = g_strndup(cmdline, next - cmdline); + return next; + } + + *param = g_strndup(cmdline, equal_index); + + if (cmdline[equal_index + 1] == '"') { + is_quoted = true; + equal_index++; + } + + if (is_quoted && next[-1] == '"') + *val = g_strndup(cmdline + equal_index + 1, + next - cmdline - equal_index - 2); + else + *val = g_strndup(cmdline + equal_index + 1, + next - cmdline - equal_index - 1); + return next; +} + + +static bool virKernelCmdlineStrCmp(const char *kernel_val, + const char *caller_val, + virKernelCmdlineFlags flags) +{ + if (flags & VIR_KERNEL_CMDLINE_FLAGS_CMP_PREFIX) + return STRPREFIX(kernel_val, caller_val); + return STREQ(kernel_val, caller_val); +} + + +/** + * virKernelCmdlineMatchParam: + * @cmdline: kernel command line string to be checked for @arg + * @arg: kernel command line argument + * @values: array of possible values to match @arg + * @len_values: size of array, it can be 0 meaning a match will be positive if + * the argument has no value. + * @flags: bitwise-OR of virKernelCmdlineFlags + * + * Try to match the provided kernel cmdline string with the provided @arg + * and the list @values of possible values according to the matching strategy + * defined in @flags. + * + * + * Returns true if a match is found, false otherwise + */ +bool virKernelCmdlineMatchParam(const char *cmdline, + const char *arg, + const char **values, + size_t len_values, + virKernelCmdlineFlags flags) +{ + bool match = false; + size_t i; + const char *next = cmdline; + g_autofree char *arg_norm = virKernelArgNormalize(arg); + + while (next[0] != '\0') { + g_autofree char *kparam = NULL; + g_autofree char *kparam_norm = NULL; + g_autofree char *kval = NULL; + + next = virKernelCmdlineNextParam(next, &kparam, &kval); + + if (!kparam) + break; + + kparam_norm = virKernelArgNormalize(kparam); + + if (STRNEQ(kparam_norm, arg_norm)) + continue; + + if (!kval) { + match = (len_values == 0) ? true : false; + } else { + match = false; + for (i = 0; i < len_values; i++) { + if (virKernelCmdlineStrCmp(kval, values[i], flags)) { + match = true; + break; + } + } + } + + if (match && (flags & VIR_KERNEL_CMDLINE_FLAGS_SEARCH_FIRST)) + break; + } + + return match; +} diff --git a/src/util/virutil.h b/src/util/virutil.h index 0dcaff79ac..f1d2ccdd1f 100644 --- a/src/util/virutil.h +++ b/src/util/virutil.h @@ -145,6 +145,40 @@ bool virHostHasIOMMU(void); char *virHostGetDRMRenderNode(void) G_GNUC_NO_INLINE; +/* Kernel cmdline match and comparison strategy for arg=value pairs */ +typedef enum { + /* substring comparison of argument values */ + VIR_KERNEL_CMDLINE_FLAGS_CMP_PREFIX = 1, + + /* strict string comparison of argument values */ + VIR_KERNEL_CMDLINE_FLAGS_CMP_EQ = 2, + + /* look for any occurrence of the argument with the expected value, + * this should be used when an argument set to the expected value overrides + * all the other occurrences of the argument, e.g. when looking for 'arg=1' + * in 'arg=0 arg=1 arg=0' the search would succeed with this flag + */ + VIR_KERNEL_CMDLINE_FLAGS_SEARCH_FIRST = 4, + + /* look for the last occurrence of argument with the expected value, + * this should be used when the last occurrence of the argument overrides + * all the other ones, e.g. when looking for 'arg=1' in 'arg=0 arg=1' the + * search would succeed with this flag, but in 'arg=1 arg=0' it would not, + * because 'arg=0' overrides all the previous occurrences of 'arg' + */ + VIR_KERNEL_CMDLINE_FLAGS_SEARCH_LAST = 8, +} virKernelCmdlineFlags; + +const char *virKernelCmdlineNextParam(const char *cmdline, + char **param, + char **val); + +bool virKernelCmdlineMatchParam(const char *cmdline, + const char *arg, + const char **values, + size_t len_values, + virKernelCmdlineFlags flags); + /** * VIR_ASSIGN_IS_OVERFLOW: * @rvalue: value that is checked (evaluated twice) diff --git a/tests/utiltest.c b/tests/utiltest.c index 5ae04585cb..2bff7859dc 100644 --- a/tests/utiltest.c +++ b/tests/utiltest.c @@ -254,6 +254,140 @@ testOverflowCheckMacro(const void *data G_GNUC_UNUSED) } +struct testKernelCmdlineNextParamData +{ + const char *cmdline; + const char *param; + const char *val; + const char *next; +}; + +static struct testKernelCmdlineNextParamData kEntries[] = { + { "arg1 arg2 arg3=val1", "arg1", NULL, " arg2 arg3=val1" }, + { "arg1=val1 arg2 arg3=val3 arg4", "arg1", "val1", " arg2 arg3=val3 arg4" }, + { "arg1=sub1=val1,sub2=val2 arg3=val3 arg4", "arg1", "sub1=val1,sub2=val2", " arg3=val3 arg4" }, + { "arg3=val3 ", "arg3", "val3", " " }, + { "arg3=val3", "arg3", "val3", "" }, + { "arg-3=val3 arg4", "arg-3", "val3", " arg4" }, + { " arg_3=val3 arg4", "arg_3", "val3", " arg4" }, + { "arg2=\"value with space\" arg3=val3", "arg2", "value with space", " arg3=val3" }, + { " arg2=\"value with space\" arg3=val3", "arg2", "value with space", " arg3=val3" }, + { " \"arg2=value with space\" arg3=val3", "arg2", "value with space", " arg3=val3" }, + { "arg2=\"val\"ue arg3", "arg2", "val\"ue", " arg3" }, + { "arg2=value\" long\" arg3", "arg2", "value\" long\"", " arg3" }, + { " \"arg2 with space=value with space\" arg3", "arg2 with space", "value with space", " arg3" }, + { " arg2\" with space=val2\" arg3", "arg2\" with space", "val2\"", " arg3" }, + { " arg2longer=someval\" long\" arg2=val2", "arg2longer", "someval\" long\"", " arg2=val2" }, + { "=val1 arg2=val2", "=val1", NULL, " arg2=val2" }, + { " ", NULL, NULL, "" }, + { "", NULL, NULL, "" }, +}; + +static int +testKernelCmdlineNextParam(const void *data G_GNUC_UNUSED) +{ + const char *next; + size_t i; + + for (i = 0; i < G_N_ELEMENTS(kEntries); ++i) { + g_autofree char * param = NULL; + g_autofree char * val = NULL; + + next = virKernelCmdlineNextParam(kEntries[i].cmdline, ¶m, &val); + + if (STRNEQ_NULLABLE(param, kEntries[i].param) || + STRNEQ_NULLABLE(val, kEntries[i].val) || + STRNEQ(next, kEntries[i].next)) { + VIR_TEST_DEBUG("\nKernel cmdline [%s]", kEntries[i].cmdline); + VIR_TEST_DEBUG("Expect param [%s]", kEntries[i].param); + VIR_TEST_DEBUG("Actual param [%s]", param); + VIR_TEST_DEBUG("Expect value [%s]", kEntries[i].val); + VIR_TEST_DEBUG("Actual value [%s]", val); + VIR_TEST_DEBUG("Expect next [%s]", kEntries[i].next); + VIR_TEST_DEBUG("Actual next [%s]", next); + + return -1; + } + } + + return 0; +} + + +struct testKernelCmdlineMatchData +{ + const char *cmdline; + const char *arg; + const char *values[2]; + virKernelCmdlineFlags flags; + bool result; +}; + +static struct testKernelCmdlineMatchData kMatchEntries[] = { + {"arg1 myarg=no arg2=val2 myarg=yes arg4=val4 myarg=no arg5", "myarg", {"1", "y"}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_FIRST | VIR_KERNEL_CMDLINE_FLAGS_CMP_EQ, false }, + {"arg1 myarg=no arg2=val2 myarg=yes arg4=val4 myarg=no arg5", "myarg", {"on", "yes"}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_FIRST | VIR_KERNEL_CMDLINE_FLAGS_CMP_EQ, true }, + {"arg1 myarg=no arg2=val2 myarg=yes arg4=val4 myarg=no arg5", "myarg", {"1", "y"}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_FIRST | VIR_KERNEL_CMDLINE_FLAGS_CMP_PREFIX, true }, + {"arg1 myarg=no arg2=val2 myarg=yes arg4=val4 myarg=no arg5", "myarg", {"a", "b"}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_FIRST | VIR_KERNEL_CMDLINE_FLAGS_CMP_PREFIX, false }, + {"arg1 myarg=no arg2=val2 myarg=yes arg4=val4 myarg=no arg5", "myarg", {"on", "yes"}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_LAST | VIR_KERNEL_CMDLINE_FLAGS_CMP_EQ, false }, + {"arg1 myarg=no arg2=val2 myarg=yes arg4=val4 myarg=no arg5", "myarg", {"1", "y"}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_LAST | VIR_KERNEL_CMDLINE_FLAGS_CMP_PREFIX, false }, + {"arg1 myarg=no arg2=val2 arg4=val4 myarg=yes arg5", "myarg", {"on", "yes"}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_LAST | VIR_KERNEL_CMDLINE_FLAGS_CMP_EQ, true }, + {"arg1 myarg=no arg2=val2 arg4=val4 myarg=yes arg5", "myarg", {"1", "y"}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_LAST | VIR_KERNEL_CMDLINE_FLAGS_CMP_PREFIX, true }, + {"arg1 myarg=no arg2=val2 arg4=val4 myarg arg5", "myarg", {NULL, NULL}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_LAST, true }, + {"arg1 myarg arg2=val2 arg4=val4 myarg=yes arg5", "myarg", {NULL, NULL}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_FIRST, true }, + {"arg1 myarg arg2=val2 arg4=val4 myarg=yes arg5", "myarg", {NULL, NULL}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_LAST, false }, + {"arg1 my-arg=no arg2=val2 arg4=val4 my_arg=yes arg5", "my-arg", {"on", "yes"}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_LAST, true }, + {"arg1 my-arg=no arg2=val2 arg4=val4 my_arg=yes arg5 ", "my-arg", {"on", "yes"}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_LAST | VIR_KERNEL_CMDLINE_FLAGS_CMP_EQ, true }, + {"arg1 my-arg arg2=val2 arg4=val4 my_arg=yes arg5", "my_arg", {NULL, NULL}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_FIRST, true }, + {"arg1 my-arg arg2=val2 arg4=val4 my-arg=yes arg5", "my_arg", {NULL, NULL}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_FIRST, true }, + {"=arg1 my-arg arg2=val2 arg4=val4 my-arg=yes arg5", "my_arg", {NULL, NULL}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_FIRST, true }, + {"my-arg =arg1 arg2=val2 arg4=val4 my-arg=yes arg5", "=arg1", {NULL, NULL}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_LAST, true }, + {"arg1 arg2=val2 myarg=sub1=val1 arg5", "myarg", {"sub1=val1", NULL}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_LAST, true }, + {"arg1 arg2=", "arg2", {"", ""}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_LAST | VIR_KERNEL_CMDLINE_FLAGS_CMP_EQ, true }, + {" ", "myarg", {NULL, NULL}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_LAST, false }, + {"", "", {NULL, NULL}, VIR_KERNEL_CMDLINE_FLAGS_SEARCH_LAST, false }, +}; + + +static int +testKernelCmdlineMatchParam(const void *data G_GNUC_UNUSED) +{ + bool result; + size_t i, lenValues; + + for (i = 0; i < G_N_ELEMENTS(kMatchEntries); ++i) { + if (kMatchEntries[i].values[0] == NULL) + lenValues = 0; + else + lenValues = G_N_ELEMENTS(kMatchEntries[i].values); + + result = virKernelCmdlineMatchParam(kMatchEntries[i].cmdline, + kMatchEntries[i].arg, + kMatchEntries[i].values, + lenValues, + kMatchEntries[i].flags); + + if (result != kMatchEntries[i].result) { + VIR_TEST_DEBUG("\nKernel cmdline [%s]", kMatchEntries[i].cmdline); + VIR_TEST_DEBUG("Kernel argument [%s]", kMatchEntries[i].arg); + VIR_TEST_DEBUG("Kernel values [%s] [%s]", kMatchEntries[i].values[0], + kMatchEntries[i].values[1]); + if (kMatchEntries[i].flags & VIR_KERNEL_CMDLINE_FLAGS_CMP_PREFIX) + VIR_TEST_DEBUG("Flag [VIR_KERNEL_CMDLINE_FLAGS_CMP_PREFIX]"); + if (kMatchEntries[i].flags & VIR_KERNEL_CMDLINE_FLAGS_CMP_EQ) + VIR_TEST_DEBUG("Flag [VIR_KERNEL_CMDLINE_FLAGS_CMP_EQ]"); + if (kMatchEntries[i].flags & VIR_KERNEL_CMDLINE_FLAGS_SEARCH_FIRST) + VIR_TEST_DEBUG("Flag [VIR_KERNEL_CMDLINE_FLAGS_SEARCH_FIRST]"); + if (kMatchEntries[i].flags & VIR_KERNEL_CMDLINE_FLAGS_SEARCH_LAST) + VIR_TEST_DEBUG("Flag [VIR_KERNEL_CMDLINE_FLAGS_SEARCH_LAST]"); + VIR_TEST_DEBUG("Expect result [%d]", kMatchEntries[i].result); + VIR_TEST_DEBUG("Actual result [%d]", result); + + return -1; + } + } + + return 0; +} static int @@ -277,6 +411,8 @@ mymain(void) DO_TEST(ParseVersionString); DO_TEST(RoundValueToPowerOfTwo); DO_TEST(OverflowCheckMacro); + DO_TEST(KernelCmdlineNextParam); + DO_TEST(KernelCmdlineMatchParam); return result == 0 ? EXIT_SUCCESS : EXIT_FAILURE; } -- 2.27.0