From 609f8d0214692a7b0a6dfd84ed470e7194d24823 Mon Sep 17 00:00:00 2001 From: wyu <97944796+wyu71@users.noreply.github.com> Date: Wed, 12 Aug 2026 08:30:00 +0800 Subject: [PATCH 1/3] extract-word: also handle EXTRACT_UNESCAPE_RELAX in the escape branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit extract_first_word() handled a backslash followed by another character in the escape-processing branch only when EXTRACT_CUNESCAPE or EXTRACT_UNESCAPE_SEPARATORS was set. With EXTRACT_UNESCAPE_RELAX alone, the backslash was silently dropped and only the following character kept, which is inconsistent with the documented "keep invalid escapes verbatim" intent of RELAX. Let RELAX also enter that branch, so an invalid escape like "\x" is preserved verbatim as "\x" rather than collapsing to "x". EXTRACT_UNESCAPE_RELAX is not used on its own anywhere outside the test cases — the non-test callers always combine it with EXTRACT_CUNESCAPE, which already enters this branch — so only the test cases that use RELAX by itself are affected, and their expectations are updated accordingly. --- src/basic/extract-word.c | 2 +- src/test/test-extract-word.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/basic/extract-word.c b/src/basic/extract-word.c index 5ebf67437c..c21ac1fde5 100644 --- a/src/basic/extract-word.c +++ b/src/basic/extract-word.c @@ -78,7 +78,7 @@ int extract_first_word(const char **p, char **ret, const char *separators, Extra return -EINVAL; } - if (flags & (EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_SEPARATORS)) { + if (flags & (EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_SEPARATORS|EXTRACT_UNESCAPE_RELAX)) { bool eight_bit = false; char32_t u; diff --git a/src/test/test-extract-word.c b/src/test/test-extract-word.c index aad9bbc47d..3e11f5922b 100644 --- a/src/test/test-extract-word.c +++ b/src/test/test-extract-word.c @@ -247,13 +247,13 @@ TEST(extract_first_word) { p = original = "fooo\\ bar quux"; assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNESCAPE_RELAX) > 0); - ASSERT_STREQ(t, "fooo bar"); + ASSERT_STREQ(t, "fooo\\ bar"); free(t); assert_se(p == original + 10); p = original = "fooo\\ bar quux"; assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNESCAPE_RELAX|EXTRACT_RELAX) > 0); - ASSERT_STREQ(t, "fooo bar"); + ASSERT_STREQ(t, "fooo\\ bar"); free(t); assert_se(p == original + 10); From eb5ee83c1f7e0b8287c9ca9b1535d3e94845476a Mon Sep 17 00:00:00 2001 From: wyu <97944796+wyu71@users.noreply.github.com> Date: Wed, 12 Aug 2026 09:43:55 +0800 Subject: [PATCH 2/3] fstab-filter: preserve fstab option escaping in the filtered output fstab_filter_options() used strv_split_full(..., EXTRACT_UNESCAPE_SEPARATORS | EXTRACT_UNESCAPE_RELAX), which unescapes "\," and "\\" while splitting, and then strv_join_full(escape_separator=true) to re-escape the kept options. The round-trip was lossy for invalid escapes kept verbatim by RELAX (e.g. an option value "\xff" became "\\xff"), so the generated Options= line diverged from the input. A trailing backslash was also collapsed: "val1\\" in the fstab became "val1\" in the split word, which let the backslash escape the option separator in Options= (issue #42787). Rework fstab_filter_options() to split with EXTRACT_UNESCAPE_RELAX only, so the escape sequences of kept options are preserved verbatim and the filtered output is assembled with the plain strv_join(); the matching option values are unescaped manually so callers keep getting the unescaped form they had before. Fixes a bug introduced by d4d9f034b13acef37375daec5074d9d271e21eed. Fixes #42787. --- src/shared/fstab-util.c | 48 ++++++++++++++++++++++++++++++++++---- src/test/test-fstab-util.c | 2 +- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/shared/fstab-util.c b/src/shared/fstab-util.c index 25e229bf3e..a4f5e348ab 100644 --- a/src/shared/fstab-util.c +++ b/src/shared/fstab-util.c @@ -200,6 +200,34 @@ int fstab_is_mount_point_full(const char *where, const char *path) { #endif } +static char* fstab_unescape_option(const char *s) { + /* Undo the fstab option escaping: "\," -> ",", "\\" -> "\", and keep any + * other "\x" verbatim (matches EXTRACT_UNESCAPE_SEPARATORS|EXTRACT_UNESCAPE_RELAX). */ + char *ret, *e; + + assert(s); + + ret = new(char, strlen(s) + 1); + if (!ret) + return NULL; + + e = ret; + for (const char *p = s; *p; p++) { + if (*p == '\\' && p[1] != '\0') { + if (IN_SET(p[1], ',', '\\')) { + *e++ = p[1]; + p++; + continue; + } + /* Keep the backslash and the following character verbatim. */ + *e++ = *p++; + } + *e++ = *p; + } + *e = '\0'; + return ret; +} + int fstab_filter_options( const char *opts, const char *names, @@ -234,8 +262,10 @@ int fstab_filter_options( _cleanup_free_ char **filtered_strv = NULL; /* strings are owned by 'opts_split' */ /* For backwards compatibility, we need to pass-through escape characters. - * The only ones we "consume" are the ones used as "\," or "\\". */ - r = strv_split_full(&opts_split, opts, ",", EXTRACT_UNESCAPE_SEPARATORS|EXTRACT_UNESCAPE_RELAX); + * The only ones we "consume" are the ones used as "\," or "\\". The option + * values are not unescaped by strv_split_full() here; matching option values are + * unescaped manually below so that callers keep getting the unescaped form. */ + r = strv_split_full(&opts_split, opts, ",", EXTRACT_UNESCAPE_RELAX); if (r < 0) return r; @@ -257,10 +287,18 @@ int fstab_filter_options( } if (found) { + _cleanup_free_ char *unescaped = NULL; + + if ((ret_value && *x == '=') || ret_values) { + unescaped = fstab_unescape_option(x + 1); + if (!unescaped) + return -ENOMEM; + } + if (ret_value) - r = free_and_strdup(&value, *x == '=' ? x + 1 : NULL); + r = free_and_replace(value, unescaped); else if (ret_values) - r = strv_extend(&values, x + 1); + r = strv_extend(&values, unescaped); else r = 0; } else @@ -270,7 +308,7 @@ int fstab_filter_options( } if (ret_filtered) { - filtered = strv_join_full(filtered_strv, ",", NULL, /* escape_separator= */ true); + filtered = strv_join(filtered_strv, ","); if (!filtered) return -ENOMEM; } diff --git a/src/test/test-fstab-util.c b/src/test/test-fstab-util.c index 773b1f9799..63b70863e6 100644 --- a/src/test/test-fstab-util.c +++ b/src/test/test-fstab-util.c @@ -125,7 +125,7 @@ TEST(fstab_filter_options) { /* escaped characters */ do_fstab_filter_options("opt1=\\\\,opt2=\\xff", "opt1\0", 1, 1, "opt1", "\\", "\\", "opt2=\\xff"); - do_fstab_filter_options("opt1=\\\\,opt2=\\xff", "opt2\0", 1, 1, "opt2", "\\xff", "\\xff", "opt1=\\"); + do_fstab_filter_options("opt1=\\\\,opt2=\\xff", "opt2\0", 1, 1, "opt2", "\\xff", "\\xff", "opt1=\\\\"); } TEST(fstab_find_pri) { From 7b00f957fece2bc63bf624b1c77291288145c94c Mon Sep 17 00:00:00 2001 From: wyu <97944796+wyu71@users.noreply.github.com> Date: Wed, 12 Aug 2026 09:43:57 +0800 Subject: [PATCH 3/3] strv: drop the escape_separator flag from strv_join_full() Now that fstab_filter_options() no longer uses strv_join_full() with escape_separator=true (it splits with EXTRACT_UNESCAPE_RELAX and joins the kept options verbatim), the escape_separator flag has no users. Drop it from strv_join_full() and its wrapper strv_join(), and update the remaining callers in kernel-install and the tests accordingly. --- src/basic/strv.c | 20 +++----------------- src/basic/strv.h | 4 ++-- src/kernel-install/kernel-install.c | 4 ++-- src/shared/options.c | 2 +- src/test/test-strv.c | 27 +++++++-------------------- 5 files changed, 15 insertions(+), 42 deletions(-) diff --git a/src/basic/strv.c b/src/basic/strv.c index eb3cd8d4b2..77057f12a6 100644 --- a/src/basic/strv.c +++ b/src/basic/strv.c @@ -529,7 +529,7 @@ int strv_split_colon_pairs(char ***t, const char *s) { return (int) n; } -char* strv_join_full(char * const *l, const char *separator, const char *prefix, bool escape_separator) { +char* strv_join_full(char * const *l, const char *separator, const char *prefix) { char *r, *e; size_t n, k, m; @@ -539,17 +539,12 @@ char* strv_join_full(char * const *l, const char *separator, const char *prefix, k = strlen(separator); m = strlen_ptr(prefix); - if (escape_separator) /* If the separator was multi-char, we wouldn't know how to escape it. */ - assert(k == 1); - n = 0; STRV_FOREACH(s, l) { if (s != l) n += k; - bool needs_escaping = escape_separator && strchr(*s, *separator); - - n += m + strlen(*s) * (1 + needs_escaping); + n += m + strlen(*s); } r = new(char, n+1); @@ -564,16 +559,7 @@ char* strv_join_full(char * const *l, const char *separator, const char *prefix, if (prefix) e = stpcpy(e, prefix); - bool needs_escaping = escape_separator && strchr(*s, *separator); - - if (needs_escaping) - for (size_t i = 0; (*s)[i]; i++) { - if ((*s)[i] == *separator) - *(e++) = '\\'; - *(e++) = (*s)[i]; - } - else - e = stpcpy(e, *s); + e = stpcpy(e, *s); } *e = 0; diff --git a/src/basic/strv.h b/src/basic/strv.h index 2a0c2730d1..5afd345358 100644 --- a/src/basic/strv.h +++ b/src/basic/strv.h @@ -120,9 +120,9 @@ char** strv_split_newlines(const char *s); * string in the vector is an empty string. */ int strv_split_colon_pairs(char ***t, const char *s); -char* strv_join_full(char * const *l, const char *separator, const char *prefix, bool escape_separator); +char* strv_join_full(char * const *l, const char *separator, const char *prefix); static inline char *strv_join(char * const *l, const char *separator) { - return strv_join_full(l, separator, NULL, false); + return strv_join_full(l, separator, NULL); } bool strv_overlap(char * const *a, char * const *b) _pure_; diff --git a/src/kernel-install/kernel-install.c b/src/kernel-install/kernel-install.c index 69fdbcfc34..53f203f665 100644 --- a/src/kernel-install/kernel-install.c +++ b/src/kernel-install/kernel-install.c @@ -1093,10 +1093,10 @@ static int context_execute(Context *c) { return r; if (DEBUG_LOGGING) { - _cleanup_free_ char *x = strv_join_full(c->plugins, "", "\n ", /* escape_separator= */ false); + _cleanup_free_ char *x = strv_join_full(c->plugins, "", "\n "); log_debug("Using plugins: %s", strna(x)); - _cleanup_free_ char *y = strv_join_full(c->envp, "", "\n ", /* escape_separator= */ false); + _cleanup_free_ char *y = strv_join_full(c->envp, "", "\n "); log_debug("Plugin environment: %s", strna(y)); _cleanup_free_ char *z = strv_join(strv_skip(c->argv, 1), " "); diff --git a/src/shared/options.c b/src/shared/options.c index 1a24106fc5..68a275037c 100644 --- a/src/shared/options.c +++ b/src/shared/options.c @@ -64,7 +64,7 @@ static int partial_match_error( assert(strv_length(s) == n_partial_matches); - _cleanup_free_ char *p = strv_join_full(s, ", ", /* prefix= */ NULL, /* escape_separator= */ false); + _cleanup_free_ char *p = strv_join(s, ", "); return log_full_errno(LOG_ERR + state->log_level_shift, SYNTHETIC_ERRNO(EINVAL), "%s: option '%s' is ambiguous; possibilities: %s", diff --git a/src/test/test-strv.c b/src/test/test-strv.c index 53156a9412..105f34fe8d 100644 --- a/src/test/test-strv.c +++ b/src/test/test-strv.c @@ -159,44 +159,31 @@ TEST(strv_join) { } TEST(strv_join_full) { - _cleanup_free_ char *p = strv_join_full((char **)input_table_multiple, ", ", "foo", false); + _cleanup_free_ char *p = strv_join_full((char **)input_table_multiple, ", ", "foo"); assert_se(p); ASSERT_STREQ(p, "fooone, footwo, foothree"); - _cleanup_free_ char *q = strv_join_full((char **)input_table_multiple, ";", "foo", false); + _cleanup_free_ char *q = strv_join_full((char **)input_table_multiple, ";", "foo"); assert_se(q); ASSERT_STREQ(q, "fooone;footwo;foothree"); - _cleanup_free_ char *r = strv_join_full(STRV_MAKE("a", "a;b", "a:c"), ";", NULL, true); - assert_se(r); - ASSERT_STREQ(r, "a;a\\;b;a:c"); - - _cleanup_free_ char *s = strv_join_full(STRV_MAKE("a", "a;b", "a;;c", ";", ";x"), ";", NULL, true); - assert_se(s); - ASSERT_STREQ(s, "a;a\\;b;a\\;\\;c;\\;;\\;x"); - - _cleanup_free_ char *t = strv_join_full(STRV_MAKE("a", "a;b", "a:c", ";"), ";", "=", true); - assert_se(t); - ASSERT_STREQ(t, "=a;=a\\;b;=a:c;=\\;"); - t = mfree(t); - - _cleanup_free_ char *u = strv_join_full((char **)input_table_multiple, NULL, "foo", false); + _cleanup_free_ char *u = strv_join_full((char **)input_table_multiple, NULL, "foo"); assert_se(u); ASSERT_STREQ(u, "fooone footwo foothree"); - _cleanup_free_ char *v = strv_join_full((char **)input_table_one, ", ", "foo", false); + _cleanup_free_ char *v = strv_join_full((char **)input_table_one, ", ", "foo"); assert_se(v); ASSERT_STREQ(v, "fooone"); - _cleanup_free_ char *w = strv_join_full((char **)input_table_none, ", ", "foo", false); + _cleanup_free_ char *w = strv_join_full((char **)input_table_none, ", ", "foo"); assert_se(w); ASSERT_STREQ(w, ""); - _cleanup_free_ char *x = strv_join_full((char **)input_table_two_empties, ", ", "foo", false); + _cleanup_free_ char *x = strv_join_full((char **)input_table_two_empties, ", ", "foo"); assert_se(x); ASSERT_STREQ(x, "foo, foo"); - _cleanup_free_ char *y = strv_join_full((char **)input_table_one_empty, ", ", "foo", false); + _cleanup_free_ char *y = strv_join_full((char **)input_table_one_empty, ", ", "foo"); assert_se(y); ASSERT_STREQ(y, "foo"); }