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:

  - d811c8e17c (versionsort: support reorder prerelease suffixes, 2015-02-26)
  - c026557a37 (versioncmp: generalize version sort suffix reordering, 2016-12-08)
  - a086f921a7 (submodule: decouple url and submodule interest, 2017-03-17)
  - a6be5e6764 (log: add log.excludeDecoration config option, 2020-04-16)
  - 92156291ca (log: add default decoration filter, 2022-08-05)
  - 50a044f1e4 (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. 40ea4ed903 (Add config_error_nonbool() helper function,
   2008-02-11)
2. 6c47d0e8f3 (config.c: guard config parser from value=NULL,
   2008-02-11).
3. 4c715ebb96 (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>
This commit is contained in:
Ævar Arnfjörð Bjarmason 2023-03-28 16:04:27 +02:00 committed by Junio C Hamano
parent 1c7e239bd0
commit 9e2d884d0f
12 changed files with 105 additions and 22 deletions

View File

@ -1510,7 +1510,7 @@ static int maintenance_register(int argc, const char **argv, const char *prefix)
if (git_config_get("maintenance.strategy")) if (git_config_get("maintenance.strategy"))
git_config_set("maintenance.strategy", "incremental"); git_config_set("maintenance.strategy", "incremental");
if (!git_config_get_value_multi(key, &list)) { if (!git_config_get_string_multi(key, &list)) {
for_each_string_list_item(item, list) { for_each_string_list_item(item, list) {
if (!strcmp(maintpath, item->string)) { if (!strcmp(maintpath, item->string)) {
found = 1; found = 1;
@ -1578,8 +1578,8 @@ static int maintenance_unregister(int argc, const char **argv, const char *prefi
git_configset_add_file(&cs, config_file); git_configset_add_file(&cs, config_file);
} }
if (!(config_file if (!(config_file
? git_configset_get_value_multi(&cs, key, &list) ? git_configset_get_string_multi(&cs, key, &list)
: git_config_get_value_multi(key, &list))) { : git_config_get_string_multi(key, &list))) {
for_each_string_list_item(item, list) { for_each_string_list_item(item, list) {
if (!strcmp(maintpath, item->string)) { if (!strcmp(maintpath, item->string)) {
found = 1; found = 1;

View File

@ -184,8 +184,8 @@ static void set_default_decoration_filter(struct decoration_filter *decoration_f
struct string_list *include = decoration_filter->include_ref_pattern; struct string_list *include = decoration_filter->include_ref_pattern;
const struct string_list *config_exclude; const struct string_list *config_exclude;
if (!git_config_get_value_multi("log.excludeDecoration", if (!git_config_get_string_multi("log.excludeDecoration",
&config_exclude)) { &config_exclude)) {
struct string_list_item *item; struct string_list_item *item;
for_each_string_list_item(item, config_exclude) for_each_string_list_item(item, config_exclude)
string_list_append(decoration_filter->exclude_ref_config_pattern, string_list_append(decoration_filter->exclude_ref_config_pattern,

View File

@ -2434,6 +2434,25 @@ int git_configset_get_value_multi(struct config_set *cs, const char *key,
return 0; return 0;
} }
static int check_multi_string(struct string_list_item *item, void *util)
{
return item->string ? 0 : config_error_nonbool(util);
}
int git_configset_get_string_multi(struct config_set *cs, const char *key,
const struct string_list **dest)
{
int ret;
if ((ret = git_configset_get_value_multi(cs, key, dest)))
return ret;
if ((ret = for_each_string_list((struct string_list *)*dest,
check_multi_string, (void *)key)))
return ret;
return 0;
}
int git_configset_get(struct config_set *cs, const char *key) int git_configset_get(struct config_set *cs, const char *key)
{ {
struct config_set_element *e; struct config_set_element *e;
@ -2602,6 +2621,13 @@ int repo_config_get_value_multi(struct repository *repo, const char *key,
return git_configset_get_value_multi(repo->config, key, dest); return git_configset_get_value_multi(repo->config, key, dest);
} }
int repo_config_get_string_multi(struct repository *repo, const char *key,
const struct string_list **dest)
{
git_config_check_init(repo);
return git_configset_get_string_multi(repo->config, key, dest);
}
int repo_config_get_string(struct repository *repo, int repo_config_get_string(struct repository *repo,
const char *key, char **dest) const char *key, char **dest)
{ {
@ -2717,6 +2743,12 @@ int git_config_get_value_multi(const char *key, const struct string_list **dest)
return repo_config_get_value_multi(the_repository, key, dest); return repo_config_get_value_multi(the_repository, key, dest);
} }
int git_config_get_string_multi(const char *key,
const struct string_list **dest)
{
return repo_config_get_string_multi(the_repository, key, dest);
}
int git_config_get_string(const char *key, char **dest) int git_config_get_string(const char *key, char **dest)
{ {
return repo_config_get_string(the_repository, key, dest); return repo_config_get_string(the_repository, key, dest);

View File

@ -472,6 +472,19 @@ RESULT_MUST_BE_USED
int git_configset_get_value_multi(struct config_set *cs, const char *key, int git_configset_get_value_multi(struct config_set *cs, const char *key,
const struct string_list **dest); const struct string_list **dest);
/**
* A validation wrapper for git_configset_get_value_multi() which does
* for it what git_configset_get_string() does for
* git_configset_get_value().
*
* The configuration syntax allows for "[section] key", which will
* give us a NULL entry in the "struct string_list", as opposed to
* "[section] key =" which is the empty string. Most users of the API
* are not prepared to handle NULL in a "struct string_list".
*/
int git_configset_get_string_multi(struct config_set *cs, const char *key,
const struct string_list **dest);
/** /**
* Clears `config_set` structure, removes all saved variable-value pairs. * Clears `config_set` structure, removes all saved variable-value pairs.
*/ */
@ -522,6 +535,9 @@ int repo_config_get_value(struct repository *repo,
RESULT_MUST_BE_USED RESULT_MUST_BE_USED
int repo_config_get_value_multi(struct repository *repo, const char *key, int repo_config_get_value_multi(struct repository *repo, const char *key,
const struct string_list **dest); const struct string_list **dest);
RESULT_MUST_BE_USED
int repo_config_get_string_multi(struct repository *repo, const char *key,
const struct string_list **dest);
int repo_config_get_string(struct repository *repo, int repo_config_get_string(struct repository *repo,
const char *key, char **dest); const char *key, char **dest);
int repo_config_get_string_tmp(struct repository *repo, int repo_config_get_string_tmp(struct repository *repo,
@ -583,6 +599,9 @@ int git_config_get_value(const char *key, const char **value);
RESULT_MUST_BE_USED RESULT_MUST_BE_USED
int git_config_get_value_multi(const char *key, int git_config_get_value_multi(const char *key,
const struct string_list **dest); const struct string_list **dest);
RESULT_MUST_BE_USED
int git_config_get_string_multi(const char *key,
const struct string_list **dest);
/** /**
* Resets and invalidates the config cache. * Resets and invalidates the config cache.

View File

@ -2303,7 +2303,7 @@ const struct string_list *bitmap_preferred_tips(struct repository *r)
{ {
const struct string_list *dest; const struct string_list *dest;
if (!repo_config_get_value_multi(r, "pack.preferbitmaptips", &dest)) if (!repo_config_get_string_multi(r, "pack.preferbitmaptips", &dest))
return dest; return dest;
return NULL; return NULL;
} }

View File

@ -274,7 +274,7 @@ int is_tree_submodule_active(struct repository *repo,
free(key); free(key);
/* submodule.active is set */ /* submodule.active is set */
if (!repo_config_get_value_multi(repo, "submodule.active", &sl)) { if (!repo_config_get_string_multi(repo, "submodule.active", &sl)) {
struct pathspec ps; struct pathspec ps;
struct strvec args = STRVEC_INIT; struct strvec args = STRVEC_INIT;
const struct string_list_item *item; const struct string_list_item *item;

View File

@ -835,7 +835,7 @@ test_expect_success 'log.decorate configuration' '
' '
test_expect_failure 'parse log.excludeDecoration with no value' ' test_expect_success 'parse log.excludeDecoration with no value' '
cp .git/config .git/config.orig && cp .git/config .git/config.orig &&
test_when_finished mv .git/config.orig .git/config && test_when_finished mv .git/config.orig .git/config &&
@ -843,7 +843,11 @@ test_expect_failure 'parse log.excludeDecoration with no value' '
[log] [log]
excludeDecoration excludeDecoration
EOF EOF
git log --decorate=short cat >expect <<-\EOF &&
error: missing value for '\''log.excludeDecoration'\''
EOF
git log --decorate=short 2>actual &&
test_cmp expect actual
' '
test_expect_success 'decorate-refs with glob' ' test_expect_success 'decorate-refs with glob' '

View File

@ -404,7 +404,7 @@ test_bitmap_cases () {
) )
' '
test_expect_failure 'pack.preferBitmapTips' ' test_expect_success 'pack.preferBitmapTips' '
git init repo && git init repo &&
test_when_finished "rm -rf repo" && test_when_finished "rm -rf repo" &&
( (
@ -416,7 +416,11 @@ test_bitmap_cases () {
[pack] [pack]
preferBitmapTips preferBitmapTips
EOF EOF
git repack -adb cat >expect <<-\EOF &&
error: missing value for '\''pack.preferbitmaptips'\''
EOF
git repack -adb 2>actual &&
test_cmp expect actual
) )
' '

View File

@ -1843,7 +1843,7 @@ test_expect_success 'invalid sort parameter in configuratoin' '
test_must_fail git tag -l "foo*" test_must_fail git tag -l "foo*"
' '
test_expect_failure 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' ' test_expect_success 'version sort handles empty value for versionsort.{prereleaseSuffix,suffix}' '
cp .git/config .git/config.orig && cp .git/config .git/config.orig &&
test_when_finished mv .git/config.orig .git/config && test_when_finished mv .git/config.orig .git/config &&
@ -1852,7 +1852,12 @@ test_expect_failure 'version sort handles empty value for versionsort.{prereleas
prereleaseSuffix prereleaseSuffix
suffix suffix
EOF EOF
git tag -l --sort=version:refname cat >expect <<-\EOF &&
error: missing value for '\''versionsort.suffix'\''
error: missing value for '\''versionsort.prereleasesuffix'\''
EOF
git tag -l --sort=version:refname 2>actual &&
test_cmp expect actual
' '
test_expect_success 'version sort with prerelease reordering' ' test_expect_success 'version sort with prerelease reordering' '

View File

@ -51,7 +51,7 @@ test_expect_success 'is-active works with submodule.<name>.active config' '
test-tool -C super submodule is-active sub1 test-tool -C super submodule is-active sub1
' '
test_expect_failure 'is-active handles submodule.active config missing a value' ' test_expect_success 'is-active handles submodule.active config missing a value' '
cp super/.git/config super/.git/config.orig && cp super/.git/config super/.git/config.orig &&
test_when_finished mv super/.git/config.orig super/.git/config && test_when_finished mv super/.git/config.orig super/.git/config &&
@ -60,7 +60,11 @@ test_expect_failure 'is-active handles submodule.active config missing a value'
active active
EOF EOF
test-tool -C super submodule is-active sub1 cat >expect <<-\EOF &&
error: missing value for '\''submodule.active'\''
EOF
test-tool -C super submodule is-active sub1 2>actual &&
test_cmp expect actual
' '
test_expect_success 'is-active works with basic submodule.active config' ' test_expect_success 'is-active works with basic submodule.active config' '

View File

@ -524,7 +524,7 @@ test_expect_success 'register and unregister' '
git maintenance unregister --config-file ./other --force git maintenance unregister --config-file ./other --force
' '
test_expect_failure 'register with no value for maintenance.repo' ' test_expect_success 'register with no value for maintenance.repo' '
cp .git/config .git/config.orig && cp .git/config .git/config.orig &&
test_when_finished mv .git/config.orig .git/config && test_when_finished mv .git/config.orig .git/config &&
@ -532,10 +532,15 @@ test_expect_failure 'register with no value for maintenance.repo' '
[maintenance] [maintenance]
repo repo
EOF EOF
git maintenance register cat >expect <<-\EOF &&
error: missing value for '\''maintenance.repo'\''
EOF
git maintenance register 2>actual &&
test_cmp expect actual &&
git config maintenance.repo
' '
test_expect_failure 'unregister with no value for maintenance.repo' ' test_expect_success 'unregister with no value for maintenance.repo' '
cp .git/config .git/config.orig && cp .git/config .git/config.orig &&
test_when_finished mv .git/config.orig .git/config && test_when_finished mv .git/config.orig .git/config &&
@ -543,8 +548,18 @@ test_expect_failure 'unregister with no value for maintenance.repo' '
[maintenance] [maintenance]
repo repo
EOF EOF
git maintenance unregister && cat >expect <<-\EOF &&
git maintenance unregister --force error: missing value for '\''maintenance.repo'\''
EOF
test_expect_code 128 git maintenance unregister 2>actual.raw &&
grep ^error actual.raw >actual &&
test_cmp expect actual &&
git config maintenance.repo &&
git maintenance unregister --force 2>actual.raw &&
grep ^error actual.raw >actual &&
test_cmp expect actual &&
git config maintenance.repo
' '
test_expect_success !MINGW 'register and unregister with regex metacharacters' ' test_expect_success !MINGW 'register and unregister with regex metacharacters' '

View File

@ -164,8 +164,8 @@ int versioncmp(const char *s1, const char *s2)
const char *const oldk = "versionsort.prereleasesuffix"; const char *const oldk = "versionsort.prereleasesuffix";
const struct string_list *newl; const struct string_list *newl;
const struct string_list *oldl; const struct string_list *oldl;
int new = git_config_get_value_multi(newk, &newl); int new = git_config_get_string_multi(newk, &newl);
int old = git_config_get_value_multi(oldk, &oldl); int old = git_config_get_string_multi(oldk, &oldl);
if (!new && !old) if (!new && !old)
warning("ignoring %s because %s is set", oldk, newk); warning("ignoring %s because %s is set", oldk, newk);