config API: add "string" version of *_value_multi(), fix segfaults

Fix numerous and mostly long-standing segfaults in consumers of the *_config_*value_multi() API. As discussed in the preceding commit an empty key in the config syntax yields a "NULL" string, which these users would give to strcmp() (or similar), resulting in segfaults. As this change shows, most users users of the *_config_*value_multi() API didn't really want such an an unsafe and low-level API, let's give them something with the safety of git_config_get_string() instead. This fix is similar to what the *_string() functions and others acquired in[1] and [2]. Namely introducing and using a safer "*_get_string_multi()" variant of the low-level "_*value_multi()" function. This fixes segfaults in code introduced in: - d811c8e17c6 (versionsort: support reorder prerelease suffixes, 2015-02-26) - c026557a373 (versioncmp: generalize version sort suffix reordering, 2016-12-08) - a086f921a72 (submodule: decouple url and submodule interest, 2017-03-17) - a6be5e6764a (log: add log.excludeDecoration config option, 2020-04-16) - 92156291ca8 (log: add default decoration filter, 2022-08-05) - 50a044f1e40 (gc: replace config subprocesses with API calls, 2022-09-27) There are now two users ofthe low-level API: - One in "builtin/for-each-repo.c", which we'll convert in a subsequent commit. - The "t/helper/test-config.c" code added in [3]. As seen in the preceding commit we need to give the "t/helper/test-config.c" caller these "NULL" entries. We could also alter the underlying git_configset_get_value_multi() function to be "string safe", but doing so would leave no room for other variants of "*_get_value_multi()" that coerce to other types. Such coercion can't be built on the string version, since as we've established "NULL" is a true value in the boolean context, but if we coerced it to "" for use in a list of strings it'll be subsequently coerced to "false" as a boolean. The callback pattern being used here will make it easy to introduce e.g. a "multi" variant which coerces its values to "bool", "int", "path" etc. 1. 40ea4ed9032 (Add config_error_nonbool() helper function, 2008-02-11) 2. 6c47d0e8f39 (config.c: guard config parser from value=NULL, 2008-02-11). 3. 4c715ebb96a (test-config: add tests for the config_set API, 2014-07-28) Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Ævar Arnfjörð Bjarmason committed Mar 28, 2023 at 16:04 UTC 9e2d884d0fcf0631164c33d458a251bee6053bb1
12 files changed +105 -22
builtin/gc.c
+3 -3
@@ -1510,7 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
1510 if (git_config_get("maintenance.strategy"))
1511 git_config_set("maintenance.strategy", "incremental");
1512
1513 - if (!git_config_get_value_multi(key, &list)) {
1513 + if (!git_config_get_string_multi(key, &list)) {
1514 for_each_string_list_item(item, list) {
1515 if (!strcmp(maintpath, item->string)) {
1516 found = 1;
@@ -1578,8 +1578,8 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
1578 git_configset_add_file(&cs, config_file);
1579 }
1580 if (!(config_file
1581 - ? git_configset_get_value_multi(&cs, key, &list)
1582 - : git_config_get_value_multi(key, &list))) {
1581 + ? git_configset_get_string_multi(&cs, key, &list)
1582 + : git_config_get_string_multi(key, &list))) {
1583 for_each_string_list_item(item, list) {
1584 if (!strcmp(maintpath, item->string)) {
1585 found = 1;
builtin/log.c
+2 -2
@@ -184,8 +184,8 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f
184 struct string_list *include = decoration_filter->include_ref_pattern;
185 const struct string_list *config_exclude;
186
187 - if (!git_config_get_value_multi("log.excludeDecoration",
188 - &config_exclude)) {
187 + if (!git_config_get_string_multi("log.excludeDecoration",
188 + &config_exclude)) {
189 struct string_list_item *item;
190 for_each_string_list_item(item, config_exclude)
191 string_list_append(decoration_filter->exclude_ref_config_pattern,
config.c
+32
@@ -2434,6 +2434,25 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,
2434 return 0;
2435 }
2436
2437 +static int check_multi_string(struct string_list_item *item, void *util)
2438 +{
2439 + return item->string ? 0 : config_error_nonbool(util);
2440 +}
2441 +
2442 +int git_configset_get_string_multi(struct config_set *cs, const char *key,
2443 + const struct string_list **dest)
2444 +{
2445 + int ret;
2446 +
2447 + if ((ret = git_configset_get_value_multi(cs, key, dest)))
2448 + return ret;
2449 + if ((ret = for_each_string_list((struct string_list *)*dest,
2450 + check_multi_string, (void *)key)))
2451 + return ret;
2452 +
2453 + return 0;
2454 +}
2455 +
2456 int git_configset_get(struct config_set *cs, const char *key)
2457 {
2458 struct config_set_element *e;
@@ -2602,6 +2621,13 @@ int repo_config_get_value_multi(struct repository *repo, const char *key,
2621 return git_configset_get_value_multi(repo->config, key, dest);
2622 }
2623
2624 +int repo_config_get_string_multi(struct repository *repo, const char *key,
2625 + const struct string_list **dest)
2626 +{
2627 + git_config_check_init(repo);
2628 + return git_configset_get_string_multi(repo->config, key, dest);
2629 +}
2630 +
2631 int repo_config_get_string(struct repository *repo,
2632 const char *key, char **dest)
2633 {
@@ -2717,6 +2743,12 @@ int git_config_get_value_multi(const char *key, const struct string_list **dest)
2743 return repo_config_get_value_multi(the_repository, key, dest);
2744 }
2745
2746 +int git_config_get_string_multi(const char *key,
2747 + const struct string_list **dest)
2748 +{
2749 + return repo_config_get_string_multi(the_repository, key, dest);
2750 +}
2751 +
2752 int git_config_get_string(const char *key, char **dest)
2753 {
2754 return repo_config_get_string(the_repository, key, dest);
config.h
+19
@@ -472,6 +472,19 @@ RESULT_MUST_BE_USED
472 int git_configset_get_value_multi(struct config_set *cs, const char *key,
473 const struct string_list **dest);
474
475 +/**
476 + * A validation wrapper for git_configset_get_value_multi() which does
477 + * for it what git_configset_get_string() does for
478 + * git_configset_get_value().
479 + *
480 + * The configuration syntax allows for "[section] key", which will
481 + * give us a NULL entry in the "struct string_list", as opposed to
482 + * "[section] key =" which is the empty string. Most users of the API
483 + * are not prepared to handle NULL in a "struct string_list".
484 + */
485 +int git_configset_get_string_multi(struct config_set *cs, const char *key,
486 + const struct string_list **dest);
487 +
488 /**
489 * Clears `config_set` structure, removes all saved variable-value pairs.
490 */
@@ -522,6 +535,9 @@ int repo_config_get_value(struct repository *repo,
535 RESULT_MUST_BE_USED
536 int repo_config_get_value_multi(struct repository *repo, const char *key,
537 const struct string_list **dest);
538 +RESULT_MUST_BE_USED
539 +int repo_config_get_string_multi(struct repository *repo, const char *key,
540 + const struct string_list **dest);
541 int repo_config_get_string(struct repository *repo,
542 const char *key, char **dest);
543 int repo_config_get_string_tmp(struct repository *repo,
@@ -583,6 +599,9 @@ int git_config_get_value(const char *key, const char **value);
599 RESULT_MUST_BE_USED
600 int git_config_get_value_multi(const char *key,
601 const struct string_list **dest);
602 +RESULT_MUST_BE_USED
603 +int git_config_get_string_multi(const char *key,
604 + const struct string_list **dest);
605
606 /**
607 * Resets and invalidates the config cache.
pack-bitmap.c
+1 -1
@@ -2303,7 +2303,7 @@ const struct string_list *bitmap_preferred_tips(struct repository *r)
2303 {
2304 const struct string_list *dest;
2305
2306 - if (!repo_config_get_value_multi(r, "pack.preferbitmaptips", &dest))
2306 + if (!repo_config_get_string_multi(r, "pack.preferbitmaptips", &dest))
2307 return dest;
2308 return NULL;
2309 }
submodule.c
+1 -1
@@ -274,7 +274,7 @@ int is_tree_submodule_active(struct repository *repo,
274 free(key);
275
276 /* submodule.active is set */
277 - if (!repo_config_get_value_multi(repo, "submodule.active", &sl)) {
277 + if (!repo_config_get_string_multi(repo, "submodule.active", &sl)) {
278 struct pathspec ps;
279 struct strvec args = STRVEC_INIT;
280 const struct string_list_item *item;
t/t4202-log.sh
+6 -2
@@ -835,7 +835,7 @@ test_expect_success 'log.decorate configuration' '
835
836 '
837
838 -test_expect_failure 'parse log.excludeDecoration with no value' '
838 +test_expect_success 'parse log.excludeDecoration with no value' '
839 cp .git/config .git/config.orig &&
840 test_when_finished mv .git/config.orig .git/config &&
841
@@ -843,7 +843,11 @@ test_expect_failure 'parse log.excludeDecoration with no value' '
843 [log]
844 excludeDecoration
845 EOF
846 - git log --decorate=short
846 + cat >expect <<-\EOF &&
847 + error: missing value for '\''log.excludeDecoration'\''
848 + EOF
849 + git log --decorate=short 2>actual &&
850 + test_cmp expect actual
851 '
852
853 test_expect_success 'decorate-refs with glob' '
t/t5310-pack-bitmaps.sh
+6 -2
@@ -404,7 +404,7 @@ test_bitmap_cases () {
404 )
405 '
406
407 - test_expect_failure 'pack.preferBitmapTips' '
407 + test_expect_success 'pack.preferBitmapTips' '
408 git init repo &&
409 test_when_finished "rm -rf repo" &&
410 (
@@ -416,7 +416,11 @@ test_bitmap_cases () {
416 [pack]
417 preferBitmapTips
418 EOF
419 - git repack -adb
419 + cat >expect <<-\EOF &&
420 + error: missing value for '\''pack.preferbitmaptips'\''
421 + EOF
422 + git repack -adb 2>actual &&
423 + test_cmp expect actual
424 )
425 '
426
t/t7004-tag.sh
+7 -2
@@ -1843,7 +1843,7 @@ test_expect_success 'invalid sort parameter in configuratoin' '
1843 test_must_fail git tag -l "foo*"
1844 '
1845
1846 -test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
1846 +test_expect_success 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
1847 cp .git/config .git/config.orig &&
1848 test_when_finished mv .git/config.orig .git/config &&
1849
@@ -1852,7 +1852,12 @@ test_expect_failure 'version sort handles empty value for versionsort.{prereleas
1852 prereleaseSuffix
1853 suffix
1854 EOF
1855 - git tag -l --sort=version:refname
1855 + cat >expect <<-\EOF &&
1856 + error: missing value for '\''versionsort.suffix'\''
1857 + error: missing value for '\''versionsort.prereleasesuffix'\''
1858 + EOF
1859 + git tag -l --sort=version:refname 2>actual &&
1860 + test_cmp expect actual
1861 '
1862
1863 test_expect_success 'version sort with prerelease reordering' '
t/t7413-submodule-is-active.sh
+6 -2
@@ -51,7 +51,7 @@ test_expect_success 'is-active works with submodule.<name>.active config' '
51 test-tool -C super submodule is-active sub1
52 '
53
54 -test_expect_failure 'is-active handles submodule.active config missing a value' '
54 +test_expect_success 'is-active handles submodule.active config missing a value' '
55 cp super/.git/config super/.git/config.orig &&
56 test_when_finished mv super/.git/config.orig super/.git/config &&
57
@@ -60,7 +60,11 @@ test_expect_failure 'is-active handles submodule.active config missing a value'
60 active
61 EOF
62
63 - test-tool -C super submodule is-active sub1
63 + cat >expect <<-\EOF &&
64 + error: missing value for '\''submodule.active'\''
65 + EOF
66 + test-tool -C super submodule is-active sub1 2>actual &&
67 + test_cmp expect actual
68 '
69
70 test_expect_success 'is-active works with basic submodule.active config' '
t/t7900-maintenance.sh
+20 -5
@@ -524,7 +524,7 @@ test_expect_success 'register and unregister' '
524 git maintenance unregister --config-file ./other --force
525 '
526
527 -test_expect_failure 'register with no value for maintenance.repo' '
527 +test_expect_success 'register with no value for maintenance.repo' '
528 cp .git/config .git/config.orig &&
529 test_when_finished mv .git/config.orig .git/config &&
530
@@ -532,10 +532,15 @@ test_expect_failure 'register with no value for maintenance.repo' '
532 [maintenance]
533 repo
534 EOF
535 - git maintenance register
535 + cat >expect <<-\EOF &&
536 + error: missing value for '\''maintenance.repo'\''
537 + EOF
538 + git maintenance register 2>actual &&
539 + test_cmp expect actual &&
540 + git config maintenance.repo
541 '
542
538 -test_expect_failure 'unregister with no value for maintenance.repo' '
543 +test_expect_success 'unregister with no value for maintenance.repo' '
544 cp .git/config .git/config.orig &&
545 test_when_finished mv .git/config.orig .git/config &&
546
@@ -543,8 +548,18 @@ test_expect_failure 'unregister with no value for maintenance.repo' '
548 [maintenance]
549 repo
550 EOF
546 - git maintenance unregister &&
547 - git maintenance unregister --force
551 + cat >expect <<-\EOF &&
552 + error: missing value for '\''maintenance.repo'\''
553 + EOF
554 + test_expect_code 128 git maintenance unregister 2>actual.raw &&
555 + grep ^error actual.raw >actual &&
556 + test_cmp expect actual &&
557 + git config maintenance.repo &&
558 +
559 + git maintenance unregister --force 2>actual.raw &&
560 + grep ^error actual.raw >actual &&
561 + test_cmp expect actual &&
562 + git config maintenance.repo
563 '
564
565 test_expect_success !MINGW 'register and unregister with regex metacharacters' '
versioncmp.c
+2 -2
@@ -164,8 +164,8 @@ int versioncmp(const char *s1, const char *s2)
164 const char *const oldk = "versionsort.prereleasesuffix";
165 const struct string_list *newl;
166 const struct string_list *oldl;
167 - int new = git_config_get_value_multi(newk, &newl);
168 - int old = git_config_get_value_multi(oldk, &oldl);
167 + int new = git_config_get_string_multi(newk, &newl);
168 + int old = git_config_get_string_multi(oldk, &oldl);
169
170 if (!new && !old)
171 warning("ignoring %s because %s is set", oldk, newk);