Subject: [PATCH/RFC] submodule: gracefully handle links in module paths

From: "Julian Prein via GitGitGadget" <[email-masked]>
Date: Thu, 28 Aug 2025 12:43:46 +0000

From: Julian Prein <[email-masked]>

Due to security concerns the commit e8d060894448 (submodule: require the
submodule path to contain directories only, 2024-03-26) introduced some
checks and hard `exit(128)` calls for the case that a submodule path
contains symbolic links. This change was motivated by CVE-2024-32002,
which was discovered shortly before. Consequently, git currently aborts
actions of which some could be continued by not processing the faulty
module.

As an example, one of these actions that may be aborted is showing a
diff with diff.submodule set to `log` or `diff`: Let's assume a
repository where a submodule was moved and its old path replaced with a
symlink pointing to the new location. A git-show on an older commit from
before the move that also touches the module will now have only partial
output. If the submodule is the first file to list, there will be no
diff at all after the commit message. Other examples for actions that
are aborted after already being started are fetching and pushing with
their respective recurseSubmodules turned on.

Handle these cases more gracefully by returning an error value instead
of dying, if possible. This was done only in functions that already
supported returning an error.

With this change the mentioned git-show will display the full diff and
show a "(commits not present)" for the submodule - the same is done
currently when a submodule is simply moved. The fetch and push will
complete their action while skipping the faulty module and printing an
error message.

Signed-off-by: Julian Prein <[email-masked]>
---
    submodule: gracefully handle links in module paths
    
    Hello,
    
    I'm submitting this patch primarily to fix the issue described with
    diff.submodule. I maintain a repository in which this kind of move
    occurred, and I have diff.submodule set to log. With current git I can't
    properly display some older commits. Changing submodule_to_gitdir would
    be sufficient to address this, but I noticed more places doing the same
    thing (exit(128) even though an error value could be returned), so I
    changed these as well.
    
    Since this is security-relevant, and I'm not very familiar with the
    codebase, I probably lack the foresight to foresee all consequences of
    this change. For this reason, I'm submitting this as an RFC and without
    any tests for now. If you deem this patch reasonable, I'd be happy to
    write tests and add them to a future version.
    
    Thanks,
    
    Julian

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2041%2Fdruckdev%2Fpr2041-graceful-submodule-links-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2041/druckdev/pr2041-graceful-submodule-links-v1
Pull-Request: https://github.com/git/git/pull/2041

 submodule.c | 27 ++++++++++++++++++++-------
 1 file changed, 20 insertions(+), 7 deletions(-)

diff --git a/submodule.c b/submodule.c
index fff3c75570..843ec48339 100644
--- a/submodule.c
+++ b/submodule.c
@@ -1127,7 +1127,7 @@ static int push_submodule(const char *path,
 			  int dry_run)
 {
 	if (validate_submodule_path(path) < 0)
-		exit(128);
+		return 0;
 
 	if (for_each_remote_ref_submodule(path, has_remote, NULL) > 0) {
 		struct child_process cp = CHILD_PROCESS_INIT;
@@ -1514,10 +1514,19 @@ static struct fetch_task *fetch_task_create(struct submodule_parallel_fetch *spf
 {
 	struct fetch_task *task;
 
-	CALLOC_ARRAY(task, 1);
+	if (validate_submodule_path(path) < 0) {
+		// Add submodule path to the list of submodules with errors.
+		// validate_submodule_path already printed an error message, but
+		// it would be confusing if the submodule wouldn't be listed
+		// together with other erroneous modules at the end.
+		//
+		// NEEDSWORK: name instead of path would be nice
+		spf->result = 1;
+		strbuf_addf(&spf->submodules_with_errors, "\t%s\n", path);
+		return NULL;
+	}
 
-	if (validate_submodule_path(path) < 0)
-		exit(128);
+	CALLOC_ARRAY(task, 1);
 
 	task->sub = submodule_from_path(spf->r, treeish_name, path);
 
@@ -1999,8 +2008,12 @@ int bad_to_remove_submodule(const char *path, unsigned flags)
 	struct strbuf buf = STRBUF_INIT;
 	int ret = 0;
 
-	if (validate_submodule_path(path) < 0)
-		exit(128);
+	if (validate_submodule_path(path) < 0) {
+		if (flags & SUBMODULE_REMOVAL_DIE_ON_ERROR)
+			die(NULL);
+		ret = -1;
+		goto out;
+	}
 
 	if (!file_exists(path) || is_empty_dir(path))
 		return 0;
@@ -2555,7 +2568,7 @@ int submodule_to_gitdir(struct repository *repo,
 	int ret = 0;
 
 	if (validate_submodule_path(submodule) < 0)
-		exit(128);
+		return -1;
 
 	strbuf_reset(buf);
 	strbuf_addstr(buf, submodule);

base-commit: f814da676ae46aac5be0a98b99373a76dee6cedb
-- 
gitgitgadget