Skip to content

Commit 31572dc

Browse files
committed
clone: when symbolic links collide with directories, keep the latter
When recursively cloning a repository with submodules, we must ensure that the submodules paths do not suddenly contain symbolic links that would let Git write into unintended locations. We just plugged that vulnerability, but let's add some more defense-in-depth. Since we can only keep one item on disk if multiple index entries' paths collide, we may just as well avoid keeping a symbolic link (because that would allow attack vectors where Git follows those links by mistake). Technically, we handle more situations than cloning submodules into paths that were (partially) replaced by symbolic links. This provides defense-in-depth in case someone finds a case-folding confusion vulnerability in the future that does not even involve submodules. Signed-off-by: Johannes Schindelin <[email protected]>
1 parent 850c3a2 commit 31572dc

File tree

3 files changed

+31
-2
lines changed

3 files changed

+31
-2
lines changed

entry.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,20 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca,
541541
/* If it is a gitlink, leave it alone! */
542542
if (S_ISGITLINK(ce->ce_mode))
543543
return 0;
544+
/*
545+
* We must avoid replacing submodules' leading
546+
* directories with symbolic links, lest recursive
547+
* clones can write into arbitrary locations.
548+
*
549+
* Technically, this logic is not limited
550+
* to recursive clones, or for that matter to
551+
* submodules' paths colliding with symbolic links'
552+
* paths. Yet it strikes a balance in favor of
553+
* simplicity, and if paths are colliding, we might
554+
* just as well keep the directories during a clone.
555+
*/
556+
if (state->clone && S_ISLNK(ce->ce_mode))
557+
return 0;
544558
remove_subtree(&path);
545559
} else if (unlink(path.buf))
546560
return error_errno("unable to unlink old '%s'", path.buf);

t/t5601-clone.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,21 @@ test_expect_success CASE_INSENSITIVE_FS 'colliding file detection' '
633633
test_i18ngrep "the following paths have collided" icasefs/warning
634634
'
635635

636+
test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \
637+
'colliding symlink/directory keeps directory' '
638+
git init icasefs-colliding-symlink &&
639+
(
640+
cd icasefs-colliding-symlink &&
641+
a=$(printf a | git hash-object -w --stdin) &&
642+
printf "100644 %s 0\tA/dir/b\n120000 %s 0\ta\n" $a $a >idx &&
643+
git update-index --index-info <idx &&
644+
test_tick &&
645+
git commit -m initial
646+
) &&
647+
git clone icasefs-colliding-symlink icasefs-colliding-symlink-clone &&
648+
test_file_not_empty icasefs-colliding-symlink-clone/A/dir/b
649+
'
650+
636651
test_expect_success 'clone with GIT_DEFAULT_HASH' '
637652
(
638653
sane_unset GIT_DEFAULT_HASH &&

t/t7406-submodule-update.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,8 +1222,8 @@ test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \
12221222
) &&
12231223
12241224
test_path_is_missing "$tell_tale_path" &&
1225-
test_must_fail git clone --recursive captain hooked 2>err &&
1226-
grep "directory not empty" err &&
1225+
git clone --recursive captain hooked 2>err &&
1226+
! grep HOOK-RUN err &&
12271227
test_path_is_missing "$tell_tale_path"
12281228
'
12291229

0 commit comments

Comments
 (0)