submodules: submodule paths must not contain symlinks

When creating a submodule path, we must be careful not to follow symbolic links. Otherwise we may follow a symbolic link pointing to a gitdir (which are valid symbolic links!) e.g. while cloning. On case-insensitive filesystems, however, we blindly replace a directory that has been created as part of the `clone` operation with a symlink when the path to the latter differs only in case from the former's path. Let's simply avoid this situation by expecting not ever having to overwrite any existing file/directory/symlink upon cloning. That way, we won't even replace a directory that we just created. This addresses CVE-2024-32002. Reported-by: Filip Hejsek <filip.hejsek@gmail.com> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>

Johannes Schindelin committed Mar 22, 2024 at 11:19 UTC 97065761333fd62db1912d81b489db938d8c991d
2 files changed +83
builtin/submodule--helper.c
+35
@@ -1641,12 +1641,35 @@ static char *clone_submodule_sm_gitdir(const char *name)
1641 return sm_gitdir;
1642 }
1643
1644 +static int dir_contains_only_dotgit(const char *path)
1645 +{
1646 + DIR *dir = opendir(path);
1647 + struct dirent *e;
1648 + int ret = 1;
1649 +
1650 + if (!dir)
1651 + return 0;
1652 +
1653 + e = readdir_skip_dot_and_dotdot(dir);
1654 + if (!e)
1655 + ret = 0;
1656 + else if (strcmp(DEFAULT_GIT_DIR_ENVIRONMENT, e->d_name) ||
1657 + (e = readdir_skip_dot_and_dotdot(dir))) {
1658 + error("unexpected item '%s' in '%s'", e->d_name, path);
1659 + ret = 0;
1660 + }
1661 +
1662 + closedir(dir);
1663 + return ret;
1664 +}
1665 +
1666 static int clone_submodule(const struct module_clone_data *clone_data,
1667 struct string_list *reference)
1668 {
1669 char *p;
1670 char *sm_gitdir = clone_submodule_sm_gitdir(clone_data->name);
1671 char *sm_alternate = NULL, *error_strategy = NULL;
1672 + struct stat st;
1673 struct child_process cp = CHILD_PROCESS_INIT;
1674 const char *clone_data_path = clone_data->path;
1675 char *to_free = NULL;
@@ -1660,6 +1683,10 @@ static int clone_submodule(const struct module_clone_data *clone_data,
1683 "git dir"), sm_gitdir);
1684
1685 if (!file_exists(sm_gitdir)) {
1686 + if (clone_data->require_init && !stat(clone_data_path, &st) &&
1687 + !is_empty_dir(clone_data_path))
1688 + die(_("directory not empty: '%s'"), clone_data_path);
1689 +
1690 if (safe_create_leading_directories_const(sm_gitdir) < 0)
1691 die(_("could not create directory '%s'"), sm_gitdir);
1692
@@ -1704,6 +1731,14 @@ static int clone_submodule(const struct module_clone_data *clone_data,
1731 if(run_command(&cp))
1732 die(_("clone of '%s' into submodule path '%s' failed"),
1733 clone_data->url, clone_data_path);
1734 +
1735 + if (clone_data->require_init && !stat(clone_data_path, &st) &&
1736 + !dir_contains_only_dotgit(clone_data_path)) {
1737 + char *dot_git = xstrfmt("%s/.git", clone_data_path);
1738 + unlink(dot_git);
1739 + free(dot_git);
1740 + die(_("directory not empty: '%s'"), clone_data_path);
1741 + }
1742 } else {
1743 char *path;
1744
t/t7406-submodule-update.sh
+48
@@ -1179,4 +1179,52 @@ test_expect_success 'submodule update --recursive skip submodules with strategy=
1179 test_cmp expect.err actual.err
1180 '
1181
1182 +test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \
1183 + 'submodule paths must not follow symlinks' '
1184 +
1185 + # This is only needed because we want to run this in a self-contained
1186 + # test without having to spin up an HTTP server; However, it would not
1187 + # be needed in a real-world scenario where the submodule is simply
1188 + # hosted on a public site.
1189 + test_config_global protocol.file.allow always &&
1190 +
1191 + # Make sure that Git tries to use symlinks on Windows
1192 + test_config_global core.symlinks true &&
1193 +
1194 + tell_tale_path="$PWD/tell.tale" &&
1195 + git init hook &&
1196 + (
1197 + cd hook &&
1198 + mkdir -p y/hooks &&
1199 + write_script y/hooks/post-checkout <<-EOF &&
1200 + echo HOOK-RUN >&2
1201 + echo hook-run >"$tell_tale_path"
1202 + EOF
1203 + git add y/hooks/post-checkout &&
1204 + test_tick &&
1205 + git commit -m post-checkout
1206 + ) &&
1207 +
1208 + hook_repo_path="$(pwd)/hook" &&
1209 + git init captain &&
1210 + (
1211 + cd captain &&
1212 + git submodule add --name x/y "$hook_repo_path" A/modules/x &&
1213 + test_tick &&
1214 + git commit -m add-submodule &&
1215 +
1216 + printf .git >dotgit.txt &&
1217 + git hash-object -w --stdin <dotgit.txt >dot-git.hash &&
1218 + printf "120000 %s 0\ta\n" "$(cat dot-git.hash)" >index.info &&
1219 + git update-index --index-info <index.info &&
1220 + test_tick &&
1221 + git commit -m add-symlink
1222 + ) &&
1223 +
1224 + test_path_is_missing "$tell_tale_path" &&
1225 + test_must_fail git clone --recursive captain hooked 2>err &&
1226 + grep "directory not empty" err &&
1227 + test_path_is_missing "$tell_tale_path"
1228 +'
1229 +
1230 test_done