Skip to content

Commit 9706576

Browse files
committed
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 <[email protected]> Signed-off-by: Johannes Schindelin <[email protected]>
1 parent 9cf8547 commit 9706576

File tree

2 files changed

+83
-0
lines changed

2 files changed

+83
-0
lines changed

builtin/submodule--helper.c

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,12 +1641,35 @@ static char *clone_submodule_sm_gitdir(const char *name)
16411641
return sm_gitdir;
16421642
}
16431643

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+
16441666
static int clone_submodule(const struct module_clone_data *clone_data,
16451667
struct string_list *reference)
16461668
{
16471669
char *p;
16481670
char *sm_gitdir = clone_submodule_sm_gitdir(clone_data->name);
16491671
char *sm_alternate = NULL, *error_strategy = NULL;
1672+
struct stat st;
16501673
struct child_process cp = CHILD_PROCESS_INIT;
16511674
const char *clone_data_path = clone_data->path;
16521675
char *to_free = NULL;
@@ -1660,6 +1683,10 @@ static int clone_submodule(const struct module_clone_data *clone_data,
16601683
"git dir"), sm_gitdir);
16611684

16621685
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+
16631690
if (safe_create_leading_directories_const(sm_gitdir) < 0)
16641691
die(_("could not create directory '%s'"), sm_gitdir);
16651692

@@ -1704,6 +1731,14 @@ static int clone_submodule(const struct module_clone_data *clone_data,
17041731
if(run_command(&cp))
17051732
die(_("clone of '%s' into submodule path '%s' failed"),
17061733
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+
}
17071742
} else {
17081743
char *path;
17091744

t/t7406-submodule-update.sh

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,4 +1179,52 @@ test_expect_success 'submodule update --recursive skip submodules with strategy=
11791179
test_cmp expect.err actual.err
11801180
'
11811181

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+
11821230
test_done

0 commit comments

Comments
 (0)