Sync with 2.39.4

* maint-2.39: (38 commits)
  Git 2.39.4
  fsck: warn about symlink pointing inside a gitdir
  core.hooksPath: add some protection while cloning
  init.templateDir: consider this config setting protected
  clone: prevent hooks from running during a clone
  Add a helper function to compare file contents
  init: refactor the template directory discovery into its own function
  find_hook(): refactor the `STRIP_EXTENSION` logic
  clone: when symbolic links collide with directories, keep the latter
  entry: report more colliding paths
  t5510: verify that D/F confusion cannot lead to an RCE
  submodule: require the submodule path to contain directories only
  clone_submodule: avoid using `access()` on directories
  submodules: submodule paths must not contain symlinks
  clone: prevent clashing git dirs when cloning submodule in parallel
  t7423: add tests for symlinked submodule directories
  has_dir_name(): do not get confused by characters < '/'
  docs: document security issues around untrusted .git dirs
  upload-pack: disable lazy-fetching by default
  fetch/clone: detect dubious ownership of local repositories
  ...
This commit is contained in:
Johannes Schindelin
2024-04-12 09:45:28 +02:00
44 changed files with 1307 additions and 123 deletions

View File

@ -495,6 +495,16 @@ int cmd__path_utils(int argc, const char **argv)
return !!res;
}
if (argc == 4 && !strcmp(argv[1], "do_files_match")) {
int ret = do_files_match(argv[2], argv[3]);
if (ret)
printf("equal\n");
else
printf("different\n");
return !ret;
}
fprintf(stderr, "%s: unknown function name: %s\n", argv[0],
argv[1] ? argv[1] : "(there was none)");
return 1;

View File

@ -1201,6 +1201,34 @@ test_expect_success 'very long name in the index handled sanely' '
test $len = 4098
'
# D/F conflict checking uses an optimization when adding to the end.
# make sure it does not get confused by `a-` sorting _between_
# `a` and `a/`.
test_expect_success 'more update-index D/F conflicts' '
# empty the index to make sure our entry is last
git read-tree --empty &&
cacheinfo=100644,$(test_oid empty_blob) &&
git update-index --add --cacheinfo $cacheinfo,path5/a &&
test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/file &&
test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/file &&
test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/c/file &&
# "a-" sorts between "a" and "a/"
git update-index --add --cacheinfo $cacheinfo,path5/a- &&
test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/file &&
test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/file &&
test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/c/file &&
cat >expected <<-\EOF &&
path5/a
path5/a-
EOF
git ls-files >actual &&
test_cmp expected actual
'
test_expect_success 'test_must_fail on a failing git command' '
test_must_fail git notacommand
'

View File

@ -80,4 +80,28 @@ test_expect_success 'safe.directory in included file' '
git status
'
test_expect_success 'local clone of unowned repo refused in unsafe directory' '
test_when_finished "rm -rf source" &&
git init source &&
(
sane_unset GIT_TEST_ASSUME_DIFFERENT_OWNER &&
test_commit -C source initial
) &&
test_must_fail git clone --local source target &&
test_path_is_missing target
'
test_expect_success 'local clone of unowned repo accepted in safe directory' '
test_when_finished "rm -rf source" &&
git init source &&
(
sane_unset GIT_TEST_ASSUME_DIFFERENT_OWNER &&
test_commit -C source initial
) &&
test_must_fail git clone --local source target &&
git config --global --add safe.directory "$(pwd)/source/.git" &&
git clone --local source target &&
test_path_is_dir target
'
test_done

View File

@ -560,4 +560,45 @@ test_expect_success !VALGRIND,RUNTIME_PREFIX,CAN_EXEC_IN_PWD '%(prefix)/ works'
test_cmp expect actual
'
test_expect_success 'do_files_match()' '
test_seq 0 10 >0-10.txt &&
test_seq -1 10 >-1-10.txt &&
test_seq 1 10 >1-10.txt &&
test_seq 1 9 >1-9.txt &&
test_seq 0 8 >0-8.txt &&
test-tool path-utils do_files_match 0-10.txt 0-10.txt >out &&
assert_fails() {
test_must_fail \
test-tool path-utils do_files_match "$1" "$2" >out &&
grep different out
} &&
assert_fails 0-8.txt 1-9.txt &&
assert_fails -1-10.txt 0-10.txt &&
assert_fails 1-10.txt 1-9.txt &&
assert_fails 1-10.txt .git &&
assert_fails does-not-exist 1-10.txt &&
if test_have_prereq FILEMODE
then
cp 0-10.txt 0-10.x &&
chmod a+x 0-10.x &&
assert_fails 0-10.txt 0-10.x
fi &&
if test_have_prereq SYMLINKS
then
ln -sf 0-10.txt symlink &&
ln -s 0-10.txt another-symlink &&
ln -s over-the-ocean yet-another-symlink &&
ln -s "$PWD/0-10.txt" absolute-symlink &&
assert_fails 0-10.txt symlink &&
test-tool path-utils do_files_match symlink another-symlink &&
assert_fails symlink yet-another-symlink &&
assert_fails symlink absolute-symlink
fi
'
test_done

