threads / bug / 59786

[BUG] Segmentation fault in git v2.41.0.rc1

Subject: [BUG] Segmentation fault in git v2.41.0.rc1

## tl;dr

12 messages between May 24, 2023 and May 24, 2023.

replies: 11people: 6as markdown or json

Tribo Dar· May 24, 2023, 06:59 UTC · lore

Running `git submodule update` after commenting out the url setting in both .gitmodules and the corresponding section for the submodule in .git/config results in a segmentation fault instead of a suitable error message.

The cause is `sub->url` being NULL here (actual segfault comes a couple calls further): https://github.com/git/git/blob/9e49351c3060e1fa6e0d2de64505b7becf157f28/builtin/submodule--helper.c#L2027

Possibly relevant: a similar snippet is being properly protected against null-pointer dereferencing here: https://github.com/git/git/blob/9e49351c3060e1fa6e0d2de64505b7becf157f28/builtin/submodule--helper.c#L1243

Taylor Blau· May 24, 2023, 14:47 UTC · re: Tribo Dar · lore

Re: [BUG] Segmentation fault in git v2.41.0.rc1

Hi Tribo,
On Wed, May 24, 2023 at 06:59:55AM +0000, Tribo Dar wrote:
> Running `git submodule update` after commenting out the url setting in both
> .gitmodules and the corresponding section for the submodule in .git/config
> results in a segmentation fault instead of a suitable error message.

Thanks for reporting. I was able to bisect this with the following bisection script:

--- >8 --- #!/bin/sh

rm -fr clone
if ! test -d repo
then
    git init repo
    (
        cd repo
        git submodule add git@github.com:ttaylorr/dotfiles.git
        git add --all .
        git commit -m "initial commit"
    )
fi
git.compile clone repo clone
(
    cd clone
    git submodule init
    sed -ie 's/\turl = git@/#\0/g' .gitmodules .git/config
    git.compile submodule update
    test $? -gt 128 && exit 1
    exit 0
)
--- 8< ---

which points to e0a862fdaf (submodule helper: convert relative URL to absolute URL if needed, 2018-10-16) as the culprit. The fix appears to be something like:

