git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
* [PATCH 0/8] Various fixes for v2.45.1 and friends
@ 2024-05-17 23:15 Johannes Schindelin via GitGitGadget
  2024-05-17 23:15 ` [PATCH 1/8] hook: plug a new memory leak Johannes Schindelin via GitGitGadget
                   ` (9 more replies)
  0 siblings, 10 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-17 23:15 UTC (permalink / raw
  To: git; +Cc: Johannes Schindelin

There have been a couple of issues that were reported about v2.45.1, and in
addition I have noticed some myself:

 * a memory leak in the clone protection logic
 * a missed adjustment in the Makefile that leads to an incorrect templates
   path in v2.39.4, v2.40.2 and v2.41.1 (but not in v2.42.2, ..., v2.45.1)
 * an overzealous core.hooksPath check
 * that Git LFS clone problem where it exits with an error (even if the
   clone often succeeded...)

This patch series is based on maint-2.39 to allow for (relatively) easy
follow-up versions v2.39.5, ..., v2.45.2.

Johannes Schindelin (8):
  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
  hook(clone protections): add escape hatch
  hooks(clone protections): special-case current Git LFS hooks
  hooks(clone protections): simplify templates hooks validation
  Revert "Add a helper function to compare file contents"

 Documentation/config/safe.txt |   6 ++
 Makefile                      |   2 +-
 builtin/init-db.c             |   7 +++
 cache.h                       |  14 -----
 config.c                      |  13 +----
 copy.c                        |  58 --------------------
 hook.c                        | 100 +++++++++++++++++++++++++++-------
 hook.h                        |  10 ++++
 setup.c                       |   1 +
 t/helper/test-path-utils.c    |  10 ----
 t/t0060-path-utils.sh         |  41 --------------
 t/t1350-config-hooks-path.sh  |   4 ++
 t/t1800-hook.sh               |  40 ++++++++++----
 t/t5601-clone.sh              |  19 +++++++
 14 files changed, 158 insertions(+), 167 deletions(-)


base-commit: 47b6d90e91835082010da926f6a844d4441c57a6
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1732%2Fdscho%2Fvarious-fixes-for-v2.45.1-and-friends-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1732/dscho/various-fixes-for-v2.45.1-and-friends-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/1732
-- 
gitgitgadget


^ permalink raw reply	[flat|nested] 54+ messages in thread

* [PATCH 1/8] hook: plug a new memory leak
  2024-05-17 23:15 [PATCH 0/8] Various fixes for v2.45.1 and friends Johannes Schindelin via GitGitGadget
@ 2024-05-17 23:15 ` Johannes Schindelin via GitGitGadget
  2024-05-17 23:15 ` [PATCH 2/8] init: use the correct path of the templates directory again Johannes Schindelin via GitGitGadget
                   ` (8 subsequent siblings)
  9 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-17 23:15 UTC (permalink / raw
  To: git; +Cc: Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

In 8db1e8743c0 (clone: prevent hooks from running during a clone,
2024-03-28), I introduced an inadvertent memory leak that was
unfortunately not caught before v2.45.1 was released. Here is a fix.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 hook.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/hook.c b/hook.c
index 632b537b993..fc974cee1d8 100644
--- a/hook.c
+++ b/hook.c
@@ -18,8 +18,10 @@ static int identical_to_template_hook(const char *name, const char *path)
 		found_template_hook = access(template_path.buf, X_OK) >= 0;
 	}
 #endif
-	if (!found_template_hook)
+	if (!found_template_hook) {
+		strbuf_release(&template_path);
 		return 0;
+	}
 
 	ret = do_files_match(template_path.buf, path);
 
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH 2/8] init: use the correct path of the templates directory again
  2024-05-17 23:15 [PATCH 0/8] Various fixes for v2.45.1 and friends Johannes Schindelin via GitGitGadget
  2024-05-17 23:15 ` [PATCH 1/8] hook: plug a new memory leak Johannes Schindelin via GitGitGadget
@ 2024-05-17 23:15 ` Johannes Schindelin via GitGitGadget
  2024-05-17 23:15 ` [PATCH 3/8] Revert "core.hooksPath: add some protection while cloning" Johannes Schindelin via GitGitGadget
                   ` (7 subsequent siblings)
  9 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-17 23:15 UTC (permalink / raw
  To: git; +Cc: Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

In df93e407f06 (init: refactor the template directory discovery into its
own function, 2024-03-29), I refactored the way the templates directory
is discovered.

The refactoring was faithful, but missed a reference in the `Makefile`
where the `DEFAULT_GIT_TEMPLATE_DIR` constant is defined. As a
consequence, Git v2.45.1 and friends will always use the hard-coded path
`/usr/share/git-core/templates`.

Let's fix that by defining the `DEFAULT_GIT_TEMPLATE_DIR` when building
`setup.o`, where that constant is actually used.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Makefile b/Makefile
index 093829ae283..4b1502ba2c6 100644
--- a/Makefile
+++ b/Makefile
@@ -2751,7 +2751,7 @@ exec-cmd.sp exec-cmd.s exec-cmd.o: EXTRA_CPPFLAGS = \
 	'-DFALLBACK_RUNTIME_PREFIX="$(prefix_SQ)"'
 
 builtin/init-db.sp builtin/init-db.s builtin/init-db.o: GIT-PREFIX
-builtin/init-db.sp builtin/init-db.s builtin/init-db.o: EXTRA_CPPFLAGS = \
+setup.sp setup.s setup.o: EXTRA_CPPFLAGS = \
 	-DDEFAULT_GIT_TEMPLATE_DIR='"$(template_dir_SQ)"'
 
 config.sp config.s config.o: GIT-PREFIX
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH 3/8] Revert "core.hooksPath: add some protection while cloning"
  2024-05-17 23:15 [PATCH 0/8] Various fixes for v2.45.1 and friends Johannes Schindelin via GitGitGadget
  2024-05-17 23:15 ` [PATCH 1/8] hook: plug a new memory leak Johannes Schindelin via GitGitGadget
  2024-05-17 23:15 ` [PATCH 2/8] init: use the correct path of the templates directory again Johannes Schindelin via GitGitGadget
@ 2024-05-17 23:15 ` Johannes Schindelin via GitGitGadget
  2024-05-17 23:15 ` [PATCH 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again Johannes Schindelin via GitGitGadget
                   ` (6 subsequent siblings)
  9 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-17 23:15 UTC (permalink / raw
  To: git; +Cc: Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

This defense-in-depth was intended to protect the clone operation
against future escalations where bugs in `git clone` would allow
attackers to write arbitrary files in the `.git/` directory would allow
for Remote Code Execution attacks via maliciously-placed hooks.

However, it turns out that the `core.hooksPath` protection has
unintentional side effects so severe that they do not justify the
benefit of the protections. For example, it has been reported in
https://lore.kernel.org/git/FAFA34CB-9732-4A0A-87FB-BDB272E6AEE8@alchemists.io/
that the following invocation, which is intended to make `git clone`
safer, is itself broken by that protective measure:

	git clone --config core.hooksPath=/dev/null <url>

Since it turns out that the benefit does not justify the cost, let's revert
20f3588efc6 (core.hooksPath: add some protection while cloning,
2024-03-30).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 config.c        | 13 +------------
 t/t1800-hook.sh | 15 ---------------
 2 files changed, 1 insertion(+), 27 deletions(-)

diff --git a/config.c b/config.c
index 85b37f2ee09..8c1c4071f0d 100644
--- a/config.c
+++ b/config.c
@@ -1525,19 +1525,8 @@ static int git_default_core_config(const char *var, const char *value, void *cb)
 	if (!strcmp(var, "core.attributesfile"))
 		return git_config_pathname(&git_attributes_file, var, value);
 
-	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);
+	if (!strcmp(var, "core.hookspath"))
 		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/t/t1800-hook.sh b/t/t1800-hook.sh
index 7ee12e6f48a..2ef3579fa7c 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -177,19 +177,4 @@ test_expect_success 'git hook run a hook with a bad shebang' '
 	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
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again
  2024-05-17 23:15 [PATCH 0/8] Various fixes for v2.45.1 and friends Johannes Schindelin via GitGitGadget
                   ` (2 preceding siblings ...)
  2024-05-17 23:15 ` [PATCH 3/8] Revert "core.hooksPath: add some protection while cloning" Johannes Schindelin via GitGitGadget
@ 2024-05-17 23:15 ` Johannes Schindelin via GitGitGadget
  2024-05-18  0:10   ` Junio C Hamano
  2024-05-17 23:15 ` [PATCH 5/8] hook(clone protections): add escape hatch Johannes Schindelin via GitGitGadget
                   ` (5 subsequent siblings)
  9 siblings, 1 reply; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-17 23:15 UTC (permalink / raw
  To: git; +Cc: Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

As part of the protections added in Git v2.45.1 and friends,
repository-local `core.hooksPath` settings are no longer allowed, as a
defense-in-depth mechanism to prevent future Git vulnerabilities to
raise to critical level if those vulnerabilities inadvertently allow the
repository-local config to be written.

What the added protection did not anticipate is that such a
repository-local `core.hooksPath` can not only be used to point to
maliciously-placed scripts in the current worktree, but also to
_prevent_ hooks from being called altogether.

We just reverted the `core.hooksPath` protections, based on the Git
maintainer's recommendation in
https://lore.kernel.org/git/xmqq4jaxvm8z.fsf@gitster.g/ to address this
concern as well as related ones. Let's make sure that we won't regress
while trying to protect the clone operation further.

Reported-by: Brooke Kuhlmann <brooke@alchemists.io>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 t/t1350-config-hooks-path.sh | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh
index f6dc83e2aab..1eae346a6e3 100755
--- a/t/t1350-config-hooks-path.sh
+++ b/t/t1350-config-hooks-path.sh
@@ -41,4 +41,8 @@ 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
+'
+
 test_done
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH 5/8] hook(clone protections): add escape hatch
  2024-05-17 23:15 [PATCH 0/8] Various fixes for v2.45.1 and friends Johannes Schindelin via GitGitGadget
                   ` (3 preceding siblings ...)
  2024-05-17 23:15 ` [PATCH 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again Johannes Schindelin via GitGitGadget
@ 2024-05-17 23:15 ` Johannes Schindelin via GitGitGadget
  2024-05-18  0:21   ` Junio C Hamano
  2024-05-17 23:15 ` [PATCH 6/8] hooks(clone protections): special-case current Git LFS hooks Johannes Schindelin via GitGitGadget
                   ` (4 subsequent siblings)
  9 siblings, 1 reply; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-17 23:15 UTC (permalink / raw
  To: git; +Cc: Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

As defense-in-depth measures, v2.39.4 and friends leading up to v2.45.1
introduced code that detects when hooks have been installed during a
`git clone`, which is indicative of a common attack vector with critical
severity that allows Remote Code Execution.

There are legitimate use cases for such behavior, though, for example
when those hooks stem from Git's own templates, which system
administrators are at liberty to modify to enforce, say, commit message
conventions. The git clone protections specifically add exceptions to
allow for that.

Another legitimate use case that has been identified too late to be
handled in these security bug-fix versions is Git LFS: It behaves
somewhat similar to common attack vectors by writing a few hooks while
running the `smudge` filter during a regular clone, which means that Git
has no chance to know that the hooks are benign and e.g. the
`post-checkout` hook can be safely executed as part of the clone
operation.

To help Git LFS, and other tools behaving similarly (if there are any),
let's add a new, multi-valued `safe.hook.sha256` config setting. Like
the already-existing `safe.*` settings, it is ignored in
repository-local configs, and it is interpreted as a list of SHA-256
checksums of hooks' contents that are safe to execute during a clone
operation. Future Git LFS versions will need to write those entries at
the same time they install the `smudge`/`clean` filters.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 Documentation/config/safe.txt |  6 ++++
 hook.c                        | 66 ++++++++++++++++++++++++++++++++---
 t/t1800-hook.sh               | 15 ++++++++
 3 files changed, 82 insertions(+), 5 deletions(-)

diff --git a/Documentation/config/safe.txt b/Documentation/config/safe.txt
index bde7f31459b..69ee845be89 100644
--- a/Documentation/config/safe.txt
+++ b/Documentation/config/safe.txt
@@ -59,3 +59,9 @@ which id the original user has.
 If that is not what you would prefer and want git to only trust
 repositories that are owned by root instead, then you can remove
 the `SUDO_UID` variable from root's environment before invoking git.
+
+safe.hook.sha256::
+	The value is the SHA-256 of hooks that are considered to be safe
+	to run during a clone operation.
++
+Multiple values can be added via `git config --global --add`.
diff --git a/hook.c b/hook.c
index fc974cee1d8..a2479738451 100644
--- a/hook.c
+++ b/hook.c
@@ -2,6 +2,7 @@
 #include "hook.h"
 #include "run-command.h"
 #include "config.h"
+#include "strmap.h"
 
 static int identical_to_template_hook(const char *name, const char *path)
 {
@@ -29,11 +30,65 @@ static int identical_to_template_hook(const char *name, const char *path)
 	return ret;
 }
 
+static struct strset safe_hook_sha256s = STRSET_INIT;
+static int safe_hook_sha256s_initialized;
+
+static int get_sha256_of_file_contents(const char *path, char *sha256)
+{
+	struct strbuf sb = STRBUF_INIT;
+	int fd;
+	ssize_t res;
+
+	git_hash_ctx ctx;
+	const struct git_hash_algo *algo = &hash_algos[GIT_HASH_SHA256];
+	unsigned char hash[GIT_MAX_RAWSZ];
+
+	if ((fd = open(path, O_RDONLY)) < 0)
+		return -1;
+	res = strbuf_read(&sb, fd, 400);
+	close(fd);
+	if (res < 0)
+		return -1;
+
+	algo->init_fn(&ctx);
+	algo->update_fn(&ctx, sb.buf, sb.len);
+	strbuf_release(&sb);
+	algo->final_fn(hash, &ctx);
+
+	hash_to_hex_algop_r(sha256, hash, algo);
+
+	return 0;
+}
+
+static int safe_hook_cb(const char *key, const char *value, void *d)
+{
+	struct strset *set = d;
+
+	if (value && !strcmp(key, "safe.hook.sha256"))
+		strset_add(set, value);
+
+	return 0;
+}
+
+static int is_hook_safe_during_clone(const char *name, const char *path, char *sha256)
+{
+	if (get_sha256_of_file_contents(path, sha256) < 0)
+		return 0;
+
+	if (!safe_hook_sha256s_initialized) {
+		safe_hook_sha256s_initialized = 1;
+		git_protected_config(safe_hook_cb, &safe_hook_sha256s);
+	}
+
+	return strset_contains(&safe_hook_sha256s, sha256);
+}
+
 const char *find_hook(const char *name)
 {
 	static struct strbuf path = STRBUF_INIT;
 
 	int found_hook;
+	char sha256[GIT_SHA256_HEXSZ + 1] = { '\0' };
 
 	strbuf_reset(&path);
 	strbuf_git_path(&path, "hooks/%s", name);
@@ -65,13 +120,14 @@ const char *find_hook(const char *name)
 		return NULL;
 	}
 	if (!git_hooks_path && git_env_bool("GIT_CLONE_PROTECTION_ACTIVE", 0) &&
-	    !identical_to_template_hook(name, path.buf))
+	    !identical_to_template_hook(name, path.buf) &&
+	    !is_hook_safe_during_clone(name, path.buf, sha256))
 		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);
+		      "If this is intentional and the hook is safe to run, "
+		      "please run the following command and try again:\n\n"
+		      "  git config --global --add safe.hook.sha256 %s"),
+		    name, path.buf, sha256);
 	return path.buf;
 }
 
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index 2ef3579fa7c..0f74c9154d0 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -177,4 +177,19 @@ test_expect_success 'git hook run a hook with a bad shebang' '
 	test_cmp expect actual
 '
 
+test_expect_success '`safe.hook.sha256` and clone protections' '
+	git init safe-hook &&
+	write_script safe-hook/.git/hooks/pre-push <<-\EOF &&
+	echo "called hook" >safe-hook.log
+	EOF
+
+	test_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \
+		git -C safe-hook hook run pre-push 2>err &&
+	cmd="$(grep "git config --global --add safe.hook.sha256 [0-9a-f]" err)" &&
+	eval "$cmd" &&
+	GIT_CLONE_PROTECTION_ACTIVE=true \
+		git -C safe-hook hook run pre-push &&
+	test "called hook" = "$(cat safe-hook/safe-hook.log)"
+'
+
 test_done
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH 6/8] hooks(clone protections): special-case current Git LFS hooks
  2024-05-17 23:15 [PATCH 0/8] Various fixes for v2.45.1 and friends Johannes Schindelin via GitGitGadget
                   ` (4 preceding siblings ...)
  2024-05-17 23:15 ` [PATCH 5/8] hook(clone protections): add escape hatch Johannes Schindelin via GitGitGadget
@ 2024-05-17 23:15 ` Johannes Schindelin via GitGitGadget
  2024-05-18  0:20   ` Junio C Hamano
  2024-05-17 23:15 ` [PATCH 7/8] hooks(clone protections): simplify templates hooks validation Johannes Schindelin via GitGitGadget
                   ` (3 subsequent siblings)
  9 siblings, 1 reply; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-17 23:15 UTC (permalink / raw
  To: git; +Cc: Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

A notable regression in v2.45.1 and friends (all the way down to
v2.39.4) has been that Git LFS-enabled clones error out with a message
indicating that the `post-checkout` hook has been tampered with while
cloning, and as a safety measure it is not executed.

A generic fix for benign third-party applications wishing to write hooks
during clone operations has been implemented in the parent of this
commit: said applications are expected to add `safe.hook.sha256` values
to a protected config.

However, the current version of Git LFS, v3.5.1, cannot be adapted
retroactively; Therefore, let's just hard-code the SHA-256 values for
this version. That way, Git LFS usage will no longer be broken, and the
next Git LFS version can be taught to add those `safe.hook.sha256`
entries.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 hook.c          | 11 +++++++++++
 t/t1800-hook.sh | 20 ++++++++++++++++++++
 2 files changed, 31 insertions(+)

diff --git a/hook.c b/hook.c
index a2479738451..f810ee133be 100644
--- a/hook.c
+++ b/hook.c
@@ -75,6 +75,17 @@ static int is_hook_safe_during_clone(const char *name, const char *path, char *s
 	if (get_sha256_of_file_contents(path, sha256) < 0)
 		return 0;
 
+	/* Hard-code known-safe values for Git LFS v3.4.0..v3.5.1 */
+	if ((!strcmp("pre-push", name) &&
+	     !strcmp(sha256, "df5417b2daa3aa144c19681d1e997df7ebfe144fb7e3e05138bd80ae998008e4")) ||
+	    (!strcmp("post-checkout", name) &&
+	     !strcmp(sha256, "791471b4ff472aab844a4fceaa48bbb0a12193616f971e8e940625498b4938a6")) ||
+	    (!strcmp("post-commit", name) &&
+	     !strcmp(sha256, "21e961572bb3f43a5f2fbafc1cc764d86046cc2e5f0bbecebfe9684a0b73b664")) ||
+	    (!strcmp("post-merge", name) &&
+	     !strcmp(sha256, "75da0da66a803b4b030ad50801ba57062c6196105eb1d2251590d100edb9390b")))
+		return 1;
+
 	if (!safe_hook_sha256s_initialized) {
 		safe_hook_sha256s_initialized = 1;
 		git_protected_config(safe_hook_cb, &safe_hook_sha256s);
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index 0f74c9154d0..af66999aff3 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -192,4 +192,24 @@ test_expect_success '`safe.hook.sha256` and clone protections' '
 	test "called hook" = "$(cat safe-hook/safe-hook.log)"
 '
 
+write_lfs_pre_push_hook () {
+	write_script "$1" <<-\EOF
+	command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; }
+	git lfs pre-push "$@"
+	EOF
+}
+
+test_expect_success 'Git LFS special-handling in clone protections' '
+	git init lfs-hooks &&
+	write_lfs_pre_push_hook lfs-hooks/.git/hooks/pre-push &&
+	write_script git-lfs <<-\EOF &&
+	echo "called $*" >fake-git-lfs.log
+	EOF
+
+	PATH="$PWD:$PATH" GIT_CLONE_PROTECTION_ACTIVE=true \
+		git -C lfs-hooks hook run pre-push &&
+	test_write_lines "called pre-push" >expect &&
+	test_cmp lfs-hooks/fake-git-lfs.log expect
+'
+
 test_done
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH 7/8] hooks(clone protections): simplify templates hooks validation
  2024-05-17 23:15 [PATCH 0/8] Various fixes for v2.45.1 and friends Johannes Schindelin via GitGitGadget
                   ` (5 preceding siblings ...)
  2024-05-17 23:15 ` [PATCH 6/8] hooks(clone protections): special-case current Git LFS hooks Johannes Schindelin via GitGitGadget
@ 2024-05-17 23:15 ` Johannes Schindelin via GitGitGadget
  2024-05-17 23:15 ` [PATCH 8/8] Revert "Add a helper function to compare file contents" Johannes Schindelin via GitGitGadget
                   ` (2 subsequent siblings)
  9 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-17 23:15 UTC (permalink / raw
  To: git; +Cc: Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

When an active hook is encountered during a clone operation, to protect
against Remote Code Execution attack vectors, Git checks whether the
hook was copied over from the templates directory.

When that logic was introduced, there was no other way to check this
than to add a function to compare files.

In the meantime, we've added code to compute the SHA-256 checksum of a
given hook and compare that checksum against a list of known-safe ones.

Let's simplify the logic by adding to said list when copying the
templates' hooks.

We need to be careful to support multi-process operations such as
recursive submodule clones: In such a scenario, the list of SHA-256
checksums that is kept in memory is not enough, we also have to pass the
information down to child processes via `GIT_CONFIG_PARAMETERS`.

Extend the regression test in t5601 to ensure that recursive clones are
handled as expected.

Note: Technically there is no way that the checksums computed while
initializing the submodules' gitdirs can be passed to the process that
performs the checkout: For historical reasons, these operations are
performed in processes spawned in separate loops from the
super-project's `git clone` process. But since the templates from which
the submodules are initialized are the very same as the ones from which
the super-project is initialized, we can get away with using the list of
SHA-256 checksums that is computed when initializing the super-project
and passing that down to the `submodule--helper` processes that perform
the recursive checkout.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/init-db.c |  7 +++++++
 hook.c            | 43 ++++++++++++++++---------------------------
 hook.h            | 10 ++++++++++
 setup.c           |  1 +
 t/t5601-clone.sh  | 19 +++++++++++++++++++
 5 files changed, 53 insertions(+), 27 deletions(-)

diff --git a/builtin/init-db.c b/builtin/init-db.c
index a101e7f94c1..64357fdada4 100644
--- a/builtin/init-db.c
+++ b/builtin/init-db.c
@@ -10,6 +10,8 @@
 #include "exec-cmd.h"
 #include "parse-options.h"
 #include "worktree.h"
+#include "run-command.h"
+#include "hook.h"
 
 #ifdef NO_TRUSTABLE_FILEMODE
 #define TEST_FILEMODE 0
@@ -28,6 +30,7 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
 	size_t path_baselen = path->len;
 	size_t template_baselen = template_path->len;
 	struct dirent *de;
+	int is_hooks_dir = ends_with(template_path->buf, "/hooks/");
 
 	/* Note: if ".git/hooks" file exists in the repository being
 	 * re-initialized, /etc/core-git/templates/hooks/update would
@@ -80,6 +83,10 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
 			strbuf_release(&lnk);
 		}
 		else if (S_ISREG(st_template.st_mode)) {
+			if (is_hooks_dir &&
+			    is_executable(template_path->buf))
+				add_safe_hook(template_path->buf);
+
 			if (copy_file(path->buf, template_path->buf, st_template.st_mode))
 				die_errno(_("cannot copy '%s' to '%s'"),
 					  template_path->buf, path->buf);
diff --git a/hook.c b/hook.c
index f810ee133be..b69cc691bdf 100644
--- a/hook.c
+++ b/hook.c
@@ -4,32 +4,6 @@
 #include "config.h"
 #include "strmap.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) {
-		strbuf_release(&template_path);
-		return 0;
-	}
-
-	ret = do_files_match(template_path.buf, path);
-
-	strbuf_release(&template_path);
-	return ret;
-}
-
 static struct strset safe_hook_sha256s = STRSET_INIT;
 static int safe_hook_sha256s_initialized;
 
@@ -60,6 +34,22 @@ static int get_sha256_of_file_contents(const char *path, char *sha256)
 	return 0;
 }
 
+void add_safe_hook(const char *path)
+{
+	char sha256[GIT_SHA256_HEXSZ + 1] = { '\0' };
+
+	if (!get_sha256_of_file_contents(path, sha256)) {
+		char *p;
+
+		strset_add(&safe_hook_sha256s, sha256);
+
+		/* support multi-process operations e.g. recursive clones */
+		p = xstrfmt("safe.hook.sha256=%s", sha256);
+		git_config_push_parameter(p);
+		free(p);
+	}
+}
+
 static int safe_hook_cb(const char *key, const char *value, void *d)
 {
 	struct strset *set = d;
@@ -131,7 +121,6 @@ const char *find_hook(const char *name)
 		return NULL;
 	}
 	if (!git_hooks_path && git_env_bool("GIT_CLONE_PROTECTION_ACTIVE", 0) &&
-	    !identical_to_template_hook(name, path.buf) &&
 	    !is_hook_safe_during_clone(name, path.buf, sha256))
 		die(_("active `%s` hook found during `git clone`:\n\t%s\n"
 		      "For security reasons, this is disallowed by default.\n"
diff --git a/hook.h b/hook.h
index 4258b13da0d..e2034ee8b23 100644
--- a/hook.h
+++ b/hook.h
@@ -82,4 +82,14 @@ int run_hooks(const char *hook_name);
  * hook. This function behaves like the old run_hook_le() API.
  */
 int run_hooks_l(const char *hook_name, ...);
+
+/**
+ * Mark the contents of the provided path as safe to run during a clone
+ * operation.
+ *
+ * This function is mainly used when copying templates to mark the
+ * just-copied hooks as benign.
+ */
+void add_safe_hook(const char *path);
+
 #endif
diff --git a/setup.c b/setup.c
index c3301f5ab82..7f7538c9bf7 100644
--- a/setup.c
+++ b/setup.c
@@ -7,6 +7,7 @@
 #include "promisor-remote.h"
 #include "quote.h"
 #include "exec-cmd.h"
+#include "hook.h"
 
 static int inside_git_dir = -1;
 static int inside_work_tree = -1;
diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh
index 20deca0231b..71eaa3d1e14 100755
--- a/t/t5601-clone.sh
+++ b/t/t5601-clone.sh
@@ -819,6 +819,25 @@ test_expect_success 'clone with init.templatedir runs hooks' '
 		git config --unset init.templateDir &&
 		! grep "active .* hook found" err &&
 		test_path_is_missing hook-run-local-config/hook.run
+	) &&
+
+	test_config_global protocol.file.allow always &&
+	git -C tmpl/hooks submodule add "$(pwd)/tmpl/hooks" sub &&
+	test_tick &&
+	git -C tmpl/hooks add .gitmodules sub &&
+	git -C tmpl/hooks commit -m submodule &&
+
+	(
+		sane_unset GIT_TEMPLATE_DIR &&
+		NO_SET_GIT_TEMPLATE_DIR=t &&
+		export NO_SET_GIT_TEMPLATE_DIR &&
+
+		git -c init.templateDir="$(pwd)/tmpl" \
+			clone --recurse-submodules \
+			tmpl/hooks hook-run-submodule 2>err &&
+		! grep "active .* hook found" err &&
+		test_path_is_file hook-run-submodule/hook.run &&
+		test_path_is_file hook-run-submodule/sub/hook.run
 	)
 '
 
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH 8/8] Revert "Add a helper function to compare file contents"
  2024-05-17 23:15 [PATCH 0/8] Various fixes for v2.45.1 and friends Johannes Schindelin via GitGitGadget
                   ` (6 preceding siblings ...)
  2024-05-17 23:15 ` [PATCH 7/8] hooks(clone protections): simplify templates hooks validation Johannes Schindelin via GitGitGadget
@ 2024-05-17 23:15 ` Johannes Schindelin via GitGitGadget
  2024-05-17 23:52 ` [PATCH 0/8] Various fixes for v2.45.1 and friends Junio C Hamano
  2024-05-18 10:32 ` [PATCH v2 " Johannes Schindelin via GitGitGadget
  9 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-17 23:15 UTC (permalink / raw
  To: git; +Cc: Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

Now that during a `git clone`, the hooks' contents are no longer
compared to the templates' files', the caller for which the
`do_files_match()` function was introduced is gone, and therefore this
function can be retired, too.

This reverts commit 584de0b4c23 (Add a helper function to compare file
contents, 2024-03-30).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 cache.h                    | 14 ---------
 copy.c                     | 58 --------------------------------------
 t/helper/test-path-utils.c | 10 -------
 t/t0060-path-utils.sh      | 41 ---------------------------
 4 files changed, 123 deletions(-)

diff --git a/cache.h b/cache.h
index 16b34799bfd..8c5fb1e1ba1 100644
--- a/cache.h
+++ b/cache.h
@@ -1785,20 +1785,6 @@ int copy_fd(int ifd, int ofd);
 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);
-
 void write_or_die(int fd, const void *buf, size_t count);
 void fsync_or_die(int fd, const char *);
 int fsync_component(enum fsync_component component, int fd);
diff --git a/copy.c b/copy.c
index 8492f6fc831..4de6a110f09 100644
--- a/copy.c
+++ b/copy.c
@@ -65,61 +65,3 @@ int copy_file_with_time(const char *dst, const char *src, int mode)
 		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/t/helper/test-path-utils.c b/t/helper/test-path-utils.c
index 0e0de218076..f69709d674f 100644
--- a/t/helper/test-path-utils.c
+++ b/t/helper/test-path-utils.c
@@ -495,16 +495,6 @@ int cmd__path_utils(int argc, const char **argv)
 		return !!res;
 	}
 
-	if (argc == 4 && !strcmp(argv[1], "do_files_match")) {
-		int ret = do_files_match(argv[2], argv[3]);
-
-		if (ret)
-			printf("equal\n");
-		else
-			printf("different\n");
-		return !ret;
-	}
-
 	fprintf(stderr, "%s: unknown function name: %s\n", argv[0],
 		argv[1] ? argv[1] : "(there was none)");
 	return 1;
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index 73d0e1a7f10..68e29c904a6 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -560,45 +560,4 @@ test_expect_success !VALGRIND,RUNTIME_PREFIX,CAN_EXEC_IN_PWD '%(prefix)/ works'
 	test_cmp expect actual
 '
 
-test_expect_success 'do_files_match()' '
-	test_seq 0 10 >0-10.txt &&
-	test_seq -1 10 >-1-10.txt &&
-	test_seq 1 10 >1-10.txt &&
-	test_seq 1 9 >1-9.txt &&
-	test_seq 0 8 >0-8.txt &&
-
-	test-tool path-utils do_files_match 0-10.txt 0-10.txt >out &&
-
-	assert_fails() {
-		test_must_fail \
-		test-tool path-utils do_files_match "$1" "$2" >out &&
-		grep different out
-	} &&
-
-	assert_fails 0-8.txt 1-9.txt &&
-	assert_fails -1-10.txt 0-10.txt &&
-	assert_fails 1-10.txt 1-9.txt &&
-	assert_fails 1-10.txt .git &&
-	assert_fails does-not-exist 1-10.txt &&
-
-	if test_have_prereq FILEMODE
-	then
-		cp 0-10.txt 0-10.x &&
-		chmod a+x 0-10.x &&
-		assert_fails 0-10.txt 0-10.x
-	fi &&
-
-	if test_have_prereq SYMLINKS
-	then
-		ln -sf 0-10.txt symlink &&
-		ln -s 0-10.txt another-symlink &&
-		ln -s over-the-ocean yet-another-symlink &&
-		ln -s "$PWD/0-10.txt" absolute-symlink &&
-		assert_fails 0-10.txt symlink &&
-		test-tool path-utils do_files_match symlink another-symlink &&
-		assert_fails symlink yet-another-symlink &&
-		assert_fails symlink absolute-symlink
-	fi
-'
-
 test_done
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 54+ messages in thread

* Re: [PATCH 0/8] Various fixes for v2.45.1 and friends
  2024-05-17 23:15 [PATCH 0/8] Various fixes for v2.45.1 and friends Johannes Schindelin via GitGitGadget
                   ` (7 preceding siblings ...)
  2024-05-17 23:15 ` [PATCH 8/8] Revert "Add a helper function to compare file contents" Johannes Schindelin via GitGitGadget
@ 2024-05-17 23:52 ` Junio C Hamano
  2024-05-18  0:02   ` Johannes Schindelin
  2024-05-18 10:32 ` [PATCH v2 " Johannes Schindelin via GitGitGadget
  9 siblings, 1 reply; 54+ messages in thread
From: Junio C Hamano @ 2024-05-17 23:52 UTC (permalink / raw
  To: brian m. carlson
  Cc: git, Johannes Schindelin, Johannes Schindelin via GitGitGadget

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> There have been a couple of issues that were reported about v2.45.1, and in
> addition I have noticed some myself:
>
>  * a memory leak in the clone protection logic
>  * a missed adjustment in the Makefile that leads to an incorrect templates
>    path in v2.39.4, v2.40.2 and v2.41.1 (but not in v2.42.2, ..., v2.45.1)
>  * an overzealous core.hooksPath check
>  * that Git LFS clone problem where it exits with an error (even if the
>    clone often succeeded...)

I thought brian expressed an interest in the issues these patches
attempt to address, so I thought it is helpful to Cc them to his
attention.

> This patch series is based on maint-2.39 to allow for (relatively) easy
> follow-up versions v2.39.5, ..., v2.45.2.

Thanks for basing them on the oldest version that is relevant.



^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH 0/8] Various fixes for v2.45.1 and friends
  2024-05-17 23:52 ` [PATCH 0/8] Various fixes for v2.45.1 and friends Junio C Hamano
@ 2024-05-18  0:02   ` Johannes Schindelin
  0 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin @ 2024-05-18  0:02 UTC (permalink / raw
  To: Junio C Hamano; +Cc: brian m. carlson, git

Hi Junio and brian,

On Fri, 17 May 2024, Junio C Hamano wrote:

> "Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
> writes:
>
> > There have been a couple of issues that were reported about v2.45.1, and in
> > addition I have noticed some myself:
> >
> >  * a memory leak in the clone protection logic
> >  * a missed adjustment in the Makefile that leads to an incorrect templates
> >    path in v2.39.4, v2.40.2 and v2.41.1 (but not in v2.42.2, ..., v2.45.1)
> >  * an overzealous core.hooksPath check
> >  * that Git LFS clone problem where it exits with an error (even if the
> >    clone often succeeded...)
>
> I thought brian expressed an interest in the issues these patches
> attempt to address, so I thought it is helpful to Cc them to his
> attention.

oh my. I forgot to add the Cc (I guess I should have slept first, but I
wanted to get this out so that the Git LFS breakage as well as the other
breakages could be addressed as swiftly as possible)!

I really meant to Cc: you, brian, hoping for your substantial expertise
and kind code reviews to help get this landed soonish, in a shape that
makes Git better, and hopefully averts more breakages.

> > This patch series is based on maint-2.39 to allow for (relatively) easy
> > follow-up versions v2.39.5, ..., v2.45.2.
>
> Thanks for basing them on the oldest version that is relevant.

I also meant to provide `maint-*` branches but basically ran out of time.
Will work on that next week. I expect quite a few merge conflicts.

Ciao,
Johannes


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again
  2024-05-17 23:15 ` [PATCH 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again Johannes Schindelin via GitGitGadget
@ 2024-05-18  0:10   ` Junio C Hamano
  2024-05-18 18:58     ` Johannes Schindelin
  0 siblings, 1 reply; 54+ messages in thread
From: Junio C Hamano @ 2024-05-18  0:10 UTC (permalink / raw
  To: Johannes Schindelin via GitGitGadget; +Cc: git, Johannes Schindelin

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> What the added protection did not anticipate is that such a
> repository-local `core.hooksPath` can not only be used to point to
> maliciously-placed scripts in the current worktree, but also to
> _prevent_ hooks from being called altogether.
> ...
> diff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh
> index f6dc83e2aab..1eae346a6e3 100755
> --- a/t/t1350-config-hooks-path.sh
> +++ b/t/t1350-config-hooks-path.sh
> @@ -41,4 +41,8 @@ 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
> +'

Is it sufficient that the command exits with 0?  I am wondering if
we want to verify that the resulting repository looks like it
should, e.g., with

    v=$(git -C no-templates config --local --get core.hookspath) &&
    test "$v" = /dev/null

or something silly like that.




^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH 6/8] hooks(clone protections): special-case current Git LFS hooks
  2024-05-17 23:15 ` [PATCH 6/8] hooks(clone protections): special-case current Git LFS hooks Johannes Schindelin via GitGitGadget
@ 2024-05-18  0:20   ` Junio C Hamano
  0 siblings, 0 replies; 54+ messages in thread
From: Junio C Hamano @ 2024-05-18  0:20 UTC (permalink / raw
  To: Johannes Schindelin via GitGitGadget; +Cc: git, Johannes Schindelin

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> +	/* Hard-code known-safe values for Git LFS v3.4.0..v3.5.1 */
> +	if ((!strcmp("pre-push", name) &&
> +	     !strcmp(sha256, "df5417b2daa3aa144c19681d1e997df7ebfe144fb7e3e05138bd80ae998008e4")) ||
> +	    (!strcmp("post-checkout", name) &&
> +	     !strcmp(sha256, "791471b4ff472aab844a4fceaa48bbb0a12193616f971e8e940625498b4938a6")) ||
> +	    (!strcmp("post-commit", name) &&
> +	     !strcmp(sha256, "21e961572bb3f43a5f2fbafc1cc764d86046cc2e5f0bbecebfe9684a0b73b664")) ||
> +	    (!strcmp("post-merge", name) &&
> +	     !strcmp(sha256, "75da0da66a803b4b030ad50801ba57062c6196105eb1d2251590d100edb9390b")))
> +		return 1;
> +
>  	if (!safe_hook_sha256s_initialized) {
>  		safe_hook_sha256s_initialized = 1;
>  		git_protected_config(safe_hook_cb, &safe_hook_sha256s);


Not that it makes a huge difference, but if I were doing this patch
I would have instead added the special case hashes to the
safe_hook_sha256s hashmap inside this "if we haven't been
initialized" block.  That way, the checking of the hash can be done
at the central place with the same code as used to check other
custom hooks.


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH 5/8] hook(clone protections): add escape hatch
  2024-05-17 23:15 ` [PATCH 5/8] hook(clone protections): add escape hatch Johannes Schindelin via GitGitGadget
@ 2024-05-18  0:21   ` Junio C Hamano
  0 siblings, 0 replies; 54+ messages in thread
From: Junio C Hamano @ 2024-05-18  0:21 UTC (permalink / raw
  To: Johannes Schindelin via GitGitGadget; +Cc: git, Johannes Schindelin

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> +static int is_hook_safe_during_clone(const char *name, const char *path, char *sha256)
> +{
> +	if (get_sha256_of_file_contents(path, sha256) < 0)
> +		return 0;
> +
> +	if (!safe_hook_sha256s_initialized) {
> +		safe_hook_sha256s_initialized = 1;
> +		git_protected_config(safe_hook_cb, &safe_hook_sha256s);
> +	}
> +
> +	return strset_contains(&safe_hook_sha256s, sha256);
> +}

Makes sense.  Use of a hashmap/strset here makes sense.


^ permalink raw reply	[flat|nested] 54+ messages in thread

* [PATCH v2 0/8] Various fixes for v2.45.1 and friends
  2024-05-17 23:15 [PATCH 0/8] Various fixes for v2.45.1 and friends Johannes Schindelin via GitGitGadget
                   ` (8 preceding siblings ...)
  2024-05-17 23:52 ` [PATCH 0/8] Various fixes for v2.45.1 and friends Junio C Hamano
@ 2024-05-18 10:32 ` Johannes Schindelin via GitGitGadget
  2024-05-18 10:32   ` [PATCH v2 1/8] hook: plug a new memory leak Johannes Schindelin via GitGitGadget
                     ` (9 more replies)
  9 siblings, 10 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-18 10:32 UTC (permalink / raw
  To: git; +Cc: brian m. carlson, Johannes Schindelin

There have been a couple of issues that were reported about v2.45.1, and in
addition I have noticed some myself:

 * a memory leak in the clone protection logic
 * a missed adjustment in the Makefile that leads to an incorrect templates
   path in v2.39.4, v2.40.2 and v2.41.1 (but not in v2.42.2, ..., v2.45.1)
 * an overzealous core.hooksPath check
 * that Git LFS clone problem where it exits with an error (even if the
   clone often succeeded...)

This patch series is based on maint-2.39 to allow for (relatively) easy
follow-up versions v2.39.5, ..., v2.45.2.

Changes since v1:

 * simplified adding the SHA-256s corresponding to Git LFS' hooks
 * the core.hooksPath test case now verifies that the config setting was
   configured correctly

Johannes Schindelin (8):
  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
  hook(clone protections): add escape hatch
  hooks(clone protections): special-case current Git LFS hooks
  hooks(clone protections): simplify templates hooks validation
  Revert "Add a helper function to compare file contents"

 Documentation/config/safe.txt |   6 ++
 Makefile                      |   2 +-
 builtin/init-db.c             |   7 +++
 cache.h                       |  14 -----
 config.c                      |  13 +----
 copy.c                        |  58 --------------------
 hook.c                        | 100 +++++++++++++++++++++++++++-------
 hook.h                        |  10 ++++
 setup.c                       |   1 +
 t/helper/test-path-utils.c    |  10 ----
 t/t0060-path-utils.sh         |  41 --------------
 t/t1350-config-hooks-path.sh  |   7 +++
 t/t1800-hook.sh               |  40 ++++++++++----
 t/t5601-clone.sh              |  19 +++++++
 14 files changed, 161 insertions(+), 167 deletions(-)


base-commit: 47b6d90e91835082010da926f6a844d4441c57a6
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1732%2Fdscho%2Fvarious-fixes-for-v2.45.1-and-friends-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1732/dscho/various-fixes-for-v2.45.1-and-friends-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/1732

Range-diff vs v1:

 1:  d4a003bf2ce = 1:  d4a003bf2ce hook: plug a new memory leak
 2:  961dfc35f42 = 2:  961dfc35f42 init: use the correct path of the templates directory again
 3:  57db89a1497 = 3:  57db89a1497 Revert "core.hooksPath: add some protection while cloning"
 4:  7d5ef6db2a9 ! 4:  cd14042b065 tests: verify that `clone -c core.hooksPath=/dev/null` works again
     @@ t/t1350-config-hooks-path.sh: test_expect_success 'git rev-parse --git-path hook
       '
       
      +test_expect_success 'core.hooksPath=/dev/null' '
     -+	git clone -c core.hooksPath=/dev/null . no-templates
     ++	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
 5:  a4f5eeef667 = 5:  b841db8392e hook(clone protections): add escape hatch
 6:  98465797e72 ! 6:  5e5128bc232 hooks(clone protections): special-case current Git LFS hooks
     @@ Commit message
      
       ## hook.c ##
      @@ hook.c: static int is_hook_safe_during_clone(const char *name, const char *path, char *s
     - 	if (get_sha256_of_file_contents(path, sha256) < 0)
     - 		return 0;
       
     -+	/* Hard-code known-safe values for Git LFS v3.4.0..v3.5.1 */
     -+	if ((!strcmp("pre-push", name) &&
     -+	     !strcmp(sha256, "df5417b2daa3aa144c19681d1e997df7ebfe144fb7e3e05138bd80ae998008e4")) ||
     -+	    (!strcmp("post-checkout", name) &&
     -+	     !strcmp(sha256, "791471b4ff472aab844a4fceaa48bbb0a12193616f971e8e940625498b4938a6")) ||
     -+	    (!strcmp("post-commit", name) &&
     -+	     !strcmp(sha256, "21e961572bb3f43a5f2fbafc1cc764d86046cc2e5f0bbecebfe9684a0b73b664")) ||
     -+	    (!strcmp("post-merge", name) &&
     -+	     !strcmp(sha256, "75da0da66a803b4b030ad50801ba57062c6196105eb1d2251590d100edb9390b")))
     -+		return 1;
     -+
       	if (!safe_hook_sha256s_initialized) {
       		safe_hook_sha256s_initialized = 1;
     ++
     ++		/* Hard-code known-safe values for Git LFS v3.4.0..v3.5.1 */
     ++		/* pre-push */
     ++		strset_add(&safe_hook_sha256s, "df5417b2daa3aa144c19681d1e997df7ebfe144fb7e3e05138bd80ae998008e4");
     ++		/* post-checkout */
     ++		strset_add(&safe_hook_sha256s, "791471b4ff472aab844a4fceaa48bbb0a12193616f971e8e940625498b4938a6");
     ++		/* post-commit */
     ++		strset_add(&safe_hook_sha256s, "21e961572bb3f43a5f2fbafc1cc764d86046cc2e5f0bbecebfe9684a0b73b664");
     ++		/* post-merge */
     ++		strset_add(&safe_hook_sha256s, "75da0da66a803b4b030ad50801ba57062c6196105eb1d2251590d100edb9390b");
     ++
       		git_protected_config(safe_hook_cb, &safe_hook_sha256s);
     + 	}
     + 
      
       ## t/t1800-hook.sh ##
      @@ t/t1800-hook.sh: test_expect_success '`safe.hook.sha256` and clone protections' '
 7:  c487bd06be8 = 7:  bd6d72625f5 hooks(clone protections): simplify templates hooks validation
 8:  c45c33d8e3f = 8:  4b0a636d41a Revert "Add a helper function to compare file contents"

-- 
gitgitgadget


^ permalink raw reply	[flat|nested] 54+ messages in thread

* [PATCH v2 1/8] hook: plug a new memory leak
  2024-05-18 10:32 ` [PATCH v2 " Johannes Schindelin via GitGitGadget
@ 2024-05-18 10:32   ` Johannes Schindelin via GitGitGadget
  2024-05-18 10:32   ` [PATCH v2 2/8] init: use the correct path of the templates directory again Johannes Schindelin via GitGitGadget
                     ` (8 subsequent siblings)
  9 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-18 10:32 UTC (permalink / raw
  To: git; +Cc: brian m. carlson, Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

In 8db1e8743c0 (clone: prevent hooks from running during a clone,
2024-03-28), I introduced an inadvertent memory leak that was
unfortunately not caught before v2.45.1 was released. Here is a fix.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 hook.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/hook.c b/hook.c
index 632b537b993..fc974cee1d8 100644
--- a/hook.c
+++ b/hook.c
@@ -18,8 +18,10 @@ static int identical_to_template_hook(const char *name, const char *path)
 		found_template_hook = access(template_path.buf, X_OK) >= 0;
 	}
 #endif
-	if (!found_template_hook)
+	if (!found_template_hook) {
+		strbuf_release(&template_path);
 		return 0;
+	}
 
 	ret = do_files_match(template_path.buf, path);
 
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH v2 2/8] init: use the correct path of the templates directory again
  2024-05-18 10:32 ` [PATCH v2 " Johannes Schindelin via GitGitGadget
  2024-05-18 10:32   ` [PATCH v2 1/8] hook: plug a new memory leak Johannes Schindelin via GitGitGadget
@ 2024-05-18 10:32   ` Johannes Schindelin via GitGitGadget
  2024-05-18 10:32   ` [PATCH v2 3/8] Revert "core.hooksPath: add some protection while cloning" Johannes Schindelin via GitGitGadget
                     ` (7 subsequent siblings)
  9 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-18 10:32 UTC (permalink / raw
  To: git; +Cc: brian m. carlson, Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

In df93e407f06 (init: refactor the template directory discovery into its
own function, 2024-03-29), I refactored the way the templates directory
is discovered.

The refactoring was faithful, but missed a reference in the `Makefile`
where the `DEFAULT_GIT_TEMPLATE_DIR` constant is defined. As a
consequence, Git v2.45.1 and friends will always use the hard-coded path
`/usr/share/git-core/templates`.

Let's fix that by defining the `DEFAULT_GIT_TEMPLATE_DIR` when building
`setup.o`, where that constant is actually used.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Makefile b/Makefile
index 093829ae283..4b1502ba2c6 100644
--- a/Makefile
+++ b/Makefile
@@ -2751,7 +2751,7 @@ exec-cmd.sp exec-cmd.s exec-cmd.o: EXTRA_CPPFLAGS = \
 	'-DFALLBACK_RUNTIME_PREFIX="$(prefix_SQ)"'
 
 builtin/init-db.sp builtin/init-db.s builtin/init-db.o: GIT-PREFIX
-builtin/init-db.sp builtin/init-db.s builtin/init-db.o: EXTRA_CPPFLAGS = \
+setup.sp setup.s setup.o: EXTRA_CPPFLAGS = \
 	-DDEFAULT_GIT_TEMPLATE_DIR='"$(template_dir_SQ)"'
 
 config.sp config.s config.o: GIT-PREFIX
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH v2 3/8] Revert "core.hooksPath: add some protection while cloning"
  2024-05-18 10:32 ` [PATCH v2 " Johannes Schindelin via GitGitGadget
  2024-05-18 10:32   ` [PATCH v2 1/8] hook: plug a new memory leak Johannes Schindelin via GitGitGadget
  2024-05-18 10:32   ` [PATCH v2 2/8] init: use the correct path of the templates directory again Johannes Schindelin via GitGitGadget
@ 2024-05-18 10:32   ` Johannes Schindelin via GitGitGadget
  2024-05-18 10:32   ` [PATCH v2 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again Johannes Schindelin via GitGitGadget
                     ` (6 subsequent siblings)
  9 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-18 10:32 UTC (permalink / raw
  To: git; +Cc: brian m. carlson, Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

This defense-in-depth was intended to protect the clone operation
against future escalations where bugs in `git clone` would allow
attackers to write arbitrary files in the `.git/` directory would allow
for Remote Code Execution attacks via maliciously-placed hooks.

However, it turns out that the `core.hooksPath` protection has
unintentional side effects so severe that they do not justify the
benefit of the protections. For example, it has been reported in
https://lore.kernel.org/git/FAFA34CB-9732-4A0A-87FB-BDB272E6AEE8@alchemists.io/
that the following invocation, which is intended to make `git clone`
safer, is itself broken by that protective measure:

	git clone --config core.hooksPath=/dev/null <url>

Since it turns out that the benefit does not justify the cost, let's revert
20f3588efc6 (core.hooksPath: add some protection while cloning,
2024-03-30).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 config.c        | 13 +------------
 t/t1800-hook.sh | 15 ---------------
 2 files changed, 1 insertion(+), 27 deletions(-)

diff --git a/config.c b/config.c
index 85b37f2ee09..8c1c4071f0d 100644
--- a/config.c
+++ b/config.c
@@ -1525,19 +1525,8 @@ static int git_default_core_config(const char *var, const char *value, void *cb)
 	if (!strcmp(var, "core.attributesfile"))
 		return git_config_pathname(&git_attributes_file, var, value);
 
-	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);
+	if (!strcmp(var, "core.hookspath"))
 		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/t/t1800-hook.sh b/t/t1800-hook.sh
index 7ee12e6f48a..2ef3579fa7c 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -177,19 +177,4 @@ test_expect_success 'git hook run a hook with a bad shebang' '
 	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
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH v2 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again
  2024-05-18 10:32 ` [PATCH v2 " Johannes Schindelin via GitGitGadget
                     ` (2 preceding siblings ...)
  2024-05-18 10:32   ` [PATCH v2 3/8] Revert "core.hooksPath: add some protection while cloning" Johannes Schindelin via GitGitGadget
@ 2024-05-18 10:32   ` Johannes Schindelin via GitGitGadget
  2024-05-18 10:32   ` [PATCH v2 5/8] hook(clone protections): add escape hatch Johannes Schindelin via GitGitGadget
                     ` (5 subsequent siblings)
  9 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-18 10:32 UTC (permalink / raw
  To: git; +Cc: brian m. carlson, Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

As part of the protections added in Git v2.45.1 and friends,
repository-local `core.hooksPath` settings are no longer allowed, as a
defense-in-depth mechanism to prevent future Git vulnerabilities to
raise to critical level if those vulnerabilities inadvertently allow the
repository-local config to be written.

What the added protection did not anticipate is that such a
repository-local `core.hooksPath` can not only be used to point to
maliciously-placed scripts in the current worktree, but also to
_prevent_ hooks from being called altogether.

We just reverted the `core.hooksPath` protections, based on the Git
maintainer's recommendation in
https://lore.kernel.org/git/xmqq4jaxvm8z.fsf@gitster.g/ to address this
concern as well as related ones. Let's make sure that we won't regress
while trying to protect the clone operation further.

Reported-by: Brooke Kuhlmann <brooke@alchemists.io>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 t/t1350-config-hooks-path.sh | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh
index f6dc83e2aab..45a04929170 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
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH v2 5/8] hook(clone protections): add escape hatch
  2024-05-18 10:32 ` [PATCH v2 " Johannes Schindelin via GitGitGadget
                     ` (3 preceding siblings ...)
  2024-05-18 10:32   ` [PATCH v2 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again Johannes Schindelin via GitGitGadget
@ 2024-05-18 10:32   ` Johannes Schindelin via GitGitGadget
  2024-05-18 18:14     ` Jeff King
  2024-05-18 10:32   ` [PATCH v2 6/8] hooks(clone protections): special-case current Git LFS hooks Johannes Schindelin via GitGitGadget
                     ` (4 subsequent siblings)
  9 siblings, 1 reply; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-18 10:32 UTC (permalink / raw
  To: git; +Cc: brian m. carlson, Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

As defense-in-depth measures, v2.39.4 and friends leading up to v2.45.1
introduced code that detects when hooks have been installed during a
`git clone`, which is indicative of a common attack vector with critical
severity that allows Remote Code Execution.

There are legitimate use cases for such behavior, though, for example
when those hooks stem from Git's own templates, which system
administrators are at liberty to modify to enforce, say, commit message
conventions. The git clone protections specifically add exceptions to
allow for that.

Another legitimate use case that has been identified too late to be
handled in these security bug-fix versions is Git LFS: It behaves
somewhat similar to common attack vectors by writing a few hooks while
running the `smudge` filter during a regular clone, which means that Git
has no chance to know that the hooks are benign and e.g. the
`post-checkout` hook can be safely executed as part of the clone
operation.

To help Git LFS, and other tools behaving similarly (if there are any),
let's add a new, multi-valued `safe.hook.sha256` config setting. Like
the already-existing `safe.*` settings, it is ignored in
repository-local configs, and it is interpreted as a list of SHA-256
checksums of hooks' contents that are safe to execute during a clone
operation. Future Git LFS versions will need to write those entries at
the same time they install the `smudge`/`clean` filters.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 Documentation/config/safe.txt |  6 ++++
 hook.c                        | 66 ++++++++++++++++++++++++++++++++---
 t/t1800-hook.sh               | 15 ++++++++
 3 files changed, 82 insertions(+), 5 deletions(-)

diff --git a/Documentation/config/safe.txt b/Documentation/config/safe.txt
index bde7f31459b..69ee845be89 100644
--- a/Documentation/config/safe.txt
+++ b/Documentation/config/safe.txt
@@ -59,3 +59,9 @@ which id the original user has.
 If that is not what you would prefer and want git to only trust
 repositories that are owned by root instead, then you can remove
 the `SUDO_UID` variable from root's environment before invoking git.
+
+safe.hook.sha256::
+	The value is the SHA-256 of hooks that are considered to be safe
+	to run during a clone operation.
++
+Multiple values can be added via `git config --global --add`.
diff --git a/hook.c b/hook.c
index fc974cee1d8..a2479738451 100644
--- a/hook.c
+++ b/hook.c
@@ -2,6 +2,7 @@
 #include "hook.h"
 #include "run-command.h"
 #include "config.h"
+#include "strmap.h"
 
 static int identical_to_template_hook(const char *name, const char *path)
 {
@@ -29,11 +30,65 @@ static int identical_to_template_hook(const char *name, const char *path)
 	return ret;
 }
 
+static struct strset safe_hook_sha256s = STRSET_INIT;
+static int safe_hook_sha256s_initialized;
+
+static int get_sha256_of_file_contents(const char *path, char *sha256)
+{
+	struct strbuf sb = STRBUF_INIT;
+	int fd;
+	ssize_t res;
+
+	git_hash_ctx ctx;
+	const struct git_hash_algo *algo = &hash_algos[GIT_HASH_SHA256];
+	unsigned char hash[GIT_MAX_RAWSZ];
+
+	if ((fd = open(path, O_RDONLY)) < 0)
+		return -1;
+	res = strbuf_read(&sb, fd, 400);
+	close(fd);
+	if (res < 0)
+		return -1;
+
+	algo->init_fn(&ctx);
+	algo->update_fn(&ctx, sb.buf, sb.len);
+	strbuf_release(&sb);
+	algo->final_fn(hash, &ctx);
+
+	hash_to_hex_algop_r(sha256, hash, algo);
+
+	return 0;
+}
+
+static int safe_hook_cb(const char *key, const char *value, void *d)
+{
+	struct strset *set = d;
+
+	if (value && !strcmp(key, "safe.hook.sha256"))
+		strset_add(set, value);
+
+	return 0;
+}
+
+static int is_hook_safe_during_clone(const char *name, const char *path, char *sha256)
+{
+	if (get_sha256_of_file_contents(path, sha256) < 0)
+		return 0;
+
+	if (!safe_hook_sha256s_initialized) {
+		safe_hook_sha256s_initialized = 1;
+		git_protected_config(safe_hook_cb, &safe_hook_sha256s);
+	}
+
+	return strset_contains(&safe_hook_sha256s, sha256);
+}
+
 const char *find_hook(const char *name)
 {
 	static struct strbuf path = STRBUF_INIT;
 
 	int found_hook;
+	char sha256[GIT_SHA256_HEXSZ + 1] = { '\0' };
 
 	strbuf_reset(&path);
 	strbuf_git_path(&path, "hooks/%s", name);
@@ -65,13 +120,14 @@ const char *find_hook(const char *name)
 		return NULL;
 	}
 	if (!git_hooks_path && git_env_bool("GIT_CLONE_PROTECTION_ACTIVE", 0) &&
-	    !identical_to_template_hook(name, path.buf))
+	    !identical_to_template_hook(name, path.buf) &&
+	    !is_hook_safe_during_clone(name, path.buf, sha256))
 		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);
+		      "If this is intentional and the hook is safe to run, "
+		      "please run the following command and try again:\n\n"
+		      "  git config --global --add safe.hook.sha256 %s"),
+		    name, path.buf, sha256);
 	return path.buf;
 }
 
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index 2ef3579fa7c..0f74c9154d0 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -177,4 +177,19 @@ test_expect_success 'git hook run a hook with a bad shebang' '
 	test_cmp expect actual
 '
 
+test_expect_success '`safe.hook.sha256` and clone protections' '
+	git init safe-hook &&
+	write_script safe-hook/.git/hooks/pre-push <<-\EOF &&
+	echo "called hook" >safe-hook.log
+	EOF
+
+	test_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \
+		git -C safe-hook hook run pre-push 2>err &&
+	cmd="$(grep "git config --global --add safe.hook.sha256 [0-9a-f]" err)" &&
+	eval "$cmd" &&
+	GIT_CLONE_PROTECTION_ACTIVE=true \
+		git -C safe-hook hook run pre-push &&
+	test "called hook" = "$(cat safe-hook/safe-hook.log)"
+'
+
 test_done
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH v2 6/8] hooks(clone protections): special-case current Git LFS hooks
  2024-05-18 10:32 ` [PATCH v2 " Johannes Schindelin via GitGitGadget
                     ` (4 preceding siblings ...)
  2024-05-18 10:32   ` [PATCH v2 5/8] hook(clone protections): add escape hatch Johannes Schindelin via GitGitGadget
@ 2024-05-18 10:32   ` Johannes Schindelin via GitGitGadget
  2024-05-18 10:32   ` [PATCH v2 7/8] hooks(clone protections): simplify templates hooks validation Johannes Schindelin via GitGitGadget
                     ` (3 subsequent siblings)
  9 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-18 10:32 UTC (permalink / raw
  To: git; +Cc: brian m. carlson, Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

A notable regression in v2.45.1 and friends (all the way down to
v2.39.4) has been that Git LFS-enabled clones error out with a message
indicating that the `post-checkout` hook has been tampered with while
cloning, and as a safety measure it is not executed.

A generic fix for benign third-party applications wishing to write hooks
during clone operations has been implemented in the parent of this
commit: said applications are expected to add `safe.hook.sha256` values
to a protected config.

However, the current version of Git LFS, v3.5.1, cannot be adapted
retroactively; Therefore, let's just hard-code the SHA-256 values for
this version. That way, Git LFS usage will no longer be broken, and the
next Git LFS version can be taught to add those `safe.hook.sha256`
entries.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 hook.c          | 11 +++++++++++
 t/t1800-hook.sh | 20 ++++++++++++++++++++
 2 files changed, 31 insertions(+)

diff --git a/hook.c b/hook.c
index a2479738451..f81b13df142 100644
--- a/hook.c
+++ b/hook.c
@@ -77,6 +77,17 @@ static int is_hook_safe_during_clone(const char *name, const char *path, char *s
 
 	if (!safe_hook_sha256s_initialized) {
 		safe_hook_sha256s_initialized = 1;
+
+		/* Hard-code known-safe values for Git LFS v3.4.0..v3.5.1 */
+		/* pre-push */
+		strset_add(&safe_hook_sha256s, "df5417b2daa3aa144c19681d1e997df7ebfe144fb7e3e05138bd80ae998008e4");
+		/* post-checkout */
+		strset_add(&safe_hook_sha256s, "791471b4ff472aab844a4fceaa48bbb0a12193616f971e8e940625498b4938a6");
+		/* post-commit */
+		strset_add(&safe_hook_sha256s, "21e961572bb3f43a5f2fbafc1cc764d86046cc2e5f0bbecebfe9684a0b73b664");
+		/* post-merge */
+		strset_add(&safe_hook_sha256s, "75da0da66a803b4b030ad50801ba57062c6196105eb1d2251590d100edb9390b");
+
 		git_protected_config(safe_hook_cb, &safe_hook_sha256s);
 	}
 
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index 0f74c9154d0..af66999aff3 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -192,4 +192,24 @@ test_expect_success '`safe.hook.sha256` and clone protections' '
 	test "called hook" = "$(cat safe-hook/safe-hook.log)"
 '
 
+write_lfs_pre_push_hook () {
+	write_script "$1" <<-\EOF
+	command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; }
+	git lfs pre-push "$@"
+	EOF
+}
+
+test_expect_success 'Git LFS special-handling in clone protections' '
+	git init lfs-hooks &&
+	write_lfs_pre_push_hook lfs-hooks/.git/hooks/pre-push &&
+	write_script git-lfs <<-\EOF &&
+	echo "called $*" >fake-git-lfs.log
+	EOF
+
+	PATH="$PWD:$PATH" GIT_CLONE_PROTECTION_ACTIVE=true \
+		git -C lfs-hooks hook run pre-push &&
+	test_write_lines "called pre-push" >expect &&
+	test_cmp lfs-hooks/fake-git-lfs.log expect
+'
+
 test_done
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH v2 7/8] hooks(clone protections): simplify templates hooks validation
  2024-05-18 10:32 ` [PATCH v2 " Johannes Schindelin via GitGitGadget
                     ` (5 preceding siblings ...)
  2024-05-18 10:32   ` [PATCH v2 6/8] hooks(clone protections): special-case current Git LFS hooks Johannes Schindelin via GitGitGadget
@ 2024-05-18 10:32   ` Johannes Schindelin via GitGitGadget
  2024-05-18 10:32   ` [PATCH v2 8/8] Revert "Add a helper function to compare file contents" Johannes Schindelin via GitGitGadget
                     ` (2 subsequent siblings)
  9 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-18 10:32 UTC (permalink / raw
  To: git; +Cc: brian m. carlson, Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

When an active hook is encountered during a clone operation, to protect
against Remote Code Execution attack vectors, Git checks whether the
hook was copied over from the templates directory.

When that logic was introduced, there was no other way to check this
than to add a function to compare files.

In the meantime, we've added code to compute the SHA-256 checksum of a
given hook and compare that checksum against a list of known-safe ones.

Let's simplify the logic by adding to said list when copying the
templates' hooks.

We need to be careful to support multi-process operations such as
recursive submodule clones: In such a scenario, the list of SHA-256
checksums that is kept in memory is not enough, we also have to pass the
information down to child processes via `GIT_CONFIG_PARAMETERS`.

Extend the regression test in t5601 to ensure that recursive clones are
handled as expected.

Note: Technically there is no way that the checksums computed while
initializing the submodules' gitdirs can be passed to the process that
performs the checkout: For historical reasons, these operations are
performed in processes spawned in separate loops from the
super-project's `git clone` process. But since the templates from which
the submodules are initialized are the very same as the ones from which
the super-project is initialized, we can get away with using the list of
SHA-256 checksums that is computed when initializing the super-project
and passing that down to the `submodule--helper` processes that perform
the recursive checkout.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/init-db.c |  7 +++++++
 hook.c            | 43 ++++++++++++++++---------------------------
 hook.h            | 10 ++++++++++
 setup.c           |  1 +
 t/t5601-clone.sh  | 19 +++++++++++++++++++
 5 files changed, 53 insertions(+), 27 deletions(-)

diff --git a/builtin/init-db.c b/builtin/init-db.c
index a101e7f94c1..64357fdada4 100644
--- a/builtin/init-db.c
+++ b/builtin/init-db.c
@@ -10,6 +10,8 @@
 #include "exec-cmd.h"
 #include "parse-options.h"
 #include "worktree.h"
+#include "run-command.h"
+#include "hook.h"
 
 #ifdef NO_TRUSTABLE_FILEMODE
 #define TEST_FILEMODE 0
@@ -28,6 +30,7 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
 	size_t path_baselen = path->len;
 	size_t template_baselen = template_path->len;
 	struct dirent *de;
+	int is_hooks_dir = ends_with(template_path->buf, "/hooks/");
 
 	/* Note: if ".git/hooks" file exists in the repository being
 	 * re-initialized, /etc/core-git/templates/hooks/update would
@@ -80,6 +83,10 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
 			strbuf_release(&lnk);
 		}
 		else if (S_ISREG(st_template.st_mode)) {
+			if (is_hooks_dir &&
+			    is_executable(template_path->buf))
+				add_safe_hook(template_path->buf);
+
 			if (copy_file(path->buf, template_path->buf, st_template.st_mode))
 				die_errno(_("cannot copy '%s' to '%s'"),
 					  template_path->buf, path->buf);
diff --git a/hook.c b/hook.c
index f81b13df142..9e762cc9af6 100644
--- a/hook.c
+++ b/hook.c
@@ -4,32 +4,6 @@
 #include "config.h"
 #include "strmap.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) {
-		strbuf_release(&template_path);
-		return 0;
-	}
-
-	ret = do_files_match(template_path.buf, path);
-
-	strbuf_release(&template_path);
-	return ret;
-}
-
 static struct strset safe_hook_sha256s = STRSET_INIT;
 static int safe_hook_sha256s_initialized;
 
@@ -60,6 +34,22 @@ static int get_sha256_of_file_contents(const char *path, char *sha256)
 	return 0;
 }
 
+void add_safe_hook(const char *path)
+{
+	char sha256[GIT_SHA256_HEXSZ + 1] = { '\0' };
+
+	if (!get_sha256_of_file_contents(path, sha256)) {
+		char *p;
+
+		strset_add(&safe_hook_sha256s, sha256);
+
+		/* support multi-process operations e.g. recursive clones */
+		p = xstrfmt("safe.hook.sha256=%s", sha256);
+		git_config_push_parameter(p);
+		free(p);
+	}
+}
+
 static int safe_hook_cb(const char *key, const char *value, void *d)
 {
 	struct strset *set = d;
@@ -131,7 +121,6 @@ const char *find_hook(const char *name)
 		return NULL;
 	}
 	if (!git_hooks_path && git_env_bool("GIT_CLONE_PROTECTION_ACTIVE", 0) &&
-	    !identical_to_template_hook(name, path.buf) &&
 	    !is_hook_safe_during_clone(name, path.buf, sha256))
 		die(_("active `%s` hook found during `git clone`:\n\t%s\n"
 		      "For security reasons, this is disallowed by default.\n"
diff --git a/hook.h b/hook.h
index 4258b13da0d..e2034ee8b23 100644
--- a/hook.h
+++ b/hook.h
@@ -82,4 +82,14 @@ int run_hooks(const char *hook_name);
  * hook. This function behaves like the old run_hook_le() API.
  */
 int run_hooks_l(const char *hook_name, ...);
+
+/**
+ * Mark the contents of the provided path as safe to run during a clone
+ * operation.
+ *
+ * This function is mainly used when copying templates to mark the
+ * just-copied hooks as benign.
+ */
+void add_safe_hook(const char *path);
+
 #endif
diff --git a/setup.c b/setup.c
index c3301f5ab82..7f7538c9bf7 100644
--- a/setup.c
+++ b/setup.c
@@ -7,6 +7,7 @@
 #include "promisor-remote.h"
 #include "quote.h"
 #include "exec-cmd.h"
+#include "hook.h"
 
 static int inside_git_dir = -1;
 static int inside_work_tree = -1;
diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh
index 20deca0231b..71eaa3d1e14 100755
--- a/t/t5601-clone.sh
+++ b/t/t5601-clone.sh
@@ -819,6 +819,25 @@ test_expect_success 'clone with init.templatedir runs hooks' '
 		git config --unset init.templateDir &&
 		! grep "active .* hook found" err &&
 		test_path_is_missing hook-run-local-config/hook.run
+	) &&
+
+	test_config_global protocol.file.allow always &&
+	git -C tmpl/hooks submodule add "$(pwd)/tmpl/hooks" sub &&
+	test_tick &&
+	git -C tmpl/hooks add .gitmodules sub &&
+	git -C tmpl/hooks commit -m submodule &&
+
+	(
+		sane_unset GIT_TEMPLATE_DIR &&
+		NO_SET_GIT_TEMPLATE_DIR=t &&
+		export NO_SET_GIT_TEMPLATE_DIR &&
+
+		git -c init.templateDir="$(pwd)/tmpl" \
+			clone --recurse-submodules \
+			tmpl/hooks hook-run-submodule 2>err &&
+		! grep "active .* hook found" err &&
+		test_path_is_file hook-run-submodule/hook.run &&
+		test_path_is_file hook-run-submodule/sub/hook.run
 	)
 '
 
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH v2 8/8] Revert "Add a helper function to compare file contents"
  2024-05-18 10:32 ` [PATCH v2 " Johannes Schindelin via GitGitGadget
                     ` (6 preceding siblings ...)
  2024-05-18 10:32   ` [PATCH v2 7/8] hooks(clone protections): simplify templates hooks validation Johannes Schindelin via GitGitGadget
@ 2024-05-18 10:32   ` Johannes Schindelin via GitGitGadget
  2024-05-18 17:07   ` [PATCH v2 0/8] Various fixes for v2.45.1 and friends Junio C Hamano
  2024-05-20 20:21   ` [PATCH v3 0/6] " Johannes Schindelin via GitGitGadget
  9 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-18 10:32 UTC (permalink / raw
  To: git; +Cc: brian m. carlson, Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

Now that during a `git clone`, the hooks' contents are no longer
compared to the templates' files', the caller for which the
`do_files_match()` function was introduced is gone, and therefore this
function can be retired, too.

This reverts commit 584de0b4c23 (Add a helper function to compare file
contents, 2024-03-30).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 cache.h                    | 14 ---------
 copy.c                     | 58 --------------------------------------
 t/helper/test-path-utils.c | 10 -------
 t/t0060-path-utils.sh      | 41 ---------------------------
 4 files changed, 123 deletions(-)

diff --git a/cache.h b/cache.h
index 16b34799bfd..8c5fb1e1ba1 100644
--- a/cache.h
+++ b/cache.h
@@ -1785,20 +1785,6 @@ int copy_fd(int ifd, int ofd);
 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);
-
 void write_or_die(int fd, const void *buf, size_t count);
 void fsync_or_die(int fd, const char *);
 int fsync_component(enum fsync_component component, int fd);
diff --git a/copy.c b/copy.c
index 8492f6fc831..4de6a110f09 100644
--- a/copy.c
+++ b/copy.c
@@ -65,61 +65,3 @@ int copy_file_with_time(const char *dst, const char *src, int mode)
 		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/t/helper/test-path-utils.c b/t/helper/test-path-utils.c
index 0e0de218076..f69709d674f 100644
--- a/t/helper/test-path-utils.c
+++ b/t/helper/test-path-utils.c
@@ -495,16 +495,6 @@ int cmd__path_utils(int argc, const char **argv)
 		return !!res;
 	}
 
-	if (argc == 4 && !strcmp(argv[1], "do_files_match")) {
-		int ret = do_files_match(argv[2], argv[3]);
-
-		if (ret)
-			printf("equal\n");
-		else
-			printf("different\n");
-		return !ret;
-	}
-
 	fprintf(stderr, "%s: unknown function name: %s\n", argv[0],
 		argv[1] ? argv[1] : "(there was none)");
 	return 1;
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index 73d0e1a7f10..68e29c904a6 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -560,45 +560,4 @@ test_expect_success !VALGRIND,RUNTIME_PREFIX,CAN_EXEC_IN_PWD '%(prefix)/ works'
 	test_cmp expect actual
 '
 
-test_expect_success 'do_files_match()' '
-	test_seq 0 10 >0-10.txt &&
-	test_seq -1 10 >-1-10.txt &&
-	test_seq 1 10 >1-10.txt &&
-	test_seq 1 9 >1-9.txt &&
-	test_seq 0 8 >0-8.txt &&
-
-	test-tool path-utils do_files_match 0-10.txt 0-10.txt >out &&
-
-	assert_fails() {
-		test_must_fail \
-		test-tool path-utils do_files_match "$1" "$2" >out &&
-		grep different out
-	} &&
-
-	assert_fails 0-8.txt 1-9.txt &&
-	assert_fails -1-10.txt 0-10.txt &&
-	assert_fails 1-10.txt 1-9.txt &&
-	assert_fails 1-10.txt .git &&
-	assert_fails does-not-exist 1-10.txt &&
-
-	if test_have_prereq FILEMODE
-	then
-		cp 0-10.txt 0-10.x &&
-		chmod a+x 0-10.x &&
-		assert_fails 0-10.txt 0-10.x
-	fi &&
-
-	if test_have_prereq SYMLINKS
-	then
-		ln -sf 0-10.txt symlink &&
-		ln -s 0-10.txt another-symlink &&
-		ln -s over-the-ocean yet-another-symlink &&
-		ln -s "$PWD/0-10.txt" absolute-symlink &&
-		assert_fails 0-10.txt symlink &&
-		test-tool path-utils do_files_match symlink another-symlink &&
-		assert_fails symlink yet-another-symlink &&
-		assert_fails symlink absolute-symlink
-	fi
-'
-
 test_done
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 0/8] Various fixes for v2.45.1 and friends
  2024-05-18 10:32 ` [PATCH v2 " Johannes Schindelin via GitGitGadget
                     ` (7 preceding siblings ...)
  2024-05-18 10:32   ` [PATCH v2 8/8] Revert "Add a helper function to compare file contents" Johannes Schindelin via GitGitGadget
@ 2024-05-18 17:07   ` Junio C Hamano
  2024-05-18 19:22     ` Johannes Schindelin
  2024-05-20 20:21   ` [PATCH v3 0/6] " Johannes Schindelin via GitGitGadget
  9 siblings, 1 reply; 54+ messages in thread
From: Junio C Hamano @ 2024-05-18 17:07 UTC (permalink / raw
  To: git
  Cc: Johannes Schindelin via GitGitGadget, brian m. carlson,
	Johannes Schindelin

I have applied this to maint-2.39 and then merged them up to the
maintenance tracks.  The results will be pushed out to the "split
out" repository at

    https://github.com/gitster/git/

as these branches:

    js/fix-clone-w-hooks-2.39
    js/fix-clone-w-hooks-2.40
    js/fix-clone-w-hooks-2.41
    js/fix-clone-w-hooks-2.42
    js/fix-clone-w-hooks-2.43
    js/fix-clone-w-hooks-2.44
    js/fix-clone-w-hooks-2.45

For those who are merging them up at home yourselves, here are the
remerge diff that show the conflict resolution I made.

* js/fix-clone-w-hooks-2.40
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
remerge CONFLICT (content): Merge conflict in t/t1800-hook.sh
index 2af93d130d..bc0ed30ab9 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -177,7 +177,6 @@ test_expect_success 'git hook run a hook with a bad shebang' '
 	test_cmp expect actual
 '
 
-<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< b9b439e0e3 (Git 2.40.2)
 test_expect_success 'stdin to hooks' '
 	write_script .git/hooks/test-hook <<-\EOF &&
 	echo BEGIN stdin
@@ -196,23 +195,10 @@ test_expect_success 'stdin to hooks' '
 	test_cmp expect actual
 '
 
-test_expect_success 'clone protections' '
-	test_config core.hooksPath "$(pwd)/my-hooks" &&
-	mkdir -p my-hooks &&
-	write_script my-hooks/test-hook <<-\EOF &&
-	echo Hook ran $1
-|||||||||||||||||||||||||||||||| 47b6d90e91
-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
-================================
 test_expect_success '`safe.hook.sha256` and clone protections' '
 	git init safe-hook &&
 	write_script safe-hook/.git/hooks/pre-push <<-\EOF &&
 	echo "called hook" >safe-hook.log
->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 851218a8af (Revert "Add a helper function to compare file contents")
 	EOF
 
 	test_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \
* js/fix-clone-w-hooks-2.41
diff --git a/builtin/init-db.c b/builtin/init-db.c
remerge CONFLICT (content): Merge conflict in builtin/init-db.c
index 0f0d2033c5..b351fe6e40 100644
--- a/builtin/init-db.c
+++ b/builtin/init-db.c
@@ -17,13 +17,9 @@
 #include "path.h"
 #include "setup.h"
 #include "worktree.h"
-<<<<<<< 0f15832059 (Git 2.41.1)
 #include "wrapper.h"
-||||||| b9b439e0e3
-=======
 #include "run-command.h"
 #include "hook.h"
->>>>>>> 752e921355 (Merge branch 'js/fix-clone-w-hooks-2.39' into HEAD)
 
 #ifdef NO_TRUSTABLE_FILEMODE
 #define TEST_FILEMODE 0
diff --git a/cache.h b/cache.h
remerge CONFLICT (content): Merge conflict in cache.h
index d99735c623..bdedb87e83 100644
--- a/cache.h
+++ b/cache.h
@@ -555,1301 +555,7 @@ extern int verify_ce_order;
 #define DATA_CHANGED    0x0020
 #define TYPE_CHANGED    0x0040
 
-<<<<<<< 0f15832059 (Git 2.41.1)
 int cmp_cache_name_compare(const void *a_, const void *b_);
-||||||| b9b439e0e3
-/*
- * Return an abbreviated sha1 unique within this repository's object database.
- * The result will be at least `len` characters long, and will be NUL
- * terminated.
- *
- * The non-`_r` version returns a static buffer which remains valid until 4
- * more calls to find_unique_abbrev are made.
- *
- * The `_r` variant writes to a buffer supplied by the caller, which must be at
- * least `GIT_MAX_HEXSZ + 1` bytes. The return value is the number of bytes
- * written (excluding the NUL terminator).
- *
- * Note that while this version avoids the static buffer, it is not fully
- * reentrant, as it calls into other non-reentrant git code.
- */
-const char *repo_find_unique_abbrev(struct repository *r, const struct object_id *oid, int len);
-#define find_unique_abbrev(oid, len) repo_find_unique_abbrev(the_repository, oid, len)
-int repo_find_unique_abbrev_r(struct repository *r, char *hex, const struct object_id *oid, int len);
-#define find_unique_abbrev_r(hex, oid, len) repo_find_unique_abbrev_r(the_repository, hex, oid, len)
-
-/* set default permissions by passing mode arguments to open(2) */
-int git_mkstemps_mode(char *pattern, int suffix_len, int mode);
-int git_mkstemp_mode(char *pattern, int mode);
-
-/*
- * NOTE NOTE NOTE!!
- *
- * PERM_UMASK, OLD_PERM_GROUP and OLD_PERM_EVERYBODY enumerations must
- * not be changed. Old repositories have core.sharedrepository written in
- * numeric format, and therefore these values are preserved for compatibility
- * reasons.
- */
-enum sharedrepo {
-	PERM_UMASK          = 0,
-	OLD_PERM_GROUP      = 1,
-	OLD_PERM_EVERYBODY  = 2,
-	PERM_GROUP          = 0660,
-	PERM_EVERYBODY      = 0664
-};
-int git_config_perm(const char *var, const char *value);
-int adjust_shared_perm(const char *path);
-
-/*
- * Create the directory containing the named path, using care to be
- * somewhat safe against races. Return one of the scld_error values to
- * indicate success/failure. On error, set errno to describe the
- * problem.
- *
- * SCLD_VANISHED indicates that one of the ancestor directories of the
- * path existed at one point during the function call and then
- * suddenly vanished, probably because another process pruned the
- * directory while we were working.  To be robust against this kind of
- * race, callers might want to try invoking the function again when it
- * returns SCLD_VANISHED.
- *
- * safe_create_leading_directories() temporarily changes path while it
- * is working but restores it before returning.
- * safe_create_leading_directories_const() doesn't modify path, even
- * temporarily. Both these variants adjust the permissions of the
- * created directories to honor core.sharedRepository, so they are best
- * suited for files inside the git dir. For working tree files, use
- * safe_create_leading_directories_no_share() instead, as it ignores
- * the core.sharedRepository setting.
- */
-enum scld_error {
-	SCLD_OK = 0,
-	SCLD_FAILED = -1,
-	SCLD_PERMS = -2,
-	SCLD_EXISTS = -3,
-	SCLD_VANISHED = -4
-};
-enum scld_error safe_create_leading_directories(char *path);
-enum scld_error safe_create_leading_directories_const(const char *path);
-enum scld_error safe_create_leading_directories_no_share(char *path);
-
-int mkdir_in_gitdir(const char *path);
-char *interpolate_path(const char *path, int real_home);
-/* NEEDSWORK: remove this synonym once in-flight topics have migrated */
-#define expand_user_path interpolate_path
-const char *enter_repo(const char *path, int strict);
-static inline int is_absolute_path(const char *path)
-{
-	return is_dir_sep(path[0]) || has_dos_drive_prefix(path);
-}
-int is_directory(const char *);
-char *strbuf_realpath(struct strbuf *resolved, const char *path,
-		      int die_on_error);
-char *strbuf_realpath_forgiving(struct strbuf *resolved, const char *path,
-				int die_on_error);
-char *real_pathdup(const char *path, int die_on_error);
-const char *absolute_path(const char *path);
-char *absolute_pathdup(const char *path);
-const char *remove_leading_path(const char *in, const char *prefix);
-const char *relative_path(const char *in, const char *prefix, struct strbuf *sb);
-int normalize_path_copy_len(char *dst, const char *src, int *prefix_len);
-int normalize_path_copy(char *dst, const char *src);
-int longest_ancestor_length(const char *path, struct string_list *prefixes);
-char *strip_path_suffix(const char *path, const char *suffix);
-int daemon_avoid_alias(const char *path);
-
-/*
- * These functions match their is_hfs_dotgit() counterparts; see utf8.h for
- * details.
- */
-int is_ntfs_dotgit(const char *name);
-int is_ntfs_dotgitmodules(const char *name);
-int is_ntfs_dotgitignore(const char *name);
-int is_ntfs_dotgitattributes(const char *name);
-int is_ntfs_dotmailmap(const char *name);
-
-/*
- * Returns true iff "str" could be confused as a command-line option when
- * passed to a sub-program like "ssh". Note that this has nothing to do with
- * shell-quoting, which should be handled separately; we're assuming here that
- * the string makes it verbatim to the sub-program.
- */
-int looks_like_command_line_option(const char *str);
-
-/**
- * Return a newly allocated string with the evaluation of
- * "$XDG_CONFIG_HOME/$subdir/$filename" if $XDG_CONFIG_HOME is non-empty, otherwise
- * "$HOME/.config/$subdir/$filename". Return NULL upon error.
- */
-char *xdg_config_home_for(const char *subdir, const char *filename);
-
-/**
- * Return a newly allocated string with the evaluation of
- * "$XDG_CONFIG_HOME/git/$filename" if $XDG_CONFIG_HOME is non-empty, otherwise
- * "$HOME/.config/git/$filename". Return NULL upon error.
- */
-char *xdg_config_home(const char *filename);
-
-/**
- * Return a newly allocated string with the evaluation of
- * "$XDG_CACHE_HOME/git/$filename" if $XDG_CACHE_HOME is non-empty, otherwise
- * "$HOME/.cache/git/$filename". Return NULL upon error.
- */
-char *xdg_cache_home(const char *filename);
-
-int git_open_cloexec(const char *name, int flags);
-#define git_open(name) git_open_cloexec(name, O_RDONLY)
-
-/**
- * unpack_loose_header() initializes the data stream needed to unpack
- * a loose object header.
- *
- * Returns:
- *
- * - ULHR_OK on success
- * - ULHR_BAD on error
- * - ULHR_TOO_LONG if the header was too long
- *
- * It will only parse up to MAX_HEADER_LEN bytes unless an optional
- * "hdrbuf" argument is non-NULL. This is intended for use with
- * OBJECT_INFO_ALLOW_UNKNOWN_TYPE to extract the bad type for (error)
- * reporting. The full header will be extracted to "hdrbuf" for use
- * with parse_loose_header(), ULHR_TOO_LONG will still be returned
- * from this function to indicate that the header was too long.
- */
-enum unpack_loose_header_result {
-	ULHR_OK,
-	ULHR_BAD,
-	ULHR_TOO_LONG,
-};
-enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,
-						    unsigned char *map,
-						    unsigned long mapsize,
-						    void *buffer,
-						    unsigned long bufsiz,
-						    struct strbuf *hdrbuf);
-
-/**
- * parse_loose_header() parses the starting "<type> <len>\0" of an
- * object. If it doesn't follow that format -1 is returned. To check
- * the validity of the <type> populate the "typep" in the "struct
- * object_info". It will be OBJ_BAD if the object type is unknown. The
- * parsed <len> can be retrieved via "oi->sizep", and from there
- * passed to unpack_loose_rest().
- */
-struct object_info;
-int parse_loose_header(const char *hdr, struct object_info *oi);
-
-/**
- * With in-core object data in "buf", rehash it to make sure the
- * object name actually matches "oid" to detect object corruption.
- *
- * A negative value indicates an error, usually that the OID is not
- * what we expected, but it might also indicate another error.
- */
-int check_object_signature(struct repository *r, const struct object_id *oid,
-			   void *map, unsigned long size,
-			   enum object_type type);
-
-/**
- * A streaming version of check_object_signature().
- * Try reading the object named with "oid" using
- * the streaming interface and rehash it to do the same.
- */
-int stream_object_signature(struct repository *r, const struct object_id *oid);
-
-int finalize_object_file(const char *tmpfile, const char *filename);
-
-/* Helper to check and "touch" a file */
-int check_and_freshen_file(const char *fn, int freshen);
-
-extern const signed char hexval_table[256];
-static inline unsigned int hexval(unsigned char c)
-{
-	return hexval_table[c];
-}
-
-/*
- * Convert two consecutive hexadecimal digits into a char.  Return a
- * negative value on error.  Don't run over the end of short strings.
- */
-static inline int hex2chr(const char *s)
-{
-	unsigned int val = hexval(s[0]);
-	return (val & ~0xf) ? val : (val << 4) | hexval(s[1]);
-}
-
-/* Convert to/from hex/sha1 representation */
-#define MINIMUM_ABBREV minimum_abbrev
-#define DEFAULT_ABBREV default_abbrev
-
-/* used when the code does not know or care what the default abbrev is */
-#define FALLBACK_DEFAULT_ABBREV 7
-
-struct object_context {
-	unsigned short mode;
-	/*
-	 * symlink_path is only used by get_tree_entry_follow_symlinks,
-	 * and only for symlinks that point outside the repository.
-	 */
-	struct strbuf symlink_path;
-	/*
-	 * If GET_OID_RECORD_PATH is set, this will record path (if any)
-	 * found when resolving the name. The caller is responsible for
-	 * releasing the memory.
-	 */
-	char *path;
-};
-
-#define GET_OID_QUIETLY           01
-#define GET_OID_COMMIT            02
-#define GET_OID_COMMITTISH        04
-#define GET_OID_TREE             010
-#define GET_OID_TREEISH          020
-#define GET_OID_BLOB             040
-#define GET_OID_FOLLOW_SYMLINKS 0100
-#define GET_OID_RECORD_PATH     0200
-#define GET_OID_ONLY_TO_DIE    04000
-#define GET_OID_REQUIRE_PATH  010000
-
-#define GET_OID_DISAMBIGUATORS \
-	(GET_OID_COMMIT | GET_OID_COMMITTISH | \
-	GET_OID_TREE | GET_OID_TREEISH | \
-	GET_OID_BLOB)
-
-enum get_oid_result {
-	FOUND = 0,
-	MISSING_OBJECT = -1, /* The requested object is missing */
-	SHORT_NAME_AMBIGUOUS = -2,
-	/* The following only apply when symlinks are followed */
-	DANGLING_SYMLINK = -4, /*
-				* The initial symlink is there, but
-				* (transitively) points to a missing
-				* in-tree file
-				*/
-	SYMLINK_LOOP = -5,
-	NOT_DIR = -6, /*
-		       * Somewhere along the symlink chain, a path is
-		       * requested which contains a file as a
-		       * non-final element.
-		       */
-};
-
-int repo_get_oid(struct repository *r, const char *str, struct object_id *oid);
-__attribute__((format (printf, 2, 3)))
-int get_oidf(struct object_id *oid, const char *fmt, ...);
-int repo_get_oid_commit(struct repository *r, const char *str, struct object_id *oid);
-int repo_get_oid_committish(struct repository *r, const char *str, struct object_id *oid);
-int repo_get_oid_tree(struct repository *r, const char *str, struct object_id *oid);
-int repo_get_oid_treeish(struct repository *r, const char *str, struct object_id *oid);
-int repo_get_oid_blob(struct repository *r, const char *str, struct object_id *oid);
-int repo_get_oid_mb(struct repository *r, const char *str, struct object_id *oid);
-void maybe_die_on_misspelt_object_name(struct repository *repo,
-				       const char *name,
-				       const char *prefix);
-enum get_oid_result get_oid_with_context(struct repository *repo, const char *str,
-					 unsigned flags, struct object_id *oid,
-					 struct object_context *oc);
-
-#define get_oid(str, oid)		repo_get_oid(the_repository, str, oid)
-#define get_oid_commit(str, oid)	repo_get_oid_commit(the_repository, str, oid)
-#define get_oid_committish(str, oid)	repo_get_oid_committish(the_repository, str, oid)
-#define get_oid_tree(str, oid)		repo_get_oid_tree(the_repository, str, oid)
-#define get_oid_treeish(str, oid)	repo_get_oid_treeish(the_repository, str, oid)
-#define get_oid_blob(str, oid)		repo_get_oid_blob(the_repository, str, oid)
-#define get_oid_mb(str, oid) 		repo_get_oid_mb(the_repository, str, oid)
-
-typedef int each_abbrev_fn(const struct object_id *oid, void *);
-int repo_for_each_abbrev(struct repository *r, const char *prefix, each_abbrev_fn, void *);
-#define for_each_abbrev(prefix, fn, data) repo_for_each_abbrev(the_repository, prefix, fn, data)
-
-int set_disambiguate_hint_config(const char *var, const char *value);
-
-/*
- * Try to read a SHA1 in hexadecimal format from the 40 characters
- * starting at hex.  Write the 20-byte result to sha1 in binary form.
- * Return 0 on success.  Reading stops if a NUL is encountered in the
- * input, so it is safe to pass this function an arbitrary
- * null-terminated string.
- */
-int get_sha1_hex(const char *hex, unsigned char *sha1);
-int get_oid_hex(const char *hex, struct object_id *sha1);
-
-/* Like get_oid_hex, but for an arbitrary hash algorithm. */
-int get_oid_hex_algop(const char *hex, struct object_id *oid, const struct git_hash_algo *algop);
-
-/*
- * Read `len` pairs of hexadecimal digits from `hex` and write the
- * values to `binary` as `len` bytes. Return 0 on success, or -1 if
- * the input does not consist of hex digits).
- */
-int hex_to_bytes(unsigned char *binary, const char *hex, size_t len);
-
-/*
- * Convert a binary hash in "unsigned char []" or an object name in
- * "struct object_id *" to its hex equivalent. The `_r` variant is reentrant,
- * and writes the NUL-terminated output to the buffer `out`, which must be at
- * least `GIT_MAX_HEXSZ + 1` bytes, and returns a pointer to out for
- * convenience.
- *
- * The non-`_r` variant returns a static buffer, but uses a ring of 4
- * buffers, making it safe to make multiple calls for a single statement, like:
- *
- *   printf("%s -> %s", hash_to_hex(one), hash_to_hex(two));
- *   printf("%s -> %s", oid_to_hex(one), oid_to_hex(two));
- */
-char *hash_to_hex_algop_r(char *buffer, const unsigned char *hash, const struct git_hash_algo *);
-char *oid_to_hex_r(char *out, const struct object_id *oid);
-char *hash_to_hex_algop(const unsigned char *hash, const struct git_hash_algo *);	/* static buffer result! */
-char *hash_to_hex(const unsigned char *hash);						/* same static buffer */
-char *oid_to_hex(const struct object_id *oid);						/* same static buffer */
-
-/*
- * Parse a 40-character hexadecimal object ID starting from hex, updating the
- * pointer specified by end when parsing stops.  The resulting object ID is
- * stored in oid.  Returns 0 on success.  Parsing will stop on the first NUL or
- * other invalid character.  end is only updated on success; otherwise, it is
- * unmodified.
- */
-int parse_oid_hex(const char *hex, struct object_id *oid, const char **end);
-
-/* Like parse_oid_hex, but for an arbitrary hash algorithm. */
-int parse_oid_hex_algop(const char *hex, struct object_id *oid, const char **end,
-			const struct git_hash_algo *algo);
-
-
-/*
- * These functions work like get_oid_hex and parse_oid_hex, but they will parse
- * a hex value for any algorithm. The algorithm is detected based on the length
- * and the algorithm in use is returned. If this is not a hex object ID in any
- * algorithm, returns GIT_HASH_UNKNOWN.
- */
-int get_oid_hex_any(const char *hex, struct object_id *oid);
-int parse_oid_hex_any(const char *hex, struct object_id *oid, const char **end);
-
-/*
- * This reads short-hand syntax that not only evaluates to a commit
- * object name, but also can act as if the end user spelled the name
- * of the branch from the command line.
- *
- * - "@{-N}" finds the name of the Nth previous branch we were on, and
- *   places the name of the branch in the given buf and returns the
- *   number of characters parsed if successful.
- *
- * - "<branch>@{upstream}" finds the name of the other ref that
- *   <branch> is configured to merge with (missing <branch> defaults
- *   to the current branch), and places the name of the branch in the
- *   given buf and returns the number of characters parsed if
- *   successful.
- *
- * If the input is not of the accepted format, it returns a negative
- * number to signal an error.
- *
- * If the input was ok but there are not N branch switches in the
- * reflog, it returns 0.
- */
-#define INTERPRET_BRANCH_LOCAL (1<<0)
-#define INTERPRET_BRANCH_REMOTE (1<<1)
-#define INTERPRET_BRANCH_HEAD (1<<2)
-struct interpret_branch_name_options {
-	/*
-	 * If "allowed" is non-zero, it is a treated as a bitfield of allowable
-	 * expansions: local branches ("refs/heads/"), remote branches
-	 * ("refs/remotes/"), or "HEAD". If no "allowed" bits are set, any expansion is
-	 * allowed, even ones to refs outside of those namespaces.
-	 */
-	unsigned allowed;
-
-	/*
-	 * If ^{upstream} or ^{push} (or equivalent) is requested, and the
-	 * branch in question does not have such a reference, return -1 instead
-	 * of die()-ing.
-	 */
-	unsigned nonfatal_dangling_mark : 1;
-};
-int repo_interpret_branch_name(struct repository *r,
-			       const char *str, int len,
-			       struct strbuf *buf,
-			       const struct interpret_branch_name_options *options);
-#define interpret_branch_name(str, len, buf, options) \
-	repo_interpret_branch_name(the_repository, str, len, buf, options)
-
-int validate_headref(const char *ref);
-
-int base_name_compare(const char *name1, size_t len1, int mode1,
-		      const char *name2, size_t len2, int mode2);
-int df_name_compare(const char *name1, size_t len1, int mode1,
-		    const char *name2, size_t len2, int mode2);
-int name_compare(const char *name1, size_t len1, const char *name2, size_t len2);
-int cache_name_stage_compare(const char *name1, int len1, int stage1, const char *name2, int len2, int stage2);
-
-void *read_object_with_reference(struct repository *r,
-				 const struct object_id *oid,
-				 enum object_type required_type,
-				 unsigned long *size,
-				 struct object_id *oid_ret);
-
-struct object *repo_peel_to_type(struct repository *r,
-				 const char *name, int namelen,
-				 struct object *o, enum object_type);
-#define peel_to_type(name, namelen, obj, type) \
-	repo_peel_to_type(the_repository, name, namelen, obj, type)
-
-#define IDENT_STRICT	       1
-#define IDENT_NO_DATE	       2
-#define IDENT_NO_NAME	       4
-
-enum want_ident {
-	WANT_BLANK_IDENT,
-	WANT_AUTHOR_IDENT,
-	WANT_COMMITTER_IDENT
-};
-
-const char *git_author_info(int);
-const char *git_committer_info(int);
-const char *fmt_ident(const char *name, const char *email,
-		      enum want_ident whose_ident,
-		      const char *date_str, int);
-const char *fmt_name(enum want_ident);
-const char *ident_default_name(void);
-const char *ident_default_email(void);
-const char *git_editor(void);
-const char *git_sequence_editor(void);
-const char *git_pager(int stdout_is_tty);
-int is_terminal_dumb(void);
-int git_ident_config(const char *, const char *, void *);
-/*
- * Prepare an ident to fall back on if the user didn't configure it.
- */
-void prepare_fallback_ident(const char *name, const char *email);
-void reset_ident_date(void);
-
-struct ident_split {
-	const char *name_begin;
-	const char *name_end;
-	const char *mail_begin;
-	const char *mail_end;
-	const char *date_begin;
-	const char *date_end;
-	const char *tz_begin;
-	const char *tz_end;
-};
-/*
- * Signals an success with 0, but time part of the result may be NULL
- * if the input lacks timestamp and zone
- */
-int split_ident_line(struct ident_split *, const char *, int);
-
-/*
- * Given a commit or tag object buffer and the commit or tag headers, replaces
- * the idents in the headers with their canonical versions using the mailmap mechanism.
- */
-void apply_mailmap_to_header(struct strbuf *, const char **, struct string_list *);
-
-/*
- * Compare split idents for equality or strict ordering. Note that we
- * compare only the ident part of the line, ignoring any timestamp.
- *
- * Because there are two fields, we must choose one as the primary key; we
- * currently arbitrarily pick the email.
- */
-int ident_cmp(const struct ident_split *, const struct ident_split *);
-
-struct cache_def {
-	struct strbuf path;
-	int flags;
-	int track_flags;
-	int prefix_len_stat_func;
-};
-#define CACHE_DEF_INIT { \
-	.path = STRBUF_INIT, \
-}
-static inline void cache_def_clear(struct cache_def *cache)
-{
-	strbuf_release(&cache->path);
-}
-
-int has_symlink_leading_path(const char *name, int len);
-int threaded_has_symlink_leading_path(struct cache_def *, const char *, int);
-int check_leading_path(const char *name, int len, int warn_on_lstat_err);
-int has_dirs_only_path(const char *name, int len, int prefix_len);
-void invalidate_lstat_cache(void);
-void schedule_dir_for_removal(const char *name, int len);
-void remove_scheduled_dirs(void);
-
-struct pack_window {
-	struct pack_window *next;
-	unsigned char *base;
-	off_t offset;
-	size_t len;
-	unsigned int last_used;
-	unsigned int inuse_cnt;
-};
-
-struct pack_entry {
-	off_t offset;
-	struct packed_git *p;
-};
-
-/*
- * Create a temporary file rooted in the object database directory, or
- * die on failure. The filename is taken from "pattern", which should have the
- * usual "XXXXXX" trailer, and the resulting filename is written into the
- * "template" buffer. Returns the open descriptor.
- */
-int odb_mkstemp(struct strbuf *temp_filename, const char *pattern);
-
-/*
- * Create a pack .keep file named "name" (which should generally be the output
- * of odb_pack_name). Returns a file descriptor opened for writing, or -1 on
- * error.
- */
-int odb_pack_keep(const char *name);
-
-/*
- * Set this to 0 to prevent oid_object_info_extended() from fetching missing
- * blobs. This has a difference only if extensions.partialClone is set.
- *
- * Its default value is 1.
- */
-extern int fetch_if_missing;
-
-/* Dumb servers support */
-int update_server_info(int);
-
-const char *get_log_output_encoding(void);
-const char *get_commit_output_encoding(void);
-
-int committer_ident_sufficiently_given(void);
-int author_ident_sufficiently_given(void);
-
-extern const char *git_commit_encoding;
-extern const char *git_log_output_encoding;
-extern const char *git_mailmap_file;
-extern const char *git_mailmap_blob;
-
-/* IO helper functions */
-void maybe_flush_or_die(FILE *, const char *);
-__attribute__((format (printf, 2, 3)))
-void fprintf_or_die(FILE *, const char *fmt, ...);
-void fwrite_or_die(FILE *f, const void *buf, size_t count);
-void fflush_or_die(FILE *f);
-
-#define COPY_READ_ERROR (-2)
-#define COPY_WRITE_ERROR (-3)
-int copy_fd(int ifd, int ofd);
-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);
-
-void write_or_die(int fd, const void *buf, size_t count);
-void fsync_or_die(int fd, const char *);
-int fsync_component(enum fsync_component component, int fd);
-void fsync_component_or_die(enum fsync_component component, int fd, const char *msg);
-
-static inline int batch_fsync_enabled(enum fsync_component component)
-{
-	return (fsync_components & component) && (fsync_method == FSYNC_METHOD_BATCH);
-}
-
-ssize_t read_in_full(int fd, void *buf, size_t count);
-ssize_t write_in_full(int fd, const void *buf, size_t count);
-ssize_t pread_in_full(int fd, void *buf, size_t count, off_t offset);
-
-static inline ssize_t write_str_in_full(int fd, const char *str)
-{
-	return write_in_full(fd, str, strlen(str));
-}
-
-/**
- * Open (and truncate) the file at path, write the contents of buf to it,
- * and close it. Dies if any errors are encountered.
- */
-void write_file_buf(const char *path, const char *buf, size_t len);
-
-/**
- * Like write_file_buf(), but format the contents into a buffer first.
- * Additionally, write_file() will append a newline if one is not already
- * present, making it convenient to write text files:
- *
- *   write_file(path, "counter: %d", ctr);
- */
-__attribute__((format (printf, 2, 3)))
-void write_file(const char *path, const char *fmt, ...);
-
-/* pager.c */
-void setup_pager(void);
-int pager_in_use(void);
-extern int pager_use_color;
-int term_columns(void);
-void term_clear_line(void);
-int decimal_width(uintmax_t);
-int check_pager_config(const char *cmd);
-void prepare_pager_args(struct child_process *, const char *pager);
-
-extern const char *editor_program;
-extern const char *askpass_program;
-extern const char *excludes_file;
-
-/* base85 */
-int decode_85(char *dst, const char *line, int linelen);
-void encode_85(char *buf, const unsigned char *data, int bytes);
-
-/* pkt-line.c */
-void packet_trace_identity(const char *prog);
-=======
-/*
- * Return an abbreviated sha1 unique within this repository's object database.
- * The result will be at least `len` characters long, and will be NUL
- * terminated.
- *
- * The non-`_r` version returns a static buffer which remains valid until 4
- * more calls to find_unique_abbrev are made.
- *
- * The `_r` variant writes to a buffer supplied by the caller, which must be at
- * least `GIT_MAX_HEXSZ + 1` bytes. The return value is the number of bytes
- * written (excluding the NUL terminator).
- *
- * Note that while this version avoids the static buffer, it is not fully
- * reentrant, as it calls into other non-reentrant git code.
- */
-const char *repo_find_unique_abbrev(struct repository *r, const struct object_id *oid, int len);
-#define find_unique_abbrev(oid, len) repo_find_unique_abbrev(the_repository, oid, len)
-int repo_find_unique_abbrev_r(struct repository *r, char *hex, const struct object_id *oid, int len);
-#define find_unique_abbrev_r(hex, oid, len) repo_find_unique_abbrev_r(the_repository, hex, oid, len)
-
-/* set default permissions by passing mode arguments to open(2) */
-int git_mkstemps_mode(char *pattern, int suffix_len, int mode);
-int git_mkstemp_mode(char *pattern, int mode);
-
-/*
- * NOTE NOTE NOTE!!
- *
- * PERM_UMASK, OLD_PERM_GROUP and OLD_PERM_EVERYBODY enumerations must
- * not be changed. Old repositories have core.sharedrepository written in
- * numeric format, and therefore these values are preserved for compatibility
- * reasons.
- */
-enum sharedrepo {
-	PERM_UMASK          = 0,
-	OLD_PERM_GROUP      = 1,
-	OLD_PERM_EVERYBODY  = 2,
-	PERM_GROUP          = 0660,
-	PERM_EVERYBODY      = 0664
-};
-int git_config_perm(const char *var, const char *value);
-int adjust_shared_perm(const char *path);
-
-/*
- * Create the directory containing the named path, using care to be
- * somewhat safe against races. Return one of the scld_error values to
- * indicate success/failure. On error, set errno to describe the
- * problem.
- *
- * SCLD_VANISHED indicates that one of the ancestor directories of the
- * path existed at one point during the function call and then
- * suddenly vanished, probably because another process pruned the
- * directory while we were working.  To be robust against this kind of
- * race, callers might want to try invoking the function again when it
- * returns SCLD_VANISHED.
- *
- * safe_create_leading_directories() temporarily changes path while it
- * is working but restores it before returning.
- * safe_create_leading_directories_const() doesn't modify path, even
- * temporarily. Both these variants adjust the permissions of the
- * created directories to honor core.sharedRepository, so they are best
- * suited for files inside the git dir. For working tree files, use
- * safe_create_leading_directories_no_share() instead, as it ignores
- * the core.sharedRepository setting.
- */
-enum scld_error {
-	SCLD_OK = 0,
-	SCLD_FAILED = -1,
-	SCLD_PERMS = -2,
-	SCLD_EXISTS = -3,
-	SCLD_VANISHED = -4
-};
-enum scld_error safe_create_leading_directories(char *path);
-enum scld_error safe_create_leading_directories_const(const char *path);
-enum scld_error safe_create_leading_directories_no_share(char *path);
-
-int mkdir_in_gitdir(const char *path);
-char *interpolate_path(const char *path, int real_home);
-/* NEEDSWORK: remove this synonym once in-flight topics have migrated */
-#define expand_user_path interpolate_path
-const char *enter_repo(const char *path, int strict);
-static inline int is_absolute_path(const char *path)
-{
-	return is_dir_sep(path[0]) || has_dos_drive_prefix(path);
-}
-int is_directory(const char *);
-char *strbuf_realpath(struct strbuf *resolved, const char *path,
-		      int die_on_error);
-char *strbuf_realpath_forgiving(struct strbuf *resolved, const char *path,
-				int die_on_error);
-char *real_pathdup(const char *path, int die_on_error);
-const char *absolute_path(const char *path);
-char *absolute_pathdup(const char *path);
-const char *remove_leading_path(const char *in, const char *prefix);
-const char *relative_path(const char *in, const char *prefix, struct strbuf *sb);
-int normalize_path_copy_len(char *dst, const char *src, int *prefix_len);
-int normalize_path_copy(char *dst, const char *src);
-int longest_ancestor_length(const char *path, struct string_list *prefixes);
-char *strip_path_suffix(const char *path, const char *suffix);
-int daemon_avoid_alias(const char *path);
-
-/*
- * These functions match their is_hfs_dotgit() counterparts; see utf8.h for
- * details.
- */
-int is_ntfs_dotgit(const char *name);
-int is_ntfs_dotgitmodules(const char *name);
-int is_ntfs_dotgitignore(const char *name);
-int is_ntfs_dotgitattributes(const char *name);
-int is_ntfs_dotmailmap(const char *name);
-
-/*
- * Returns true iff "str" could be confused as a command-line option when
- * passed to a sub-program like "ssh". Note that this has nothing to do with
- * shell-quoting, which should be handled separately; we're assuming here that
- * the string makes it verbatim to the sub-program.
- */
-int looks_like_command_line_option(const char *str);
-
-/**
- * Return a newly allocated string with the evaluation of
- * "$XDG_CONFIG_HOME/$subdir/$filename" if $XDG_CONFIG_HOME is non-empty, otherwise
- * "$HOME/.config/$subdir/$filename". Return NULL upon error.
- */
-char *xdg_config_home_for(const char *subdir, const char *filename);
-
-/**
- * Return a newly allocated string with the evaluation of
- * "$XDG_CONFIG_HOME/git/$filename" if $XDG_CONFIG_HOME is non-empty, otherwise
- * "$HOME/.config/git/$filename". Return NULL upon error.
- */
-char *xdg_config_home(const char *filename);
-
-/**
- * Return a newly allocated string with the evaluation of
- * "$XDG_CACHE_HOME/git/$filename" if $XDG_CACHE_HOME is non-empty, otherwise
- * "$HOME/.cache/git/$filename". Return NULL upon error.
- */
-char *xdg_cache_home(const char *filename);
-
-int git_open_cloexec(const char *name, int flags);
-#define git_open(name) git_open_cloexec(name, O_RDONLY)
-
-/**
- * unpack_loose_header() initializes the data stream needed to unpack
- * a loose object header.
- *
- * Returns:
- *
- * - ULHR_OK on success
- * - ULHR_BAD on error
- * - ULHR_TOO_LONG if the header was too long
- *
- * It will only parse up to MAX_HEADER_LEN bytes unless an optional
- * "hdrbuf" argument is non-NULL. This is intended for use with
- * OBJECT_INFO_ALLOW_UNKNOWN_TYPE to extract the bad type for (error)
- * reporting. The full header will be extracted to "hdrbuf" for use
- * with parse_loose_header(), ULHR_TOO_LONG will still be returned
- * from this function to indicate that the header was too long.
- */
-enum unpack_loose_header_result {
-	ULHR_OK,
-	ULHR_BAD,
-	ULHR_TOO_LONG,
-};
-enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,
-						    unsigned char *map,
-						    unsigned long mapsize,
-						    void *buffer,
-						    unsigned long bufsiz,
-						    struct strbuf *hdrbuf);
-
-/**
- * parse_loose_header() parses the starting "<type> <len>\0" of an
- * object. If it doesn't follow that format -1 is returned. To check
- * the validity of the <type> populate the "typep" in the "struct
- * object_info". It will be OBJ_BAD if the object type is unknown. The
- * parsed <len> can be retrieved via "oi->sizep", and from there
- * passed to unpack_loose_rest().
- */
-struct object_info;
-int parse_loose_header(const char *hdr, struct object_info *oi);
-
-/**
- * With in-core object data in "buf", rehash it to make sure the
- * object name actually matches "oid" to detect object corruption.
- *
- * A negative value indicates an error, usually that the OID is not
- * what we expected, but it might also indicate another error.
- */
-int check_object_signature(struct repository *r, const struct object_id *oid,
-			   void *map, unsigned long size,
-			   enum object_type type);
-
-/**
- * A streaming version of check_object_signature().
- * Try reading the object named with "oid" using
- * the streaming interface and rehash it to do the same.
- */
-int stream_object_signature(struct repository *r, const struct object_id *oid);
-
-int finalize_object_file(const char *tmpfile, const char *filename);
-
-/* Helper to check and "touch" a file */
-int check_and_freshen_file(const char *fn, int freshen);
-
-extern const signed char hexval_table[256];
-static inline unsigned int hexval(unsigned char c)
-{
-	return hexval_table[c];
-}
-
-/*
- * Convert two consecutive hexadecimal digits into a char.  Return a
- * negative value on error.  Don't run over the end of short strings.
- */
-static inline int hex2chr(const char *s)
-{
-	unsigned int val = hexval(s[0]);
-	return (val & ~0xf) ? val : (val << 4) | hexval(s[1]);
-}
-
-/* Convert to/from hex/sha1 representation */
-#define MINIMUM_ABBREV minimum_abbrev
-#define DEFAULT_ABBREV default_abbrev
-
-/* used when the code does not know or care what the default abbrev is */
-#define FALLBACK_DEFAULT_ABBREV 7
-
-struct object_context {
-	unsigned short mode;
-	/*
-	 * symlink_path is only used by get_tree_entry_follow_symlinks,
-	 * and only for symlinks that point outside the repository.
-	 */
-	struct strbuf symlink_path;
-	/*
-	 * If GET_OID_RECORD_PATH is set, this will record path (if any)
-	 * found when resolving the name. The caller is responsible for
-	 * releasing the memory.
-	 */
-	char *path;
-};
-
-#define GET_OID_QUIETLY           01
-#define GET_OID_COMMIT            02
-#define GET_OID_COMMITTISH        04
-#define GET_OID_TREE             010
-#define GET_OID_TREEISH          020
-#define GET_OID_BLOB             040
-#define GET_OID_FOLLOW_SYMLINKS 0100
-#define GET_OID_RECORD_PATH     0200
-#define GET_OID_ONLY_TO_DIE    04000
-#define GET_OID_REQUIRE_PATH  010000
-
-#define GET_OID_DISAMBIGUATORS \
-	(GET_OID_COMMIT | GET_OID_COMMITTISH | \
-	GET_OID_TREE | GET_OID_TREEISH | \
-	GET_OID_BLOB)
-
-enum get_oid_result {
-	FOUND = 0,
-	MISSING_OBJECT = -1, /* The requested object is missing */
-	SHORT_NAME_AMBIGUOUS = -2,
-	/* The following only apply when symlinks are followed */
-	DANGLING_SYMLINK = -4, /*
-				* The initial symlink is there, but
-				* (transitively) points to a missing
-				* in-tree file
-				*/
-	SYMLINK_LOOP = -5,
-	NOT_DIR = -6, /*
-		       * Somewhere along the symlink chain, a path is
-		       * requested which contains a file as a
-		       * non-final element.
-		       */
-};
-
-int repo_get_oid(struct repository *r, const char *str, struct object_id *oid);
-__attribute__((format (printf, 2, 3)))
-int get_oidf(struct object_id *oid, const char *fmt, ...);
-int repo_get_oid_commit(struct repository *r, const char *str, struct object_id *oid);
-int repo_get_oid_committish(struct repository *r, const char *str, struct object_id *oid);
-int repo_get_oid_tree(struct repository *r, const char *str, struct object_id *oid);
-int repo_get_oid_treeish(struct repository *r, const char *str, struct object_id *oid);
-int repo_get_oid_blob(struct repository *r, const char *str, struct object_id *oid);
-int repo_get_oid_mb(struct repository *r, const char *str, struct object_id *oid);
-void maybe_die_on_misspelt_object_name(struct repository *repo,
-				       const char *name,
-				       const char *prefix);
-enum get_oid_result get_oid_with_context(struct repository *repo, const char *str,
-					 unsigned flags, struct object_id *oid,
-					 struct object_context *oc);
-
-#define get_oid(str, oid)		repo_get_oid(the_repository, str, oid)
-#define get_oid_commit(str, oid)	repo_get_oid_commit(the_repository, str, oid)
-#define get_oid_committish(str, oid)	repo_get_oid_committish(the_repository, str, oid)
-#define get_oid_tree(str, oid)		repo_get_oid_tree(the_repository, str, oid)
-#define get_oid_treeish(str, oid)	repo_get_oid_treeish(the_repository, str, oid)
-#define get_oid_blob(str, oid)		repo_get_oid_blob(the_repository, str, oid)
-#define get_oid_mb(str, oid) 		repo_get_oid_mb(the_repository, str, oid)
-
-typedef int each_abbrev_fn(const struct object_id *oid, void *);
-int repo_for_each_abbrev(struct repository *r, const char *prefix, each_abbrev_fn, void *);
-#define for_each_abbrev(prefix, fn, data) repo_for_each_abbrev(the_repository, prefix, fn, data)
-
-int set_disambiguate_hint_config(const char *var, const char *value);
-
-/*
- * Try to read a SHA1 in hexadecimal format from the 40 characters
- * starting at hex.  Write the 20-byte result to sha1 in binary form.
- * Return 0 on success.  Reading stops if a NUL is encountered in the
- * input, so it is safe to pass this function an arbitrary
- * null-terminated string.
- */
-int get_sha1_hex(const char *hex, unsigned char *sha1);
-int get_oid_hex(const char *hex, struct object_id *sha1);
-
-/* Like get_oid_hex, but for an arbitrary hash algorithm. */
-int get_oid_hex_algop(const char *hex, struct object_id *oid, const struct git_hash_algo *algop);
-
-/*
- * Read `len` pairs of hexadecimal digits from `hex` and write the
- * values to `binary` as `len` bytes. Return 0 on success, or -1 if
- * the input does not consist of hex digits).
- */
-int hex_to_bytes(unsigned char *binary, const char *hex, size_t len);
-
-/*
- * Convert a binary hash in "unsigned char []" or an object name in
- * "struct object_id *" to its hex equivalent. The `_r` variant is reentrant,
- * and writes the NUL-terminated output to the buffer `out`, which must be at
- * least `GIT_MAX_HEXSZ + 1` bytes, and returns a pointer to out for
- * convenience.
- *
- * The non-`_r` variant returns a static buffer, but uses a ring of 4
- * buffers, making it safe to make multiple calls for a single statement, like:
- *
- *   printf("%s -> %s", hash_to_hex(one), hash_to_hex(two));
- *   printf("%s -> %s", oid_to_hex(one), oid_to_hex(two));
- */
-char *hash_to_hex_algop_r(char *buffer, const unsigned char *hash, const struct git_hash_algo *);
-char *oid_to_hex_r(char *out, const struct object_id *oid);
-char *hash_to_hex_algop(const unsigned char *hash, const struct git_hash_algo *);	/* static buffer result! */
-char *hash_to_hex(const unsigned char *hash);						/* same static buffer */
-char *oid_to_hex(const struct object_id *oid);						/* same static buffer */
-
-/*
- * Parse a 40-character hexadecimal object ID starting from hex, updating the
- * pointer specified by end when parsing stops.  The resulting object ID is
- * stored in oid.  Returns 0 on success.  Parsing will stop on the first NUL or
- * other invalid character.  end is only updated on success; otherwise, it is
- * unmodified.
- */
-int parse_oid_hex(const char *hex, struct object_id *oid, const char **end);
-
-/* Like parse_oid_hex, but for an arbitrary hash algorithm. */
-int parse_oid_hex_algop(const char *hex, struct object_id *oid, const char **end,
-			const struct git_hash_algo *algo);
-
-
-/*
- * These functions work like get_oid_hex and parse_oid_hex, but they will parse
- * a hex value for any algorithm. The algorithm is detected based on the length
- * and the algorithm in use is returned. If this is not a hex object ID in any
- * algorithm, returns GIT_HASH_UNKNOWN.
- */
-int get_oid_hex_any(const char *hex, struct object_id *oid);
-int parse_oid_hex_any(const char *hex, struct object_id *oid, const char **end);
-
-/*
- * This reads short-hand syntax that not only evaluates to a commit
- * object name, but also can act as if the end user spelled the name
- * of the branch from the command line.
- *
- * - "@{-N}" finds the name of the Nth previous branch we were on, and
- *   places the name of the branch in the given buf and returns the
- *   number of characters parsed if successful.
- *
- * - "<branch>@{upstream}" finds the name of the other ref that
- *   <branch> is configured to merge with (missing <branch> defaults
- *   to the current branch), and places the name of the branch in the
- *   given buf and returns the number of characters parsed if
- *   successful.
- *
- * If the input is not of the accepted format, it returns a negative
- * number to signal an error.
- *
- * If the input was ok but there are not N branch switches in the
- * reflog, it returns 0.
- */
-#define INTERPRET_BRANCH_LOCAL (1<<0)
-#define INTERPRET_BRANCH_REMOTE (1<<1)
-#define INTERPRET_BRANCH_HEAD (1<<2)
-struct interpret_branch_name_options {
-	/*
-	 * If "allowed" is non-zero, it is a treated as a bitfield of allowable
-	 * expansions: local branches ("refs/heads/"), remote branches
-	 * ("refs/remotes/"), or "HEAD". If no "allowed" bits are set, any expansion is
-	 * allowed, even ones to refs outside of those namespaces.
-	 */
-	unsigned allowed;
-
-	/*
-	 * If ^{upstream} or ^{push} (or equivalent) is requested, and the
-	 * branch in question does not have such a reference, return -1 instead
-	 * of die()-ing.
-	 */
-	unsigned nonfatal_dangling_mark : 1;
-};
-int repo_interpret_branch_name(struct repository *r,
-			       const char *str, int len,
-			       struct strbuf *buf,
-			       const struct interpret_branch_name_options *options);
-#define interpret_branch_name(str, len, buf, options) \
-	repo_interpret_branch_name(the_repository, str, len, buf, options)
-
-int validate_headref(const char *ref);
-
-int base_name_compare(const char *name1, size_t len1, int mode1,
-		      const char *name2, size_t len2, int mode2);
-int df_name_compare(const char *name1, size_t len1, int mode1,
-		    const char *name2, size_t len2, int mode2);
-int name_compare(const char *name1, size_t len1, const char *name2, size_t len2);
-int cache_name_stage_compare(const char *name1, int len1, int stage1, const char *name2, int len2, int stage2);
-
-void *read_object_with_reference(struct repository *r,
-				 const struct object_id *oid,
-				 enum object_type required_type,
-				 unsigned long *size,
-				 struct object_id *oid_ret);
-
-struct object *repo_peel_to_type(struct repository *r,
-				 const char *name, int namelen,
-				 struct object *o, enum object_type);
-#define peel_to_type(name, namelen, obj, type) \
-	repo_peel_to_type(the_repository, name, namelen, obj, type)
-
-#define IDENT_STRICT	       1
-#define IDENT_NO_DATE	       2
-#define IDENT_NO_NAME	       4
-
-enum want_ident {
-	WANT_BLANK_IDENT,
-	WANT_AUTHOR_IDENT,
-	WANT_COMMITTER_IDENT
-};
-
-const char *git_author_info(int);
-const char *git_committer_info(int);
-const char *fmt_ident(const char *name, const char *email,
-		      enum want_ident whose_ident,
-		      const char *date_str, int);
-const char *fmt_name(enum want_ident);
-const char *ident_default_name(void);
-const char *ident_default_email(void);
-const char *git_editor(void);
-const char *git_sequence_editor(void);
-const char *git_pager(int stdout_is_tty);
-int is_terminal_dumb(void);
-int git_ident_config(const char *, const char *, void *);
-/*
- * Prepare an ident to fall back on if the user didn't configure it.
- */
-void prepare_fallback_ident(const char *name, const char *email);
-void reset_ident_date(void);
-
-struct ident_split {
-	const char *name_begin;
-	const char *name_end;
-	const char *mail_begin;
-	const char *mail_end;
-	const char *date_begin;
-	const char *date_end;
-	const char *tz_begin;
-	const char *tz_end;
-};
-/*
- * Signals an success with 0, but time part of the result may be NULL
- * if the input lacks timestamp and zone
- */
-int split_ident_line(struct ident_split *, const char *, int);
-
-/*
- * Given a commit or tag object buffer and the commit or tag headers, replaces
- * the idents in the headers with their canonical versions using the mailmap mechanism.
- */
-void apply_mailmap_to_header(struct strbuf *, const char **, struct string_list *);
-
-/*
- * Compare split idents for equality or strict ordering. Note that we
- * compare only the ident part of the line, ignoring any timestamp.
- *
- * Because there are two fields, we must choose one as the primary key; we
- * currently arbitrarily pick the email.
- */
-int ident_cmp(const struct ident_split *, const struct ident_split *);
-
-struct cache_def {
-	struct strbuf path;
-	int flags;
-	int track_flags;
-	int prefix_len_stat_func;
-};
-#define CACHE_DEF_INIT { \
-	.path = STRBUF_INIT, \
-}
-static inline void cache_def_clear(struct cache_def *cache)
-{
-	strbuf_release(&cache->path);
-}
-
-int has_symlink_leading_path(const char *name, int len);
-int threaded_has_symlink_leading_path(struct cache_def *, const char *, int);
-int check_leading_path(const char *name, int len, int warn_on_lstat_err);
-int has_dirs_only_path(const char *name, int len, int prefix_len);
-void invalidate_lstat_cache(void);
-void schedule_dir_for_removal(const char *name, int len);
-void remove_scheduled_dirs(void);
-
-struct pack_window {
-	struct pack_window *next;
-	unsigned char *base;
-	off_t offset;
-	size_t len;
-	unsigned int last_used;
-	unsigned int inuse_cnt;
-};
-
-struct pack_entry {
-	off_t offset;
-	struct packed_git *p;
-};
-
-/*
- * Create a temporary file rooted in the object database directory, or
- * die on failure. The filename is taken from "pattern", which should have the
- * usual "XXXXXX" trailer, and the resulting filename is written into the
- * "template" buffer. Returns the open descriptor.
- */
-int odb_mkstemp(struct strbuf *temp_filename, const char *pattern);
-
-/*
- * Create a pack .keep file named "name" (which should generally be the output
- * of odb_pack_name). Returns a file descriptor opened for writing, or -1 on
- * error.
- */
-int odb_pack_keep(const char *name);
-
-/*
- * Set this to 0 to prevent oid_object_info_extended() from fetching missing
- * blobs. This has a difference only if extensions.partialClone is set.
- *
- * Its default value is 1.
- */
-extern int fetch_if_missing;
-
-/* Dumb servers support */
-int update_server_info(int);
-
-const char *get_log_output_encoding(void);
-const char *get_commit_output_encoding(void);
-
-int committer_ident_sufficiently_given(void);
-int author_ident_sufficiently_given(void);
-
-extern const char *git_commit_encoding;
-extern const char *git_log_output_encoding;
-extern const char *git_mailmap_file;
-extern const char *git_mailmap_blob;
-
-/* IO helper functions */
-void maybe_flush_or_die(FILE *, const char *);
-__attribute__((format (printf, 2, 3)))
-void fprintf_or_die(FILE *, const char *fmt, ...);
-void fwrite_or_die(FILE *f, const void *buf, size_t count);
-void fflush_or_die(FILE *f);
-
-#define COPY_READ_ERROR (-2)
-#define COPY_WRITE_ERROR (-3)
-int copy_fd(int ifd, int ofd);
-int copy_file(const char *dst, const char *src, int mode);
-int copy_file_with_time(const char *dst, const char *src, int mode);
-
-void write_or_die(int fd, const void *buf, size_t count);
-void fsync_or_die(int fd, const char *);
-int fsync_component(enum fsync_component component, int fd);
-void fsync_component_or_die(enum fsync_component component, int fd, const char *msg);
-
-static inline int batch_fsync_enabled(enum fsync_component component)
-{
-	return (fsync_components & component) && (fsync_method == FSYNC_METHOD_BATCH);
-}
-
-ssize_t read_in_full(int fd, void *buf, size_t count);
-ssize_t write_in_full(int fd, const void *buf, size_t count);
-ssize_t pread_in_full(int fd, void *buf, size_t count, off_t offset);
-
-static inline ssize_t write_str_in_full(int fd, const char *str)
-{
-	return write_in_full(fd, str, strlen(str));
-}
-
-/**
- * Open (and truncate) the file at path, write the contents of buf to it,
- * and close it. Dies if any errors are encountered.
- */
-void write_file_buf(const char *path, const char *buf, size_t len);
-
-/**
- * Like write_file_buf(), but format the contents into a buffer first.
- * Additionally, write_file() will append a newline if one is not already
- * present, making it convenient to write text files:
- *
- *   write_file(path, "counter: %d", ctr);
- */
-__attribute__((format (printf, 2, 3)))
-void write_file(const char *path, const char *fmt, ...);
-
-/* pager.c */
-void setup_pager(void);
-int pager_in_use(void);
-extern int pager_use_color;
-int term_columns(void);
-void term_clear_line(void);
-int decimal_width(uintmax_t);
-int check_pager_config(const char *cmd);
-void prepare_pager_args(struct child_process *, const char *pager);
-
-extern const char *editor_program;
-extern const char *askpass_program;
-extern const char *excludes_file;
-
-/* base85 */
-int decode_85(char *dst, const char *line, int linelen);
-void encode_85(char *buf, const unsigned char *data, int bytes);
-
-/* pkt-line.c */
-void packet_trace_identity(const char *prog);
->>>>>>> 752e921355 (Merge branch 'js/fix-clone-w-hooks-2.39' into HEAD)
 
 /* add */
 /*
diff --git a/hook.c b/hook.c
remerge CONFLICT (content): Merge conflict in hook.c
index ce71f9d01a..b56f97949b 100644
--- a/hook.c
+++ b/hook.c
@@ -5,15 +5,12 @@
 #include "path.h"
 #include "run-command.h"
 #include "config.h"
-<<<<<<< 0f15832059 (Git 2.41.1)
 #include "strbuf.h"
 #include "environment.h"
 #include "setup.h"
 #include "copy.h"
-||||||| b9b439e0e3
-=======
 #include "strmap.h"
->>>>>>> 752e921355 (Merge branch 'js/fix-clone-w-hooks-2.39' into HEAD)
+#include "hex.h"
 
 static struct strset safe_hook_sha256s = STRSET_INIT;
 static int safe_hook_sha256s_initialized;
* js/fix-clone-w-hooks-2.42
diff --git a/Makefile b/Makefile
remerge CONFLICT (content): Merge conflict in Makefile
index 5ae5f3c864..5776309365 100644
--- a/Makefile
+++ b/Makefile
@@ -2743,16 +2743,8 @@ exec-cmd.sp exec-cmd.s exec-cmd.o: EXTRA_CPPFLAGS = \
 	'-DBINDIR="$(bindir_relative_SQ)"' \
 	'-DFALLBACK_RUNTIME_PREFIX="$(prefix_SQ)"'
 
-<<<<<<< babb4e5d71 (Git 2.42.2)
 setup.sp setup.s setup.o: GIT-PREFIX
 setup.sp setup.s setup.o: EXTRA_CPPFLAGS = \
-||||||| 0f15832059
-builtin/init-db.sp builtin/init-db.s builtin/init-db.o: GIT-PREFIX
-builtin/init-db.sp builtin/init-db.s builtin/init-db.o: EXTRA_CPPFLAGS = \
-=======
-builtin/init-db.sp builtin/init-db.s builtin/init-db.o: GIT-PREFIX
-setup.sp setup.s setup.o: EXTRA_CPPFLAGS = \
->>>>>>> c7b6b0a28f (Merge branch 'js/fix-clone-w-hooks-2.40' into HEAD)
 	-DDEFAULT_GIT_TEMPLATE_DIR='"$(template_dir_SQ)"'
 
 config.sp config.s config.o: GIT-PREFIX
diff --git a/builtin/init-db.c b/builtin/init-db.c
remerge CONFLICT (content): Merge conflict in builtin/init-db.c
index 6df47dd078..cb727c826f 100644
--- a/builtin/init-db.c
+++ b/builtin/init-db.c
@@ -12,920 +12,7 @@
 #include "parse-options.h"
 #include "path.h"
 #include "setup.h"
-<<<<<<< babb4e5d71 (Git 2.42.2)
 #include "strbuf.h"
-||||||| 0f15832059
-#include "worktree.h"
-#include "wrapper.h"
-
-#ifdef NO_TRUSTABLE_FILEMODE
-#define TEST_FILEMODE 0
-#else
-#define TEST_FILEMODE 1
-#endif
-
-#define GIT_DEFAULT_HASH_ENVIRONMENT "GIT_DEFAULT_HASH"
-
-static int init_is_bare_repository = 0;
-static int init_shared_repository = -1;
-
-static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
-			     DIR *dir)
-{
-	size_t path_baselen = path->len;
-	size_t template_baselen = template_path->len;
-	struct dirent *de;
-
-	/* Note: if ".git/hooks" file exists in the repository being
-	 * re-initialized, /etc/core-git/templates/hooks/update would
-	 * cause "git init" to fail here.  I think this is sane but
-	 * it means that the set of templates we ship by default, along
-	 * with the way the namespace under .git/ is organized, should
-	 * be really carefully chosen.
-	 */
-	safe_create_dir(path->buf, 1);
-	while ((de = readdir(dir)) != NULL) {
-		struct stat st_git, st_template;
-		int exists = 0;
-
-		strbuf_setlen(path, path_baselen);
-		strbuf_setlen(template_path, template_baselen);
-
-		if (de->d_name[0] == '.')
-			continue;
-		strbuf_addstr(path, de->d_name);
-		strbuf_addstr(template_path, de->d_name);
-		if (lstat(path->buf, &st_git)) {
-			if (errno != ENOENT)
-				die_errno(_("cannot stat '%s'"), path->buf);
-		}
-		else
-			exists = 1;
-
-		if (lstat(template_path->buf, &st_template))
-			die_errno(_("cannot stat template '%s'"), template_path->buf);
-
-		if (S_ISDIR(st_template.st_mode)) {
-			DIR *subdir = opendir(template_path->buf);
-			if (!subdir)
-				die_errno(_("cannot opendir '%s'"), template_path->buf);
-			strbuf_addch(path, '/');
-			strbuf_addch(template_path, '/');
-			copy_templates_1(path, template_path, subdir);
-			closedir(subdir);
-		}
-		else if (exists)
-			continue;
-		else if (S_ISLNK(st_template.st_mode)) {
-			struct strbuf lnk = STRBUF_INIT;
-			if (strbuf_readlink(&lnk, template_path->buf,
-					    st_template.st_size) < 0)
-				die_errno(_("cannot readlink '%s'"), template_path->buf);
-			if (symlink(lnk.buf, path->buf))
-				die_errno(_("cannot symlink '%s' '%s'"),
-					  lnk.buf, path->buf);
-			strbuf_release(&lnk);
-		}
-		else if (S_ISREG(st_template.st_mode)) {
-			if (copy_file(path->buf, template_path->buf, st_template.st_mode))
-				die_errno(_("cannot copy '%s' to '%s'"),
-					  template_path->buf, path->buf);
-		}
-		else
-			error(_("ignoring template %s"), template_path->buf);
-	}
-}
-
-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;
-	struct repository_format template_format = REPOSITORY_FORMAT_INIT;
-	struct strbuf err = STRBUF_INIT;
-	DIR *dir;
-	char *to_free = NULL;
-
-	if (!template_dir || !*template_dir)
-		return;
-
-	strbuf_addstr(&template_path, template_dir);
-	strbuf_complete(&template_path, '/');
-	template_len = template_path.len;
-
-	dir = opendir(template_path.buf);
-	if (!dir) {
-		warning(_("templates not found in %s"), template_dir);
-		goto free_return;
-	}
-
-	/* Make sure that template is from the correct vintage */
-	strbuf_addstr(&template_path, "config");
-	read_repository_format(&template_format, template_path.buf);
-	strbuf_setlen(&template_path, template_len);
-
-	/*
-	 * No mention of version at all is OK, but anything else should be
-	 * verified.
-	 */
-	if (template_format.version >= 0 &&
-	    verify_repository_format(&template_format, &err) < 0) {
-		warning(_("not copying templates from '%s': %s"),
-			  template_dir, err.buf);
-		strbuf_release(&err);
-		goto close_free_return;
-	}
-
-	strbuf_addstr(&path, get_git_common_dir());
-	strbuf_complete(&path, '/');
-	copy_templates_1(&path, &template_path, dir);
-close_free_return:
-	closedir(dir);
-free_return:
-	free(to_free);
-	strbuf_release(&path);
-	strbuf_release(&template_path);
-	clear_repository_format(&template_format);
-}
-
-/*
- * If the git_dir is not directly inside the working tree, then git will not
- * find it by default, and we need to set the worktree explicitly.
- */
-static int needs_work_tree_config(const char *git_dir, const char *work_tree)
-{
-	if (!strcmp(work_tree, "/") && !strcmp(git_dir, "/.git"))
-		return 0;
-	if (skip_prefix(git_dir, work_tree, &git_dir) &&
-	    !strcmp(git_dir, "/.git"))
-		return 0;
-	return 1;
-}
-
-void initialize_repository_version(int hash_algo, int reinit)
-{
-	char repo_version_string[10];
-	int repo_version = GIT_REPO_VERSION;
-
-	if (hash_algo != GIT_HASH_SHA1)
-		repo_version = GIT_REPO_VERSION_READ;
-
-	/* This forces creation of new config file */
-	xsnprintf(repo_version_string, sizeof(repo_version_string),
-		  "%d", repo_version);
-	git_config_set("core.repositoryformatversion", repo_version_string);
-
-	if (hash_algo != GIT_HASH_SHA1)
-		git_config_set("extensions.objectformat",
-			       hash_algos[hash_algo].name);
-	else if (reinit)
-		git_config_set_gently("extensions.objectformat", NULL);
-}
-
-static int create_default_files(const char *template_path,
-				const char *original_git_dir,
-				const char *initial_branch,
-				const struct repository_format *fmt,
-				int quiet)
-{
-	struct stat st1;
-	struct strbuf buf = STRBUF_INIT;
-	char *path;
-	char junk[2];
-	int reinit;
-	int filemode;
-	struct strbuf err = STRBUF_INIT;
-	const char *work_tree = get_git_work_tree();
-
-	/*
-	 * First copy the templates -- we might have the default
-	 * config file there, in which case we would want to read
-	 * from it after installing.
-	 *
-	 * Before reading that config, we also need to clear out any cached
-	 * values (since we've just potentially changed what's available on
-	 * disk).
-	 */
-	copy_templates(template_path);
-	git_config_clear();
-	reset_shared_repository();
-	git_config(git_default_config, NULL);
-
-	/*
-	 * We must make sure command-line options continue to override any
-	 * values we might have just re-read from the config.
-	 */
-	is_bare_repository_cfg = init_is_bare_repository || !work_tree;
-	if (init_shared_repository != -1)
-		set_shared_repository(init_shared_repository);
-
-	/*
-	 * We would have created the above under user's umask -- under
-	 * shared-repository settings, we would need to fix them up.
-	 */
-	if (get_shared_repository()) {
-		adjust_shared_perm(get_git_dir());
-	}
-
-	/*
-	 * We need to create a "refs" dir in any case so that older
-	 * versions of git can tell that this is a repository.
-	 */
-	safe_create_dir(git_path("refs"), 1);
-	adjust_shared_perm(git_path("refs"));
-
-	if (refs_init_db(&err))
-		die("failed to set up refs db: %s", err.buf);
-
-	/*
-	 * Point the HEAD symref to the initial branch with if HEAD does
-	 * not yet exist.
-	 */
-	path = git_path_buf(&buf, "HEAD");
-	reinit = (!access(path, R_OK)
-		  || readlink(path, junk, sizeof(junk)-1) != -1);
-	if (!reinit) {
-		char *ref;
-
-		if (!initial_branch)
-			initial_branch = git_default_branch_name(quiet);
-
-		ref = xstrfmt("refs/heads/%s", initial_branch);
-		if (check_refname_format(ref, 0) < 0)
-			die(_("invalid initial branch name: '%s'"),
-			    initial_branch);
-
-		if (create_symref("HEAD", ref, NULL) < 0)
-			exit(1);
-		free(ref);
-	}
-
-	initialize_repository_version(fmt->hash_algo, 0);
-
-	/* Check filemode trustability */
-	path = git_path_buf(&buf, "config");
-	filemode = TEST_FILEMODE;
-	if (TEST_FILEMODE && !lstat(path, &st1)) {
-		struct stat st2;
-		filemode = (!chmod(path, st1.st_mode ^ S_IXUSR) &&
-				!lstat(path, &st2) &&
-				st1.st_mode != st2.st_mode &&
-				!chmod(path, st1.st_mode));
-		if (filemode && !reinit && (st1.st_mode & S_IXUSR))
-			filemode = 0;
-	}
-	git_config_set("core.filemode", filemode ? "true" : "false");
-
-	if (is_bare_repository())
-		git_config_set("core.bare", "true");
-	else {
-		git_config_set("core.bare", "false");
-		/* allow template config file to override the default */
-		if (log_all_ref_updates == LOG_REFS_UNSET)
-			git_config_set("core.logallrefupdates", "true");
-		if (needs_work_tree_config(original_git_dir, work_tree))
-			git_config_set("core.worktree", work_tree);
-	}
-
-	if (!reinit) {
-		/* Check if symlink is supported in the work tree */
-		path = git_path_buf(&buf, "tXXXXXX");
-		if (!close(xmkstemp(path)) &&
-		    !unlink(path) &&
-		    !symlink("testing", path) &&
-		    !lstat(path, &st1) &&
-		    S_ISLNK(st1.st_mode))
-			unlink(path); /* good */
-		else
-			git_config_set("core.symlinks", "false");
-
-		/* Check if the filesystem is case-insensitive */
-		path = git_path_buf(&buf, "CoNfIg");
-		if (!access(path, F_OK))
-			git_config_set("core.ignorecase", "true");
-		probe_utf8_pathname_composition();
-	}
-
-	strbuf_release(&buf);
-	return reinit;
-}
-
-static void create_object_directory(void)
-{
-	struct strbuf path = STRBUF_INIT;
-	size_t baselen;
-
-	strbuf_addstr(&path, get_object_directory());
-	baselen = path.len;
-
-	safe_create_dir(path.buf, 1);
-
-	strbuf_setlen(&path, baselen);
-	strbuf_addstr(&path, "/pack");
-	safe_create_dir(path.buf, 1);
-
-	strbuf_setlen(&path, baselen);
-	strbuf_addstr(&path, "/info");
-	safe_create_dir(path.buf, 1);
-
-	strbuf_release(&path);
-}
-
-static void separate_git_dir(const char *git_dir, const char *git_link)
-{
-	struct stat st;
-
-	if (!stat(git_link, &st)) {
-		const char *src;
-
-		if (S_ISREG(st.st_mode))
-			src = read_gitfile(git_link);
-		else if (S_ISDIR(st.st_mode))
-			src = git_link;
-		else
-			die(_("unable to handle file type %d"), (int)st.st_mode);
-
-		if (rename(src, git_dir))
-			die_errno(_("unable to move %s to %s"), src, git_dir);
-		repair_worktrees(NULL, NULL);
-	}
-
-	write_file(git_link, "gitdir: %s", git_dir);
-}
-
-static void validate_hash_algorithm(struct repository_format *repo_fmt, int hash)
-{
-	const char *env = getenv(GIT_DEFAULT_HASH_ENVIRONMENT);
-	/*
-	 * If we already have an initialized repo, don't allow the user to
-	 * specify a different algorithm, as that could cause corruption.
-	 * Otherwise, if the user has specified one on the command line, use it.
-	 */
-	if (repo_fmt->version >= 0 && hash != GIT_HASH_UNKNOWN && hash != repo_fmt->hash_algo)
-		die(_("attempt to reinitialize repository with different hash"));
-	else if (hash != GIT_HASH_UNKNOWN)
-		repo_fmt->hash_algo = hash;
-	else if (env) {
-		int env_algo = hash_algo_by_name(env);
-		if (env_algo == GIT_HASH_UNKNOWN)
-			die(_("unknown hash algorithm '%s'"), env);
-		repo_fmt->hash_algo = env_algo;
-	}
-}
-
-int init_db(const char *git_dir, const char *real_git_dir,
-	    const char *template_dir, int hash, const char *initial_branch,
-	    unsigned int flags)
-{
-	int reinit;
-	int exist_ok = flags & INIT_DB_EXIST_OK;
-	char *original_git_dir = real_pathdup(git_dir, 1);
-	struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;
-
-	if (real_git_dir) {
-		struct stat st;
-
-		if (!exist_ok && !stat(git_dir, &st))
-			die(_("%s already exists"), git_dir);
-
-		if (!exist_ok && !stat(real_git_dir, &st))
-			die(_("%s already exists"), real_git_dir);
-
-		set_git_dir(real_git_dir, 1);
-		git_dir = get_git_dir();
-		separate_git_dir(git_dir, original_git_dir);
-	}
-	else {
-		set_git_dir(git_dir, 1);
-		git_dir = get_git_dir();
-	}
-	startup_info->have_repository = 1;
-
-	/* Ensure `core.hidedotfiles` is processed */
-	git_config(platform_core_config, NULL);
-
-	safe_create_dir(git_dir, 0);
-
-	init_is_bare_repository = is_bare_repository();
-
-	/* Check to see if the repository version is right.
-	 * Note that a newly created repository does not have
-	 * config file, so this will not fail.  What we are catching
-	 * is an attempt to reinitialize new repository with an old tool.
-	 */
-	check_repository_format(&repo_fmt);
-
-	validate_hash_algorithm(&repo_fmt, hash);
-
-	reinit = create_default_files(template_dir, original_git_dir,
-				      initial_branch, &repo_fmt,
-				      flags & INIT_DB_QUIET);
-	if (reinit && initial_branch)
-		warning(_("re-init: ignored --initial-branch=%s"),
-			initial_branch);
-
-	create_object_directory();
-
-	if (get_shared_repository()) {
-		char buf[10];
-		/* We do not spell "group" and such, so that
-		 * the configuration can be read by older version
-		 * of git. Note, we use octal numbers for new share modes,
-		 * and compatibility values for PERM_GROUP and
-		 * PERM_EVERYBODY.
-		 */
-		if (get_shared_repository() < 0)
-			/* force to the mode value */
-			xsnprintf(buf, sizeof(buf), "0%o", -get_shared_repository());
-		else if (get_shared_repository() == PERM_GROUP)
-			xsnprintf(buf, sizeof(buf), "%d", OLD_PERM_GROUP);
-		else if (get_shared_repository() == PERM_EVERYBODY)
-			xsnprintf(buf, sizeof(buf), "%d", OLD_PERM_EVERYBODY);
-		else
-			BUG("invalid value for shared_repository");
-		git_config_set("core.sharedrepository", buf);
-		git_config_set("receive.denyNonFastforwards", "true");
-	}
-
-	if (!(flags & INIT_DB_QUIET)) {
-		int len = strlen(git_dir);
-
-		if (reinit)
-			printf(get_shared_repository()
-			       ? _("Reinitialized existing shared Git repository in %s%s\n")
-			       : _("Reinitialized existing Git repository in %s%s\n"),
-			       git_dir, len && git_dir[len-1] != '/' ? "/" : "");
-		else
-			printf(get_shared_repository()
-			       ? _("Initialized empty shared Git repository in %s%s\n")
-			       : _("Initialized empty Git repository in %s%s\n"),
-			       git_dir, len && git_dir[len-1] != '/' ? "/" : "");
-	}
-
-	free(original_git_dir);
-	return 0;
-}
-=======
-#include "worktree.h"
-#include "wrapper.h"
-#include "run-command.h"
-#include "hook.h"
-
-#ifdef NO_TRUSTABLE_FILEMODE
-#define TEST_FILEMODE 0
-#else
-#define TEST_FILEMODE 1
-#endif
-
-#define GIT_DEFAULT_HASH_ENVIRONMENT "GIT_DEFAULT_HASH"
-
-static int init_is_bare_repository = 0;
-static int init_shared_repository = -1;
-
-static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
-			     DIR *dir)
-{
-	size_t path_baselen = path->len;
-	size_t template_baselen = template_path->len;
-	struct dirent *de;
-	int is_hooks_dir = ends_with(template_path->buf, "/hooks/");
-
-	/* Note: if ".git/hooks" file exists in the repository being
-	 * re-initialized, /etc/core-git/templates/hooks/update would
-	 * cause "git init" to fail here.  I think this is sane but
-	 * it means that the set of templates we ship by default, along
-	 * with the way the namespace under .git/ is organized, should
-	 * be really carefully chosen.
-	 */
-	safe_create_dir(path->buf, 1);
-	while ((de = readdir(dir)) != NULL) {
-		struct stat st_git, st_template;
-		int exists = 0;
-
-		strbuf_setlen(path, path_baselen);
-		strbuf_setlen(template_path, template_baselen);
-
-		if (de->d_name[0] == '.')
-			continue;
-		strbuf_addstr(path, de->d_name);
-		strbuf_addstr(template_path, de->d_name);
-		if (lstat(path->buf, &st_git)) {
-			if (errno != ENOENT)
-				die_errno(_("cannot stat '%s'"), path->buf);
-		}
-		else
-			exists = 1;
-
-		if (lstat(template_path->buf, &st_template))
-			die_errno(_("cannot stat template '%s'"), template_path->buf);
-
-		if (S_ISDIR(st_template.st_mode)) {
-			DIR *subdir = opendir(template_path->buf);
-			if (!subdir)
-				die_errno(_("cannot opendir '%s'"), template_path->buf);
-			strbuf_addch(path, '/');
-			strbuf_addch(template_path, '/');
-			copy_templates_1(path, template_path, subdir);
-			closedir(subdir);
-		}
-		else if (exists)
-			continue;
-		else if (S_ISLNK(st_template.st_mode)) {
-			struct strbuf lnk = STRBUF_INIT;
-			if (strbuf_readlink(&lnk, template_path->buf,
-					    st_template.st_size) < 0)
-				die_errno(_("cannot readlink '%s'"), template_path->buf);
-			if (symlink(lnk.buf, path->buf))
-				die_errno(_("cannot symlink '%s' '%s'"),
-					  lnk.buf, path->buf);
-			strbuf_release(&lnk);
-		}
-		else if (S_ISREG(st_template.st_mode)) {
-			if (is_hooks_dir &&
-			    is_executable(template_path->buf))
-				add_safe_hook(template_path->buf);
-
-			if (copy_file(path->buf, template_path->buf, st_template.st_mode))
-				die_errno(_("cannot copy '%s' to '%s'"),
-					  template_path->buf, path->buf);
-		}
-		else
-			error(_("ignoring template %s"), template_path->buf);
-	}
-}
-
-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;
-	struct repository_format template_format = REPOSITORY_FORMAT_INIT;
-	struct strbuf err = STRBUF_INIT;
-	DIR *dir;
-	char *to_free = NULL;
-
-	if (!template_dir || !*template_dir)
-		return;
-
-	strbuf_addstr(&template_path, template_dir);
-	strbuf_complete(&template_path, '/');
-	template_len = template_path.len;
-
-	dir = opendir(template_path.buf);
-	if (!dir) {
-		warning(_("templates not found in %s"), template_dir);
-		goto free_return;
-	}
-
-	/* Make sure that template is from the correct vintage */
-	strbuf_addstr(&template_path, "config");
-	read_repository_format(&template_format, template_path.buf);
-	strbuf_setlen(&template_path, template_len);
-
-	/*
-	 * No mention of version at all is OK, but anything else should be
-	 * verified.
-	 */
-	if (template_format.version >= 0 &&
-	    verify_repository_format(&template_format, &err) < 0) {
-		warning(_("not copying templates from '%s': %s"),
-			  template_dir, err.buf);
-		strbuf_release(&err);
-		goto close_free_return;
-	}
-
-	strbuf_addstr(&path, get_git_common_dir());
-	strbuf_complete(&path, '/');
-	copy_templates_1(&path, &template_path, dir);
-close_free_return:
-	closedir(dir);
-free_return:
-	free(to_free);
-	strbuf_release(&path);
-	strbuf_release(&template_path);
-	clear_repository_format(&template_format);
-}
-
-/*
- * If the git_dir is not directly inside the working tree, then git will not
- * find it by default, and we need to set the worktree explicitly.
- */
-static int needs_work_tree_config(const char *git_dir, const char *work_tree)
-{
-	if (!strcmp(work_tree, "/") && !strcmp(git_dir, "/.git"))
-		return 0;
-	if (skip_prefix(git_dir, work_tree, &git_dir) &&
-	    !strcmp(git_dir, "/.git"))
-		return 0;
-	return 1;
-}
-
-void initialize_repository_version(int hash_algo, int reinit)
-{
-	char repo_version_string[10];
-	int repo_version = GIT_REPO_VERSION;
-
-	if (hash_algo != GIT_HASH_SHA1)
-		repo_version = GIT_REPO_VERSION_READ;
-
-	/* This forces creation of new config file */
-	xsnprintf(repo_version_string, sizeof(repo_version_string),
-		  "%d", repo_version);
-	git_config_set("core.repositoryformatversion", repo_version_string);
-
-	if (hash_algo != GIT_HASH_SHA1)
-		git_config_set("extensions.objectformat",
-			       hash_algos[hash_algo].name);
-	else if (reinit)
-		git_config_set_gently("extensions.objectformat", NULL);
-}
-
-static int create_default_files(const char *template_path,
-				const char *original_git_dir,
-				const char *initial_branch,
-				const struct repository_format *fmt,
-				int quiet)
-{
-	struct stat st1;
-	struct strbuf buf = STRBUF_INIT;
-	char *path;
-	char junk[2];
-	int reinit;
-	int filemode;
-	struct strbuf err = STRBUF_INIT;
-	const char *work_tree = get_git_work_tree();
-
-	/*
-	 * First copy the templates -- we might have the default
-	 * config file there, in which case we would want to read
-	 * from it after installing.
-	 *
-	 * Before reading that config, we also need to clear out any cached
-	 * values (since we've just potentially changed what's available on
-	 * disk).
-	 */
-	copy_templates(template_path);
-	git_config_clear();
-	reset_shared_repository();
-	git_config(git_default_config, NULL);
-
-	/*
-	 * We must make sure command-line options continue to override any
-	 * values we might have just re-read from the config.
-	 */
-	is_bare_repository_cfg = init_is_bare_repository || !work_tree;
-	if (init_shared_repository != -1)
-		set_shared_repository(init_shared_repository);
-
-	/*
-	 * We would have created the above under user's umask -- under
-	 * shared-repository settings, we would need to fix them up.
-	 */
-	if (get_shared_repository()) {
-		adjust_shared_perm(get_git_dir());
-	}
-
-	/*
-	 * We need to create a "refs" dir in any case so that older
-	 * versions of git can tell that this is a repository.
-	 */
-	safe_create_dir(git_path("refs"), 1);
-	adjust_shared_perm(git_path("refs"));
-
-	if (refs_init_db(&err))
-		die("failed to set up refs db: %s", err.buf);
-
-	/*
-	 * Point the HEAD symref to the initial branch with if HEAD does
-	 * not yet exist.
-	 */
-	path = git_path_buf(&buf, "HEAD");
-	reinit = (!access(path, R_OK)
-		  || readlink(path, junk, sizeof(junk)-1) != -1);
-	if (!reinit) {
-		char *ref;
-
-		if (!initial_branch)
-			initial_branch = git_default_branch_name(quiet);
-
-		ref = xstrfmt("refs/heads/%s", initial_branch);
-		if (check_refname_format(ref, 0) < 0)
-			die(_("invalid initial branch name: '%s'"),
-			    initial_branch);
-
-		if (create_symref("HEAD", ref, NULL) < 0)
-			exit(1);
-		free(ref);
-	}
-
-	initialize_repository_version(fmt->hash_algo, 0);
-
-	/* Check filemode trustability */
-	path = git_path_buf(&buf, "config");
-	filemode = TEST_FILEMODE;
-	if (TEST_FILEMODE && !lstat(path, &st1)) {
-		struct stat st2;
-		filemode = (!chmod(path, st1.st_mode ^ S_IXUSR) &&
-				!lstat(path, &st2) &&
-				st1.st_mode != st2.st_mode &&
-				!chmod(path, st1.st_mode));
-		if (filemode && !reinit && (st1.st_mode & S_IXUSR))
-			filemode = 0;
-	}
-	git_config_set("core.filemode", filemode ? "true" : "false");
-
-	if (is_bare_repository())
-		git_config_set("core.bare", "true");
-	else {
-		git_config_set("core.bare", "false");
-		/* allow template config file to override the default */
-		if (log_all_ref_updates == LOG_REFS_UNSET)
-			git_config_set("core.logallrefupdates", "true");
-		if (needs_work_tree_config(original_git_dir, work_tree))
-			git_config_set("core.worktree", work_tree);
-	}
-
-	if (!reinit) {
-		/* Check if symlink is supported in the work tree */
-		path = git_path_buf(&buf, "tXXXXXX");
-		if (!close(xmkstemp(path)) &&
-		    !unlink(path) &&
-		    !symlink("testing", path) &&
-		    !lstat(path, &st1) &&
-		    S_ISLNK(st1.st_mode))
-			unlink(path); /* good */
-		else
-			git_config_set("core.symlinks", "false");
-
-		/* Check if the filesystem is case-insensitive */
-		path = git_path_buf(&buf, "CoNfIg");
-		if (!access(path, F_OK))
-			git_config_set("core.ignorecase", "true");
-		probe_utf8_pathname_composition();
-	}
-
-	strbuf_release(&buf);
-	return reinit;
-}
-
-static void create_object_directory(void)
-{
-	struct strbuf path = STRBUF_INIT;
-	size_t baselen;
-
-	strbuf_addstr(&path, get_object_directory());
-	baselen = path.len;
-
-	safe_create_dir(path.buf, 1);
-
-	strbuf_setlen(&path, baselen);
-	strbuf_addstr(&path, "/pack");
-	safe_create_dir(path.buf, 1);
-
-	strbuf_setlen(&path, baselen);
-	strbuf_addstr(&path, "/info");
-	safe_create_dir(path.buf, 1);
-
-	strbuf_release(&path);
-}
-
-static void separate_git_dir(const char *git_dir, const char *git_link)
-{
-	struct stat st;
-
-	if (!stat(git_link, &st)) {
-		const char *src;
-
-		if (S_ISREG(st.st_mode))
-			src = read_gitfile(git_link);
-		else if (S_ISDIR(st.st_mode))
-			src = git_link;
-		else
-			die(_("unable to handle file type %d"), (int)st.st_mode);
-
-		if (rename(src, git_dir))
-			die_errno(_("unable to move %s to %s"), src, git_dir);
-		repair_worktrees(NULL, NULL);
-	}
-
-	write_file(git_link, "gitdir: %s", git_dir);
-}
-
-static void validate_hash_algorithm(struct repository_format *repo_fmt, int hash)
-{
-	const char *env = getenv(GIT_DEFAULT_HASH_ENVIRONMENT);
-	/*
-	 * If we already have an initialized repo, don't allow the user to
-	 * specify a different algorithm, as that could cause corruption.
-	 * Otherwise, if the user has specified one on the command line, use it.
-	 */
-	if (repo_fmt->version >= 0 && hash != GIT_HASH_UNKNOWN && hash != repo_fmt->hash_algo)
-		die(_("attempt to reinitialize repository with different hash"));
-	else if (hash != GIT_HASH_UNKNOWN)
-		repo_fmt->hash_algo = hash;
-	else if (env) {
-		int env_algo = hash_algo_by_name(env);
-		if (env_algo == GIT_HASH_UNKNOWN)
-			die(_("unknown hash algorithm '%s'"), env);
-		repo_fmt->hash_algo = env_algo;
-	}
-}
-
-int init_db(const char *git_dir, const char *real_git_dir,
-	    const char *template_dir, int hash, const char *initial_branch,
-	    unsigned int flags)
-{
-	int reinit;
-	int exist_ok = flags & INIT_DB_EXIST_OK;
-	char *original_git_dir = real_pathdup(git_dir, 1);
-	struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;
-
-	if (real_git_dir) {
-		struct stat st;
-
-		if (!exist_ok && !stat(git_dir, &st))
-			die(_("%s already exists"), git_dir);
-
-		if (!exist_ok && !stat(real_git_dir, &st))
-			die(_("%s already exists"), real_git_dir);
-
-		set_git_dir(real_git_dir, 1);
-		git_dir = get_git_dir();
-		separate_git_dir(git_dir, original_git_dir);
-	}
-	else {
-		set_git_dir(git_dir, 1);
-		git_dir = get_git_dir();
-	}
-	startup_info->have_repository = 1;
-
-	/* Ensure `core.hidedotfiles` is processed */
-	git_config(platform_core_config, NULL);
-
-	safe_create_dir(git_dir, 0);
-
-	init_is_bare_repository = is_bare_repository();
-
-	/* Check to see if the repository version is right.
-	 * Note that a newly created repository does not have
-	 * config file, so this will not fail.  What we are catching
-	 * is an attempt to reinitialize new repository with an old tool.
-	 */
-	check_repository_format(&repo_fmt);
-
-	validate_hash_algorithm(&repo_fmt, hash);
-
-	reinit = create_default_files(template_dir, original_git_dir,
-				      initial_branch, &repo_fmt,
-				      flags & INIT_DB_QUIET);
-	if (reinit && initial_branch)
-		warning(_("re-init: ignored --initial-branch=%s"),
-			initial_branch);
-
-	create_object_directory();
-
-	if (get_shared_repository()) {
-		char buf[10];
-		/* We do not spell "group" and such, so that
-		 * the configuration can be read by older version
-		 * of git. Note, we use octal numbers for new share modes,
-		 * and compatibility values for PERM_GROUP and
-		 * PERM_EVERYBODY.
-		 */
-		if (get_shared_repository() < 0)
-			/* force to the mode value */
-			xsnprintf(buf, sizeof(buf), "0%o", -get_shared_repository());
-		else if (get_shared_repository() == PERM_GROUP)
-			xsnprintf(buf, sizeof(buf), "%d", OLD_PERM_GROUP);
-		else if (get_shared_repository() == PERM_EVERYBODY)
-			xsnprintf(buf, sizeof(buf), "%d", OLD_PERM_EVERYBODY);
-		else
-			BUG("invalid value for shared_repository");
-		git_config_set("core.sharedrepository", buf);
-		git_config_set("receive.denyNonFastforwards", "true");
-	}
-
-	if (!(flags & INIT_DB_QUIET)) {
-		int len = strlen(git_dir);
-
-		if (reinit)
-			printf(get_shared_repository()
-			       ? _("Reinitialized existing shared Git repository in %s%s\n")
-			       : _("Reinitialized existing Git repository in %s%s\n"),
-			       git_dir, len && git_dir[len-1] != '/' ? "/" : "");
-		else
-			printf(get_shared_repository()
-			       ? _("Initialized empty shared Git repository in %s%s\n")
-			       : _("Initialized empty Git repository in %s%s\n"),
-			       git_dir, len && git_dir[len-1] != '/' ? "/" : "");
-	}
-
-	free(original_git_dir);
-	return 0;
-}
->>>>>>> c7b6b0a28f (Merge branch 'js/fix-clone-w-hooks-2.40' into HEAD)
 
 static int guess_repository_type(const char *git_dir)
 {
diff --git a/config.c b/config.c
remerge CONFLICT (content): Merge conflict in config.c
index f0f09004c8..3846a37be9 100644
--- a/config.c
+++ b/config.c
@@ -1558,33 +1558,7 @@ static int git_default_core_config(const char *var, const char *value,
 	if (!strcmp(var, "core.attributesfile"))
 		return git_config_pathname(&git_attributes_file, var, value);
 
-<<<<<<< babb4e5d71 (Git 2.42.2)
-	if (!strcmp(var, "core.hookspath")) {
-		if (ctx->kvi && ctx->kvi->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);
-||||||| 0f15832059
-	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);
-=======
 	if (!strcmp(var, "core.hookspath"))
->>>>>>> c7b6b0a28f (Merge branch 'js/fix-clone-w-hooks-2.40' into HEAD)
 		return git_config_pathname(&git_hooks_path, var, value);
 
 	if (!strcmp(var, "core.bare")) {
diff --git a/hook.c b/hook.c
index 7ddb677216..7be471e335 100644
--- a/hook.c
+++ b/hook.c
@@ -59,7 +59,8 @@ void add_safe_hook(const char *path)
 	}
 }
 
-static int safe_hook_cb(const char *key, const char *value, void *d)
+static int safe_hook_cb(const char *key, const char *value,
+			const struct config_context *ctx UNUSED, void *d)
 {
 	struct strset *set = d;
 
diff --git a/setup.c b/setup.c
index ea2aec5aaa..dc4897543e 100644
--- a/setup.c
+++ b/setup.c
@@ -18,6 +18,7 @@
 #include "trace2.h"
 #include "worktree.h"
 #include "exec-cmd.h"
+#include "run-command.h"
 #include "hook.h"
 
 static int inside_git_dir = -1;
@@ -1794,6 +1795,7 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
 	size_t path_baselen = path->len;
 	size_t template_baselen = template_path->len;
 	struct dirent *de;
+	int is_hooks_dir = ends_with(template_path->buf, "/hooks/");
 
 	/* Note: if ".git/hooks" file exists in the repository being
 	 * re-initialized, /etc/core-git/templates/hooks/update would
@@ -1846,6 +1848,10 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
 			strbuf_release(&lnk);
 		}
 		else if (S_ISREG(st_template.st_mode)) {
+			if (is_hooks_dir &&
+			    is_executable(template_path->buf))
+				add_safe_hook(template_path->buf);
+
 			if (copy_file(path->buf, template_path->buf, st_template.st_mode))
 				die_errno(_("cannot copy '%s' to '%s'"),
 					  template_path->buf, path->buf);
* js/fix-clone-w-hooks-2.43
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
remerge CONFLICT (content): Merge conflict in t/t1800-hook.sh
index 1283a9bd47..c51be5f7a0 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -191,24 +191,7 @@ test_expect_success '`safe.hook.sha256` and clone protections' '
 	echo "called hook" >safe-hook.log
 	EOF
 
-<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 1f2e64e22d (Git 2.43.4)
-	git hook run test-hook 2>err &&
-	test_grep "Hook ran" err &&
-|||||||||||||||||||||||||||||||| babb4e5d71
-	git hook run test-hook 2>err &&
-	grep "Hook ran" err &&
-================================
->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 0268cf79cb (Merge branch 'js/fix-clone-w-hooks-2.41' into HEAD)
 	test_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \
-<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 1f2e64e22d (Git 2.43.4)
-		git hook run test-hook 2>err &&
-	test_grep "active .core.hooksPath" err &&
-	test_grep ! "Hook ran" err
-|||||||||||||||||||||||||||||||| babb4e5d71
-		git hook run test-hook 2>err &&
-	grep "active .core.hooksPath" err &&
-	! grep "Hook ran" err
-================================
 		git -C safe-hook hook run pre-push 2>err &&
 	cmd="$(grep "git config --global --add safe.hook.sha256 [0-9a-f]" err)" &&
 	eval "$cmd" &&
@@ -235,7 +218,6 @@ test_expect_success 'Git LFS special-handling in clone protections' '
 		git -C lfs-hooks hook run pre-push &&
 	test_write_lines "called pre-push" >expect &&
 	test_cmp lfs-hooks/fake-git-lfs.log expect
->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 0268cf79cb (Merge branch 'js/fix-clone-w-hooks-2.41' into HEAD)
 '
 
 test_done
diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh
index 6ee833bac2..0fcfe6f2ce 100755
--- a/t/t5601-clone.sh
+++ b/t/t5601-clone.sh
@@ -835,7 +835,7 @@ test_expect_success 'clone with init.templatedir runs hooks' '
 		git -c init.templateDir="$(pwd)/tmpl" \
 			clone --recurse-submodules \
 			tmpl/hooks hook-run-submodule 2>err &&
-		! grep "active .* hook found" err &&
+		test_grep ! "active .* hook found" err &&
 		test_path_is_file hook-run-submodule/hook.run &&
 		test_path_is_file hook-run-submodule/sub/hook.run
 	)
* js/fix-clone-w-hooks-2.44* js/fix-clone-w-hooks-2.45


^ permalink raw reply related	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 5/8] hook(clone protections): add escape hatch
  2024-05-18 10:32   ` [PATCH v2 5/8] hook(clone protections): add escape hatch Johannes Schindelin via GitGitGadget
@ 2024-05-18 18:14     ` Jeff King
  2024-05-18 18:54       ` Junio C Hamano
  2024-05-18 19:32       ` Johannes Schindelin
  0 siblings, 2 replies; 54+ messages in thread
From: Jeff King @ 2024-05-18 18:14 UTC (permalink / raw
  To: Johannes Schindelin via GitGitGadget
  Cc: git, brian m. carlson, Johannes Schindelin

On Sat, May 18, 2024 at 10:32:43AM +0000, Johannes Schindelin via GitGitGadget wrote:

> To help Git LFS, and other tools behaving similarly (if there are any),
> let's add a new, multi-valued `safe.hook.sha256` config setting. Like
> the already-existing `safe.*` settings, it is ignored in
> repository-local configs, and it is interpreted as a list of SHA-256
> checksums of hooks' contents that are safe to execute during a clone
> operation. Future Git LFS versions will need to write those entries at
> the same time they install the `smudge`/`clean` filters.

This scheme seems more complicated for the user than the sometimes
discussed ability to specify hook paths via config (not core.hooksPath,
which covers _all_ hooks, but one which allows a per-hook path).

In either case, we're considering config to be a trusted source of
truth, so I think the security properties are the same. But for the
system here, a user updating a hook needs to do multiple steps:

  - compute the sha256 of the hook (for which we provide no tooling
    support, though hopefully it is obvious how to use other tools)

  - add the config for the sha256

  - install the new hook into $GIT_DIR/hooks

Whereas if the config can just point at the hook, then there is only one
step: add the config for the hook (presumably pointing to a system
version that would have been copied into $GIT_DIR/hooks previously).

Likewise for updates of the hooks, where the sha256 scheme requires
computing and adding a new hash. But when the config just points to the
path, there is no additional step for updating.

In either scheme, programs like git-lfs would have to adjust to the new
world view. The main advantage of the sha256 scheme, it seems to me, is
that the baked-in sha256 values let existing versions of git-lfs work.
But we could also support that internally, without exposing
safe.hook.sha256 to the world (and thus creating an ecosystem where we
have to support it forever).

Implied here is that I also think config-based hooks have a lot of
_other_ advantages, and so would be worth pursuing anyway, and this
extra safety would come along for free. I won't enumerate those
advantages here, but we that can be a separate discussion if need be.

And of course that feature doesn't yet exist, and is a much larger one.
But besides un-breaking current LFS, I'm not sure that we need to rush
out a more generic version of the feature.

-Peff


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 5/8] hook(clone protections): add escape hatch
  2024-05-18 18:14     ` Jeff King
@ 2024-05-18 18:54       ` Junio C Hamano
  2024-05-18 19:35         ` Jeff King
  2024-05-18 19:37         ` Johannes Schindelin
  2024-05-18 19:32       ` Johannes Schindelin
  1 sibling, 2 replies; 54+ messages in thread
From: Junio C Hamano @ 2024-05-18 18:54 UTC (permalink / raw
  To: Jeff King
  Cc: Johannes Schindelin via GitGitGadget, git, brian m. carlson,
	Johannes Schindelin

Jeff King <peff@peff.net> writes:

> In either case, we're considering config to be a trusted source of
> truth, so I think the security properties are the same. But for the
> system here, a user updating a hook needs to do multiple steps:
>
>   - compute the sha256 of the hook (for which we provide no tooling
>     support, though hopefully it is obvious how to use other tools)
>
>   - add the config for the sha256
>
>   - install the new hook into $GIT_DIR/hooks

I am not sure why any of the above is needed.  

Hmph.

I was somehow (because that is how "git config --help" explains
"safe.hook.*") led to believe that this "safety" was only about "git
clone would prefer not to run ANY hook before it finishes operation
and gives back the control to the end user, but historically it ran
any enabled hooks in the resulting repository that was freshly
created by it---so let's at least make sure the contents of the
hooks are known-to-be-good ones when 'git clone' runs the hooks".
Most importantly, once "git clone" gives control back to the end
user and the end user had a chance to inspect the resulting
repository, the files in $GIT_DIR/hooks can be updated and the hooks
will run without incurring any cost of checking.

Isn't that what happens?

Looking at the control flow, hook.c:find_hook() is the one that
calls the function is_hook_safe_during_clone() to reject "unsafe"
ones (and allow the white-listed ones), but I do not know offhand
how the code limits the rejection only during clone.  So perhaps
this set of patches need further work to restrict the checks only to
"while we are cloning" case?




^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again
  2024-05-18  0:10   ` Junio C Hamano
@ 2024-05-18 18:58     ` Johannes Schindelin
  0 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin @ 2024-05-18 18:58 UTC (permalink / raw
  To: Junio C Hamano; +Cc: Johannes Schindelin via GitGitGadget, git

Hi Junio,

On Fri, 17 May 2024, Junio C Hamano wrote:

> "Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
> writes:
>
> > What the added protection did not anticipate is that such a
> > repository-local `core.hooksPath` can not only be used to point to
> > maliciously-placed scripts in the current worktree, but also to
> > _prevent_ hooks from being called altogether.
> > ...
> > diff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh
> > index f6dc83e2aab..1eae346a6e3 100755
> > --- a/t/t1350-config-hooks-path.sh
> > +++ b/t/t1350-config-hooks-path.sh
> > @@ -41,4 +41,8 @@ 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
> > +'
>
> Is it sufficient that the command exits with 0?  I am wondering if
> we want to verify that the resulting repository looks like it
> should, e.g., with
>
>     v=$(git -C no-templates config --local --get core.hookspath) &&
>     test "$v" = /dev/null
>
> or something silly like that.

I've added that, but would like to stress that the regression was _not_
that the `core.hooksPath` setting was missing from the local config. I've
added it because the implied suggestion is valid that we'll want to ensure
that the test case passes for the _correct_ reason ;-)

Ciao,
Johannes


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 0/8] Various fixes for v2.45.1 and friends
  2024-05-18 17:07   ` [PATCH v2 0/8] Various fixes for v2.45.1 and friends Junio C Hamano
@ 2024-05-18 19:22     ` Johannes Schindelin
  2024-05-18 20:13       ` Johannes Schindelin
  0 siblings, 1 reply; 54+ messages in thread
From: Johannes Schindelin @ 2024-05-18 19:22 UTC (permalink / raw
  To: Junio C Hamano
  Cc: git, Johannes Schindelin via GitGitGadget, brian m. carlson

Hi Junio,

On Sat, 18 May 2024, Junio C Hamano wrote:

> I have applied this to maint-2.39 and then merged them up to the
> maintenance tracks.  The results will be pushed out to the "split
> out" repository at
>
>     https://github.com/gitster/git/
>
> as these branches:
>
>     js/fix-clone-w-hooks-2.39
>     js/fix-clone-w-hooks-2.40
>     js/fix-clone-w-hooks-2.41
>     js/fix-clone-w-hooks-2.42
>     js/fix-clone-w-hooks-2.43
>     js/fix-clone-w-hooks-2.44
>     js/fix-clone-w-hooks-2.45

Thank you!

As it happens, I had already worked on tentative/maint-* branches (that
you can see here: https://github.com/dscho/git/branches/active), but
had to take a break before finalizing them.

The major difference I see is that js/fix-clone-w-hooks-2.41 still
declares `do_files_match()` in `setup.h` (even if it is no longer defined
or called), and that `hook.c` includes `copy.h` (but that is no longer
needed), and that `setup.h` includes `hook.h` (but that's not needed
either).

While comparing, I noticed that I had missed an extra empty line in my
merge conflict resolutions, a `grep` -> `test_grep` conversation and an
`UNUSED` attribute for the `ctx` parameter of the `safe_hook_cb()`
function. So I am really grateful that you did those integrations
independently.

You will also note that I tentatively added commits to mark v2.39.5, ...,
v2.45.2 ready for tagging, along with release notes, just so we can hit
the ground running as soon as reviews settle down.

Ciao,
Johannes

>
> For those who are merging them up at home yourselves, here are the
> remerge diff that show the conflict resolution I made.
>
> * js/fix-clone-w-hooks-2.40
> diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
> remerge CONFLICT (content): Merge conflict in t/t1800-hook.sh
> index 2af93d130d..bc0ed30ab9 100755
> --- a/t/t1800-hook.sh
> +++ b/t/t1800-hook.sh
> @@ -177,7 +177,6 @@ test_expect_success 'git hook run a hook with a bad shebang' '
>  	test_cmp expect actual
>  '
>
> -<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< b9b439e0e3 (Git 2.40.2)
>  test_expect_success 'stdin to hooks' '
>  	write_script .git/hooks/test-hook <<-\EOF &&
>  	echo BEGIN stdin
> @@ -196,23 +195,10 @@ test_expect_success 'stdin to hooks' '
>  	test_cmp expect actual
>  '
>
> -test_expect_success 'clone protections' '
> -	test_config core.hooksPath "$(pwd)/my-hooks" &&
> -	mkdir -p my-hooks &&
> -	write_script my-hooks/test-hook <<-\EOF &&
> -	echo Hook ran $1
> -|||||||||||||||||||||||||||||||| 47b6d90e91
> -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
> -================================
>  test_expect_success '`safe.hook.sha256` and clone protections' '
>  	git init safe-hook &&
>  	write_script safe-hook/.git/hooks/pre-push <<-\EOF &&
>  	echo "called hook" >safe-hook.log
> ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 851218a8af (Revert "Add a helper function to compare file contents")
>  	EOF
>
>  	test_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \
> * js/fix-clone-w-hooks-2.41
> diff --git a/builtin/init-db.c b/builtin/init-db.c
> remerge CONFLICT (content): Merge conflict in builtin/init-db.c
> index 0f0d2033c5..b351fe6e40 100644
> --- a/builtin/init-db.c
> +++ b/builtin/init-db.c
> @@ -17,13 +17,9 @@
>  #include "path.h"
>  #include "setup.h"
>  #include "worktree.h"
> -<<<<<<< 0f15832059 (Git 2.41.1)
>  #include "wrapper.h"
> -||||||| b9b439e0e3
> -=======
>  #include "run-command.h"
>  #include "hook.h"
> ->>>>>>> 752e921355 (Merge branch 'js/fix-clone-w-hooks-2.39' into HEAD)
>
>  #ifdef NO_TRUSTABLE_FILEMODE
>  #define TEST_FILEMODE 0
> diff --git a/cache.h b/cache.h
> remerge CONFLICT (content): Merge conflict in cache.h
> index d99735c623..bdedb87e83 100644
> --- a/cache.h
> +++ b/cache.h
> @@ -555,1301 +555,7 @@ extern int verify_ce_order;
>  #define DATA_CHANGED    0x0020
>  #define TYPE_CHANGED    0x0040
>
> -<<<<<<< 0f15832059 (Git 2.41.1)
>  int cmp_cache_name_compare(const void *a_, const void *b_);
> -||||||| b9b439e0e3
> -/*
> - * Return an abbreviated sha1 unique within this repository's object database.
> - * The result will be at least `len` characters long, and will be NUL
> - * terminated.
> - *
> - * The non-`_r` version returns a static buffer which remains valid until 4
> - * more calls to find_unique_abbrev are made.
> - *
> - * The `_r` variant writes to a buffer supplied by the caller, which must be at
> - * least `GIT_MAX_HEXSZ + 1` bytes. The return value is the number of bytes
> - * written (excluding the NUL terminator).
> - *
> - * Note that while this version avoids the static buffer, it is not fully
> - * reentrant, as it calls into other non-reentrant git code.
> - */
> -const char *repo_find_unique_abbrev(struct repository *r, const struct object_id *oid, int len);
> -#define find_unique_abbrev(oid, len) repo_find_unique_abbrev(the_repository, oid, len)
> -int repo_find_unique_abbrev_r(struct repository *r, char *hex, const struct object_id *oid, int len);
> -#define find_unique_abbrev_r(hex, oid, len) repo_find_unique_abbrev_r(the_repository, hex, oid, len)
> -
> -/* set default permissions by passing mode arguments to open(2) */
> -int git_mkstemps_mode(char *pattern, int suffix_len, int mode);
> -int git_mkstemp_mode(char *pattern, int mode);
> -
> -/*
> - * NOTE NOTE NOTE!!
> - *
> - * PERM_UMASK, OLD_PERM_GROUP and OLD_PERM_EVERYBODY enumerations must
> - * not be changed. Old repositories have core.sharedrepository written in
> - * numeric format, and therefore these values are preserved for compatibility
> - * reasons.
> - */
> -enum sharedrepo {
> -	PERM_UMASK          = 0,
> -	OLD_PERM_GROUP      = 1,
> -	OLD_PERM_EVERYBODY  = 2,
> -	PERM_GROUP          = 0660,
> -	PERM_EVERYBODY      = 0664
> -};
> -int git_config_perm(const char *var, const char *value);
> -int adjust_shared_perm(const char *path);
> -
> -/*
> - * Create the directory containing the named path, using care to be
> - * somewhat safe against races. Return one of the scld_error values to
> - * indicate success/failure. On error, set errno to describe the
> - * problem.
> - *
> - * SCLD_VANISHED indicates that one of the ancestor directories of the
> - * path existed at one point during the function call and then
> - * suddenly vanished, probably because another process pruned the
> - * directory while we were working.  To be robust against this kind of
> - * race, callers might want to try invoking the function again when it
> - * returns SCLD_VANISHED.
> - *
> - * safe_create_leading_directories() temporarily changes path while it
> - * is working but restores it before returning.
> - * safe_create_leading_directories_const() doesn't modify path, even
> - * temporarily. Both these variants adjust the permissions of the
> - * created directories to honor core.sharedRepository, so they are best
> - * suited for files inside the git dir. For working tree files, use
> - * safe_create_leading_directories_no_share() instead, as it ignores
> - * the core.sharedRepository setting.
> - */
> -enum scld_error {
> -	SCLD_OK = 0,
> -	SCLD_FAILED = -1,
> -	SCLD_PERMS = -2,
> -	SCLD_EXISTS = -3,
> -	SCLD_VANISHED = -4
> -};
> -enum scld_error safe_create_leading_directories(char *path);
> -enum scld_error safe_create_leading_directories_const(const char *path);
> -enum scld_error safe_create_leading_directories_no_share(char *path);
> -
> -int mkdir_in_gitdir(const char *path);
> -char *interpolate_path(const char *path, int real_home);
> -/* NEEDSWORK: remove this synonym once in-flight topics have migrated */
> -#define expand_user_path interpolate_path
> -const char *enter_repo(const char *path, int strict);
> -static inline int is_absolute_path(const char *path)
> -{
> -	return is_dir_sep(path[0]) || has_dos_drive_prefix(path);
> -}
> -int is_directory(const char *);
> -char *strbuf_realpath(struct strbuf *resolved, const char *path,
> -		      int die_on_error);
> -char *strbuf_realpath_forgiving(struct strbuf *resolved, const char *path,
> -				int die_on_error);
> -char *real_pathdup(const char *path, int die_on_error);
> -const char *absolute_path(const char *path);
> -char *absolute_pathdup(const char *path);
> -const char *remove_leading_path(const char *in, const char *prefix);
> -const char *relative_path(const char *in, const char *prefix, struct strbuf *sb);
> -int normalize_path_copy_len(char *dst, const char *src, int *prefix_len);
> -int normalize_path_copy(char *dst, const char *src);
> -int longest_ancestor_length(const char *path, struct string_list *prefixes);
> -char *strip_path_suffix(const char *path, const char *suffix);
> -int daemon_avoid_alias(const char *path);
> -
> -/*
> - * These functions match their is_hfs_dotgit() counterparts; see utf8.h for
> - * details.
> - */
> -int is_ntfs_dotgit(const char *name);
> -int is_ntfs_dotgitmodules(const char *name);
> -int is_ntfs_dotgitignore(const char *name);
> -int is_ntfs_dotgitattributes(const char *name);
> -int is_ntfs_dotmailmap(const char *name);
> -
> -/*
> - * Returns true iff "str" could be confused as a command-line option when
> - * passed to a sub-program like "ssh". Note that this has nothing to do with
> - * shell-quoting, which should be handled separately; we're assuming here that
> - * the string makes it verbatim to the sub-program.
> - */
> -int looks_like_command_line_option(const char *str);
> -
> -/**
> - * Return a newly allocated string with the evaluation of
> - * "$XDG_CONFIG_HOME/$subdir/$filename" if $XDG_CONFIG_HOME is non-empty, otherwise
> - * "$HOME/.config/$subdir/$filename". Return NULL upon error.
> - */
> -char *xdg_config_home_for(const char *subdir, const char *filename);
> -
> -/**
> - * Return a newly allocated string with the evaluation of
> - * "$XDG_CONFIG_HOME/git/$filename" if $XDG_CONFIG_HOME is non-empty, otherwise
> - * "$HOME/.config/git/$filename". Return NULL upon error.
> - */
> -char *xdg_config_home(const char *filename);
> -
> -/**
> - * Return a newly allocated string with the evaluation of
> - * "$XDG_CACHE_HOME/git/$filename" if $XDG_CACHE_HOME is non-empty, otherwise
> - * "$HOME/.cache/git/$filename". Return NULL upon error.
> - */
> -char *xdg_cache_home(const char *filename);
> -
> -int git_open_cloexec(const char *name, int flags);
> -#define git_open(name) git_open_cloexec(name, O_RDONLY)
> -
> -/**
> - * unpack_loose_header() initializes the data stream needed to unpack
> - * a loose object header.
> - *
> - * Returns:
> - *
> - * - ULHR_OK on success
> - * - ULHR_BAD on error
> - * - ULHR_TOO_LONG if the header was too long
> - *
> - * It will only parse up to MAX_HEADER_LEN bytes unless an optional
> - * "hdrbuf" argument is non-NULL. This is intended for use with
> - * OBJECT_INFO_ALLOW_UNKNOWN_TYPE to extract the bad type for (error)
> - * reporting. The full header will be extracted to "hdrbuf" for use
> - * with parse_loose_header(), ULHR_TOO_LONG will still be returned
> - * from this function to indicate that the header was too long.
> - */
> -enum unpack_loose_header_result {
> -	ULHR_OK,
> -	ULHR_BAD,
> -	ULHR_TOO_LONG,
> -};
> -enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,
> -						    unsigned char *map,
> -						    unsigned long mapsize,
> -						    void *buffer,
> -						    unsigned long bufsiz,
> -						    struct strbuf *hdrbuf);
> -
> -/**
> - * parse_loose_header() parses the starting "<type> <len>\0" of an
> - * object. If it doesn't follow that format -1 is returned. To check
> - * the validity of the <type> populate the "typep" in the "struct
> - * object_info". It will be OBJ_BAD if the object type is unknown. The
> - * parsed <len> can be retrieved via "oi->sizep", and from there
> - * passed to unpack_loose_rest().
> - */
> -struct object_info;
> -int parse_loose_header(const char *hdr, struct object_info *oi);
> -
> -/**
> - * With in-core object data in "buf", rehash it to make sure the
> - * object name actually matches "oid" to detect object corruption.
> - *
> - * A negative value indicates an error, usually that the OID is not
> - * what we expected, but it might also indicate another error.
> - */
> -int check_object_signature(struct repository *r, const struct object_id *oid,
> -			   void *map, unsigned long size,
> -			   enum object_type type);
> -
> -/**
> - * A streaming version of check_object_signature().
> - * Try reading the object named with "oid" using
> - * the streaming interface and rehash it to do the same.
> - */
> -int stream_object_signature(struct repository *r, const struct object_id *oid);
> -
> -int finalize_object_file(const char *tmpfile, const char *filename);
> -
> -/* Helper to check and "touch" a file */
> -int check_and_freshen_file(const char *fn, int freshen);
> -
> -extern const signed char hexval_table[256];
> -static inline unsigned int hexval(unsigned char c)
> -{
> -	return hexval_table[c];
> -}
> -
> -/*
> - * Convert two consecutive hexadecimal digits into a char.  Return a
> - * negative value on error.  Don't run over the end of short strings.
> - */
> -static inline int hex2chr(const char *s)
> -{
> -	unsigned int val = hexval(s[0]);
> -	return (val & ~0xf) ? val : (val << 4) | hexval(s[1]);
> -}
> -
> -/* Convert to/from hex/sha1 representation */
> -#define MINIMUM_ABBREV minimum_abbrev
> -#define DEFAULT_ABBREV default_abbrev
> -
> -/* used when the code does not know or care what the default abbrev is */
> -#define FALLBACK_DEFAULT_ABBREV 7
> -
> -struct object_context {
> -	unsigned short mode;
> -	/*
> -	 * symlink_path is only used by get_tree_entry_follow_symlinks,
> -	 * and only for symlinks that point outside the repository.
> -	 */
> -	struct strbuf symlink_path;
> -	/*
> -	 * If GET_OID_RECORD_PATH is set, this will record path (if any)
> -	 * found when resolving the name. The caller is responsible for
> -	 * releasing the memory.
> -	 */
> -	char *path;
> -};
> -
> -#define GET_OID_QUIETLY           01
> -#define GET_OID_COMMIT            02
> -#define GET_OID_COMMITTISH        04
> -#define GET_OID_TREE             010
> -#define GET_OID_TREEISH          020
> -#define GET_OID_BLOB             040
> -#define GET_OID_FOLLOW_SYMLINKS 0100
> -#define GET_OID_RECORD_PATH     0200
> -#define GET_OID_ONLY_TO_DIE    04000
> -#define GET_OID_REQUIRE_PATH  010000
> -
> -#define GET_OID_DISAMBIGUATORS \
> -	(GET_OID_COMMIT | GET_OID_COMMITTISH | \
> -	GET_OID_TREE | GET_OID_TREEISH | \
> -	GET_OID_BLOB)
> -
> -enum get_oid_result {
> -	FOUND = 0,
> -	MISSING_OBJECT = -1, /* The requested object is missing */
> -	SHORT_NAME_AMBIGUOUS = -2,
> -	/* The following only apply when symlinks are followed */
> -	DANGLING_SYMLINK = -4, /*
> -				* The initial symlink is there, but
> -				* (transitively) points to a missing
> -				* in-tree file
> -				*/
> -	SYMLINK_LOOP = -5,
> -	NOT_DIR = -6, /*
> -		       * Somewhere along the symlink chain, a path is
> -		       * requested which contains a file as a
> -		       * non-final element.
> -		       */
> -};
> -
> -int repo_get_oid(struct repository *r, const char *str, struct object_id *oid);
> -__attribute__((format (printf, 2, 3)))
> -int get_oidf(struct object_id *oid, const char *fmt, ...);
> -int repo_get_oid_commit(struct repository *r, const char *str, struct object_id *oid);
> -int repo_get_oid_committish(struct repository *r, const char *str, struct object_id *oid);
> -int repo_get_oid_tree(struct repository *r, const char *str, struct object_id *oid);
> -int repo_get_oid_treeish(struct repository *r, const char *str, struct object_id *oid);
> -int repo_get_oid_blob(struct repository *r, const char *str, struct object_id *oid);
> -int repo_get_oid_mb(struct repository *r, const char *str, struct object_id *oid);
> -void maybe_die_on_misspelt_object_name(struct repository *repo,
> -				       const char *name,
> -				       const char *prefix);
> -enum get_oid_result get_oid_with_context(struct repository *repo, const char *str,
> -					 unsigned flags, struct object_id *oid,
> -					 struct object_context *oc);
> -
> -#define get_oid(str, oid)		repo_get_oid(the_repository, str, oid)
> -#define get_oid_commit(str, oid)	repo_get_oid_commit(the_repository, str, oid)
> -#define get_oid_committish(str, oid)	repo_get_oid_committish(the_repository, str, oid)
> -#define get_oid_tree(str, oid)		repo_get_oid_tree(the_repository, str, oid)
> -#define get_oid_treeish(str, oid)	repo_get_oid_treeish(the_repository, str, oid)
> -#define get_oid_blob(str, oid)		repo_get_oid_blob(the_repository, str, oid)
> -#define get_oid_mb(str, oid) 		repo_get_oid_mb(the_repository, str, oid)
> -
> -typedef int each_abbrev_fn(const struct object_id *oid, void *);
> -int repo_for_each_abbrev(struct repository *r, const char *prefix, each_abbrev_fn, void *);
> -#define for_each_abbrev(prefix, fn, data) repo_for_each_abbrev(the_repository, prefix, fn, data)
> -
> -int set_disambiguate_hint_config(const char *var, const char *value);
> -
> -/*
> - * Try to read a SHA1 in hexadecimal format from the 40 characters
> - * starting at hex.  Write the 20-byte result to sha1 in binary form.
> - * Return 0 on success.  Reading stops if a NUL is encountered in the
> - * input, so it is safe to pass this function an arbitrary
> - * null-terminated string.
> - */
> -int get_sha1_hex(const char *hex, unsigned char *sha1);
> -int get_oid_hex(const char *hex, struct object_id *sha1);
> -
> -/* Like get_oid_hex, but for an arbitrary hash algorithm. */
> -int get_oid_hex_algop(const char *hex, struct object_id *oid, const struct git_hash_algo *algop);
> -
> -/*
> - * Read `len` pairs of hexadecimal digits from `hex` and write the
> - * values to `binary` as `len` bytes. Return 0 on success, or -1 if
> - * the input does not consist of hex digits).
> - */
> -int hex_to_bytes(unsigned char *binary, const char *hex, size_t len);
> -
> -/*
> - * Convert a binary hash in "unsigned char []" or an object name in
> - * "struct object_id *" to its hex equivalent. The `_r` variant is reentrant,
> - * and writes the NUL-terminated output to the buffer `out`, which must be at
> - * least `GIT_MAX_HEXSZ + 1` bytes, and returns a pointer to out for
> - * convenience.
> - *
> - * The non-`_r` variant returns a static buffer, but uses a ring of 4
> - * buffers, making it safe to make multiple calls for a single statement, like:
> - *
> - *   printf("%s -> %s", hash_to_hex(one), hash_to_hex(two));
> - *   printf("%s -> %s", oid_to_hex(one), oid_to_hex(two));
> - */
> -char *hash_to_hex_algop_r(char *buffer, const unsigned char *hash, const struct git_hash_algo *);
> -char *oid_to_hex_r(char *out, const struct object_id *oid);
> -char *hash_to_hex_algop(const unsigned char *hash, const struct git_hash_algo *);	/* static buffer result! */
> -char *hash_to_hex(const unsigned char *hash);						/* same static buffer */
> -char *oid_to_hex(const struct object_id *oid);						/* same static buffer */
> -
> -/*
> - * Parse a 40-character hexadecimal object ID starting from hex, updating the
> - * pointer specified by end when parsing stops.  The resulting object ID is
> - * stored in oid.  Returns 0 on success.  Parsing will stop on the first NUL or
> - * other invalid character.  end is only updated on success; otherwise, it is
> - * unmodified.
> - */
> -int parse_oid_hex(const char *hex, struct object_id *oid, const char **end);
> -
> -/* Like parse_oid_hex, but for an arbitrary hash algorithm. */
> -int parse_oid_hex_algop(const char *hex, struct object_id *oid, const char **end,
> -			const struct git_hash_algo *algo);
> -
> -
> -/*
> - * These functions work like get_oid_hex and parse_oid_hex, but they will parse
> - * a hex value for any algorithm. The algorithm is detected based on the length
> - * and the algorithm in use is returned. If this is not a hex object ID in any
> - * algorithm, returns GIT_HASH_UNKNOWN.
> - */
> -int get_oid_hex_any(const char *hex, struct object_id *oid);
> -int parse_oid_hex_any(const char *hex, struct object_id *oid, const char **end);
> -
> -/*
> - * This reads short-hand syntax that not only evaluates to a commit
> - * object name, but also can act as if the end user spelled the name
> - * of the branch from the command line.
> - *
> - * - "@{-N}" finds the name of the Nth previous branch we were on, and
> - *   places the name of the branch in the given buf and returns the
> - *   number of characters parsed if successful.
> - *
> - * - "<branch>@{upstream}" finds the name of the other ref that
> - *   <branch> is configured to merge with (missing <branch> defaults
> - *   to the current branch), and places the name of the branch in the
> - *   given buf and returns the number of characters parsed if
> - *   successful.
> - *
> - * If the input is not of the accepted format, it returns a negative
> - * number to signal an error.
> - *
> - * If the input was ok but there are not N branch switches in the
> - * reflog, it returns 0.
> - */
> -#define INTERPRET_BRANCH_LOCAL (1<<0)
> -#define INTERPRET_BRANCH_REMOTE (1<<1)
> -#define INTERPRET_BRANCH_HEAD (1<<2)
> -struct interpret_branch_name_options {
> -	/*
> -	 * If "allowed" is non-zero, it is a treated as a bitfield of allowable
> -	 * expansions: local branches ("refs/heads/"), remote branches
> -	 * ("refs/remotes/"), or "HEAD". If no "allowed" bits are set, any expansion is
> -	 * allowed, even ones to refs outside of those namespaces.
> -	 */
> -	unsigned allowed;
> -
> -	/*
> -	 * If ^{upstream} or ^{push} (or equivalent) is requested, and the
> -	 * branch in question does not have such a reference, return -1 instead
> -	 * of die()-ing.
> -	 */
> -	unsigned nonfatal_dangling_mark : 1;
> -};
> -int repo_interpret_branch_name(struct repository *r,
> -			       const char *str, int len,
> -			       struct strbuf *buf,
> -			       const struct interpret_branch_name_options *options);
> -#define interpret_branch_name(str, len, buf, options) \
> -	repo_interpret_branch_name(the_repository, str, len, buf, options)
> -
> -int validate_headref(const char *ref);
> -
> -int base_name_compare(const char *name1, size_t len1, int mode1,
> -		      const char *name2, size_t len2, int mode2);
> -int df_name_compare(const char *name1, size_t len1, int mode1,
> -		    const char *name2, size_t len2, int mode2);
> -int name_compare(const char *name1, size_t len1, const char *name2, size_t len2);
> -int cache_name_stage_compare(const char *name1, int len1, int stage1, const char *name2, int len2, int stage2);
> -
> -void *read_object_with_reference(struct repository *r,
> -				 const struct object_id *oid,
> -				 enum object_type required_type,
> -				 unsigned long *size,
> -				 struct object_id *oid_ret);
> -
> -struct object *repo_peel_to_type(struct repository *r,
> -				 const char *name, int namelen,
> -				 struct object *o, enum object_type);
> -#define peel_to_type(name, namelen, obj, type) \
> -	repo_peel_to_type(the_repository, name, namelen, obj, type)
> -
> -#define IDENT_STRICT	       1
> -#define IDENT_NO_DATE	       2
> -#define IDENT_NO_NAME	       4
> -
> -enum want_ident {
> -	WANT_BLANK_IDENT,
> -	WANT_AUTHOR_IDENT,
> -	WANT_COMMITTER_IDENT
> -};
> -
> -const char *git_author_info(int);
> -const char *git_committer_info(int);
> -const char *fmt_ident(const char *name, const char *email,
> -		      enum want_ident whose_ident,
> -		      const char *date_str, int);
> -const char *fmt_name(enum want_ident);
> -const char *ident_default_name(void);
> -const char *ident_default_email(void);
> -const char *git_editor(void);
> -const char *git_sequence_editor(void);
> -const char *git_pager(int stdout_is_tty);
> -int is_terminal_dumb(void);
> -int git_ident_config(const char *, const char *, void *);
> -/*
> - * Prepare an ident to fall back on if the user didn't configure it.
> - */
> -void prepare_fallback_ident(const char *name, const char *email);
> -void reset_ident_date(void);
> -
> -struct ident_split {
> -	const char *name_begin;
> -	const char *name_end;
> -	const char *mail_begin;
> -	const char *mail_end;
> -	const char *date_begin;
> -	const char *date_end;
> -	const char *tz_begin;
> -	const char *tz_end;
> -};
> -/*
> - * Signals an success with 0, but time part of the result may be NULL
> - * if the input lacks timestamp and zone
> - */
> -int split_ident_line(struct ident_split *, const char *, int);
> -
> -/*
> - * Given a commit or tag object buffer and the commit or tag headers, replaces
> - * the idents in the headers with their canonical versions using the mailmap mechanism.
> - */
> -void apply_mailmap_to_header(struct strbuf *, const char **, struct string_list *);
> -
> -/*
> - * Compare split idents for equality or strict ordering. Note that we
> - * compare only the ident part of the line, ignoring any timestamp.
> - *
> - * Because there are two fields, we must choose one as the primary key; we
> - * currently arbitrarily pick the email.
> - */
> -int ident_cmp(const struct ident_split *, const struct ident_split *);
> -
> -struct cache_def {
> -	struct strbuf path;
> -	int flags;
> -	int track_flags;
> -	int prefix_len_stat_func;
> -};
> -#define CACHE_DEF_INIT { \
> -	.path = STRBUF_INIT, \
> -}
> -static inline void cache_def_clear(struct cache_def *cache)
> -{
> -	strbuf_release(&cache->path);
> -}
> -
> -int has_symlink_leading_path(const char *name, int len);
> -int threaded_has_symlink_leading_path(struct cache_def *, const char *, int);
> -int check_leading_path(const char *name, int len, int warn_on_lstat_err);
> -int has_dirs_only_path(const char *name, int len, int prefix_len);
> -void invalidate_lstat_cache(void);
> -void schedule_dir_for_removal(const char *name, int len);
> -void remove_scheduled_dirs(void);
> -
> -struct pack_window {
> -	struct pack_window *next;
> -	unsigned char *base;
> -	off_t offset;
> -	size_t len;
> -	unsigned int last_used;
> -	unsigned int inuse_cnt;
> -};
> -
> -struct pack_entry {
> -	off_t offset;
> -	struct packed_git *p;
> -};
> -
> -/*
> - * Create a temporary file rooted in the object database directory, or
> - * die on failure. The filename is taken from "pattern", which should have the
> - * usual "XXXXXX" trailer, and the resulting filename is written into the
> - * "template" buffer. Returns the open descriptor.
> - */
> -int odb_mkstemp(struct strbuf *temp_filename, const char *pattern);
> -
> -/*
> - * Create a pack .keep file named "name" (which should generally be the output
> - * of odb_pack_name). Returns a file descriptor opened for writing, or -1 on
> - * error.
> - */
> -int odb_pack_keep(const char *name);
> -
> -/*
> - * Set this to 0 to prevent oid_object_info_extended() from fetching missing
> - * blobs. This has a difference only if extensions.partialClone is set.
> - *
> - * Its default value is 1.
> - */
> -extern int fetch_if_missing;
> -
> -/* Dumb servers support */
> -int update_server_info(int);
> -
> -const char *get_log_output_encoding(void);
> -const char *get_commit_output_encoding(void);
> -
> -int committer_ident_sufficiently_given(void);
> -int author_ident_sufficiently_given(void);
> -
> -extern const char *git_commit_encoding;
> -extern const char *git_log_output_encoding;
> -extern const char *git_mailmap_file;
> -extern const char *git_mailmap_blob;
> -
> -/* IO helper functions */
> -void maybe_flush_or_die(FILE *, const char *);
> -__attribute__((format (printf, 2, 3)))
> -void fprintf_or_die(FILE *, const char *fmt, ...);
> -void fwrite_or_die(FILE *f, const void *buf, size_t count);
> -void fflush_or_die(FILE *f);
> -
> -#define COPY_READ_ERROR (-2)
> -#define COPY_WRITE_ERROR (-3)
> -int copy_fd(int ifd, int ofd);
> -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);
> -
> -void write_or_die(int fd, const void *buf, size_t count);
> -void fsync_or_die(int fd, const char *);
> -int fsync_component(enum fsync_component component, int fd);
> -void fsync_component_or_die(enum fsync_component component, int fd, const char *msg);
> -
> -static inline int batch_fsync_enabled(enum fsync_component component)
> -{
> -	return (fsync_components & component) && (fsync_method == FSYNC_METHOD_BATCH);
> -}
> -
> -ssize_t read_in_full(int fd, void *buf, size_t count);
> -ssize_t write_in_full(int fd, const void *buf, size_t count);
> -ssize_t pread_in_full(int fd, void *buf, size_t count, off_t offset);
> -
> -static inline ssize_t write_str_in_full(int fd, const char *str)
> -{
> -	return write_in_full(fd, str, strlen(str));
> -}
> -
> -/**
> - * Open (and truncate) the file at path, write the contents of buf to it,
> - * and close it. Dies if any errors are encountered.
> - */
> -void write_file_buf(const char *path, const char *buf, size_t len);
> -
> -/**
> - * Like write_file_buf(), but format the contents into a buffer first.
> - * Additionally, write_file() will append a newline if one is not already
> - * present, making it convenient to write text files:
> - *
> - *   write_file(path, "counter: %d", ctr);
> - */
> -__attribute__((format (printf, 2, 3)))
> -void write_file(const char *path, const char *fmt, ...);
> -
> -/* pager.c */
> -void setup_pager(void);
> -int pager_in_use(void);
> -extern int pager_use_color;
> -int term_columns(void);
> -void term_clear_line(void);
> -int decimal_width(uintmax_t);
> -int check_pager_config(const char *cmd);
> -void prepare_pager_args(struct child_process *, const char *pager);
> -
> -extern const char *editor_program;
> -extern const char *askpass_program;
> -extern const char *excludes_file;
> -
> -/* base85 */
> -int decode_85(char *dst, const char *line, int linelen);
> -void encode_85(char *buf, const unsigned char *data, int bytes);
> -
> -/* pkt-line.c */
> -void packet_trace_identity(const char *prog);
> -=======
> -/*
> - * Return an abbreviated sha1 unique within this repository's object database.
> - * The result will be at least `len` characters long, and will be NUL
> - * terminated.
> - *
> - * The non-`_r` version returns a static buffer which remains valid until 4
> - * more calls to find_unique_abbrev are made.
> - *
> - * The `_r` variant writes to a buffer supplied by the caller, which must be at
> - * least `GIT_MAX_HEXSZ + 1` bytes. The return value is the number of bytes
> - * written (excluding the NUL terminator).
> - *
> - * Note that while this version avoids the static buffer, it is not fully
> - * reentrant, as it calls into other non-reentrant git code.
> - */
> -const char *repo_find_unique_abbrev(struct repository *r, const struct object_id *oid, int len);
> -#define find_unique_abbrev(oid, len) repo_find_unique_abbrev(the_repository, oid, len)
> -int repo_find_unique_abbrev_r(struct repository *r, char *hex, const struct object_id *oid, int len);
> -#define find_unique_abbrev_r(hex, oid, len) repo_find_unique_abbrev_r(the_repository, hex, oid, len)
> -
> -/* set default permissions by passing mode arguments to open(2) */
> -int git_mkstemps_mode(char *pattern, int suffix_len, int mode);
> -int git_mkstemp_mode(char *pattern, int mode);
> -
> -/*
> - * NOTE NOTE NOTE!!
> - *
> - * PERM_UMASK, OLD_PERM_GROUP and OLD_PERM_EVERYBODY enumerations must
> - * not be changed. Old repositories have core.sharedrepository written in
> - * numeric format, and therefore these values are preserved for compatibility
> - * reasons.
> - */
> -enum sharedrepo {
> -	PERM_UMASK          = 0,
> -	OLD_PERM_GROUP      = 1,
> -	OLD_PERM_EVERYBODY  = 2,
> -	PERM_GROUP          = 0660,
> -	PERM_EVERYBODY      = 0664
> -};
> -int git_config_perm(const char *var, const char *value);
> -int adjust_shared_perm(const char *path);
> -
> -/*
> - * Create the directory containing the named path, using care to be
> - * somewhat safe against races. Return one of the scld_error values to
> - * indicate success/failure. On error, set errno to describe the
> - * problem.
> - *
> - * SCLD_VANISHED indicates that one of the ancestor directories of the
> - * path existed at one point during the function call and then
> - * suddenly vanished, probably because another process pruned the
> - * directory while we were working.  To be robust against this kind of
> - * race, callers might want to try invoking the function again when it
> - * returns SCLD_VANISHED.
> - *
> - * safe_create_leading_directories() temporarily changes path while it
> - * is working but restores it before returning.
> - * safe_create_leading_directories_const() doesn't modify path, even
> - * temporarily. Both these variants adjust the permissions of the
> - * created directories to honor core.sharedRepository, so they are best
> - * suited for files inside the git dir. For working tree files, use
> - * safe_create_leading_directories_no_share() instead, as it ignores
> - * the core.sharedRepository setting.
> - */
> -enum scld_error {
> -	SCLD_OK = 0,
> -	SCLD_FAILED = -1,
> -	SCLD_PERMS = -2,
> -	SCLD_EXISTS = -3,
> -	SCLD_VANISHED = -4
> -};
> -enum scld_error safe_create_leading_directories(char *path);
> -enum scld_error safe_create_leading_directories_const(const char *path);
> -enum scld_error safe_create_leading_directories_no_share(char *path);
> -
> -int mkdir_in_gitdir(const char *path);
> -char *interpolate_path(const char *path, int real_home);
> -/* NEEDSWORK: remove this synonym once in-flight topics have migrated */
> -#define expand_user_path interpolate_path
> -const char *enter_repo(const char *path, int strict);
> -static inline int is_absolute_path(const char *path)
> -{
> -	return is_dir_sep(path[0]) || has_dos_drive_prefix(path);
> -}
> -int is_directory(const char *);
> -char *strbuf_realpath(struct strbuf *resolved, const char *path,
> -		      int die_on_error);
> -char *strbuf_realpath_forgiving(struct strbuf *resolved, const char *path,
> -				int die_on_error);
> -char *real_pathdup(const char *path, int die_on_error);
> -const char *absolute_path(const char *path);
> -char *absolute_pathdup(const char *path);
> -const char *remove_leading_path(const char *in, const char *prefix);
> -const char *relative_path(const char *in, const char *prefix, struct strbuf *sb);
> -int normalize_path_copy_len(char *dst, const char *src, int *prefix_len);
> -int normalize_path_copy(char *dst, const char *src);
> -int longest_ancestor_length(const char *path, struct string_list *prefixes);
> -char *strip_path_suffix(const char *path, const char *suffix);
> -int daemon_avoid_alias(const char *path);
> -
> -/*
> - * These functions match their is_hfs_dotgit() counterparts; see utf8.h for
> - * details.
> - */
> -int is_ntfs_dotgit(const char *name);
> -int is_ntfs_dotgitmodules(const char *name);
> -int is_ntfs_dotgitignore(const char *name);
> -int is_ntfs_dotgitattributes(const char *name);
> -int is_ntfs_dotmailmap(const char *name);
> -
> -/*
> - * Returns true iff "str" could be confused as a command-line option when
> - * passed to a sub-program like "ssh". Note that this has nothing to do with
> - * shell-quoting, which should be handled separately; we're assuming here that
> - * the string makes it verbatim to the sub-program.
> - */
> -int looks_like_command_line_option(const char *str);
> -
> -/**
> - * Return a newly allocated string with the evaluation of
> - * "$XDG_CONFIG_HOME/$subdir/$filename" if $XDG_CONFIG_HOME is non-empty, otherwise
> - * "$HOME/.config/$subdir/$filename". Return NULL upon error.
> - */
> -char *xdg_config_home_for(const char *subdir, const char *filename);
> -
> -/**
> - * Return a newly allocated string with the evaluation of
> - * "$XDG_CONFIG_HOME/git/$filename" if $XDG_CONFIG_HOME is non-empty, otherwise
> - * "$HOME/.config/git/$filename". Return NULL upon error.
> - */
> -char *xdg_config_home(const char *filename);
> -
> -/**
> - * Return a newly allocated string with the evaluation of
> - * "$XDG_CACHE_HOME/git/$filename" if $XDG_CACHE_HOME is non-empty, otherwise
> - * "$HOME/.cache/git/$filename". Return NULL upon error.
> - */
> -char *xdg_cache_home(const char *filename);
> -
> -int git_open_cloexec(const char *name, int flags);
> -#define git_open(name) git_open_cloexec(name, O_RDONLY)
> -
> -/**
> - * unpack_loose_header() initializes the data stream needed to unpack
> - * a loose object header.
> - *
> - * Returns:
> - *
> - * - ULHR_OK on success
> - * - ULHR_BAD on error
> - * - ULHR_TOO_LONG if the header was too long
> - *
> - * It will only parse up to MAX_HEADER_LEN bytes unless an optional
> - * "hdrbuf" argument is non-NULL. This is intended for use with
> - * OBJECT_INFO_ALLOW_UNKNOWN_TYPE to extract the bad type for (error)
> - * reporting. The full header will be extracted to "hdrbuf" for use
> - * with parse_loose_header(), ULHR_TOO_LONG will still be returned
> - * from this function to indicate that the header was too long.
> - */
> -enum unpack_loose_header_result {
> -	ULHR_OK,
> -	ULHR_BAD,
> -	ULHR_TOO_LONG,
> -};
> -enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,
> -						    unsigned char *map,
> -						    unsigned long mapsize,
> -						    void *buffer,
> -						    unsigned long bufsiz,
> -						    struct strbuf *hdrbuf);
> -
> -/**
> - * parse_loose_header() parses the starting "<type> <len>\0" of an
> - * object. If it doesn't follow that format -1 is returned. To check
> - * the validity of the <type> populate the "typep" in the "struct
> - * object_info". It will be OBJ_BAD if the object type is unknown. The
> - * parsed <len> can be retrieved via "oi->sizep", and from there
> - * passed to unpack_loose_rest().
> - */
> -struct object_info;
> -int parse_loose_header(const char *hdr, struct object_info *oi);
> -
> -/**
> - * With in-core object data in "buf", rehash it to make sure the
> - * object name actually matches "oid" to detect object corruption.
> - *
> - * A negative value indicates an error, usually that the OID is not
> - * what we expected, but it might also indicate another error.
> - */
> -int check_object_signature(struct repository *r, const struct object_id *oid,
> -			   void *map, unsigned long size,
> -			   enum object_type type);
> -
> -/**
> - * A streaming version of check_object_signature().
> - * Try reading the object named with "oid" using
> - * the streaming interface and rehash it to do the same.
> - */
> -int stream_object_signature(struct repository *r, const struct object_id *oid);
> -
> -int finalize_object_file(const char *tmpfile, const char *filename);
> -
> -/* Helper to check and "touch" a file */
> -int check_and_freshen_file(const char *fn, int freshen);
> -
> -extern const signed char hexval_table[256];
> -static inline unsigned int hexval(unsigned char c)
> -{
> -	return hexval_table[c];
> -}
> -
> -/*
> - * Convert two consecutive hexadecimal digits into a char.  Return a
> - * negative value on error.  Don't run over the end of short strings.
> - */
> -static inline int hex2chr(const char *s)
> -{
> -	unsigned int val = hexval(s[0]);
> -	return (val & ~0xf) ? val : (val << 4) | hexval(s[1]);
> -}
> -
> -/* Convert to/from hex/sha1 representation */
> -#define MINIMUM_ABBREV minimum_abbrev
> -#define DEFAULT_ABBREV default_abbrev
> -
> -/* used when the code does not know or care what the default abbrev is */
> -#define FALLBACK_DEFAULT_ABBREV 7
> -
> -struct object_context {
> -	unsigned short mode;
> -	/*
> -	 * symlink_path is only used by get_tree_entry_follow_symlinks,
> -	 * and only for symlinks that point outside the repository.
> -	 */
> -	struct strbuf symlink_path;
> -	/*
> -	 * If GET_OID_RECORD_PATH is set, this will record path (if any)
> -	 * found when resolving the name. The caller is responsible for
> -	 * releasing the memory.
> -	 */
> -	char *path;
> -};
> -
> -#define GET_OID_QUIETLY           01
> -#define GET_OID_COMMIT            02
> -#define GET_OID_COMMITTISH        04
> -#define GET_OID_TREE             010
> -#define GET_OID_TREEISH          020
> -#define GET_OID_BLOB             040
> -#define GET_OID_FOLLOW_SYMLINKS 0100
> -#define GET_OID_RECORD_PATH     0200
> -#define GET_OID_ONLY_TO_DIE    04000
> -#define GET_OID_REQUIRE_PATH  010000
> -
> -#define GET_OID_DISAMBIGUATORS \
> -	(GET_OID_COMMIT | GET_OID_COMMITTISH | \
> -	GET_OID_TREE | GET_OID_TREEISH | \
> -	GET_OID_BLOB)
> -
> -enum get_oid_result {
> -	FOUND = 0,
> -	MISSING_OBJECT = -1, /* The requested object is missing */
> -	SHORT_NAME_AMBIGUOUS = -2,
> -	/* The following only apply when symlinks are followed */
> -	DANGLING_SYMLINK = -4, /*
> -				* The initial symlink is there, but
> -				* (transitively) points to a missing
> -				* in-tree file
> -				*/
> -	SYMLINK_LOOP = -5,
> -	NOT_DIR = -6, /*
> -		       * Somewhere along the symlink chain, a path is
> -		       * requested which contains a file as a
> -		       * non-final element.
> -		       */
> -};
> -
> -int repo_get_oid(struct repository *r, const char *str, struct object_id *oid);
> -__attribute__((format (printf, 2, 3)))
> -int get_oidf(struct object_id *oid, const char *fmt, ...);
> -int repo_get_oid_commit(struct repository *r, const char *str, struct object_id *oid);
> -int repo_get_oid_committish(struct repository *r, const char *str, struct object_id *oid);
> -int repo_get_oid_tree(struct repository *r, const char *str, struct object_id *oid);
> -int repo_get_oid_treeish(struct repository *r, const char *str, struct object_id *oid);
> -int repo_get_oid_blob(struct repository *r, const char *str, struct object_id *oid);
> -int repo_get_oid_mb(struct repository *r, const char *str, struct object_id *oid);
> -void maybe_die_on_misspelt_object_name(struct repository *repo,
> -				       const char *name,
> -				       const char *prefix);
> -enum get_oid_result get_oid_with_context(struct repository *repo, const char *str,
> -					 unsigned flags, struct object_id *oid,
> -					 struct object_context *oc);
> -
> -#define get_oid(str, oid)		repo_get_oid(the_repository, str, oid)
> -#define get_oid_commit(str, oid)	repo_get_oid_commit(the_repository, str, oid)
> -#define get_oid_committish(str, oid)	repo_get_oid_committish(the_repository, str, oid)
> -#define get_oid_tree(str, oid)		repo_get_oid_tree(the_repository, str, oid)
> -#define get_oid_treeish(str, oid)	repo_get_oid_treeish(the_repository, str, oid)
> -#define get_oid_blob(str, oid)		repo_get_oid_blob(the_repository, str, oid)
> -#define get_oid_mb(str, oid) 		repo_get_oid_mb(the_repository, str, oid)
> -
> -typedef int each_abbrev_fn(const struct object_id *oid, void *);
> -int repo_for_each_abbrev(struct repository *r, const char *prefix, each_abbrev_fn, void *);
> -#define for_each_abbrev(prefix, fn, data) repo_for_each_abbrev(the_repository, prefix, fn, data)
> -
> -int set_disambiguate_hint_config(const char *var, const char *value);
> -
> -/*
> - * Try to read a SHA1 in hexadecimal format from the 40 characters
> - * starting at hex.  Write the 20-byte result to sha1 in binary form.
> - * Return 0 on success.  Reading stops if a NUL is encountered in the
> - * input, so it is safe to pass this function an arbitrary
> - * null-terminated string.
> - */
> -int get_sha1_hex(const char *hex, unsigned char *sha1);
> -int get_oid_hex(const char *hex, struct object_id *sha1);
> -
> -/* Like get_oid_hex, but for an arbitrary hash algorithm. */
> -int get_oid_hex_algop(const char *hex, struct object_id *oid, const struct git_hash_algo *algop);
> -
> -/*
> - * Read `len` pairs of hexadecimal digits from `hex` and write the
> - * values to `binary` as `len` bytes. Return 0 on success, or -1 if
> - * the input does not consist of hex digits).
> - */
> -int hex_to_bytes(unsigned char *binary, const char *hex, size_t len);
> -
> -/*
> - * Convert a binary hash in "unsigned char []" or an object name in
> - * "struct object_id *" to its hex equivalent. The `_r` variant is reentrant,
> - * and writes the NUL-terminated output to the buffer `out`, which must be at
> - * least `GIT_MAX_HEXSZ + 1` bytes, and returns a pointer to out for
> - * convenience.
> - *
> - * The non-`_r` variant returns a static buffer, but uses a ring of 4
> - * buffers, making it safe to make multiple calls for a single statement, like:
> - *
> - *   printf("%s -> %s", hash_to_hex(one), hash_to_hex(two));
> - *   printf("%s -> %s", oid_to_hex(one), oid_to_hex(two));
> - */
> -char *hash_to_hex_algop_r(char *buffer, const unsigned char *hash, const struct git_hash_algo *);
> -char *oid_to_hex_r(char *out, const struct object_id *oid);
> -char *hash_to_hex_algop(const unsigned char *hash, const struct git_hash_algo *);	/* static buffer result! */
> -char *hash_to_hex(const unsigned char *hash);						/* same static buffer */
> -char *oid_to_hex(const struct object_id *oid);						/* same static buffer */
> -
> -/*
> - * Parse a 40-character hexadecimal object ID starting from hex, updating the
> - * pointer specified by end when parsing stops.  The resulting object ID is
> - * stored in oid.  Returns 0 on success.  Parsing will stop on the first NUL or
> - * other invalid character.  end is only updated on success; otherwise, it is
> - * unmodified.
> - */
> -int parse_oid_hex(const char *hex, struct object_id *oid, const char **end);
> -
> -/* Like parse_oid_hex, but for an arbitrary hash algorithm. */
> -int parse_oid_hex_algop(const char *hex, struct object_id *oid, const char **end,
> -			const struct git_hash_algo *algo);
> -
> -
> -/*
> - * These functions work like get_oid_hex and parse_oid_hex, but they will parse
> - * a hex value for any algorithm. The algorithm is detected based on the length
> - * and the algorithm in use is returned. If this is not a hex object ID in any
> - * algorithm, returns GIT_HASH_UNKNOWN.
> - */
> -int get_oid_hex_any(const char *hex, struct object_id *oid);
> -int parse_oid_hex_any(const char *hex, struct object_id *oid, const char **end);
> -
> -/*
> - * This reads short-hand syntax that not only evaluates to a commit
> - * object name, but also can act as if the end user spelled the name
> - * of the branch from the command line.
> - *
> - * - "@{-N}" finds the name of the Nth previous branch we were on, and
> - *   places the name of the branch in the given buf and returns the
> - *   number of characters parsed if successful.
> - *
> - * - "<branch>@{upstream}" finds the name of the other ref that
> - *   <branch> is configured to merge with (missing <branch> defaults
> - *   to the current branch), and places the name of the branch in the
> - *   given buf and returns the number of characters parsed if
> - *   successful.
> - *
> - * If the input is not of the accepted format, it returns a negative
> - * number to signal an error.
> - *
> - * If the input was ok but there are not N branch switches in the
> - * reflog, it returns 0.
> - */
> -#define INTERPRET_BRANCH_LOCAL (1<<0)
> -#define INTERPRET_BRANCH_REMOTE (1<<1)
> -#define INTERPRET_BRANCH_HEAD (1<<2)
> -struct interpret_branch_name_options {
> -	/*
> -	 * If "allowed" is non-zero, it is a treated as a bitfield of allowable
> -	 * expansions: local branches ("refs/heads/"), remote branches
> -	 * ("refs/remotes/"), or "HEAD". If no "allowed" bits are set, any expansion is
> -	 * allowed, even ones to refs outside of those namespaces.
> -	 */
> -	unsigned allowed;
> -
> -	/*
> -	 * If ^{upstream} or ^{push} (or equivalent) is requested, and the
> -	 * branch in question does not have such a reference, return -1 instead
> -	 * of die()-ing.
> -	 */
> -	unsigned nonfatal_dangling_mark : 1;
> -};
> -int repo_interpret_branch_name(struct repository *r,
> -			       const char *str, int len,
> -			       struct strbuf *buf,
> -			       const struct interpret_branch_name_options *options);
> -#define interpret_branch_name(str, len, buf, options) \
> -	repo_interpret_branch_name(the_repository, str, len, buf, options)
> -
> -int validate_headref(const char *ref);
> -
> -int base_name_compare(const char *name1, size_t len1, int mode1,
> -		      const char *name2, size_t len2, int mode2);
> -int df_name_compare(const char *name1, size_t len1, int mode1,
> -		    const char *name2, size_t len2, int mode2);
> -int name_compare(const char *name1, size_t len1, const char *name2, size_t len2);
> -int cache_name_stage_compare(const char *name1, int len1, int stage1, const char *name2, int len2, int stage2);
> -
> -void *read_object_with_reference(struct repository *r,
> -				 const struct object_id *oid,
> -				 enum object_type required_type,
> -				 unsigned long *size,
> -				 struct object_id *oid_ret);
> -
> -struct object *repo_peel_to_type(struct repository *r,
> -				 const char *name, int namelen,
> -				 struct object *o, enum object_type);
> -#define peel_to_type(name, namelen, obj, type) \
> -	repo_peel_to_type(the_repository, name, namelen, obj, type)
> -
> -#define IDENT_STRICT	       1
> -#define IDENT_NO_DATE	       2
> -#define IDENT_NO_NAME	       4
> -
> -enum want_ident {
> -	WANT_BLANK_IDENT,
> -	WANT_AUTHOR_IDENT,
> -	WANT_COMMITTER_IDENT
> -};
> -
> -const char *git_author_info(int);
> -const char *git_committer_info(int);
> -const char *fmt_ident(const char *name, const char *email,
> -		      enum want_ident whose_ident,
> -		      const char *date_str, int);
> -const char *fmt_name(enum want_ident);
> -const char *ident_default_name(void);
> -const char *ident_default_email(void);
> -const char *git_editor(void);
> -const char *git_sequence_editor(void);
> -const char *git_pager(int stdout_is_tty);
> -int is_terminal_dumb(void);
> -int git_ident_config(const char *, const char *, void *);
> -/*
> - * Prepare an ident to fall back on if the user didn't configure it.
> - */
> -void prepare_fallback_ident(const char *name, const char *email);
> -void reset_ident_date(void);
> -
> -struct ident_split {
> -	const char *name_begin;
> -	const char *name_end;
> -	const char *mail_begin;
> -	const char *mail_end;
> -	const char *date_begin;
> -	const char *date_end;
> -	const char *tz_begin;
> -	const char *tz_end;
> -};
> -/*
> - * Signals an success with 0, but time part of the result may be NULL
> - * if the input lacks timestamp and zone
> - */
> -int split_ident_line(struct ident_split *, const char *, int);
> -
> -/*
> - * Given a commit or tag object buffer and the commit or tag headers, replaces
> - * the idents in the headers with their canonical versions using the mailmap mechanism.
> - */
> -void apply_mailmap_to_header(struct strbuf *, const char **, struct string_list *);
> -
> -/*
> - * Compare split idents for equality or strict ordering. Note that we
> - * compare only the ident part of the line, ignoring any timestamp.
> - *
> - * Because there are two fields, we must choose one as the primary key; we
> - * currently arbitrarily pick the email.
> - */
> -int ident_cmp(const struct ident_split *, const struct ident_split *);
> -
> -struct cache_def {
> -	struct strbuf path;
> -	int flags;
> -	int track_flags;
> -	int prefix_len_stat_func;
> -};
> -#define CACHE_DEF_INIT { \
> -	.path = STRBUF_INIT, \
> -}
> -static inline void cache_def_clear(struct cache_def *cache)
> -{
> -	strbuf_release(&cache->path);
> -}
> -
> -int has_symlink_leading_path(const char *name, int len);
> -int threaded_has_symlink_leading_path(struct cache_def *, const char *, int);
> -int check_leading_path(const char *name, int len, int warn_on_lstat_err);
> -int has_dirs_only_path(const char *name, int len, int prefix_len);
> -void invalidate_lstat_cache(void);
> -void schedule_dir_for_removal(const char *name, int len);
> -void remove_scheduled_dirs(void);
> -
> -struct pack_window {
> -	struct pack_window *next;
> -	unsigned char *base;
> -	off_t offset;
> -	size_t len;
> -	unsigned int last_used;
> -	unsigned int inuse_cnt;
> -};
> -
> -struct pack_entry {
> -	off_t offset;
> -	struct packed_git *p;
> -};
> -
> -/*
> - * Create a temporary file rooted in the object database directory, or
> - * die on failure. The filename is taken from "pattern", which should have the
> - * usual "XXXXXX" trailer, and the resulting filename is written into the
> - * "template" buffer. Returns the open descriptor.
> - */
> -int odb_mkstemp(struct strbuf *temp_filename, const char *pattern);
> -
> -/*
> - * Create a pack .keep file named "name" (which should generally be the output
> - * of odb_pack_name). Returns a file descriptor opened for writing, or -1 on
> - * error.
> - */
> -int odb_pack_keep(const char *name);
> -
> -/*
> - * Set this to 0 to prevent oid_object_info_extended() from fetching missing
> - * blobs. This has a difference only if extensions.partialClone is set.
> - *
> - * Its default value is 1.
> - */
> -extern int fetch_if_missing;
> -
> -/* Dumb servers support */
> -int update_server_info(int);
> -
> -const char *get_log_output_encoding(void);
> -const char *get_commit_output_encoding(void);
> -
> -int committer_ident_sufficiently_given(void);
> -int author_ident_sufficiently_given(void);
> -
> -extern const char *git_commit_encoding;
> -extern const char *git_log_output_encoding;
> -extern const char *git_mailmap_file;
> -extern const char *git_mailmap_blob;
> -
> -/* IO helper functions */
> -void maybe_flush_or_die(FILE *, const char *);
> -__attribute__((format (printf, 2, 3)))
> -void fprintf_or_die(FILE *, const char *fmt, ...);
> -void fwrite_or_die(FILE *f, const void *buf, size_t count);
> -void fflush_or_die(FILE *f);
> -
> -#define COPY_READ_ERROR (-2)
> -#define COPY_WRITE_ERROR (-3)
> -int copy_fd(int ifd, int ofd);
> -int copy_file(const char *dst, const char *src, int mode);
> -int copy_file_with_time(const char *dst, const char *src, int mode);
> -
> -void write_or_die(int fd, const void *buf, size_t count);
> -void fsync_or_die(int fd, const char *);
> -int fsync_component(enum fsync_component component, int fd);
> -void fsync_component_or_die(enum fsync_component component, int fd, const char *msg);
> -
> -static inline int batch_fsync_enabled(enum fsync_component component)
> -{
> -	return (fsync_components & component) && (fsync_method == FSYNC_METHOD_BATCH);
> -}
> -
> -ssize_t read_in_full(int fd, void *buf, size_t count);
> -ssize_t write_in_full(int fd, const void *buf, size_t count);
> -ssize_t pread_in_full(int fd, void *buf, size_t count, off_t offset);
> -
> -static inline ssize_t write_str_in_full(int fd, const char *str)
> -{
> -	return write_in_full(fd, str, strlen(str));
> -}
> -
> -/**
> - * Open (and truncate) the file at path, write the contents of buf to it,
> - * and close it. Dies if any errors are encountered.
> - */
> -void write_file_buf(const char *path, const char *buf, size_t len);
> -
> -/**
> - * Like write_file_buf(), but format the contents into a buffer first.
> - * Additionally, write_file() will append a newline if one is not already
> - * present, making it convenient to write text files:
> - *
> - *   write_file(path, "counter: %d", ctr);
> - */
> -__attribute__((format (printf, 2, 3)))
> -void write_file(const char *path, const char *fmt, ...);
> -
> -/* pager.c */
> -void setup_pager(void);
> -int pager_in_use(void);
> -extern int pager_use_color;
> -int term_columns(void);
> -void term_clear_line(void);
> -int decimal_width(uintmax_t);
> -int check_pager_config(const char *cmd);
> -void prepare_pager_args(struct child_process *, const char *pager);
> -
> -extern const char *editor_program;
> -extern const char *askpass_program;
> -extern const char *excludes_file;
> -
> -/* base85 */
> -int decode_85(char *dst, const char *line, int linelen);
> -void encode_85(char *buf, const unsigned char *data, int bytes);
> -
> -/* pkt-line.c */
> -void packet_trace_identity(const char *prog);
> ->>>>>>> 752e921355 (Merge branch 'js/fix-clone-w-hooks-2.39' into HEAD)
>
>  /* add */
>  /*
> diff --git a/hook.c b/hook.c
> remerge CONFLICT (content): Merge conflict in hook.c
> index ce71f9d01a..b56f97949b 100644
> --- a/hook.c
> +++ b/hook.c
> @@ -5,15 +5,12 @@
>  #include "path.h"
>  #include "run-command.h"
>  #include "config.h"
> -<<<<<<< 0f15832059 (Git 2.41.1)
>  #include "strbuf.h"
>  #include "environment.h"
>  #include "setup.h"
>  #include "copy.h"
> -||||||| b9b439e0e3
> -=======
>  #include "strmap.h"
> ->>>>>>> 752e921355 (Merge branch 'js/fix-clone-w-hooks-2.39' into HEAD)
> +#include "hex.h"
>
>  static struct strset safe_hook_sha256s = STRSET_INIT;
>  static int safe_hook_sha256s_initialized;
> * js/fix-clone-w-hooks-2.42
> diff --git a/Makefile b/Makefile
> remerge CONFLICT (content): Merge conflict in Makefile
> index 5ae5f3c864..5776309365 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -2743,16 +2743,8 @@ exec-cmd.sp exec-cmd.s exec-cmd.o: EXTRA_CPPFLAGS = \
>  	'-DBINDIR="$(bindir_relative_SQ)"' \
>  	'-DFALLBACK_RUNTIME_PREFIX="$(prefix_SQ)"'
>
> -<<<<<<< babb4e5d71 (Git 2.42.2)
>  setup.sp setup.s setup.o: GIT-PREFIX
>  setup.sp setup.s setup.o: EXTRA_CPPFLAGS = \
> -||||||| 0f15832059
> -builtin/init-db.sp builtin/init-db.s builtin/init-db.o: GIT-PREFIX
> -builtin/init-db.sp builtin/init-db.s builtin/init-db.o: EXTRA_CPPFLAGS = \
> -=======
> -builtin/init-db.sp builtin/init-db.s builtin/init-db.o: GIT-PREFIX
> -setup.sp setup.s setup.o: EXTRA_CPPFLAGS = \
> ->>>>>>> c7b6b0a28f (Merge branch 'js/fix-clone-w-hooks-2.40' into HEAD)
>  	-DDEFAULT_GIT_TEMPLATE_DIR='"$(template_dir_SQ)"'
>
>  config.sp config.s config.o: GIT-PREFIX
> diff --git a/builtin/init-db.c b/builtin/init-db.c
> remerge CONFLICT (content): Merge conflict in builtin/init-db.c
> index 6df47dd078..cb727c826f 100644
> --- a/builtin/init-db.c
> +++ b/builtin/init-db.c
> @@ -12,920 +12,7 @@
>  #include "parse-options.h"
>  #include "path.h"
>  #include "setup.h"
> -<<<<<<< babb4e5d71 (Git 2.42.2)
>  #include "strbuf.h"
> -||||||| 0f15832059
> -#include "worktree.h"
> -#include "wrapper.h"
> -
> -#ifdef NO_TRUSTABLE_FILEMODE
> -#define TEST_FILEMODE 0
> -#else
> -#define TEST_FILEMODE 1
> -#endif
> -
> -#define GIT_DEFAULT_HASH_ENVIRONMENT "GIT_DEFAULT_HASH"
> -
> -static int init_is_bare_repository = 0;
> -static int init_shared_repository = -1;
> -
> -static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
> -			     DIR *dir)
> -{
> -	size_t path_baselen = path->len;
> -	size_t template_baselen = template_path->len;
> -	struct dirent *de;
> -
> -	/* Note: if ".git/hooks" file exists in the repository being
> -	 * re-initialized, /etc/core-git/templates/hooks/update would
> -	 * cause "git init" to fail here.  I think this is sane but
> -	 * it means that the set of templates we ship by default, along
> -	 * with the way the namespace under .git/ is organized, should
> -	 * be really carefully chosen.
> -	 */
> -	safe_create_dir(path->buf, 1);
> -	while ((de = readdir(dir)) != NULL) {
> -		struct stat st_git, st_template;
> -		int exists = 0;
> -
> -		strbuf_setlen(path, path_baselen);
> -		strbuf_setlen(template_path, template_baselen);
> -
> -		if (de->d_name[0] == '.')
> -			continue;
> -		strbuf_addstr(path, de->d_name);
> -		strbuf_addstr(template_path, de->d_name);
> -		if (lstat(path->buf, &st_git)) {
> -			if (errno != ENOENT)
> -				die_errno(_("cannot stat '%s'"), path->buf);
> -		}
> -		else
> -			exists = 1;
> -
> -		if (lstat(template_path->buf, &st_template))
> -			die_errno(_("cannot stat template '%s'"), template_path->buf);
> -
> -		if (S_ISDIR(st_template.st_mode)) {
> -			DIR *subdir = opendir(template_path->buf);
> -			if (!subdir)
> -				die_errno(_("cannot opendir '%s'"), template_path->buf);
> -			strbuf_addch(path, '/');
> -			strbuf_addch(template_path, '/');
> -			copy_templates_1(path, template_path, subdir);
> -			closedir(subdir);
> -		}
> -		else if (exists)
> -			continue;
> -		else if (S_ISLNK(st_template.st_mode)) {
> -			struct strbuf lnk = STRBUF_INIT;
> -			if (strbuf_readlink(&lnk, template_path->buf,
> -					    st_template.st_size) < 0)
> -				die_errno(_("cannot readlink '%s'"), template_path->buf);
> -			if (symlink(lnk.buf, path->buf))
> -				die_errno(_("cannot symlink '%s' '%s'"),
> -					  lnk.buf, path->buf);
> -			strbuf_release(&lnk);
> -		}
> -		else if (S_ISREG(st_template.st_mode)) {
> -			if (copy_file(path->buf, template_path->buf, st_template.st_mode))
> -				die_errno(_("cannot copy '%s' to '%s'"),
> -					  template_path->buf, path->buf);
> -		}
> -		else
> -			error(_("ignoring template %s"), template_path->buf);
> -	}
> -}
> -
> -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;
> -	struct repository_format template_format = REPOSITORY_FORMAT_INIT;
> -	struct strbuf err = STRBUF_INIT;
> -	DIR *dir;
> -	char *to_free = NULL;
> -
> -	if (!template_dir || !*template_dir)
> -		return;
> -
> -	strbuf_addstr(&template_path, template_dir);
> -	strbuf_complete(&template_path, '/');
> -	template_len = template_path.len;
> -
> -	dir = opendir(template_path.buf);
> -	if (!dir) {
> -		warning(_("templates not found in %s"), template_dir);
> -		goto free_return;
> -	}
> -
> -	/* Make sure that template is from the correct vintage */
> -	strbuf_addstr(&template_path, "config");
> -	read_repository_format(&template_format, template_path.buf);
> -	strbuf_setlen(&template_path, template_len);
> -
> -	/*
> -	 * No mention of version at all is OK, but anything else should be
> -	 * verified.
> -	 */
> -	if (template_format.version >= 0 &&
> -	    verify_repository_format(&template_format, &err) < 0) {
> -		warning(_("not copying templates from '%s': %s"),
> -			  template_dir, err.buf);
> -		strbuf_release(&err);
> -		goto close_free_return;
> -	}
> -
> -	strbuf_addstr(&path, get_git_common_dir());
> -	strbuf_complete(&path, '/');
> -	copy_templates_1(&path, &template_path, dir);
> -close_free_return:
> -	closedir(dir);
> -free_return:
> -	free(to_free);
> -	strbuf_release(&path);
> -	strbuf_release(&template_path);
> -	clear_repository_format(&template_format);
> -}
> -
> -/*
> - * If the git_dir is not directly inside the working tree, then git will not
> - * find it by default, and we need to set the worktree explicitly.
> - */
> -static int needs_work_tree_config(const char *git_dir, const char *work_tree)
> -{
> -	if (!strcmp(work_tree, "/") && !strcmp(git_dir, "/.git"))
> -		return 0;
> -	if (skip_prefix(git_dir, work_tree, &git_dir) &&
> -	    !strcmp(git_dir, "/.git"))
> -		return 0;
> -	return 1;
> -}
> -
> -void initialize_repository_version(int hash_algo, int reinit)
> -{
> -	char repo_version_string[10];
> -	int repo_version = GIT_REPO_VERSION;
> -
> -	if (hash_algo != GIT_HASH_SHA1)
> -		repo_version = GIT_REPO_VERSION_READ;
> -
> -	/* This forces creation of new config file */
> -	xsnprintf(repo_version_string, sizeof(repo_version_string),
> -		  "%d", repo_version);
> -	git_config_set("core.repositoryformatversion", repo_version_string);
> -
> -	if (hash_algo != GIT_HASH_SHA1)
> -		git_config_set("extensions.objectformat",
> -			       hash_algos[hash_algo].name);
> -	else if (reinit)
> -		git_config_set_gently("extensions.objectformat", NULL);
> -}
> -
> -static int create_default_files(const char *template_path,
> -				const char *original_git_dir,
> -				const char *initial_branch,
> -				const struct repository_format *fmt,
> -				int quiet)
> -{
> -	struct stat st1;
> -	struct strbuf buf = STRBUF_INIT;
> -	char *path;
> -	char junk[2];
> -	int reinit;
> -	int filemode;
> -	struct strbuf err = STRBUF_INIT;
> -	const char *work_tree = get_git_work_tree();
> -
> -	/*
> -	 * First copy the templates -- we might have the default
> -	 * config file there, in which case we would want to read
> -	 * from it after installing.
> -	 *
> -	 * Before reading that config, we also need to clear out any cached
> -	 * values (since we've just potentially changed what's available on
> -	 * disk).
> -	 */
> -	copy_templates(template_path);
> -	git_config_clear();
> -	reset_shared_repository();
> -	git_config(git_default_config, NULL);
> -
> -	/*
> -	 * We must make sure command-line options continue to override any
> -	 * values we might have just re-read from the config.
> -	 */
> -	is_bare_repository_cfg = init_is_bare_repository || !work_tree;
> -	if (init_shared_repository != -1)
> -		set_shared_repository(init_shared_repository);
> -
> -	/*
> -	 * We would have created the above under user's umask -- under
> -	 * shared-repository settings, we would need to fix them up.
> -	 */
> -	if (get_shared_repository()) {
> -		adjust_shared_perm(get_git_dir());
> -	}
> -
> -	/*
> -	 * We need to create a "refs" dir in any case so that older
> -	 * versions of git can tell that this is a repository.
> -	 */
> -	safe_create_dir(git_path("refs"), 1);
> -	adjust_shared_perm(git_path("refs"));
> -
> -	if (refs_init_db(&err))
> -		die("failed to set up refs db: %s", err.buf);
> -
> -	/*
> -	 * Point the HEAD symref to the initial branch with if HEAD does
> -	 * not yet exist.
> -	 */
> -	path = git_path_buf(&buf, "HEAD");
> -	reinit = (!access(path, R_OK)
> -		  || readlink(path, junk, sizeof(junk)-1) != -1);
> -	if (!reinit) {
> -		char *ref;
> -
> -		if (!initial_branch)
> -			initial_branch = git_default_branch_name(quiet);
> -
> -		ref = xstrfmt("refs/heads/%s", initial_branch);
> -		if (check_refname_format(ref, 0) < 0)
> -			die(_("invalid initial branch name: '%s'"),
> -			    initial_branch);
> -
> -		if (create_symref("HEAD", ref, NULL) < 0)
> -			exit(1);
> -		free(ref);
> -	}
> -
> -	initialize_repository_version(fmt->hash_algo, 0);
> -
> -	/* Check filemode trustability */
> -	path = git_path_buf(&buf, "config");
> -	filemode = TEST_FILEMODE;
> -	if (TEST_FILEMODE && !lstat(path, &st1)) {
> -		struct stat st2;
> -		filemode = (!chmod(path, st1.st_mode ^ S_IXUSR) &&
> -				!lstat(path, &st2) &&
> -				st1.st_mode != st2.st_mode &&
> -				!chmod(path, st1.st_mode));
> -		if (filemode && !reinit && (st1.st_mode & S_IXUSR))
> -			filemode = 0;
> -	}
> -	git_config_set("core.filemode", filemode ? "true" : "false");
> -
> -	if (is_bare_repository())
> -		git_config_set("core.bare", "true");
> -	else {
> -		git_config_set("core.bare", "false");
> -		/* allow template config file to override the default */
> -		if (log_all_ref_updates == LOG_REFS_UNSET)
> -			git_config_set("core.logallrefupdates", "true");
> -		if (needs_work_tree_config(original_git_dir, work_tree))
> -			git_config_set("core.worktree", work_tree);
> -	}
> -
> -	if (!reinit) {
> -		/* Check if symlink is supported in the work tree */
> -		path = git_path_buf(&buf, "tXXXXXX");
> -		if (!close(xmkstemp(path)) &&
> -		    !unlink(path) &&
> -		    !symlink("testing", path) &&
> -		    !lstat(path, &st1) &&
> -		    S_ISLNK(st1.st_mode))
> -			unlink(path); /* good */
> -		else
> -			git_config_set("core.symlinks", "false");
> -
> -		/* Check if the filesystem is case-insensitive */
> -		path = git_path_buf(&buf, "CoNfIg");
> -		if (!access(path, F_OK))
> -			git_config_set("core.ignorecase", "true");
> -		probe_utf8_pathname_composition();
> -	}
> -
> -	strbuf_release(&buf);
> -	return reinit;
> -}
> -
> -static void create_object_directory(void)
> -{
> -	struct strbuf path = STRBUF_INIT;
> -	size_t baselen;
> -
> -	strbuf_addstr(&path, get_object_directory());
> -	baselen = path.len;
> -
> -	safe_create_dir(path.buf, 1);
> -
> -	strbuf_setlen(&path, baselen);
> -	strbuf_addstr(&path, "/pack");
> -	safe_create_dir(path.buf, 1);
> -
> -	strbuf_setlen(&path, baselen);
> -	strbuf_addstr(&path, "/info");
> -	safe_create_dir(path.buf, 1);
> -
> -	strbuf_release(&path);
> -}
> -
> -static void separate_git_dir(const char *git_dir, const char *git_link)
> -{
> -	struct stat st;
> -
> -	if (!stat(git_link, &st)) {
> -		const char *src;
> -
> -		if (S_ISREG(st.st_mode))
> -			src = read_gitfile(git_link);
> -		else if (S_ISDIR(st.st_mode))
> -			src = git_link;
> -		else
> -			die(_("unable to handle file type %d"), (int)st.st_mode);
> -
> -		if (rename(src, git_dir))
> -			die_errno(_("unable to move %s to %s"), src, git_dir);
> -		repair_worktrees(NULL, NULL);
> -	}
> -
> -	write_file(git_link, "gitdir: %s", git_dir);
> -}
> -
> -static void validate_hash_algorithm(struct repository_format *repo_fmt, int hash)
> -{
> -	const char *env = getenv(GIT_DEFAULT_HASH_ENVIRONMENT);
> -	/*
> -	 * If we already have an initialized repo, don't allow the user to
> -	 * specify a different algorithm, as that could cause corruption.
> -	 * Otherwise, if the user has specified one on the command line, use it.
> -	 */
> -	if (repo_fmt->version >= 0 && hash != GIT_HASH_UNKNOWN && hash != repo_fmt->hash_algo)
> -		die(_("attempt to reinitialize repository with different hash"));
> -	else if (hash != GIT_HASH_UNKNOWN)
> -		repo_fmt->hash_algo = hash;
> -	else if (env) {
> -		int env_algo = hash_algo_by_name(env);
> -		if (env_algo == GIT_HASH_UNKNOWN)
> -			die(_("unknown hash algorithm '%s'"), env);
> -		repo_fmt->hash_algo = env_algo;
> -	}
> -}
> -
> -int init_db(const char *git_dir, const char *real_git_dir,
> -	    const char *template_dir, int hash, const char *initial_branch,
> -	    unsigned int flags)
> -{
> -	int reinit;
> -	int exist_ok = flags & INIT_DB_EXIST_OK;
> -	char *original_git_dir = real_pathdup(git_dir, 1);
> -	struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;
> -
> -	if (real_git_dir) {
> -		struct stat st;
> -
> -		if (!exist_ok && !stat(git_dir, &st))
> -			die(_("%s already exists"), git_dir);
> -
> -		if (!exist_ok && !stat(real_git_dir, &st))
> -			die(_("%s already exists"), real_git_dir);
> -
> -		set_git_dir(real_git_dir, 1);
> -		git_dir = get_git_dir();
> -		separate_git_dir(git_dir, original_git_dir);
> -	}
> -	else {
> -		set_git_dir(git_dir, 1);
> -		git_dir = get_git_dir();
> -	}
> -	startup_info->have_repository = 1;
> -
> -	/* Ensure `core.hidedotfiles` is processed */
> -	git_config(platform_core_config, NULL);
> -
> -	safe_create_dir(git_dir, 0);
> -
> -	init_is_bare_repository = is_bare_repository();
> -
> -	/* Check to see if the repository version is right.
> -	 * Note that a newly created repository does not have
> -	 * config file, so this will not fail.  What we are catching
> -	 * is an attempt to reinitialize new repository with an old tool.
> -	 */
> -	check_repository_format(&repo_fmt);
> -
> -	validate_hash_algorithm(&repo_fmt, hash);
> -
> -	reinit = create_default_files(template_dir, original_git_dir,
> -				      initial_branch, &repo_fmt,
> -				      flags & INIT_DB_QUIET);
> -	if (reinit && initial_branch)
> -		warning(_("re-init: ignored --initial-branch=%s"),
> -			initial_branch);
> -
> -	create_object_directory();
> -
> -	if (get_shared_repository()) {
> -		char buf[10];
> -		/* We do not spell "group" and such, so that
> -		 * the configuration can be read by older version
> -		 * of git. Note, we use octal numbers for new share modes,
> -		 * and compatibility values for PERM_GROUP and
> -		 * PERM_EVERYBODY.
> -		 */
> -		if (get_shared_repository() < 0)
> -			/* force to the mode value */
> -			xsnprintf(buf, sizeof(buf), "0%o", -get_shared_repository());
> -		else if (get_shared_repository() == PERM_GROUP)
> -			xsnprintf(buf, sizeof(buf), "%d", OLD_PERM_GROUP);
> -		else if (get_shared_repository() == PERM_EVERYBODY)
> -			xsnprintf(buf, sizeof(buf), "%d", OLD_PERM_EVERYBODY);
> -		else
> -			BUG("invalid value for shared_repository");
> -		git_config_set("core.sharedrepository", buf);
> -		git_config_set("receive.denyNonFastforwards", "true");
> -	}
> -
> -	if (!(flags & INIT_DB_QUIET)) {
> -		int len = strlen(git_dir);
> -
> -		if (reinit)
> -			printf(get_shared_repository()
> -			       ? _("Reinitialized existing shared Git repository in %s%s\n")
> -			       : _("Reinitialized existing Git repository in %s%s\n"),
> -			       git_dir, len && git_dir[len-1] != '/' ? "/" : "");
> -		else
> -			printf(get_shared_repository()
> -			       ? _("Initialized empty shared Git repository in %s%s\n")
> -			       : _("Initialized empty Git repository in %s%s\n"),
> -			       git_dir, len && git_dir[len-1] != '/' ? "/" : "");
> -	}
> -
> -	free(original_git_dir);
> -	return 0;
> -}
> -=======
> -#include "worktree.h"
> -#include "wrapper.h"
> -#include "run-command.h"
> -#include "hook.h"
> -
> -#ifdef NO_TRUSTABLE_FILEMODE
> -#define TEST_FILEMODE 0
> -#else
> -#define TEST_FILEMODE 1
> -#endif
> -
> -#define GIT_DEFAULT_HASH_ENVIRONMENT "GIT_DEFAULT_HASH"
> -
> -static int init_is_bare_repository = 0;
> -static int init_shared_repository = -1;
> -
> -static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
> -			     DIR *dir)
> -{
> -	size_t path_baselen = path->len;
> -	size_t template_baselen = template_path->len;
> -	struct dirent *de;
> -	int is_hooks_dir = ends_with(template_path->buf, "/hooks/");
> -
> -	/* Note: if ".git/hooks" file exists in the repository being
> -	 * re-initialized, /etc/core-git/templates/hooks/update would
> -	 * cause "git init" to fail here.  I think this is sane but
> -	 * it means that the set of templates we ship by default, along
> -	 * with the way the namespace under .git/ is organized, should
> -	 * be really carefully chosen.
> -	 */
> -	safe_create_dir(path->buf, 1);
> -	while ((de = readdir(dir)) != NULL) {
> -		struct stat st_git, st_template;
> -		int exists = 0;
> -
> -		strbuf_setlen(path, path_baselen);
> -		strbuf_setlen(template_path, template_baselen);
> -
> -		if (de->d_name[0] == '.')
> -			continue;
> -		strbuf_addstr(path, de->d_name);
> -		strbuf_addstr(template_path, de->d_name);
> -		if (lstat(path->buf, &st_git)) {
> -			if (errno != ENOENT)
> -				die_errno(_("cannot stat '%s'"), path->buf);
> -		}
> -		else
> -			exists = 1;
> -
> -		if (lstat(template_path->buf, &st_template))
> -			die_errno(_("cannot stat template '%s'"), template_path->buf);
> -
> -		if (S_ISDIR(st_template.st_mode)) {
> -			DIR *subdir = opendir(template_path->buf);
> -			if (!subdir)
> -				die_errno(_("cannot opendir '%s'"), template_path->buf);
> -			strbuf_addch(path, '/');
> -			strbuf_addch(template_path, '/');
> -			copy_templates_1(path, template_path, subdir);
> -			closedir(subdir);
> -		}
> -		else if (exists)
> -			continue;
> -		else if (S_ISLNK(st_template.st_mode)) {
> -			struct strbuf lnk = STRBUF_INIT;
> -			if (strbuf_readlink(&lnk, template_path->buf,
> -					    st_template.st_size) < 0)
> -				die_errno(_("cannot readlink '%s'"), template_path->buf);
> -			if (symlink(lnk.buf, path->buf))
> -				die_errno(_("cannot symlink '%s' '%s'"),
> -					  lnk.buf, path->buf);
> -			strbuf_release(&lnk);
> -		}
> -		else if (S_ISREG(st_template.st_mode)) {
> -			if (is_hooks_dir &&
> -			    is_executable(template_path->buf))
> -				add_safe_hook(template_path->buf);
> -
> -			if (copy_file(path->buf, template_path->buf, st_template.st_mode))
> -				die_errno(_("cannot copy '%s' to '%s'"),
> -					  template_path->buf, path->buf);
> -		}
> -		else
> -			error(_("ignoring template %s"), template_path->buf);
> -	}
> -}
> -
> -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;
> -	struct repository_format template_format = REPOSITORY_FORMAT_INIT;
> -	struct strbuf err = STRBUF_INIT;
> -	DIR *dir;
> -	char *to_free = NULL;
> -
> -	if (!template_dir || !*template_dir)
> -		return;
> -
> -	strbuf_addstr(&template_path, template_dir);
> -	strbuf_complete(&template_path, '/');
> -	template_len = template_path.len;
> -
> -	dir = opendir(template_path.buf);
> -	if (!dir) {
> -		warning(_("templates not found in %s"), template_dir);
> -		goto free_return;
> -	}
> -
> -	/* Make sure that template is from the correct vintage */
> -	strbuf_addstr(&template_path, "config");
> -	read_repository_format(&template_format, template_path.buf);
> -	strbuf_setlen(&template_path, template_len);
> -
> -	/*
> -	 * No mention of version at all is OK, but anything else should be
> -	 * verified.
> -	 */
> -	if (template_format.version >= 0 &&
> -	    verify_repository_format(&template_format, &err) < 0) {
> -		warning(_("not copying templates from '%s': %s"),
> -			  template_dir, err.buf);
> -		strbuf_release(&err);
> -		goto close_free_return;
> -	}
> -
> -	strbuf_addstr(&path, get_git_common_dir());
> -	strbuf_complete(&path, '/');
> -	copy_templates_1(&path, &template_path, dir);
> -close_free_return:
> -	closedir(dir);
> -free_return:
> -	free(to_free);
> -	strbuf_release(&path);
> -	strbuf_release(&template_path);
> -	clear_repository_format(&template_format);
> -}
> -
> -/*
> - * If the git_dir is not directly inside the working tree, then git will not
> - * find it by default, and we need to set the worktree explicitly.
> - */
> -static int needs_work_tree_config(const char *git_dir, const char *work_tree)
> -{
> -	if (!strcmp(work_tree, "/") && !strcmp(git_dir, "/.git"))
> -		return 0;
> -	if (skip_prefix(git_dir, work_tree, &git_dir) &&
> -	    !strcmp(git_dir, "/.git"))
> -		return 0;
> -	return 1;
> -}
> -
> -void initialize_repository_version(int hash_algo, int reinit)
> -{
> -	char repo_version_string[10];
> -	int repo_version = GIT_REPO_VERSION;
> -
> -	if (hash_algo != GIT_HASH_SHA1)
> -		repo_version = GIT_REPO_VERSION_READ;
> -
> -	/* This forces creation of new config file */
> -	xsnprintf(repo_version_string, sizeof(repo_version_string),
> -		  "%d", repo_version);
> -	git_config_set("core.repositoryformatversion", repo_version_string);
> -
> -	if (hash_algo != GIT_HASH_SHA1)
> -		git_config_set("extensions.objectformat",
> -			       hash_algos[hash_algo].name);
> -	else if (reinit)
> -		git_config_set_gently("extensions.objectformat", NULL);
> -}
> -
> -static int create_default_files(const char *template_path,
> -				const char *original_git_dir,
> -				const char *initial_branch,
> -				const struct repository_format *fmt,
> -				int quiet)
> -{
> -	struct stat st1;
> -	struct strbuf buf = STRBUF_INIT;
> -	char *path;
> -	char junk[2];
> -	int reinit;
> -	int filemode;
> -	struct strbuf err = STRBUF_INIT;
> -	const char *work_tree = get_git_work_tree();
> -
> -	/*
> -	 * First copy the templates -- we might have the default
> -	 * config file there, in which case we would want to read
> -	 * from it after installing.
> -	 *
> -	 * Before reading that config, we also need to clear out any cached
> -	 * values (since we've just potentially changed what's available on
> -	 * disk).
> -	 */
> -	copy_templates(template_path);
> -	git_config_clear();
> -	reset_shared_repository();
> -	git_config(git_default_config, NULL);
> -
> -	/*
> -	 * We must make sure command-line options continue to override any
> -	 * values we might have just re-read from the config.
> -	 */
> -	is_bare_repository_cfg = init_is_bare_repository || !work_tree;
> -	if (init_shared_repository != -1)
> -		set_shared_repository(init_shared_repository);
> -
> -	/*
> -	 * We would have created the above under user's umask -- under
> -	 * shared-repository settings, we would need to fix them up.
> -	 */
> -	if (get_shared_repository()) {
> -		adjust_shared_perm(get_git_dir());
> -	}
> -
> -	/*
> -	 * We need to create a "refs" dir in any case so that older
> -	 * versions of git can tell that this is a repository.
> -	 */
> -	safe_create_dir(git_path("refs"), 1);
> -	adjust_shared_perm(git_path("refs"));
> -
> -	if (refs_init_db(&err))
> -		die("failed to set up refs db: %s", err.buf);
> -
> -	/*
> -	 * Point the HEAD symref to the initial branch with if HEAD does
> -	 * not yet exist.
> -	 */
> -	path = git_path_buf(&buf, "HEAD");
> -	reinit = (!access(path, R_OK)
> -		  || readlink(path, junk, sizeof(junk)-1) != -1);
> -	if (!reinit) {
> -		char *ref;
> -
> -		if (!initial_branch)
> -			initial_branch = git_default_branch_name(quiet);
> -
> -		ref = xstrfmt("refs/heads/%s", initial_branch);
> -		if (check_refname_format(ref, 0) < 0)
> -			die(_("invalid initial branch name: '%s'"),
> -			    initial_branch);
> -
> -		if (create_symref("HEAD", ref, NULL) < 0)
> -			exit(1);
> -		free(ref);
> -	}
> -
> -	initialize_repository_version(fmt->hash_algo, 0);
> -
> -	/* Check filemode trustability */
> -	path = git_path_buf(&buf, "config");
> -	filemode = TEST_FILEMODE;
> -	if (TEST_FILEMODE && !lstat(path, &st1)) {
> -		struct stat st2;
> -		filemode = (!chmod(path, st1.st_mode ^ S_IXUSR) &&
> -				!lstat(path, &st2) &&
> -				st1.st_mode != st2.st_mode &&
> -				!chmod(path, st1.st_mode));
> -		if (filemode && !reinit && (st1.st_mode & S_IXUSR))
> -			filemode = 0;
> -	}
> -	git_config_set("core.filemode", filemode ? "true" : "false");
> -
> -	if (is_bare_repository())
> -		git_config_set("core.bare", "true");
> -	else {
> -		git_config_set("core.bare", "false");
> -		/* allow template config file to override the default */
> -		if (log_all_ref_updates == LOG_REFS_UNSET)
> -			git_config_set("core.logallrefupdates", "true");
> -		if (needs_work_tree_config(original_git_dir, work_tree))
> -			git_config_set("core.worktree", work_tree);
> -	}
> -
> -	if (!reinit) {
> -		/* Check if symlink is supported in the work tree */
> -		path = git_path_buf(&buf, "tXXXXXX");
> -		if (!close(xmkstemp(path)) &&
> -		    !unlink(path) &&
> -		    !symlink("testing", path) &&
> -		    !lstat(path, &st1) &&
> -		    S_ISLNK(st1.st_mode))
> -			unlink(path); /* good */
> -		else
> -			git_config_set("core.symlinks", "false");
> -
> -		/* Check if the filesystem is case-insensitive */
> -		path = git_path_buf(&buf, "CoNfIg");
> -		if (!access(path, F_OK))
> -			git_config_set("core.ignorecase", "true");
> -		probe_utf8_pathname_composition();
> -	}
> -
> -	strbuf_release(&buf);
> -	return reinit;
> -}
> -
> -static void create_object_directory(void)
> -{
> -	struct strbuf path = STRBUF_INIT;
> -	size_t baselen;
> -
> -	strbuf_addstr(&path, get_object_directory());
> -	baselen = path.len;
> -
> -	safe_create_dir(path.buf, 1);
> -
> -	strbuf_setlen(&path, baselen);
> -	strbuf_addstr(&path, "/pack");
> -	safe_create_dir(path.buf, 1);
> -
> -	strbuf_setlen(&path, baselen);
> -	strbuf_addstr(&path, "/info");
> -	safe_create_dir(path.buf, 1);
> -
> -	strbuf_release(&path);
> -}
> -
> -static void separate_git_dir(const char *git_dir, const char *git_link)
> -{
> -	struct stat st;
> -
> -	if (!stat(git_link, &st)) {
> -		const char *src;
> -
> -		if (S_ISREG(st.st_mode))
> -			src = read_gitfile(git_link);
> -		else if (S_ISDIR(st.st_mode))
> -			src = git_link;
> -		else
> -			die(_("unable to handle file type %d"), (int)st.st_mode);
> -
> -		if (rename(src, git_dir))
> -			die_errno(_("unable to move %s to %s"), src, git_dir);
> -		repair_worktrees(NULL, NULL);
> -	}
> -
> -	write_file(git_link, "gitdir: %s", git_dir);
> -}
> -
> -static void validate_hash_algorithm(struct repository_format *repo_fmt, int hash)
> -{
> -	const char *env = getenv(GIT_DEFAULT_HASH_ENVIRONMENT);
> -	/*
> -	 * If we already have an initialized repo, don't allow the user to
> -	 * specify a different algorithm, as that could cause corruption.
> -	 * Otherwise, if the user has specified one on the command line, use it.
> -	 */
> -	if (repo_fmt->version >= 0 && hash != GIT_HASH_UNKNOWN && hash != repo_fmt->hash_algo)
> -		die(_("attempt to reinitialize repository with different hash"));
> -	else if (hash != GIT_HASH_UNKNOWN)
> -		repo_fmt->hash_algo = hash;
> -	else if (env) {
> -		int env_algo = hash_algo_by_name(env);
> -		if (env_algo == GIT_HASH_UNKNOWN)
> -			die(_("unknown hash algorithm '%s'"), env);
> -		repo_fmt->hash_algo = env_algo;
> -	}
> -}
> -
> -int init_db(const char *git_dir, const char *real_git_dir,
> -	    const char *template_dir, int hash, const char *initial_branch,
> -	    unsigned int flags)
> -{
> -	int reinit;
> -	int exist_ok = flags & INIT_DB_EXIST_OK;
> -	char *original_git_dir = real_pathdup(git_dir, 1);
> -	struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;
> -
> -	if (real_git_dir) {
> -		struct stat st;
> -
> -		if (!exist_ok && !stat(git_dir, &st))
> -			die(_("%s already exists"), git_dir);
> -
> -		if (!exist_ok && !stat(real_git_dir, &st))
> -			die(_("%s already exists"), real_git_dir);
> -
> -		set_git_dir(real_git_dir, 1);
> -		git_dir = get_git_dir();
> -		separate_git_dir(git_dir, original_git_dir);
> -	}
> -	else {
> -		set_git_dir(git_dir, 1);
> -		git_dir = get_git_dir();
> -	}
> -	startup_info->have_repository = 1;
> -
> -	/* Ensure `core.hidedotfiles` is processed */
> -	git_config(platform_core_config, NULL);
> -
> -	safe_create_dir(git_dir, 0);
> -
> -	init_is_bare_repository = is_bare_repository();
> -
> -	/* Check to see if the repository version is right.
> -	 * Note that a newly created repository does not have
> -	 * config file, so this will not fail.  What we are catching
> -	 * is an attempt to reinitialize new repository with an old tool.
> -	 */
> -	check_repository_format(&repo_fmt);
> -
> -	validate_hash_algorithm(&repo_fmt, hash);
> -
> -	reinit = create_default_files(template_dir, original_git_dir,
> -				      initial_branch, &repo_fmt,
> -				      flags & INIT_DB_QUIET);
> -	if (reinit && initial_branch)
> -		warning(_("re-init: ignored --initial-branch=%s"),
> -			initial_branch);
> -
> -	create_object_directory();
> -
> -	if (get_shared_repository()) {
> -		char buf[10];
> -		/* We do not spell "group" and such, so that
> -		 * the configuration can be read by older version
> -		 * of git. Note, we use octal numbers for new share modes,
> -		 * and compatibility values for PERM_GROUP and
> -		 * PERM_EVERYBODY.
> -		 */
> -		if (get_shared_repository() < 0)
> -			/* force to the mode value */
> -			xsnprintf(buf, sizeof(buf), "0%o", -get_shared_repository());
> -		else if (get_shared_repository() == PERM_GROUP)
> -			xsnprintf(buf, sizeof(buf), "%d", OLD_PERM_GROUP);
> -		else if (get_shared_repository() == PERM_EVERYBODY)
> -			xsnprintf(buf, sizeof(buf), "%d", OLD_PERM_EVERYBODY);
> -		else
> -			BUG("invalid value for shared_repository");
> -		git_config_set("core.sharedrepository", buf);
> -		git_config_set("receive.denyNonFastforwards", "true");
> -	}
> -
> -	if (!(flags & INIT_DB_QUIET)) {
> -		int len = strlen(git_dir);
> -
> -		if (reinit)
> -			printf(get_shared_repository()
> -			       ? _("Reinitialized existing shared Git repository in %s%s\n")
> -			       : _("Reinitialized existing Git repository in %s%s\n"),
> -			       git_dir, len && git_dir[len-1] != '/' ? "/" : "");
> -		else
> -			printf(get_shared_repository()
> -			       ? _("Initialized empty shared Git repository in %s%s\n")
> -			       : _("Initialized empty Git repository in %s%s\n"),
> -			       git_dir, len && git_dir[len-1] != '/' ? "/" : "");
> -	}
> -
> -	free(original_git_dir);
> -	return 0;
> -}
> ->>>>>>> c7b6b0a28f (Merge branch 'js/fix-clone-w-hooks-2.40' into HEAD)
>
>  static int guess_repository_type(const char *git_dir)
>  {
> diff --git a/config.c b/config.c
> remerge CONFLICT (content): Merge conflict in config.c
> index f0f09004c8..3846a37be9 100644
> --- a/config.c
> +++ b/config.c
> @@ -1558,33 +1558,7 @@ static int git_default_core_config(const char *var, const char *value,
>  	if (!strcmp(var, "core.attributesfile"))
>  		return git_config_pathname(&git_attributes_file, var, value);
>
> -<<<<<<< babb4e5d71 (Git 2.42.2)
> -	if (!strcmp(var, "core.hookspath")) {
> -		if (ctx->kvi && ctx->kvi->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);
> -||||||| 0f15832059
> -	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);
> -=======
>  	if (!strcmp(var, "core.hookspath"))
> ->>>>>>> c7b6b0a28f (Merge branch 'js/fix-clone-w-hooks-2.40' into HEAD)
>  		return git_config_pathname(&git_hooks_path, var, value);
>
>  	if (!strcmp(var, "core.bare")) {
> diff --git a/hook.c b/hook.c
> index 7ddb677216..7be471e335 100644
> --- a/hook.c
> +++ b/hook.c
> @@ -59,7 +59,8 @@ void add_safe_hook(const char *path)
>  	}
>  }
>
> -static int safe_hook_cb(const char *key, const char *value, void *d)
> +static int safe_hook_cb(const char *key, const char *value,
> +			const struct config_context *ctx UNUSED, void *d)
>  {
>  	struct strset *set = d;
>
> diff --git a/setup.c b/setup.c
> index ea2aec5aaa..dc4897543e 100644
> --- a/setup.c
> +++ b/setup.c
> @@ -18,6 +18,7 @@
>  #include "trace2.h"
>  #include "worktree.h"
>  #include "exec-cmd.h"
> +#include "run-command.h"
>  #include "hook.h"
>
>  static int inside_git_dir = -1;
> @@ -1794,6 +1795,7 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
>  	size_t path_baselen = path->len;
>  	size_t template_baselen = template_path->len;
>  	struct dirent *de;
> +	int is_hooks_dir = ends_with(template_path->buf, "/hooks/");
>
>  	/* Note: if ".git/hooks" file exists in the repository being
>  	 * re-initialized, /etc/core-git/templates/hooks/update would
> @@ -1846,6 +1848,10 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
>  			strbuf_release(&lnk);
>  		}
>  		else if (S_ISREG(st_template.st_mode)) {
> +			if (is_hooks_dir &&
> +			    is_executable(template_path->buf))
> +				add_safe_hook(template_path->buf);
> +
>  			if (copy_file(path->buf, template_path->buf, st_template.st_mode))
>  				die_errno(_("cannot copy '%s' to '%s'"),
>  					  template_path->buf, path->buf);
> * js/fix-clone-w-hooks-2.43
> diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
> remerge CONFLICT (content): Merge conflict in t/t1800-hook.sh
> index 1283a9bd47..c51be5f7a0 100755
> --- a/t/t1800-hook.sh
> +++ b/t/t1800-hook.sh
> @@ -191,24 +191,7 @@ test_expect_success '`safe.hook.sha256` and clone protections' '
>  	echo "called hook" >safe-hook.log
>  	EOF
>
> -<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 1f2e64e22d (Git 2.43.4)
> -	git hook run test-hook 2>err &&
> -	test_grep "Hook ran" err &&
> -|||||||||||||||||||||||||||||||| babb4e5d71
> -	git hook run test-hook 2>err &&
> -	grep "Hook ran" err &&
> -================================
> ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 0268cf79cb (Merge branch 'js/fix-clone-w-hooks-2.41' into HEAD)
>  	test_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \
> -<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 1f2e64e22d (Git 2.43.4)
> -		git hook run test-hook 2>err &&
> -	test_grep "active .core.hooksPath" err &&
> -	test_grep ! "Hook ran" err
> -|||||||||||||||||||||||||||||||| babb4e5d71
> -		git hook run test-hook 2>err &&
> -	grep "active .core.hooksPath" err &&
> -	! grep "Hook ran" err
> -================================
>  		git -C safe-hook hook run pre-push 2>err &&
>  	cmd="$(grep "git config --global --add safe.hook.sha256 [0-9a-f]" err)" &&
>  	eval "$cmd" &&
> @@ -235,7 +218,6 @@ test_expect_success 'Git LFS special-handling in clone protections' '
>  		git -C lfs-hooks hook run pre-push &&
>  	test_write_lines "called pre-push" >expect &&
>  	test_cmp lfs-hooks/fake-git-lfs.log expect
> ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 0268cf79cb (Merge branch 'js/fix-clone-w-hooks-2.41' into HEAD)
>  '
>
>  test_done
> diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh
> index 6ee833bac2..0fcfe6f2ce 100755
> --- a/t/t5601-clone.sh
> +++ b/t/t5601-clone.sh
> @@ -835,7 +835,7 @@ test_expect_success 'clone with init.templatedir runs hooks' '
>  		git -c init.templateDir="$(pwd)/tmpl" \
>  			clone --recurse-submodules \
>  			tmpl/hooks hook-run-submodule 2>err &&
> -		! grep "active .* hook found" err &&
> +		test_grep ! "active .* hook found" err &&
>  		test_path_is_file hook-run-submodule/hook.run &&
>  		test_path_is_file hook-run-submodule/sub/hook.run
>  	)
> * js/fix-clone-w-hooks-2.44* js/fix-clone-w-hooks-2.45
>


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 5/8] hook(clone protections): add escape hatch
  2024-05-18 18:14     ` Jeff King
  2024-05-18 18:54       ` Junio C Hamano
@ 2024-05-18 19:32       ` Johannes Schindelin
  2024-05-18 19:47         ` Jeff King
  1 sibling, 1 reply; 54+ messages in thread
From: Johannes Schindelin @ 2024-05-18 19:32 UTC (permalink / raw
  To: Jeff King; +Cc: Johannes Schindelin via GitGitGadget, git, brian m. carlson

Hi Jeff,

On Sat, 18 May 2024, Jeff King wrote:

> On Sat, May 18, 2024 at 10:32:43AM +0000, Johannes Schindelin via GitGitGadget wrote:
>
> > To help Git LFS, and other tools behaving similarly (if there are any),
> > let's add a new, multi-valued `safe.hook.sha256` config setting. Like
> > the already-existing `safe.*` settings, it is ignored in
> > repository-local configs, and it is interpreted as a list of SHA-256
> > checksums of hooks' contents that are safe to execute during a clone
> > operation. Future Git LFS versions will need to write those entries at
> > the same time they install the `smudge`/`clean` filters.
>
> This scheme seems more complicated for the user than the sometimes
> discussed ability to specify hook paths via config (not core.hooksPath,
> which covers _all_ hooks, but one which allows a per-hook path).

Right, it is more complicated.

But then, we are talking about `git clone` protections, as Junio points
out, i.e. preventing hooks from running that the user did not install.

Git LFS' `post-checkout` hook is an example: The user never explicitly
installed this hook, and it was not there before the checkout phase of the
clone started, yet it is there after it finished. That's the same pattern
as many attack vectors used that we saw in the path for critical CVEs.

> In either case, we're considering config to be a trusted source of
> truth, so I think the security properties are the same. But for the
> system here, a user updating a hook needs to do multiple steps:
>
>   - compute the sha256 of the hook (for which we provide no tooling
>     support, though hopefully it is obvious how to use other tools)
>
>   - add the config for the sha256
>
>   - install the new hook into $GIT_DIR/hooks

Well, there is tooling support: With the proposed patches (patch 5, to be
precise), Git will complain about hooks that are installed _during_ a
clone, and then provide the following advice:

	If this is intentional and the hook is safe to run,
	please run the following command and try again:

	  git config --global --add safe.hook.sha256 <hash>

While this won't help with the just-completed clone operation, it assists
preventing the same issue in future clones.

> Whereas if the config can just point at the hook, then there is only one
> step: add the config for the hook (presumably pointing to a system
> version that would have been copied into $GIT_DIR/hooks previously).
>
> Likewise for updates of the hooks, where the sha256 scheme requires
> computing and adding a new hash. But when the config just points to the
> path, there is no additional step for updating.
>
> In either scheme, programs like git-lfs would have to adjust to the new
> world view. The main advantage of the sha256 scheme, it seems to me, is
> that the baked-in sha256 values let existing versions of git-lfs work.
> But we could also support that internally, without exposing
> safe.hook.sha256 to the world (and thus creating an ecosystem where we
> have to support it forever).
>
> Implied here is that I also think config-based hooks have a lot of
> _other_ advantages, and so would be worth pursuing anyway, and this
> extra safety would come along for free. I won't enumerate those
> advantages here, but we that can be a separate discussion if need be.

One disadvantage of config-based hooks is that it is quite hard to verify
the provenance of the settings: Was it the user who added it, was it a
program the user called, or was it exploiting a vulnerability whereby the
config was written inadvertently?

> And of course that feature doesn't yet exist, and is a much larger one.
> But besides un-breaking current LFS, I'm not sure that we need to rush
> out a more generic version of the feature.

Exactly. We need to unbreak Git LFS-enabled clones and release v2.45.2
before I even have the head space to think more about config-based hooks.

Ciao,
Johannes


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 5/8] hook(clone protections): add escape hatch
  2024-05-18 18:54       ` Junio C Hamano
@ 2024-05-18 19:35         ` Jeff King
  2024-05-18 19:37         ` Johannes Schindelin
  1 sibling, 0 replies; 54+ messages in thread
From: Jeff King @ 2024-05-18 19:35 UTC (permalink / raw
  To: Junio C Hamano
  Cc: Johannes Schindelin via GitGitGadget, git, brian m. carlson,
	Johannes Schindelin

On Sat, May 18, 2024 at 11:54:41AM -0700, Junio C Hamano wrote:

> Jeff King <peff@peff.net> writes:
> 
> > In either case, we're considering config to be a trusted source of
> > truth, so I think the security properties are the same. But for the
> > system here, a user updating a hook needs to do multiple steps:
> >
> >   - compute the sha256 of the hook (for which we provide no tooling
> >     support, though hopefully it is obvious how to use other tools)
> >
> >   - add the config for the sha256
> >
> >   - install the new hook into $GIT_DIR/hooks
> 
> I am not sure why any of the above is needed.  
> 
> Hmph.
> 
> I was somehow (because that is how "git config --help" explains
> "safe.hook.*") led to believe that this "safety" was only about "git
> clone would prefer not to run ANY hook before it finishes operation
> and gives back the control to the end user, but historically it ran
> any enabled hooks in the resulting repository that was freshly
> created by it---so let's at least make sure the contents of the
> hooks are known-to-be-good ones when 'git clone' runs the hooks".
> Most importantly, once "git clone" gives control back to the end
> user and the end user had a chance to inspect the resulting
> repository, the files in $GIT_DIR/hooks can be updated and the hooks
> will run without incurring any cost of checking.
> 
> Isn't that what happens?

Yes, sorry if I was unclear. This is _only_ about the hooks-during-clone
safety. So my "a user must do this" is really "a user who wants a hook
to be installed during a clone must do this". And plausibly speaking,
that is mostly going to be script/program writers like git-lfs.

So the extra complexity is limited to those cases.

> Looking at the control flow, hook.c:find_hook() is the one that
> calls the function is_hook_safe_during_clone() to reject "unsafe"
> ones (and allow the white-listed ones), but I do not know offhand
> how the code limits the rejection only during clone.  So perhaps
> this set of patches need further work to restrict the checks only to
> "while we are cloning" case?

I think the git_env_bool() call to check GIT_CLONE_PROTECTION_ACTIVE is
what kicks in here. During non-clone calls, that will use the default of
0.

-Peff


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 5/8] hook(clone protections): add escape hatch
  2024-05-18 18:54       ` Junio C Hamano
  2024-05-18 19:35         ` Jeff King
@ 2024-05-18 19:37         ` Johannes Schindelin
  1 sibling, 0 replies; 54+ messages in thread
From: Johannes Schindelin @ 2024-05-18 19:37 UTC (permalink / raw
  To: Junio C Hamano
  Cc: Jeff King, Johannes Schindelin via GitGitGadget, git,
	brian m. carlson

Hi Junio,

On Sat, 18 May 2024, Junio C Hamano wrote:

> Jeff King <peff@peff.net> writes:
>
> > In either case, we're considering config to be a trusted source of
> > truth, so I think the security properties are the same. But for the
> > system here, a user updating a hook needs to do multiple steps:
> >
> >   - compute the sha256 of the hook (for which we provide no tooling
> >     support, though hopefully it is obvious how to use other tools)
> >
> >   - add the config for the sha256
> >
> >   - install the new hook into $GIT_DIR/hooks
>
> I am not sure why any of the above is needed.
>
> Hmph.
>
> I was somehow (because that is how "git config --help" explains
> "safe.hook.*") led to believe that this "safety" was only about "git
> clone would prefer not to run ANY hook before it finishes operation
> and gives back the control to the end user, but historically it ran
> any enabled hooks in the resulting repository that was freshly
> created by it---so let's at least make sure the contents of the
> hooks are known-to-be-good ones when 'git clone' runs the hooks".
> Most importantly, once "git clone" gives control back to the end
> user and the end user had a chance to inspect the resulting
> repository, the files in $GIT_DIR/hooks can be updated and the hooks
> will run without incurring any cost of checking.
>
> Isn't that what happens?
>
> Looking at the control flow, hook.c:find_hook() is the one that
> calls the function is_hook_safe_during_clone() to reject "unsafe"
> ones (and allow the white-listed ones), but I do not know offhand
> how the code limits the rejection only during clone.  So perhaps
> this set of patches need further work to restrict the checks only to
> "while we are cloning" case?

The logic in `find_hook()` reads like this:

        if (!git_hooks_path && git_env_bool("GIT_CLONE_PROTECTION_ACTIVE", 0) &&
            !is_hook_safe_during_clone(name, path.buf, sha256))
                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 is safe to run, "
                      "please run the following command and try again:\n\n"
                      "  git config --global --add safe.hook.sha256 %s"),
                    name, path.buf, sha256);

The `!git_hooks_path` accounts for the fact that users can choose to set
the `core.hooksPath` in their global configs, in which case `git clone`
_should_ expect hooks to be present that do not originate from Git's
templates.

The `GIT_CLONE_PROTECTION_ACTIVE` check is the one that limits the
rejection to only happen during a clone: This environment variable is set
in `git clone` (carefully passing 0 as `overwrite` parameter to `setenv()`
to allow users to override this protection).

The reason why it has to be done via an environment variable is that `git
clone` can spawn many processes that all need to respect this protection,
most notably when a recursive clone calls `git submodule--helper update`.

Ciao,
Johannes


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 5/8] hook(clone protections): add escape hatch
  2024-05-18 19:32       ` Johannes Schindelin
@ 2024-05-18 19:47         ` Jeff King
  2024-05-18 20:06           ` Johannes Schindelin
  0 siblings, 1 reply; 54+ messages in thread
From: Jeff King @ 2024-05-18 19:47 UTC (permalink / raw
  To: Johannes Schindelin
  Cc: Johannes Schindelin via GitGitGadget, git, brian m. carlson

On Sat, May 18, 2024 at 09:32:07PM +0200, Johannes Schindelin wrote:

> >   - compute the sha256 of the hook (for which we provide no tooling
> >     support, though hopefully it is obvious how to use other tools)
> [...]
> 
> Well, there is tooling support: With the proposed patches (patch 5, to be
> precise), Git will complain about hooks that are installed _during_ a
> clone, and then provide the following advice:
> 
> 	If this is intentional and the hook is safe to run,
> 	please run the following command and try again:
> 
> 	  git config --global --add safe.hook.sha256 <hash>
> 
> While this won't help with the just-completed clone operation, it assists
> preventing the same issue in future clones.

What I meant by tooling support was: how do you find out the sha256 hash
of the hook you're wanting to bless?

I'd imagine you'd reach for the stand-alone "sha256" tool. But there is
no Git tool to compute the hash (you can't use any of the usual tools
like "hash-object" because it is not a pure hash of the content). Should
we provide one? Or at least tell the user which third-party command is
likely to be used?

> > Implied here is that I also think config-based hooks have a lot of
> > _other_ advantages, and so would be worth pursuing anyway, and this
> > extra safety would come along for free. I won't enumerate those
> > advantages here, but we that can be a separate discussion if need be.
> 
> One disadvantage of config-based hooks is that it is quite hard to verify
> the provenance of the settings: Was it the user who added it, was it a
> program the user called, or was it exploiting a vulnerability whereby the
> config was written inadvertently?

But isn't that true of the safe.hook.sha256 value, too? If I can attack
.git/config, then I can set it to match the attack hook (not to mention
the zillion other config options which execute arbitrary code).

If we really want to harden .git against attacks which can overwrite
files in it, then I think the long-term path may be something like:

  - add support for specifying hooks via config. Leave .git/hooks for
    compatibility.

  - introduce a config option to disable .git/hooks support. Respect it
    only outside of .git/config. Default to false to start for backwards
    compatibility. Eventually flip it to true by default.

And then perhaps something similar for in-repo config (add an option to
disable in-repo config except for repos marked as safe).

> > And of course that feature doesn't yet exist, and is a much larger one.
> > But besides un-breaking current LFS, I'm not sure that we need to rush
> > out a more generic version of the feature.
> 
> Exactly. We need to unbreak Git LFS-enabled clones and release v2.45.2
> before I even have the head space to think more about config-based hooks.

To be clear, I'm not proposing doing nothing. I'm proposing un-breaking
LFS either by rolling back the defense-in-depth or adding hard-coded
hashes, neither of which introduces a user-visible feature that must be
supported. And then proceed with new features in the regular cycle.

The hard-coded hashes are obviously a ticking time bomb until lfs
updates again (and they don't help any as-yet-unknown program which does
the same thing). So I'd suggest just rolling back the feature entirely
in the meantime.

-Peff


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 5/8] hook(clone protections): add escape hatch
  2024-05-18 19:47         ` Jeff King
@ 2024-05-18 20:06           ` Johannes Schindelin
  2024-05-18 21:12             ` Jeff King
  0 siblings, 1 reply; 54+ messages in thread
From: Johannes Schindelin @ 2024-05-18 20:06 UTC (permalink / raw
  To: Jeff King; +Cc: Johannes Schindelin via GitGitGadget, git, brian m. carlson

Hi Jeff,

On Sat, 18 May 2024, Jeff King wrote:

> On Sat, May 18, 2024 at 09:32:07PM +0200, Johannes Schindelin wrote:
>
> > > Implied here is that I also think config-based hooks have a lot of
> > > _other_ advantages, and so would be worth pursuing anyway, and this
> > > extra safety would come along for free. I won't enumerate those
> > > advantages here, but we that can be a separate discussion if need
> > > be.
> >
> > One disadvantage of config-based hooks is that it is quite hard to
> > verify the provenance of the settings: Was it the user who added it,
> > was it a program the user called, or was it exploiting a vulnerability
> > whereby the config was written inadvertently?
>
> But isn't that true of the safe.hook.sha256 value, too?

No, because `safe.hook.sha256` (like `safe.directory` and
`safe.bareRepository`) is only respected in "protected" configs, i.e.
system-wide, user-wide and command-line config. Any such settings in the
repository-local config are ignored.

> If I can attack .git/config, then I can set it to match the attack hook
> (not to mention the zillion other config options which execute arbitrary
> code).
>
> If we really want to harden .git against attacks which can overwrite
> files in it, then I think the long-term path may be something like:
>
>   - add support for specifying hooks via config. Leave .git/hooks for
>     compatibility.
>
>   - introduce a config option to disable .git/hooks support. Respect it
>     only outside of .git/config. Default to false to start for backwards
>     compatibility. Eventually flip it to true by default.
>
> And then perhaps something similar for in-repo config (add an option to
> disable in-repo config except for repos marked as safe).
>
> > > And of course that feature doesn't yet exist, and is a much larger one.
> > > But besides un-breaking current LFS, I'm not sure that we need to rush
> > > out a more generic version of the feature.
> >
> > Exactly. We need to unbreak Git LFS-enabled clones and release v2.45.2
> > before I even have the head space to think more about config-based hooks.
>
> To be clear, I'm not proposing doing nothing. I'm proposing un-breaking
> LFS either by rolling back the defense-in-depth or adding hard-coded
> hashes, neither of which introduces a user-visible feature that must be
> supported. And then proceed with new features in the regular cycle.
>
> The hard-coded hashes are obviously a ticking time bomb until lfs
> updates again (and they don't help any as-yet-unknown program which does
> the same thing). So I'd suggest just rolling back the feature entirely
> in the meantime.

Rolling back the defense-in-depth would be a mistake: We do see (seemingly
on a yearly cadence) reports of vulnerabilities in Git that often raise to
critical severity by exploiting the hooks feature (typically in
conjunction with submodules). There is no reason to believe that this
steady trickle will stop any time soon. The defense-in-depth we introduced
would stop at least that escalation path that turns those vulnerabilities
into critical attack vectors putting users at risk.

Even worse: If we removed these protections without any replacement, now
we basically told hackers where to look for nice, exploitable bugs,
publicly.

For what it's worth, I was originally also in favor of the pretty surgical
addition of the hard-coded hashes specifically to unbreak Git LFS-enabled
clones. You must have seen my proposal that I sent to the Git security
mailing list.

However, brian suggested that Git LFS may not be the only 3rd-party
application that is affected by the clone protections. I have my doubts
that other applications use a similar route, it strikes me as quite hacky
to install a hook while running a `smudge` filter, yet I do admit that
there is a possibility. Which is why we introduced the `safe.hooks.sha256`
settings.

This strikes a good balance between unbreaking Git LFS and still
benefitting from the defense-in-depth that helps fend off future critical
vulnerabilities.

If we did not have such a balanced way to address the Git LFS breakage, I
would totally agree with you that we would need to consider rolling back
the defense-in-depth. Happily, we don't have to.

Ciao,
Johannes

P.S.: For what it's worth, the pattern we see in Git LFS is relatively
hard to replicate. `git clone` does not offer any easy and convenient way
to install hooks during the operation other than via templates (which,
unlike Git LFS-enabled clones, is _not_ broken in v2.45.1). Of course,
users could start a clone and then manually copy a `post-checkout` hook
into `.git/hooks/` _while the clone is still running_. I kind of doubt
that that's common practice we need to support, though.


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 0/8] Various fixes for v2.45.1 and friends
  2024-05-18 19:22     ` Johannes Schindelin
@ 2024-05-18 20:13       ` Johannes Schindelin
  0 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin @ 2024-05-18 20:13 UTC (permalink / raw
  To: Junio C Hamano
  Cc: git, Johannes Schindelin via GitGitGadget, brian m. carlson

Hi Junio,

On Sat, 18 May 2024, Johannes Schindelin wrote:

> On Sat, 18 May 2024, Junio C Hamano wrote:
>
> > I have applied this to maint-2.39 and then merged them up to the
> > maintenance tracks.  The results will be pushed out to the "split
> > out" repository at
> >
> >     https://github.com/gitster/git/
> >
> > as these branches:
> >
> >     js/fix-clone-w-hooks-2.39
> >     js/fix-clone-w-hooks-2.40
> >     js/fix-clone-w-hooks-2.41
> >     js/fix-clone-w-hooks-2.42
> >     js/fix-clone-w-hooks-2.43
> >     js/fix-clone-w-hooks-2.44
> >     js/fix-clone-w-hooks-2.45
>
> Thank you!
>
> As it happens, I had already worked on tentative/maint-* branches (that
> you can see here: https://github.com/dscho/git/branches/active), but
> had to take a break before finalizing them.
>
> The major difference I see is that js/fix-clone-w-hooks-2.41 still
> declares `do_files_match()` in `setup.h` (even if it is no longer defined
> or called), and that `hook.c` includes `copy.h` (but that is no longer
> needed), and that `setup.h` includes `hook.h` (but that's not needed
> either).
>
> While comparing, I noticed that I had missed an extra empty line in my
> merge conflict resolutions, a `grep` -> `test_grep` conversation and an
> `UNUSED` attribute for the `ctx` parameter of the `safe_hook_cb()`
> function. So I am really grateful that you did those integrations
> independently.
>
> You will also note that I tentatively added commits to mark v2.39.5, ...,
> v2.45.2 ready for tagging, along with release notes, just so we can hit
> the ground running as soon as reviews settle down.

Oh, I forgot to mention that I also integrated backports of some CI fixes
into the `tentative/maint-*` branches, to let the `osx-gcc` job pass
again.

Ciao,
Johannes
>


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 5/8] hook(clone protections): add escape hatch
  2024-05-18 20:06           ` Johannes Schindelin
@ 2024-05-18 21:12             ` Jeff King
  2024-05-19  1:15               ` Junio C Hamano
  0 siblings, 1 reply; 54+ messages in thread
From: Jeff King @ 2024-05-18 21:12 UTC (permalink / raw
  To: Johannes Schindelin
  Cc: Johannes Schindelin via GitGitGadget, git, brian m. carlson

On Sat, May 18, 2024 at 10:06:36PM +0200, Johannes Schindelin wrote:

> > > One disadvantage of config-based hooks is that it is quite hard to
> > > verify the provenance of the settings: Was it the user who added it,
> > > was it a program the user called, or was it exploiting a vulnerability
> > > whereby the config was written inadvertently?
> >
> > But isn't that true of the safe.hook.sha256 value, too?
> 
> No, because `safe.hook.sha256` (like `safe.directory` and
> `safe.bareRepository`) is only respected in "protected" configs, i.e.
> system-wide, user-wide and command-line config. Any such settings in the
> repository-local config are ignored.

Ah, true. I think the issue still holds for all of the other config that
runs arbitrary code, though, doesn't it?

> Rolling back the defense-in-depth would be a mistake: We do see (seemingly
> on a yearly cadence) reports of vulnerabilities in Git that often raise to
> critical severity by exploiting the hooks feature (typically in
> conjunction with submodules). There is no reason to believe that this
> steady trickle will stop any time soon. The defense-in-depth we introduced
> would stop at least that escalation path that turns those vulnerabilities
> into critical attack vectors putting users at risk.

Most of the vulnerabilities that I recall could just as easily write
over .git/config. But I didn't catalog them. Do you have specific ones
in mind?

> Even worse: If we removed these protections without any replacement, now
> we basically told hackers where to look for nice, exploitable bugs,
> publicly.

I don't find this line of reasoning all that compelling. The existing
vulnerabilities that led you to the defense-in-depth protection already
pointed them in the right direction.

So I'm not convinced that temporarily moving back to the v2.45.0 state
is all that dangerous. If it were a known vulnerability, yes, I'd worry.
For defense-in-depth, less so.

> For what it's worth, I was originally also in favor of the pretty surgical
> addition of the hard-coded hashes specifically to unbreak Git LFS-enabled
> clones. You must have seen my proposal that I sent to the Git security
> mailing list.
> 
> However, brian suggested that Git LFS may not be the only 3rd-party
> application that is affected by the clone protections. I have my doubts
> that other applications use a similar route, it strikes me as quite hacky
> to install a hook while running a `smudge` filter, yet I do admit that
> there is a possibility. Which is why we introduced the `safe.hooks.sha256`
> settings.
> 
> This strikes a good balance between unbreaking Git LFS and still
> benefitting from the defense-in-depth that helps fend off future critical
> vulnerabilities.
> 
> If we did not have such a balanced way to address the Git LFS breakage, I
> would totally agree with you that we would need to consider rolling back
> the defense-in-depth. Happily, we don't have to.

My main complaint is that it introduces a confusing and complicated
requirement that LFS (and maybe others) have to think about in
perpetuity. And we may end up with a better solution. We got bit by
pushing out the v2.45.1 change without a lot of end-user testing. Now it
seems like v2.45.2 is being rushed out to fix it. It would hopefully see
_more_ testing, as it's being done in the open.

But it sounds like we're throwing away our usual release-engineering
practices (where the usual practice for a regression is "revert it, it
can happen in the next cycle") in favor of a security fix. Again, for a
vulnerability fix, that makes sense. But for layered defense, I find it
less compelling.

Anyway, I have said my piece and I don't think I have much to add. So
either you agree or not, and if this is the direction the project wants
to go, I won't object further.

> P.S.: For what it's worth, the pattern we see in Git LFS is relatively
> hard to replicate. `git clone` does not offer any easy and convenient way
> to install hooks during the operation other than via templates (which,
> unlike Git LFS-enabled clones, is _not_ broken in v2.45.1). Of course,
> users could start a clone and then manually copy a `post-checkout` hook
> into `.git/hooks/` _while the clone is still running_. I kind of doubt
> that that's common practice we need to support, though.

I think we've run into similar issues with remote helpers, which run
arbitrary code and could install hooks. The recent one I'm thinking of
is:

  https://lore.kernel.org/git/20240503020432.2fxwuhjsvumy7i7z@glandium.org/

though that wasn't a hook problem, but rather leaving the repo in an
uninitialized state for longer.

-Peff


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 5/8] hook(clone protections): add escape hatch
  2024-05-18 21:12             ` Jeff King
@ 2024-05-19  1:15               ` Junio C Hamano
  2024-05-20 16:05                 ` Johannes Schindelin
  0 siblings, 1 reply; 54+ messages in thread
From: Junio C Hamano @ 2024-05-19  1:15 UTC (permalink / raw
  To: Jeff King
  Cc: Johannes Schindelin, Johannes Schindelin via GitGitGadget, git,
	brian m. carlson

Jeff King <peff@peff.net> writes:

> But it sounds like we're throwing away our usual release-engineering
> practices (where the usual practice for a regression is "revert it, it
> can happen in the next cycle") in favor of a security fix. Again, for a
> vulnerability fix, that makes sense. But for layered defense, I find it
> less compelling.

I find it a lot less compelling, too.

It unfortunately involves about the same amount of conflict
management to do the (partial) revert for all these maintenance
tracks as it would then later take a "fix in the next cycle" for all
these tracks, which made me feel somewhat hesitant.

But considering that we are not talking about lifting vulnerability
fix, it may make sense to do the (partial) revert all the way down
to 2.39 track but do the "fix in the next cycle" only for 2.45 and
later (or even in 2.46 only, without even aiming to touch 2.45
track).

Thanks for a dose of sanity.



^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 5/8] hook(clone protections): add escape hatch
  2024-05-19  1:15               ` Junio C Hamano
@ 2024-05-20 16:05                 ` Johannes Schindelin
  2024-05-20 18:18                   ` Junio C Hamano
  0 siblings, 1 reply; 54+ messages in thread
From: Johannes Schindelin @ 2024-05-20 16:05 UTC (permalink / raw
  To: Junio C Hamano
  Cc: Jeff King, Johannes Schindelin via GitGitGadget, git,
	brian m. carlson

Hi,

On Sat, 18 May 2024, Junio C Hamano wrote:

> But considering that we are not talking about lifting vulnerability fix,
> it may make sense to do the (partial) revert all the way down to 2.39
> track but do the "fix in the next cycle" only for 2.45 and later (or
> even in 2.46 only, without even aiming to touch 2.45 track).

To ensure that I don't misunderstand you: You are talking about reverting
8db1e8743c0 (clone: prevent hooks from running during a clone,
2024-03-28), right?

It is _technically_ true that this is not a vulnerability fix. But only
_technically_. Practically, it is preventing vulnerabilities from reaching
the critical level.

Let's take the most recently-addressed critical vulnerability,
CVE-2024-32002. I carefully crafted above-mentioned commit such that it
would have prevent the Remote Code Execution attack vector noted in that
CVE.

To put this into perspective: If this protection had been put in place
before v2.39.4, the CVSS score of CVS-2024-32002 would not have been
9.1 (Critical), but instead 2.2 (Low).

In other words, even if a vulnerability was found, Git's users would have
been safer with this defense-in-depth in place.

The same `post-checkout` hook attack vector likewise raised the severity
of CVE-2021-21300, CVE-2019-1354, CVE-2019-1353, CVE-2019-1352, and
CVE-2019-1349.

It also raised the severity of the vulnerability fixed in v1.8.5.6 (for
which we did not obtain a CVE).

Based on past experience, we must expect the semi-steady trickle of Git
vulnerabilities to continue, and having this defense-in-depth in place
will invaluable in helping to keep the severity of those future
vulnerabilities low.

Therefore reverting that commit would put Git's users at risk.

In combination with the fact that we have a path forward via the patches
that are under discussion in this here mail thread, a path forward that
avoids that risk and incurs an acceptable cost [*1*], the plan to revert
8db1e8743c0 instead should be questioned.

Ciao,
Johannes

P.S.: A concern was raised about `safe.hook.sha256` not having tooling to
generate those SHA-256 checksums, and putting a burden on 3rd-party tool
developers.

However, tooling to generate SHA-256 checksums of files is ubiquitous,
there is `sha256sum` and `openssl dgst -sha256`, just to name two tools
that are widely available.

And it's not as if _Git users_ would have to generate those checksums. It
would be a one-time cost for the developers whose tools install those
hooks during a clone (Git offers no option to install hooks during cloning
other than templates, which is not broken in v2.39.4, ..., v2.45.1).

Which brings me to that mysterious mention of tools other than Git LFS
being potentially affected. It is quite dubitable that tools other than
Git LFS use this method of changing the Git repository configuration in
such a major way as to install hooks _while running a `smudge` filter_.

It must be put into doubt, too, that this method of abusing the `smudge`
filter is the best design to address Git LFS' needs. A serious downside,
for example, is that this Git LFS strategy is in conflict with
user-provided `post-checkout` hooks that are copied via templates. A
better design that comes immediately to mind would be to add a new,
generic batched-smudge filter that Git LFS could use, and that would be
configured in the system or user-wide config just like Git LFS'
`smudge`/`clean` filters, without the need to play hacky games during a
clone operation that are very similar to malicious attacks' strategies to
abuse Git's hook feature to allow Remote Code Execution.


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 5/8] hook(clone protections): add escape hatch
  2024-05-20 16:05                 ` Johannes Schindelin
@ 2024-05-20 18:18                   ` Junio C Hamano
  2024-05-20 19:38                     ` Johannes Schindelin
  0 siblings, 1 reply; 54+ messages in thread
From: Junio C Hamano @ 2024-05-20 18:18 UTC (permalink / raw
  To: Johannes Schindelin
  Cc: Jeff King, Johannes Schindelin via GitGitGadget, git,
	brian m. carlson

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:

> To put this into perspective: If this protection had been put in place
> before v2.39.4, the CVSS score of CVS-2024-32002 would not have been
> 9.1 (Critical), but instead 2.2 (Low).

But we wouldn't have a working git-lfs then, so that comparison is
not quite fair.  As brian already said, you can reduce the score by
making Git do nothing, which is _also_ an absurd position to take
"security" (in air quotes) over everything else like usability and
functionality.  And this time, the layered security went a bit too
aggressive.

Also as Peff said and I agreed to, we are not talking about refusing
to do anything on top.  It was just that the "never run any approved
hook during clone" turned out to be not-quite-fully thought out and
it should be reworked in the open, and reverting that wholesale
would hopefully give us a cleaner ground to design it.

The end-result of such a reworking in the open may turn out to be
the same (or similar) "register the blob object name of the contents
to appear in approved hook scripts", or it may look completely
different.  But the road to get there, and the state of the system
while we get there, would be different.

I would probably see if I can take brian's revert directly; if it
applies to the oldest maint-2.39 track, it would be the ideal, but
we'd still need to prepare a similar 7-track cascade like we did for
the js/fix-clone-w-hooks-2.XX topics.  If it is only for the master,
it needs to be munged to apply to maint-2.39 first.

Thanks.


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 5/8] hook(clone protections): add escape hatch
  2024-05-20 18:18                   ` Junio C Hamano
@ 2024-05-20 19:38                     ` Johannes Schindelin
  2024-05-20 20:07                       ` Junio C Hamano
  2024-05-20 21:03                       ` Johannes Schindelin
  0 siblings, 2 replies; 54+ messages in thread
From: Johannes Schindelin @ 2024-05-20 19:38 UTC (permalink / raw
  To: Junio C Hamano
  Cc: Jeff King, Johannes Schindelin via GitGitGadget, git,
	brian m. carlson

Hi Junio,

On Mon, 20 May 2024, Junio C Hamano wrote:

> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
>
> > To put this into perspective: If this protection had been put in place
> > before v2.39.4, the CVSS score of CVS-2024-32002 would not have been
> > 9.1 (Critical), but instead 2.2 (Low).
>
> But we wouldn't have a working git-lfs then, so that comparison is
> not quite fair.

I obviously did not mean to break Git LFS. And obviously I did not mean to
insist on the current clone protection if we could not have fixed Git LFS.
But we have patches in hand for that.

> As brian already said, you can reduce the score by making Git do
> nothing, which is _also_ an absurd position to take "security" (in air
> quotes) over everything else like usability and functionality.  And this
> time, the layered security went a bit too aggressive.

Right. And I never said that we should do something as absurd, so I fail
to see your point.

> Also as Peff said and I agreed to, we are not talking about refusing
> to do anything on top.  It was just that the "never run any approved
> hook during clone" turned out to be not-quite-fully thought out and
> it should be reworked in the open, and reverting that wholesale
> would hopefully give us a cleaner ground to design it.
>
> The end-result of such a reworking in the open may turn out to be
> the same (or similar) "register the blob object name of the contents
> to appear in approved hook scripts", or it may look completely
> different.  But the road to get there, and the state of the system
> while we get there, would be different.

I see there is no convincing you that the difference to our regular
"revert-then-redesign-in-the-open" process is that we're talking about
something security-relevant here, and that the revert should hence not be
done lightly.

I've made my position clear, so have you. Since you have the last say,
it's what you say that goes.

Let me quickly iterate on this here patch series (as well as the
`tentative/maint-*` branches) so that we can accelerate toward a fixed
version again; Git LFS has been broken for long enough, I'd think.

Ciao,
Johannes

> I would probably see if I can take brian's revert directly; if it
> applies to the oldest maint-2.39 track, it would be the ideal, but
> we'd still need to prepare a similar 7-track cascade like we did for
> the js/fix-clone-w-hooks-2.XX topics.  If it is only for the master,
> it needs to be munged to apply to maint-2.39 first.
>
> Thanks.
>


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 5/8] hook(clone protections): add escape hatch
  2024-05-20 19:38                     ` Johannes Schindelin
@ 2024-05-20 20:07                       ` Junio C Hamano
  2024-05-20 21:03                       ` Johannes Schindelin
  1 sibling, 0 replies; 54+ messages in thread
From: Junio C Hamano @ 2024-05-20 20:07 UTC (permalink / raw
  To: Johannes Schindelin
  Cc: Jeff King, Johannes Schindelin via GitGitGadget, git,
	brian m. carlson

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:

>> As brian already said, you can reduce the score by making Git do
>> nothing, which is _also_ an absurd position to take "security" (in air
>> quotes) over everything else like usability and functionality.  And this
>> time, the layered security went a bit too aggressive.
>
> Right. And I never said that we should do something as absurd, so I fail
> to see your point.

It went a bit too aggressive, closer to an absurd version of Git
that does nothing, for users of git-lfs and the hooksdir config.
Luckily these two were reported/found soon enough but we do not know
what other fallouts remain.

> Let me quickly iterate on this here patch series (as well as the
> `tentative/maint-*` branches) so that we can accelerate toward a fixed
> version again; Git LFS has been broken for long enough, I'd think.

It would be nice to go back to the pre-2.39.4 state so that we
can redo it from the clean slate soon.

Thanks.


^ permalink raw reply	[flat|nested] 54+ messages in thread

* [PATCH v3 0/6] Various fixes for v2.45.1 and friends
  2024-05-18 10:32 ` [PATCH v2 " Johannes Schindelin via GitGitGadget
                     ` (8 preceding siblings ...)
  2024-05-18 17:07   ` [PATCH v2 0/8] Various fixes for v2.45.1 and friends Junio C Hamano
@ 2024-05-20 20:21   ` Johannes Schindelin via GitGitGadget
  2024-05-20 20:22     ` [PATCH v3 1/6] hook: plug a new memory leak Johannes Schindelin via GitGitGadget
                       ` (7 more replies)
  9 siblings, 8 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-20 20:21 UTC (permalink / raw
  To: git; +Cc: brian m. carlson, Jeff King, Johannes Schindelin

There have been a couple of issues that were reported about v2.45.1, and in
addition I have noticed some myself:

 * a memory leak in the clone protection logic
 * a missed adjustment in the Makefile that leads to an incorrect templates
   path in v2.39.4, v2.40.2 and v2.41.1 (but not in v2.42.2, ..., v2.45.1)
 * an overzealous core.hooksPath check
 * that Git LFS clone problem where it exits with an error (even if the
   clone often succeeded...)

This patch series is based on maint-2.39 to allow for (relatively) easy
follow-up versions v2.39.5, ..., v2.45.2.

Changes since v2:

 * instead of introducing an escape hatch for the clone protections and
   special-casing Git LFS, drop the clone protections

Changes since v1:

 * simplified adding the SHA-256s corresponding to Git LFS' hooks
 * the core.hooksPath test case now verifies that the config setting was
   configured correctly

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"

 Makefile                     |  2 +-
 builtin/clone.c              | 12 +-------
 cache.h                      | 14 ---------
 config.c                     | 13 +-------
 copy.c                       | 58 ------------------------------------
 hook.c                       | 32 --------------------
 t/helper/test-path-utils.c   | 10 -------
 t/t0060-path-utils.sh        | 41 -------------------------
 t/t1350-config-hooks-path.sh |  7 +++++
 t/t1800-hook.sh              | 15 ----------
 t/t5601-clone.sh             | 51 -------------------------------
 11 files changed, 10 insertions(+), 245 deletions(-)


base-commit: 47b6d90e91835082010da926f6a844d4441c57a6
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1732%2Fdscho%2Fvarious-fixes-for-v2.45.1-and-friends-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1732/dscho/various-fixes-for-v2.45.1-and-friends-v3
Pull-Request: https://github.com/gitgitgadget/git/pull/1732

Range-diff vs v2:

 1:  d4a003bf2ce = 1:  d4a003bf2ce hook: plug a new memory leak
 2:  961dfc35f42 = 2:  961dfc35f42 init: use the correct path of the templates directory again
 3:  57db89a1497 = 3:  57db89a1497 Revert "core.hooksPath: add some protection while cloning"
 4:  cd14042b065 = 4:  cd14042b065 tests: verify that `clone -c core.hooksPath=/dev/null` works again
 5:  b841db8392e < -:  ----------- hook(clone protections): add escape hatch
 6:  5e5128bc232 < -:  ----------- hooks(clone protections): special-case current Git LFS hooks
 7:  bd6d72625f5 ! 5:  0044a355674 hooks(clone protections): simplify templates hooks validation
     @@ Metadata
      Author: Johannes Schindelin <Johannes.Schindelin@gmx.de>
      
       ## Commit message ##
     -    hooks(clone protections): simplify templates hooks validation
     +    clone: drop the protections where hooks aren't run
      
     -    When an active hook is encountered during a clone operation, to protect
     -    against Remote Code Execution attack vectors, Git checks whether the
     -    hook was copied over from the templates directory.
     +    As part of the security bug-fix releases v2.39.4, ..., v2.45.1, I
     +    introduced logic to safeguard `git clone` from running hooks that were
     +    installed _during_ the clone operation.
      
     -    When that logic was introduced, there was no other way to check this
     -    than to add a function to compare files.
     +    The rationale was that Git's CVE-2024-32002, CVE-2021-21300,
     +    CVE-2019-1354, CVE-2019-1353, CVE-2019-1352, and CVE-2019-1349 should
     +    have been low-severity vulnerabilities but were elevated to
     +    critical/high severity by the attack vector that allows a weakness where
     +    files inside `.git/` can be inadvertently written during a `git clone`
     +    to escalate to a Remote Code Execution attack by virtue of installing a
     +    malicious `post-checkout` hook that Git will then run at the end of the
     +    operation without giving the user a chance to see what code is executed.
      
     -    In the meantime, we've added code to compute the SHA-256 checksum of a
     -    given hook and compare that checksum against a list of known-safe ones.
     +    Unfortunately, Git LFS uses a similar strategy to install its own
     +    `post-checkout` hook during a `git clone`; In fact, Git LFS is
     +    installing four separate hooks while running the `smudge` filter.
      
     -    Let's simplify the logic by adding to said list when copying the
     -    templates' hooks.
     +    While this pattern is probably in want of being improved by introducing
     +    better support in Git for Git LFS and other tools wishing to register
     +    hooks to be run at various stages of Git's commands, let's undo the
     +    clone protections to unbreak Git LFS-enabled clones.
      
     -    We need to be careful to support multi-process operations such as
     -    recursive submodule clones: In such a scenario, the list of SHA-256
     -    checksums that is kept in memory is not enough, we also have to pass the
     -    information down to child processes via `GIT_CONFIG_PARAMETERS`.
     -
     -    Extend the regression test in t5601 to ensure that recursive clones are
     -    handled as expected.
     -
     -    Note: Technically there is no way that the checksums computed while
     -    initializing the submodules' gitdirs can be passed to the process that
     -    performs the checkout: For historical reasons, these operations are
     -    performed in processes spawned in separate loops from the
     -    super-project's `git clone` process. But since the templates from which
     -    the submodules are initialized are the very same as the ones from which
     -    the super-project is initialized, we can get away with using the list of
     -    SHA-256 checksums that is computed when initializing the super-project
     -    and passing that down to the `submodule--helper` processes that perform
     -    the recursive checkout.
     +    This reverts commit 8db1e8743c0 (clone: prevent hooks from running
     +    during a clone, 2024-03-28).
      
          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
      
     - ## builtin/init-db.c ##
     -@@
     - #include "exec-cmd.h"
     - #include "parse-options.h"
     - #include "worktree.h"
     -+#include "run-command.h"
     -+#include "hook.h"
     + ## builtin/clone.c ##
     +@@ builtin/clone.c: int cmd_clone(int argc, const char **argv, const char *prefix)
     + 	int err = 0, complete_refs_before_fetch = 1;
     + 	int submodule_progress;
     + 	int filter_submodules = 0;
     +-	const char *template_dir;
     +-	char *template_dir_dup = NULL;
       
     - #ifdef NO_TRUSTABLE_FILEMODE
     - #define TEST_FILEMODE 0
     -@@ builtin/init-db.c: static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
     - 	size_t path_baselen = path->len;
     - 	size_t template_baselen = template_path->len;
     - 	struct dirent *de;
     -+	int is_hooks_dir = ends_with(template_path->buf, "/hooks/");
     + 	struct transport_ls_refs_options transport_ls_refs_options =
     + 		TRANSPORT_LS_REFS_OPTIONS_INIT;
     +@@ builtin/clone.c: int cmd_clone(int argc, const char **argv, const char *prefix)
     + 		usage_msg_opt(_("You must specify a repository to clone."),
     + 			builtin_clone_usage, builtin_clone_options);
       
     - 	/* Note: if ".git/hooks" file exists in the repository being
     - 	 * re-initialized, /etc/core-git/templates/hooks/update would
     -@@ builtin/init-db.c: static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
     - 			strbuf_release(&lnk);
     +-	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)
     +@@ builtin/clone.c: int cmd_clone(int argc, const char **argv, const char *prefix)
       		}
     - 		else if (S_ISREG(st_template.st_mode)) {
     -+			if (is_hooks_dir &&
     -+			    is_executable(template_path->buf))
     -+				add_safe_hook(template_path->buf);
     -+
     - 			if (copy_file(path->buf, template_path->buf, st_template.st_mode))
     - 				die_errno(_("cannot copy '%s' to '%s'"),
     - 					  template_path->buf, path->buf);
     + 	}
     + 
     +-	init_db(git_dir, real_git_dir, template_dir, GIT_HASH_UNKNOWN, NULL,
     ++	init_db(git_dir, real_git_dir, option_template, GIT_HASH_UNKNOWN, NULL,
     + 		INIT_DB_QUIET);
     + 
     + 	if (real_git_dir) {
     +@@ builtin/clone.c: int cmd_clone(int argc, const char **argv, const char *prefix)
     + 	free(unborn_head);
     + 	free(dir);
     + 	free(path);
     +-	free(template_dir_dup);
     + 	UNLEAK(repo);
     + 	junk_mode = JUNK_LEAVE_ALL;
     + 
      
       ## hook.c ##
      @@
     + #include "run-command.h"
       #include "config.h"
     - #include "strmap.h"
       
      -static int identical_to_template_hook(const char *name, const char *path)
      -{
     @@ hook.c
      -	return ret;
      -}
      -
     - static struct strset safe_hook_sha256s = STRSET_INIT;
     - static int safe_hook_sha256s_initialized;
     - 
     -@@ hook.c: static int get_sha256_of_file_contents(const char *path, char *sha256)
     - 	return 0;
     - }
     - 
     -+void add_safe_hook(const char *path)
     -+{
     -+	char sha256[GIT_SHA256_HEXSZ + 1] = { '\0' };
     -+
     -+	if (!get_sha256_of_file_contents(path, sha256)) {
     -+		char *p;
     -+
     -+		strset_add(&safe_hook_sha256s, sha256);
     -+
     -+		/* support multi-process operations e.g. recursive clones */
     -+		p = xstrfmt("safe.hook.sha256=%s", sha256);
     -+		git_config_push_parameter(p);
     -+		free(p);
     -+	}
     -+}
     -+
     - static int safe_hook_cb(const char *key, const char *value, void *d)
     + const char *find_hook(const char *name)
       {
     - 	struct strset *set = d;
     + 	static struct strbuf path = STRBUF_INIT;
      @@ hook.c: const char *find_hook(const char *name)
     + 		}
       		return NULL;
       	}
     - 	if (!git_hooks_path && git_env_bool("GIT_CLONE_PROTECTION_ACTIVE", 0) &&
     --	    !identical_to_template_hook(name, path.buf) &&
     - 	    !is_hook_safe_during_clone(name, path.buf, sha256))
     - 		die(_("active `%s` hook found during `git clone`:\n\t%s\n"
     - 		      "For security reasons, this is disallowed by default.\n"
     -
     - ## hook.h ##
     -@@ hook.h: int run_hooks(const char *hook_name);
     -  * hook. This function behaves like the old run_hook_le() API.
     -  */
     - int run_hooks_l(const char *hook_name, ...);
     -+
     -+/**
     -+ * Mark the contents of the provided path as safe to run during a clone
     -+ * operation.
     -+ *
     -+ * This function is mainly used when copying templates to mark the
     -+ * just-copied hooks as benign.
     -+ */
     -+void add_safe_hook(const char *path);
     -+
     - #endif
     -
     - ## setup.c ##
     -@@
     - #include "promisor-remote.h"
     - #include "quote.h"
     - #include "exec-cmd.h"
     -+#include "hook.h"
     +-	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;
     + }
       
     - static int inside_git_dir = -1;
     - static int inside_work_tree = -1;
      
       ## t/t5601-clone.sh ##
     -@@ t/t5601-clone.sh: test_expect_success 'clone with init.templatedir runs hooks' '
     - 		git config --unset init.templateDir &&
     - 		! grep "active .* hook found" err &&
     - 		test_path_is_missing hook-run-local-config/hook.run
     -+	) &&
     -+
     -+	test_config_global protocol.file.allow always &&
     -+	git -C tmpl/hooks submodule add "$(pwd)/tmpl/hooks" sub &&
     -+	test_tick &&
     -+	git -C tmpl/hooks add .gitmodules sub &&
     -+	git -C tmpl/hooks commit -m submodule &&
     -+
     -+	(
     -+		sane_unset GIT_TEMPLATE_DIR &&
     -+		NO_SET_GIT_TEMPLATE_DIR=t &&
     -+		export NO_SET_GIT_TEMPLATE_DIR &&
     -+
     -+		git -c init.templateDir="$(pwd)/tmpl" \
     -+			clone --recurse-submodules \
     -+			tmpl/hooks hook-run-submodule 2>err &&
     -+		! grep "active .* hook found" err &&
     -+		test_path_is_file hook-run-submodule/hook.run &&
     -+		test_path_is_file hook-run-submodule/sub/hook.run
     - 	)
     +@@ t/t5601-clone.sh: test_expect_success 'batch missing blob request does not inadvertently try to fe
     + 	git clone --filter=blob:limit=0 "file://$(pwd)/server" client
       '
       
     +-test_expect_success 'clone with init.templatedir runs hooks' '
     +-	git init tmpl/hooks &&
     +-	write_script tmpl/hooks/post-checkout <<-EOF &&
     +-	echo HOOK-RUN >&2
     +-	echo I was here >hook.run
     +-	EOF
     +-	git -C tmpl/hooks add . &&
     +-	test_tick &&
     +-	git -C tmpl/hooks commit -m post-checkout &&
     +-
     +-	test_when_finished "git config --global --unset init.templateDir || :" &&
     +-	test_when_finished "git config --unset init.templateDir || :" &&
     +-	(
     +-		sane_unset GIT_TEMPLATE_DIR &&
     +-		NO_SET_GIT_TEMPLATE_DIR=t &&
     +-		export NO_SET_GIT_TEMPLATE_DIR &&
     +-
     +-		git -c core.hooksPath="$(pwd)/tmpl/hooks" \
     +-			clone tmpl/hooks hook-run-hookspath 2>err &&
     +-		! grep "active .* hook found" err &&
     +-		test_path_is_file hook-run-hookspath/hook.run &&
     +-
     +-		git -c init.templateDir="$(pwd)/tmpl" \
     +-			clone tmpl/hooks hook-run-config 2>err &&
     +-		! grep "active .* hook found" err &&
     +-		test_path_is_file hook-run-config/hook.run &&
     +-
     +-		git clone --template=tmpl tmpl/hooks hook-run-option 2>err &&
     +-		! grep "active .* hook found" err &&
     +-		test_path_is_file hook-run-option/hook.run &&
     +-
     +-		git config --global init.templateDir "$(pwd)/tmpl" &&
     +-		git clone tmpl/hooks hook-run-global-config 2>err &&
     +-		git config --global --unset init.templateDir &&
     +-		! grep "active .* hook found" err &&
     +-		test_path_is_file hook-run-global-config/hook.run &&
     +-
     +-		# clone ignores local `init.templateDir`; need to create
     +-		# a new repository because we deleted `.git/` in the
     +-		# `setup` test case above
     +-		git init local-clone &&
     +-		cd local-clone &&
     +-
     +-		git config init.templateDir "$(pwd)/../tmpl" &&
     +-		git clone ../tmpl/hooks hook-run-local-config 2>err &&
     +-		git config --unset init.templateDir &&
     +-		! grep "active .* hook found" err &&
     +-		test_path_is_missing hook-run-local-config/hook.run
     +-	)
     +-'
     +-
     + . "$TEST_DIRECTORY"/lib-httpd.sh
     + start_httpd
     + 
 8:  4b0a636d41a = 6:  5c576e889d8 Revert "Add a helper function to compare file contents"

-- 
gitgitgadget


^ permalink raw reply	[flat|nested] 54+ messages in thread

* [PATCH v3 1/6] hook: plug a new memory leak
  2024-05-20 20:21   ` [PATCH v3 0/6] " Johannes Schindelin via GitGitGadget
@ 2024-05-20 20:22     ` Johannes Schindelin via GitGitGadget
  2024-05-20 20:22     ` [PATCH v3 2/6] init: use the correct path of the templates directory again Johannes Schindelin via GitGitGadget
                       ` (6 subsequent siblings)
  7 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-20 20:22 UTC (permalink / raw
  To: git; +Cc: brian m. carlson, Jeff King, Johannes Schindelin,
	Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

In 8db1e8743c0 (clone: prevent hooks from running during a clone,
2024-03-28), I introduced an inadvertent memory leak that was
unfortunately not caught before v2.45.1 was released. Here is a fix.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 hook.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/hook.c b/hook.c
index 632b537b993..fc974cee1d8 100644
--- a/hook.c
+++ b/hook.c
@@ -18,8 +18,10 @@ static int identical_to_template_hook(const char *name, const char *path)
 		found_template_hook = access(template_path.buf, X_OK) >= 0;
 	}
 #endif
-	if (!found_template_hook)
+	if (!found_template_hook) {
+		strbuf_release(&template_path);
 		return 0;
+	}
 
 	ret = do_files_match(template_path.buf, path);
 
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH v3 2/6] init: use the correct path of the templates directory again
  2024-05-20 20:21   ` [PATCH v3 0/6] " Johannes Schindelin via GitGitGadget
  2024-05-20 20:22     ` [PATCH v3 1/6] hook: plug a new memory leak Johannes Schindelin via GitGitGadget
@ 2024-05-20 20:22     ` Johannes Schindelin via GitGitGadget
  2024-05-20 20:22     ` [PATCH v3 3/6] Revert "core.hooksPath: add some protection while cloning" Johannes Schindelin via GitGitGadget
                       ` (5 subsequent siblings)
  7 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-20 20:22 UTC (permalink / raw
  To: git; +Cc: brian m. carlson, Jeff King, Johannes Schindelin,
	Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

In df93e407f06 (init: refactor the template directory discovery into its
own function, 2024-03-29), I refactored the way the templates directory
is discovered.

The refactoring was faithful, but missed a reference in the `Makefile`
where the `DEFAULT_GIT_TEMPLATE_DIR` constant is defined. As a
consequence, Git v2.45.1 and friends will always use the hard-coded path
`/usr/share/git-core/templates`.

Let's fix that by defining the `DEFAULT_GIT_TEMPLATE_DIR` when building
`setup.o`, where that constant is actually used.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Makefile b/Makefile
index 093829ae283..4b1502ba2c6 100644
--- a/Makefile
+++ b/Makefile
@@ -2751,7 +2751,7 @@ exec-cmd.sp exec-cmd.s exec-cmd.o: EXTRA_CPPFLAGS = \
 	'-DFALLBACK_RUNTIME_PREFIX="$(prefix_SQ)"'
 
 builtin/init-db.sp builtin/init-db.s builtin/init-db.o: GIT-PREFIX
-builtin/init-db.sp builtin/init-db.s builtin/init-db.o: EXTRA_CPPFLAGS = \
+setup.sp setup.s setup.o: EXTRA_CPPFLAGS = \
 	-DDEFAULT_GIT_TEMPLATE_DIR='"$(template_dir_SQ)"'
 
 config.sp config.s config.o: GIT-PREFIX
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH v3 3/6] Revert "core.hooksPath: add some protection while cloning"
  2024-05-20 20:21   ` [PATCH v3 0/6] " Johannes Schindelin via GitGitGadget
  2024-05-20 20:22     ` [PATCH v3 1/6] hook: plug a new memory leak Johannes Schindelin via GitGitGadget
  2024-05-20 20:22     ` [PATCH v3 2/6] init: use the correct path of the templates directory again Johannes Schindelin via GitGitGadget
@ 2024-05-20 20:22     ` Johannes Schindelin via GitGitGadget
  2024-05-20 20:22     ` [PATCH v3 4/6] tests: verify that `clone -c core.hooksPath=/dev/null` works again Johannes Schindelin via GitGitGadget
                       ` (4 subsequent siblings)
  7 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-20 20:22 UTC (permalink / raw
  To: git; +Cc: brian m. carlson, Jeff King, Johannes Schindelin,
	Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

This defense-in-depth was intended to protect the clone operation
against future escalations where bugs in `git clone` would allow
attackers to write arbitrary files in the `.git/` directory would allow
for Remote Code Execution attacks via maliciously-placed hooks.

However, it turns out that the `core.hooksPath` protection has
unintentional side effects so severe that they do not justify the
benefit of the protections. For example, it has been reported in
https://lore.kernel.org/git/FAFA34CB-9732-4A0A-87FB-BDB272E6AEE8@alchemists.io/
that the following invocation, which is intended to make `git clone`
safer, is itself broken by that protective measure:

	git clone --config core.hooksPath=/dev/null <url>

Since it turns out that the benefit does not justify the cost, let's revert
20f3588efc6 (core.hooksPath: add some protection while cloning,
2024-03-30).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 config.c        | 13 +------------
 t/t1800-hook.sh | 15 ---------------
 2 files changed, 1 insertion(+), 27 deletions(-)

diff --git a/config.c b/config.c
index 85b37f2ee09..8c1c4071f0d 100644
--- a/config.c
+++ b/config.c
@@ -1525,19 +1525,8 @@ static int git_default_core_config(const char *var, const char *value, void *cb)
 	if (!strcmp(var, "core.attributesfile"))
 		return git_config_pathname(&git_attributes_file, var, value);
 
-	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);
+	if (!strcmp(var, "core.hookspath"))
 		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/t/t1800-hook.sh b/t/t1800-hook.sh
index 7ee12e6f48a..2ef3579fa7c 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -177,19 +177,4 @@ test_expect_success 'git hook run a hook with a bad shebang' '
 	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
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH v3 4/6] tests: verify that `clone -c core.hooksPath=/dev/null` works again
  2024-05-20 20:21   ` [PATCH v3 0/6] " Johannes Schindelin via GitGitGadget
                       ` (2 preceding siblings ...)
  2024-05-20 20:22     ` [PATCH v3 3/6] Revert "core.hooksPath: add some protection while cloning" Johannes Schindelin via GitGitGadget
@ 2024-05-20 20:22     ` Johannes Schindelin via GitGitGadget
  2024-05-20 20:22     ` [PATCH v3 5/6] clone: drop the protections where hooks aren't run Johannes Schindelin via GitGitGadget
                       ` (3 subsequent siblings)
  7 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-20 20:22 UTC (permalink / raw
  To: git; +Cc: brian m. carlson, Jeff King, Johannes Schindelin,
	Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

As part of the protections added in Git v2.45.1 and friends,
repository-local `core.hooksPath` settings are no longer allowed, as a
defense-in-depth mechanism to prevent future Git vulnerabilities to
raise to critical level if those vulnerabilities inadvertently allow the
repository-local config to be written.

What the added protection did not anticipate is that such a
repository-local `core.hooksPath` can not only be used to point to
maliciously-placed scripts in the current worktree, but also to
_prevent_ hooks from being called altogether.

We just reverted the `core.hooksPath` protections, based on the Git
maintainer's recommendation in
https://lore.kernel.org/git/xmqq4jaxvm8z.fsf@gitster.g/ to address this
concern as well as related ones. Let's make sure that we won't regress
while trying to protect the clone operation further.

Reported-by: Brooke Kuhlmann <brooke@alchemists.io>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 t/t1350-config-hooks-path.sh | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh
index f6dc83e2aab..45a04929170 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
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH v3 5/6] clone: drop the protections where hooks aren't run
  2024-05-20 20:21   ` [PATCH v3 0/6] " Johannes Schindelin via GitGitGadget
                       ` (3 preceding siblings ...)
  2024-05-20 20:22     ` [PATCH v3 4/6] tests: verify that `clone -c core.hooksPath=/dev/null` works again Johannes Schindelin via GitGitGadget
@ 2024-05-20 20:22     ` Johannes Schindelin via GitGitGadget
  2024-05-20 20:22     ` [PATCH v3 6/6] Revert "Add a helper function to compare file contents" Johannes Schindelin via GitGitGadget
                       ` (2 subsequent siblings)
  7 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-20 20:22 UTC (permalink / raw
  To: git; +Cc: brian m. carlson, Jeff King, Johannes Schindelin,
	Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

As part of the security bug-fix releases v2.39.4, ..., v2.45.1, I
introduced logic to safeguard `git clone` from running hooks that were
installed _during_ the clone operation.

The rationale was that Git's CVE-2024-32002, CVE-2021-21300,
CVE-2019-1354, CVE-2019-1353, CVE-2019-1352, and CVE-2019-1349 should
have been low-severity vulnerabilities but were elevated to
critical/high severity by the attack vector that allows a weakness where
files inside `.git/` can be inadvertently written during a `git clone`
to escalate to a Remote Code Execution attack by virtue of installing a
malicious `post-checkout` hook that Git will then run at the end of the
operation without giving the user a chance to see what code is executed.

Unfortunately, Git LFS uses a similar strategy to install its own
`post-checkout` hook during a `git clone`; In fact, Git LFS is
installing four separate hooks while running the `smudge` filter.

While this pattern is probably in want of being improved by introducing
better support in Git for Git LFS and other tools wishing to register
hooks to be run at various stages of Git's commands, let's undo the
clone protections to unbreak Git LFS-enabled clones.

This reverts commit 8db1e8743c0 (clone: prevent hooks from running
during a clone, 2024-03-28).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/clone.c  | 12 +-----------
 hook.c           | 34 --------------------------------
 t/t5601-clone.sh | 51 ------------------------------------------------
 3 files changed, 1 insertion(+), 96 deletions(-)

diff --git a/builtin/clone.c b/builtin/clone.c
index e7721f5c22c..9ec500d427e 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -937,8 +937,6 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
 	int err = 0, complete_refs_before_fetch = 1;
 	int submodule_progress;
 	int filter_submodules = 0;
-	const char *template_dir;
-	char *template_dir_dup = NULL;
 
 	struct transport_ls_refs_options transport_ls_refs_options =
 		TRANSPORT_LS_REFS_OPTIONS_INIT;
@@ -958,13 +956,6 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
 		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)
@@ -1112,7 +1103,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
 		}
 	}
 
-	init_db(git_dir, real_git_dir, template_dir, GIT_HASH_UNKNOWN, NULL,
+	init_db(git_dir, real_git_dir, option_template, GIT_HASH_UNKNOWN, NULL,
 		INIT_DB_QUIET);
 
 	if (real_git_dir) {
@@ -1430,7 +1421,6 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
 	free(unborn_head);
 	free(dir);
 	free(path);
-	free(template_dir_dup);
 	UNLEAK(repo);
 	junk_mode = JUNK_LEAVE_ALL;
 
diff --git a/hook.c b/hook.c
index fc974cee1d8..22b274b60b1 100644
--- a/hook.c
+++ b/hook.c
@@ -3,32 +3,6 @@
 #include "run-command.h"
 #include "config.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) {
-		strbuf_release(&template_path);
-		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;
@@ -64,14 +38,6 @@ const char *find_hook(const char *name)
 		}
 		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/t/t5601-clone.sh b/t/t5601-clone.sh
index 20deca0231b..fd029843307 100755
--- a/t/t5601-clone.sh
+++ b/t/t5601-clone.sh
@@ -771,57 +771,6 @@ test_expect_success 'batch missing blob request does not inadvertently try to fe
 	git clone --filter=blob:limit=0 "file://$(pwd)/server" client
 '
 
-test_expect_success 'clone with init.templatedir runs hooks' '
-	git init tmpl/hooks &&
-	write_script tmpl/hooks/post-checkout <<-EOF &&
-	echo HOOK-RUN >&2
-	echo I was here >hook.run
-	EOF
-	git -C tmpl/hooks add . &&
-	test_tick &&
-	git -C tmpl/hooks commit -m post-checkout &&
-
-	test_when_finished "git config --global --unset init.templateDir || :" &&
-	test_when_finished "git config --unset init.templateDir || :" &&
-	(
-		sane_unset GIT_TEMPLATE_DIR &&
-		NO_SET_GIT_TEMPLATE_DIR=t &&
-		export NO_SET_GIT_TEMPLATE_DIR &&
-
-		git -c core.hooksPath="$(pwd)/tmpl/hooks" \
-			clone tmpl/hooks hook-run-hookspath 2>err &&
-		! grep "active .* hook found" err &&
-		test_path_is_file hook-run-hookspath/hook.run &&
-
-		git -c init.templateDir="$(pwd)/tmpl" \
-			clone tmpl/hooks hook-run-config 2>err &&
-		! grep "active .* hook found" err &&
-		test_path_is_file hook-run-config/hook.run &&
-
-		git clone --template=tmpl tmpl/hooks hook-run-option 2>err &&
-		! grep "active .* hook found" err &&
-		test_path_is_file hook-run-option/hook.run &&
-
-		git config --global init.templateDir "$(pwd)/tmpl" &&
-		git clone tmpl/hooks hook-run-global-config 2>err &&
-		git config --global --unset init.templateDir &&
-		! grep "active .* hook found" err &&
-		test_path_is_file hook-run-global-config/hook.run &&
-
-		# clone ignores local `init.templateDir`; need to create
-		# a new repository because we deleted `.git/` in the
-		# `setup` test case above
-		git init local-clone &&
-		cd local-clone &&
-
-		git config init.templateDir "$(pwd)/../tmpl" &&
-		git clone ../tmpl/hooks hook-run-local-config 2>err &&
-		git config --unset init.templateDir &&
-		! grep "active .* hook found" err &&
-		test_path_is_missing hook-run-local-config/hook.run
-	)
-'
-
 . "$TEST_DIRECTORY"/lib-httpd.sh
 start_httpd
 
-- 
gitgitgadget



^ permalink raw reply related	[flat|nested] 54+ messages in thread

* [PATCH v3 6/6] Revert "Add a helper function to compare file contents"
  2024-05-20 20:21   ` [PATCH v3 0/6] " Johannes Schindelin via GitGitGadget
                       ` (4 preceding siblings ...)
  2024-05-20 20:22     ` [PATCH v3 5/6] clone: drop the protections where hooks aren't run Johannes Schindelin via GitGitGadget
@ 2024-05-20 20:22     ` Johannes Schindelin via GitGitGadget
  2024-05-20 23:56     ` [PATCH v3 0/6] Various fixes for v2.45.1 and friends Junio C Hamano
  2024-05-21 22:33     ` brian m. carlson
  7 siblings, 0 replies; 54+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2024-05-20 20:22 UTC (permalink / raw
  To: git; +Cc: brian m. carlson, Jeff King, Johannes Schindelin,
	Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

Now that during a `git clone`, the hooks' contents are no longer
compared to the templates' files', the caller for which the
`do_files_match()` function was introduced is gone, and therefore this
function can be retired, too.

This reverts commit 584de0b4c23 (Add a helper function to compare file
contents, 2024-03-30).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 cache.h                    | 14 ---------
 copy.c                     | 58 --------------------------------------
 t/helper/test-path-utils.c | 10 -------
 t/t0060-path-utils.sh      | 41 ---------------------------
 4 files changed, 123 deletions(-)

diff --git a/cache.h b/cache.h
index 16b34799bfd..8c5fb1e1ba1 100644
--- a/cache.h
+++ b/cache.h
@@ -1785,20 +1785,6 @@ int copy_fd(int ifd, int ofd);
 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);
-
 void write_or_die(int fd, const void *buf, size_t count);
 void fsync_or_die(int fd, const char *);
 int fsync_component(enum fsync_component component, int fd);
diff --git a/copy.c b/copy.c
index 8492f6fc831..4de6a110f09 100644
--- a/copy.c
+++ b/copy.c
@@ -65,61 +65,3 @@ int copy_file_with_time(const char *dst, const char *src, int mode)
 		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/t/helper/test-path-utils.c b/t/helper/test-path-utils.c
index 0e0de218076..f69709d674f 100644
--- a/t/helper/test-path-utils.c
+++ b/t/helper/test-path-utils.c
@@ -495,16 +495,6 @@ int cmd__path_utils(int argc, const char **argv)
 		return !!res;
 	}
 
-	if (argc == 4 && !strcmp(argv[1], "do_files_match")) {
-		int ret = do_files_match(argv[2], argv[3]);
-
-		if (ret)
-			printf("equal\n");
-		else
-			printf("different\n");
-		return !ret;
-	}
-
 	fprintf(stderr, "%s: unknown function name: %s\n", argv[0],
 		argv[1] ? argv[1] : "(there was none)");
 	return 1;
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index 73d0e1a7f10..68e29c904a6 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -560,45 +560,4 @@ test_expect_success !VALGRIND,RUNTIME_PREFIX,CAN_EXEC_IN_PWD '%(prefix)/ works'
 	test_cmp expect actual
 '
 
-test_expect_success 'do_files_match()' '
-	test_seq 0 10 >0-10.txt &&
-	test_seq -1 10 >-1-10.txt &&
-	test_seq 1 10 >1-10.txt &&
-	test_seq 1 9 >1-9.txt &&
-	test_seq 0 8 >0-8.txt &&
-
-	test-tool path-utils do_files_match 0-10.txt 0-10.txt >out &&
-
-	assert_fails() {
-		test_must_fail \
-		test-tool path-utils do_files_match "$1" "$2" >out &&
-		grep different out
-	} &&
-
-	assert_fails 0-8.txt 1-9.txt &&
-	assert_fails -1-10.txt 0-10.txt &&
-	assert_fails 1-10.txt 1-9.txt &&
-	assert_fails 1-10.txt .git &&
-	assert_fails does-not-exist 1-10.txt &&
-
-	if test_have_prereq FILEMODE
-	then
-		cp 0-10.txt 0-10.x &&
-		chmod a+x 0-10.x &&
-		assert_fails 0-10.txt 0-10.x
-	fi &&
-
-	if test_have_prereq SYMLINKS
-	then
-		ln -sf 0-10.txt symlink &&
-		ln -s 0-10.txt another-symlink &&
-		ln -s over-the-ocean yet-another-symlink &&
-		ln -s "$PWD/0-10.txt" absolute-symlink &&
-		assert_fails 0-10.txt symlink &&
-		test-tool path-utils do_files_match symlink another-symlink &&
-		assert_fails symlink yet-another-symlink &&
-		assert_fails symlink absolute-symlink
-	fi
-'
-
 test_done
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 54+ messages in thread

* Re: [PATCH v2 5/8] hook(clone protections): add escape hatch
  2024-05-20 19:38                     ` Johannes Schindelin
  2024-05-20 20:07                       ` Junio C Hamano
@ 2024-05-20 21:03                       ` Johannes Schindelin
  1 sibling, 0 replies; 54+ messages in thread
From: Johannes Schindelin @ 2024-05-20 21:03 UTC (permalink / raw
  To: Junio C Hamano
  Cc: Jeff King, Johannes Schindelin via GitGitGadget, git,
	brian m. carlson

Hi,

On Mon, 20 May 2024, Johannes Schindelin wrote:

> Let me quickly iterate on this here patch series (as well as the
> `tentative/maint-*` branches) so that we can accelerate toward a fixed
> version again

v3 is on the list. The `tentative/maint-*` branches have been pushed to
https://github.com/dscho/git, with these commit OIDs:

b9a96c4e5dc4e04258214ab772972a0e1eefd3c5 refs/heads/tentative/maint-2.39
4bf5d57da62f91db9b74d490d5dae69e65cbdc73 refs/heads/tentative/maint-2.40
5215e4e36879d1ee0ad5da7790f4598c3314ed45 refs/heads/tentative/maint-2.41
33efa2ad1a6c14fc5d8bc5cdf38ba13b25926b42 refs/heads/tentative/maint-2.42
0aeca2f80b17fcfdd9186c585ce84004ed43f46a refs/heads/tentative/maint-2.43
9953011fcdd895fd3ff4e2f2e5ff266eaf8b0b49 refs/heads/tentative/maint-2.44
aeddcb02756259e4b221f37a60e4ee1ece3889f1 refs/heads/tentative/maint-2.45

Ciao,
Johannes


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v3 0/6] Various fixes for v2.45.1 and friends
  2024-05-20 20:21   ` [PATCH v3 0/6] " Johannes Schindelin via GitGitGadget
                       ` (5 preceding siblings ...)
  2024-05-20 20:22     ` [PATCH v3 6/6] Revert "Add a helper function to compare file contents" Johannes Schindelin via GitGitGadget
@ 2024-05-20 23:56     ` Junio C Hamano
  2024-05-21  5:33       ` Junio C Hamano
  2024-05-21 22:33     ` brian m. carlson
  7 siblings, 1 reply; 54+ messages in thread
From: Junio C Hamano @ 2024-05-20 23:56 UTC (permalink / raw
  To: Johannes Schindelin via GitGitGadget
  Cc: git, brian m. carlson, Jeff King, Johannes Schindelin

"Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
writes:

> This patch series is based on maint-2.39 to allow for (relatively) easy
> follow-up versions v2.39.5, ..., v2.45.2.
>
> Changes since v2:
>
>  * instead of introducing an escape hatch for the clone protections and
>    special-casing Git LFS, drop the clone protections

It is debatable if we are ripping out clone "protection" or a new
restriction on executing hooks before the end of clone that has
backfired. 

In any case, I just compared the result of applying these patches to
v2.39.4 with the result of reverting the following out of v2.39.4:

    584de0b4 (Add a helper function to compare file contents, 2024-03-30)
    8db1e874 (clone: prevent hooks from running during a clone, 2024-03-28)
    20f3588e (core.hooksPath: add some protection while cloning, 2024-03-30)

and the differences was exactly as I expected.  A Makefile fix and a
new test added to t1350 are the extra in the series, but otherwise
the patches are essentially reversion of these three steps.  Very
nicely done.

Thanks for a quick turnaround.  Will take further look.




^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v3 0/6] Various fixes for v2.45.1 and friends
  2024-05-20 23:56     ` [PATCH v3 0/6] Various fixes for v2.45.1 and friends Junio C Hamano
@ 2024-05-21  5:33       ` Junio C Hamano
  2024-05-21 18:14         ` Junio C Hamano
  0 siblings, 1 reply; 54+ messages in thread
From: Junio C Hamano @ 2024-05-21  5:33 UTC (permalink / raw
  To: Johannes Schindelin via GitGitGadget
  Cc: git, brian m. carlson, Jeff King, Johannes Schindelin

Junio C Hamano <gitster@pobox.com> writes:

> In any case, I just compared the result of applying these patches to
> v2.39.4 with the result of reverting the following out of v2.39.4:
>
>     584de0b4 (Add a helper function to compare file contents, 2024-03-30)
>     8db1e874 (clone: prevent hooks from running during a clone, 2024-03-28)
>     20f3588e (core.hooksPath: add some protection while cloning, 2024-03-30)
>
> and the differences was exactly as I expected.  A Makefile fix and a
> new test added to t1350 are the extra in the series, but otherwise
> the patches are essentially reversion of these three steps.  Very
> nicely done.
>
> Thanks for a quick turnaround.  Will take further look.

I completed merge-up exercise and compared the result with your
"tentative" cascade from maint-2.39 to maint-2.45 tracks.  

The differences came from pointed cherry-picks (like 'ci: avoid bare
"gcc" for osx-gcc job') looked minimal and sensible.  I wonder what
the best way to do a public review of this kind of history, though.

$ git log --oneline --graph maint-2.45..dscho/tentative/maint-2.45
* aeddcb0275 Git 2.45.2
* 65f0d62523 Sync with 2.44.2
* 9953011fcd Git 2.44.2
* f78818b645 Sync with 2.43.5
* 0aeca2f80b Git 2.43.5
* 0cc3782b1a Sync with 2.42.3
* 33efa2ad1a Git 2.42.3
* 30195eb2b6 Sync with 2.41.2
* 5215e4e368 Git 2.41.2
* 9d6788fd73 Sync with 2.40.3
* 4bf5d57da6 Git 2.40.3
* 9f7a956be5 Sync with 2.39.5
* b9a96c4e5d Git 2.39.5

All of the above (and the one below) are merging up, resolving
conflicts, and updating release notes and GIT-VERSION-GEN.

*   b674c6f66c Merge branch 'js/fix-v2.39.4-regressions' into maint-2.39
|\  
| * 5c576e889d Revert "Add a helper function to compare file contents"
| * 0044a35567 clone: drop the protections where hooks aren't run
| * cd14042b06 tests: verify that `clone -c core.hooksPath=/dev/null` works again
| * 57db89a149 Revert "core.hooksPath: add some protection while cloning"
| * 961dfc35f4 init: use the correct path of the templates directory again
| * d4a003bf2c hook: plug a new memory leak

The above 6 patches all appeared on the list in this "v3" thread.

* 883ca51e0a Merge branch 'jk/ci-macos-gcc13-fix' into 'maint-2.39'

This is a merge of the following three patches to maint-2.39

* d4543be3f2 ci: stop installing "gcc-13" for osx-gcc
* 2aef8020d2 ci: avoid bare "gcc" for osx-gcc job
* f3e5bdfebc ci: drop mention of BREW_INSTALL_PACKAGES variable

These three patches were taken from jk/ci-macos-gcc13-fix that was
forked from v2.45.0 and rebased them on top of v2.39.4.  The bottom
one seems to have been adjusted for the older contexts, which during
subsequent merging-up has been adjusted back again for the more
recent contexts (e.g., we used to use $HOME/bin but use $P4_PATH for
Perforce these days, and such differences in the base version appear
in the context for "BREW_INSTALL_PACKAGES" change).

So, in short, I didn't see anything unexpected to see in these
branches.  The "ci" fixes were already reviewed elsewhere (even
though there are slight deviations), so if people are OK with the 6
patches in this thread, I would say we are good to go.

Thanks.







^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v3 0/6] Various fixes for v2.45.1 and friends
  2024-05-21  5:33       ` Junio C Hamano
@ 2024-05-21 18:14         ` Junio C Hamano
  0 siblings, 0 replies; 54+ messages in thread
From: Junio C Hamano @ 2024-05-21 18:14 UTC (permalink / raw
  To: Johannes Schindelin via GitGitGadget
  Cc: git, brian m. carlson, Jeff King, Johannes Schindelin

Junio C Hamano <gitster@pobox.com> writes:

> So, in short, I didn't see anything unexpected to see in these
> branches.  The "ci" fixes were already reviewed elsewhere (even
> though there are slight deviations), so if people are OK with the 6
> patches in this thread, I would say we are good to go.

Maybe I spoke a bit too early, and we may need to redo (not
necessarily revert) f4aa8c8b as well.




^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v3 0/6] Various fixes for v2.45.1 and friends
  2024-05-20 20:21   ` [PATCH v3 0/6] " Johannes Schindelin via GitGitGadget
                       ` (6 preceding siblings ...)
  2024-05-20 23:56     ` [PATCH v3 0/6] Various fixes for v2.45.1 and friends Junio C Hamano
@ 2024-05-21 22:33     ` brian m. carlson
  2024-05-21 22:40       ` Junio C Hamano
  2024-05-21 23:04       ` Junio C Hamano
  7 siblings, 2 replies; 54+ messages in thread
From: brian m. carlson @ 2024-05-21 22:33 UTC (permalink / raw
  To: Johannes Schindelin via GitGitGadget; +Cc: git, Jeff King, Johannes Schindelin

[-- Attachment #1: Type: text/plain, Size: 1075 bytes --]

On 2024-05-20 at 20:21:59, Johannes Schindelin via GitGitGadget wrote:
> There have been a couple of issues that were reported about v2.45.1, and in
> addition I have noticed some myself:
> 
>  * a memory leak in the clone protection logic
>  * a missed adjustment in the Makefile that leads to an incorrect templates
>    path in v2.39.4, v2.40.2 and v2.41.1 (but not in v2.42.2, ..., v2.45.1)
>  * an overzealous core.hooksPath check
>  * that Git LFS clone problem where it exits with an error (even if the
>    clone often succeeded...)
> 
> This patch series is based on maint-2.39 to allow for (relatively) easy
> follow-up versions v2.39.5, ..., v2.45.2.

I looked at this series and seems fine.  I tested it with the latest
HEAD of Git LFS and it seems to function as expected.  I appreciate the
prompt fixes.

(My apologies for not getting back to this sooner.  I took a long
weekend for the Victoria Day holiday and was busy riding rollercoasters
instead of reading the list.)
-- 
brian m. carlson (they/them or he/him)
Toronto, Ontario, CA

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 262 bytes --]

^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v3 0/6] Various fixes for v2.45.1 and friends
  2024-05-21 22:33     ` brian m. carlson
@ 2024-05-21 22:40       ` Junio C Hamano
  2024-05-21 23:04       ` Junio C Hamano
  1 sibling, 0 replies; 54+ messages in thread
From: Junio C Hamano @ 2024-05-21 22:40 UTC (permalink / raw
  To: brian m. carlson
  Cc: Johannes Schindelin via GitGitGadget, git, Jeff King,
	Johannes Schindelin

"brian m. carlson" <sandals@crustytoothpaste.net> writes:

> (My apologies for not getting back to this sooner.  I took a long
> weekend for the Victoria Day holiday and was busy riding rollercoasters
> instead of reading the list.)

That's fine.  We all must have fun sometimes ;-)


^ permalink raw reply	[flat|nested] 54+ messages in thread

* Re: [PATCH v3 0/6] Various fixes for v2.45.1 and friends
  2024-05-21 22:33     ` brian m. carlson
  2024-05-21 22:40       ` Junio C Hamano
@ 2024-05-21 23:04       ` Junio C Hamano
  1 sibling, 0 replies; 54+ messages in thread
From: Junio C Hamano @ 2024-05-21 23:04 UTC (permalink / raw
  To: brian m. carlson
  Cc: Johannes Schindelin via GitGitGadget, git, Jeff King,
	Johannes Schindelin

"brian m. carlson" <sandals@crustytoothpaste.net> writes:

> I looked at this series and seems fine.  I tested it with the latest
> HEAD of Git LFS and it seems to function as expected.  I appreciate the
> prompt fixes.

I've merged the 11 patches (the 12-patch series without the last
step) https://lore.kernel.org/git/xmqqed9u95l0.fsf@gitster.g/ to
'seen'.  These 11 patches contains Dscho's 6 patches but also
contains backports Dscho had on his "tentative" topic branch, plus
two additional backports to help tests pass.  Additionally what is
in 'seen' obviously does not update GIT-VERSION-GEN or add new
release notes files.  Other than that, it is the same topic, only
done in a bit more open way to make it easier to apply further
polish.  Testing is appreciated.

Hopefully they can be merged down to 'next' and then 'master'
soonish, but will be updated if a new issue is discovered.  After
then, we'll do a release engineering to merge them to all the
maintenance tracks affected by the recent security updates and we
can go from there.  Hopefully, as not critical vulnerability fix but
a "layered defence" on top, we probably do not have to merge down
whatever additional follow-up changes beyond the latest maintenance
track (i.e. maint-2.45).

Thanks.


^ permalink raw reply	[flat|nested] 54+ messages in thread

end of thread, other threads:[~2024-05-22  1:52 UTC | newest]

Thread overview: 54+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2024-05-17 23:15 [PATCH 0/8] Various fixes for v2.45.1 and friends Johannes Schindelin via GitGitGadget
2024-05-17 23:15 ` [PATCH 1/8] hook: plug a new memory leak Johannes Schindelin via GitGitGadget
2024-05-17 23:15 ` [PATCH 2/8] init: use the correct path of the templates directory again Johannes Schindelin via GitGitGadget
2024-05-17 23:15 ` [PATCH 3/8] Revert "core.hooksPath: add some protection while cloning" Johannes Schindelin via GitGitGadget
2024-05-17 23:15 ` [PATCH 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again Johannes Schindelin via GitGitGadget
2024-05-18  0:10   ` Junio C Hamano
2024-05-18 18:58     ` Johannes Schindelin
2024-05-17 23:15 ` [PATCH 5/8] hook(clone protections): add escape hatch Johannes Schindelin via GitGitGadget
2024-05-18  0:21   ` Junio C Hamano
2024-05-17 23:15 ` [PATCH 6/8] hooks(clone protections): special-case current Git LFS hooks Johannes Schindelin via GitGitGadget
2024-05-18  0:20   ` Junio C Hamano
2024-05-17 23:15 ` [PATCH 7/8] hooks(clone protections): simplify templates hooks validation Johannes Schindelin via GitGitGadget
2024-05-17 23:15 ` [PATCH 8/8] Revert "Add a helper function to compare file contents" Johannes Schindelin via GitGitGadget
2024-05-17 23:52 ` [PATCH 0/8] Various fixes for v2.45.1 and friends Junio C Hamano
2024-05-18  0:02   ` Johannes Schindelin
2024-05-18 10:32 ` [PATCH v2 " Johannes Schindelin via GitGitGadget
2024-05-18 10:32   ` [PATCH v2 1/8] hook: plug a new memory leak Johannes Schindelin via GitGitGadget
2024-05-18 10:32   ` [PATCH v2 2/8] init: use the correct path of the templates directory again Johannes Schindelin via GitGitGadget
2024-05-18 10:32   ` [PATCH v2 3/8] Revert "core.hooksPath: add some protection while cloning" Johannes Schindelin via GitGitGadget
2024-05-18 10:32   ` [PATCH v2 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again Johannes Schindelin via GitGitGadget
2024-05-18 10:32   ` [PATCH v2 5/8] hook(clone protections): add escape hatch Johannes Schindelin via GitGitGadget
2024-05-18 18:14     ` Jeff King
2024-05-18 18:54       ` Junio C Hamano
2024-05-18 19:35         ` Jeff King
2024-05-18 19:37         ` Johannes Schindelin
2024-05-18 19:32       ` Johannes Schindelin
2024-05-18 19:47         ` Jeff King
2024-05-18 20:06           ` Johannes Schindelin
2024-05-18 21:12             ` Jeff King
2024-05-19  1:15               ` Junio C Hamano
2024-05-20 16:05                 ` Johannes Schindelin
2024-05-20 18:18                   ` Junio C Hamano
2024-05-20 19:38                     ` Johannes Schindelin
2024-05-20 20:07                       ` Junio C Hamano
2024-05-20 21:03                       ` Johannes Schindelin
2024-05-18 10:32   ` [PATCH v2 6/8] hooks(clone protections): special-case current Git LFS hooks Johannes Schindelin via GitGitGadget
2024-05-18 10:32   ` [PATCH v2 7/8] hooks(clone protections): simplify templates hooks validation Johannes Schindelin via GitGitGadget
2024-05-18 10:32   ` [PATCH v2 8/8] Revert "Add a helper function to compare file contents" Johannes Schindelin via GitGitGadget
2024-05-18 17:07   ` [PATCH v2 0/8] Various fixes for v2.45.1 and friends Junio C Hamano
2024-05-18 19:22     ` Johannes Schindelin
2024-05-18 20:13       ` Johannes Schindelin
2024-05-20 20:21   ` [PATCH v3 0/6] " Johannes Schindelin via GitGitGadget
2024-05-20 20:22     ` [PATCH v3 1/6] hook: plug a new memory leak Johannes Schindelin via GitGitGadget
2024-05-20 20:22     ` [PATCH v3 2/6] init: use the correct path of the templates directory again Johannes Schindelin via GitGitGadget
2024-05-20 20:22     ` [PATCH v3 3/6] Revert "core.hooksPath: add some protection while cloning" Johannes Schindelin via GitGitGadget
2024-05-20 20:22     ` [PATCH v3 4/6] tests: verify that `clone -c core.hooksPath=/dev/null` works again Johannes Schindelin via GitGitGadget
2024-05-20 20:22     ` [PATCH v3 5/6] clone: drop the protections where hooks aren't run Johannes Schindelin via GitGitGadget
2024-05-20 20:22     ` [PATCH v3 6/6] Revert "Add a helper function to compare file contents" Johannes Schindelin via GitGitGadget
2024-05-20 23:56     ` [PATCH v3 0/6] Various fixes for v2.45.1 and friends Junio C Hamano
2024-05-21  5:33       ` Junio C Hamano
2024-05-21 18:14         ` Junio C Hamano
2024-05-21 22:33     ` brian m. carlson
2024-05-21 22:40       ` Junio C Hamano
2024-05-21 23:04       ` Junio C Hamano

Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).