78
t/t0411-clone-from-partial.sh Executable file
View File

@ -0,0 +1,78 @@
#!/bin/sh
test_description='check that local clone does not fetch from promisor remotes'
. ./test-lib.sh
test_expect_success 'create evil repo' '
git init tmp &&
test_commit -C tmp a &&
git -C tmp config uploadpack.allowfilter 1 &&
git clone --filter=blob:none --no-local --no-checkout tmp evil &&
rm -rf tmp &&
git -C evil config remote.origin.uploadpack \"\$TRASH_DIRECTORY/fake-upload-pack\" &&
write_script fake-upload-pack <<-\EOF &&
echo >&2 "fake-upload-pack running"
>"$TRASH_DIRECTORY/script-executed"
exit 1
EOF
export TRASH_DIRECTORY &&
# empty shallow file disables local clone optimization
>evil/.git/shallow
'
test_expect_success 'local clone must not fetch from promisor remote and execute script' '
rm -f script-executed &&
test_must_fail git clone \
--upload-pack="GIT_TEST_ASSUME_DIFFERENT_OWNER=true git-upload-pack" \
evil clone1 2>err &&
grep "detected dubious ownership" err &&
! grep "fake-upload-pack running" err &&
test_path_is_missing script-executed
'
test_expect_success 'clone from file://... must not fetch from promisor remote and execute script' '
rm -f script-executed &&
test_must_fail git clone \
--upload-pack="GIT_TEST_ASSUME_DIFFERENT_OWNER=true git-upload-pack" \
"file://$(pwd)/evil" clone2 2>err &&
grep "detected dubious ownership" err &&
! grep "fake-upload-pack running" err &&
test_path_is_missing script-executed
'
test_expect_success 'fetch from file://... must not fetch from promisor remote and execute script' '
rm -f script-executed &&
test_must_fail git fetch \
--upload-pack="GIT_TEST_ASSUME_DIFFERENT_OWNER=true git-upload-pack" \
"file://$(pwd)/evil" 2>err &&
grep "detected dubious ownership" err &&
! grep "fake-upload-pack running" err &&
test_path_is_missing script-executed
'
test_expect_success 'pack-objects should fetch from promisor remote and execute script' '
rm -f script-executed &&
echo "HEAD" | test_must_fail git -C evil pack-objects --revs --stdout >/dev/null 2>err &&
grep "fake-upload-pack running" err &&
test_path_is_file script-executed
'
test_expect_success 'clone from promisor remote does not lazy-fetch by default' '
rm -f script-executed &&
test_must_fail git clone evil no-lazy 2>err &&
grep "lazy fetching disabled" err &&
test_path_is_missing script-executed
'
test_expect_success 'promisor lazy-fetching can be re-enabled' '
rm -f script-executed &&
test_must_fail env GIT_NO_LAZY_FETCH=0 \
git clone evil lazy-ok 2>err &&
grep "fake-upload-pack running" err &&
test_path_is_file script-executed
'
test_done

View File

