Sync with 2.40.2
* maint-2.40: (39 commits)
Git 2.40.2
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
...
diff --git a/.github/workflows/check-whitespace.yml b/.github/workflows/check-whitespace.yml
index a58e2dc..a241a63 100644
--- a/.github/workflows/check-whitespace.yml
+++ b/.github/workflows/check-whitespace.yml
@@ -19,7 +19,7 @@
check-whitespace:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 30492ea..b8aa4c9 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -46,7 +46,7 @@
echo "skip_concurrent=$skip_concurrent" >>$GITHUB_OUTPUT
- name: skip if the commit or tree was already tested
id: skip-if-redundant
- uses: actions/github-script@v6
+ uses: actions/github-script@v7
if: steps.check-ref.outputs.enabled == 'yes'
with:
github-token: ${{secrets.GITHUB_TOKEN}}
@@ -95,7 +95,7 @@
group: windows-build-${{ github.ref }}
cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: git-for-windows/setup-git-for-windows-sdk@v1
- name: build
shell: bash
@@ -106,7 +106,7 @@
- name: zip up tracked files
run: git archive -o artifacts/tracked.tar.gz HEAD
- name: upload tracked files and build artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: windows-artifacts
path: artifacts
@@ -123,7 +123,7 @@
cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}
steps:
- name: download tracked files and build artifacts
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
with:
name: windows-artifacts
path: ${{github.workspace}}
@@ -140,7 +140,7 @@
run: ci/print-test-failures.sh
- name: Upload failed tests' directories
if: failure() && env.FAILED_TEST_ARTIFACTS != ''
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: failed-tests-windows
path: ${{env.FAILED_TEST_ARTIFACTS}}
@@ -156,10 +156,10 @@
group: vs-build-${{ github.ref }}
cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: git-for-windows/setup-git-for-windows-sdk@v1
- name: initialize vcpkg
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: 'microsoft/vcpkg'
path: 'compat/vcbuild/vcpkg'
@@ -195,7 +195,7 @@
- name: zip up tracked files
run: git archive -o artifacts/tracked.tar.gz HEAD
- name: upload tracked files and build artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: vs-artifacts
path: artifacts
@@ -213,7 +213,7 @@
steps:
- uses: git-for-windows/setup-git-for-windows-sdk@v1
- name: download tracked files and build artifacts
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
with:
name: vs-artifacts
path: ${{github.workspace}}
@@ -231,7 +231,7 @@
run: ci/print-test-failures.sh
- name: Upload failed tests' directories
if: failure() && env.FAILED_TEST_ARTIFACTS != ''
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: failed-tests-windows
path: ${{env.FAILED_TEST_ARTIFACTS}}
@@ -262,11 +262,11 @@
pool: ubuntu-20.04
- jobname: osx-clang
cc: clang
- pool: macos-12
+ pool: macos-13
- jobname: osx-gcc
cc: gcc
- cc_package: gcc-9
- pool: macos-12
+ cc_package: gcc-13
+ pool: macos-13
- jobname: linux-gcc-default
cc: gcc
pool: ubuntu-latest
@@ -286,7 +286,7 @@
runs_on_pool: ${{matrix.vector.pool}}
runs-on: ${{matrix.vector.pool}}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- run: ci/install-dependencies.sh
- run: ci/run-build-and-tests.sh
- name: print test failures
@@ -294,7 +294,7 @@
run: ci/print-test-failures.sh
- name: Upload failed tests' directories
if: failure() && env.FAILED_TEST_ARTIFACTS != ''
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: failed-tests-${{matrix.vector.jobname}}
path: ${{env.FAILED_TEST_ARTIFACTS}}
@@ -320,9 +320,9 @@
runs-on: ubuntu-latest
container: ${{matrix.vector.image}}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
if: matrix.vector.jobname != 'linux32'
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v1 # cannot be upgraded because Node.js Actions aren't supported in this container
if: matrix.vector.jobname == 'linux32'
- run: ci/install-docker-dependencies.sh
- run: ci/run-build-and-tests.sh
@@ -331,13 +331,13 @@
run: ci/print-test-failures.sh
- name: Upload failed tests' directories
if: failure() && env.FAILED_TEST_ARTIFACTS != '' && matrix.vector.jobname != 'linux32'
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: failed-tests-${{matrix.vector.jobname}}
path: ${{env.FAILED_TEST_ARTIFACTS}}
- name: Upload failed tests' directories
if: failure() && env.FAILED_TEST_ARTIFACTS != '' && matrix.vector.jobname == 'linux32'
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v1 # cannot be upgraded because Node.js Actions aren't supported in this container
with:
name: failed-tests-${{matrix.vector.jobname}}
path: ${{env.FAILED_TEST_ARTIFACTS}}
@@ -351,7 +351,7 @@
group: static-analysis-${{ github.ref }}
cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- run: ci/install-dependencies.sh
- run: ci/run-static-analysis.sh
- run: ci/check-directional-formatting.bash
@@ -374,7 +374,7 @@
artifact: sparse-20.04
- name: Install the current `sparse` package
run: sudo dpkg -i sparse-20.04/sparse_*.deb
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Install other dependencies
run: ci/install-dependencies.sh
- run: make sparse
@@ -389,6 +389,6 @@
jobname: Documentation
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- run: ci/install-dependencies.sh
- run: ci/test-documentation.sh
diff --git a/Documentation/RelNotes/2.39.4.txt b/Documentation/RelNotes/2.39.4.txt
new file mode 100644
index 0000000..7f54521
--- /dev/null
+++ b/Documentation/RelNotes/2.39.4.txt
@@ -0,0 +1,79 @@
+Git v2.39.4 Release Notes
+=========================
+
+This addresses the security issues CVE-2024-32002, CVE-2024-32004,
+CVE-2024-32020 and CVE-2024-32021.
+
+This release also backports fixes necessary to let the CI builds pass
+successfully.
+
+Fixes since v2.39.3
+-------------------
+
+ * CVE-2024-32002:
+
+ Recursive clones on case-insensitive filesystems that support symbolic
+ links are susceptible to case confusion that can be exploited to
+ execute just-cloned code during the clone operation.
+
+ * CVE-2024-32004:
+
+ Repositories can be configured to execute arbitrary code during local
+ clones. To address this, the ownership checks introduced in v2.30.3
+ are now extended to cover cloning local repositories.
+
+ * CVE-2024-32020:
+
+ Local clones may end up hardlinking files into the target repository's
+ object database when source and target repository reside on the same
+ disk. If the source repository is owned by a different user, then
+ those hardlinked files may be rewritten at any point in time by the
+ untrusted user.
+
+ * CVE-2024-32021:
+
+ When cloning a local source repository that contains symlinks via the
+ filesystem, Git may create hardlinks to arbitrary user-readable files
+ on the same filesystem as the target repository in the objects/
+ directory.
+
+ * CVE-2024-32465:
+
+ It is supposed to be safe to clone untrusted repositories, even those
+ unpacked from zip archives or tarballs originating from untrusted
+ sources, but Git can be tricked to run arbitrary code as part of the
+ clone.
+
+ * Defense-in-depth: submodule: require the submodule path to contain
+ directories only.
+
+ * Defense-in-depth: clone: when symbolic links collide with directories, keep
+ the latter.
+
+ * Defense-in-depth: clone: prevent hooks from running during a clone.
+
+ * Defense-in-depth: core.hooksPath: add some protection while cloning.
+
+ * Defense-in-depth: fsck: warn about symlink pointing inside a gitdir.
+
+ * Various fix-ups on HTTP tests.
+
+ * Test update.
+
+ * HTTP Header redaction code has been adjusted for a newer version of
+ cURL library that shows its traces differently from earlier
+ versions.
+
+ * Fix was added to work around a regression in libcURL 8.7.0 (which has
+ already been fixed in their tip of the tree).
+
+ * Replace macos-12 used at GitHub CI with macos-13.
+
+ * ci(linux-asan/linux-ubsan): let's save some time
+
+ * Tests with LSan from time to time seem to emit harmless message that makes
+ our tests unnecessarily flakey; we work it around by filtering the
+ uninteresting output.
+
+ * Update GitHub Actions jobs to avoid warnings against using deprecated
+ version of Node.js.
diff --git a/Documentation/RelNotes/2.40.2.txt b/Documentation/RelNotes/2.40.2.txt
new file mode 100644
index 0000000..646a2cc
--- /dev/null
+++ b/Documentation/RelNotes/2.40.2.txt
@@ -0,0 +1,7 @@
+Git v2.40.2 Release Notes
+=========================
+
+This release merges up the fix that appears in v2.39.4 to address
+the security issues CVE-2024-32002, CVE-2024-32004, CVE-2024-32020,
+CVE-2024-32021 and CVE-2024-32465; see the release notes for that
+version for details.
diff --git a/Documentation/fsck-msgids.txt b/Documentation/fsck-msgids.txt
index 12eae8a..b06ec38 100644
--- a/Documentation/fsck-msgids.txt
+++ b/Documentation/fsck-msgids.txt
@@ -157,6 +157,18 @@
`nullSha1`::
(WARN) Tree contains entries pointing to a null sha1.
+`symlinkPointsToGitDir`::
+ (WARN) Symbolic link points inside a gitdir.
+
+`symlinkTargetBlob`::
+ (ERROR) A non-blob found instead of a symbolic link's target.
+
+`symlinkTargetLength`::
+ (WARN) Symbolic link target longer than maximum path length.
+
+`symlinkTargetMissing`::
+ (ERROR) Unable to read symbolic link target's blob.
+
`treeNotSorted`::
(ERROR) A tree is not properly sorted.
diff --git a/Documentation/git-upload-pack.txt b/Documentation/git-upload-pack.txt
index b656b47..1d30a4f 100644
--- a/Documentation/git-upload-pack.txt
+++ b/Documentation/git-upload-pack.txt
@@ -55,6 +55,37 @@
admins may need to configure some transports to allow this
variable to be passed. See the discussion in linkgit:git[1].
+`GIT_NO_LAZY_FETCH`::
+ When cloning or fetching from a partial repository (i.e., one
+ itself cloned with `--filter`), the server-side `upload-pack`
+ may need to fetch extra objects from its upstream in order to
+ complete the request. By default, `upload-pack` will refuse to
+ perform such a lazy fetch, because `git fetch` may run arbitrary
+ commands specified in configuration and hooks of the source
+ repository (and `upload-pack` tries to be safe to run even in
+ untrusted `.git` directories).
++
+This is implemented by having `upload-pack` internally set the
+`GIT_NO_LAZY_FETCH` variable to `1`. If you want to override it
+(because you are fetching from a partial clone, and you are sure
+you trust it), you can explicitly set `GIT_NO_LAZY_FETCH` to
+`0`.
+
+SECURITY
+--------
+
+Most Git commands should not be run in an untrusted `.git` directory
+(see the section `SECURITY` in linkgit:git[1]). `upload-pack` tries to
+avoid any dangerous configuration options or hooks from the repository
+it's serving, making it safe to clone an untrusted directory and run
+commands on the resulting clone.
+
+For an extra level of safety, you may be able to run `upload-pack` as an
+alternate user. The details will be platform dependent, but on many
+systems you can run:
+
+ git clone --no-local --upload-pack='sudo -u nobody git-upload-pack' ...
+
SEE ALSO
--------
linkgit:gitnamespaces[7]
diff --git a/Documentation/git.txt b/Documentation/git.txt
index f0cafa2..0f409b2 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -1034,6 +1034,37 @@
for a given pathname. These stages are used to hold the various
unmerged version of a file when a merge is in progress.
+SECURITY
+--------
+
+Some configuration options and hook files may cause Git to run arbitrary
+shell commands. Because configuration and hooks are not copied using
+`git clone`, it is generally safe to clone remote repositories with
+untrusted content, inspect them with `git log`, and so on.
+
+However, it is not safe to run Git commands in a `.git` directory (or
+the working tree that surrounds it) when that `.git` directory itself
+comes from an untrusted source. The commands in its config and hooks
+are executed in the usual way.
+
+By default, Git will refuse to run when the repository is owned by
+someone other than the user running the command. See the entry for
+`safe.directory` in linkgit:git-config[1]. While this can help protect
+you in a multi-user environment, note that you can also acquire
+untrusted repositories that are owned by you (for example, if you
+extract a zip file or tarball from an untrusted source). In such cases,
+you'd need to "sanitize" the untrusted repository first.
+
+If you have an untrusted `.git` directory, you should first clone it
+with `git clone --no-local` to obtain a clean copy. Git does restrict
+the set of options and hooks that will be run by `upload-pack`, which
+handles the server side of a clone or fetch, but beware that the
+surface area for attack against `upload-pack` is large, so this does
+carry some risk. The safest thing is to serve the repository as an
+unprivileged user (either via linkgit:git-daemon[1], ssh, or using
+other tools to change user ids). See the discussion in the `SECURITY`
+section of linkgit:git-upload-pack[1].
+
FURTHER DOCUMENTATION
---------------------
diff --git a/INSTALL b/INSTALL
index 4b42288..6745146 100644
--- a/INSTALL
+++ b/INSTALL
@@ -139,7 +139,7 @@
not need that functionality, use NO_CURL to build without
it.
- Git requires version "7.19.5" or later of "libcurl" to build
+ Git requires version "7.21.3" or later of "libcurl" to build
without NO_CURL. This version requirement may be bumped in
the future.
diff --git a/builtin/clone.c b/builtin/clone.c
index 15f9912..d6545d0 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -330,7 +330,20 @@
int src_len, dest_len;
struct dir_iterator *iter;
int iter_status;
- struct strbuf realpath = STRBUF_INIT;
+
+ /*
+ * Refuse copying directories by default which aren't owned by us. The
+ * code that performs either the copying or hardlinking is not prepared
+ * to handle various edge cases where an adversary may for example
+ * racily swap out files for symlinks. This can cause us to
+ * inadvertently use the wrong source file.
+ *
+ * Furthermore, even if we were prepared to handle such races safely,
+ * creating hardlinks across user boundaries is an inherently unsafe
+ * operation as the hardlinked files can be rewritten at will by the
+ * potentially-untrusted user. We thus refuse to do so by default.
+ */
+ die_upon_dubious_ownership(NULL, NULL, src_repo);
mkdir_if_missing(dest->buf, 0777);
@@ -378,9 +391,27 @@
if (unlink(dest->buf) && errno != ENOENT)
die_errno(_("failed to unlink '%s'"), dest->buf);
if (!option_no_hardlinks) {
- strbuf_realpath(&realpath, src->buf, 1);
- if (!link(realpath.buf, dest->buf))
+ if (!link(src->buf, dest->buf)) {
+ struct stat st;
+
+ /*
+ * Sanity-check whether the created hardlink
+ * actually links to the expected file now. This
+ * catches time-of-check-time-of-use bugs in
+ * case the source file was meanwhile swapped.
+ */
+ if (lstat(dest->buf, &st))
+ die(_("hardlink cannot be checked at '%s'"), dest->buf);
+ if (st.st_mode != iter->st.st_mode ||
+ st.st_ino != iter->st.st_ino ||
+ st.st_dev != iter->st.st_dev ||
+ st.st_size != iter->st.st_size ||
+ st.st_uid != iter->st.st_uid ||
+ st.st_gid != iter->st.st_gid)
+ die(_("hardlink different from source at '%s'"), dest->buf);
+
continue;
+ }
if (option_local > 0)
die_errno(_("failed to create link '%s'"), dest->buf);
option_no_hardlinks = 1;
@@ -393,8 +424,6 @@
strbuf_setlen(src, src_len);
die(_("failed to iterate over '%s'"), src->buf);
}
-
- strbuf_release(&realpath);
}
static void clone_local(const char *src_repo, const char *dest_repo)
@@ -930,6 +959,8 @@
int submodule_progress;
int filter_submodules = 0;
int hash_algo;
+ const char *template_dir;
+ char *template_dir_dup = NULL;
struct transport_ls_refs_options transport_ls_refs_options =
TRANSPORT_LS_REFS_OPTIONS_INIT;
@@ -949,6 +980,13 @@
usage_msg_opt(_("You must specify a repository to clone."),
builtin_clone_usage, builtin_clone_options);
+ xsetenv("GIT_CLONE_PROTECTION_ACTIVE", "true", 0 /* allow user override */);
+ template_dir = get_template_dir(option_template);
+ if (*template_dir && !is_absolute_path(template_dir))
+ template_dir = template_dir_dup =
+ absolute_pathdup(template_dir);
+ xsetenv("GIT_CLONE_TEMPLATE_DIR", template_dir, 1);
+
if (option_depth || option_since || option_not.nr)
deepen = 1;
if (option_single_branch == -1)
@@ -1096,7 +1134,7 @@
}
}
- init_db(git_dir, real_git_dir, option_template, GIT_HASH_UNKNOWN, NULL,
+ init_db(git_dir, real_git_dir, template_dir, GIT_HASH_UNKNOWN, NULL,
INIT_DB_QUIET);
if (real_git_dir) {
@@ -1440,6 +1478,7 @@
free(dir);
free(path);
free(repo_to_free);
+ free(template_dir_dup);
junk_mode = JUNK_LEAVE_ALL;
transport_ls_refs_options_release(&transport_ls_refs_options);
diff --git a/builtin/init-db.c b/builtin/init-db.c
index aef4036..848f7ec 100644
--- a/builtin/init-db.c
+++ b/builtin/init-db.c
@@ -19,10 +19,6 @@
#include "worktree.h"
#include "wrapper.h"
-#ifndef DEFAULT_GIT_TEMPLATE_DIR
-#define DEFAULT_GIT_TEMPLATE_DIR "/usr/share/git-core/templates"
-#endif
-
#ifdef NO_TRUSTABLE_FILEMODE
#define TEST_FILEMODE 0
#else
@@ -101,8 +97,9 @@
}
}
-static void copy_templates(const char *template_dir, const char *init_template_dir)
+static void copy_templates(const char *option_template)
{
+ const char *template_dir = get_template_dir(option_template);
struct strbuf path = STRBUF_INIT;
struct strbuf template_path = STRBUF_INIT;
size_t template_len;
@@ -111,16 +108,8 @@
DIR *dir;
char *to_free = NULL;
- if (!template_dir)
- template_dir = getenv(TEMPLATE_DIR_ENVIRONMENT);
- if (!template_dir)
- template_dir = init_template_dir;
- if (!template_dir)
- template_dir = to_free = system_path(DEFAULT_GIT_TEMPLATE_DIR);
- if (!template_dir[0]) {
- free(to_free);
+ if (!template_dir || !*template_dir)
return;
- }
strbuf_addstr(&template_path, template_dir);
strbuf_complete(&template_path, '/');
@@ -208,7 +197,6 @@
int reinit;
int filemode;
struct strbuf err = STRBUF_INIT;
- const char *init_template_dir = NULL;
const char *work_tree = get_git_work_tree();
/*
@@ -220,9 +208,7 @@
* values (since we've just potentially changed what's available on
* disk).
*/
- git_config_get_pathname("init.templatedir", &init_template_dir);
- copy_templates(template_path, init_template_dir);
- free((char *)init_template_dir);
+ copy_templates(template_path);
git_config_clear();
reset_shared_repository();
git_config(git_default_config, NULL);
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 6bf8d66..7f6981e 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -302,6 +302,9 @@
struct child_process cp = CHILD_PROCESS_INIT;
char *displaypath;
+ if (validate_submodule_path(path) < 0)
+ exit(128);
+
displaypath = get_submodule_displaypath(path, info->prefix,
info->super_prefix);
@@ -634,6 +637,9 @@
.free_removed_argv_elements = 1,
};
+ if (validate_submodule_path(path) < 0)
+ exit(128);
+
if (!submodule_from_path(the_repository, null_oid(), path))
die(_("no submodule mapping found in .gitmodules for path '%s'"),
path);
@@ -1238,6 +1244,9 @@
if (!is_submodule_active(the_repository, path))
return;
+ if (validate_submodule_path(path) < 0)
+ exit(128);
+
sub = submodule_from_path(the_repository, null_oid(), path);
if (sub && sub->url) {
@@ -1381,6 +1390,9 @@
struct strbuf sb_config = STRBUF_INIT;
char *sub_git_dir = xstrfmt("%s/.git", path);
+ if (validate_submodule_path(path) < 0)
+ exit(128);
+
sub = submodule_from_path(the_repository, null_oid(), path);
if (!sub || !sub->name)
@@ -1662,16 +1674,42 @@
return sm_gitdir;
}
+static int dir_contains_only_dotgit(const char *path)
+{
+ DIR *dir = opendir(path);
+ struct dirent *e;
+ int ret = 1;
+
+ if (!dir)
+ return 0;
+
+ e = readdir_skip_dot_and_dotdot(dir);
+ if (!e)
+ ret = 0;
+ else if (strcmp(DEFAULT_GIT_DIR_ENVIRONMENT, e->d_name) ||
+ (e = readdir_skip_dot_and_dotdot(dir))) {
+ error("unexpected item '%s' in '%s'", e->d_name, path);
+ ret = 0;
+ }
+
+ closedir(dir);
+ return ret;
+}
+
static int clone_submodule(const struct module_clone_data *clone_data,
struct string_list *reference)
{
char *p;
char *sm_gitdir = clone_submodule_sm_gitdir(clone_data->name);
char *sm_alternate = NULL, *error_strategy = NULL;
+ struct stat st;
struct child_process cp = CHILD_PROCESS_INIT;
const char *clone_data_path = clone_data->path;
char *to_free = NULL;
+ if (validate_submodule_path(clone_data_path) < 0)
+ exit(128);
+
if (!is_absolute_path(clone_data->path))
clone_data_path = to_free = xstrfmt("%s/%s", get_git_work_tree(),
clone_data->path);
@@ -1681,6 +1719,10 @@
"git dir"), sm_gitdir);
if (!file_exists(sm_gitdir)) {
+ if (clone_data->require_init && !stat(clone_data_path, &st) &&
+ !is_empty_dir(clone_data_path))
+ die(_("directory not empty: '%s'"), clone_data_path);
+
if (safe_create_leading_directories_const(sm_gitdir) < 0)
die(_("could not create directory '%s'"), sm_gitdir);
@@ -1725,10 +1767,18 @@
if(run_command(&cp))
die(_("clone of '%s' into submodule path '%s' failed"),
clone_data->url, clone_data_path);
+
+ if (clone_data->require_init && !stat(clone_data_path, &st) &&
+ !dir_contains_only_dotgit(clone_data_path)) {
+ char *dot_git = xstrfmt("%s/.git", clone_data_path);
+ unlink(dot_git);
+ free(dot_git);
+ die(_("directory not empty: '%s'"), clone_data_path);
+ }
} else {
char *path;
- if (clone_data->require_init && !access(clone_data_path, X_OK) &&
+ if (clone_data->require_init && !stat(clone_data_path, &st) &&
!is_empty_dir(clone_data_path))
die(_("directory not empty: '%s'"), clone_data_path);
if (safe_create_leading_directories_const(clone_data_path) < 0)
@@ -1738,6 +1788,23 @@
free(path);
}
+ /*
+ * We already performed this check at the beginning of this function,
+ * before cloning the objects. This tries to detect racy behavior e.g.
+ * in parallel clones, where another process could easily have made the
+ * gitdir nested _after_ it was created.
+ *
+ * To prevent further harm coming from this unintentionally-nested
+ * gitdir, let's disable it by deleting the `HEAD` file.
+ */
+ if (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0) {
+ char *head = xstrfmt("%s/HEAD", sm_gitdir);
+ unlink(head);
+ free(head);
+ die(_("refusing to create/use '%s' in another submodule's "
+ "git dir"), sm_gitdir);
+ }
+
connect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0);
p = git_pathdup_submodule(clone_data_path, "config");
@@ -2513,6 +2580,9 @@
{
int ret;
+ if (validate_submodule_path(update_data->sm_path) < 0)
+ return -1;
+
ret = determine_submodule_update_strategy(the_repository,
update_data->just_cloned,
update_data->sm_path,
@@ -2620,12 +2690,21 @@
for (i = 0; i < suc.update_clone_nr; i++) {
struct update_clone_data ucd = suc.update_clone[i];
- int code;
+ int code = 128;
oidcpy(&update_data->oid, &ucd.oid);
update_data->just_cloned = ucd.just_cloned;
update_data->sm_path = ucd.sub->path;
+ /*
+ * Verify that the submodule path does not contain any
+ * symlinks; if it does, it might have been tampered with.
+ * TODO: allow exempting it via
+ * `safe.submodule.path` or something
+ */
+ if (validate_submodule_path(update_data->sm_path) < 0)
+ goto fail;
+
code = ensure_core_worktree(update_data->sm_path);
if (code)
goto fail;
@@ -3336,6 +3415,9 @@
normalize_path_copy(add_data.sm_path, add_data.sm_path);
strip_dir_trailing_slashes(add_data.sm_path);
+ if (validate_submodule_path(add_data.sm_path) < 0)
+ exit(128);
+
die_on_index_match(add_data.sm_path, force);
die_on_repo_without_commits(add_data.sm_path);
diff --git a/builtin/upload-pack.c b/builtin/upload-pack.c
index beb9dd0..edb01aa 100644
--- a/builtin/upload-pack.c
+++ b/builtin/upload-pack.c
@@ -37,6 +37,8 @@
packet_trace_identity("upload-pack");
read_replace_refs = 0;
+ /* TODO: This should use NO_LAZY_FETCH_ENVIRONMENT */
+ xsetenv("GIT_NO_LAZY_FETCH", "1", 0);
argc = parse_options(argc, argv, prefix, options, upload_pack_usage, 0);
diff --git a/ci/lib.sh b/ci/lib.sh
index db7105e..e467784 100755
--- a/ci/lib.sh
+++ b/ci/lib.sh
@@ -253,11 +253,9 @@
export PATH="$GIT_LFS_PATH:$P4_PATH:$PATH"
;;
macos-*)
- if [ "$jobname" = osx-gcc ]
+ MAKEFLAGS="$MAKEFLAGS PYTHON_PATH=$(which python3)"
+ if [ "$jobname" != osx-gcc ]
then
- MAKEFLAGS="$MAKEFLAGS PYTHON_PATH=$(which python3)"
- else
- MAKEFLAGS="$MAKEFLAGS PYTHON_PATH=$(which python2)"
MAKEFLAGS="$MAKEFLAGS APPLE_COMMON_CRYPTO_SHA1=Yes"
fi
;;
@@ -280,9 +278,13 @@
;;
linux-asan)
export SANITIZE=address
+ export NO_SVN_TESTS=LetsSaveSomeTime
+ MAKEFLAGS="$MAKEFLAGS NO_PYTHON=YepBecauseP4FlakesTooOften"
;;
linux-ubsan)
export SANITIZE=undefined
+ export NO_SVN_TESTS=LetsSaveSomeTime
+ MAKEFLAGS="$MAKEFLAGS NO_PYTHON=YepBecauseP4FlakesTooOften"
;;
esac
diff --git a/config.c b/config.c
index b79baf8..66fc088 100644
--- a/config.c
+++ b/config.c
@@ -1596,8 +1596,19 @@
if (!strcmp(var, "core.attributesfile"))
return git_config_pathname(&git_attributes_file, var, value);
- if (!strcmp(var, "core.hookspath"))
+ if (!strcmp(var, "core.hookspath")) {
+ if (current_config_scope() == CONFIG_SCOPE_LOCAL &&
+ git_env_bool("GIT_CLONE_PROTECTION_ACTIVE", 0))
+ die(_("active `core.hooksPath` found in the local "
+ "repository config:\n\t%s\nFor security "
+ "reasons, this is disallowed by default.\nIf "
+ "this is intentional and the hook should "
+ "actually be run, please\nrun the command "
+ "again with "
+ "`GIT_CLONE_PROTECTION_ACTIVE=false`"),
+ value);
return git_config_pathname(&git_hooks_path, var, value);
+ }
if (!strcmp(var, "core.bare")) {
is_bare_repository_cfg = git_config_bool(var, value);
diff --git a/copy.c b/copy.c
index 882c79c..1b4069c 100644
--- a/copy.c
+++ b/copy.c
@@ -2,6 +2,9 @@
#include "copy.h"
#include "path.h"
#include "wrapper.h"
+#include "gettext.h"
+#include "strbuf.h"
+#include "abspath.h"
int copy_fd(int ifd, int ofd)
{
@@ -68,3 +71,61 @@
return copy_times(dst, src);
return status;
}
+
+static int do_symlinks_match(const char *path1, const char *path2)
+{
+ struct strbuf buf1 = STRBUF_INIT, buf2 = STRBUF_INIT;
+ int ret = 0;
+
+ if (!strbuf_readlink(&buf1, path1, 0) &&
+ !strbuf_readlink(&buf2, path2, 0))
+ ret = !strcmp(buf1.buf, buf2.buf);
+
+ strbuf_release(&buf1);
+ strbuf_release(&buf2);
+ return ret;
+}
+
+int do_files_match(const char *path1, const char *path2)
+{
+ struct stat st1, st2;
+ int fd1 = -1, fd2 = -1, ret = 1;
+ char buf1[8192], buf2[8192];
+
+ if ((fd1 = open_nofollow(path1, O_RDONLY)) < 0 ||
+ fstat(fd1, &st1) || !S_ISREG(st1.st_mode)) {
+ if (fd1 < 0 && errno == ELOOP)
+ /* maybe this is a symbolic link? */
+ return do_symlinks_match(path1, path2);
+ ret = 0;
+ } else if ((fd2 = open_nofollow(path2, O_RDONLY)) < 0 ||
+ fstat(fd2, &st2) || !S_ISREG(st2.st_mode)) {
+ ret = 0;
+ }
+
+ if (ret)
+ /* to match, neither must be executable, or both */
+ ret = !(st1.st_mode & 0111) == !(st2.st_mode & 0111);
+
+ if (ret)
+ ret = st1.st_size == st2.st_size;
+
+ while (ret) {
+ ssize_t len1 = read_in_full(fd1, buf1, sizeof(buf1));
+ ssize_t len2 = read_in_full(fd2, buf2, sizeof(buf2));
+
+ if (len1 < 0 || len2 < 0 || len1 != len2)
+ ret = 0; /* read error or different file size */
+ else if (!len1) /* len2 is also 0; hit EOF on both */
+ break; /* ret is still true */
+ else
+ ret = !memcmp(buf1, buf2, len1);
+ }
+
+ if (fd1 >= 0)
+ close(fd1);
+ if (fd2 >= 0)
+ close(fd2);
+
+ return ret;
+}
diff --git a/copy.h b/copy.h
index 2af77cb..057259a 100644
--- a/copy.h
+++ b/copy.h
@@ -7,4 +7,18 @@
int copy_file(const char *dst, const char *src, int mode);
int copy_file_with_time(const char *dst, const char *src, int mode);
+/*
+ * Compare the file mode and contents of two given files.
+ *
+ * If both files are actually symbolic links, the function returns 1 if the link
+ * targets are identical or 0 if they are not.
+ *
+ * If any of the two files cannot be accessed or in case of read failures, this
+ * function returns 0.
+ *
+ * If the file modes and contents are identical, the function returns 1,
+ * otherwise it returns 0.
+ */
+int do_files_match(const char *path1, const char *path2);
+
#endif /* COPY_H */
diff --git a/dir.c b/dir.c
index a7469df..713282d 100644
--- a/dir.c
+++ b/dir.c
@@ -99,6 +99,18 @@
return ignore_case ? strncasecmp(a, b, count) : strncmp(a, b, count);
}
+int paths_collide(const char *a, const char *b)
+{
+ size_t len_a = strlen(a), len_b = strlen(b);
+
+ if (len_a == len_b)
+ return fspatheq(a, b);
+
+ if (len_a < len_b)
+ return is_dir_sep(b[len_a]) && !fspathncmp(a, b, len_a);
+ return is_dir_sep(a[len_b]) && !fspathncmp(a, b, len_b);
+}
+
unsigned int fspathhash(const char *str)
{
return ignore_case ? strihash(str) : strhash(str);
diff --git a/dir.h b/dir.h
index 79b85a0..0fef9a0 100644
--- a/dir.h
+++ b/dir.h
@@ -528,6 +528,13 @@
unsigned int fspathhash(const char *str);
/*
+ * Reports whether paths collide. This may be because the paths differ only in
+ * case on a case-sensitive filesystem, or that one path refers to a symlink
+ * that collides with one of the parent directories of the other.
+ */
+int paths_collide(const char *a, const char *b);
+
+/*
* The prefix part of pattern must not contains wildcards.
*/
struct pathspec_item;
diff --git a/entry.c b/entry.c
index 91a540b..1267ce2 100644
--- a/entry.c
+++ b/entry.c
@@ -460,7 +460,7 @@
continue;
if ((trust_ino && !match_stat_data(&dup->ce_stat_data, st)) ||
- (!trust_ino && !fspathcmp(ce->name, dup->name))) {
+ paths_collide(ce->name, dup->name)) {
dup->ce_flags |= CE_MATCHED;
break;
}
@@ -547,6 +547,20 @@
/* If it is a gitlink, leave it alone! */
if (S_ISGITLINK(ce->ce_mode))
return 0;
+ /*
+ * We must avoid replacing submodules' leading
+ * directories with symbolic links, lest recursive
+ * clones can write into arbitrary locations.
+ *
+ * Technically, this logic is not limited
+ * to recursive clones, or for that matter to
+ * submodules' paths colliding with symbolic links'
+ * paths. Yet it strikes a balance in favor of
+ * simplicity, and if paths are colliding, we might
+ * just as well keep the directories during a clone.
+ */
+ if (state->clone && S_ISLNK(ce->ce_mode))
+ return 0;
remove_subtree(&path);
} else if (unlink(path.buf))
return error_errno("unable to unlink old '%s'", path.buf);
diff --git a/fsck.c b/fsck.c
index 3261ef9..a731e35 100644
--- a/fsck.c
+++ b/fsck.c
@@ -639,6 +639,8 @@
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_MAILMAP_SYMLINK,
".mailmap is a symlink");
+ oidset_insert(&options->symlink_targets_found,
+ entry_oid);
}
if ((backslash = strchr(name, '\\'))) {
@@ -1272,6 +1274,56 @@
}
}
+ if (oidset_contains(&options->symlink_targets_found, oid)) {
+ const char *ptr = buf;
+ const struct object_id *reported = NULL;
+
+ oidset_insert(&options->symlink_targets_done, oid);
+
+ if (!buf || size > PATH_MAX) {
+ /*
+ * A missing buffer here is a sign that the caller found the
+ * blob too gigantic to load into memory. Let's just consider
+ * that an error.
+ */
+ return report(options, oid, OBJ_BLOB,
+ FSCK_MSG_SYMLINK_TARGET_LENGTH,
+ "symlink target too long");
+ }
+
+ while (!reported && ptr) {
+ const char *p = ptr;
+ char c, *slash = strchrnul(ptr, '/');
+ char *backslash = memchr(ptr, '\\', slash - ptr);
+
+ c = *slash;
+ *slash = '\0';
+
+ while (!reported && backslash) {
+ *backslash = '\0';
+ if (is_ntfs_dotgit(p))
+ ret |= report(options, reported = oid, OBJ_BLOB,
+ FSCK_MSG_SYMLINK_POINTS_TO_GIT_DIR,
+ "symlink target points to git dir");
+ *backslash = '\\';
+ p = backslash + 1;
+ backslash = memchr(p, '\\', slash - p);
+ }
+ if (!reported && is_ntfs_dotgit(p))
+ ret |= report(options, reported = oid, OBJ_BLOB,
+ FSCK_MSG_SYMLINK_POINTS_TO_GIT_DIR,
+ "symlink target points to git dir");
+
+ if (!reported && is_hfs_dotgit(ptr))
+ ret |= report(options, reported = oid, OBJ_BLOB,
+ FSCK_MSG_SYMLINK_POINTS_TO_GIT_DIR,
+ "symlink target points to git dir");
+
+ *slash = c;
+ ptr = c ? slash + 1 : NULL;
+ }
+ }
+
return ret;
}
@@ -1370,6 +1422,10 @@
FSCK_MSG_GITATTRIBUTES_MISSING, FSCK_MSG_GITATTRIBUTES_BLOB,
options, ".gitattributes");
+ ret |= fsck_blobs(&options->symlink_targets_found, &options->symlink_targets_done,
+ FSCK_MSG_SYMLINK_TARGET_MISSING, FSCK_MSG_SYMLINK_TARGET_BLOB,
+ options, "<symlink-target>");
+
return ret;
}
diff --git a/fsck.h b/fsck.h
index e17730e..6071117 100644
--- a/fsck.h
+++ b/fsck.h
@@ -64,6 +64,8 @@
FUNC(GITATTRIBUTES_LARGE, ERROR) \
FUNC(GITATTRIBUTES_LINE_LENGTH, ERROR) \
FUNC(GITATTRIBUTES_BLOB, ERROR) \
+ FUNC(SYMLINK_TARGET_MISSING, ERROR) \
+ FUNC(SYMLINK_TARGET_BLOB, ERROR) \
/* warnings */ \
FUNC(EMPTY_NAME, WARN) \
FUNC(FULL_PATHNAME, WARN) \
@@ -73,6 +75,8 @@
FUNC(NULL_SHA1, WARN) \
FUNC(ZERO_PADDED_FILEMODE, WARN) \
FUNC(NUL_IN_COMMIT, WARN) \
+ FUNC(SYMLINK_TARGET_LENGTH, WARN) \
+ FUNC(SYMLINK_POINTS_TO_GIT_DIR, WARN) \
/* infos (reported as warnings, but ignored by default) */ \
FUNC(BAD_FILEMODE, INFO) \
FUNC(GITMODULES_PARSE, INFO) \
@@ -140,6 +144,8 @@
struct oidset gitmodules_done;
struct oidset gitattributes_found;
struct oidset gitattributes_done;
+ struct oidset symlink_targets_found;
+ struct oidset symlink_targets_done;
kh_oid_map_t *object_names;
};
@@ -149,6 +155,8 @@
.gitmodules_done = OIDSET_INIT, \
.gitattributes_found = OIDSET_INIT, \
.gitattributes_done = OIDSET_INIT, \
+ .symlink_targets_found = OIDSET_INIT, \
+ .symlink_targets_done = OIDSET_INIT, \
.error_func = fsck_error_function \
}
#define FSCK_OPTIONS_STRICT { \
@@ -157,6 +165,8 @@
.gitmodules_done = OIDSET_INIT, \
.gitattributes_found = OIDSET_INIT, \
.gitattributes_done = OIDSET_INIT, \
+ .symlink_targets_found = OIDSET_INIT, \
+ .symlink_targets_done = OIDSET_INIT, \
.error_func = fsck_error_function, \
}
#define FSCK_OPTIONS_MISSING_GITMODULES { \
@@ -165,6 +175,8 @@
.gitmodules_done = OIDSET_INIT, \
.gitattributes_found = OIDSET_INIT, \
.gitattributes_done = OIDSET_INIT, \
+ .symlink_targets_found = OIDSET_INIT, \
+ .symlink_targets_done = OIDSET_INIT, \
.error_func = fsck_error_cb_print_missing_gitmodules, \
}
diff --git a/git-curl-compat.h b/git-curl-compat.h
index fd96b3c..e1d0bdd 100644
--- a/git-curl-compat.h
+++ b/git-curl-compat.h
@@ -127,6 +127,15 @@
#endif
/**
+ * Versions before curl 7.66.0 (September 2019) required manually setting the
+ * transfer-encoding for a streaming POST; after that this is handled
+ * automatically.
+ */
+#if LIBCURL_VERSION_NUM < 0x074200
+#define GIT_CURL_NEED_TRANSFER_ENCODING_HEADER
+#endif
+
+/**
* CURLOPT_PROTOCOLS_STR and CURLOPT_REDIR_PROTOCOLS_STR were added in 7.85.0,
* released in August 2022.
*/
diff --git a/hook.c b/hook.c
index 3ca5e60..22a976c 100644
--- a/hook.c
+++ b/hook.c
@@ -6,25 +6,56 @@
#include "run-command.h"
#include "config.h"
#include "strbuf.h"
+#include "environment.h"
+#include "setup.h"
+#include "copy.h"
+
+static int identical_to_template_hook(const char *name, const char *path)
+{
+ const char *env = getenv("GIT_CLONE_TEMPLATE_DIR");
+ const char *template_dir = get_template_dir(env && *env ? env : NULL);
+ struct strbuf template_path = STRBUF_INIT;
+ int found_template_hook, ret;
+
+ strbuf_addf(&template_path, "%s/hooks/%s", template_dir, name);
+ found_template_hook = access(template_path.buf, X_OK) >= 0;
+#ifdef STRIP_EXTENSION
+ if (!found_template_hook) {
+ strbuf_addstr(&template_path, STRIP_EXTENSION);
+ found_template_hook = access(template_path.buf, X_OK) >= 0;
+ }
+#endif
+ if (!found_template_hook)
+ return 0;
+
+ ret = do_files_match(template_path.buf, path);
+
+ strbuf_release(&template_path);
+ return ret;
+}
const char *find_hook(const char *name)
{
static struct strbuf path = STRBUF_INIT;
+ int found_hook;
+
strbuf_reset(&path);
strbuf_git_path(&path, "hooks/%s", name);
- if (access(path.buf, X_OK) < 0) {
+ found_hook = access(path.buf, X_OK) >= 0;
+#ifdef STRIP_EXTENSION
+ if (!found_hook) {
int err = errno;
-#ifdef STRIP_EXTENSION
strbuf_addstr(&path, STRIP_EXTENSION);
- if (access(path.buf, X_OK) >= 0)
- return path.buf;
- if (errno == EACCES)
- err = errno;
+ found_hook = access(path.buf, X_OK) >= 0;
+ if (!found_hook)
+ errno = err;
+ }
#endif
- if (err == EACCES && advice_enabled(ADVICE_IGNORED_HOOK)) {
+ if (!found_hook) {
+ if (errno == EACCES && advice_enabled(ADVICE_IGNORED_HOOK)) {
static struct string_list advise_given = STRING_LIST_INIT_DUP;
if (!string_list_lookup(&advise_given, name)) {
@@ -38,6 +69,14 @@
}
return NULL;
}
+ if (!git_hooks_path && git_env_bool("GIT_CLONE_PROTECTION_ACTIVE", 0) &&
+ !identical_to_template_hook(name, path.buf))
+ die(_("active `%s` hook found during `git clone`:\n\t%s\n"
+ "For security reasons, this is disallowed by default.\n"
+ "If this is intentional and the hook should actually "
+ "be run, please\nrun the command again with "
+ "`GIT_CLONE_PROTECTION_ACTIVE=false`"),
+ name, path.buf);
return path.buf;
}
diff --git a/http.c b/http.c
index b71bb1e..9ee9b19 100644
--- a/http.c
+++ b/http.c
@@ -736,18 +736,43 @@
return ret;
}
+static int match_curl_h2_trace(const char *line, const char **out)
+{
+ const char *p;
+
+ /*
+ * curl prior to 8.1.0 gives us:
+ *
+ * h2h3 [<header-name>: <header-val>]
+ *
+ * Starting in 8.1.0, the first token became just "h2".
+ */
+ if (skip_iprefix(line, "h2h3 [", out) ||
+ skip_iprefix(line, "h2 [", out))
+ return 1;
+
+ /*
+ * curl 8.3.0 uses:
+ * [HTTP/2] [<stream-id>] [<header-name>: <header-val>]
+ * where <stream-id> is numeric.
+ */
+ if (skip_iprefix(line, "[HTTP/2] [", &p)) {
+ while (isdigit(*p))
+ p++;
+ if (skip_prefix(p, "] [", out))
+ return 1;
+ }
+
+ return 0;
+}
+
/* Redact headers in info */
static void redact_sensitive_info_header(struct strbuf *header)
{
const char *sensitive_header;
- /*
- * curl's h2h3 prints headers in info, e.g.:
- * h2h3 [<header-name>: <header-val>]
- */
if (trace_curl_redact &&
- (skip_iprefix(header->buf, "h2h3 [", &sensitive_header) ||
- skip_iprefix(header->buf, "h2 [", &sensitive_header))) {
+ match_curl_h2_trace(header->buf, &sensitive_header)) {
if (redact_sensitive_header(header, sensitive_header - header->buf)) {
/* redaction ate our closing bracket */
strbuf_addch(header, ']');
@@ -1425,6 +1450,7 @@
curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, NULL);
curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, NULL);
curl_easy_setopt(slot->curl, CURLOPT_POSTFIELDS, NULL);
+ curl_easy_setopt(slot->curl, CURLOPT_POSTFIELDSIZE, -1L);
curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 0);
curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1);
curl_easy_setopt(slot->curl, CURLOPT_FAILONERROR, 1);
diff --git a/path.c b/path.c
index 7c1cd81..4330315 100644
--- a/path.c
+++ b/path.c
@@ -847,6 +847,7 @@
if (!suffix[i])
return NULL;
gitfile = read_gitfile(used_path.buf);
+ die_upon_dubious_ownership(gitfile, NULL, used_path.buf);
if (gitfile) {
strbuf_reset(&used_path);
strbuf_addstr(&used_path, gitfile);
@@ -857,6 +858,7 @@
}
else {
const char *gitfile = read_gitfile(path);
+ die_upon_dubious_ownership(gitfile, NULL, path);
if (gitfile)
path = gitfile;
if (chdir(path))
diff --git a/promisor-remote.c b/promisor-remote.c
index 1adcd6f..9f0441e 100644
--- a/promisor-remote.c
+++ b/promisor-remote.c
@@ -23,6 +23,16 @@
int i;
FILE *child_in;
+ /* TODO: This should use NO_LAZY_FETCH_ENVIRONMENT */
+ if (git_env_bool("GIT_NO_LAZY_FETCH", 0)) {
+ static int warning_shown;
+ if (!warning_shown) {
+ warning_shown = 1;
+ warning(_("lazy fetching disabled; some objects may not be available"));
+ }
+ return -1;
+ }
+
child.git_cmd = 1;
child.in = -1;
if (repo != the_repository)
diff --git a/read-cache.c b/read-cache.c
index f4c31a6..e094008 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -1142,19 +1142,32 @@
istate->cache[istate->cache_nr - 1]->name,
&len_eq_last);
if (cmp_last > 0) {
- if (len_eq_last == 0) {
+ if (name[len_eq_last] != '/') {
/*
* The entry sorts AFTER the last one in the
- * index and their paths have no common prefix,
- * so there cannot be a F/D conflict.
+ * index.
+ *
+ * If there were a conflict with "file", then our
+ * name would start with "file/" and the last index
+ * entry would start with "file" but not "file/".
+ *
+ * The next character after common prefix is
+ * not '/', so there can be no conflict.
*/
return retval;
} else {
/*
* The entry sorts AFTER the last one in the
- * index, but has a common prefix. Fall through
- * to the loop below to disect the entry's path
- * and see where the difference is.
+ * index, and the next character after common
+ * prefix is '/'.
+ *
+ * Either the last index entry is a file in
+ * conflict with this entry, or it has a name
+ * which sorts between this entry and the
+ * potential conflicting file.
+ *
+ * In both cases, we fall through to the loop
+ * below and let the regular search code handle it.
*/
}
} else if (cmp_last == 0) {
@@ -1178,53 +1191,6 @@
}
len = slash - name;
- if (cmp_last > 0) {
- /*
- * (len + 1) is a directory boundary (including
- * the trailing slash). And since the loop is
- * decrementing "slash", the first iteration is
- * the longest directory prefix; subsequent
- * iterations consider parent directories.
- */
-
- if (len + 1 <= len_eq_last) {
- /*
- * The directory prefix (including the trailing
- * slash) also appears as a prefix in the last
- * entry, so the remainder cannot collide (because
- * strcmp said the whole path was greater).
- *
- * EQ: last: xxx/A
- * this: xxx/B
- *
- * LT: last: xxx/file_A
- * this: xxx/file_B
- */
- return retval;
- }
-
- if (len > len_eq_last) {
- /*
- * This part of the directory prefix (excluding
- * the trailing slash) is longer than the known
- * equal portions, so this sub-directory cannot
- * collide with a file.
- *
- * GT: last: xxxA
- * this: xxxB/file
- */
- return retval;
- }
-
- /*
- * This is a possible collision. Fall through and
- * let the regular search code handle it.
- *
- * last: xxx
- * this: xxx/file
- */
- }
-
pos = index_name_stage_pos(istate, name, len, stage, EXPAND_SPARSE);
if (pos >= 0) {
/*
diff --git a/remote-curl.c b/remote-curl.c
index acf7b2b..a0777e3 100644
--- a/remote-curl.c
+++ b/remote-curl.c
@@ -1,4 +1,5 @@
#include "git-compat-util.h"
+#include "git-curl-compat.h"
#include "alloc.h"
#include "config.h"
#include "environment.h"
@@ -961,7 +962,9 @@
/* The request body is large and the size cannot be predicted.
* We must use chunked encoding to send it.
*/
+#ifdef GIT_CURL_NEED_TRANSFER_ENCODING_HEADER
headers = curl_slist_append(headers, "Transfer-Encoding: chunked");
+#endif
rpc->initial_buffer = 1;
curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, rpc_out);
curl_easy_setopt(slot->curl, CURLOPT_INFILE, rpc);
diff --git a/repository.c b/repository.c
index c53e480..a9d86c8 100644
--- a/repository.c
+++ b/repository.c
@@ -274,6 +274,8 @@
parsed_object_pool_clear(repo->parsed_objects);
FREE_AND_NULL(repo->parsed_objects);
+ FREE_AND_NULL(repo->settings.fsmonitor);
+
if (repo->config) {
git_configset_clear(repo->config);
FREE_AND_NULL(repo->config);
diff --git a/setup.c b/setup.c
index 4585822..84324e3 100644
--- a/setup.c
+++ b/setup.c
@@ -13,6 +13,7 @@
#include "quote.h"
#include "trace2.h"
#include "wrapper.h"
+#include "exec-cmd.h"
static int inside_git_dir = -1;
static int inside_work_tree = -1;
@@ -1172,6 +1173,27 @@
return data.is_safe;
}
+void die_upon_dubious_ownership(const char *gitfile, const char *worktree,
+ const char *gitdir)
+{
+ struct strbuf report = STRBUF_INIT, quoted = STRBUF_INIT;
+ const char *path;
+
+ if (ensure_valid_ownership(gitfile, worktree, gitdir, &report))
+ return;
+
+ strbuf_complete(&report, '\n');
+ path = gitfile ? gitfile : gitdir;
+ sq_quote_buf_pretty("ed, path);
+
+ die(_("detected dubious ownership in repository at '%s'\n"
+ "%s"
+ "To add an exception for this directory, call:\n"
+ "\n"
+ "\tgit config --global --add safe.directory %s"),
+ path, report.buf, quoted.buf);
+}
+
static int allowed_bare_repo_cb(const char *key, const char *value, void *d)
{
enum allowed_bare_repo *allowed_bare_repo = d;
@@ -1707,3 +1729,57 @@
return 0;
#endif
}
+
+#ifndef DEFAULT_GIT_TEMPLATE_DIR
+#define DEFAULT_GIT_TEMPLATE_DIR "/usr/share/git-core/templates"
+#endif
+
+struct template_dir_cb_data {
+ char *path;
+ int initialized;
+};
+
+static int template_dir_cb(const char *key, const char *value, void *d)
+{
+ struct template_dir_cb_data *data = d;
+
+ if (strcmp(key, "init.templatedir"))
+ return 0;
+
+ if (!value) {
+ data->path = NULL;
+ } else {
+ char *path = NULL;
+
+ FREE_AND_NULL(data->path);
+ if (!git_config_pathname((const char **)&path, key, value))
+ data->path = path ? path : xstrdup(value);
+ }
+
+ return 0;
+}
+
+const char *get_template_dir(const char *option_template)
+{
+ const char *template_dir = option_template;
+
+ if (!template_dir)
+ template_dir = getenv(TEMPLATE_DIR_ENVIRONMENT);
+ if (!template_dir) {
+ static struct template_dir_cb_data data;
+
+ if (!data.initialized) {
+ git_protected_config(template_dir_cb, &data);
+ data.initialized = 1;
+ }
+ template_dir = data.path;
+ }
+ if (!template_dir) {
+ static char *dir;
+
+ if (!dir)
+ dir = system_path(DEFAULT_GIT_TEMPLATE_DIR);
+ template_dir = dir;
+ }
+ return template_dir;
+}
diff --git a/setup.h b/setup.h
index 4c1ca9d..e7708f5 100644
--- a/setup.h
+++ b/setup.h
@@ -41,6 +41,18 @@
const char *resolve_gitdir_gently(const char *suspect, int *return_error_code);
#define resolve_gitdir(path) resolve_gitdir_gently((path), NULL)
+/*
+ * Check if a repository is safe and die if it is not, by verifying the
+ * ownership of the worktree (if any), the git directory, and the gitfile (if
+ * any).
+ *
+ * Exemptions for known-safe repositories can be added via `safe.directory`
+ * config settings; for non-bare repositories, their worktree needs to be
+ * added, for bare ones their git directory.
+ */
+void die_upon_dubious_ownership(const char *gitfile, const char *worktree,
+ const char *gitdir);
+
void setup_work_tree(void);
/*
* Find the commondir and gitdir of the repository that contains the current
@@ -140,6 +152,8 @@
*/
void check_repository_format(struct repository_format *fmt);
+const char *get_template_dir(const char *option_template);
+
/*
* NOTE NOTE NOTE!!
*
diff --git a/submodule.c b/submodule.c
index 2e78f51..c483fc2 100644
--- a/submodule.c
+++ b/submodule.c
@@ -1012,6 +1012,9 @@
.super_oid = super_oid
};
+ if (validate_submodule_path(path) < 0)
+ exit(128);
+
oid_array_for_each_unique(commits, check_has_commit, &has_commit);
if (has_commit.result) {
@@ -1134,6 +1137,9 @@
const struct string_list *push_options,
int dry_run)
{
+ if (validate_submodule_path(path) < 0)
+ exit(128);
+
if (for_each_remote_ref_submodule(path, has_remote, NULL) > 0) {
struct child_process cp = CHILD_PROCESS_INIT;
strvec_push(&cp.args, "push");
@@ -1183,6 +1189,9 @@
struct child_process cp = CHILD_PROCESS_INIT;
int i;
+ if (validate_submodule_path(path) < 0)
+ exit(128);
+
strvec_push(&cp.args, "submodule--helper");
strvec_push(&cp.args, "push-check");
strvec_push(&cp.args, head);
@@ -1514,6 +1523,9 @@
struct fetch_task *task = xmalloc(sizeof(*task));
memset(task, 0, sizeof(*task));
+ if (validate_submodule_path(path) < 0)
+ exit(128);
+
task->sub = submodule_from_path(spf->r, treeish_name, path);
if (!task->sub) {
@@ -1886,6 +1898,9 @@
const char *git_dir;
int ignore_cp_exit_code = 0;
+ if (validate_submodule_path(path) < 0)
+ exit(128);
+
strbuf_addf(&buf, "%s/.git", path);
git_dir = read_gitfile(buf.buf);
if (!git_dir)
@@ -1962,6 +1977,9 @@
struct strbuf buf = STRBUF_INIT;
const char *git_dir;
+ if (validate_submodule_path(path) < 0)
+ exit(128);
+
strbuf_addf(&buf, "%s/.git", path);
git_dir = read_gitfile(buf.buf);
if (!git_dir) {
@@ -2001,6 +2019,9 @@
struct strbuf buf = STRBUF_INIT;
int ret = 0;
+ if (validate_submodule_path(path) < 0)
+ exit(128);
+
if (!file_exists(path) || is_empty_dir(path))
return 0;
@@ -2051,6 +2072,9 @@
{
struct strbuf config_path = STRBUF_INIT;
+ if (validate_submodule_path(sub->path) < 0)
+ exit(128);
+
submodule_name_to_gitdir(&config_path, the_repository, sub->name);
strbuf_addstr(&config_path, "/config");
@@ -2065,6 +2089,9 @@
{
struct child_process cp = CHILD_PROCESS_INIT;
+ if (validate_submodule_path(sub->path) < 0)
+ exit(128);
+
prepare_submodule_repo_env(&cp.env);
cp.git_cmd = 1;
@@ -2082,6 +2109,10 @@
static void submodule_reset_index(const char *path, const char *super_prefix)
{
struct child_process cp = CHILD_PROCESS_INIT;
+
+ if (validate_submodule_path(path) < 0)
+ exit(128);
+
prepare_submodule_repo_env(&cp.env);
cp.git_cmd = 1;
@@ -2145,10 +2176,27 @@
if (!submodule_uses_gitfile(path))
absorb_git_dir_into_superproject(path,
super_prefix);
+ else {
+ char *dotgit = xstrfmt("%s/.git", path);
+ char *git_dir = xstrdup(read_gitfile(dotgit));
+
+ free(dotgit);
+ if (validate_submodule_git_dir(git_dir,
+ sub->name) < 0)
+ die(_("refusing to create/use '%s' in "
+ "another submodule's git dir"),
+ git_dir);
+ free(git_dir);
+ }
} else {
struct strbuf gitdir = STRBUF_INIT;
submodule_name_to_gitdir(&gitdir, the_repository,
sub->name);
+ if (validate_submodule_git_dir(gitdir.buf,
+ sub->name) < 0)
+ die(_("refusing to create/use '%s' in another "
+ "submodule's git dir"),
+ gitdir.buf);
connect_work_tree_and_git_dir(path, gitdir.buf, 0);
strbuf_release(&gitdir);
@@ -2269,6 +2317,34 @@
return 0;
}
+int validate_submodule_path(const char *path)
+{
+ char *p = xstrdup(path);
+ struct stat st;
+ int i, ret = 0;
+ char sep;
+
+ for (i = 0; !ret && p[i]; i++) {
+ if (!is_dir_sep(p[i]))
+ continue;
+
+ sep = p[i];
+ p[i] = '\0';
+ /* allow missing components, but no symlinks */
+ ret = lstat(p, &st) || !S_ISLNK(st.st_mode) ? 0 : -1;
+ p[i] = sep;
+ if (ret)
+ error(_("expected '%.*s' in submodule path '%s' not to "
+ "be a symbolic link"), i, p, p);
+ }
+ if (!lstat(p, &st) && S_ISLNK(st.st_mode))
+ ret = error(_("expected submodule path '%s' not to be a "
+ "symbolic link"), p);
+ free(p);
+ return ret;
+}
+
+
/*
* Embeds a single submodules git directory into the superprojects git dir,
* non recursively.
@@ -2280,6 +2356,9 @@
struct strbuf new_gitdir = STRBUF_INIT;
const struct submodule *sub;
+ if (validate_submodule_path(path) < 0)
+ exit(128);
+
if (submodule_uses_worktrees(path))
die(_("relocate_gitdir for submodule '%s' with "
"more than one worktree not supported"), path);
@@ -2321,6 +2400,9 @@
struct child_process cp = CHILD_PROCESS_INIT;
+ if (validate_submodule_path(path) < 0)
+ exit(128);
+
cp.dir = path;
cp.git_cmd = 1;
cp.no_stdin = 1;
@@ -2345,6 +2427,10 @@
int err_code;
const char *sub_git_dir;
struct strbuf gitdir = STRBUF_INIT;
+
+ if (validate_submodule_path(path) < 0)
+ exit(128);
+
strbuf_addf(&gitdir, "%s/.git", path);
sub_git_dir = resolve_gitdir_gently(gitdir.buf, &err_code);
@@ -2487,6 +2573,9 @@
const char *git_dir;
int ret = 0;
+ if (validate_submodule_path(submodule) < 0)
+ exit(128);
+
strbuf_reset(buf);
strbuf_addstr(buf, submodule);
strbuf_complete(buf, '/');
diff --git a/submodule.h b/submodule.h
index c55a25c..b50d29e 100644
--- a/submodule.h
+++ b/submodule.h
@@ -148,6 +148,11 @@
*/
int validate_submodule_git_dir(char *git_dir, const char *submodule_name);
+/*
+ * Make sure that the given submodule path does not follow symlinks.
+ */
+int validate_submodule_path(const char *path);
+
#define SUBMODULE_MOVE_HEAD_DRY_RUN (1<<0)
#define SUBMODULE_MOVE_HEAD_FORCE (1<<1)
int submodule_move_head(const char *path, const char *super_prefix,
diff --git a/t/helper/test-path-utils.c b/t/helper/test-path-utils.c
index 2ef53d5..164b6a6 100644
--- a/t/helper/test-path-utils.c
+++ b/t/helper/test-path-utils.c
@@ -7,6 +7,7 @@
#include "string-list.h"
#include "trace.h"
#include "utf8.h"
+#include "copy.h"
/*
* A "string_list_each_func_t" function that normalizes an entry from
@@ -500,6 +501,16 @@
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;
diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh
index 8ea31d1..402b201 100755
--- a/t/t0000-basic.sh
+++ b/t/t0000-basic.sh
@@ -1201,6 +1201,34 @@
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
'
diff --git a/t/t0033-safe-directory.sh b/t/t0033-safe-directory.sh
index dc34968..11c3e8f 100755
--- a/t/t0033-safe-directory.sh
+++ b/t/t0033-safe-directory.sh
@@ -80,4 +80,28 @@
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
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index 0afa3d0..85686ee 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -610,4 +610,45 @@
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
diff --git a/t/t0411-clone-from-partial.sh b/t/t0411-clone-from-partial.sh
new file mode 100755
index 0000000..b3d6ddc
--- /dev/null
+++ b/t/t0411-clone-from-partial.sh
@@ -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
diff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh
index 8c442ad..21d65b2 100755
--- a/t/t1450-fsck.sh
+++ b/t/t1450-fsck.sh
@@ -1050,4 +1050,41 @@
test_cmp expect 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
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index 3506f62..0f0c706 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -195,4 +195,19 @@
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
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 4f28906..4bb9f87 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -1204,6 +1204,30 @@
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
diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh
index b7d5551..1bcf652 100755
--- a/t/t5601-clone.sh
+++ b/t/t5601-clone.sh
@@ -633,6 +633,21 @@
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 @@
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
diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh
index eae6a46..3e8cf9b 100755
--- a/t/t7400-submodule-basic.sh
+++ b/t/t7400-submodule-basic.sh
@@ -1436,4 +1436,35 @@
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
diff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh
index f094e3d..dae8709 100755
--- a/t/t7406-submodule-update.sh
+++ b/t/t7406-submodule-update.sh
@@ -1179,4 +1179,52 @@
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
diff --git a/t/t7423-submodule-symlinks.sh b/t/t7423-submodule-symlinks.sh
new file mode 100755
index 0000000..3d3c7af
--- /dev/null
+++ b/t/t7423-submodule-symlinks.sh
@@ -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
diff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh
index 0d0c3f2..60d6275 100755
--- a/t/t7450-bad-git-dotfiles.sh
+++ b/t/t7450-bad-git-dotfiles.sh
@@ -294,7 +294,7 @@
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 @@
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
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 293caf0..5ea5d1d 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -334,6 +334,7 @@
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
}