--- 8< ---
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 6bf8d666ce..98ee7697f3 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -2024,8 +2024,9 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "submodule.%s.url", sub->name);
 	if (repo_config_get_string_tmp(the_repository, sb.buf, &url)) {
-		if (starts_with_dot_slash(sub->url) ||
-		    starts_with_dot_dot_slash(sub->url)) {
+		if (sub && sub->url &&
+		    (starts_with_dot_slash(sub->url) ||
+		     starts_with_dot_dot_slash(sub->url))) {
 			url = resolve_relative_url(sub->url, NULL, 0);
 			need_free_url = 1;
 		} else
diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh
index eae6a46ef3..c97c543dd8 100755
--- a/t/t7400-submodule-basic.sh
+++ b/t/t7400-submodule-basic.sh
@@ -1351,6 +1351,22 @@ test_expect_success 'clone active submodule without submodule url set' '
 	)
 '

+test_expect_success 'update submodules without url set in .gitconfig' '
+	test_when_finished "rm -rf multisuper_clone" &&
+	git clone file://"$pwd"/multisuper multisuper_clone &&
+
+	git -C multisuper_clone submodule init &&
+	for s in sub0 sub1 sub2 sub3
+	do
+		key=submodule.$s.url &&
+		git -C multisuper_clone config --local --unset $key &&
+		git -C multisuper_clone config --file .gitmodules --unset $key || return 1
+	done &&
+
+	test_must_fail git -C multisuper_clone submodule update 2>err &&
+	grep "option .url. requires a value" err
+'
+
 test_expect_success 'clone --recurse-submodules with a pathspec works' '
 	test_when_finished "rm -rf multisuper_clone" &&
 	cat >expected <<-\EOF &&
--- >8 ---

I'll clean it up and submit it as a patch shortly.

Thanks,
Taylor
Taylor Blau· May 24, 2023, 16:02 UTC · re: Taylor Blau · lore

[PATCH] builtin/submodule--helper.c: handle missing submodule URLs

In e0a862fdaf (submodule helper: convert relative URL to absolute URL if needed, 2018-10-16), `prepare_to_clone_next_submodule()` lost the ability to handle URL-less submodules, due to a change from:

    if (repo_get_config_string_const(the_repostiory, sb.buf, &url))
        url = sub->url;
to
    if (repo_get_config_string_const(the_repostiory, sb.buf, &url)) {
        if (starts_with_dot_slash(sub->url) ||
            starts_with_dot_dot_slash(sub->url)) {
                /* ... */
            }
    }

, which will segfault when `sub->url` is NULL, since both `starts_with_dot_slash()` does not guard its arguments as non-NULL.

Guard the checks to both of the above functions by first whether `sub->url` is non-NULL. There is no need to check whether `sub` itself is NULL, since we already perform this check earlier in `prepare_to_clone_next_submodule()`.

By adding a NULL-ness check on `sub->url`, we'll fall into the 'else' branch, setting `url` to `sub->url` (which is NULL). When we then try and invoke `git submodule-helper` below, we'll get the expected error message:

    error: option `url' requires a value

Note that this means that `--url` and its value must come last when preparing the argument list, to avoid confusing the next argument as the value for `--url` if `url` itself is NULL.

Reported-by: Tribo Dar <3bodar@gmail.com>
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
 builtin/submodule--helper.c | 11 ++++++++---
 t/t7400-submodule-basic.sh  | 16 ++++++++++++++++
 2 files changed, 24 insertions(+), 3 deletions(-)
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 6bf8d666ce..a7b7ea374f 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -2024,8 +2024,8 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "submodule.%s.url", sub->name);
 	if (repo_config_get_string_tmp(the_repository, sb.buf, &url)) {
-		if (starts_with_dot_slash(sub->url) ||
-		    starts_with_dot_dot_slash(sub->url)) {
+		if (sub->url && (starts_with_dot_slash(sub->url) ||
+				 starts_with_dot_dot_slash(sub->url))) {
 			url = resolve_relative_url(sub->url, NULL, 0);
 			need_free_url = 1;
 		} else
@@ -2069,7 +2069,6 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
 		strvec_push(&child->args, "--require-init");
 	strvec_pushl(&child->args, "--path", sub->path, NULL);
 	strvec_pushl(&child->args, "--name", sub->name, NULL);
-	strvec_pushl(&child->args, "--url", url, NULL);
 	if (suc->update_data->references.nr) {
 		struct string_list_item *item;
 
@@ -2082,6 +2081,12 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
 		strvec_push(&child->args, suc->update_data->single_branch ?
 					      "--single-branch" :
 					      "--no-single-branch");
+	/*
+	 * `url` might be NULL, so ensure that this argument is passed
+	 * last to avoid confusing the next argument as the value for
+	 * `--url`.
+	 */
+	strvec_pushl(&child->args, "--url", url, NULL);
 
 cleanup:
 	free(displaypath);
diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh
index eae6a46ef3..c97c543dd8 100755
--- a/t/t7400-submodule-basic.sh
+++ b/t/t7400-submodule-basic.sh
@@ -1351,6 +1351,22 @@ test_expect_success 'clone active submodule without submodule url set' '
 	)
 '
 
+test_expect_success 'update submodules without url set in .gitconfig' '
+	test_when_finished "rm -rf multisuper_clone" &&
+	git clone file://"$pwd"/multisuper multisuper_clone &&
+
+	git -C multisuper_clone submodule init &&
+	for s in sub0 sub1 sub2 sub3
+	do
+		key=submodule.$s.url &&
+		git -C multisuper_clone config --local --unset $key &&
+		git -C multisuper_clone config --file .gitmodules --unset $key || return 1
+	done &&
+
+	test_must_fail git -C multisuper_clone submodule update 2>err &&
+	grep "option .url. requires a value" err
+'
+
 test_expect_success 'clone --recurse-submodules with a pathspec works' '
 	test_when_finished "rm -rf multisuper_clone" &&
 	cat >expected <<-\EOF &&
-- 
2.41.0.rc1.11.gf7a8de14fe
Taylor Blau· May 24, 2023, 16:25 UTC · re: Taylor Blau · lore

[PATCH v2] builtin/submodule--helper.c: handle missing submodule URLs

In e0a862fdaf (submodule helper: convert relative URL to absolute URL if needed, 2018-10-16), `prepare_to_clone_next_submodule()` lost the ability to handle URL-less submodules, due to a change from:

    if (repo_get_config_string_const(the_repostiory, sb.buf, &url))
        url = sub->url;
to
    if (repo_get_config_string_const(the_repostiory, sb.buf, &url)) {
        if (starts_with_dot_slash(sub->url) ||
            starts_with_dot_dot_slash(sub->url)) {
                /* ... */
            }
    }

, which will segfault when `sub->url` is NULL, since both `starts_with_dot_slash()` does not guard its arguments as non-NULL.

Guard the checks to both of the above functions by first whether `sub->url` is non-NULL. There is no need to check whether `sub` itself is NULL, since we already perform this check earlier in `prepare_to_clone_next_submodule()`.

By adding a NULL-ness check on `sub->url`, we'll fall into the 'else' branch, setting `url` to `sub->url` (which is NULL). Before attempting to invoke `git submodule--helper clone`, check whether `url` is NULL, and die() if it is.

Reported-by: Tribo Dar <3bodar@gmail.com>
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
Junio: feel free to queue either, but after thinking on v1 a little more
I think that it is pretty gross and that this version is much cleaner.
Range-diff against v1:
1:  f7a8de14fe ! 1:  bc204f450a builtin/submodule--helper.c: handle missing submodule URLs
    @@ Commit message
         `prepare_to_clone_next_submodule()`.
         By adding a NULL-ness check on `sub->url`, we'll fall into the 'else'
    -    branch, setting `url` to `sub->url` (which is NULL). When we then try
    -    and invoke `git submodule-helper` below, we'll get the expected error
    -    message:
    -
    -        error: option `url' requires a value
    -
    -    Note that this means that `--url` and its value must come last when
    -    preparing the argument list, to avoid confusing the next argument as the
    -    value for `--url` if `url` itself is NULL.
    +    branch, setting `url` to `sub->url` (which is NULL). Before attempting
    +    to invoke `git submodule--helper clone`, check whether `url` is NULL,
    +    and die() if it is.
         Reported-by: Tribo Dar <3bodar@gmail.com>
         Signed-off-by: Taylor Blau <me@ttaylorr.com>
    @@ builtin/submodule--helper.c: static int prepare_to_clone_next_submodule(const st
      			url = resolve_relative_url(sub->url, NULL, 0);
      			need_free_url = 1;
      		} else
    + 			url = sub->url;
    + 	}
    +
    ++	if (!url)
    ++		die(_("cannot clone submodule '%s' without a URL"), sub->name);
    ++
    + 	strbuf_reset(&sb);
    + 	strbuf_addf(&sb, "%s/.git", ce->name);
    + 	needs_cloning = !file_exists(sb.buf);
     @@ builtin/submodule--helper.c: static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
    + 	if (suc->update_data->filter_options && suc->update_data->filter_options->choice)
    + 		strvec_pushf(&child->args, "--filter=%s",
    + 			     expand_list_objects_filter_spec(suc->update_data->filter_options));
    ++	strvec_pushl(&child->args, "--url", url, NULL);
    + 	if (suc->update_data->require_init)
      		strvec_push(&child->args, "--require-init");
      	strvec_pushl(&child->args, "--path", sub->path, NULL);
      	strvec_pushl(&child->args, "--name", sub->name, NULL);
    @@ builtin/submodule--helper.c: static int prepare_to_clone_next_submodule(const st
      	if (suc->update_data->references.nr) {
      		struct string_list_item *item;
    -@@ builtin/submodule--helper.c: static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
    - 		strvec_push(&child->args, suc->update_data->single_branch ?
    - 					      "--single-branch" :
    - 					      "--no-single-branch");
    -+	/*
    -+	 * `url` might be NULL, so ensure that this argument is passed
    -+	 * last to avoid confusing the next argument as the value for
    -+	 * `--url`.
    -+	 */
    -+	strvec_pushl(&child->args, "--url", url, NULL);
    -
    - cleanup:
    - 	free(displaypath);
      ## t/t7400-submodule-basic.sh ##
     @@ t/t7400-submodule-basic.sh: test_expect_success 'clone active submodule without submodule url set' '
    @@ t/t7400-submodule-basic.sh: test_expect_success 'clone active submodule without
     +	done &&
     +
     +	test_must_fail git -C multisuper_clone submodule update 2>err &&
    -+	grep "option .url. requires a value" err
    ++	grep "cannot clone submodule .sub[0-3]. without a URL" err
     +'
     +
      test_expect_success 'clone --recurse-submodules with a pathspec works' '
 builtin/submodule--helper.c |  9 ++++++---
 t/t7400-submodule-basic.sh  | 16 ++++++++++++++++
 2 files changed, 22 insertions(+), 3 deletions(-)
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 6bf8d666ce..efd0761cd0 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -2024,14 +2024,17 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "submodule.%s.url", sub->name);
 	if (repo_config_get_string_tmp(the_repository, sb.buf, &url)) {
-		if (starts_with_dot_slash(sub->url) ||
-		    starts_with_dot_dot_slash(sub->url)) {
+		if (sub->url && (starts_with_dot_slash(sub->url) ||
+				 starts_with_dot_dot_slash(sub->url))) {
 			url = resolve_relative_url(sub->url, NULL, 0);
 			need_free_url = 1;
 		} else
 			url = sub->url;
 	}

+	if (!url)
+		die(_("cannot clone submodule '%s' without a URL"), sub->name);
+
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/.git", ce->name);
 	needs_cloning = !file_exists(sb.buf);
@@ -2065,11 +2068,11 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
 	if (suc->update_data->filter_options && suc->update_data->filter_options->choice)
 		strvec_pushf(&child->args, "--filter=%s",
 			     expand_list_objects_filter_spec(suc->update_data->filter_options));
+	strvec_pushl(&child->args, "--url", url, NULL);
 	if (suc->update_data->require_init)
 		strvec_push(&child->args, "--require-init");
 	strvec_pushl(&child->args, "--path", sub->path, NULL);
 	strvec_pushl(&child->args, "--name", sub->name, NULL);
-	strvec_pushl(&child->args, "--url", url, NULL);
 	if (suc->update_data->references.nr) {
 		struct string_list_item *item;

diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh
index eae6a46ef3..d9fbabb2b9 100755
--- a/t/t7400-submodule-basic.sh
+++ b/t/t7400-submodule-basic.sh
@@ -1351,6 +1351,22 @@ test_expect_success 'clone active submodule without submodule url set' '
 	)
 '

+test_expect_success 'update submodules without url set in .gitconfig' '
+	test_when_finished "rm -rf multisuper_clone" &&
+	git clone file://"$pwd"/multisuper multisuper_clone &&
+
+	git -C multisuper_clone submodule init &&
+	for s in sub0 sub1 sub2 sub3
+	do
+		key=submodule.$s.url &&
+		git -C multisuper_clone config --local --unset $key &&
+		git -C multisuper_clone config --file .gitmodules --unset $key || return 1
+	done &&
+
+	test_must_fail git -C multisuper_clone submodule update 2>err &&
+	grep "cannot clone submodule .sub[0-3]. without a URL" err
+'
+
 test_expect_success 'clone --recurse-submodules with a pathspec works' '
 	test_when_finished "rm -rf multisuper_clone" &&
 	cat >expected <<-\EOF &&
--
2.41.0.rc1.11.gf7a8de14fe.dirty
Eric Sunshine· May 24, 2023, 18:48 UTC · re: Taylor Blau · lore

Re: [PATCH v2] builtin/submodule--helper.c: handle missing submodule URLs

On Wed, May 24, 2023 at 12:40 PM Taylor Blau <me@ttaylorr.com> wrote:
Show 20 quoted lines
> In e0a862fdaf (submodule helper: convert relative URL to absolute URL if
> needed, 2018-10-16), `prepare_to_clone_next_submodule()` lost the
> ability to handle URL-less submodules, due to a change from:
>
>     if (repo_get_config_string_const(the_repostiory, sb.buf, &url))
>         url = sub->url;
>
> to
>
>     if (repo_get_config_string_const(the_repostiory, sb.buf, &url)) {
>         if (starts_with_dot_slash(sub->url) ||
>             starts_with_dot_dot_slash(sub->url)) {
>                 /* ... */
>             }
>     }
>
> , which will segfault when `sub->url` is NULL, since both
> `starts_with_dot_slash()` does not guard its arguments as non-NULL.
>
> Guard the checks to both of the above functions by first whether
s/first/first checking/
Show 26 quoted lines
> `sub->url` is non-NULL. There is no need to check whether `sub` itself
> is NULL, since we already perform this check earlier in
> `prepare_to_clone_next_submodule()`.
>
> By adding a NULL-ness check on `sub->url`, we'll fall into the 'else'
> branch, setting `url` to `sub->url` (which is NULL). Before attempting
> to invoke `git submodule--helper clone`, check whether `url` is NULL,
> and die() if it is.
>
> Reported-by: Tribo Dar <3bodar@gmail.com>
> Signed-off-by: Taylor Blau <me@ttaylorr.com>
> ---
> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
> @@ -2024,14 +2024,17 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
> -               if (starts_with_dot_slash(sub->url) ||
> -                   starts_with_dot_dot_slash(sub->url)) {
> +               if (sub->url && (starts_with_dot_slash(sub->url) ||
> +                                starts_with_dot_dot_slash(sub->url))) {
>                         url = resolve_relative_url(sub->url, NULL, 0);
>                         need_free_url = 1;
>                 } else
>                         url = sub->url;
>         }
>
> +       if (!url)
> +               die(_("cannot clone submodule '%s' without a URL"), sub->name);

Good. The first version of this patch was more difficult to reason about due to its "error-at-a-distance" approach. This version is much cleaner and obvious.

Show 9 quoted lines
> @@ -2065,11 +2068,11 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
>                 strvec_pushf(&child->args, "--filter=%s",
>                              expand_list_objects_filter_spec(suc->update_data->filter_options));
> +       strvec_pushl(&child->args, "--url", url, NULL);
>         if (suc->update_data->require_init)
>                 strvec_push(&child->args, "--require-init");
>         strvec_pushl(&child->args, "--path", sub->path, NULL);
>         strvec_pushl(&child->args, "--name", sub->name, NULL);
> -       strvec_pushl(&child->args, "--url", url, NULL);

This change is unnecessary now, isn't it? Or is there something nonobvious going on here?

Taylor Blau· May 24, 2023, 19:50 UTC · re: Eric Sunshine · lore

Re: [PATCH v2] builtin/submodule--helper.c: handle missing submodule URLs

On Wed, May 24, 2023 at 02:48:40PM -0400, Eric Sunshine wrote:
> On Wed, May 24, 2023 at 12:40 PM Taylor Blau <me@ttaylorr.com> wrote:
> > Guard the checks to both of the above functions by first whether
>
> s/first/first checking/
Oops. Good eyes, thanks for spotting.
Show 12 quoted lines
> > @@ -2065,11 +2068,11 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
> >                 strvec_pushf(&child->args, "--filter=%s",
> >                              expand_list_objects_filter_spec(suc->update_data->filter_options));
> > +       strvec_pushl(&child->args, "--url", url, NULL);
> >         if (suc->update_data->require_init)
> >                 strvec_push(&child->args, "--require-init");
> >         strvec_pushl(&child->args, "--path", sub->path, NULL);
> >         strvec_pushl(&child->args, "--name", sub->name, NULL);
> > -       strvec_pushl(&child->args, "--url", url, NULL);
>
> This change is unnecessary now, isn't it? Or is there something
> nonobvious going on here?

Yeah, this is a stray diff. I'll send a cleaned up version as soon as 'make test' finishes ;-).

Thanks, Taylor

Taylor Blau· May 24, 2023, 19:51 UTC · re: Taylor Blau · lore

[PATCH v3] builtin/submodule--helper.c: handle missing submodule URLs

In e0a862fdaf (submodule helper: convert relative URL to absolute URL if needed, 2018-10-16), `prepare_to_clone_next_submodule()` lost the ability to handle URL-less submodules, due to a change from:

    if (repo_get_config_string_const(the_repostiory, sb.buf, &url))
        url = sub->url;
to
    if (repo_get_config_string_const(the_repostiory, sb.buf, &url)) {
        if (starts_with_dot_slash(sub->url) ||
            starts_with_dot_dot_slash(sub->url)) {
                /* ... */
            }
    }

, which will segfault when `sub->url` is NULL, since both `starts_with_dot_slash()` does not guard its arguments as non-NULL.

Guard the checks to both of the above functions by first checking whether `sub->url` is non-NULL. There is no need to check whether `sub` itself is NULL, since we already perform this check earlier in `prepare_to_clone_next_submodule()`.

By adding a NULL-ness check on `sub->url`, we'll fall into the 'else' branch, setting `url` to `sub->url` (which is NULL). Before attempting to invoke `git submodule--helper clone`, check whether `url` is NULL, and die() if it is.

Reported-by: Tribo Dar <3bodar@gmail.com>
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
Range-diff against v2:
1:  38202878b5 ! 1:  ae6cf3fa46 builtin/submodule--helper.c: handle missing submodule URLs
    @@ Commit message
         , which will segfault when `sub->url` is NULL, since both
         `starts_with_dot_slash()` does not guard its arguments as non-NULL.
     
    -    Guard the checks to both of the above functions by first whether
    -    `sub->url` is non-NULL. There is no need to check whether `sub` itself
    -    is NULL, since we already perform this check earlier in
    +    Guard the checks to both of the above functions by first checking
    +    whether `sub->url` is non-NULL. There is no need to check whether `sub`
    +    itself is NULL, since we already perform this check earlier in
         `prepare_to_clone_next_submodule()`.
     
         By adding a NULL-ness check on `sub->url`, we'll fall into the 'else'
    @@ builtin/submodule--helper.c: static int prepare_to_clone_next_submodule(const st
      	strbuf_reset(&sb);
      	strbuf_addf(&sb, "%s/.git", ce->name);
      	needs_cloning = !file_exists(sb.buf);
    -@@ builtin/submodule--helper.c: static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
    - 	if (suc->update_data->filter_options && suc->update_data->filter_options->choice)
    - 		strvec_pushf(&child->args, "--filter=%s",
    - 			     expand_list_objects_filter_spec(suc->update_data->filter_options));
    -+	strvec_pushl(&child->args, "--url", url, NULL);
    - 	if (suc->update_data->require_init)
    - 		strvec_push(&child->args, "--require-init");
    - 	strvec_pushl(&child->args, "--path", sub->path, NULL);
    - 	strvec_pushl(&child->args, "--name", sub->name, NULL);
    --	strvec_pushl(&child->args, "--url", url, NULL);
    - 	if (suc->update_data->references.nr) {
    - 		struct string_list_item *item;
    - 
     
      ## t/t7400-submodule-basic.sh ##
     @@ t/t7400-submodule-basic.sh: test_expect_success 'clone active submodule without submodule url set' '
 builtin/submodule--helper.c |  7 +++++--
 t/t7400-submodule-basic.sh  | 16 ++++++++++++++++
 2 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 6bf8d666ce..6a16208e8a 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -2024,14 +2024,17 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "submodule.%s.url", sub->name);
 	if (repo_config_get_string_tmp(the_repository, sb.buf, &url)) {
-		if (starts_with_dot_slash(sub->url) ||
-		    starts_with_dot_dot_slash(sub->url)) {
+		if (sub->url && (starts_with_dot_slash(sub->url) ||
+				 starts_with_dot_dot_slash(sub->url))) {
 			url = resolve_relative_url(sub->url, NULL, 0);
 			need_free_url = 1;
 		} else
 			url = sub->url;
 	}
 
+	if (!url)
+		die(_("cannot clone submodule '%s' without a URL"), sub->name);
+
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/.git", ce->name);
 	needs_cloning = !file_exists(sb.buf);
diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh
index eae6a46ef3..d9fbabb2b9 100755
--- a/t/t7400-submodule-basic.sh
+++ b/t/t7400-submodule-basic.sh
@@ -1351,6 +1351,22 @@ test_expect_success 'clone active submodule without submodule url set' '
 	)
 '
 
+test_expect_success 'update submodules without url set in .gitconfig' '
+	test_when_finished "rm -rf multisuper_clone" &&
+	git clone file://"$pwd"/multisuper multisuper_clone &&
+
+	git -C multisuper_clone submodule init &&
+	for s in sub0 sub1 sub2 sub3
+	do
+		key=submodule.$s.url &&
+		git -C multisuper_clone config --local --unset $key &&
+		git -C multisuper_clone config --file .gitmodules --unset $key || return 1
+	done &&
+
+	test_must_fail git -C multisuper_clone submodule update 2>err &&
+	grep "cannot clone submodule .sub[0-3]. without a URL" err
+'
+
 test_expect_success 'clone --recurse-submodules with a pathspec works' '
 	test_when_finished "rm -rf multisuper_clone" &&
 	cat >expected <<-\EOF &&
-- 
2.41.0.rc1.11.gae6cf3fa46
René Scharfe· May 24, 2023, 20:29 UTC · re: Taylor Blau · lore

Re: [PATCH v3] builtin/submodule--helper.c: handle missing submodule URLs

Am 24.05.23 um 21:51 schrieb Taylor Blau:
> There is no need to check whether `sub`
> itself is NULL, since we already perform this check earlier in
> `prepare_to_clone_next_submodule()`.

Right, and if "sub" is NULL then next_submodule_warn_missing() is called and prepare_to_clone_next_submodule() is exited early.

> By adding a NULL-ness check on `sub->url`, we'll fall into the 'else'
> branch, setting `url` to `sub->url` (which is NULL). Before attempting
> to invoke `git submodule--helper clone`, check whether `url` is NULL,
> and die() if it is.
Why die() here instead of just warn and skip as well?
René
Taylor Blau· May 24, 2023, 20:36 UTC · re: René Scharfe · lore

Re: [PATCH v3] builtin/submodule--helper.c: handle missing submodule URLs

On Wed, May 24, 2023 at 10:29:39PM +0200, René Scharfe wrote:
Show 8 quoted lines
> Am 24.05.23 um 21:51 schrieb Taylor Blau:
>
> > By adding a NULL-ness check on `sub->url`, we'll fall into the 'else'
> > branch, setting `url` to `sub->url` (which is NULL). Before attempting
> > to invoke `git submodule--helper clone`, check whether `url` is NULL,
> > and die() if it is.
>
> Why die() here instead of just warn and skip as well?

That's a good point. When I read prepare_to_clone_next_submodule(), I thought that it was already too late to skip that submodule. But my understanding was incorrect, we could easily issue a warning() and return '0', which would indicate to skip it.

But I concur with Junio earlier in the thread that we don't have to rush things to get this into -rc2 or even 2.41, since this bug has been with us since v2.20.0.

Thanks, Taylor

Jeff King· May 24, 2023, 20:33 UTC · re: Taylor Blau · lore

Re: [PATCH v3] builtin/submodule--helper.c: handle missing submodule URLs

On Wed, May 24, 2023 at 03:51:43PM -0400, Taylor Blau wrote:
Show 15 quoted lines
> In e0a862fdaf (submodule helper: convert relative URL to absolute URL if
> needed, 2018-10-16), `prepare_to_clone_next_submodule()` lost the
> ability to handle URL-less submodules, due to a change from:
> 
>     if (repo_get_config_string_const(the_repostiory, sb.buf, &url))
>         url = sub->url;
> 
> to
> 
>     if (repo_get_config_string_const(the_repostiory, sb.buf, &url)) {
>         if (starts_with_dot_slash(sub->url) ||
>             starts_with_dot_dot_slash(sub->url)) {
>                 /* ... */
>             }
>     }

This patch looks pretty good to me. I read your v1 and the word "gross" also crossed my mind at the "--url" handling. This one is much better. I did have a few questions, though (below).

If I understand correctly, this is not at all new in the -rc releases, but just something that happened to get unearthed? I.e., it can wait until post-release.

> , which will segfault when `sub->url` is NULL, since both
> `starts_with_dot_slash()` does not guard its arguments as non-NULL.

Funny gramm-o, presumably from editing: "both" is plural, but "does" and "its" are singular. I think the gist of it is communicated, though.

> Guard the checks to both of the above functions by first checking
> whether `sub->url` is non-NULL. There is no need to check whether `sub`
> itself is NULL, since we already perform this check earlier in
> `prepare_to_clone_next_submodule()`.
Good, thanks for checking (and communicating) that possible gotha.
> By adding a NULL-ness check on `sub->url`, we'll fall into the 'else'
> branch, setting `url` to `sub->url` (which is NULL). Before attempting
> to invoke `git submodule--helper clone`, check whether `url` is NULL,
> and die() if it is.

If I hadn't read v1, I might wonder whether this die() is consistent with the existing behavior. But the point is that submodule--helper would have barfed in such a case anyway, so we are just trading one error for another.

One side effect, though, is that this die() will take down the whole superproject process. Whereas I think the intent of the submodule code is to keep going, handling other submodules, even if one fails. This isn't a failure exactly (more of a misconfiguration, if I understand it). But should we be somehow returning an error instead?

I say "somehow" because it's not clear how to work that in with the needs_cloning return value (obviously we can say "0", but that is the same as the "skipped" code path; we presumably want to tell the caller there was a failure, so it affects the ultimate return code).

> +test_expect_success 'update submodules without url set in .gitconfig' '
Should this be .gitmodules in the title?
-Peff
Jeff King· May 24, 2023, 22:58 UTC · re: Taylor Blau · lore

Re: [PATCH] builtin/submodule--helper.c: handle missing submodule URLs

On Wed, May 24, 2023 at 12:02:26PM -0400, Taylor Blau wrote:
Show 15 quoted lines
> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
> index 6bf8d666ce..a7b7ea374f 100644
> --- a/builtin/submodule--helper.c
> +++ b/builtin/submodule--helper.c
> @@ -2024,8 +2024,8 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
>  	strbuf_reset(&sb);
>  	strbuf_addf(&sb, "submodule.%s.url", sub->name);
>  	if (repo_config_get_string_tmp(the_repository, sb.buf, &url)) {
> -		if (starts_with_dot_slash(sub->url) ||
> -		    starts_with_dot_dot_slash(sub->url)) {
> +		if (sub->url && (starts_with_dot_slash(sub->url) ||
> +				 starts_with_dot_dot_slash(sub->url))) {
>  			url = resolve_relative_url(sub->url, NULL, 0);
>  			need_free_url = 1;
>  		} else

Oh, btw, this need_free_url made me look at the memory handling for the other side of the else. I think it is all good, but it would be a little simpler using an extra pointer instead of a boolean:

diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 6a16208e8a..6b7849b828 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -1984,7 +1984,7 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
 						      ud->super_prefix);
 	struct strbuf sb = STRBUF_INIT;
 	int needs_cloning = 0;
-	int need_free_url = 0;
+	char *to_free_url = NULL;
 
 	if (ce_stage(ce)) {
 		strbuf_addf(out, _("Skipping unmerged submodule %s"), displaypath);
@@ -2025,10 +2025,9 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
 	strbuf_addf(&sb, "submodule.%s.url", sub->name);
 	if (repo_config_get_string_tmp(the_repository, sb.buf, &url)) {
 		if (sub->url && (starts_with_dot_slash(sub->url) ||
-				 starts_with_dot_dot_slash(sub->url))) {
-			url = resolve_relative_url(sub->url, NULL, 0);
-			need_free_url = 1;
-		} else
+				 starts_with_dot_dot_slash(sub->url)))
+			url = to_free_url = resolve_relative_url(sub->url, NULL, 0);
+		else
 			url = sub->url;
 	}
 
@@ -2089,8 +2088,7 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
 cleanup:
 	free(displaypath);
 	strbuf_release(&sb);
-	if (need_free_url)
-		free((void*)url);
+	free(to_free_url);
 
 	return needs_cloning;
 }

Probably at this point that is just churn, but I thought I'd throw it
out there.

-Peff
Junio C Hamano· May 24, 2023, 20:25 UTC · re: Taylor Blau · lore

Re: [BUG] Segmentation fault in git v2.41.0.rc1

Taylor Blau <me@ttaylorr.com> writes:
> which points to e0a862fdaf (submodule helper: convert relative URL to
> absolute URL if needed, 2018-10-16) as the culprit.

Whew, that is fairly ancient. I was afraid if we have another regression, but it does not look that way. Fixing is certainly good and we'd need to eventually get to it, but we have luxury to make sure that the fix is sound without having to rush anything ;-)

In the meantime, "if it hurts, don't do it" is what we can say. Telling random users to muck with their config in certain ways that violate the way how the system represents (un-)initialized submodules and then to run certain command to induce a NULL pointer dereference is rather an ineffective social engineering as an attack vector, so this is not urgent in that sense.

I am more worried that the original report talked about mucking with the in-tree .gitmodules file affects the result, though. Once a submodule is initialized, what is in the file for that submodule should not affect the working of local Git (otherwise the file can be used as a route to inject stuff to unsuspecting repositories), but in this case apparently it does?

Thanks.

← back to recent threads