@ -1023,4 +1023,41 @@ test_expect_success 'fsck error on gitattributes with excessive size' '
test_cmp expected actual
'
test_expect_success 'fsck warning on symlink target with excessive length' '
symlink_target=$(printf "pattern %032769d" 1 | git hash-object -w --stdin) &&
test_when_finished "remove_object $symlink_target" &&
tree=$(printf "120000 blob %s\t%s\n" $symlink_target symlink | git mktree) &&
test_when_finished "remove_object $tree" &&
cat >expected <<-EOF &&
warning in blob $symlink_target: symlinkTargetLength: symlink target too long
EOF
git fsck --no-dangling >actual 2>&1 &&
test_cmp expected actual
'
test_expect_success 'fsck warning on symlink target pointing inside git dir' '
gitdir=$(printf ".git" | git hash-object -w --stdin) &&
ntfs_gitdir=$(printf "GIT~1" | git hash-object -w --stdin) &&
hfs_gitdir=$(printf ".${u200c}git" | git hash-object -w --stdin) &&
inside_gitdir=$(printf "nested/.git/config" | git hash-object -w --stdin) &&
benign_target=$(printf "legit/config" | git hash-object -w --stdin) &&
tree=$(printf "120000 blob %s\t%s\n" \
$benign_target benign_target \
$gitdir gitdir \
$hfs_gitdir hfs_gitdir \
$inside_gitdir inside_gitdir \
$ntfs_gitdir ntfs_gitdir |
git mktree) &&
for o in $gitdir $ntfs_gitdir $hfs_gitdir $inside_gitdir $benign_target $tree
do
test_when_finished "remove_object $o" || return 1
done &&
printf "warning in blob %s: symlinkPointsToGitDir: symlink target points to git dir\n" \
$gitdir $hfs_gitdir $inside_gitdir $ntfs_gitdir |
sort >expected &&
git fsck --no-dangling >actual 2>&1 &&
sort actual >actual.sorted &&
test_cmp expected actual.sorted
'
test_done

View File

@ -195,4 +195,19 @@ test_expect_success 'stdin to hooks' '
test_cmp expect actual
'
test_expect_success 'clone protections' '
test_config core.hooksPath "$(pwd)/my-hooks" &&
mkdir -p my-hooks &&
write_script my-hooks/test-hook <<-\EOF &&
echo Hook ran $1
EOF
git hook run test-hook 2>err &&
grep "Hook ran" err &&
test_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \
git hook run test-hook 2>err &&
grep "active .core.hooksPath" err &&
! grep "Hook ran" err
'
test_done

View File

@ -1248,6 +1248,30 @@ EOF
test_cmp fatal-expect fatal-actual
'
test_expect_success SYMLINKS 'clone does not get confused by a D/F conflict' '
git init df-conflict &&
(
cd df-conflict &&
ln -s .git a &&
git add a &&
test_tick &&
git commit -m symlink &&
test_commit a- &&
rm a &&
mkdir -p a/hooks &&
write_script a/hooks/post-checkout <<-EOF &&
echo WHOOPSIE >&2
echo whoopsie >"$TRASH_DIRECTORY"/whoops
EOF
git add a/hooks/post-checkout &&
test_tick &&
git commit -m post-checkout
) &&
git clone df-conflict clone 2>err &&
! grep WHOOPS err &&
test_path_is_missing whoops
'
. "$TEST_DIRECTORY"/lib-httpd.sh
start_httpd

View File

