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(&quoted, 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
 }