aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/main.yml3
-rw-r--r--Documentation/RelNotes/2.39.4.txt79
-rw-r--r--Documentation/RelNotes/2.39.5.txt26
-rw-r--r--Documentation/RelNotes/2.40.2.txt7
-rw-r--r--Documentation/RelNotes/2.40.3.txt26
-rw-r--r--Documentation/RelNotes/2.41.1.txt7
-rw-r--r--Documentation/RelNotes/2.41.2.txt26
-rw-r--r--Documentation/RelNotes/2.42.2.txt7
-rw-r--r--Documentation/RelNotes/2.42.3.txt26
-rw-r--r--Documentation/RelNotes/2.43.4.txt7
-rw-r--r--Documentation/RelNotes/2.43.5.txt26
-rw-r--r--Documentation/RelNotes/2.44.1.txt8
-rw-r--r--Documentation/RelNotes/2.44.2.txt26
-rw-r--r--Documentation/RelNotes/2.45.1.txt8
-rw-r--r--Documentation/RelNotes/2.45.2.txt26
-rw-r--r--Documentation/SubmittingPatches4
-rw-r--r--Documentation/git-gui.txt2
-rw-r--r--Documentation/git-upload-pack.txt31
-rw-r--r--Documentation/git.txt31
-rwxr-xr-xGIT-VERSION-GEN2
l---------RelNotes2
-rw-r--r--builtin/clone.c40
-rw-r--r--builtin/submodule--helper.c86
-rw-r--r--builtin/upload-pack.c2
-rwxr-xr-xci/install-dependencies.sh2
-rw-r--r--copy.c3
-rw-r--r--dir.c12
-rw-r--r--dir.h7
-rw-r--r--entry.c16
-rw-r--r--hook.c20
-rw-r--r--path.c2
-rw-r--r--promisor-remote.c10
-rw-r--r--read-cache.c72
-rw-r--r--repository.c2
-rw-r--r--setup.c91
-rw-r--r--setup.h14
-rw-r--r--submodule.c89
-rw-r--r--submodule.h5
-rw-r--r--t/helper/test-path-utils.c1
-rw-r--r--t/lib-chunk.sh3
-rwxr-xr-xt/t0000-basic.sh28
-rwxr-xr-xt/t0033-safe-directory.sh24
-rwxr-xr-xt/t0411-clone-from-partial.sh78
-rwxr-xr-xt/t1350-config-hooks-path.sh7
-rwxr-xr-xt/t5510-fetch.sh24
-rwxr-xr-xt/t5601-clone.sh15
-rwxr-xr-xt/t7400-submodule-basic.sh31
-rwxr-xr-xt/t7406-submodule-update.sh48
-rwxr-xr-xt/t7423-submodule-symlinks.sh67
-rwxr-xr-xt/t7450-bad-git-dotfiles.sh34
50 files changed, 1119 insertions, 94 deletions
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 3428773b09..8922bd73bd 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -284,8 +284,7 @@ jobs:
cc: clang
pool: macos-13
- jobname: osx-gcc
- cc: gcc
- cc_package: gcc-13
+ cc: gcc-13
pool: macos-13
- jobname: linux-gcc-default
cc: gcc
diff --git a/Documentation/RelNotes/2.39.4.txt b/Documentation/RelNotes/2.39.4.txt
new file mode 100644
index 0000000000..7f54521fea
--- /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.39.5.txt b/Documentation/RelNotes/2.39.5.txt
new file mode 100644
index 0000000000..97c0185de4
--- /dev/null
+++ b/Documentation/RelNotes/2.39.5.txt
@@ -0,0 +1,26 @@
+Git v2.39.5 Release Notes
+=========================
+
+In preparing security fixes for four CVEs, we made overly aggressive
+"defense in depth" changes that broke legitimate use cases like 'git
+lfs' and 'git annex.' This release is to revert these misguided, if
+well-intentioned, changes that were shipped in 2.39.4 and were not
+direct security fixes.
+
+Jeff King (5):
+ send-email: drop FakeTerm hack
+ send-email: avoid creating more than one Term::ReadLine object
+ ci: drop mention of BREW_INSTALL_PACKAGES variable
+ ci: avoid bare "gcc" for osx-gcc job
+ ci: stop installing "gcc-13" for osx-gcc
+
+Johannes Schindelin (6):
+ hook: plug a new memory leak
+ init: use the correct path of the templates directory again
+ Revert "core.hooksPath: add some protection while cloning"
+ tests: verify that `clone -c core.hooksPath=/dev/null` works again
+ clone: drop the protections where hooks aren't run
+ Revert "Add a helper function to compare file contents"
+
+Junio C Hamano (1):
+ Revert "fsck: warn about symlink pointing inside a gitdir"
diff --git a/Documentation/RelNotes/2.40.2.txt b/Documentation/RelNotes/2.40.2.txt
new file mode 100644
index 0000000000..646a2cc3eb
--- /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/RelNotes/2.40.3.txt b/Documentation/RelNotes/2.40.3.txt
new file mode 100644
index 0000000000..6ca088ec86
--- /dev/null
+++ b/Documentation/RelNotes/2.40.3.txt
@@ -0,0 +1,26 @@
+Git v2.40.3 Release Notes
+=========================
+
+In preparing security fixes for four CVEs, we made overly aggressive
+"defense in depth" changes that broke legitimate use cases like 'git
+lfs' and 'git annex.' This release is to revert these misguided, if
+well-intentioned, changes that were shipped in 2.40.2 and were not
+direct security fixes.
+
+Jeff King (5):
+ send-email: drop FakeTerm hack
+ send-email: avoid creating more than one Term::ReadLine object
+ ci: drop mention of BREW_INSTALL_PACKAGES variable
+ ci: avoid bare "gcc" for osx-gcc job
+ ci: stop installing "gcc-13" for osx-gcc
+
+Johannes Schindelin (6):
+ hook: plug a new memory leak
+ init: use the correct path of the templates directory again
+ Revert "core.hooksPath: add some protection while cloning"
+ tests: verify that `clone -c core.hooksPath=/dev/null` works again
+ clone: drop the protections where hooks aren't run
+ Revert "Add a helper function to compare file contents"
+
+Junio C Hamano (1):
+ Revert "fsck: warn about symlink pointing inside a gitdir"
diff --git a/Documentation/RelNotes/2.41.1.txt b/Documentation/RelNotes/2.41.1.txt
new file mode 100644
index 0000000000..9fb4c218b2
--- /dev/null
+++ b/Documentation/RelNotes/2.41.1.txt
@@ -0,0 +1,7 @@
+Git v2.41.1 Release Notes
+=========================
+
+This release merges up the fix that appears in v2.39.4 and v2.40.2
+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 these versions for details.
diff --git a/Documentation/RelNotes/2.41.2.txt b/Documentation/RelNotes/2.41.2.txt
new file mode 100644
index 0000000000..f94afde8c2
--- /dev/null
+++ b/Documentation/RelNotes/2.41.2.txt
@@ -0,0 +1,26 @@
+Git v2.41.2 Release Notes
+=========================
+
+In preparing security fixes for four CVEs, we made overly aggressive
+"defense in depth" changes that broke legitimate use cases like 'git
+lfs' and 'git annex.' This release is to revert these misguided, if
+well-intentioned, changes that were shipped in 2.41.1 and were not
+direct security fixes.
+
+Jeff King (5):
+ send-email: drop FakeTerm hack
+ send-email: avoid creating more than one Term::ReadLine object
+ ci: drop mention of BREW_INSTALL_PACKAGES variable
+ ci: avoid bare "gcc" for osx-gcc job
+ ci: stop installing "gcc-13" for osx-gcc
+
+Johannes Schindelin (6):
+ hook: plug a new memory leak
+ init: use the correct path of the templates directory again
+ Revert "core.hooksPath: add some protection while cloning"
+ tests: verify that `clone -c core.hooksPath=/dev/null` works again
+ clone: drop the protections where hooks aren't run
+ Revert "Add a helper function to compare file contents"
+
+Junio C Hamano (1):
+ Revert "fsck: warn about symlink pointing inside a gitdir"
diff --git a/Documentation/RelNotes/2.42.2.txt b/Documentation/RelNotes/2.42.2.txt
new file mode 100644
index 0000000000..dbf761a01d
--- /dev/null
+++ b/Documentation/RelNotes/2.42.2.txt
@@ -0,0 +1,7 @@
+Git v2.42.2 Release Notes
+=========================
+
+This release merges up the fix that appears in v2.39.4, v2.40.2
+and v2.41.1 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 these versions for details.
diff --git a/Documentation/RelNotes/2.42.3.txt b/Documentation/RelNotes/2.42.3.txt
new file mode 100644
index 0000000000..bfe3ba5629
--- /dev/null
+++ b/Documentation/RelNotes/2.42.3.txt
@@ -0,0 +1,26 @@
+Git v2.42.3 Release Notes
+=========================
+
+In preparing security fixes for four CVEs, we made overly aggressive
+"defense in depth" changes that broke legitimate use cases like 'git
+lfs' and 'git annex.' This release is to revert these misguided, if
+well-intentioned, changes that were shipped in 2.42.2 and were not
+direct security fixes.
+
+Jeff King (5):
+ send-email: drop FakeTerm hack
+ send-email: avoid creating more than one Term::ReadLine object
+ ci: drop mention of BREW_INSTALL_PACKAGES variable
+ ci: avoid bare "gcc" for osx-gcc job
+ ci: stop installing "gcc-13" for osx-gcc
+
+Johannes Schindelin (6):
+ hook: plug a new memory leak
+ init: use the correct path of the templates directory again
+ Revert "core.hooksPath: add some protection while cloning"
+ tests: verify that `clone -c core.hooksPath=/dev/null` works again
+ clone: drop the protections where hooks aren't run
+ Revert "Add a helper function to compare file contents"
+
+Junio C Hamano (1):
+ Revert "fsck: warn about symlink pointing inside a gitdir"
diff --git a/Documentation/RelNotes/2.43.4.txt b/Documentation/RelNotes/2.43.4.txt
new file mode 100644
index 0000000000..0a842515ff
--- /dev/null
+++ b/Documentation/RelNotes/2.43.4.txt
@@ -0,0 +1,7 @@
+Git v2.43.4 Release Notes
+=========================
+
+This release merges up the fix that appears in v2.39.4, v2.40.2,
+v2.41.1 and v2.42.2 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 these versions for details.
diff --git a/Documentation/RelNotes/2.43.5.txt b/Documentation/RelNotes/2.43.5.txt
new file mode 100644
index 0000000000..236b234b06
--- /dev/null
+++ b/Documentation/RelNotes/2.43.5.txt
@@ -0,0 +1,26 @@
+Git v2.43.5 Release Notes
+=========================
+
+In preparing security fixes for four CVEs, we made overly aggressive
+"defense in depth" changes that broke legitimate use cases like 'git
+lfs' and 'git annex.' This release is to revert these misguided, if
+well-intentioned, changes that were shipped in 2.43.4 and were not
+direct security fixes.
+
+Jeff King (5):
+ send-email: drop FakeTerm hack
+ send-email: avoid creating more than one Term::ReadLine object
+ ci: drop mention of BREW_INSTALL_PACKAGES variable
+ ci: avoid bare "gcc" for osx-gcc job
+ ci: stop installing "gcc-13" for osx-gcc
+
+Johannes Schindelin (6):
+ hook: plug a new memory leak
+ init: use the correct path of the templates directory again
+ Revert "core.hooksPath: add some protection while cloning"
+ tests: verify that `clone -c core.hooksPath=/dev/null` works again
+ clone: drop the protections where hooks aren't run
+ Revert "Add a helper function to compare file contents"
+
+Junio C Hamano (1):
+ Revert "fsck: warn about symlink pointing inside a gitdir"
diff --git a/Documentation/RelNotes/2.44.1.txt b/Documentation/RelNotes/2.44.1.txt
new file mode 100644
index 0000000000..b5135c3281
--- /dev/null
+++ b/Documentation/RelNotes/2.44.1.txt
@@ -0,0 +1,8 @@
+Git v2.44.1 Release Notes
+=========================
+
+This release merges up the fix that appears in v2.39.4, v2.40.2,
+v2.41.1, v2.42.2 and v2.43.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 these versions
+for details.
diff --git a/Documentation/RelNotes/2.44.2.txt b/Documentation/RelNotes/2.44.2.txt
new file mode 100644
index 0000000000..76700f0b73
--- /dev/null
+++ b/Documentation/RelNotes/2.44.2.txt
@@ -0,0 +1,26 @@
+Git v2.44.2 Release Notes
+=========================
+
+In preparing security fixes for four CVEs, we made overly aggressive
+"defense in depth" changes that broke legitimate use cases like 'git
+lfs' and 'git annex.' This release is to revert these misguided, if
+well-intentioned, changes that were shipped in 2.44.1 and were not
+direct security fixes.
+
+Jeff King (5):
+ send-email: drop FakeTerm hack
+ send-email: avoid creating more than one Term::ReadLine object
+ ci: drop mention of BREW_INSTALL_PACKAGES variable
+ ci: avoid bare "gcc" for osx-gcc job
+ ci: stop installing "gcc-13" for osx-gcc
+
+Johannes Schindelin (6):
+ hook: plug a new memory leak
+ init: use the correct path of the templates directory again
+ Revert "core.hooksPath: add some protection while cloning"
+ tests: verify that `clone -c core.hooksPath=/dev/null` works again
+ clone: drop the protections where hooks aren't run
+ Revert "Add a helper function to compare file contents"
+
+Junio C Hamano (1):
+ Revert "fsck: warn about symlink pointing inside a gitdir"
diff --git a/Documentation/RelNotes/2.45.1.txt b/Documentation/RelNotes/2.45.1.txt
new file mode 100644
index 0000000000..3b0d60cfa3
--- /dev/null
+++ b/Documentation/RelNotes/2.45.1.txt
@@ -0,0 +1,8 @@
+Git v2.45.1 Release Notes
+=========================
+
+This release merges up the fix that appears in v2.39.4,
+v2.40.2, v2.41.1, v2.42.2, v2.43.4 and v2.44.1 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
+these versions for details.
diff --git a/Documentation/RelNotes/2.45.2.txt b/Documentation/RelNotes/2.45.2.txt
new file mode 100644
index 0000000000..13429e6491
--- /dev/null
+++ b/Documentation/RelNotes/2.45.2.txt
@@ -0,0 +1,26 @@
+Git v2.45.2 Release Notes
+=========================
+
+In preparing security fixes for four CVEs, we made overly aggressive
+"defense in depth" changes that broke legitimate use cases like 'git
+lfs' and 'git annex.' This release is to revert these misguided, if
+well-intentioned, changes that were shipped in 2.45.1 and were not
+direct security fixes.
+
+Jeff King (5):
+ send-email: drop FakeTerm hack
+ send-email: avoid creating more than one Term::ReadLine object
+ ci: drop mention of BREW_INSTALL_PACKAGES variable
+ ci: avoid bare "gcc" for osx-gcc job
+ ci: stop installing "gcc-13" for osx-gcc
+
+Johannes Schindelin (6):
+ hook: plug a new memory leak
+ init: use the correct path of the templates directory again
+ Revert "core.hooksPath: add some protection while cloning"
+ tests: verify that `clone -c core.hooksPath=/dev/null` works again
+ clone: drop the protections where hooks aren't run
+ Revert "Add a helper function to compare file contents"
+
+Junio C Hamano (1):
+ Revert "fsck: warn about symlink pointing inside a gitdir"
diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches
index c647c7e1b4..52cdb1b18c 100644
--- a/Documentation/SubmittingPatches
+++ b/Documentation/SubmittingPatches
@@ -531,9 +531,9 @@ patch, and "cc:" them when sending such a final version for inclusion.
Some parts of the system have dedicated maintainers with their own
repositories.
-- `git-gui/` comes from git-gui project, maintained by Pratyush Yadav:
+- `git-gui/` comes from git-gui project, maintained by Johannes Sixt:
- https://github.com/prati0100/git-gui.git
+ https://github.com/j6t/git-gui
- `gitk-git/` comes from Paul Mackerras's gitk project:
diff --git a/Documentation/git-gui.txt b/Documentation/git-gui.txt
index e8f3ccb433..f5b02ef114 100644
--- a/Documentation/git-gui.txt
+++ b/Documentation/git-gui.txt
@@ -114,7 +114,7 @@ of end users.
The official repository of the 'git gui' project can be found at:
- https://github.com/prati0100/git-gui.git/
+ https://github.com/j6t/git-gui
GIT
---
diff --git a/Documentation/git-upload-pack.txt b/Documentation/git-upload-pack.txt
index 7ad60bc348..516d1639d9 100644
--- a/Documentation/git-upload-pack.txt
+++ b/Documentation/git-upload-pack.txt
@@ -55,6 +55,37 @@ ENVIRONMENT
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 7a1b112a3e..024a01df6c 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -1067,6 +1067,37 @@ The index is also capable of storing multiple entries (called "stages")
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/GIT-VERSION-GEN b/GIT-VERSION-GEN
index 433cf41e59..eb53b0ee01 100755
--- a/GIT-VERSION-GEN
+++ b/GIT-VERSION-GEN
@@ -1,7 +1,7 @@
#!/bin/sh
GVF=GIT-VERSION-FILE
-DEF_VER=v2.45.0
+DEF_VER=v2.45.2
LF='
'
diff --git a/RelNotes b/RelNotes
index ae70277109..207912064a 120000
--- a/RelNotes
+++ b/RelNotes
@@ -1 +1 @@
-Documentation/RelNotes/2.45.0.txt \ No newline at end of file
+Documentation/RelNotes/2.45.2.txt \ No newline at end of file
diff --git a/builtin/clone.c b/builtin/clone.c
index 74ec14542e..5fbe39f898 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -329,7 +329,20 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
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);
@@ -377,9 +390,27 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
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;
@@ -392,8 +423,6 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
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)
@@ -1507,6 +1536,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
free(dir);
free(path);
free(repo_to_free);
+ UNLEAK(repo);
junk_mode = JUNK_LEAVE_ALL;
transport_ls_refs_options_release(&transport_ls_refs_options);
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index e4e18adb57..5a71b1ee7e 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -303,6 +303,9 @@ static void runcommand_in_submodule_cb(const struct cache_entry *list_item,
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 @@ static void status_submodule(const char *path, const struct object_id *ce_oid,
.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 @@ static void sync_submodule(const char *path, const char *prefix,
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 @@ static void deinit_submodule(const char *path, const char *prefix,
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 @@ static char *clone_submodule_sm_gitdir(const char *name)
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 @@ static int clone_submodule(const struct module_clone_data *clone_data,
"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 @@ static int clone_submodule(const struct module_clone_data *clone_data,
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 @@ static int clone_submodule(const struct module_clone_data *clone_data,
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");
@@ -2517,6 +2584,9 @@ static int update_submodule(struct update_data *update_data)
{
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,
@@ -2624,12 +2694,21 @@ static int update_submodules(struct update_data *update_data)
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;
@@ -3356,6 +3435,9 @@ static int module_add(int argc, const char **argv, const char *prefix)
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 15afb97260..46d93278d9 100644
--- a/builtin/upload-pack.c
+++ b/builtin/upload-pack.c
@@ -9,6 +9,7 @@
#include "upload-pack.h"
#include "serve.h"
#include "commit.h"
+#include "environment.h"
static const char * const upload_pack_usage[] = {
N_("git-upload-pack [--[no-]strict] [--timeout=<n>] [--stateless-rpc]\n"
@@ -39,6 +40,7 @@ int cmd_upload_pack(int argc, const char **argv, const char *prefix)
packet_trace_identity("upload-pack");
disable_replace_refs();
save_commit_buffer = 0;
+ xsetenv(NO_LAZY_FETCH_ENVIRONMENT, "1", 0);
argc = parse_options(argc, argv, prefix, options, upload_pack_usage, 0);
diff --git a/ci/install-dependencies.sh b/ci/install-dependencies.sh
index b4e22de3cb..fa0c00c606 100755
--- a/ci/install-dependencies.sh
+++ b/ci/install-dependencies.sh
@@ -34,8 +34,6 @@ macos-*)
export HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1
# Uncomment this if you want to run perf tests:
# brew install gnu-time
- test -z "$BREW_INSTALL_PACKAGES" ||
- brew install $BREW_INSTALL_PACKAGES
brew link --force gettext
mkdir -p "$P4_PATH"
diff --git a/copy.c b/copy.c
index 23d84c6c1d..d9d2092012 100644
--- a/copy.c
+++ b/copy.c
@@ -1,6 +1,9 @@
#include "git-compat-util.h"
#include "copy.h"
#include "path.h"
+#include "gettext.h"
+#include "strbuf.h"
+#include "abspath.h"
int copy_fd(int ifd, int ofd)
{
diff --git a/dir.c b/dir.c
index 20ebe4cba2..2d83f3311a 100644
--- a/dir.c
+++ b/dir.c
@@ -100,6 +100,18 @@ int fspathncmp(const char *a, const char *b, size_t count)
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 45a7b9ec5f..b9e8e96128 100644
--- a/dir.h
+++ b/dir.h
@@ -549,6 +549,13 @@ int fspathncmp(const char *a, const char *b, size_t count);
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 f918a3a78e..b8c257f6f9 100644
--- a/entry.c
+++ b/entry.c
@@ -460,7 +460,7 @@ static void mark_colliding_entries(const struct checkout *state,
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 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca,
/* 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/hook.c b/hook.c
index f6306d72b3..7e90787bca 100644
--- a/hook.c
+++ b/hook.c
@@ -7,25 +7,31 @@
#include "run-command.h"
#include "config.h"
#include "strbuf.h"
+#include "environment.h"
+#include "setup.h"
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)) {
diff --git a/path.c b/path.c
index 67229edb9c..d838d4be9b 100644
--- a/path.c
+++ b/path.c
@@ -829,6 +829,7 @@ const char *enter_repo(const char *path, int strict)
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);
@@ -839,6 +840,7 @@ const char *enter_repo(const char *path, int strict)
}
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 ac3aa1e365..b414922c44 100644
--- a/promisor-remote.c
+++ b/promisor-remote.c
@@ -8,6 +8,7 @@
#include "transport.h"
#include "strvec.h"
#include "packfile.h"
+#include "environment.h"
struct promisor_remote_config {
struct promisor_remote *promisors;
@@ -23,6 +24,15 @@ static int fetch_objects(struct repository *repo,
int i;
FILE *child_in;
+ if (git_env_bool(NO_LAZY_FETCH_ENVIRONMENT, 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 e1723ad796..a6db25a16d 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -1116,19 +1116,32 @@ static int has_dir_name(struct index_state *istate,
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) {
@@ -1152,53 +1165,6 @@ static int has_dir_name(struct index_state *istate,
}
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/repository.c b/repository.c
index e15b416944..da42ff550e 100644
--- a/repository.c
+++ b/repository.c
@@ -295,6 +295,8 @@ void repo_clear(struct repository *repo)
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 f4b32f76e3..30f243fc32 100644
--- a/setup.c
+++ b/setup.c
@@ -16,6 +16,7 @@
#include "quote.h"
#include "trace2.h"
#include "worktree.h"
+#include "exec-cmd.h"
static int inside_git_dir = -1;
static int inside_work_tree = -1;
@@ -1220,6 +1221,27 @@ static int ensure_valid_ownership(const char *gitfile,
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,
const struct config_context *ctx UNUSED,
void *d)
@@ -1781,6 +1803,57 @@ int daemonize(void)
#endif
}
+struct template_dir_cb_data {
+ char *path;
+ int initialized;
+};
+
+static int template_dir_cb(const char *key, const char *value,
+ const struct config_context *ctx, 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;
+}
+
#ifdef NO_TRUSTABLE_FILEMODE
#define TEST_FILEMODE 0
#else
@@ -1856,8 +1929,9 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
}
}
-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;
@@ -1866,16 +1940,8 @@ static void copy_templates(const char *template_dir, const char *init_template_d
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, '/');
@@ -2023,7 +2089,6 @@ static int create_default_files(const char *template_path,
char *path;
int reinit;
int filemode;
- const char *init_template_dir = NULL;
const char *work_tree = get_git_work_tree();
/*
@@ -2035,9 +2100,7 @@ static int create_default_files(const char *template_path,
* 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/setup.h b/setup.h
index d88bb37aaf..b3fd3bf45a 100644
--- a/setup.h
+++ b/setup.h
@@ -41,6 +41,18 @@ const char *read_gitfile_gently(const char *path, int *return_error_code);
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);
/*
@@ -172,6 +184,8 @@ int verify_repository_format(const struct repository_format *format,
*/
void check_repository_format(struct repository_format *fmt);
+const char *get_template_dir(const char *option_template);
+
#define INIT_DB_QUIET (1 << 0)
#define INIT_DB_EXIST_OK (1 << 1)
#define INIT_DB_SKIP_REFDB (1 << 2)
diff --git a/submodule.c b/submodule.c
index ce2d032521..05d6db9d3d 100644
--- a/submodule.c
+++ b/submodule.c
@@ -1015,6 +1015,9 @@ static int submodule_has_commits(struct repository *r,
.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) {
@@ -1137,6 +1140,9 @@ static int push_submodule(const char *path,
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");
@@ -1186,6 +1192,9 @@ static void submodule_push_check(const char *path, const char *head,
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);
@@ -1517,6 +1526,9 @@ static struct fetch_task *fetch_task_create(struct submodule_parallel_fetch *spf
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) {
@@ -1878,6 +1890,9 @@ unsigned is_submodule_modified(const char *path, int ignore_untracked)
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)
@@ -1954,6 +1969,9 @@ int submodule_uses_gitfile(const char *path)
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) {
@@ -1993,6 +2011,9 @@ int bad_to_remove_submodule(const char *path, unsigned flags)
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;
@@ -2043,6 +2064,9 @@ void submodule_unset_core_worktree(const struct submodule *sub)
{
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");
@@ -2057,6 +2081,9 @@ static int submodule_has_dirty_index(const struct submodule *sub)
{
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;
@@ -2074,6 +2101,10 @@ static int submodule_has_dirty_index(const struct submodule *sub)
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;
@@ -2137,10 +2168,27 @@ int submodule_move_head(const char *path, const char *super_prefix,
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);
@@ -2261,6 +2309,34 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)
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.
@@ -2272,6 +2348,9 @@ static void relocate_single_git_dir_into_superproject(const char *path,
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);
@@ -2313,6 +2392,9 @@ static void absorb_git_dir_into_superproject_recurse(const char *path,
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;
@@ -2337,6 +2419,10 @@ void absorb_git_dir_into_superproject(const char *path,
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);
@@ -2479,6 +2565,9 @@ int submodule_to_gitdir(struct strbuf *buf, const char *submodule)
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 c55a25ca37..b50d29eba4 100644
--- a/submodule.h
+++ b/submodule.h
@@ -148,6 +148,11 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,
*/
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 70396fa384..bf0e23ed50 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
diff --git a/t/lib-chunk.sh b/t/lib-chunk.sh
index a7cd9c3c6d..9f01df190b 100644
--- a/t/lib-chunk.sh
+++ b/t/lib-chunk.sh
@@ -13,5 +13,6 @@ corrupt_chunk_file () {
fn=$1; shift
perl "$TEST_DIRECTORY"/lib-chunk/corrupt-chunk-file.pl \
"$@" <"$fn" >"$fn.tmp" &&
- mv "$fn.tmp" "$fn"
+ # some vintages of macOS 'mv' fails to overwrite a read-only file.
+ mv -f "$fn.tmp" "$fn"
}
diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh
index 6e300be2ac..98b81e4d63 100755
--- a/t/t0000-basic.sh
+++ b/t/t0000-basic.sh
@@ -1201,6 +1201,34 @@ test_expect_success 'very long name in the index handled sanely' '
test $len = 4098
'
+# D/F conflict checking uses an optimization when adding to the end.
+# make sure it does not get confused by `a-` sorting _between_
+# `a` and `a/`.
+test_expect_success 'more update-index D/F conflicts' '
+ # empty the index to make sure our entry is last
+ git read-tree --empty &&
+ cacheinfo=100644,$(test_oid empty_blob) &&
+ git update-index --add --cacheinfo $cacheinfo,path5/a &&
+
+ test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/file &&
+ test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/file &&
+ test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/c/file &&
+
+ # "a-" sorts between "a" and "a/"
+ git update-index --add --cacheinfo $cacheinfo,path5/a- &&
+
+ test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/file &&
+ test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/file &&
+ test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/c/file &&
+
+ cat >expected <<-\EOF &&
+ path5/a
+ path5/a-
+ EOF
+ git ls-files >actual &&
+ test_cmp expected actual
+'
+
test_expect_success 'test_must_fail on a failing git command' '
test_must_fail git notacommand
'
diff --git a/t/t0033-safe-directory.sh b/t/t0033-safe-directory.sh
index dc3496897a..11c3e8f28e 100755
--- a/t/t0033-safe-directory.sh
+++ b/t/t0033-safe-directory.sh
@@ -80,4 +80,28 @@ test_expect_success 'safe.directory in included file' '
git status
'
+test_expect_success 'local clone of unowned repo refused in unsafe directory' '
+ test_when_finished "rm -rf source" &&
+ git init source &&
+ (
+ sane_unset GIT_TEST_ASSUME_DIFFERENT_OWNER &&
+ test_commit -C source initial
+ ) &&
+ test_must_fail git clone --local source target &&
+ test_path_is_missing target
+'
+
+test_expect_success 'local clone of unowned repo accepted in safe directory' '
+ test_when_finished "rm -rf source" &&
+ git init source &&
+ (
+ sane_unset GIT_TEST_ASSUME_DIFFERENT_OWNER &&
+ test_commit -C source initial
+ ) &&
+ test_must_fail git clone --local source target &&
+ git config --global --add safe.directory "$(pwd)/source/.git" &&
+ git clone --local source target &&
+ test_path_is_dir target
+'
+
test_done
diff --git a/t/t0411-clone-from-partial.sh b/t/t0411-clone-from-partial.sh
new file mode 100755
index 0000000000..c98d501869
--- /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 &&
+ test_grep "detected dubious ownership" err &&
+ test_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 &&
+ test_grep "detected dubious ownership" err &&
+ test_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 &&
+ test_grep "detected dubious ownership" err &&
+ test_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 &&
+ test_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 &&
+ test_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 &&
+ test_grep "fake-upload-pack running" err &&
+ test_path_is_file script-executed
+'
+
+test_done
diff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh
index f6dc83e2aa..45a0492917 100755
--- a/t/t1350-config-hooks-path.sh
+++ b/t/t1350-config-hooks-path.sh
@@ -41,4 +41,11 @@ test_expect_success 'git rev-parse --git-path hooks' '
test .git/custom-hooks/abc = "$(cat actual)"
'
+test_expect_success 'core.hooksPath=/dev/null' '
+ git clone -c core.hooksPath=/dev/null . no-templates &&
+ value="$(git -C no-templates config --local core.hooksPath)" &&
+ # The Bash used by Git for Windows rewrites `/dev/null` to `nul`
+ { test /dev/null = "$value" || test nul = "$value"; }
+'
+
test_done
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 33d34d5ae9..9441793d06 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -1252,6 +1252,30 @@ EOF
test_cmp fatal-expect fatal-actual
'
+test_expect_success SYMLINKS 'clone does not get confused by a D/F conflict' '
+ git init df-conflict &&
+ (
+ cd df-conflict &&
+ ln -s .git a &&
+ git add a &&
+ test_tick &&
+ git commit -m symlink &&
+ test_commit a- &&
+ rm a &&
+ mkdir -p a/hooks &&
+ write_script a/hooks/post-checkout <<-EOF &&
+ echo WHOOPSIE >&2
+ echo whoopsie >"$TRASH_DIRECTORY"/whoops
+ EOF
+ git add a/hooks/post-checkout &&
+ test_tick &&
+ git commit -m post-checkout
+ ) &&
+ git clone df-conflict clone 2>err &&
+ test_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 ca43185681..cc0b953f14 100755
--- a/t/t5601-clone.sh
+++ b/t/t5601-clone.sh
@@ -650,6 +650,21 @@ test_expect_success CASE_INSENSITIVE_FS 'colliding file detection' '
test_grep "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 &&
diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh
index 5c4a89df5c..981488885f 100755
--- a/t/t7400-submodule-basic.sh
+++ b/t/t7400-submodule-basic.sh
@@ -1451,4 +1451,35 @@ test_expect_success 'recursive clone respects -q' '
test_must_be_empty actual
'
+test_expect_success '`submodule init` and `init.templateDir`' '
+ mkdir -p tmpl/hooks &&
+ write_script tmpl/hooks/post-checkout <<-EOF &&
+ echo HOOK-RUN >&2
+ echo I was here >hook.run
+ exit 1
+ EOF
+
+ test_config init.templateDir "$(pwd)/tmpl" &&
+ test_when_finished \
+ "git config --global --unset init.templateDir || true" &&
+ (
+ sane_unset GIT_TEMPLATE_DIR &&
+ NO_SET_GIT_TEMPLATE_DIR=t &&
+ export NO_SET_GIT_TEMPLATE_DIR &&
+
+ git config --global init.templateDir "$(pwd)/tmpl" &&
+ test_must_fail git submodule \
+ add "$submodurl" sub-global 2>err &&
+ git config --global --unset init.templateDir &&
+ test_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 &&
+ test_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 8491b8c58b..297c6c3b5c 100755
--- a/t/t7406-submodule-update.sh
+++ b/t/t7406-submodule-update.sh
@@ -1202,4 +1202,52 @@ test_expect_success 'commit with staged submodule change with ignoreSubmodules a
add_submodule_commit_and_validate
'
+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 &&
+ test_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 0000000000..3d3c7af3ce
--- /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 46d4fb0354..4a9c22c9e2 100755
--- a/t/t7450-bad-git-dotfiles.sh
+++ b/t/t7450-bad-git-dotfiles.sh
@@ -320,7 +320,7 @@ test_expect_success WINDOWS 'prevent git~1 squatting on Windows' '
fi
'
-test_expect_success 'git dirs of sibling submodules must not be nested' '
+test_expect_success 'setup submodules with nested git dirs' '
git init nested &&
test_commit -C nested nested &&
(
@@ -338,9 +338,39 @@ test_expect_success 'git dirs of sibling submodules must not be nested' '
git add .gitmodules thing1 thing2 &&
test_tick &&
git commit -m nested
- ) &&
+ )
+'
+
+test_expect_success 'git dirs of sibling submodules must not be nested' '
test_must_fail git clone --recurse-submodules nested clone 2>err &&
test_grep "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