@ -633,6 +633,21 @@ test_expect_success CASE_INSENSITIVE_FS 'colliding file detection' '
test_i18ngrep "the following paths have collided" icasefs/warning
'
test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \
'colliding symlink/directory keeps directory' '
git init icasefs-colliding-symlink &&
(
cd icasefs-colliding-symlink &&
a=$(printf a | git hash-object -w --stdin) &&
printf "100644 %s 0\tA/dir/b\n120000 %s 0\ta\n" $a $a >idx &&
git update-index --index-info <idx &&
test_tick &&
git commit -m initial
) &&
git clone icasefs-colliding-symlink icasefs-colliding-symlink-clone &&
test_file_not_empty icasefs-colliding-symlink-clone/A/dir/b
'
test_expect_success 'clone with GIT_DEFAULT_HASH' '
(
sane_unset GIT_DEFAULT_HASH &&
@ -756,6 +771,57 @@ test_expect_success 'batch missing blob request does not inadvertently try to fe
git clone --filter=blob:limit=0 "file://$(pwd)/server" client
'
test_expect_success 'clone with init.templatedir runs hooks' '
git init tmpl/hooks &&
write_script tmpl/hooks/post-checkout <<-EOF &&
echo HOOK-RUN >&2
echo I was here >hook.run
EOF
git -C tmpl/hooks add . &&
test_tick &&
git -C tmpl/hooks commit -m post-checkout &&
test_when_finished "git config --global --unset init.templateDir || :" &&
test_when_finished "git config --unset init.templateDir || :" &&
(
sane_unset GIT_TEMPLATE_DIR &&
NO_SET_GIT_TEMPLATE_DIR=t &&
export NO_SET_GIT_TEMPLATE_DIR &&
git -c core.hooksPath="$(pwd)/tmpl/hooks" \
clone tmpl/hooks hook-run-hookspath 2>err &&
! grep "active .* hook found" err &&
test_path_is_file hook-run-hookspath/hook.run &&
git -c init.templateDir="$(pwd)/tmpl" \
clone tmpl/hooks hook-run-config 2>err &&
! grep "active .* hook found" err &&
test_path_is_file hook-run-config/hook.run &&
git clone --template=tmpl tmpl/hooks hook-run-option 2>err &&
! grep "active .* hook found" err &&
test_path_is_file hook-run-option/hook.run &&
git config --global init.templateDir "$(pwd)/tmpl" &&
git clone tmpl/hooks hook-run-global-config 2>err &&
git config --global --unset init.templateDir &&
! grep "active .* hook found" err &&
test_path_is_file hook-run-global-config/hook.run &&
# clone ignores local `init.templateDir`; need to create
# a new repository because we deleted `.git/` in the
# `setup` test case above
git init local-clone &&
cd local-clone &&
git config init.templateDir "$(pwd)/../tmpl" &&
git clone ../tmpl/hooks hook-run-local-config 2>err &&
git config --unset init.templateDir &&
! grep "active .* hook found" err &&
test_path_is_missing hook-run-local-config/hook.run
)
'
. "$TEST_DIRECTORY"/lib-httpd.sh
start_httpd

View File

@ -1436,4 +1436,35 @@ test_expect_success 'recursive clone respects -q' '
test_must_be_empty actual
'
test_expect_success '`submodule init` and `init.templateDir`' '
mkdir -p tmpl/hooks &&
write_script tmpl/hooks/post-checkout <<-EOF &&
echo HOOK-RUN >&2
echo I was here >hook.run
exit 1
EOF
test_config init.templateDir "$(pwd)/tmpl" &&
test_when_finished \
"git config --global --unset init.templateDir || true" &&
(
sane_unset GIT_TEMPLATE_DIR &&
NO_SET_GIT_TEMPLATE_DIR=t &&
export NO_SET_GIT_TEMPLATE_DIR &&
git config --global init.templateDir "$(pwd)/tmpl" &&
test_must_fail git submodule \
add "$submodurl" sub-global 2>err &&
git config --global --unset init.templateDir &&
grep HOOK-RUN err &&
test_path_is_file sub-global/hook.run &&
git config init.templateDir "$(pwd)/tmpl" &&
git submodule add "$submodurl" sub-local 2>err &&
git config --unset init.templateDir &&
! grep HOOK-RUN err &&
test_path_is_missing sub-local/hook.run
)
'
test_done

View File

@ -1179,4 +1179,52 @@ test_expect_success 'submodule update --recursive skip submodules with strategy=
test_cmp expect.err actual.err
'
test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \
'submodule paths must not follow symlinks' '
# This is only needed because we want to run this in a self-contained
# test without having to spin up an HTTP server; However, it would not
# be needed in a real-world scenario where the submodule is simply
# hosted on a public site.
test_config_global protocol.file.allow always &&
# Make sure that Git tries to use symlinks on Windows
test_config_global core.symlinks true &&
tell_tale_path="$PWD/tell.tale" &&
git init hook &&
(
cd hook &&
mkdir -p y/hooks &&
write_script y/hooks/post-checkout <<-EOF &&
echo HOOK-RUN >&2
echo hook-run >"$tell_tale_path"
EOF
git add y/hooks/post-checkout &&
test_tick &&
git commit -m post-checkout
) &&
hook_repo_path="$(pwd)/hook" &&
git init captain &&
(
cd captain &&
git submodule add --name x/y "$hook_repo_path" A/modules/x &&
test_tick &&
git commit -m add-submodule &&
printf .git >dotgit.txt &&
git hash-object -w --stdin <dotgit.txt >dot-git.hash &&
printf "120000 %s 0\ta\n" "$(cat dot-git.hash)" >index.info &&
git update-index --index-info <index.info &&
test_tick &&
git commit -m add-symlink
) &&
test_path_is_missing "$tell_tale_path" &&
git clone --recursive captain hooked 2>err &&
! grep HOOK-RUN err &&
test_path_is_missing "$tell_tale_path"
'
test_done

67
t/t7423-submodule-symlinks.sh Executable file
View File

@ -0,0 +1,67 @@
#!/bin/sh
test_description='check that submodule operations do not follow symlinks'
. ./test-lib.sh
test_expect_success 'prepare' '
git config --global protocol.file.allow always &&
test_commit initial &&
git init upstream &&
test_commit -C upstream upstream submodule_file &&
git submodule add ./upstream a/sm &&
test_tick &&
git commit -m submodule
'
test_expect_success SYMLINKS 'git submodule update must not create submodule behind symlink' '
rm -rf a b &&
mkdir b &&
ln -s b a &&
test_path_is_missing b/sm &&
test_must_fail git submodule update &&
test_path_is_missing b/sm
'
test_expect_success SYMLINKS,CASE_INSENSITIVE_FS 'git submodule update must not create submodule behind symlink on case insensitive fs' '
rm -rf a b &&
mkdir b &&
ln -s b A &&
test_must_fail git submodule update &&
test_path_is_missing b/sm
'
prepare_symlink_to_repo() {
rm -rf a &&
mkdir a &&
git init a/target &&
git -C a/target fetch ../../upstream &&
ln -s target a/sm
}
test_expect_success SYMLINKS 'git restore --recurse-submodules must not be confused by a symlink' '
prepare_symlink_to_repo &&
test_must_fail git restore --recurse-submodules a/sm &&
test_path_is_missing a/sm/submodule_file &&
test_path_is_dir a/target/.git &&
test_path_is_missing a/target/submodule_file
'
test_expect_success SYMLINKS 'git restore --recurse-submodules must not migrate git dir of symlinked repo' '
prepare_symlink_to_repo &&
rm -rf .git/modules &&
test_must_fail git restore --recurse-submodules a/sm &&
test_path_is_dir a/target/.git &&
test_path_is_missing .git/modules/a/sm &&
test_path_is_missing a/target/submodule_file
'
test_expect_success SYMLINKS 'git checkout -f --recurse-submodules must not migrate git dir of symlinked repo when removing submodule' '
prepare_symlink_to_repo &&
rm -rf .git/modules &&
test_must_fail git checkout -f --recurse-submodules initial &&
test_path_is_dir a/target/.git &&
test_path_is_missing .git/modules/a/sm
'
test_done

View File

@ -294,7 +294,7 @@ test_expect_success WINDOWS 'prevent git~1 squatting on Windows' '
fi
'
test_expect_success 'git dirs of sibling submodules must not be nested' '
test_expect_success 'setup submodules with nested git dirs' '
git init nested &&
test_commit -C nested nested &&
(
@ -312,9 +312,39 @@ test_expect_success 'git dirs of sibling submodules must not be nested' '
git add .gitmodules thing1 thing2 &&
test_tick &&
git commit -m nested
) &&
)
'
test_expect_success 'git dirs of sibling submodules must not be nested' '
test_must_fail git clone --recurse-submodules nested clone 2>err &&
test_i18ngrep "is inside git dir" err
'
test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '
test_must_fail git clone --recurse-submodules --jobs=2 nested clone_parallel 2>err &&
cat err &&
grep -E "(already exists|is inside git dir|not a git repository)" err &&
{
test_path_is_missing .git/modules/hippo/HEAD ||
test_path_is_missing .git/modules/hippo/hooks/HEAD
}
'
test_expect_success 'checkout -f --recurse-submodules must not use a nested gitdir' '
git clone nested nested_checkout &&
(
cd nested_checkout &&
git submodule init &&
git submodule update thing1 &&
mkdir -p .git/modules/hippo/hooks/refs &&
mkdir -p .git/modules/hippo/hooks/objects/info &&
echo "../../../../objects" >.git/modules/hippo/hooks/objects/info/alternates &&
echo "ref: refs/heads/master" >.git/modules/hippo/hooks/HEAD
) &&
test_must_fail git -C nested_checkout checkout -f --recurse-submodules HEAD 2>err &&
cat err &&
grep "is inside git dir" err &&
test_path_is_missing nested_checkout/thing2/.git
'
test_done

View File

@ -334,6 +334,7 @@ nr_san_dir_leaks_ () {
find "$TEST_RESULTS_SAN_DIR" \
-type f \
-name "$TEST_RESULTS_SAN_FILE_PFX.*" 2>/dev/null |
xargs grep -lv "Unable to get registers from thread" |
wc -l
}