{"thread":{"id":"57859","subject":"[PATCH] [RFC] setup.c: make bare repo discovery optional","startedAt":"2022-05-06T18:30:31Z","lastAt":"2022-07-25T20:56:30Z","messageCount":113,"participants":["Glen Choo via GitGitGadget","Junio C Hamano","Taylor Blau","Glen Choo","Derrick Stolee","Jonathan Tan","Ævar Arnfjörð Bjarmason"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"454935","messageId":"pull.1261.git.git.1651861810633.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":null,"subject":"[PATCH] [RFC] setup.c: make bare repo discovery optional","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-05-06T18:30:10Z","receivedAt":"2022-05-06T18:30:31Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nAdd a config variable, `safe.barerepository`, that tells Git whether or\nnot to recognize bare repositories when it is trying to discover the\nrepository. This only affects repository discovery, thus it has no\neffect if discovery was not done (e.g. `--git-dir` was passed).\n\nThis is motivated by the fact that some workflows don't use bare\nrepositories at all, and users may prefer to opt out of bare repository\ndiscovery altogether:\n\n- An easy assumption for a user to make is that Git commands run\n  anywhere inside a repository's working tree will use the same\n  repository. However, if the working tree contains a bare repository\n  below the root-level (\".git\" is preferred at the root-level), any\n  operations inside that bare repository use the bare repository\n  instead.\n\n  In the worst case, attackers can use this confusion to trick users\n  into running arbitrary code (see [1] for a deeper discussion). But\n  even in benign situations (e.g. a user renames \".git/\" to \".git.old/\"\n  and commits it for archival purposes), disabling bare repository\n  discovery can be a simpler mode of operation (e.g. because the user\n  doesn't actually want to use \".git.old/\") [2].\n\n- Git won't \"accidentally\" recognize a directory that wasn't meant to be\n  a bare repository, but happens to resemble one. While such accidents\n  are probably very rare in practice, this lets users reduce the chance\n  to zero.\n\nThis config is designed to be used like an allow-list, but it is not yet\nclear what a good format for this allow-list would be. As such, this\npatch limits the config value to a tri-state of [true|false|unset]:\n\n- [*|(unset)] recognize all bare repositories (like Git does today)\n- (empty) recognize no bare repositories\n\nand leaves the full format to be determined later.\n\n[1]: https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n[2]: I don't personally know anyone who does this as part of their\nnormal workflow, but a cursory search on GitHub suggests that there is a\nnot insubstantial number of people who munge \".git\" in order to store\nits contents.\n\nhttps://github.com/search?l=&o=desc&p=1&q=ref+size%3A%3C1000+filename%3AHEAD&s=indexed&type=Code\n(aka search for the text \"ref\", size:<1000, filename:HEAD)\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n    RFC setup.c: make bare repo discovery optional\n    \n    (Forgive the non-standard RFC tag, I haven't figured out how to send as\n    RFC using GGG. I also didn't realize that /preview would also respect\n    CC...)\n    \n    = Description\n    \n    A relatively easy win that came out of the discussions around embedded\n    bare repos [1], is to just let users opt-out of discovering bare repos.\n    This patch does exactly that, by adding a 'boolean' config variable,\n    safe.barerepository.\n    \n    safe.barerepository is presented to users as an allow-list of\n    directories that Git will recognize as a bare repository during the\n    repository discovery process (much like safe.directory), but this patch\n    only implements (and permits) boolean behavior (i.e. on, off and unset).\n    Hopefully, this gives us some room to discuss and experiment with\n    possible formats.\n    \n    Thanks to Taylor for suggesting the allow-list idea :)\n    \n    I think the core concept of letting users toggle bare repo discovery is\n    solid, but I'm sending this as RFC for the following reasons:\n    \n     * I don't love the name safe.barerepository, because it feels like Git\n       is saying that bare repos are unsafe and consequently, that bare repo\n       users are behaving unsafely. On the other hand, this is quite similar\n       to safe.directory in a few respects, so it might make sense for the\n       naming to reflect that.\n    \n     * The *-gcc CI jobs don't pass. I haven't discerned any kind of pattern\n       yet.\n    \n    = How this relates to embedded bare repos\n    \n    This does not change the default behavior (i.e. Git will still discover\n    all bare repos by default) because that would be catastrophic for bare\n    repo users [2]. As such, this patch isn't intended to solve the problem\n    of embedded bare repos for all users once and for all, but I think it\n    does improve the our stance on the matter:\n    \n     * In the short-term, users who know they won't need bare repos (or\n       those who are willing to set GIT_DIR for all of their bare repos) can\n       opt-in to a safer, easier to reason about mode of operation.\n    \n     * In the longer-term, we might identify a usable-enough default that we\n       can give opt-out protection that works for the vast majority of\n       users.\n    \n    = Other questions/Concerns\n    \n     * Maybe it's more informative for the user if we die() (or warn()) when\n       we find a bare repo instead of silently ignoring it?\n    \n     * I wonder if it makes sense to separate the toggle for bare repo\n       discovery and the allow-list of bare repositories. Something like\n       core.barediscovery or discovery.barerepository has a lot less baggage\n       than safe.*, and boolean enable/disable is a lot simpler, but this\n       isn't good from an extensibility perspective.\n    \n     * Is there any reason why safe.barerepository shouldn't use the same\n       format as (its obvious inspiration) safe.directory?\n    \n     * Are the docs clear enough? I found those hard to put into words, so\n       I'd especially appreciate wording suggestions :)\n    \n    = Future work\n    \n     * Like safe.directory, safe.barerepository is only read from system and\n       global config. I anticipate that this is too restrictive; there has\n       already been some discussion of adding a GIT_SAFE_DIRECTORIES\n       environment variable for safe.directory [3], and it would be useful\n       to have the same thing for safe.barerepository.\n    \n    [1]\n    https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n    [2] In https://lore.kernel.org/git/xmqqh76ucdg6.fsf@gitster.g, Junio\n    experimented with switching off bare repo discovery altogether and\n    relying solely on GIT_DIR. The resulting fallout was deemed too big to\n    be feasible. [3] https://lore.kernel.org/git/xmqqee1il09v.fsf@gitster.g/\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1261%2Fchooglen%2Fsetup%2Fdisable-bare-repo-config-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1261/chooglen/setup/disable-bare-repo-config-v1\nPull-Request: https://github.com/git/git/pull/1261\n\n Documentation/config/safe.txt | 24 +++++++++++++++\n setup.c                       | 36 ++++++++++++++++++++++-\n t/t1510-repo-setup.sh         | 55 +++++++++++++++++++++++++++++++++++\n 3 files changed, 114 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/config/safe.txt b/Documentation/config/safe.txt\nindex 6d764fe0ccf..02032251ffd 100644\n--- a/Documentation/config/safe.txt\n+++ b/Documentation/config/safe.txt\n@@ -1,3 +1,27 @@\n+safe.barerepository::\n+\tThis config entry specifies directories that Git can recognize as\n+\ta bare repository when looking for the repository (aka repository\n+\tdiscovery). This has no effect if repository discovery is not\n+\tperformed e.g. the path to the repository is set via `--git-dir`\n+\t(see linkgit:git[1]).\n++\n+It is recommended that you set this value so that Git will only use the bare\n+repositories you intend it to. This prevents certain types of security and\n+non-security problems, such as:\n+\n+* `git clone`-ing a repository containing a maliciously bare repository\n+  inside it.\n+* Git recognizing a directory that isn't mean to be a bare repository,\n+  but happens to look like one.\n++\n+The currently supported values are `*` (Git recognizes all bare\n+repositories) and the empty value (Git never recognizes bare repositories).\n+Defaults to `*`.\n++\n+This config setting is only respected when specified in a system or global\n+config, not when it is specified in a repository config or via the command\n+line option `-c safe.barerepository=<path>`.\n+\n safe.directory::\n \tThese config entries specify Git-tracked directories that are\n \tconsidered safe even if they are owned by someone other than the\ndiff --git a/setup.c b/setup.c\nindex a7b36f3ffbf..9b5dd877273 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1133,6 +1133,40 @@ static int ensure_valid_ownership(const char *path)\n \treturn data.is_safe;\n }\n \n+/*\n+ * This is similar to safe_directory_data, but only supports true/false.\n+ */\n+struct safe_bare_repository_data {\n+\tint is_safe;\n+};\n+\n+static int safe_bare_repository_cb(const char *key, const char *value, void *d)\n+{\n+\tstruct safe_bare_repository_data *data = d;\n+\n+\tif (strcmp(key, \"safe.barerepository\"))\n+\t\treturn 0;\n+\n+\tif (!value || !strcmp(value, \"*\")) {\n+\t\tdata->is_safe = 1;\n+\t\treturn 0;\n+\t}\n+\tif (!*value) {\n+\t\tdata->is_safe = 0;\n+\t\treturn 0;\n+\t}\n+\treturn -1;\n+}\n+\n+static int should_detect_bare(void)\n+{\n+\tstruct safe_bare_repository_data data;\n+\n+\tread_very_early_config(safe_bare_repository_cb, &data);\n+\n+\treturn data.is_safe;\n+}\n+\n enum discovery_result {\n \tGIT_DIR_NONE = 0,\n \tGIT_DIR_EXPLICIT,\n@@ -1238,7 +1272,7 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n \t\t\treturn GIT_DIR_DISCOVERED;\n \t\t}\n \n-\t\tif (is_git_directory(dir->buf)) {\n+\t\tif (should_detect_bare() && is_git_directory(dir->buf)) {\n \t\t\tif (!ensure_valid_ownership(dir->buf))\n \t\t\t\treturn GIT_DIR_INVALID_OWNERSHIP;\n \t\t\tstrbuf_addstr(gitdir, \".\");\ndiff --git a/t/t1510-repo-setup.sh b/t/t1510-repo-setup.sh\nindex 591505a39c0..3ce8f776921 100755\n--- a/t/t1510-repo-setup.sh\n+++ b/t/t1510-repo-setup.sh\n@@ -541,6 +541,61 @@ test_expect_success '#16e: bareness preserved by --bare' '\n \t)\n '\n \n+# Test the tri-state of [(unset)|\"\"|\"*\"].\n+test_expect_success '#16f: bare repo in worktree' '\n+\ttest_when_finished \"git config --global --unset safe.barerepository\" &&\n+\tsetup_repo 16f unset \"\" unset &&\n+\n+\tgit init --bare 16f/default/bare &&\n+\tgit init --bare 16f/default/bare/bare &&\n+\ttry_case 16f/default/bare unset unset \\\n+\t\t. \"(null)\" \"$here/16f/default/bare\" \"(null)\" &&\n+\ttry_case 16f/default/bare/bare unset unset \\\n+\t\t. \"(null)\" \"$here/16f/default/bare/bare\" \"(null)\" &&\n+\n+\tgit config --global safe.barerepository \"*\" &&\n+\tgit init --bare 16f/all/bare &&\n+\tgit init --bare 16f/all/bare/bare &&\n+\ttry_case 16f/all/bare unset unset \\\n+\t\t. \"(null)\" \"$here/16f/all/bare\" \"(null)\" &&\n+\ttry_case 16f/all/bare/bare unset unset \\\n+\t\t. \"(null)\" \"$here/16f/all/bare/bare\" \"(null)\" &&\n+\n+\tgit config --global safe.barerepository \"\" &&\n+\tgit init --bare 16f/never/bare &&\n+\tgit init --bare 16f/never/bare/bare &&\n+\ttry_case 16f/never/bare unset unset \\\n+\t\t\".git\" \"$here/16f\" \"$here/16f\" \"never/bare/\" &&\n+\ttry_case 16f/never/bare/bare unset unset \\\n+\t\t\".git\" \"$here/16f\" \"$here/16f\" \"never/bare/bare/\"\n+'\n+\n+test_expect_success '#16g: inside .git with safe.barerepository' '\n+\ttest_when_finished \"git config --global --unset safe.barerepository\" &&\n+\n+\t# Omit the \"default\" case; it is covered by 16a.\n+\n+\tgit config --global safe.barerepository \"*\" &&\n+\tsetup_repo 16g/all unset \"\" unset &&\n+\tmkdir -p 16g/all/.git/wt/sub &&\n+\ttry_case 16g/all/.git unset unset \\\n+\t\t. \"(null)\" \"$here/16g/all/.git\" \"(null)\" &&\n+\ttry_case 16g/all/.git/wt unset unset \\\n+\t\t\"$here/16g/all/.git\" \"(null)\" \"$here/16g/all/.git/wt\" \"(null)\" &&\n+\ttry_case 16g/all/.git/wt/sub unset unset \\\n+\t\t\"$here/16g/all/.git\" \"(null)\" \"$here/16g/all/.git/wt/sub\" \"(null)\" &&\n+\n+\tgit config --global safe.barerepository \"\" &&\n+\tsetup_repo 16g/never unset \"\" unset &&\n+\tmkdir -p 16g/never/.git/wt/sub &&\n+\ttry_case 16g/never/.git unset unset \\\n+\t\t\".git\" \"$here/16g/never\" \"$here/16g/never\" \".git/\" &&\n+\ttry_case 16g/never/.git/wt unset unset \\\n+\t\t\".git\" \"$here/16g/never\" \"$here/16g/never\" \".git/wt/\" &&\n+\ttry_case 16g/never/.git/wt/sub unset unset \\\n+\t\t\".git\" \"$here/16g/never\" \"$here/16g/never\" \".git/wt/sub/\"\n+'\n+\n test_expect_success '#17: GIT_WORK_TREE without explicit GIT_DIR is accepted (bare case)' '\n \t# Just like #16.\n \tsetup_repo 17a unset \"\" true &&\n\nbase-commit: 0f828332d5ac36fc63b7d8202652efa152809856\n-- \ngitgitgadget\n"},{"id":"454944","messageId":"xmqqmtfujt9h.fsf@gitster.g","threadId":"57859","inReplyTo":"pull.1261.git.git.1651861810633.gitgitgadget@gmail.com","subject":"Re: [PATCH] [RFC] setup.c: make bare repo discovery optional","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-05-06T20:33:30Z","receivedAt":"2022-05-06T20:33:38Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Glen Choo <chooglen@google.com>\n>\n> Add a config variable, `safe.barerepository`, that tells Git whether or\n> not to recognize bare repositories when it is trying to discover the\n> repository. This only affects repository discovery, thus it has no\n> effect if discovery was not done (e.g. `--git-dir` was passed).\n\n> +safe.barerepository::\n> +\tThis config entry specifies directories that Git can recognize as\n> +\ta bare repository when looking for the repository (aka repository\n> +\tdiscovery). This has no effect if repository discovery is not\n> +\tperformed e.g. the path to the repository is set via `--git-dir`\n> +\t(see linkgit:git[1]).\n> ++\n> +It is recommended that you set this value so that Git will only use the bare\n> +repositories you intend it to. This prevents certain types of security and\n> +non-security problems, such as:\n> +\n> +* `git clone`-ing a repository containing a maliciously bare repository\n> +  inside it.\n\n\"maliciously bare\"? \"malicious bare\" probably.\n\n> +* Git recognizing a directory that isn't mean to be a bare repository,\n\n\"mean to be\" -> \"meant to be\".\n\n> +  but happens to look like one.\n\n> diff --git a/setup.c b/setup.c\n> index a7b36f3ffbf..9b5dd877273 100644\n> --- a/setup.c\n> +++ b/setup.c\n> @@ -1133,6 +1133,40 @@ static int ensure_valid_ownership(const char *path)\n>  \treturn data.is_safe;\n>  }\n>  \n> +/*\n> + * This is similar to safe_directory_data, but only supports true/false.\n> + */\n> +struct safe_bare_repository_data {\n> +\tint is_safe;\n> +};\n> +\n> +static int safe_bare_repository_cb(const char *key, const char *value, void *d)\n> +{\n> +\tstruct safe_bare_repository_data *data = d;\n> +\n> +\tif (strcmp(key, \"safe.barerepository\"))\n> +\t\treturn 0;\n> +\n> +\tif (!value || !strcmp(value, \"*\")) {\n> +\t\tdata->is_safe = 1;\n> +\t\treturn 0;\n> +\t}\n> +\tif (!*value) {\n> +\t\tdata->is_safe = 0;\n> +\t\treturn 0;\n> +\t}\n> +\treturn -1;\n> +}\n> +\n> +static int should_detect_bare(void)\n> +{\n> +\tstruct safe_bare_repository_data data;\n> +\n> +\tread_very_early_config(safe_bare_repository_cb, &data);\n> +\n> +\treturn data.is_safe;\n> +}\n> +\n>  enum discovery_result {\n>  \tGIT_DIR_NONE = 0,\n>  \tGIT_DIR_EXPLICIT,\n> @@ -1238,7 +1272,7 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n>  \t\t\treturn GIT_DIR_DISCOVERED;\n>  \t\t}\n>  \n> -\t\tif (is_git_directory(dir->buf)) {\n> +\t\tif (should_detect_bare() && is_git_directory(dir->buf)) {\n>  \t\t\tif (!ensure_valid_ownership(dir->buf))\n>  \t\t\t\treturn GIT_DIR_INVALID_OWNERSHIP;\n>  \t\t\tstrbuf_addstr(gitdir, \".\");\n\nThis is in a loop, which will go up and try the parent directory if\nthe body of this block is not entered, so it is calling the new\nshould_detect_bare() helper over and over if it returns false.\n\nNot a very good idea.\n\nPerhaps this would help?  I dunno.\n\nstatic int should_detect_bare(void)\n{\n\tstatic int should = -1; /* unknown yet */\n\n\tif (should < 0) {\n\t\tstruct safe_bare_repository_data data = { 0 };\n\t\tread_very_early_config(safe_bare_repository_cb, &data);\n\t\tshould = data.is_safe;\n\t}\n\treturn should;\n}\n\nIn any case, I very much appreciate the fact that this touches the\nsetup_git_directory_gently_1() codepath only minimally, as we have\nother plans to update the code further soonish.\n\nThanks.\n"},{"id":"455057","messageId":"YnmKwLoQCorBnMe2@nand.local","threadId":"57859","inReplyTo":"pull.1261.git.git.1651861810633.gitgitgadget@gmail.com","subject":"Re: [PATCH] [RFC] setup.c: make bare repo discovery optional","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2022-05-09T21:42:24Z","receivedAt":"2022-05-09T21:42:30Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"Hi Glen,\n\nOn Fri, May 06, 2022 at 06:30:10PM +0000, Glen Choo via GitGitGadget wrote:\n> From: Glen Choo <chooglen@google.com>\n>\n> Add a config variable, `safe.barerepository`, that tells Git whether or\n> not to recognize bare repositories when it is trying to discover the\n> repository. This only affects repository discovery, thus it has no\n> effect if discovery was not done (e.g. `--git-dir` was passed).\n\nThanks for working on this! I'm excited to see some patches here, though\nI'm not totally convinced of this direction. More below.\n\nTo summarize, this proposal attempts to work around the problem of\nembedding bare repositories in non-bare checkouts by providing a way to\nopt-out of bare repository discovery (which is to only discover things\nthat are listed in the safe.bareRepository configuration).\n\nI agree that this would prevent the problem you're trying to solve, but\nI have significant concerns that this patch is going too far (at the\nrisk of future damage to unrelated workflows) in order to accomplish\nthat goal.\n\nMy concern is that if we ever flipped the default (i.e. that\n\"safe.bareRepository\" might someday be \"\"), that many legitimate cases\nof using bare repositories would be broken. I think there are many such\nlegitimate use cases that _do_ rely on discovering bare repositories\n(i.e., Git invocations that do not have a `--git-dir` in their\ncommand-line). One such example would be forges, but I imagine that\nthere are many other uses we don't even know about, and I would like to\navoid breaking those if we ended up changing the default.\n\nIf it's possible to pursue a more targeted fix that leaves non-embedded\nbare repositories alone, I'd like to try and focus these efforts on a\nmore narrow fix that would address just the case of embedded bare\nrepositories. I think that the direction I outlined in:\n\n    https://lore.kernel.org/git/Ylobp7sntKeWTLDX@nand.local/\n\ncould be a good place to start (see the paragraph beginning with \"Here's\nan alternative approach\" and below for the details).\n\nOne potential problem with that approach (that this patch doesn't suffer\nfrom) is that any discovery which finds a bare repository would have to\ncontinue up to the root of the volume in order to figure out whether or\nnot that bare repository is embedded in another non-bare one. That is\nprobably a non-starter due to performance, but I think you could easily\nwork around with a top-level setting that controls whether or not you\neven _care_ about embedded bare repositories.\n\nFor example, if I set safe.bareRepository='*' in my top-level\n/etc/gitconfig, then we can avoid having to continue discovery for bare\nrepositories altogether because we know we'll allow it anyway.\n\nTo pursue a change that targets just embedded bare repositories, I think\nyou fundamentally have to do an exhaustive repository discovery in order\nto figure out whether the (bare) repository you're dealing with is\nembedded or not. So having an opt-out for users that either (a) don't\ncare or (b) can't accept the performance degradation that Emily\nmentioned as a result of doing unbounded filesystem traversal would be\nsensible.\n\nPlaying devil's advocate for a moment, though, even if we had something\nlike the proposal I outlined, flipping the top-level default from '*' to\nsome value that implies we stop working in embedded bare repositories\nwill break existing workflows. But that breakage would just be limited\nto embedded bare repositories, and not non-embedded ones. So I think on\nbalance that breakage would affect fewer real-world users, while still\nbeing just as easy to recover from.\n\n>     safe.barerepository is presented to users as an allow-list of\n>     directories that Git will recognize as a bare repository during the\n>     repository discovery process (much like safe.directory), but this patch\n>     only implements (and permits) boolean behavior (i.e. on, off and unset).\n>     Hopefully, this gives us some room to discuss and experiment with\n>     possible formats.\n>\n>     Thanks to Taylor for suggesting the allow-list idea :)\n\nI did suggest an allow-list, but not this one ;-).\n\n>     I think the core concept of letting users toggle bare repo discovery is\n>     solid, but I'm sending this as RFC for the following reasons:\n>\n>      * I don't love the name safe.barerepository, because it feels like Git\n>        is saying that bare repos are unsafe and consequently, that bare repo\n>        users are behaving unsafely. On the other hand, this is quite similar\n>        to safe.directory in a few respects, so it might make sense for the\n>        naming to reflect that.\n\nYes, the concerns I outlined above are definitely echoing this\nsentiment. Another way to say it is that this feels like too big of a\nhammer (i.e., it is targeting _all_ bare repositories, not just embedded\nones) for too small of a nail (embedded bare repositories). As you're\nprobably sick of hearing me say by now, I would strongly prefer a more\ntargeted solution (perhaps what I outlined, or perhaps something else,\nso long as it doesn't break non-embedded bare repositories if/ever we\ndecided to change the default value of safe.bareRepository).\n\n>      * The *-gcc CI jobs don't pass. I haven't discerned any kind of pattern\n>        yet.\n\nInteresting. I wouldn't expect this to be the case (since the default is\nto allow everything right now).\n\n>      * In the longer-term, we might identify a usable-enough default that we\n>        can give opt-out protection that works for the vast majority of\n>        users.\n\nPerhaps, and I think if this were the case then I would feel differently\nabout this patch. But I don't want us to paint ourselves into a corner,\neither. It would be unfortunate to, say, find ourselves in a position\nwhere the only protection against some novel embedded bare repository\nattack is to change a default that would break many existing workflows\nfor _non_-embedded bare repositories.\n\n>     = Other questions/Concerns\n>\n>      * Maybe it's more informative for the user if we die() (or warn()) when\n>        we find a bare repo instead of silently ignoring it?\n\nWe should definitely provide more feedback to the user. If I set\n`safe.bareRepository` to the empty string via a global config, and then\nexecute a Git command in a non-embedded bare repository, I get:\n\n    $ git.compile config --get --global --default='*' safe.bareRepository\n\n    $ git.compile rev-parse --absolute-git-dir\n    fatal: not a git repository (or any of the parent directories): .git\n\nwhereas on the last release of Git, I get instead:\n\n    $ git rev-parse --absolute-git-dir\n    /home/ttaylorr/repo.git\n\nI'm still not convinced that just reading repository extensions while\nignoring the rest of config and hooks is too confusing, so I'd be more\nin favor of something like:\n\n    $ git.compile rev-parse --absolute-git-dir\n    warning: ignoring repository config and hooks\n    advice: to permit bare repository discovery (which\n    advice: will read config and hooks), consider running:\n    advice:\n    advice:   $ git config --global --add safe.bareRepository /home/ttaylorr/repo.git\n    /home/ttaylorr/repo.git\n\n(though I still feel strongly that we should pursue a more targeted\napproach here).\n\nThanks,\nTaylor\n"},{"id":"455062","messageId":"xmqq8rrab9m7.fsf@gitster.g","threadId":"57859","inReplyTo":"YnmKwLoQCorBnMe2@nand.local","subject":"Re: [PATCH] [RFC] setup.c: make bare repo discovery optional","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-05-09T22:54:08Z","receivedAt":"2022-05-09T22:54:26Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Taylor Blau <me@ttaylorr.com> writes:\n\n> Thanks for working on this! I'm excited to see some patches here, though\n> I'm not totally convinced of this direction. More below.\n>\n> To summarize, this proposal attempts to work around the problem of\n> embedding bare repositories in non-bare checkouts by providing a way to\n> opt-out of bare repository discovery (which is to only discover things\n> that are listed in the safe.bareRepository configuration).\n>\n> I agree that this would prevent the problem you're trying to solve, but\n> I have significant concerns that this patch is going too far (at the\n> risk of future damage to unrelated workflows) in order to accomplish\n> that goal.\n>\n> My concern is that if we ever flipped the default (i.e. that\n> \"safe.bareRepository\" might someday be \"\"), that many legitimate cases\n> of using bare repositories would be broken. I think there are many such\n> legitimate use cases that _do_ rely on discovering bare repositories\n> (i.e., Git invocations that do not have a `--git-dir` in their\n> command-line).\n\nI think 99% of such use is to chdir into the directory with HEAD,\nrefs/ and objects/ in it and let git recognise the cwd is a git\ndirectory.  Am I mistaken, or are there tools that chdir into\nobjects/08/ and rely on setup_git_directory_gently_1() to find the\nparent directory of that 'objects' directory to be a git directory?\n\nI am wondering if another knob to help that particular use case\neasier may be sufficient.  If you are a forge operator, you'd just\nset a boolean configuration variable to say \"it is sufficient to\nchdir into a directory to use it a bare repository without exporting\nthe environment variable GIT_DIR=.\"\n\nIt is likely that end-user human users would not want to enable such\na variable, of course, but I wonder if a simple single knob would be\nsufficient to help other use cases you are worried about?\n\nWhile I wish \"extensions and nothing else\", i.e. we use \"degraded\naccess\", not \"refuse to give access at all\", were workable, I am\npessimistic that it would work well in practice.\n\nSaying \"nothing else\" is easy, but we do \"if X exists, use it\" for\nhook, and to implement \"nothing else\", you'd need to find such a\ncode and say \"even if X exists, because we are in this strange\nembedded bare thing, ignore this part of the logic\" for every X.\nWe've been casually saying \"potentially risky config\" and then\nstarted mixing \"hooks\" in the discussion, but who knows what other\nthings are used from the repository by third-party tools that we\nneed to yet add to the mix?\n\n> I'm still not convinced that just reading repository extensions while\n> ignoring the rest of config and hooks is too confusing, so I'd be more\n> in favor of something like:\n\nI do not think it would be confusing.  I am worried about it being\nerror prone.\n\n>     $ git.compile rev-parse --absolute-git-dir\n>     warning: ignoring repository config and hooks\n>     advice: to permit bare repository discovery (which\n>     advice: will read config and hooks), consider running:\n>     advice:\n>     advice:   $ git config --global --add safe.bareRepository /home/ttaylorr/repo.git\n>     /home/ttaylorr/repo.git\n\nIs the last line meant to be an output from \"rev-parse --absolute-git-dir\"?\nIOW, the warning says you are ignoring, but we are still recognising\nit as a repository?\n\nBy the way, do we need safe.bareRepository?  Shouldn't\nsafe.directory cover the same purpose?  \n\nIf a directory is on the latter, you are saying that (1) the\ndirectory is OK to use as a repository, and (2) it is so even if the\ndirectory is owned by somebody else, not you.\n\nTheoretically you can argue that there can be cases where you only\nwant (1) and not (2), but as long as you control such a directory\n(like an embedded repository in your project's checkout) yourself,\nyou do not have to worry about the \"ok even if it is owned by\nsomebody else\" part.\n\n\n\n\n"},{"id":"455066","messageId":"YnmqgdJUaKCYOT4J@nand.local","threadId":"57859","inReplyTo":"xmqq8rrab9m7.fsf@gitster.g","subject":"Re: [PATCH] [RFC] setup.c: make bare repo discovery optional","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2022-05-09T23:57:53Z","receivedAt":"2022-05-09T23:58:00Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Mon, May 09, 2022 at 03:54:08PM -0700, Junio C Hamano wrote:\n> Taylor Blau <me@ttaylorr.com> writes:\n>\n> > Thanks for working on this! I'm excited to see some patches here, though\n> > I'm not totally convinced of this direction. More below.\n> >\n> > To summarize, this proposal attempts to work around the problem of\n> > embedding bare repositories in non-bare checkouts by providing a way to\n> > opt-out of bare repository discovery (which is to only discover things\n> > that are listed in the safe.bareRepository configuration).\n> >\n> > I agree that this would prevent the problem you're trying to solve, but\n> > I have significant concerns that this patch is going too far (at the\n> > risk of future damage to unrelated workflows) in order to accomplish\n> > that goal.\n> >\n> > My concern is that if we ever flipped the default (i.e. that\n> > \"safe.bareRepository\" might someday be \"\"), that many legitimate cases\n> > of using bare repositories would be broken. I think there are many such\n> > legitimate use cases that _do_ rely on discovering bare repositories\n> > (i.e., Git invocations that do not have a `--git-dir` in their\n> > command-line).\n>\n> I think 99% of such use is to chdir into the directory with HEAD,\n> refs/ and objects/ in it and let git recognise the cwd is a git\n> directory.  Am I mistaken, or are there tools that chdir into\n> objects/08/ and rely on setup_git_directory_gently_1() to find the\n> parent directory of that 'objects' directory to be a git directory?\n\nIf you took this change, and then at some point in the future we changed\nthe default value of safe.bareRepository to \"\", wouldn't that break that\n99% of use cases you are talking about?\n\nWhen I read your \"I think 99% of such use is ...\", it makes me think\nthat this change won't disrupt bare repo discovery when we only traverse\none layer above $CWD. But this change disrupts the case where we don't\nneed to traverse at all to do discovery (i.e., when $CWD is the root of\na bare repository).\n\n> I am wondering if another knob to help that particular use case\n> easier may be sufficient.  If you are a forge operator, you'd just\n> set a boolean configuration variable to say \"it is sufficient to\n> chdir into a directory to use it a bare repository without exporting\n> the environment variable GIT_DIR=.\"\n\nYes, GitHub would almost certainly set safe.bareRepository to \"*\"\nregardless of what Git's own default would be.\n\n> It is likely that end-user human users would not want to enable such\n> a variable, of course, but I wonder if a simple single knob would be\n> sufficient to help other use cases you are worried about?\n\nI'm not sure I agree that end-users wouldn't want to touch this knob. If\nthey have embedded bare repositories that they rely on as test fixtures,\nfor example, wouldn't safe.bareRepository need to be tweaked?\n\n(On a separate but somewhat-related note, I still think that this\nsetting should be read from the repository config, too, i.e., it seems\nodd that we'd force a user to set safe.bareRepository to some deeply\nnested repository (in the embedded case) via their global config.)\n\n> While I wish \"extensions and nothing else\", i.e. we use \"degraded\n> access\", not \"refuse to give access at all\", were workable, I am\n> pessimistic that it would work well in practice.\n>\n> Saying \"nothing else\" is easy, but we do \"if X exists, use it\" for\n> hook, and to implement \"nothing else\", you'd need to find such a\n> code and say \"even if X exists, because we are in this strange\n> embedded bare thing, ignore this part of the logic\" for every X.\n> We've been casually saying \"potentially risky config\" and then\n> started mixing \"hooks\" in the discussion, but who knows what other\n> things are used from the repository by third-party tools that we\n> need to yet add to the mix?\n>\n> > I'm still not convinced that just reading repository extensions while\n> > ignoring the rest of config and hooks is too confusing, so I'd be more\n> > in favor of something like:\n>\n> I do not think it would be confusing.  I am worried about it being\n> error prone.\n\nYeah, on this and the above quoted hunk, I am fine if our behavior\neventually became \"call die()\" for when we are in an embedded bare\nrepository. But I do think this transition should be gradual, i.e., we\nshould likely emit a warning in those cases that would be broken in the\nfuture to say \"this will break, run this `git config` invocation if you\nwant it to remain working\".\n\n> >     $ git.compile rev-parse --absolute-git-dir\n> >     warning: ignoring repository config and hooks\n> >     advice: to permit bare repository discovery (which\n> >     advice: will read config and hooks), consider running:\n> >     advice:\n> >     advice:   $ git config --global --add safe.bareRepository /home/ttaylorr/repo.git\n> >     /home/ttaylorr/repo.git\n>\n> Is the last line meant to be an output from \"rev-parse --absolute-git-dir\"?\n> IOW, the warning says you are ignoring, but we are still recognising\n> it as a repository?\n\nIn this example, yes. But again, I'm not so deeply attached to the idea\nthat we *have* to run in those cases. So I would equally be OK with the\nabove s/warning/fatal and minus the last line, too (i.e., that we call\ndie(), obviously we'd have to emit the advice before calling die()).\n\n> By the way, do we need safe.bareRepository?  Shouldn't\n> safe.directory cover the same purpose?\n>\n> If a directory is on the latter, you are saying that (1) the\n> directory is OK to use as a repository, and (2) it is so even if the\n> directory is owned by somebody else, not you.\n>\n> Theoretically you can argue that there can be cases where you only\n> want (1) and not (2), but as long as you control such a directory\n> (like an embedded repository in your project's checkout) yourself,\n> you do not have to worry about the \"ok even if it is owned by\n> somebody else\" part.\n\nI'm not sure yet, but will think more about it.\n\nThanks,\nTaylor\n"},{"id":"455068","messageId":"xmqqbkw69qx8.fsf@gitster.g","threadId":"57859","inReplyTo":"YnmqgdJUaKCYOT4J@nand.local","subject":"Re: [PATCH] [RFC] setup.c: make bare repo discovery optional","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-05-10T00:23:15Z","receivedAt":"2022-05-10T00:23:27Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Taylor Blau <me@ttaylorr.com> writes:\n\n>> > My concern is that if we ever flipped the default (i.e. that\n>> > \"safe.bareRepository\" might someday be \"\"), that many legitimate cases\n>> > of using bare repositories would be broken. I think there are many such\n>> > legitimate use cases that _do_ rely on discovering bare repositories\n>> > (i.e., Git invocations that do not have a `--git-dir` in their\n>> > command-line).\n>>\n>> I think 99% of such use is to chdir into the directory with HEAD,\n>> refs/ and objects/ in it and let git recognise the cwd is a git\n>> directory.  Am I mistaken, or are there tools that chdir into\n>> objects/08/ and rely on setup_git_directory_gently_1() to find the\n>> parent directory of that 'objects' directory to be a git directory?\n>\n> If you took this change, and then at some point in the future we changed\n> the default value of safe.bareRepository to \"\", wouldn't that break that\n> 99% of use cases you are talking about?\n\nOur spawning (e.g. \"fetch\" run_command()s \"upload-pack\" in a local\nrepository, or \"fetch\" runs \"upload-pack\" over ssh connection, or\nhttp gateway runs \"upload-pack\" after learning which repository the\nrequest is fetching from) of subcommands can and should be fixed by\nexporting \"GIT_DIR=.\" when we spawn them in the target directory,\nand such a fix should be more or less trivial.  It must happen\nbefore such a switch of default happens (if it is what we plan to\ndo, that is).  Also, the trivial fix must be conveyed to third-party\ntool authors and give them time to adjust their ware.\n\nThat's part of the usual migration process, and I am not so worried\nabout it.\n\nIf some third-party tool for whatever reason wants to start from a\nrandom subdirectory in a bare repository, that is a different story.\nFixing such a third-party tool would be more involved than \"more or\nless trivial\".\n\n> When I read your \"I think 99% of such use is ...\", it makes me think\n> that this change won't disrupt bare repo discovery when we only traverse\n> one layer above $CWD. But this change disrupts the case where we don't\n> need to traverse at all to do discovery (i.e., when $CWD is the root of\n> a bare repository).\n\nBy \"this change\" you mean what Glen proposes?  I think it was\ndesigned to break the use case where you go there to signal that you\nwant to use the directory as a repository.\n\n>> I am wondering if another knob to help that particular use case\n>> easier may be sufficient.  If you are a forge operator, you'd just\n>> set a boolean configuration variable to say \"it is sufficient to\n>> chdir into a directory to use it a bare repository without exporting\n>> the environment variable GIT_DIR=.\"\n\nAnd such a boolean, without safe.bareRepository setting, should be\nsufficient to cover that 99% of such use, because it disables that\ndeliberate refusal of treating CWD as a repository without\nexplicitly saying that is what you want with \"GIT_DIR=.\".  One thing\nI wasn't sure about was if that 99% number is close to reality,\nhence my question.\n\n> Yes, GitHub would almost certainly set safe.bareRepository to \"*\"\n> regardless of what Git's own default would be.\n\nAnd with such a boolean, I am hoping that GitHub do not have to make\nsuch a wildly open setting.  Only $CWD that is the top of a repository,\nwithout allowing it to be any random subdirectory, would be allowed.\n\n> I'm not sure I agree that end-users wouldn't want to touch this knob. If\n> they have embedded bare repositories that they rely on as test fixtures,\n> for example, wouldn't safe.bareRepository need to be tweaked?\n\nBut not in the \"My $CWD is always fine\" knob, whose only reason is\nto simplify things without opening you up unnecessarily too widely\nfor hosting sites.\n"},{"id":"455122","messageId":"kl6lwnet6ob2.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"YnmKwLoQCorBnMe2@nand.local","subject":"Re: [PATCH] [RFC] setup.c: make bare repo discovery optional","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-05-10T22:00:17Z","receivedAt":"2022-05-10T22:00:36Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Hi Taylor,\n\nTaylor Blau <me@ttaylorr.com> writes:\n\n> Hi Glen,\n>\n> On Fri, May 06, 2022 at 06:30:10PM +0000, Glen Choo via GitGitGadget wrote:\n>> From: Glen Choo <chooglen@google.com>\n>>\n>> Add a config variable, `safe.barerepository`, that tells Git whether or\n>> not to recognize bare repositories when it is trying to discover the\n>> repository. This only affects repository discovery, thus it has no\n>> effect if discovery was not done (e.g. `--git-dir` was passed).\n>\n> To summarize, this proposal attempts to work around the problem of\n> embedding bare repositories in non-bare checkouts by providing a way to\n> opt-out of bare repository discovery (which is to only discover things\n> that are listed in the safe.bareRepository configuration).\n>\n> I agree that this would prevent the problem you're trying to solve, but\n> I have significant concerns that this patch is going too far (at the\n> risk of future damage to unrelated workflows) in order to accomplish\n> that goal.\n\nThanks again for the careful read. As I understand it, your concern is\nthat making bare repository discovery configurable and then flipping the\ndefault to e.g. never detecting bare repositories is too disruptive to\nfix the embedded bare repository problem. And to avoid disrupting\nnon-embedded bare repositories, you would prefer to pursue a more\ntargeted fix.\n\nIf the problem statement were limited to embedded bare repositories,\nthen I agree that this is way more than overkill, and that a targeted\nsolution would be preferable.\n\nMore generally however, the problem of embedded bare repositories seems\nto suggest that bare repository discovery doesn't serve all users well,\nand in fact, may even be a net negative for a subset of users. I'd be\ninterested in hearing your thoughts from that perspective, e.g.\n\n- Should bare repository discovery should be configurable?\n- What is a good default for bare repository discovery? (regardless of\n  how feasible changing the default is)\n\nThis is a somewhat different direction from how the conversation started\n(I hope it doesn't look like I'm shifting the goal posts), but I think\nit's a good opportunity to step back and simplify something that we\nwished we got right in the beginning.\n\nAnd even if we don't flip the default, shipping the config value still\nseems useful e.g. there's a good amount of interest in disabling bare\nrepository discovery at $DAYJOB (and I think we'll get a lot of\ninteresting results once we do).\n\n>>     safe.barerepository is presented to users as an allow-list of\n>>     directories that Git will recognize as a bare repository during the\n>>     repository discovery process (much like safe.directory), but this patch\n>>     only implements (and permits) boolean behavior (i.e. on, off and unset).\n>>     Hopefully, this gives us some room to discuss and experiment with\n>>     possible formats.\n>>\n>>     Thanks to Taylor for suggesting the allow-list idea :)\n>\n> I did suggest an allow-list, but not this one ;-).\n\nAh, yes. Oops. Sorry if it looked like I was putting words in your\nmouth.\n\nWhat I really meant was that an allow-list (untethered from any specific\npurpose) seems like a useful 'UI primitive', so thanks for bringing up\nthe option.\n\n>>     I think the core concept of letting users toggle bare repo discovery is\n>>     solid, but I'm sending this as RFC for the following reasons:\n>>\n>>      * I don't love the name safe.barerepository, because it feels like Git\n>>        is saying that bare repos are unsafe and consequently, that bare repo\n>>        users are behaving unsafely. On the other hand, this is quite similar\n>>        to safe.directory in a few respects, so it might make sense for the\n>>        naming to reflect that.\n>\n> Yes, the concerns I outlined above are definitely echoing this\n> sentiment. Another way to say it is that this feels like too big of a\n> hammer (i.e., it is targeting _all_ bare repositories, not just embedded\n> ones) for too small of a nail (embedded bare repositories). As you're\n> probably sick of hearing me say by now, I would strongly prefer a more\n> targeted solution (perhaps what I outlined, or perhaps something else,\n> so long as it doesn't break non-embedded bare repositories if/ever we\n> decided to change the default value of safe.bareRepository).\n\nOk, yeah I think safe.barerepository is a terrible way to achieve my\npurported goal of 'making bare repository discovery\nconfigurable/simpler/' - using the \"safe.\" namespace makes it impossible\nto see this as anything other than protection against dangerous, unknown\nbare repositories. I'll drop the idea of safe-listing known bare\nrepositories for now, that seems unproductive.\n\n'Optionally disable bare repository discovery' still sounds like it's on\nthe table though, but probably with a different kind of UX e.g.\n\"discovery.barerepository\" with the options:\n\n- always: always discover bare repos\n- never: never discover bare repos\n- cwd-only: only discover bare repos if they are the cwd\n- dotgit-only: only discover bare repos if they are a descendant of\n  .git/\n\n>>      * The *-gcc CI jobs don't pass. I haven't discerned any kind of pattern\n>>        yet.\n>\n> Interesting. I wouldn't expect this to be the case (since the default is\n> to allow everything right now).\n\nThis might be a false alarm - I saw similar failures on an unrelated\npatch. I think my \"master\" is just out of date :(\n"},{"id":"455277","messageId":"pull.1261.v2.git.git.1652485058.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.git.git.1651861810633.gitgitgadget@gmail.com","subject":"[PATCH v2 0/2] setup.c: make bare repo discovery optional","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-05-13T23:37:36Z","receivedAt":"2022-05-13T23:48:12Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Thanks all for the comments on v1, I've expanded this series somewhat to\naddress them, notably:\n\n * The config value is now named discovery.bare and is an enum (instead of\n   an allow-list). This hopefully moves us away from \"bare repos are unsafe\n   and need to be guarded against\" and towards \"bare repos can be made\n   optional if it serves your needs better\".\n * discovery.bare now causes git to die() instead of silently ignoring the\n   bare repo.\n * Add an option that allows a bare repo if it is the CWD, since this is\n   presumably a reasonable default for 99% of bare repo users [1].\n\n= Questions/Concerns\n\n * die()-ing is necessary if we're trying to flip the default value of\n   discovery.bare. We'd expect many bare repo users to be broken, and it's\n   more helpful to fail loudly than to silently ignore the bare repo.\n   \n   But in the long term, long after we've flipped the default and users know\n   that they need to opt into bare repo discovery, would it be a better UX\n   to just silently ignore the bare repo?\n\n= Patch organization\n\n * Patch 1 introduces discovery.bare with allowed values [always|never].\n\n * Patch 2 adds discover.bare=cwd, which is useful when users don't always\n   set GIT_DIR e.g. their workflow really depends on it, they are in the\n   midst of migration.\n\n= Series history\n\nChanges since v1:\n\n * Rename safe.barerepository to discovery.bare and make it die()\n * Move tests into t/t0034-discovery-bare.sh\n * Avoid unnecessary config reading by using a static variable\n * Add discovery.bare=cwd\n * Fix typos\n\n[1] I tried this 'cwd' setting on our test suite, with some pretty promising\nresults.\n\nhttps://github.com/chooglen/git/actions/runs/2321914777\n\nOut of the 8 failing scripts:\n\n * 6 are of the form \"make sure we 'do the right thing' inside a\n   subdirectory of a bare repo\" (which typically means .git) e.g.\n   t9903-bash-prompt.sh. We should be setting discovery.bare=always for\n   these tests, so this is a non-issue.\n * t5323-pack-redundant.sh can be rewritten to -C into the root of the bare\n   repo instead of a subdirectory.\n * t3310-notes-merge-manual-resolve.sh: not sure what the test is checking\n   in particular, but I think this can be rewritten.\n\nIOW, I don't think we have any commands that require that CWD is a\nsubdirectory of a bare repo, and we could use discovery.bare without much\nhassle.\n\nGlen Choo (2):\n  setup.c: make bare repo discovery optional\n  setup.c: learn discovery.bareRepository=cwd\n\n Documentation/config/discovery.txt | 26 +++++++++\n setup.c                            | 89 ++++++++++++++++++++++++++++--\n t/t0034-discovery-bare.sh          | 69 +++++++++++++++++++++++\n 3 files changed, 178 insertions(+), 6 deletions(-)\n create mode 100644 Documentation/config/discovery.txt\n create mode 100755 t/t0034-discovery-bare.sh\n\n\nbase-commit: e8005e4871f130c4e402ddca2032c111252f070a\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1261%2Fchooglen%2Fsetup%2Fdisable-bare-repo-config-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1261/chooglen/setup/disable-bare-repo-config-v2\nPull-Request: https://github.com/git/git/pull/1261\n\nRange-diff vs v1:\n\n 1:  3370258c4b3 ! 1:  22b10bf9da8 [RFC] setup.c: make bare repo discovery optional\n     @@ Metadata\n      Author: Glen Choo <chooglen@google.com>\n      \n       ## Commit message ##\n     -    [RFC] setup.c: make bare repo discovery optional\n     +    setup.c: make bare repo discovery optional\n      \n     -    Add a config variable, `safe.barerepository`, that tells Git whether or\n     -    not to recognize bare repositories when it is trying to discover the\n     -    repository. This only affects repository discovery, thus it has no\n     +    Add a config variable, `discovery.bare`, that tells Git whether or not\n     +    it should work with the bare repository it has discovered i.e. Git will\n     +    die() if it discovers a bare repository, but it is not allowed by\n     +    `discovery.bare`. This only affects repository discovery, thus it has no\n          effect if discovery was not done (e.g. `--git-dir` was passed).\n      \n          This is motivated by the fact that some workflows don't use bare\n     @@ Commit message\n            are probably very rare in practice, this lets users reduce the chance\n            to zero.\n      \n     -    This config is designed to be used like an allow-list, but it is not yet\n     -    clear what a good format for this allow-list would be. As such, this\n     -    patch limits the config value to a tri-state of [true|false|unset]:\n     +    This config is an enum of:\n      \n     -    - [*|(unset)] recognize all bare repositories (like Git does today)\n     -    - (empty) recognize no bare repositories\n     +    - [\"always\"|(unset)]: always recognize bare repositories (like Git does\n     +      today)\n     +    - \"never\": never recognize bare repositories\n      \n     -    and leaves the full format to be determined later.\n     +    More values are expected to be added later, and the default is expected\n     +    to change (i.e. to something other than \"always\").\n      \n          [1]: https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n          [2]: I don't personally know anyone who does this as part of their\n     @@ Commit message\n      \n          Signed-off-by: Glen Choo <chooglen@google.com>\n      \n     - ## Documentation/config/safe.txt ##\n     +    WIP setup.c: make discovery.bare die on failure\n     +\n     +    Signed-off-by: Glen Choo <chooglen@google.com>\n     +\n     + ## Documentation/config/discovery.txt (new) ##\n      @@\n     -+safe.barerepository::\n     -+\tThis config entry specifies directories that Git can recognize as\n     -+\ta bare repository when looking for the repository (aka repository\n     ++discovery.bare::\n     ++\tSpecifies what kinds of directories Git can recognize as a bare\n     ++\trepository when looking for the repository (aka repository\n      +\tdiscovery). This has no effect if repository discovery is not\n      +\tperformed e.g. the path to the repository is set via `--git-dir`\n      +\t(see linkgit:git[1]).\n      ++\n     -+It is recommended that you set this value so that Git will only use the bare\n     -+repositories you intend it to. This prevents certain types of security and\n     -+non-security problems, such as:\n     -+\n     -+* `git clone`-ing a repository containing a maliciously bare repository\n     -+  inside it.\n     -+* Git recognizing a directory that isn't mean to be a bare repository,\n     -+  but happens to look like one.\n     -++\n     -+The currently supported values are `*` (Git recognizes all bare\n     -+repositories) and the empty value (Git never recognizes bare repositories).\n     -+Defaults to `*`.\n     -++\n      +This config setting is only respected when specified in a system or global\n      +config, not when it is specified in a repository config or via the command\n     -+line option `-c safe.barerepository=<path>`.\n     ++line option `-c discovery.bare=<value>`.\n     +++\n     ++The currently supported values are `always` (Git always recognizes bare\n     ++repositories) and `never` (Git never recognizes bare repositories).\n     ++This defaults to `always`, but this default is likely to change.\n     +++\n     ++If your workflow does not rely on bare repositories, it is recommended that\n     ++you set this value to `never`. This makes repository discovery easier to\n     ++reason about and prevents certain types of security and non-security\n     ++problems, such as:\n      +\n     - safe.directory::\n     - \tThese config entries specify Git-tracked directories that are\n     - \tconsidered safe even if they are owned by someone other than the\n     ++* `git clone`-ing a repository containing a malicious bare repository\n     ++  inside it.\n     ++* Git recognizing a directory that isn't meant to be a bare repository,\n     ++  but happens to look like one.\n      \n       ## setup.c ##\n     +@@\n     + static int inside_git_dir = -1;\n     + static int inside_work_tree = -1;\n     + static int work_tree_config_is_bogus;\n     ++enum discovery_bare_config {\n     ++\tDISCOVERY_BARE_UNKNOWN = -1,\n     ++\tDISCOVERY_BARE_NEVER = 0,\n     ++\tDISCOVERY_BARE_ALWAYS,\n     ++};\n     ++static enum discovery_bare_config discovery_bare_config =\n     ++\tDISCOVERY_BARE_UNKNOWN;\n     + \n     + static struct startup_info the_startup_info;\n     + struct startup_info *startup_info = &the_startup_info;\n      @@ setup.c: static int ensure_valid_ownership(const char *path)\n       \treturn data.is_safe;\n       }\n       \n     -+/*\n     -+ * This is similar to safe_directory_data, but only supports true/false.\n     -+ */\n     -+struct safe_bare_repository_data {\n     -+\tint is_safe;\n     -+};\n     -+\n     -+static int safe_bare_repository_cb(const char *key, const char *value, void *d)\n     ++static int discovery_bare_cb(const char *key, const char *value, void *d)\n      +{\n     -+\tstruct safe_bare_repository_data *data = d;\n     -+\n     -+\tif (strcmp(key, \"safe.barerepository\"))\n     ++\tif (strcmp(key, \"discovery.bare\"))\n      +\t\treturn 0;\n      +\n     -+\tif (!value || !strcmp(value, \"*\")) {\n     -+\t\tdata->is_safe = 1;\n     ++\tif (!strcmp(value, \"never\")) {\n     ++\t\tdiscovery_bare_config = DISCOVERY_BARE_NEVER;\n      +\t\treturn 0;\n      +\t}\n     -+\tif (!*value) {\n     -+\t\tdata->is_safe = 0;\n     ++\tif (!strcmp(value, \"always\")) {\n     ++\t\tdiscovery_bare_config = DISCOVERY_BARE_ALWAYS;\n      +\t\treturn 0;\n      +\t}\n      +\treturn -1;\n      +}\n      +\n     -+static int should_detect_bare(void)\n     ++static int check_bare_repo_allowed(void)\n      +{\n     -+\tstruct safe_bare_repository_data data;\n     -+\n     -+\tread_very_early_config(safe_bare_repository_cb, &data);\n     ++\tif (discovery_bare_config == DISCOVERY_BARE_UNKNOWN) {\n     ++\t\tread_very_early_config(discovery_bare_cb, NULL);\n     ++\t\t/* We didn't find a value; use the default. */\n     ++\t\tif (discovery_bare_config == DISCOVERY_BARE_UNKNOWN)\n     ++\t\t\tdiscovery_bare_config = DISCOVERY_BARE_ALWAYS;\n     ++\t}\n     ++\tswitch (discovery_bare_config) {\n     ++\tcase DISCOVERY_BARE_NEVER:\n     ++\t\treturn 0;\n     ++\tcase DISCOVERY_BARE_ALWAYS:\n     ++\t\treturn 1;\n     ++\tdefault:\n     ++\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n     ++\t}\n     ++}\n      +\n     -+\treturn data.is_safe;\n     ++static const char *discovery_bare_config_to_string(void)\n     ++{\n     ++\tswitch (discovery_bare_config) {\n     ++\tcase DISCOVERY_BARE_NEVER:\n     ++\t\treturn \"never\";\n     ++\tcase DISCOVERY_BARE_ALWAYS:\n     ++\t\treturn \"always\";\n     ++\tdefault:\n     ++\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n     ++\t}\n      +}\n      +\n       enum discovery_result {\n       \tGIT_DIR_NONE = 0,\n       \tGIT_DIR_EXPLICIT,\n     +@@ setup.c: enum discovery_result {\n     + \tGIT_DIR_HIT_CEILING = -1,\n     + \tGIT_DIR_HIT_MOUNT_POINT = -2,\n     + \tGIT_DIR_INVALID_GITFILE = -3,\n     +-\tGIT_DIR_INVALID_OWNERSHIP = -4\n     ++\tGIT_DIR_INVALID_OWNERSHIP = -4,\n     ++\tGIT_DIR_DISALLOWED_BARE = -5\n     + };\n     + \n     + /*\n      @@ setup.c: static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n     - \t\t\treturn GIT_DIR_DISCOVERED;\n       \t\t}\n       \n     --\t\tif (is_git_directory(dir->buf)) {\n     -+\t\tif (should_detect_bare() && is_git_directory(dir->buf)) {\n     + \t\tif (is_git_directory(dir->buf)) {\n     ++\t\t\tif (!check_bare_repo_allowed())\n     ++\t\t\t\treturn GIT_DIR_DISALLOWED_BARE;\n       \t\t\tif (!ensure_valid_ownership(dir->buf))\n       \t\t\t\treturn GIT_DIR_INVALID_OWNERSHIP;\n       \t\t\tstrbuf_addstr(gitdir, \".\");\n     +@@ setup.c: const char *setup_git_directory_gently(int *nongit_ok)\n     + \t\t}\n     + \t\t*nongit_ok = 1;\n     + \t\tbreak;\n     ++\tcase GIT_DIR_DISALLOWED_BARE:\n     ++\t\tif (!nongit_ok) {\n     ++\t\t\tdie(_(\"cannot use bare repository '%s' (discovery.bare is '%s')\"),\n     ++\t\t\t    dir.buf,\n     ++\t\t\t    discovery_bare_config_to_string());\n     ++\t\t}\n     ++\t\t*nongit_ok = 1;\n     ++\t\tbreak;\n     + \tcase GIT_DIR_NONE:\n     + \t\t/*\n     + \t\t * As a safeguard against setup_git_directory_gently_1 returning\n      \n     - ## t/t1510-repo-setup.sh ##\n     -@@ t/t1510-repo-setup.sh: test_expect_success '#16e: bareness preserved by --bare' '\n     - \t)\n     - '\n     - \n     -+# Test the tri-state of [(unset)|\"\"|\"*\"].\n     -+test_expect_success '#16f: bare repo in worktree' '\n     -+\ttest_when_finished \"git config --global --unset safe.barerepository\" &&\n     -+\tsetup_repo 16f unset \"\" unset &&\n     + ## t/t0034-discovery-bare.sh (new) ##\n     +@@\n     ++#!/bin/sh\n      +\n     -+\tgit init --bare 16f/default/bare &&\n     -+\tgit init --bare 16f/default/bare/bare &&\n     -+\ttry_case 16f/default/bare unset unset \\\n     -+\t\t. \"(null)\" \"$here/16f/default/bare\" \"(null)\" &&\n     -+\ttry_case 16f/default/bare/bare unset unset \\\n     -+\t\t. \"(null)\" \"$here/16f/default/bare/bare\" \"(null)\" &&\n     ++test_description='verify discovery.bare checks'\n      +\n     -+\tgit config --global safe.barerepository \"*\" &&\n     -+\tgit init --bare 16f/all/bare &&\n     -+\tgit init --bare 16f/all/bare/bare &&\n     -+\ttry_case 16f/all/bare unset unset \\\n     -+\t\t. \"(null)\" \"$here/16f/all/bare\" \"(null)\" &&\n     -+\ttry_case 16f/all/bare/bare unset unset \\\n     -+\t\t. \"(null)\" \"$here/16f/all/bare/bare\" \"(null)\" &&\n     ++. ./test-lib.sh\n      +\n     -+\tgit config --global safe.barerepository \"\" &&\n     -+\tgit init --bare 16f/never/bare &&\n     -+\tgit init --bare 16f/never/bare/bare &&\n     -+\ttry_case 16f/never/bare unset unset \\\n     -+\t\t\".git\" \"$here/16f\" \"$here/16f\" \"never/bare/\" &&\n     -+\ttry_case 16f/never/bare/bare unset unset \\\n     -+\t\t\".git\" \"$here/16f\" \"$here/16f\" \"never/bare/bare/\"\n     -+'\n     ++pwd=\"$(pwd)\"\n      +\n     -+test_expect_success '#16g: inside .git with safe.barerepository' '\n     -+\ttest_when_finished \"git config --global --unset safe.barerepository\" &&\n     ++expect_allowed () {\n     ++\tgit rev-parse --absolute-git-dir >actual &&\n     ++\techo \"$pwd/outer-repo/bare-repo\" >expected &&\n     ++\ttest_cmp expected actual\n     ++}\n      +\n     -+\t# Omit the \"default\" case; it is covered by 16a.\n     ++expect_rejected () {\n     ++\ttest_must_fail git rev-parse --absolute-git-dir 2>err &&\n     ++\tgrep \"discovery.bare\" err\n     ++}\n      +\n     -+\tgit config --global safe.barerepository \"*\" &&\n     -+\tsetup_repo 16g/all unset \"\" unset &&\n     -+\tmkdir -p 16g/all/.git/wt/sub &&\n     -+\ttry_case 16g/all/.git unset unset \\\n     -+\t\t. \"(null)\" \"$here/16g/all/.git\" \"(null)\" &&\n     -+\ttry_case 16g/all/.git/wt unset unset \\\n     -+\t\t\"$here/16g/all/.git\" \"(null)\" \"$here/16g/all/.git/wt\" \"(null)\" &&\n     -+\ttry_case 16g/all/.git/wt/sub unset unset \\\n     -+\t\t\"$here/16g/all/.git\" \"(null)\" \"$here/16g/all/.git/wt/sub\" \"(null)\" &&\n     ++test_expect_success 'setup bare repo in worktree' '\n     ++\tgit init outer-repo &&\n     ++\tgit init --bare outer-repo/bare-repo\n     ++'\n     ++\n     ++test_expect_success 'discovery.bare unset' '\n     ++\t(\n     ++\t\tcd outer-repo/bare-repo &&\n     ++\t\texpect_allowed &&\n     ++\t\tcd refs/ &&\n     ++\t\texpect_allowed\n     ++\t)\n     ++'\n     ++\n     ++test_expect_success 'discovery.bare=always' '\n     ++\tgit config --global discovery.bare always &&\n     ++\t(\n     ++\t\tcd outer-repo/bare-repo &&\n     ++\t\texpect_allowed &&\n     ++\t\tcd refs/ &&\n     ++\t\texpect_allowed\n     ++\t)\n     ++'\n      +\n     -+\tgit config --global safe.barerepository \"\" &&\n     -+\tsetup_repo 16g/never unset \"\" unset &&\n     -+\tmkdir -p 16g/never/.git/wt/sub &&\n     -+\ttry_case 16g/never/.git unset unset \\\n     -+\t\t\".git\" \"$here/16g/never\" \"$here/16g/never\" \".git/\" &&\n     -+\ttry_case 16g/never/.git/wt unset unset \\\n     -+\t\t\".git\" \"$here/16g/never\" \"$here/16g/never\" \".git/wt/\" &&\n     -+\ttry_case 16g/never/.git/wt/sub unset unset \\\n     -+\t\t\".git\" \"$here/16g/never\" \"$here/16g/never\" \".git/wt/sub/\"\n     ++test_expect_success 'discovery.bare=never' '\n     ++\tgit config --global discovery.bare never &&\n     ++\t(\n     ++\t\tcd outer-repo/bare-repo &&\n     ++\t\texpect_rejected &&\n     ++\t\tcd refs/ &&\n     ++\t\texpect_rejected\n     ++\t) &&\n     ++\t(\n     ++\t\tGIT_DIR=outer-repo/bare-repo &&\n     ++\t\texport GIT_DIR &&\n     ++\t\texpect_allowed\n     ++\t)\n      +'\n      +\n     - test_expect_success '#17: GIT_WORK_TREE without explicit GIT_DIR is accepted (bare case)' '\n     - \t# Just like #16.\n     - \tsetup_repo 17a unset \"\" true &&\n     ++test_done\n -:  ----------- > 2:  62070aab7eb setup.c: learn discovery.bareRepository=cwd\n\n-- \ngitgitgadget\n"},{"id":"455278","messageId":"22b10bf9da8ccf4ae4da634aadfdaff5ee7a3508.1652485058.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v2.git.git.1652485058.gitgitgadget@gmail.com","subject":"[PATCH v2 1/2] setup.c: make bare repo discovery optional","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-05-13T23:37:37Z","receivedAt":"2022-05-13T23:48:14Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nAdd a config variable, `discovery.bare`, that tells Git whether or not\nit should work with the bare repository it has discovered i.e. Git will\ndie() if it discovers a bare repository, but it is not allowed by\n`discovery.bare`. This only affects repository discovery, thus it has no\neffect if discovery was not done (e.g. `--git-dir` was passed).\n\nThis is motivated by the fact that some workflows don't use bare\nrepositories at all, and users may prefer to opt out of bare repository\ndiscovery altogether:\n\n- An easy assumption for a user to make is that Git commands run\n  anywhere inside a repository's working tree will use the same\n  repository. However, if the working tree contains a bare repository\n  below the root-level (\".git\" is preferred at the root-level), any\n  operations inside that bare repository use the bare repository\n  instead.\n\n  In the worst case, attackers can use this confusion to trick users\n  into running arbitrary code (see [1] for a deeper discussion). But\n  even in benign situations (e.g. a user renames \".git/\" to \".git.old/\"\n  and commits it for archival purposes), disabling bare repository\n  discovery can be a simpler mode of operation (e.g. because the user\n  doesn't actually want to use \".git.old/\") [2].\n\n- Git won't \"accidentally\" recognize a directory that wasn't meant to be\n  a bare repository, but happens to resemble one. While such accidents\n  are probably very rare in practice, this lets users reduce the chance\n  to zero.\n\nThis config is an enum of:\n\n- [\"always\"|(unset)]: always recognize bare repositories (like Git does\n  today)\n- \"never\": never recognize bare repositories\n\nMore values are expected to be added later, and the default is expected\nto change (i.e. to something other than \"always\").\n\n[1]: https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n[2]: I don't personally know anyone who does this as part of their\nnormal workflow, but a cursory search on GitHub suggests that there is a\nnot insubstantial number of people who munge \".git\" in order to store\nits contents.\n\nhttps://github.com/search?l=&o=desc&p=1&q=ref+size%3A%3C1000+filename%3AHEAD&s=indexed&type=Code\n(aka search for the text \"ref\", size:<1000, filename:HEAD)\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n\nWIP setup.c: make discovery.bare die on failure\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config/discovery.txt | 24 +++++++++++\n setup.c                            | 66 +++++++++++++++++++++++++++++-\n t/t0034-discovery-bare.sh          | 59 ++++++++++++++++++++++++++\n 3 files changed, 148 insertions(+), 1 deletion(-)\n create mode 100644 Documentation/config/discovery.txt\n create mode 100755 t/t0034-discovery-bare.sh\n\ndiff --git a/Documentation/config/discovery.txt b/Documentation/config/discovery.txt\nnew file mode 100644\nindex 00000000000..761cabe6e70\n--- /dev/null\n+++ b/Documentation/config/discovery.txt\n@@ -0,0 +1,24 @@\n+discovery.bare::\n+\tSpecifies what kinds of directories Git can recognize as a bare\n+\trepository when looking for the repository (aka repository\n+\tdiscovery). This has no effect if repository discovery is not\n+\tperformed e.g. the path to the repository is set via `--git-dir`\n+\t(see linkgit:git[1]).\n++\n+This config setting is only respected when specified in a system or global\n+config, not when it is specified in a repository config or via the command\n+line option `-c discovery.bare=<value>`.\n++\n+The currently supported values are `always` (Git always recognizes bare\n+repositories) and `never` (Git never recognizes bare repositories).\n+This defaults to `always`, but this default is likely to change.\n++\n+If your workflow does not rely on bare repositories, it is recommended that\n+you set this value to `never`. This makes repository discovery easier to\n+reason about and prevents certain types of security and non-security\n+problems, such as:\n+\n+* `git clone`-ing a repository containing a malicious bare repository\n+  inside it.\n+* Git recognizing a directory that isn't meant to be a bare repository,\n+  but happens to look like one.\ndiff --git a/setup.c b/setup.c\nindex a7b36f3ffbf..cee01d86f0c 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -10,6 +10,13 @@\n static int inside_git_dir = -1;\n static int inside_work_tree = -1;\n static int work_tree_config_is_bogus;\n+enum discovery_bare_config {\n+\tDISCOVERY_BARE_UNKNOWN = -1,\n+\tDISCOVERY_BARE_NEVER = 0,\n+\tDISCOVERY_BARE_ALWAYS,\n+};\n+static enum discovery_bare_config discovery_bare_config =\n+\tDISCOVERY_BARE_UNKNOWN;\n \n static struct startup_info the_startup_info;\n struct startup_info *startup_info = &the_startup_info;\n@@ -1133,6 +1140,52 @@ static int ensure_valid_ownership(const char *path)\n \treturn data.is_safe;\n }\n \n+static int discovery_bare_cb(const char *key, const char *value, void *d)\n+{\n+\tif (strcmp(key, \"discovery.bare\"))\n+\t\treturn 0;\n+\n+\tif (!strcmp(value, \"never\")) {\n+\t\tdiscovery_bare_config = DISCOVERY_BARE_NEVER;\n+\t\treturn 0;\n+\t}\n+\tif (!strcmp(value, \"always\")) {\n+\t\tdiscovery_bare_config = DISCOVERY_BARE_ALWAYS;\n+\t\treturn 0;\n+\t}\n+\treturn -1;\n+}\n+\n+static int check_bare_repo_allowed(void)\n+{\n+\tif (discovery_bare_config == DISCOVERY_BARE_UNKNOWN) {\n+\t\tread_very_early_config(discovery_bare_cb, NULL);\n+\t\t/* We didn't find a value; use the default. */\n+\t\tif (discovery_bare_config == DISCOVERY_BARE_UNKNOWN)\n+\t\t\tdiscovery_bare_config = DISCOVERY_BARE_ALWAYS;\n+\t}\n+\tswitch (discovery_bare_config) {\n+\tcase DISCOVERY_BARE_NEVER:\n+\t\treturn 0;\n+\tcase DISCOVERY_BARE_ALWAYS:\n+\t\treturn 1;\n+\tdefault:\n+\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n+\t}\n+}\n+\n+static const char *discovery_bare_config_to_string(void)\n+{\n+\tswitch (discovery_bare_config) {\n+\tcase DISCOVERY_BARE_NEVER:\n+\t\treturn \"never\";\n+\tcase DISCOVERY_BARE_ALWAYS:\n+\t\treturn \"always\";\n+\tdefault:\n+\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n+\t}\n+}\n+\n enum discovery_result {\n \tGIT_DIR_NONE = 0,\n \tGIT_DIR_EXPLICIT,\n@@ -1142,7 +1195,8 @@ enum discovery_result {\n \tGIT_DIR_HIT_CEILING = -1,\n \tGIT_DIR_HIT_MOUNT_POINT = -2,\n \tGIT_DIR_INVALID_GITFILE = -3,\n-\tGIT_DIR_INVALID_OWNERSHIP = -4\n+\tGIT_DIR_INVALID_OWNERSHIP = -4,\n+\tGIT_DIR_DISALLOWED_BARE = -5\n };\n \n /*\n@@ -1239,6 +1293,8 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n \t\t}\n \n \t\tif (is_git_directory(dir->buf)) {\n+\t\t\tif (!check_bare_repo_allowed())\n+\t\t\t\treturn GIT_DIR_DISALLOWED_BARE;\n \t\t\tif (!ensure_valid_ownership(dir->buf))\n \t\t\t\treturn GIT_DIR_INVALID_OWNERSHIP;\n \t\t\tstrbuf_addstr(gitdir, \".\");\n@@ -1385,6 +1441,14 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \t\t}\n \t\t*nongit_ok = 1;\n \t\tbreak;\n+\tcase GIT_DIR_DISALLOWED_BARE:\n+\t\tif (!nongit_ok) {\n+\t\t\tdie(_(\"cannot use bare repository '%s' (discovery.bare is '%s')\"),\n+\t\t\t    dir.buf,\n+\t\t\t    discovery_bare_config_to_string());\n+\t\t}\n+\t\t*nongit_ok = 1;\n+\t\tbreak;\n \tcase GIT_DIR_NONE:\n \t\t/*\n \t\t * As a safeguard against setup_git_directory_gently_1 returning\ndiff --git a/t/t0034-discovery-bare.sh b/t/t0034-discovery-bare.sh\nnew file mode 100755\nindex 00000000000..9c774872c4e\n--- /dev/null\n+++ b/t/t0034-discovery-bare.sh\n@@ -0,0 +1,59 @@\n+#!/bin/sh\n+\n+test_description='verify discovery.bare checks'\n+\n+. ./test-lib.sh\n+\n+pwd=\"$(pwd)\"\n+\n+expect_allowed () {\n+\tgit rev-parse --absolute-git-dir >actual &&\n+\techo \"$pwd/outer-repo/bare-repo\" >expected &&\n+\ttest_cmp expected actual\n+}\n+\n+expect_rejected () {\n+\ttest_must_fail git rev-parse --absolute-git-dir 2>err &&\n+\tgrep \"discovery.bare\" err\n+}\n+\n+test_expect_success 'setup bare repo in worktree' '\n+\tgit init outer-repo &&\n+\tgit init --bare outer-repo/bare-repo\n+'\n+\n+test_expect_success 'discovery.bare unset' '\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\texpect_allowed &&\n+\t\tcd refs/ &&\n+\t\texpect_allowed\n+\t)\n+'\n+\n+test_expect_success 'discovery.bare=always' '\n+\tgit config --global discovery.bare always &&\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\texpect_allowed &&\n+\t\tcd refs/ &&\n+\t\texpect_allowed\n+\t)\n+'\n+\n+test_expect_success 'discovery.bare=never' '\n+\tgit config --global discovery.bare never &&\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\texpect_rejected &&\n+\t\tcd refs/ &&\n+\t\texpect_rejected\n+\t) &&\n+\t(\n+\t\tGIT_DIR=outer-repo/bare-repo &&\n+\t\texport GIT_DIR &&\n+\t\texpect_allowed\n+\t)\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"455279","messageId":"62070aab7eb8734f82e3881e25ff0f9cee6bb077.1652485058.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v2.git.git.1652485058.gitgitgadget@gmail.com","subject":"[PATCH v2 2/2] setup.c: learn discovery.bareRepository=cwd","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-05-13T23:37:38Z","receivedAt":"2022-05-13T23:48:29Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nAdd a 'cwd' option to discovery.bareRepository, which allows a bare\nrepository to be used if and only if the cwd is the root of a bare\nrepository. This covers the common case where a user works with a bare\nrepository by cd-ing into the repository's root.\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config/discovery.txt |  6 ++++--\n setup.c                            | 27 ++++++++++++++++++++-------\n t/t0034-discovery-bare.sh          | 10 ++++++++++\n 3 files changed, 34 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/config/discovery.txt b/Documentation/config/discovery.txt\nindex 761cabe6e70..d7cdee3a5e1 100644\n--- a/Documentation/config/discovery.txt\n+++ b/Documentation/config/discovery.txt\n@@ -10,8 +10,10 @@ config, not when it is specified in a repository config or via the command\n line option `-c discovery.bare=<value>`.\n +\n The currently supported values are `always` (Git always recognizes bare\n-repositories) and `never` (Git never recognizes bare repositories).\n-This defaults to `always`, but this default is likely to change.\n+repositories), `cwd` (Git only recognizes bare repositories if they are the\n+current working directory) and `never` (Git never recognizes bare\n+repositories). This defaults to `always`, but this default is likely to\n+change.\n +\n If your workflow does not rely on bare repositories, it is recommended that\n you set this value to `never`. This makes repository discovery easier to\ndiff --git a/setup.c b/setup.c\nindex cee01d86f0c..ead999f404c 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -14,6 +14,7 @@ enum discovery_bare_config {\n \tDISCOVERY_BARE_UNKNOWN = -1,\n \tDISCOVERY_BARE_NEVER = 0,\n \tDISCOVERY_BARE_ALWAYS,\n+\tDISCOVERY_BARE_CWD,\n };\n static enum discovery_bare_config discovery_bare_config =\n \tDISCOVERY_BARE_UNKNOWN;\n@@ -1153,10 +1154,14 @@ static int discovery_bare_cb(const char *key, const char *value, void *d)\n \t\tdiscovery_bare_config = DISCOVERY_BARE_ALWAYS;\n \t\treturn 0;\n \t}\n+\tif (!strcmp(value, \"cwd\")) {\n+\t\tdiscovery_bare_config = DISCOVERY_BARE_CWD;\n+\t\treturn 0;\n+\t}\n \treturn -1;\n }\n \n-static int check_bare_repo_allowed(void)\n+static int check_bare_repo_allowed(const char *cwd, const char *path)\n {\n \tif (discovery_bare_config == DISCOVERY_BARE_UNKNOWN) {\n \t\tread_very_early_config(discovery_bare_cb, NULL);\n@@ -1169,6 +1174,8 @@ static int check_bare_repo_allowed(void)\n \t\treturn 0;\n \tcase DISCOVERY_BARE_ALWAYS:\n \t\treturn 1;\n+\tcase DISCOVERY_BARE_CWD:\n+\t\treturn !strcmp(cwd, path);\n \tdefault:\n \t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n \t}\n@@ -1181,6 +1188,8 @@ static const char *discovery_bare_config_to_string(void)\n \t\treturn \"never\";\n \tcase DISCOVERY_BARE_ALWAYS:\n \t\treturn \"always\";\n+\tcase DISCOVERY_BARE_CWD:\n+\t\treturn \"cwd\";\n \tdefault:\n \t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n \t}\n@@ -1212,7 +1221,8 @@ enum discovery_result {\n  * the discovered .git/ directory, if any. If `gitdir` is not absolute, it\n  * is relative to `dir` (i.e. *not* necessarily the cwd).\n  */\n-static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n+static enum discovery_result setup_git_directory_gently_1(struct strbuf *cwd,\n+\t\t\t\t\t\t\t  struct strbuf *dir,\n \t\t\t\t\t\t\t  struct strbuf *gitdir,\n \t\t\t\t\t\t\t  int die_on_error)\n {\n@@ -1293,7 +1303,7 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n \t\t}\n \n \t\tif (is_git_directory(dir->buf)) {\n-\t\t\tif (!check_bare_repo_allowed())\n+\t\t\tif (!check_bare_repo_allowed(cwd->buf, dir->buf))\n \t\t\t\treturn GIT_DIR_DISALLOWED_BARE;\n \t\t\tif (!ensure_valid_ownership(dir->buf))\n \t\t\t\treturn GIT_DIR_INVALID_OWNERSHIP;\n@@ -1319,16 +1329,18 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n int discover_git_directory(struct strbuf *commondir,\n \t\t\t   struct strbuf *gitdir)\n {\n-\tstruct strbuf dir = STRBUF_INIT, err = STRBUF_INIT;\n+\tstruct strbuf cwd = STRBUF_INIT, dir = STRBUF_INIT, err = STRBUF_INIT;\n \tsize_t gitdir_offset = gitdir->len, cwd_len;\n \tsize_t commondir_offset = commondir->len;\n \tstruct repository_format candidate = REPOSITORY_FORMAT_INIT;\n \n-\tif (strbuf_getcwd(&dir))\n+\tif (strbuf_getcwd(&cwd))\n \t\treturn -1;\n+\tstrbuf_addbuf(&dir, &cwd);\n \n \tcwd_len = dir.len;\n-\tif (setup_git_directory_gently_1(&dir, gitdir, 0) <= 0) {\n+\tif (setup_git_directory_gently_1(&cwd, &dir, gitdir, 0) <= 0) {\n+\t\tstrbuf_release(&cwd);\n \t\tstrbuf_release(&dir);\n \t\treturn -1;\n \t}\n@@ -1351,6 +1363,7 @@ int discover_git_directory(struct strbuf *commondir,\n \tstrbuf_reset(&dir);\n \tstrbuf_addf(&dir, \"%s/config\", commondir->buf + commondir_offset);\n \tread_repository_format(&candidate, dir.buf);\n+\tstrbuf_release(&cwd);\n \tstrbuf_release(&dir);\n \n \tif (verify_repository_format(&candidate, &err) < 0) {\n@@ -1400,7 +1413,7 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \t\tdie_errno(_(\"Unable to read current working directory\"));\n \tstrbuf_addbuf(&dir, &cwd);\n \n-\tswitch (setup_git_directory_gently_1(&dir, &gitdir, 1)) {\n+\tswitch (setup_git_directory_gently_1(&cwd, &dir, &gitdir, 1)) {\n \tcase GIT_DIR_EXPLICIT:\n \t\tprefix = setup_explicit_git_dir(gitdir.buf, &cwd, &repo_fmt, nongit_ok);\n \t\tbreak;\ndiff --git a/t/t0034-discovery-bare.sh b/t/t0034-discovery-bare.sh\nindex 9c774872c4e..ba44cf19c99 100755\n--- a/t/t0034-discovery-bare.sh\n+++ b/t/t0034-discovery-bare.sh\n@@ -56,4 +56,14 @@ test_expect_success 'discovery.bare=never' '\n \t)\n '\n \n+test_expect_success 'discovery.bare=cwd' '\n+\tgit config --global discovery.bare cwd &&\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\texpect_allowed &&\n+\t\tcd refs/ &&\n+\t\texpect_rejected\n+\t)\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"455328","messageId":"xmqqy1z12zxr.fsf@gitster.g","threadId":"57859","inReplyTo":"pull.1261.v2.git.git.1652485058.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 0/2] setup.c: make bare repo discovery optional","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-05-16T16:40:48Z","receivedAt":"2022-05-16T16:41:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>  * die()-ing is necessary if we're trying to flip the default value of\n>    discovery.bare. We'd expect many bare repo users to be broken, and it's\n>    more helpful to fail loudly than to silently ignore the bare repo.\n>\n>    But in the long term, long after we've flipped the default and users know\n>    that they need to opt into bare repo discovery, would it be a better UX\n>    to just silently ignore the bare repo?\n\nWould a middle-ground of giving a warning() message help?  Can it be\nloud and annoying enough to knudge the users to adjust without\nbreaking the functionality?\n\nThe longer-term default should be \"cwd is allowed, but we do not\nbother going up from object/04 subdirectory of a bare repository\",\nnot \"bare repositories should not be usable at all without GIT_DIR\".\n\n>      +    Add a config variable, `discovery.bare`, that tells Git whether or not\n>      +    it should work with the bare repository it has discovered i.e. Git will\n>      +    die() if it discovers a bare repository, but it is not allowed by\n\nMissing comma before \"i.e.\"\n\n>      ++discovery.bare::\n>      ++\tSpecifies what kinds of directories Git can recognize as a bare\n>      ++\trepository when looking for the repository (aka repository\n>       +\tdiscovery). This has no effect if repository discovery is not\n>       +\tperformed e.g. the path to the repository is set via `--git-dir`\n>       +\t(see linkgit:git[1]).\n>       ++\n>       +This config setting is only respected when specified in a system or global\n>       +config, not when it is specified in a repository config or via the command\n>      ++line option `-c discovery.bare=<value>`.\n\n;-)\n\n>      +++\n>      ++The currently supported values are `always` (Git always recognizes bare\n>      ++repositories) and `never` (Git never recognizes bare repositories).\n>      ++This defaults to `always`, but this default is likely to change.\n>      +++\n>      ++If your workflow does not rely on bare repositories, it is recommended that\n>      ++you set this value to `never`. This makes repository discovery easier to\n>      ++reason about and prevents certain types of security and non-security\n>      ++problems, such as:\n\nHopefully \"git fetch\" over ssh:// and file:/// would run the other\nside with GIT_DIR explicitly set?  As long as this recommendation\ndoes not break these use cases, I think we are OK, but I do not yet\nfind these \"problems, such as...\" so convincing.\n"},{"id":"455329","messageId":"xmqqtu9p2zta.fsf@gitster.g","threadId":"57859","inReplyTo":"pull.1261.v2.git.git.1652485058.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 0/2] setup.c: make bare repo discovery optional","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-05-16T16:43:29Z","receivedAt":"2022-05-16T16:43:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>  t/t0034-discovery-bare.sh          | 69 +++++++++++++++++++++++\n\nThis number is already in use by an in-flight topic, if I am not\nmistaken.  Please make it a habit to always check your topic works\nwell when merged to 'next' and to 'seen'.\n\nThanks.\n\n"},{"id":"455341","messageId":"kl6l1qwtjqhq.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"22b10bf9da8ccf4ae4da634aadfdaff5ee7a3508.1652485058.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 1/2] setup.c: make bare repo discovery optional","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-05-16T18:12:49Z","receivedAt":"2022-05-16T18:13:12Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"\"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Glen Choo <chooglen@google.com>\n>\n> Add a config variable, `discovery.bare`, that tells Git whether or not\n> it should work with the bare repository it has discovered i.e. Git will\n> die() if it discovers a bare repository, but it is not allowed by\n> `discovery.bare`. This only affects repository discovery, thus it has no\n> effect if discovery was not done (e.g. `--git-dir` was passed).\n>\n> This is motivated by the fact that some workflows don't use bare\n> repositories at all, and users may prefer to opt out of bare repository\n> discovery altogether:\n>\n> - An easy assumption for a user to make is that Git commands run\n>   anywhere inside a repository's working tree will use the same\n>   repository. However, if the working tree contains a bare repository\n>   below the root-level (\".git\" is preferred at the root-level), any\n>   operations inside that bare repository use the bare repository\n>   instead.\n>\n>   In the worst case, attackers can use this confusion to trick users\n>   into running arbitrary code (see [1] for a deeper discussion). But\n>   even in benign situations (e.g. a user renames \".git/\" to \".git.old/\"\n>   and commits it for archival purposes), disabling bare repository\n>   discovery can be a simpler mode of operation (e.g. because the user\n>   doesn't actually want to use \".git.old/\") [2].\n>\n> - Git won't \"accidentally\" recognize a directory that wasn't meant to be\n>   a bare repository, but happens to resemble one. While such accidents\n>   are probably very rare in practice, this lets users reduce the chance\n>   to zero.\n>\n> This config is an enum of:\n>\n> - [\"always\"|(unset)]: always recognize bare repositories (like Git does\n>   today)\n> - \"never\": never recognize bare repositories\n>\n> More values are expected to be added later, and the default is expected\n> to change (i.e. to something other than \"always\").\n>\n> [1]: https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n> [2]: I don't personally know anyone who does this as part of their\n> normal workflow, but a cursory search on GitHub suggests that there is a\n> not insubstantial number of people who munge \".git\" in order to store\n> its contents.\n>\n> https://github.com/search?l=&o=desc&p=1&q=ref+size%3A%3C1000+filename%3AHEAD&s=indexed&type=Code\n> (aka search for the text \"ref\", size:<1000, filename:HEAD)\n>\n> Signed-off-by: Glen Choo <chooglen@google.com>\n\nThe intended commit message ends here...\n\n> WIP setup.c: make discovery.bare die on failure\n>\n> Signed-off-by: Glen Choo <chooglen@google.com>\n\nUgh, dumb mistake (bad squash). Fortunately this was one of my more\nprofessional-sounding WIP commit messages.\n"},{"id":"455343","messageId":"kl6ly1z1iati.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"xmqqy1z12zxr.fsf@gitster.g","subject":"Re: [PATCH v2 0/2] setup.c: make bare repo discovery optional","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-05-16T18:36:41Z","receivedAt":"2022-05-16T18:36:48Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> \"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>>  * die()-ing is necessary if we're trying to flip the default value of\n>>    discovery.bare. We'd expect many bare repo users to be broken, and it's\n>>    more helpful to fail loudly than to silently ignore the bare repo.\n>>\n>>    But in the long term, long after we've flipped the default and users know\n>>    that they need to opt into bare repo discovery, would it be a better UX\n>>    to just silently ignore the bare repo?\n>\n> Would a middle-ground of giving a warning() message help?  Can it be\n> loud and annoying enough to knudge the users to adjust without\n> breaking the functionality?\n\nPersonally, when my tool changes its behavior, I would strongly prefer\nit to die than to \"change behavior + warn\". I'd feel more comfortable\nknowing that the tool did nothing as opposed to doing the wrong thing\nand only being informed after the fact. Also, I sometimes ignore\nwarnings ;)\n\nWhen we _do_ transition away from die(), ignore + warning() sounds like\na good first step.\n\nBut if any of this flies in the face of the project's conventions, let\nme know as such.\n\n>>      +    Add a config variable, `discovery.bare`, that tells Git whether or not\n>>      +    it should work with the bare repository it has discovered i.e. Git will\n>>      +    die() if it discovers a bare repository, but it is not allowed by\n>\n> Missing comma before \"i.e.\"\n\nThanks.\n\n>>      +++\n>>      ++The currently supported values are `always` (Git always recognizes bare\n>>      ++repositories) and `never` (Git never recognizes bare repositories).\n>>      ++This defaults to `always`, but this default is likely to change.\n>>      +++\n>>      ++If your workflow does not rely on bare repositories, it is recommended that\n>>      ++you set this value to `never`. This makes repository discovery easier to\n>>      ++reason about and prevents certain types of security and non-security\n>>      ++problems, such as:\n>\n> Hopefully \"git fetch\" over ssh:// and file:/// would run the other\n> side with GIT_DIR explicitly set?\n\nAh, I'll check this and get back to you.\n\n>                                                        I do not yet\n> find these \"problems, such as...\" so convincing.\n\nWhat would be a convincing rationale to you? I'll capture that here.\n\nI'm assuming that you already have such an rationale in mind when you\nsay that the longer-term default is that \"we respect bare repositories\nonly if they are the cwd.\". I'm also assuming that this rationale is\nsomething other than embedded bare repos, because \"cwd-only\" does not\nprotect against that.\n\nPerhaps \"never\" sounds better to folks who don't ever expect bare\nrepositories and want to lock down the environment. Randall (cc-ed)\nsuggests one such use case in [1].\n\n(To Randall: Oops, I actually meant to cc you earlier, since you were\nthe first to suggest a practical use case for never allowing bare repos.\nIt must've slipped my mind).\n\n[1] https://lore.kernel.org/git/005d01d84ad0$782e8fc0$688baf40$@nexbridge.com.\n"},{"id":"455344","messageId":"e5139922-9b74-ebfa-756c-629918e0456b@github.com","threadId":"57859","inReplyTo":"22b10bf9da8ccf4ae4da634aadfdaff5ee7a3508.1652485058.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 1/2] setup.c: make bare repo discovery optional","fromName":"Derrick Stolee","fromEmail":"derrickstolee@github.com","sentAt":"2022-05-16T18:46:55Z","receivedAt":"2022-05-16T18:47:01Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 5/13/2022 7:37 PM, Glen Choo via GitGitGadget wrote:\n> From: Glen Choo <chooglen@google.com>\n> \n> Add a config variable, `discovery.bare`, that tells Git whether or not\n> it should work with the bare repository it has discovered i.e. Git will\n> die() if it discovers a bare repository, but it is not allowed by\n> `discovery.bare`. This only affects repository discovery, thus it has no\n> effect if discovery was not done (e.g. `--git-dir` was passed).\n\n> This config is an enum of:\n> \n> - [\"always\"|(unset)]: always recognize bare repositories (like Git does\n>   today)\n> - \"never\": never recognize bare repositories\n> \n> More values are expected to be added later, and the default is expected\n> to change (i.e. to something other than \"always\").\n\nI think it is fine to include the \"never\" option for users to opt-in to\nthis super-protected state, but I want to make it very clear that we\nshould never move to it as a new default. This phrasing of 'something\nother than \"always\"' is key, but it might be good to point out that\n\"never\" is very unlikely to be that default.\n\n> WIP setup.c: make discovery.bare die on failure\n> \n> Signed-off-by: Glen Choo <chooglen@google.com>\n\nAccidental concatenation of squashed commit?\n\n> diff --git a/Documentation/config/discovery.txt b/Documentation/config/discovery.txt\n> new file mode 100644\n> index 00000000000..761cabe6e70\n> --- /dev/null\n> +++ b/Documentation/config/discovery.txt\n> @@ -0,0 +1,24 @@\n> +discovery.bare::\n> +\tSpecifies what kinds of directories Git can recognize as a bare\n> +\trepository when looking for the repository (aka repository\n> +\tdiscovery). This has no effect if repository discovery is not\n> +\tperformed e.g. the path to the repository is set via `--git-dir`\n> +\t(see linkgit:git[1]).\n\nAvoid \"e.g.\" here.\n\n\tThis has no effect if the repository is specified directly via\n\tthe --git-dir command-line option or the GIT_DIR environment\n\tvariable.\n\n> +This config setting is only respected when specified in a system or global\n> +config, not when it is specified in a repository config or via the command\n> +line option `-c discovery.bare=<value>`.\n\nWe are sprinkling config options that have these same restrictions throughout\nthe config documentation. It might be time to define a term like \"protected\nconfig\" at the top of git-config.txt and then refer to that from these other\nlocations.\n\n> +The currently supported values are `always` (Git always recognizes bare\n> +repositories) and `never` (Git never recognizes bare repositories).\n\nThis sentence structure is likely to change in the future, and as it stands\nwill become complicated. A bulleted list will have easier edits in the future.\n\n> +This defaults to `always`, but this default is likely to change.\n\nFor now, I would say \"but this default may change in the future.\" instead.\n\n> +If your workflow does not rely on bare repositories, it is recommended that\n> +you set this value to `never`. This makes repository discovery easier to\n> +reason about and prevents certain types of security and non-security\n> +problems, such as:\n> +\n\n(You might need a \"+\" here.)\n\n> +* `git clone`-ing a repository containing a malicious bare repository\n> +  inside it.\n> +* Git recognizing a directory that isn't meant to be a bare repository,\n> +  but happens to look like one.\n\nI think these last bits recommending the 'never' option are a bit\ndistracting. It doesn't make repository discovery \"easier to reason\nabout\" because we still discover the bare repo and die() instead of\nskipping it and looking higher for a non-bare repository in the\nparent directories. The case of an \"accidentally-recognized bare\nrepo\" is so unlikely it is probably not worth mention in these docs.\n\nInstead, I think something like this might be better:\n\n  If you do not use bare repositories in your workflow, then it may\n  be beneficial to set `discovery.bare` to `never` in your global\n  config. This will protect you from attacks that involve cloning a\n  repository that contains a bare repository and running a Git\n  command within that directory.\n\n> +static int check_bare_repo_allowed(void)\n> +{\n> +\tif (discovery_bare_config == DISCOVERY_BARE_UNKNOWN) {\n> +\t\tread_very_early_config(discovery_bare_cb, NULL);\n\nThis will add the third place where we use read_very_early_config(),\nadding to the existing calls in tr2_sysenv_load() and\nensure_valid_ownership(). If I understand it correctly, that means\nthat every Git execution in a bare repository will now parse the\nsystem and global config three times.\n\nThis doesn't count the check for uploadpack.packobjectshook in\nupload-pack.c that uses current_config_scope() to restrict its\nvalue to the system and global config.\n\nWe are probably at the point where we need to instead create a\nconfigset that stores this \"protected config\" and allow us to\nlookup config keys directly from that configset instead of\niterating through these config files repeatedly.\n\n> +\t\t/* We didn't find a value; use the default. */\n> +\t\tif (discovery_bare_config == DISCOVERY_BARE_UNKNOWN)\n> +\t\t\tdiscovery_bare_config = DISCOVERY_BARE_ALWAYS;\n\nThis could also be done in advance of the config parsing\nby setting discovery_bare_config = DISCOVERY_BARE_ALWAYS before\ncalling read_very_early_config(). Avoids an if and a comment\nhere, which might be nice.\n\n> +\t}\n> +\tswitch (discovery_bare_config) {\n> +\tcase DISCOVERY_BARE_NEVER:\n> +\t\treturn 0;\n> +\tcase DISCOVERY_BARE_ALWAYS:\n> +\t\treturn 1;\n> +\tdefault:\n> +\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n> +\t}\n\nYou return -1 in discovery_bare_cb when the key matches, but\nthe value is not understood. Should we check the return value\nof read_very_early_config(), too?\n> +static const char *discovery_bare_config_to_string(void)\n> +{\n> +\tswitch (discovery_bare_config) {\n> +\tcase DISCOVERY_BARE_NEVER:\n> +\t\treturn \"never\";\n> +\tcase DISCOVERY_BARE_ALWAYS:\n> +\t\treturn \"always\";\n> +\tdefault:\n> +\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n\nIn general, I'm not sure these BUG() statements are helpful,\nbut they aren't hurting anything. I wonder if it would be\nbetter to use DISCOVERY_BARE_UNKNOWN instead of default,\nbecause then the compiler should notice that the switch needs\nupdating when a new enum mode is added.\n\n> @@ -1142,7 +1195,8 @@ enum discovery_result {\n>  \tGIT_DIR_HIT_CEILING = -1,\n>  \tGIT_DIR_HIT_MOUNT_POINT = -2,\n>  \tGIT_DIR_INVALID_GITFILE = -3,\n> -\tGIT_DIR_INVALID_OWNERSHIP = -4\n> +\tGIT_DIR_INVALID_OWNERSHIP = -4,\n> +\tGIT_DIR_DISALLOWED_BARE = -5\n\nI think that you can add a comma at the end of this enum to avoid the\nchanged line the next time the enum needs to be expanded.\n\n>  };\n>  \n>  /*\n> @@ -1239,6 +1293,8 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n>  \t\t}\n>  \n>  \t\tif (is_git_directory(dir->buf)) {\n> +\t\t\tif (!check_bare_repo_allowed())\n> +\t\t\t\treturn GIT_DIR_DISALLOWED_BARE;\n\nWon't this fail if someone runs a Git command inside of a .git/\ndirectory for a non-bare repository? I just want to be sure that\nwe hit this error instead:\n\n\tfatal: this operation must be run in a work tree\n\nI see that this error is tested in t0008-ignores.sh, but that's\nwith the default \"always\" value. It would be good to explicitly\ncheck that this is the right error when using the \"never\" config.\n\nThanks,\n-Stolee\n"},{"id":"455345","messageId":"c01a4ab9-675d-e0df-dfc9-2ec48a2a3ea2@github.com","threadId":"57859","inReplyTo":"62070aab7eb8734f82e3881e25ff0f9cee6bb077.1652485058.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 2/2] setup.c: learn discovery.bareRepository=cwd","fromName":"Derrick Stolee","fromEmail":"derrickstolee@github.com","sentAt":"2022-05-16T18:49:02Z","receivedAt":"2022-05-16T18:49:08Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 5/13/2022 7:37 PM, Glen Choo via GitGitGadget wrote:\n> From: Glen Choo <chooglen@google.com>\n> \n> Add a 'cwd' option to discovery.bareRepository, which allows a bare\n> repository to be used if and only if the cwd is the root of a bare\n> repository. This covers the common case where a user works with a bare\n> repository by cd-ing into the repository's root.\n\nI don't consider this case valuable. In addition to allowing\nthe most-common use case, it also allows the most-common route\nthat an attacker would use to try to get a user to run a Git\ncommand in a malicious embedded bare repo. I think we are\nbetter off without it.\n\nThanks,\n-Stolee\n"},{"id":"455347","messageId":"5b969c5e-e802-c447-ad25-6acc0b784582@github.com","threadId":"57859","inReplyTo":"pull.1261.v2.git.git.1652485058.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 0/2] setup.c: make bare repo discovery optional","fromName":"Derrick Stolee","fromEmail":"derrickstolee@github.com","sentAt":"2022-05-16T19:07:35Z","receivedAt":"2022-05-16T19:07:43Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 5/13/2022 7:37 PM, Glen Choo via GitGitGadget wrote:\n> Thanks all for the comments on v1, I've expanded this series somewhat to\n> address them,...\n\nPlease include a full cover letter with each version, so reviewers\ncan respond to the full series goals.\n\nYour series here intends to start protecting against malicious\nembedded bare repositories by allowing users to opt-in to a more\nprotected state. When the 'discovery.bare' option is set, then\nGit may die() on a bare repository that is discovered based on\nthe current working directory (these protections are ignored if\nthe user specifies the directory directly through --git-dir or\n$GIT_DIR).\n\nThe 'discovery.bare' option has these values at the end of your\nseries:\n\n* 'always' (default) allows all bare repos, matching the current\n  behavior of Git.\n\n* 'never' avoids operating in bare repositories altogether.\n\n* 'cwd' operates in a bare repository only if the current directory\n  is exactly the root of the bare repository.\n\nIt is important that we keep 'always' as the default at first,\nbecause we do not want to introduce a breaking change without\nwarning (at least for an issue like this that has been around\nfor a long time).\n\nThe 'never' option is a good one for very security-conscious\nusers who really want to avoid problems. I don't anticipate that\nusers who know about this option and set it themselves are the\ntype that would fall for the social engineering required to\nattack using this vector, but I can imagine an IT department\ninstalling the value in system config across a fleet of machines.\n\nI find the 'cwd' option to not be valuable. It unblocks most\nexisting users, but also almost completely removes the protection\nthat the option was supposed to provide.\n\nI find neither the 'never' or 'cwd' options an acceptable choice\nfor a future default.\n\nI also think that this protection is too rigid: it restricts\n_all_ bare repositories, not just embedded ones. There is no check\nto see if the parent directory of the bare repository is inside a\nnon-bare repository.\n\nThis leads to what I think would be a valuable replacement for\nthe 'cwd' option:\n\n* 'no-embedded' allows non-embedded bare repositories. An\n  _embedded bare repository_ is a bare repository whose parent\n  directory is contained in the worktree of a non-bare Git\n  repository. When in this mode, embedded bare repositories are\n  not allowed unless the parent non-bare Git repository has a\n  'safe.embedded' config value storing the path to the current\n  embedded bare repository.\n\nThat was certainly difficult to write, but here it is as\npseudo-code to hopefully remove some doubt as to how this might\nwork:\n\n  if repo is bare:\n    if value == \"always\":\n       return ALLOWED\n    if value == \"never\":\n       return FORBIDDEN;\n\n    path = get_parent_repo()\n\n    if !path:\n       return ALLOWED\n    \n    if config_file_has_value(\"{path}/.git/config\", \"safe.embedded\", repo):\n       return ALLOWED\n\n    return FORBIDDEN\n\nWith this kind of option, we can protect users from these\nsocial engineering attacks while providing an opt-in protection\nfor scenarios where embedded bare repos are currently being used\n(while also not breaking anyone using non-embedded bare repos).\n\nI think Taylor was mentioning something like this in his previous\nreplies, perhaps even to the previous thread on this topic.\n\nThis 'no-embedded' option is something that I could see as a\npotential new default, after it has proven itself in a released\nversion of Git.\n\nThere are performance drawbacks to checking the parent path for\na Git repo, which is why it is only done when in \"no-embedded\"\nmode.\n\nI mentioned some other concerns in your PATCH 1 about how we\nare now adding the third use of read_very_early_config() and that\nwe should probably refactor that before adding the third option,\nin order to avoid additional performance costs as well as it\nbeing difficult to audit which config options are only checked\nfrom these \"protected\" config files.\n\nThanks,\n-Stolee\n"},{"id":"455348","messageId":"xmqqk0al1e50.fsf@gitster.g","threadId":"57859","inReplyTo":"kl6ly1z1iati.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v2 0/2] setup.c: make bare repo discovery optional","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-05-16T19:16:59Z","receivedAt":"2022-05-16T19:17:20Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Glen Choo <chooglen@google.com> writes:\n\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n>> \"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>>\n>>>  * die()-ing is necessary if we're trying to flip the default value of\n>>>    discovery.bare. We'd expect many bare repo users to be broken, and it's\n>>>    more helpful to fail loudly than to silently ignore the bare repo.\n>>>\n>>>    But in the long term, long after we've flipped the default and users know\n>>>    that they need to opt into bare repo discovery, would it be a better UX\n>>>    to just silently ignore the bare repo?\n>>\n>> Would a middle-ground of giving a warning() message help?  Can it be\n>> loud and annoying enough to knudge the users to adjust without\n>> breaking the functionality?\n>\n> Personally, when my tool changes its behavior, I would strongly prefer\n> it to die than to \"change behavior + warn\". I'd feel more comfortable\n> knowing that the tool did nothing as opposed to doing the wrong thing\n> and only being informed after the fact. Also, I sometimes ignore\n> warnings ;)\n\nHeh, personally I would try very hard not to change the behaviour\nwithout explicitly asked by the users with configuration or command\nline option.  Flipping the default has traditionally been done in\ntwo or three phases.\n\n (1) We start by giving a loud and annoying warning to those who\n     haven't configured and tell them the default *will* change, how\n     to keep the current behaviour forever, and how to live in the\n     future by adopting the future default early.\n\n (2) After a while, we flip the default.  Those who haven't\n     configured are given a notice that the default has changed, how\n     to keep the old behaviour forever, and how to explicitly choose\n     the same value as the default to squelch the notice.\n\n (3) After yet another while, we stop giving the notice.  If we\n     omitted (2), here is where we flip the default.\n\nStrictly speaking, we can have (1) in one release and then could\ndirectly jump to (3), but some distros may skip the releases that\nhas (1), and (2) is an attempt to help users of such distros.\n\n>> Hopefully \"git fetch\" over ssh:// and file:/// would run the other\n>> side with GIT_DIR explicitly set?\n>\n> Ah, I'll check this and get back to you.\n>\n>>                                                        I do not yet\n>> find these \"problems, such as...\" so convincing.\n>\n> What would be a convincing rationale to you? I'll capture that here.\n\nThat is a wrong question.  You are the one pushing for castrating\nthe bare repositories.\n\n> I'm assuming that you already have such an rationale in mind when you\n> say that the longer-term default is that \"we respect bare repositories\n> only if they are the cwd.\". I'm also assuming that this rationale is\n> something other than embedded bare repos, because \"cwd-only\" does not\n> protect against that.\n\nNo, I do not have such a \"different\" rationale to justify the change\nproposed in this patch.  I was saying that the claim \"embedded bare\nrepos are risky\", backed by your two examples, did not sound all\nthat serious a problem.  Presented with a more serious brekage\nscenario, it may make the description more convincing.\n\nThanks.\n"},{"id":"455358","messageId":"kl6lv8u5i5pn.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"xmqqk0al1e50.fsf@gitster.g","subject":"Re: [PATCH v2 0/2] setup.c: make bare repo discovery optional","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-05-16T20:27:00Z","receivedAt":"2022-05-16T20:52:07Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Glen Choo <chooglen@google.com> writes:\n>\n>> Junio C Hamano <gitster@pobox.com> writes:\n>>\n>>> \"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>>>\n>>>>  * die()-ing is necessary if we're trying to flip the default value of\n>>>>    discovery.bare. We'd expect many bare repo users to be broken, and it's\n>>>>    more helpful to fail loudly than to silently ignore the bare repo.\n>>>>\n>>>>    But in the long term, long after we've flipped the default and users know\n>>>>    that they need to opt into bare repo discovery, would it be a better UX\n>>>>    to just silently ignore the bare repo?\n>>>\n>>> Would a middle-ground of giving a warning() message help?  Can it be\n>>> loud and annoying enough to knudge the users to adjust without\n>>> breaking the functionality?\n>>\n>> Personally, when my tool changes its behavior, I would strongly prefer\n>> it to die than to \"change behavior + warn\". I'd feel more comfortable\n>> knowing that the tool did nothing as opposed to doing the wrong thing\n>> and only being informed after the fact. Also, I sometimes ignore\n>> warnings ;)\n>\n> Heh, personally I would try very hard not to change the behaviour\n> without explicitly asked by the users with configuration or command\n> line option.  Flipping the default has traditionally been done in\n> two or three phases.\n>\n>  (1) We start by giving a loud and annoying warning to those who\n>      haven't configured and tell them the default *will* change, how\n>      to keep the current behaviour forever, and how to live in the\n>      future by adopting the future default early.\n>\n>  (2) After a while, we flip the default.  Those who haven't\n>      configured are given a notice that the default has changed, how\n>      to keep the old behaviour forever, and how to explicitly choose\n>      the same value as the default to squelch the notice.\n>\n>  (3) After yet another while, we stop giving the notice.  If we\n>      omitted (2), here is where we flip the default.\n>\n> Strictly speaking, we can have (1) in one release and then could\n> directly jump to (3), but some distros may skip the releases that\n> has (1), and (2) is an attempt to help users of such distros.\n\nAh, that is very helpful. Thanks. It's pretty clear that I misunderstood\nwhat you meant by \"giving a warning() message\" - the warning() is there\nto prepare users in advance of the change; we don't actually want the\nwarning() in the long term.\n\nFor something as disruptive as discovering bare repos, having all of\n(1), (2) and (3) sounds appropriate.\n\n>>> Hopefully \"git fetch\" over ssh:// and file:/// would run the other\n>>> side with GIT_DIR explicitly set?\n>>\n>> Ah, I'll check this and get back to you.\n>>\n>>>                                                        I do not yet\n>>> find these \"problems, such as...\" so convincing.\n>>\n>> What would be a convincing rationale to you? I'll capture that here.\n>\n> That is a wrong question.  You are the one pushing for castrating\n> the bare repositories.\n\nLet me clarify in case this wasn't received the way I intended. Earlier\nin the thread, you mentioned:\n\n  The longer-term default should be \"cwd is allowed, but we do not\n  bother going up from object/04 subdirectory of a bare repository\",\n  [...]\n\nwhich I took to mean \"Junio thinks that, by default, Git should stop\nwalking up to find a bare repo, and thinks this is better because of\nrationale X.\", and not, \"Junio does not think that the default needs to\nchange, but is just suggesting a better default than Glen's\".\n\nIf it is the former, then there is obviously some thought process here\nthat is worth sharing.\n\nIf it the latter, then I'm in favor of taking Stolee's suggestion to\ndrop \"cwd\", since nobody else finds it useful enough. (I like the\n'simplification' story, but not enough to push \"cwd\" through, especially\nsince it does quite little security-wise.)\n\n>> I'm assuming that you already have such an rationale in mind when you\n>> say that the longer-term default is that \"we respect bare repositories\n>> only if they are the cwd.\". I'm also assuming that this rationale is\n>> something other than embedded bare repos, because \"cwd-only\" does not\n>> protect against that.\n>\n> No, I do not have such a \"different\" rationale to justify the change\n> proposed in this patch.  I was saying that the claim \"embedded bare\n> repos are risky\", backed by your two examples, did not sound all\n> that serious a problem.  Presented with a more serious brekage\n> scenario, it may make the description more convincing.\n\nFair. I'll mull over this.\n"},{"id":"455365","messageId":"xmqq8rr115ub.fsf@gitster.g","threadId":"57859","inReplyTo":"kl6lv8u5i5pn.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v2 0/2] setup.c: make bare repo discovery optional","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-05-16T22:16:12Z","receivedAt":"2022-05-16T22:16:20Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Glen Choo <chooglen@google.com> writes:\n\n> which I took to mean \"Junio thinks that, by default, Git should stop\n> walking up to find a bare repo, and thinks this is better because of\n> rationale X.\"\n\nThe X is \"it would not break existing use case too badly, just to\naddress a 'security' story whose severity is not so clearly\nexpressed\".\n\n> If it the latter, then I'm in favor of taking Stolee's suggestion to\n> drop \"cwd\", since nobody else finds it useful enough. (I like the\n> 'simplification' story, but not enough to push \"cwd\" through, especially\n> since it does quite little security-wise.)\n\nAs long as you'll be there to answer the angry mob that complain\nloudly (and irritatingly enough, the only do so after a release is\nmade to flip the default), I do not care too much either way ;-).\n\nThanks.\n\n"},{"id":"455366","messageId":"YoLPQVAnOOX0AZ2v@nand.local","threadId":"57859","inReplyTo":"e5139922-9b74-ebfa-756c-629918e0456b@github.com","subject":"Re: [PATCH v2 1/2] setup.c: make bare repo discovery optional","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2022-05-16T22:25:05Z","receivedAt":"2022-05-16T22:25:12Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Mon, May 16, 2022 at 02:46:55PM -0400, Derrick Stolee wrote:\n> On 5/13/2022 7:37 PM, Glen Choo via GitGitGadget wrote:\n> > From: Glen Choo <chooglen@google.com>\n> >\n> > Add a config variable, `discovery.bare`, that tells Git whether or not\n> > it should work with the bare repository it has discovered i.e. Git will\n> > die() if it discovers a bare repository, but it is not allowed by\n> > `discovery.bare`. This only affects repository discovery, thus it has no\n> > effect if discovery was not done (e.g. `--git-dir` was passed).\n>\n> > This config is an enum of:\n> >\n> > - [\"always\"|(unset)]: always recognize bare repositories (like Git does\n> >   today)\n> > - \"never\": never recognize bare repositories\n> >\n> > More values are expected to be added later, and the default is expected\n> > to change (i.e. to something other than \"always\").\n>\n> I think it is fine to include the \"never\" option for users to opt-in to\n> this super-protected state, but I want to make it very clear that we\n> should never move to it as a new default. This phrasing of 'something\n> other than \"always\"' is key, but it might be good to point out that\n> \"never\" is very unlikely to be that default.\n\nI am confused, then.\n\nWhat does a user who has some legitimate (non-embedded) bare\nrepositories do if they are skeptical of other bare repositories? I\nsuspect the best answer we would be able to provide with these patches\nis \"use `--git-dir`\".\n\nWhat happens to a user who has a combination of legitimate bare\nrepositories, embedded bare repositories that they trust, and other\nembedded bare repositories that they don't?\n\nAs far as I can tell, our recommendation with these tools would be to:\n\n  - run `git config --global discovery.bare never`, and\n  - include `--git-dir=$(pwd)` in any git invocations in bare\n    repositories that they do trust\n\nThis gets at my concerns from [1] and [2] (mostly [2], in this case)\nthat we're trying to close the embedded bare repos problem with an\noverly broad solution, at the expense of usability.\n\nI can't shake the feeling that something like I described towards the\nbottom of [2] would give you all of the security guarantees you're after\nwithout compromising on usability for non-embedded bare repositories.\n\nI'm happy to explore this direction more myself if you don't want to. I\nwould just much rather see us adopt an approach that doesn't break more\nuse-cases than it has to if such a thing can be avoided.\n\nI cannot endorse these patches as-is.\n\nThanks,\nTaylor\n\n[1]: https://lore.kernel.org/git/Ylobp7sntKeWTLDX@nand.local/\n[2]: https://lore.kernel.org/git/YnmKwLoQCorBnMe2@nand.local/\n"},{"id":"455367","messageId":"YoLTqxYU7VO91Dam@nand.local","threadId":"57859","inReplyTo":"5b969c5e-e802-c447-ad25-6acc0b784582@github.com","subject":"Re: [PATCH v2 0/2] setup.c: make bare repo discovery optional","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2022-05-16T22:43:55Z","receivedAt":"2022-05-16T22:44:10Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Mon, May 16, 2022 at 03:07:35PM -0400, Derrick Stolee wrote:\n> On 5/13/2022 7:37 PM, Glen Choo via GitGitGadget wrote:\n> > Thanks all for the comments on v1, I've expanded this series somewhat to\n> > address them,...\n>\n> Please include a full cover letter with each version, so reviewers\n> can respond to the full series goals.\n>\n> Your series here intends to start protecting against malicious\n> embedded bare repositories by allowing users to opt-in to a more\n> protected state. [...]\n\nThanks for the summary, which I think will be especially helpful to\nothers looking at this series for the very first time.\n\n> The 'never' option is a good one for very security-conscious\n> users who really want to avoid problems. I don't anticipate that\n> users who know about this option and set it themselves are the\n> type that would fall for the social engineering required to\n> attack using this vector, but I can imagine an IT department\n> installing the value in system config across a fleet of machines.\n\nWhen I first read this, I disagreed, since presumably that same crowd\nhas legitimate bare repositories that they want to continue being able\nto operate in without having to pass `--git-dir` or `$GIT_DIR` in.\n\nIn fact...\n\n> I also think that this protection is too rigid: it restricts\n> _all_ bare repositories, not just embedded ones. There is no check\n> to see if the parent directory of the bare repository is inside a\n> non-bare repository.\n\n...this resonates quite a bit more with me. \"never\" isn't a good option\nunless you aren't a user of bare repositories _and_ don't have any\nembedded bare repositories (either at all, or any ones that you trust).\n\n> This leads to what I think would be a valuable replacement for\n> the 'cwd' option:\n>\n> * 'no-embedded' allows non-embedded bare repositories. An\n>   _embedded bare repository_ is a bare repository whose parent\n>   directory is contained in the worktree of a non-bare Git\n>   repository. When in this mode, embedded bare repositories are\n>   not allowed unless the parent non-bare Git repository has a\n>   'safe.embedded' config value storing the path to the current\n>   embedded bare repository.\n>\n> That was certainly difficult to write, but here it is as\n> pseudo-code to hopefully remove some doubt as to how this might\n> work:\n>\n>   if repo is bare:\n>     if value == \"always\":\n>        return ALLOWED\n>     if value == \"never\":\n>        return FORBIDDEN;\n\nThis is indeed very similar to a proposal I had made upthread (which you\nnote lower down in this email). One thing that's nice is that we only\nhave to traverse up to the parent repo when in the \"no-embedded\" mode.\nThat may be slow (since it's unbounded all the way up to the filesystem\nroot or a ceiling directory, whichever we encounter first), but I think\nit's unavoidable if you need to distinguish between embedded and\nnon-embedded bare repositories.\n\n>     path = get_parent_repo()\n>\n>     if !path:\n>        return ALLOWED\n>\n>     if config_file_has_value(\"{path}/.git/config\", \"safe.embedded\", repo):\n>        return ALLOWED\n>\n>     return FORBIDDEN\n>\n> With this kind of option, we can protect users from these\n> social engineering attacks while providing an opt-in protection\n> for scenarios where embedded bare repos are currently being used\n> (while also not breaking anyone using non-embedded bare repos).\n>\n> I think Taylor was mentioning something like this in his previous\n> replies, perhaps even to the previous thread on this topic.\n\nYep, see: https://lore.kernel.org/git/Ylobp7sntKeWTLDX@nand.local/.a\n\n> This 'no-embedded' option is something that I could see as a\n> potential new default, after it has proven itself in a released\n> version of Git.\n\nI would be totally happy to see \"no-embedded\" become the default. It\nmight be nice to issue a warning when the top-level config is unset,\nto give users a heads up about cases that may be broken, perhaps like:\n\n    if repo is bare:\n      switch (value) {\n      case \"always\":\n        return ALLOWED;\n      case \"never\":\n        return FORBIDDEN;\n      case \"no-embedded\": # fallthrough\n      case \"\":\n        path = get_parent_repo()\n        if !path\n          return ALLOWED;\n\n        if config_file_has_value(\"{path}/.git/config\", \"safe.embedded\", repo)\n          return ALLOWED;\n\n        if value == \"no-embedded\":\n          return FORBIDDEN;\n\n        # otherwise, we're in an embedded bare repository with an unset\n        # discovery.bare config.\n        #\n        # warn that this will break in the future...\n        warning(_(\"%s is embedded within %s\"), the_repository.path, path);\n        advise(_(\"to allow discovery for this embedded repo, either run\"));\n        advise(_(\"\"));\n        advise(_(\"  $ git config --global discovery.bare always, or\"));\n        advise(_(\"  $ git -C '%s' config --local safe.embedded '%s'\"),\n               path, relpath(path, the_repository.path));\n\n        # ...but allow the invocation for now until the default is\n        # changed.\n        return ALLOWED;\n      default:\n        die(_(\"unrecognized value of discovery.bare: '%s'\"), value);\n      }\n\n...where relpath is similar to Go's path/filepath.Rel function.\n\nWith an appropriate deprecation period, I think we could even get away\nfrom the \"continue executing, but don't read config+hooks\", which in\nretrospect is more error-prone and difficult to reason about than I\ninitially had given it credit for.\n\nThanks,\nTaylor\n"},{"id":"455369","messageId":"xmqq4k1p12wm.fsf@gitster.g","threadId":"57859","inReplyTo":"5b969c5e-e802-c447-ad25-6acc0b784582@github.com","subject":"Re: [PATCH v2 0/2] setup.c: make bare repo discovery optional","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-05-16T23:19:37Z","receivedAt":"2022-05-16T23:19:55Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Derrick Stolee <derrickstolee@github.com> writes:\n\n> * 'no-embedded' allows non-embedded bare repositories. An\n>   _embedded bare repository_ is a bare repository whose parent\n>   directory is contained in the worktree of a non-bare Git\n>   repository. When in this mode, embedded bare repositories are\n>   not allowed unless the parent non-bare Git repository has a\n>   'safe.embedded' config value storing the path to the current\n>   embedded bare repository.\n\nSounds sensible.  I wonder how expensive this will be in practice,\nbut the behaviour seems well thought out.\n"},{"id":"455392","messageId":"kl6llev0htsy.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"5b969c5e-e802-c447-ad25-6acc0b784582@github.com","subject":"Re: [PATCH v2 0/2] setup.c: make bare repo discovery optional","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-05-17T18:56:29Z","receivedAt":"2022-05-17T18:56:43Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"\nThanks, I think this has advanced the conversation quite a bit.\n\nDerrick Stolee <derrickstolee@github.com> writes:\n\n> On 5/13/2022 7:37 PM, Glen Choo via GitGitGadget wrote:\n>> Thanks all for the comments on v1, I've expanded this series somewhat to\n>> address them,...\n>\n> Please include a full cover letter with each version, so reviewers\n> can respond to the full series goals.\n>\n> Your series here intends to start protecting against malicious\n> embedded bare repositories by allowing users to opt-in to a more\n> protected state. When the 'discovery.bare' option is set, then\n> Git may die() on a bare repository that is discovered based on\n> the current working directory (these protections are ignored if\n> the user specifies the directory directly through --git-dir or\n> $GIT_DIR).\n>\n> The 'discovery.bare' option has these values at the end of your\n> series:\n>\n> * 'always' (default) allows all bare repos, matching the current\n>   behavior of Git.\n>\n> * 'never' avoids operating in bare repositories altogether.\n>\n> * 'cwd' operates in a bare repository only if the current directory\n>   is exactly the root of the bare repository.\n\nMy mistake, I should have prepared this summary myself. Thanks again.\n\n> It is important that we keep 'always' as the default at first,\n> because we do not want to introduce a breaking change without\n> warning (at least for an issue like this that has been around\n> for a long time).\n\nYes.\n\n> The 'never' option is a good one for very security-conscious\n> users who really want to avoid problems. I don't anticipate that\n> users who know about this option and set it themselves are the\n> type that would fall for the social engineering required to\n> attack using this vector, but I can imagine an IT department\n> installing the value in system config across a fleet of machines.\n\nYes. Setting the 'never' option in a system config is the use case that\nmotivated this.\n\n> I find the 'cwd' option to not be valuable. It unblocks most\n> existing users, but also almost completely removes the protection\n> that the option was supposed to provide.\n\nOk, I agree that it provides next-to-no protection. I'll drop it in this\nseries; it's easy enough to reimplement if users really want it anyway.\n\n> This leads to what I think would be a valuable replacement for\n> the 'cwd' option:\n>\n> * 'no-embedded' allows non-embedded bare repositories. An\n>   _embedded bare repository_ is a bare repository whose parent\n>   directory is contained in the worktree of a non-bare Git\n>   repository. When in this mode, embedded bare repositories are\n>   not allowed unless the parent non-bare Git repository has a\n>   'safe.embedded' config value storing the path to the current\n>   embedded bare repository.\n>\n> That was certainly difficult to write, but here it is as\n> pseudo-code to hopefully remove some doubt as to how this might\n> work:\n>\n>   if repo is bare:\n>     if value == \"always\":\n>        return ALLOWED\n>     if value == \"never\":\n>        return FORBIDDEN;\n>\n>     path = get_parent_repo()\n>\n>     if !path:\n>        return ALLOWED\n>     \n>     if config_file_has_value(\"{path}/.git/config\", \"safe.embedded\", repo):\n>        return ALLOWED\n>\n>     return FORBIDDEN\n>\n> With this kind of option, we can protect users from these\n> social engineering attacks while providing an opt-in protection\n> for scenarios where embedded bare repos are currently being used\n> (while also not breaking anyone using non-embedded bare repos).\n\n[...]\n\n> This 'no-embedded' option is something that I could see as a\n> potential new default, after it has proven itself in a released\n> version of Git.\n\nI agree, this sounds like a good default that should work for most\nusers.\n\nThat said, I don't think I will implement it, and even if I do, it won't\nbe in this series. I have serious doubts that I'd be able to deliver it\nin a reasonable amount of time (I tried preparing patches to this effect\nand failed [1]), and 'never' is sufficient for $DAYJOB's current needs.\n\nI would be very happy to see this come to fruition though. I have no\nobjections to anyone preparing patches for this, and I'll gladly review\nthose if that's helpful.\n\n[1] The specific trouble I had was figuring out whether or not the\n 'parent' repo was tracking the bare repo, since an untracked bare repo\n in the working tree isn't (in some sense) really \"embedded\" and it\n can't have come from a remote.\n\n But maybe the tracking check is unnecessary. We would break a few more\n users without it, but 'safe.embedded' is an easy enough way for a user\n to unbreak themselves.\n\n> I mentioned some other concerns in your PATCH 1 about how we\n> are now adding the third use of read_very_early_config() and that\n> we should probably refactor that before adding the third option,\n> in order to avoid additional performance costs as well as it\n> being difficult to audit which config options are only checked\n> from these \"protected\" config files.\n\nMakes sense. I'll ask about specifics on that subthread.\n\n>\n> Thanks,\n> -Stolee\n"},{"id":"455396","messageId":"kl6lilq3j4aq.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"e5139922-9b74-ebfa-756c-629918e0456b@github.com","subject":"Re: [PATCH v2 1/2] setup.c: make bare repo discovery optional","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-05-17T20:24:29Z","receivedAt":"2022-05-17T20:24:43Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Thanks for being thorough, I find it really helpful.\n\nFor brevity, I won't reply to comments that I think are obviously good,\nso you can assume I'll incorproate anything that isn't commented on.\n\nDerrick Stolee <derrickstolee@github.com> writes:\n\n> On 5/13/2022 7:37 PM, Glen Choo via GitGitGadget wrote:\n>> From: Glen Choo <chooglen@google.com>\n>> \n>> +This config setting is only respected when specified in a system or global\n>> +config, not when it is specified in a repository config or via the command\n>> +line option `-c discovery.bare=<value>`.\n>\n> We are sprinkling config options that have these same restrictions throughout\n> the config documentation. It might be time to define a term like \"protected\n> config\" at the top of git-config.txt and then refer to that from these other\n> locations.\n\nAgree, and I think defining the term will be useful in future on-list\ndiscussions.\n\n>> +static int check_bare_repo_allowed(void)\n>> +{\n>> +\tif (discovery_bare_config == DISCOVERY_BARE_UNKNOWN) {\n>> +\t\tread_very_early_config(discovery_bare_cb, NULL);\n>\n> This will add the third place where we use read_very_early_config(),\n> adding to the existing calls in tr2_sysenv_load() and\n> ensure_valid_ownership(). If I understand it correctly, that means\n> that every Git execution in a bare repository will now parse the\n> system and global config three times.\n>\n> This doesn't count the check for uploadpack.packobjectshook in\n> upload-pack.c that uses current_config_scope() to restrict its\n> value to the system and global config.\n>\n> We are probably at the point where we need to instead create a\n> configset that stores this \"protected config\" and allow us to\n> lookup config keys directly from that configset instead of\n> iterating through these config files repeatedly.\n\nLooking at all of the read_very_early_config() calls,\n\n- check_bare_repo_allowed() can use git_configset_get_string()\n- ensure_valid_ownership() can use git_configset_get_value_multi()\n- tr2_sysenv_load() reads every value with the \"trace2.\" prefix. AFAICT\n  configsets only support exact key lookups and I don't see an easy way\n  teach configsets to support prefix lookups.\n\n(I didn't look too closely at uploadpack.packobjectshook because I don't\nknow enough about config scopes to comment.)\n\nSo using a configset, we'll still need to read the config files at least\ntwice. That's better than thrice, but it doesn't cover the\ntr2_sysenv_load() use case, and we'll run into this yet again if add\nfunction that reads all config values with a given prefix.\n\nAn hacky alternative that covers all of these use cases would be to read\nall protected config in a single pass, e.g.\n\n  static struct protected_config {\n         struct safe_directory_data safe_directory_data;\n         const char *discovery_bare;\n         struct string_list tr2_sysenv;\n  };\n\n  static int protected_config_cb()\n  {\n    /* Parse EVERYTHING that belongs in protected_config. */\n  }\n\nbut protected_config_cb() would have to parse too many unrelated things\nfor my liking.\n\nSo I'll use the configset for the cases where the key is known, and\nperhaps we'll punt on tr2_sysenv_load().\n\n>> +\t}\n>> +\tswitch (discovery_bare_config) {\n>> +\tcase DISCOVERY_BARE_NEVER:\n>> +\t\treturn 0;\n>> +\tcase DISCOVERY_BARE_ALWAYS:\n>> +\t\treturn 1;\n>> +\tdefault:\n>> +\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n>> +\t}\n>\n> You return -1 in discovery_bare_cb when the key matches, but\n> the value is not understood. Should we check the return value\n> of read_very_early_config(), too?\n\nThis comment doesn't apply because unlike most other config reading\nfunctions, read_very_early_config() and read_early_config() die when the\ncallback returns -1.\n\nI'm not sure why this is the case though, and maybe you think there is\nvalue in having a non-die()-ing variant, e.g.\nread_very_early_config_gently()?\n\n>>  };\n>>  \n>>  /*\n>> @@ -1239,6 +1293,8 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n>>  \t\t}\n>>  \n>>  \t\tif (is_git_directory(dir->buf)) {\n>> +\t\t\tif (!check_bare_repo_allowed())\n>> +\t\t\t\treturn GIT_DIR_DISALLOWED_BARE;\n>\n> Won't this fail if someone runs a Git command inside of a .git/\n> directory for a non-bare repository? I just want to be sure that\n> we hit this error instead:\n>\n> \tfatal: this operation must be run in a work tree\n>\n> I see that this error is tested in t0008-ignores.sh, but that's\n> with the default \"always\" value. It would be good to explicitly\n> check that this is the right error when using the \"never\" config.\n\nYes, it will fail if run inside of a .git/ directory. \"never\" prevents\nyou from working from inside .git/ unless you set GIT_DIR.\n\nIIRC, we don't show \"fatal: this operation must be run in a work\ntree\" for every Git command, e.g. \"git log\" works just fine. It makes\nsense to show this warning when the CWD supports 'some, but not all' Git\ncommands, but I don't think this is valuable if we forbid *all* Git\ncommands.\n\nInstead of trying to make \"never\" accomodate this use case, perhaps what\nwe want is a \"dotgit-only\" option that allows a bare repository if it is\nbelow a .git/ directory. Since we forbid .git in the index, this seems\nsomewhat safe, but I hadn't proposed this sooner because I don't know if\nwe need it yet, and I'm certain that there are less secure edge cases\nthat need to be thought through.\n"},{"id":"455398","messageId":"kl6lfsl7j098.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"kl6lilq3j4aq.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v2 1/2] setup.c: make bare repo discovery optional","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-05-17T21:51:47Z","receivedAt":"2022-05-17T21:52:00Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Glen Choo <chooglen@google.com> writes:\n\n>>> +static int check_bare_repo_allowed(void)\n>>> +{\n>>> +\tif (discovery_bare_config == DISCOVERY_BARE_UNKNOWN) {\n>>> +\t\tread_very_early_config(discovery_bare_cb, NULL);\n>>\n>> This will add the third place where we use read_very_early_config(),\n>> adding to the existing calls in tr2_sysenv_load() and\n>> ensure_valid_ownership(). If I understand it correctly, that means\n>> that every Git execution in a bare repository will now parse the\n>> system and global config three times.\n>>\n>> This doesn't count the check for uploadpack.packobjectshook in\n>> upload-pack.c that uses current_config_scope() to restrict its\n>> value to the system and global config.\n>>\n>> We are probably at the point where we need to instead create a\n>> configset that stores this \"protected config\" and allow us to\n>> lookup config keys directly from that configset instead of\n>> iterating through these config files repeatedly.\n>\n> Looking at all of the read_very_early_config() calls,\n>\n> - check_bare_repo_allowed() can use git_configset_get_string()\n> - ensure_valid_ownership() can use git_configset_get_value_multi()\n> - tr2_sysenv_load() reads every value with the \"trace2.\" prefix. AFAICT\n>   configsets only support exact key lookups and I don't see an easy way\n>   teach configsets to support prefix lookups.\n>\n> (I didn't look too closely at uploadpack.packobjectshook because I don't\n> know enough about config scopes to comment.)\n>\n> So using a configset, we'll still need to read the config files at least\n> twice. That's better than thrice, but it doesn't cover the\n> tr2_sysenv_load() use case, and we'll run into this yet again if add\n> function that reads all config values with a given prefix.\n>\n> An hacky alternative that covers all of these use cases would be to read\n> all protected config in a single pass, e.g.\n>\n>   static struct protected_config {\n>          struct safe_directory_data safe_directory_data;\n>          const char *discovery_bare;\n>          struct string_list tr2_sysenv;\n>   };\n>\n>   static int protected_config_cb()\n>   {\n>     /* Parse EVERYTHING that belongs in protected_config. */\n>   }\n>\n> but protected_config_cb() would have to parse too many unrelated things\n> for my liking.\n>\n> So I'll use the configset for the cases where the key is known, and\n> perhaps we'll punt on tr2_sysenv_load().\n\nSince I'm trying to replace read_very_early_config() anyway, is this a\ngood time to teach git to respect \"-c safe.directory\"?\n\nMy understanding of [1] is that we only ignore \"-c safe.directory\"\nbecause read_very_early_config() doesn't support it, but we would prefer\nto support it if we could.\n\n[1] https://lore.kernel.org/git/xmqqlevabcsu.fsf@gitster.g/\n"},{"id":"456323","messageId":"pull.1261.v3.git.git.1653685761.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v2.git.git.1652485058.gitgitgadget@gmail.com","subject":"[PATCH v3 0/5] config: introduce discovery.bare and protected config","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-05-27T21:09:16Z","receivedAt":"2022-05-27T21:09:29Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Thanks everyone for the feedback. This round has two major changes compared\nto the last:\n\n * This feature is now purely motivated by the embedded bare repo attack;\n   I've dropped all of language pertaining to 'simplifying' bare repository\n   discovery.\n\n * Protected config gets a formal definition and is implemented using a\n   config_set, which should hopefully address most of the performance\n   concerns of using read_very_early_config() (see 2/5 for more discussion).\n   \n   With this new implementation, it's easy to teach protected config to\n   include \"-c\". This round includes some patches (4-5/5) that do this but\n   I'm ok to drop them if we don't agree that they are a good idea.\n\nThis round is newly rebased onto master because\nsg/safe-directory-tests-and-docs has been recently integrated and the \"-c\"\npatches directly contradict that.\n\n= Description\n\nThere is a known social engineering attack that takes advantage of the fact\nthat a working tree can include an entire bare repository, including a\nconfig file. A user could run a Git command inside the bare repository\nthinking that the config file of the 'outer' repository would be used, but\nin reality, the bare repository's config file (which is attacker-controlled)\nis used, which may result in arbitrary code execution. See [1] for a fuller\ndescription and deeper discussion.\n\nThis series implements a simple way of preventing such attacks: create a\nconfig option, discovery.bare, that tells Git whether or not to die when it\nfinds a bare repository. discovery.bare has two values:\n\n * \"always\": always allow bare repositories (default), identical to current\n   behavior\n * \"never\": never allow bare repositories\n\nand users/system administrators who never expect to work with bare\nrepositories can secure their environments using \"never\". discovery.bare has\nno effect if --git-dir or GIT_DIR is passed because we are confident that\nthe user is not confused about which repository is being used.\n\nThis series does not change the default behavior, but in the long-run, a\n\"no-embedded\" option might be a safe and usable default [2]. \"never\" is too\nrestrictive and unlikely to be the default.\n\nFor security reasons, discovery.bare cannot be read from repository-level\nconfig (because we would end up trusting the embedded bare repository that\nwe aren't supposed to trust to begin with). Since this would introduce a 3rd\nvariable that is only read from 'protected/trusted config' (the others are\nsafe.directory and uploadpack.packObjectsHook) this series also defines and\ncreates a shared implementation for 'protected config'\n\n= Patch organization\n\n * Patches 1-2 define \"protected config\" and create a shared implementation.\n * Patch 3 introduces discovery.bare.\n * Patches 4-5 expand the definition of \"protected config\" to include the\n   CLI option \"-c\" [3]. Since this is identical to how\n   uploadpack.packObjectsHook currently behaves, it is refactored to a\n   \"protected config only\" variable.\n\n= Series history\n\nChanges in v3:\n\n * Rebase onto a more recent 'master'\n * Reframe this feature in only in terms of the 'embedded bare repo' attack.\n * Other docs improvements (thanks Stolee in particular!)\n * Protected config no longer uses read_very_early_config() and is only read\n   once\n * Protected config now includes \"-c\"\n * uploadpack.packObjectsHook now uses protected config instead of ignoring\n   repo config using config scopes\n\nChanges in v2:\n\n * Rename safe.barerepository to discovery.bare and make it die()\n * Move tests into t/t0034-discovery-bare.sh\n * Avoid unnecessary config reading by using a static variable\n * Add discovery.bare=cwd\n * Fix typos\n\n= Future work\n\n * This series does not implement the \"no-embedded\" option [2] and I won't\n   work on it any time soon, but I'd be more than happy to review if someone\n   sends patches.\n * With discovery.bare, if a builtin is marked RUN_SETUP_GENTLY, setup.c\n   doesn't die() and we don't tell users why their repository was rejected,\n   e.g. \"git config\" gives an opaque \"fatal: not in a git directory\". This\n   isn't a new problem though, since safe.directory has the same issue.\n\n[1]\nhttps://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n[2] This was first suggested in\nhttps://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n[3] https://lore.kernel.org/git/xmqqlevabcsu.fsf@gitster.g/ suggests that\nsafe.directory doesn't need to ignore \"-c\", it just happened to be\nimplemented that way.\n\nGlen Choo (5):\n  Documentation: define protected configuration\n  config: read protected config with `git_protected_config()`\n  setup.c: create `discovery.bare`\n  config: include \"-c\" in protected config\n  upload-pack: make uploadpack.packObjectsHook protected\n\n Documentation/config.txt            |  8 ++++\n Documentation/config/discovery.txt  | 19 ++++++++\n Documentation/config/safe.txt       | 19 ++++----\n Documentation/config/uploadpack.txt | 22 ++++------\n Documentation/glossary-content.txt  | 18 ++++++++\n config.c                            | 41 +++++++++++++++++\n config.h                            | 17 ++++++++\n repository.c                        |  5 +++\n repository.h                        |  8 ++++\n setup.c                             | 68 ++++++++++++++++++++++++++++-\n t/t0033-safe-directory.sh           | 24 +++++-----\n t/t0035-discovery-bare.sh           | 63 ++++++++++++++++++++++++++\n upload-pack.c                       | 17 +++++---\n 13 files changed, 283 insertions(+), 46 deletions(-)\n create mode 100644 Documentation/config/discovery.txt\n create mode 100755 t/t0035-discovery-bare.sh\n\n\nbase-commit: f9b95943b68b6b8ca5a6072f50a08411c6449b55\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1261%2Fchooglen%2Fsetup%2Fdisable-bare-repo-config-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1261/chooglen/setup/disable-bare-repo-config-v3\nPull-Request: https://github.com/git/git/pull/1261\n\nRange-diff vs v2:\n\n -:  ----------- > 1:  575676c760d Documentation: define protected configuration\n -:  ----------- > 2:  7499a280961 config: read protected config with `git_protected_config()`\n 1:  22b10bf9da8 ! 3:  d5a3e9f9845 setup.c: make bare repo discovery optional\n     @@ Metadata\n      Author: Glen Choo <chooglen@google.com>\n      \n       ## Commit message ##\n     -    setup.c: make bare repo discovery optional\n     +    setup.c: create `discovery.bare`\n      \n     -    Add a config variable, `discovery.bare`, that tells Git whether or not\n     -    it should work with the bare repository it has discovered i.e. Git will\n     -    die() if it discovers a bare repository, but it is not allowed by\n     -    `discovery.bare`. This only affects repository discovery, thus it has no\n     -    effect if discovery was not done (e.g. `--git-dir` was passed).\n     +    There is a known social engineering attack that takes advantage of the\n     +    fact that a working tree can include an entire bare repository,\n     +    including a config file. A user could run a Git command inside the bare\n     +    repository thinking that the config file of the 'outer' repository would\n     +    be used, but in reality, the bare repository's config file (which is\n     +    attacker-controlled) is used, which may result in arbitrary code\n     +    execution. See [1] for a fuller description and deeper discussion.\n      \n     -    This is motivated by the fact that some workflows don't use bare\n     -    repositories at all, and users may prefer to opt out of bare repository\n     -    discovery altogether:\n     +    A simple mitigation is to forbid bare repositories unless specified via\n     +    `--git-dir` or `GIT_DIR`. In environments that don't use bare\n     +    repositories, this would be minimally disruptive.\n      \n     -    - An easy assumption for a user to make is that Git commands run\n     -      anywhere inside a repository's working tree will use the same\n     -      repository. However, if the working tree contains a bare repository\n     -      below the root-level (\".git\" is preferred at the root-level), any\n     -      operations inside that bare repository use the bare repository\n     -      instead.\n     -\n     -      In the worst case, attackers can use this confusion to trick users\n     -      into running arbitrary code (see [1] for a deeper discussion). But\n     -      even in benign situations (e.g. a user renames \".git/\" to \".git.old/\"\n     -      and commits it for archival purposes), disabling bare repository\n     -      discovery can be a simpler mode of operation (e.g. because the user\n     -      doesn't actually want to use \".git.old/\") [2].\n     -\n     -    - Git won't \"accidentally\" recognize a directory that wasn't meant to be\n     -      a bare repository, but happens to resemble one. While such accidents\n     -      are probably very rare in practice, this lets users reduce the chance\n     -      to zero.\n     +    Create a config variable, `discovery.bare`, that tells Git whether or\n     +    not to die() when it discovers a bare repository. This only affects\n     +    repository discovery, thus it has no effect if discovery was not\n     +    done (e.g. `--git-dir` was passed).\n      \n          This config is an enum of:\n      \n     -    - [\"always\"|(unset)]: always recognize bare repositories (like Git does\n     -      today)\n     -    - \"never\": never recognize bare repositories\n     +    - \"always\": always allow bare repositories (this is the default)\n     +    - \"never\": never allow bare repositories\n      \n     -    More values are expected to be added later, and the default is expected\n     -    to change (i.e. to something other than \"always\").\n     +    If we want to protect users from such attacks by default, neither value\n     +    will suffice - \"always\" provides no protection, but \"never\" is\n     +    impractical for bare repository users. A more usable default would be to\n     +    allow only non-embedded bare repositories ([2] contains one such\n     +    proposal), but detecting if a repository is embedded is potentially\n     +    non-trivial, so this work is not implemented in this series.\n      \n          [1]: https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n     -    [2]: I don't personally know anyone who does this as part of their\n     -    normal workflow, but a cursory search on GitHub suggests that there is a\n     -    not insubstantial number of people who munge \".git\" in order to store\n     -    its contents.\n     -\n     -    https://github.com/search?l=&o=desc&p=1&q=ref+size%3A%3C1000+filename%3AHEAD&s=indexed&type=Code\n     -    (aka search for the text \"ref\", size:<1000, filename:HEAD)\n     +    [2]: https://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n      \n          Signed-off-by: Glen Choo <chooglen@google.com>\n      \n     -    WIP setup.c: make discovery.bare die on failure\n     -\n     -    Signed-off-by: Glen Choo <chooglen@google.com>\n     + ## Documentation/config.txt ##\n     +@@ Documentation/config.txt: include::config/diff.txt[]\n     + \n     + include::config/difftool.txt[]\n     + \n     ++include::config/discovery.txt[]\n     ++\n     + include::config/extensions.txt[]\n     + \n     + include::config/fastimport.txt[]\n      \n       ## Documentation/config/discovery.txt (new) ##\n      @@\n      +discovery.bare::\n     -+\tSpecifies what kinds of directories Git can recognize as a bare\n     -+\trepository when looking for the repository (aka repository\n     -+\tdiscovery). This has no effect if repository discovery is not\n     -+\tperformed e.g. the path to the repository is set via `--git-dir`\n     -+\t(see linkgit:git[1]).\n     ++\t'(Protected config only)' Specifies whether Git will work with a\n     ++\tbare repository that it found during repository discovery. This\n     ++\thas no effect if the repository is specified directly via the\n     ++\t--git-dir command-line option or the GIT_DIR environment\n     ++\tvariable (see linkgit:git[1]).\n      ++\n     -+This config setting is only respected when specified in a system or global\n     -+config, not when it is specified in a repository config or via the command\n     -+line option `-c discovery.bare=<value>`.\n     ++The currently supported values are:\n      ++\n     -+The currently supported values are `always` (Git always recognizes bare\n     -+repositories) and `never` (Git never recognizes bare repositories).\n     -+This defaults to `always`, but this default is likely to change.\n     ++* `always`: Git always works with bare repositories\n     ++* `never`: Git never works with bare repositories\n      ++\n     -+If your workflow does not rely on bare repositories, it is recommended that\n     -+you set this value to `never`. This makes repository discovery easier to\n     -+reason about and prevents certain types of security and non-security\n     -+problems, such as:\n     -+\n     -+* `git clone`-ing a repository containing a malicious bare repository\n     -+  inside it.\n     -+* Git recognizing a directory that isn't meant to be a bare repository,\n     -+  but happens to look like one.\n     ++This defaults to `always`, but this default may change in the future.\n     +++\n     ++If you do not use bare repositories in your workflow, then it may be\n     ++beneficial to set `discovery.bare` to `never` in your global config.\n     ++This will protect you from attacks that involve cloning a repository\n     ++that contains a bare repository and running a Git command within that\n     ++directory.\n      \n       ## setup.c ##\n      @@\n     @@ setup.c: static int ensure_valid_ownership(const char *path)\n      +static int check_bare_repo_allowed(void)\n      +{\n      +\tif (discovery_bare_config == DISCOVERY_BARE_UNKNOWN) {\n     -+\t\tread_very_early_config(discovery_bare_cb, NULL);\n     -+\t\t/* We didn't find a value; use the default. */\n     -+\t\tif (discovery_bare_config == DISCOVERY_BARE_UNKNOWN)\n     -+\t\t\tdiscovery_bare_config = DISCOVERY_BARE_ALWAYS;\n     ++\t\tdiscovery_bare_config = DISCOVERY_BARE_ALWAYS;\n     ++\t\tgit_protected_config(discovery_bare_cb, NULL);\n      +\t}\n      +\tswitch (discovery_bare_config) {\n      +\tcase DISCOVERY_BARE_NEVER:\n      +\t\treturn 0;\n      +\tcase DISCOVERY_BARE_ALWAYS:\n      +\t\treturn 1;\n     -+\tdefault:\n     ++\tcase DISCOVERY_BARE_UNKNOWN:\n      +\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n      +\t}\n     ++\treturn 0;\n      +}\n      +\n      +static const char *discovery_bare_config_to_string(void)\n     @@ setup.c: static int ensure_valid_ownership(const char *path)\n      +\t\treturn \"never\";\n      +\tcase DISCOVERY_BARE_ALWAYS:\n      +\t\treturn \"always\";\n     -+\tdefault:\n     ++\tcase DISCOVERY_BARE_UNKNOWN:\n      +\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n      +\t}\n     ++\treturn NULL;\n      +}\n      +\n       enum discovery_result {\n     @@ setup.c: enum discovery_result {\n       \tGIT_DIR_INVALID_GITFILE = -3,\n      -\tGIT_DIR_INVALID_OWNERSHIP = -4\n      +\tGIT_DIR_INVALID_OWNERSHIP = -4,\n     -+\tGIT_DIR_DISALLOWED_BARE = -5\n     ++\tGIT_DIR_DISALLOWED_BARE = -5,\n       };\n       \n       /*\n     @@ setup.c: const char *setup_git_directory_gently(int *nongit_ok)\n       \t\t/*\n       \t\t * As a safeguard against setup_git_directory_gently_1 returning\n      \n     - ## t/t0034-discovery-bare.sh (new) ##\n     + ## t/t0035-discovery-bare.sh (new) ##\n      @@\n      +#!/bin/sh\n      +\n     @@ t/t0034-discovery-bare.sh (new)\n      +\n      +pwd=\"$(pwd)\"\n      +\n     -+expect_allowed () {\n     -+\tgit rev-parse --absolute-git-dir >actual &&\n     -+\techo \"$pwd/outer-repo/bare-repo\" >expected &&\n     -+\ttest_cmp expected actual\n     -+}\n     -+\n      +expect_rejected () {\n     -+\ttest_must_fail git rev-parse --absolute-git-dir 2>err &&\n     ++\ttest_must_fail git rev-parse --git-dir 2>err &&\n      +\tgrep \"discovery.bare\" err\n      +}\n      +\n     @@ t/t0034-discovery-bare.sh (new)\n      +test_expect_success 'discovery.bare unset' '\n      +\t(\n      +\t\tcd outer-repo/bare-repo &&\n     -+\t\texpect_allowed &&\n     -+\t\tcd refs/ &&\n     -+\t\texpect_allowed\n     ++\t\tgit rev-parse --git-dir\n      +\t)\n      +'\n      +\n     @@ t/t0034-discovery-bare.sh (new)\n      +\tgit config --global discovery.bare always &&\n      +\t(\n      +\t\tcd outer-repo/bare-repo &&\n     -+\t\texpect_allowed &&\n     -+\t\tcd refs/ &&\n     -+\t\texpect_allowed\n     ++\t\tgit rev-parse --git-dir\n      +\t)\n      +'\n      +\n     @@ t/t0034-discovery-bare.sh (new)\n      +\tgit config --global discovery.bare never &&\n      +\t(\n      +\t\tcd outer-repo/bare-repo &&\n     -+\t\texpect_rejected &&\n     -+\t\tcd refs/ &&\n      +\t\texpect_rejected\n     -+\t) &&\n     ++\t)\n     ++'\n     ++\n     ++test_expect_success 'discovery.bare in the repository' '\n     ++\t(\n     ++\t\tcd outer-repo/bare-repo &&\n     ++\t\t# Temporarily set discovery.bare=always, otherwise git\n     ++\t\t# config fails with \"fatal: not in a git directory\"\n     ++\t\t# (like safe.directory)\n     ++\t\tgit config --global discovery.bare always &&\n     ++\t\tgit config discovery.bare always &&\n     ++\t\tgit config --global discovery.bare never &&\n     ++\t\texpect_rejected\n     ++\t)\n     ++'\n     ++\n     ++test_expect_success 'discovery.bare on the command line' '\n     ++\tgit config --global discovery.bare never &&\n      +\t(\n     -+\t\tGIT_DIR=outer-repo/bare-repo &&\n     -+\t\texport GIT_DIR &&\n     -+\t\texpect_allowed\n     ++\t\tcd outer-repo/bare-repo &&\n     ++\t\ttest_must_fail git -c discovery.bare=always rev-parse --git-dir 2>err &&\n     ++\t\tgrep \"discovery.bare\" err\n      +\t)\n      +'\n      +\n 2:  62070aab7eb < -:  ----------- setup.c: learn discovery.bareRepository=cwd\n -:  ----------- > 4:  66a0a208176 config: include \"-c\" in protected config\n -:  ----------- > 5:  e25d5907cd1 upload-pack: make uploadpack.packObjectsHook protected\n\n-- \ngitgitgadget\n"},{"id":"456324","messageId":"575676c760d9a2ce4a59d50e93aa0f45d54620ab.1653685761.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v3.git.git.1653685761.gitgitgadget@gmail.com","subject":"[PATCH v3 1/5] Documentation: define protected configuration","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-05-27T21:09:17Z","receivedAt":"2022-05-27T21:09:32Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nFor security reasons, some config variables are only trusted when they\nare specified in so-called 'protected configuration' [1]. A future\ncommit will introduce another such config variable, so this is a good\ntime to standardize the documentation and implementation of 'protected\nconfiguration'.\n\nDefine 'protected configuration' as global and system-level config, and\nmark `safe.directory` 'Protected config only'. In a future commit,\nprotected configuration will also include \"-c\".\n\nThe following variables are intentionally not marked 'Protected config\nonly':\n\n- `uploadpack.packObjectsHook` has the same security concerns as\n  `safe.directory`, but due to a different implementation, it also\n  respects the \"-c\" option.\n\n  When protected configuration includes \"-c\", `upload.packObjectsHook`\n  will be marked 'Protected config only'.\n\n- `trace2.*` happens to read the same config as `safe.directory` because\n  they share an implementation. However, this is not for security\n  reasons; it is because we want to start tracing so early that\n  repository-level config and \"-c\" are not available [2].\n\n  This requirement is unique to `trace2.*`, so it does not makes sense\n  for protected configuration to be subject to the same constraints.\n\n[1] For example,\nhttps://lore.kernel.org/git/6af83767-576b-75c4-c778-0284344a8fe7@github.com/\n[2] https://lore.kernel.org/git/a0c89d0d-669e-bf56-25d2-cbb09b012e70@jeffhostetler.com/\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config.txt           |  6 ++++++\n Documentation/config/safe.txt      | 19 ++++++++-----------\n Documentation/glossary-content.txt | 18 ++++++++++++++++++\n 3 files changed, 32 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex e284b042f22..07832de1a6c 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -369,6 +369,12 @@ inventing new variables for use in your own tool, make sure their\n names do not conflict with those that are used by Git itself and\n other popular tools, and describe them in your documentation.\n \n+Variables marked with '(Protected config only)' are only respected when\n+they are specified in protected configuration. This includes global and\n+system-level config, and excludes repository config, the command line\n+option `-c`, and environment variables. For more details, see the\n+'protected configuration' entry in linkgit:gitglossary[7].\n+\n include::config/advice.txt[]\n \n include::config/core.txt[]\ndiff --git a/Documentation/config/safe.txt b/Documentation/config/safe.txt\nindex ae0e2e3bdb4..c1caec460e8 100644\n--- a/Documentation/config/safe.txt\n+++ b/Documentation/config/safe.txt\n@@ -1,21 +1,18 @@\n safe.directory::\n-\tThese config entries specify Git-tracked directories that are\n-\tconsidered safe even if they are owned by someone other than the\n-\tcurrent user. By default, Git will refuse to even parse a Git\n-\tconfig of a repository owned by someone else, let alone run its\n-\thooks, and this config setting allows users to specify exceptions,\n-\te.g. for intentionally shared repositories (see the `--shared`\n-\toption in linkgit:git-init[1]).\n+\t'(Protected config only) ' These config entries specify\n+\tGit-tracked directories that are considered safe even if they\n+\tare owned by someone other than the current user. By default,\n+\tGit will refuse to even parse a Git config of a repository owned\n+\tby someone else, let alone run its hooks, and this config\n+\tsetting allows users to specify exceptions, e.g. for\n+\tintentionally shared repositories (see the `--shared` option in\n+\tlinkgit:git-init[1]).\n +\n This is a multi-valued setting, i.e. you can add more than one directory\n via `git config --add`. To reset the list of safe directories (e.g. to\n override any such directories specified in the system config), add a\n `safe.directory` entry with an empty value.\n +\n-This config setting is only respected when specified in a system or global\n-config, not when it is specified in a repository config, via the command\n-line option `-c safe.directory=<path>`, or in environment variables.\n-+\n The value of this setting is interpolated, i.e. `~/<path>` expands to a\n path relative to the home directory and `%(prefix)/<path>` expands to a\n path relative to Git's (runtime) prefix.\ndiff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt\nindex aa2f41f5e70..a669983abd6 100644\n--- a/Documentation/glossary-content.txt\n+++ b/Documentation/glossary-content.txt\n@@ -483,6 +483,24 @@ exclude;;\n \thead ref. If the remote <<def_head,head>> is not an\n \tancestor to the local head, the push fails.\n \n+[[def_protected_config]]protected configuration::\n+\tProtected configuration is configuration that Git considers more\n+\ttrustworthy because it is unlikely to be tampered with by an\n+\tattacker. For security reasons, some configuration variables are\n+\tonly respected when they are defined in protected configuration.\n++\n+Protected configuration includes:\n++\n+- system-level config, e.g. `/etc/git/config`\n+- global config, e.g. `$XDG_CONFIG_HOME/git/config` and\n+  `$HOME/.gitconfig`\n++\n+Protected configuration excludes:\n++\n+- repository config, e.g. `$GIT_DIR/config` and\n+  `$GIT_DIR/config.worktree`\n+- the command line option `-c` and its equivalent environment variables\n+\n [[def_reachable]]reachable::\n \tAll of the ancestors of a given <<def_commit,commit>> are said to be\n \t\"reachable\" from that commit. More\n-- \ngitgitgadget\n\n"},{"id":"456325","messageId":"7499a2809615d42eaf3649e1c33f38d099d27c1a.1653685761.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v3.git.git.1653685761.gitgitgadget@gmail.com","subject":"[PATCH v3 2/5] config: read protected config with `git_protected_config()`","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-05-27T21:09:18Z","receivedAt":"2022-05-27T21:09:34Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nProtected config is read using `read_very_early_config()`, which has\nseveral downsides:\n\n- Every call to `read_very_early_config()` parses global and\n  system-level config files anew, but this can be optimized by just\n  parsing them once [1].\n- Protected variables should respect \"-c\" because we can reasonably\n  assume that it comes from the user. But, `read_very_early_config()`\n  can't use \"-c\" because it is called so early that it does not have\n  access to command line arguments.\n\nIntroduce `git_protected_config()`, which reads protected config and\ncaches the values in `the_repository.protected_config`. Then, refactor\n`safe.directory` to use `git_protected_config()`.\n\nThis implementation can still be improved, however:\n\n- `git_protected_config()` iterates through every variable in\n  `the_repository.protected_config`, which may still be too expensive to\n  be called in every \"git\" invocation. There exist constant time lookup\n  functions for non-protected config (repo_config_get_*()), but for\n  simplicity, this commit does not implement similar functions for\n  protected config.\n\n- Protected config is stored in `the_repository` so that we don't need\n  to statically allocate it. But this might be confusing since protected\n  config ignores repository config by definition.\n\n[1] While `git_protected_config()` should save on file I/O, I wasn't\nable to measure a meaningful difference between that and\n`read_very_early_config()` on my machine (which has an SSD).\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n config.c     | 35 +++++++++++++++++++++++++++++++++++\n config.h     |  8 ++++++++\n repository.c |  5 +++++\n repository.h |  8 ++++++++\n setup.c      |  2 +-\n 5 files changed, 57 insertions(+), 1 deletion(-)\n\ndiff --git a/config.c b/config.c\nindex fa471dbdb89..c30bb7c5d09 100644\n--- a/config.c\n+++ b/config.c\n@@ -2614,6 +2614,41 @@ int repo_config_get_pathname(struct repository *repo,\n \treturn ret;\n }\n \n+/* Read protected config into the_repository->protected_config. */\n+static void read_protected_config(void)\n+{\n+\tchar *xdg_config = NULL, *user_config = NULL, *system_config = NULL;\n+\n+\tCALLOC_ARRAY(the_repository->protected_config, 1);\n+\tgit_configset_init(the_repository->protected_config);\n+\n+\tsystem_config = git_system_config();\n+\tgit_global_config(&user_config, &xdg_config);\n+\n+\tgit_configset_add_file(the_repository->protected_config, system_config);\n+\tgit_configset_add_file(the_repository->protected_config, xdg_config);\n+\tgit_configset_add_file(the_repository->protected_config, user_config);\n+\n+\tfree(system_config);\n+\tfree(xdg_config);\n+\tfree(user_config);\n+}\n+\n+/* Ensure that the_repository->protected_config has been initialized. */\n+static void git_protected_config_check_init(void)\n+{\n+\tif (the_repository->protected_config &&\n+\t    the_repository->protected_config->hash_initialized)\n+\t\treturn;\n+\tread_protected_config();\n+}\n+\n+void git_protected_config(config_fn_t fn, void *data)\n+{\n+\tgit_protected_config_check_init();\n+\tconfigset_iter(the_repository->protected_config, fn, data);\n+}\n+\n /* Functions used historically to read configuration from 'the_repository' */\n void git_config(config_fn_t fn, void *data)\n {\ndiff --git a/config.h b/config.h\nindex 7654f61c634..411965f52b5 100644\n--- a/config.h\n+++ b/config.h\n@@ -505,6 +505,14 @@ int repo_config_get_maybe_bool(struct repository *repo,\n int repo_config_get_pathname(struct repository *repo,\n \t\t\t     const char *key, const char **dest);\n \n+/*\n+ * Functions for reading protected config. By definition, protected\n+ * config ignores repository config, so it is unnecessary to read\n+ * protected config from any `struct repository` other than\n+ * the_repository.\n+ */\n+void git_protected_config(config_fn_t fn, void *data);\n+\n /**\n  * Querying For Specific Variables\n  * -------------------------------\ndiff --git a/repository.c b/repository.c\nindex 5d166b692c8..ec319a5e09a 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -295,6 +295,11 @@ void repo_clear(struct repository *repo)\n \t\tFREE_AND_NULL(repo->remote_state);\n \t}\n \n+\tif (repo->protected_config) {\n+\t\tgit_configset_clear(repo->protected_config);\n+\t\tFREE_AND_NULL(repo->protected_config);\n+\t}\n+\n \trepo_clear_path_cache(&repo->cached_paths);\n }\n \ndiff --git a/repository.h b/repository.h\nindex 6cc661e5a43..24251aac553 100644\n--- a/repository.h\n+++ b/repository.h\n@@ -126,6 +126,14 @@ struct repository {\n \n \tstruct repo_settings settings;\n \n+\t/*\n+\t * Config that comes from trusted sources, namely\n+\t * - system config files (e.g. /etc/gitconfig)\n+\t * - global config files (e.g. $HOME/.gitconfig,\n+\t *   $XDG_CONFIG_HOME/git)\n+\t */\n+\tstruct config_set *protected_config;\n+\n \t/* Subsystems */\n \t/*\n \t * Repository's config which contains key-value pairs from the usual\ndiff --git a/setup.c b/setup.c\nindex f818dd858c6..847d47f9195 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1128,7 +1128,7 @@ static int ensure_valid_ownership(const char *path)\n \t    is_path_owned_by_current_user(path))\n \t\treturn 1;\n \n-\tread_very_early_config(safe_directory_cb, &data);\n+\tgit_protected_config(safe_directory_cb, &data);\n \n \treturn data.is_safe;\n }\n-- \ngitgitgadget\n\n"},{"id":"456326","messageId":"d5a3e9f98450a0d602cf21790b988c1259a3466d.1653685761.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v3.git.git.1653685761.gitgitgadget@gmail.com","subject":"[PATCH v3 3/5] setup.c: create `discovery.bare`","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-05-27T21:09:19Z","receivedAt":"2022-05-27T21:09:45Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nThere is a known social engineering attack that takes advantage of the\nfact that a working tree can include an entire bare repository,\nincluding a config file. A user could run a Git command inside the bare\nrepository thinking that the config file of the 'outer' repository would\nbe used, but in reality, the bare repository's config file (which is\nattacker-controlled) is used, which may result in arbitrary code\nexecution. See [1] for a fuller description and deeper discussion.\n\nA simple mitigation is to forbid bare repositories unless specified via\n`--git-dir` or `GIT_DIR`. In environments that don't use bare\nrepositories, this would be minimally disruptive.\n\nCreate a config variable, `discovery.bare`, that tells Git whether or\nnot to die() when it discovers a bare repository. This only affects\nrepository discovery, thus it has no effect if discovery was not\ndone (e.g. `--git-dir` was passed).\n\nThis config is an enum of:\n\n- \"always\": always allow bare repositories (this is the default)\n- \"never\": never allow bare repositories\n\nIf we want to protect users from such attacks by default, neither value\nwill suffice - \"always\" provides no protection, but \"never\" is\nimpractical for bare repository users. A more usable default would be to\nallow only non-embedded bare repositories ([2] contains one such\nproposal), but detecting if a repository is embedded is potentially\nnon-trivial, so this work is not implemented in this series.\n\n[1]: https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n[2]: https://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config.txt           |  2 +\n Documentation/config/discovery.txt | 19 +++++++++\n setup.c                            | 66 +++++++++++++++++++++++++++++-\n t/t0035-discovery-bare.sh          | 64 +++++++++++++++++++++++++++++\n 4 files changed, 150 insertions(+), 1 deletion(-)\n create mode 100644 Documentation/config/discovery.txt\n create mode 100755 t/t0035-discovery-bare.sh\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex 07832de1a6c..34133288d75 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -415,6 +415,8 @@ include::config/diff.txt[]\n \n include::config/difftool.txt[]\n \n+include::config/discovery.txt[]\n+\n include::config/extensions.txt[]\n \n include::config/fastimport.txt[]\ndiff --git a/Documentation/config/discovery.txt b/Documentation/config/discovery.txt\nnew file mode 100644\nindex 00000000000..fbe93597e7c\n--- /dev/null\n+++ b/Documentation/config/discovery.txt\n@@ -0,0 +1,19 @@\n+discovery.bare::\n+\t'(Protected config only)' Specifies whether Git will work with a\n+\tbare repository that it found during repository discovery. This\n+\thas no effect if the repository is specified directly via the\n+\t--git-dir command-line option or the GIT_DIR environment\n+\tvariable (see linkgit:git[1]).\n++\n+The currently supported values are:\n++\n+* `always`: Git always works with bare repositories\n+* `never`: Git never works with bare repositories\n++\n+This defaults to `always`, but this default may change in the future.\n++\n+If you do not use bare repositories in your workflow, then it may be\n+beneficial to set `discovery.bare` to `never` in your global config.\n+This will protect you from attacks that involve cloning a repository\n+that contains a bare repository and running a Git command within that\n+directory.\ndiff --git a/setup.c b/setup.c\nindex 847d47f9195..6686743ab7d 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -10,6 +10,13 @@\n static int inside_git_dir = -1;\n static int inside_work_tree = -1;\n static int work_tree_config_is_bogus;\n+enum discovery_bare_config {\n+\tDISCOVERY_BARE_UNKNOWN = -1,\n+\tDISCOVERY_BARE_NEVER = 0,\n+\tDISCOVERY_BARE_ALWAYS,\n+};\n+static enum discovery_bare_config discovery_bare_config =\n+\tDISCOVERY_BARE_UNKNOWN;\n \n static struct startup_info the_startup_info;\n struct startup_info *startup_info = &the_startup_info;\n@@ -1133,6 +1140,52 @@ static int ensure_valid_ownership(const char *path)\n \treturn data.is_safe;\n }\n \n+static int discovery_bare_cb(const char *key, const char *value, void *d)\n+{\n+\tif (strcmp(key, \"discovery.bare\"))\n+\t\treturn 0;\n+\n+\tif (!strcmp(value, \"never\")) {\n+\t\tdiscovery_bare_config = DISCOVERY_BARE_NEVER;\n+\t\treturn 0;\n+\t}\n+\tif (!strcmp(value, \"always\")) {\n+\t\tdiscovery_bare_config = DISCOVERY_BARE_ALWAYS;\n+\t\treturn 0;\n+\t}\n+\treturn -1;\n+}\n+\n+static int check_bare_repo_allowed(void)\n+{\n+\tif (discovery_bare_config == DISCOVERY_BARE_UNKNOWN) {\n+\t\tdiscovery_bare_config = DISCOVERY_BARE_ALWAYS;\n+\t\tgit_protected_config(discovery_bare_cb, NULL);\n+\t}\n+\tswitch (discovery_bare_config) {\n+\tcase DISCOVERY_BARE_NEVER:\n+\t\treturn 0;\n+\tcase DISCOVERY_BARE_ALWAYS:\n+\t\treturn 1;\n+\tcase DISCOVERY_BARE_UNKNOWN:\n+\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n+\t}\n+\treturn 0;\n+}\n+\n+static const char *discovery_bare_config_to_string(void)\n+{\n+\tswitch (discovery_bare_config) {\n+\tcase DISCOVERY_BARE_NEVER:\n+\t\treturn \"never\";\n+\tcase DISCOVERY_BARE_ALWAYS:\n+\t\treturn \"always\";\n+\tcase DISCOVERY_BARE_UNKNOWN:\n+\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n+\t}\n+\treturn NULL;\n+}\n+\n enum discovery_result {\n \tGIT_DIR_NONE = 0,\n \tGIT_DIR_EXPLICIT,\n@@ -1142,7 +1195,8 @@ enum discovery_result {\n \tGIT_DIR_HIT_CEILING = -1,\n \tGIT_DIR_HIT_MOUNT_POINT = -2,\n \tGIT_DIR_INVALID_GITFILE = -3,\n-\tGIT_DIR_INVALID_OWNERSHIP = -4\n+\tGIT_DIR_INVALID_OWNERSHIP = -4,\n+\tGIT_DIR_DISALLOWED_BARE = -5,\n };\n \n /*\n@@ -1239,6 +1293,8 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n \t\t}\n \n \t\tif (is_git_directory(dir->buf)) {\n+\t\t\tif (!check_bare_repo_allowed())\n+\t\t\t\treturn GIT_DIR_DISALLOWED_BARE;\n \t\t\tif (!ensure_valid_ownership(dir->buf))\n \t\t\t\treturn GIT_DIR_INVALID_OWNERSHIP;\n \t\t\tstrbuf_addstr(gitdir, \".\");\n@@ -1385,6 +1441,14 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \t\t}\n \t\t*nongit_ok = 1;\n \t\tbreak;\n+\tcase GIT_DIR_DISALLOWED_BARE:\n+\t\tif (!nongit_ok) {\n+\t\t\tdie(_(\"cannot use bare repository '%s' (discovery.bare is '%s')\"),\n+\t\t\t    dir.buf,\n+\t\t\t    discovery_bare_config_to_string());\n+\t\t}\n+\t\t*nongit_ok = 1;\n+\t\tbreak;\n \tcase GIT_DIR_NONE:\n \t\t/*\n \t\t * As a safeguard against setup_git_directory_gently_1 returning\ndiff --git a/t/t0035-discovery-bare.sh b/t/t0035-discovery-bare.sh\nnew file mode 100755\nindex 00000000000..94c2f76d774\n--- /dev/null\n+++ b/t/t0035-discovery-bare.sh\n@@ -0,0 +1,64 @@\n+#!/bin/sh\n+\n+test_description='verify discovery.bare checks'\n+\n+. ./test-lib.sh\n+\n+pwd=\"$(pwd)\"\n+\n+expect_rejected () {\n+\ttest_must_fail git rev-parse --git-dir 2>err &&\n+\tgrep \"discovery.bare\" err\n+}\n+\n+test_expect_success 'setup bare repo in worktree' '\n+\tgit init outer-repo &&\n+\tgit init --bare outer-repo/bare-repo\n+'\n+\n+test_expect_success 'discovery.bare unset' '\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\tgit rev-parse --git-dir\n+\t)\n+'\n+\n+test_expect_success 'discovery.bare=always' '\n+\tgit config --global discovery.bare always &&\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\tgit rev-parse --git-dir\n+\t)\n+'\n+\n+test_expect_success 'discovery.bare=never' '\n+\tgit config --global discovery.bare never &&\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\texpect_rejected\n+\t)\n+'\n+\n+test_expect_success 'discovery.bare in the repository' '\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\t# Temporarily set discovery.bare=always, otherwise git\n+\t\t# config fails with \"fatal: not in a git directory\"\n+\t\t# (like safe.directory)\n+\t\tgit config --global discovery.bare always &&\n+\t\tgit config discovery.bare always &&\n+\t\tgit config --global discovery.bare never &&\n+\t\texpect_rejected\n+\t)\n+'\n+\n+test_expect_success 'discovery.bare on the command line' '\n+\tgit config --global discovery.bare never &&\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\ttest_must_fail git -c discovery.bare=always rev-parse --git-dir 2>err &&\n+\t\tgrep \"discovery.bare\" err\n+\t)\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"456327","messageId":"e25d5907cd1e3894f19ffbfb3310175fd660563b.1653685761.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v3.git.git.1653685761.gitgitgadget@gmail.com","subject":"[PATCH v3 5/5] upload-pack: make uploadpack.packObjectsHook protected","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-05-27T21:09:21Z","receivedAt":"2022-05-27T21:09:46Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nNow that protected config includes \"-c\", \"uploadpack.packObjectsHook\"\nbehaves identically to a 'Protected config only' variable. Refactor it\nto use git_protected_config() and mark it 'Protected config only'.\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config/uploadpack.txt | 22 +++++++++-------------\n upload-pack.c                       | 17 +++++++++++------\n 2 files changed, 20 insertions(+), 19 deletions(-)\n\ndiff --git a/Documentation/config/uploadpack.txt b/Documentation/config/uploadpack.txt\nindex 32fad5bbe81..57e5e021323 100644\n--- a/Documentation/config/uploadpack.txt\n+++ b/Documentation/config/uploadpack.txt\n@@ -39,19 +39,15 @@ uploadpack.keepAlive::\n \tdisables keepalive packets entirely. The default is 5 seconds.\n \n uploadpack.packObjectsHook::\n-\tIf this option is set, when `upload-pack` would run\n-\t`git pack-objects` to create a packfile for a client, it will\n-\trun this shell command instead.  The `pack-objects` command and\n-\targuments it _would_ have run (including the `git pack-objects`\n-\tat the beginning) are appended to the shell command. The stdin\n-\tand stdout of the hook are treated as if `pack-objects` itself\n-\twas run. I.e., `upload-pack` will feed input intended for\n-\t`pack-objects` to the hook, and expects a completed packfile on\n-\tstdout.\n-+\n-Note that this configuration variable is ignored if it is seen in the\n-repository-level config (this is a safety measure against fetching from\n-untrusted repositories).\n+\t'(Protected config only)' If this option is set, when\n+\t`upload-pack` would run `git pack-objects` to create a packfile\n+\tfor a client, it will run this shell command instead. The\n+\t`pack-objects` command and arguments it _would_ have run\n+\t(including the `git pack-objects` at the beginning) are appended\n+\tto the shell command. The stdin and stdout of the hook are\n+\ttreated as if `pack-objects` itself was run. I.e., `upload-pack`\n+\twill feed input intended for `pack-objects` to the hook, and\n+\texpects a completed packfile on stdout.\n \n uploadpack.allowFilter::\n \tIf this option is set, `upload-pack` will support partial\ndiff --git a/upload-pack.c b/upload-pack.c\nindex 3a851b36066..2a39391369d 100644\n--- a/upload-pack.c\n+++ b/upload-pack.c\n@@ -1321,18 +1321,21 @@ static int upload_pack_config(const char *var, const char *value, void *cb_data)\n \t\tdata->advertise_sid = git_config_bool(var, value);\n \t}\n \n-\tif (current_config_scope() != CONFIG_SCOPE_LOCAL &&\n-\t    current_config_scope() != CONFIG_SCOPE_WORKTREE) {\n-\t\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n-\t\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n-\t}\n-\n \tif (parse_object_filter_config(var, value, data) < 0)\n \t\treturn -1;\n \n \treturn parse_hide_refs_config(var, value, \"uploadpack\");\n }\n \n+static int upload_pack_protected_config(const char *var, const char *value, void *cb_data)\n+{\n+\tstruct upload_pack_data *data = cb_data;\n+\n+\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n+\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n+\treturn 0;\n+}\n+\n void upload_pack(const int advertise_refs, const int stateless_rpc,\n \t\t const int timeout)\n {\n@@ -1342,6 +1345,7 @@ void upload_pack(const int advertise_refs, const int stateless_rpc,\n \tupload_pack_data_init(&data);\n \n \tgit_config(upload_pack_config, &data);\n+\tgit_protected_config(upload_pack_protected_config, &data);\n \n \tdata.stateless_rpc = stateless_rpc;\n \tdata.timeout = timeout;\n@@ -1697,6 +1701,7 @@ int upload_pack_v2(struct repository *r, struct packet_reader *request)\n \tdata.use_sideband = LARGE_PACKET_MAX;\n \n \tgit_config(upload_pack_config, &data);\n+\tgit_protected_config(upload_pack_protected_config, &data);\n \n \twhile (state != FETCH_DONE) {\n \t\tswitch (state) {\n-- \ngitgitgadget\n"},{"id":"456328","messageId":"66a0a208176094415d7b0ae1f686bdd1638b8a8a.1653685761.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v3.git.git.1653685761.gitgitgadget@gmail.com","subject":"[PATCH v3 4/5] config: include \"-c\" in protected config","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-05-27T21:09:20Z","receivedAt":"2022-05-27T21:09:48Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nProtected config should include the command line (aka \"-c\") because we\ncan be quite certain that this config is specified by the user.\n\nIntroduce a function, `git_configset_add_parameters()`, that adds \"-c\"\nconfig to a config_set, and use it to add \"-c\" to protected config.\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config.txt           |  6 +++---\n Documentation/glossary-content.txt |  2 +-\n config.c                           |  6 ++++++\n config.h                           |  9 +++++++++\n t/t0033-safe-directory.sh          | 24 ++++++++++--------------\n t/t0035-discovery-bare.sh          |  3 +--\n 6 files changed, 30 insertions(+), 20 deletions(-)\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex 34133288d75..f40a3e297ce 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -370,9 +370,9 @@ names do not conflict with those that are used by Git itself and\n other popular tools, and describe them in your documentation.\n \n Variables marked with '(Protected config only)' are only respected when\n-they are specified in protected configuration. This includes global and\n-system-level config, and excludes repository config, the command line\n-option `-c`, and environment variables. For more details, see the\n+they are specified in protected configuration. This includes global,\n+system-level config, the command line option `-c`, and environment\n+variables, and excludes repository config. For more details, see the\n 'protected configuration' entry in linkgit:gitglossary[7].\n \n include::config/advice.txt[]\ndiff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt\nindex a669983abd6..4190c410a00 100644\n--- a/Documentation/glossary-content.txt\n+++ b/Documentation/glossary-content.txt\n@@ -494,12 +494,12 @@ Protected configuration includes:\n - system-level config, e.g. `/etc/git/config`\n - global config, e.g. `$XDG_CONFIG_HOME/git/config` and\n   `$HOME/.gitconfig`\n+- the command line option `-c` and its equivalent environment variables\n +\n Protected configuration excludes:\n +\n - repository config, e.g. `$GIT_DIR/config` and\n   `$GIT_DIR/config.worktree`\n-- the command line option `-c` and its equivalent environment variables\n \n [[def_reachable]]reachable::\n \tAll of the ancestors of a given <<def_commit,commit>> are said to be\ndiff --git a/config.c b/config.c\nindex c30bb7c5d09..22192ca1d63 100644\n--- a/config.c\n+++ b/config.c\n@@ -2373,6 +2373,11 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n \treturn git_config_from_file(config_set_callback, filename, cs);\n }\n \n+int git_configset_add_parameters(struct config_set *cs)\n+{\n+\treturn git_config_from_parameters(config_set_callback, cs);\n+}\n+\n int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n {\n \tconst struct string_list *values = NULL;\n@@ -2628,6 +2633,7 @@ static void read_protected_config(void)\n \tgit_configset_add_file(the_repository->protected_config, system_config);\n \tgit_configset_add_file(the_repository->protected_config, xdg_config);\n \tgit_configset_add_file(the_repository->protected_config, user_config);\n+\tgit_configset_add_parameters(the_repository->protected_config);\n \n \tfree(system_config);\n \tfree(xdg_config);\ndiff --git a/config.h b/config.h\nindex 411965f52b5..e3ff1fcf683 100644\n--- a/config.h\n+++ b/config.h\n@@ -446,6 +446,15 @@ void git_configset_init(struct config_set *cs);\n  */\n int git_configset_add_file(struct config_set *cs, const char *filename);\n \n+/**\n+ * Parses command line options and environment variables, and adds the\n+ * variable-value pairs to the `config_set`. Returns 0 on success, or -1\n+ * if there is an error in parsing. The caller decides whether to free\n+ * the incomplete configset or continue using it when the function\n+ * returns -1.\n+ */\n+int git_configset_add_parameters(struct config_set *cs);\n+\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key` and config set `cs`. When the\ndiff --git a/t/t0033-safe-directory.sh b/t/t0033-safe-directory.sh\nindex 238b25f91a3..5a1cd0d0947 100755\n--- a/t/t0033-safe-directory.sh\n+++ b/t/t0033-safe-directory.sh\n@@ -16,24 +16,20 @@ test_expect_success 'safe.directory is not set' '\n \texpect_rejected_dir\n '\n \n-test_expect_success 'ignoring safe.directory on the command line' '\n-\ttest_must_fail git -c safe.directory=\"$(pwd)\" status 2>err &&\n-\tgrep \"unsafe repository\" err\n+test_expect_success 'safe.directory on the command line' '\n+\tgit -c safe.directory=\"$(pwd)\" status\n '\n \n-test_expect_success 'ignoring safe.directory in the environment' '\n-\ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n-\t\tGIT_CONFIG_KEY_0=\"safe.directory\" \\\n-\t\tGIT_CONFIG_VALUE_0=\"$(pwd)\" \\\n-\t\tgit status 2>err &&\n-\tgrep \"unsafe repository\" err\n+test_expect_success 'safe.directory in the environment' '\n+\tenv GIT_CONFIG_COUNT=1 \\\n+\t    GIT_CONFIG_KEY_0=\"safe.directory\" \\\n+\t    GIT_CONFIG_VALUE_0=\"$(pwd)\" \\\n+\t    git status\n '\n \n-test_expect_success 'ignoring safe.directory in GIT_CONFIG_PARAMETERS' '\n-\ttest_must_fail env \\\n-\t\tGIT_CONFIG_PARAMETERS=\"${SQ}safe.directory${SQ}=${SQ}$(pwd)${SQ}\" \\\n-\t\tgit status 2>err &&\n-\tgrep \"unsafe repository\" err\n+test_expect_success 'safe.directory in GIT_CONFIG_PARAMETERS' '\n+\tenv GIT_CONFIG_PARAMETERS=\"${SQ}safe.directory${SQ}=${SQ}$(pwd)${SQ}\" \\\n+\t    git status\n '\n \n test_expect_success 'ignoring safe.directory in repo config' '\ndiff --git a/t/t0035-discovery-bare.sh b/t/t0035-discovery-bare.sh\nindex 94c2f76d774..0d5983df307 100755\n--- a/t/t0035-discovery-bare.sh\n+++ b/t/t0035-discovery-bare.sh\n@@ -56,8 +56,7 @@ test_expect_success 'discovery.bare on the command line' '\n \tgit config --global discovery.bare never &&\n \t(\n \t\tcd outer-repo/bare-repo &&\n-\t\ttest_must_fail git -c discovery.bare=always rev-parse --git-dir 2>err &&\n-\t\tgrep \"discovery.bare\" err\n+\t\tgit -c discovery.bare=always rev-parse --git-dir\n \t)\n '\n \n-- \ngitgitgadget\n\n"},{"id":"456336","messageId":"xmqqh75a1rmd.fsf@gitster.g","threadId":"57859","inReplyTo":"575676c760d9a2ce4a59d50e93aa0f45d54620ab.1653685761.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 1/5] Documentation: define protected configuration","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-05-27T23:29:46Z","receivedAt":"2022-05-27T23:30:02Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>  safe.directory::\n> -\tThese config entries specify Git-tracked directories that are\n> -\tconsidered safe even if they are owned by someone other than the\n> -\tcurrent user. By default, Git will refuse to even parse a Git\n> -\tconfig of a repository owned by someone else, let alone run its\n> -\thooks, and this config setting allows users to specify exceptions,\n> -\te.g. for intentionally shared repositories (see the `--shared`\n> -\toption in linkgit:git-init[1]).\n> +\t'(Protected config only) ' These config entries specify\n\nWhat's the SP in \"only) '\" doing?\n\n> diff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt\n> index aa2f41f5e70..a669983abd6 100644\n> --- a/Documentation/glossary-content.txt\n> +++ b/Documentation/glossary-content.txt\n> @@ -483,6 +483,24 @@ exclude;;\n>  \thead ref. If the remote <<def_head,head>> is not an\n>  \tancestor to the local head, the push fails.\n>  \n> +[[def_protected_config]]protected configuration::\n> +\tProtected configuration is configuration that Git considers more\n> +\ttrustworthy because it is unlikely to be tampered with by an\n> +\tattacker. For security reasons, some configuration variables are\n> +\tonly respected when they are defined in protected configuration.\n> ++\n> +Protected configuration includes:\n> ++\n> +- system-level config, e.g. `/etc/git/config`\n> +- global config, e.g. `$XDG_CONFIG_HOME/git/config` and\n> +  `$HOME/.gitconfig`\n> +Protected configuration excludes:\n> ++\n> +- repository config, e.g. `$GIT_DIR/config` and\n> +  `$GIT_DIR/config.worktree`\n> +- the command line option `-c` and its equivalent environment variables\n\nThe description is a bit unclear what \"protected configuration\"\nrefers.\n\nIf it is the scopes (as in \"git config --show-scope\") Git can trust\nmore, in other words, a statement like this\n\n    safe.directory is honored only when it comes from a protected\n    configuration.\n\nis what you want to make easier to write by introducing a new\nphrase, perhaps use the word \"scope\" for more consistency?  E.g.\n\n    Only safe.directory that is defined in a trusted scope is\n    honored.\n\nI dunno.\n\nIt would make sense to give a rationale behind the seemingly\narbitrary choice of what is and what is not \"protected\".  Not\nnecessarily in the glossary, but in the proposed log message of the\ncommit that makes the decision.  The rationale must help readers to\nbe able to answer the following questions.\n\n - The system level is \"protected\" because?  Is it because we do not\n   even try to protect ourselves from those who can write anywhere\n   in /etc/ or other system directories?\n\n - The per-user config is \"protected\" because?  Is it because our\n   primary interest in \"protection\" is to protect individual users\n   from landmines laid in the filesystem by other users, and those\n   who can already write into $HOME are not we try to guard against?\n\n - The per-repo config is not \"protected\" (i.e. \"trusted\"), because?\n   If we are not honoring a configuration in the repository, why are\n   we working in that repository in the first place?\n\n - The per invocation config is not \"protected\" (i.e. \"trusted\"),\n   because?  If we cannot trusting our own command line, what\n   prevents an attacker from mucking with our command line to say\n   \"sudo whatever\" using the same attack vector?\n\nThanks.\n"},{"id":"456339","messageId":"xmqqbkvi1owx.fsf@gitster.g","threadId":"57859","inReplyTo":"7499a2809615d42eaf3649e1c33f38d099d27c1a.1653685761.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 2/5] config: read protected config with `git_protected_config()`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-05-28T00:28:14Z","receivedAt":"2022-05-28T00:28:25Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Glen Choo <chooglen@google.com>\n>\n> Protected config is read using `read_very_early_config()`, which has\n> several downsides:\n>\n> - Every call to `read_very_early_config()` parses global and\n>   system-level config files anew, but this can be optimized by just\n>   parsing them once [1].\n> - Protected variables should respect \"-c\" because we can reasonably\n>   assume that it comes from the user. But, `read_very_early_config()`\n>   can't use \"-c\" because it is called so early that it does not have\n>   access to command line arguments.\n\nNow we are talking about protected \"variable\".  Is that a synonym\nfor \"config\", or are there some distinctions between them?\n\n> - Protected config is stored in `the_repository` so that we don't need\n>   to statically allocate it. But this might be confusing since protected\n>   config ignores repository config by definition.\n\nYes, it indeed is.  Is it because we were over-eager when we\nintroduced the \"struct repository *repo\" parameter to many functions\nand the configuration system wants you to have some repository, even\nwhen you know you are not reading from any repository?  \n\nI am wondering if it is a cleaner solution *not* to hang the\nprotected config as a configset in the_repository, but keep the\nconfigset as a separate global variable, perhaps static to config.c\nand is meant to be only accessed via git_protected_config() and the\nlike.\n\n> @@ -295,6 +295,11 @@ void repo_clear(struct repository *repo)\n>  \t\tFREE_AND_NULL(repo->remote_state);\n>  \t}\n>  \n> +\tif (repo->protected_config) {\n> +\t\tgit_configset_clear(repo->protected_config);\n> +\t\tFREE_AND_NULL(repo->protected_config);\n> +\t}\n> +\n\nThis becomes necessary only because each repository instance has\nprotected_config, even though we need only one instance, no matter\nhow many repositories we are accessing in this single invocation of\nGit, no?\n\nHow should \"git config -l\" interact with \"protected config\" and\n\"protected variables\", by the way?  Should a user be able to tell\nwhich ones are coming from protected scope?  Should we gain, next to\n--global, --system, etc., --protected option to list only the\nprotected config/variable?\n\nThis is another thing that I find iffy on terminology.  Should a\nrandom variable, like user.name, be a \"protected config\", if it is\nfound in $HOME/.gitconfig?  If it comes from there, surely we can\ntrust its value, but unlike things like safe.directory, there is no\ncode that wants to enforce that we pay attention only to user.name\nthat came from trusted scopes.  Should such a variable be called\n\"protected variable\"?\n\nThanks.\n"},{"id":"456340","messageId":"xmqqy1ymzd41.fsf@gitster.g","threadId":"57859","inReplyTo":"d5a3e9f98450a0d602cf21790b988c1259a3466d.1653685761.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 3/5] setup.c: create `discovery.bare`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-05-28T00:59:10Z","receivedAt":"2022-05-28T00:59:15Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> +enum discovery_bare_config {\n> +\tDISCOVERY_BARE_UNKNOWN = -1,\n> +\tDISCOVERY_BARE_NEVER = 0,\n> +\tDISCOVERY_BARE_ALWAYS,\n> +};\n> +static enum discovery_bare_config discovery_bare_config =\n> +\tDISCOVERY_BARE_UNKNOWN;\n\nCan discovery_bare come from anywhere other than config?\n\nI am wondering if both the variable and the type should be called\n\"discovery_bare_allowed\" instead.  That it comes from the config is\nnot the more important part.  That it determines if it is allowed\nis.\n\n> +static int check_bare_repo_allowed(void)\n> +{\n> +\tif (discovery_bare_config == DISCOVERY_BARE_UNKNOWN) {\n> +\t\tdiscovery_bare_config = DISCOVERY_BARE_ALWAYS;\n> +\t\tgit_protected_config(discovery_bare_cb, NULL);\n> +\t}\n\nOK, so the thing is initialized to \"unknown\", and the first time we\nwant to use the value of it, we read from the file (or default to\n\"always\").  Makes sense.\n\nAnd then ...\n\n> +\tswitch (discovery_bare_config) {\n> +\tcase DISCOVERY_BARE_NEVER:\n> +\t\treturn 0;\n> +\tcase DISCOVERY_BARE_ALWAYS:\n> +\t\treturn 1;\n> +\tcase DISCOVERY_BARE_UNKNOWN:\n> +\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n\n... this is being defensive; we know discovery_bare_cb() won't give\nUNKNOWN, but we want to make sure.\n\n> +\t}\n> +\treturn 0;\n> +}\n> +\n> +static const char *discovery_bare_config_to_string(void)\n> +{\n\nBut this one feels strangely asymmetrical, as there is no inherent\nreason why one must be called before the other.  I would expect it\nto either\n\n * take a parameter of type \"enum discovery_bare\" and return\n   \"never\", \"always\", or \"unset\", without calling any BUG().\n\nor\n\n * have the same \"we lazily figure out the discovery_bare_config\n   variable on demand\" logic.\n\nAs both of these functions are file-scope static, we can live with\nit, though.\n\n> +\tswitch (discovery_bare_config) {\n> +\tcase DISCOVERY_BARE_NEVER:\n> +\t\treturn \"never\";\n> +\tcase DISCOVERY_BARE_ALWAYS:\n> +\t\treturn \"always\";\n> +\tcase DISCOVERY_BARE_UNKNOWN:\n> +\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n> +\t}\n> +\treturn NULL;\n> +}\n\n"},{"id":"456410","messageId":"kl6lv8tlegy4.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"xmqqbkvi1owx.fsf@gitster.g","subject":"Re: [PATCH v3 2/5] config: read protected config with `git_protected_config()`","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-05-31T17:43:15Z","receivedAt":"2022-05-31T17:43:25Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n>> Protected config is read using `read_very_early_config()`, which has\n>> several downsides:\n>>\n>> - Every call to `read_very_early_config()` parses global and\n>>   system-level config files anew, but this can be optimized by just\n>>   parsing them once [1].\n>> - Protected variables should respect \"-c\" because we can reasonably\n>>   assume that it comes from the user. But, `read_very_early_config()`\n>>   can't use \"-c\" because it is called so early that it does not have\n>>   access to command line arguments.\n>\n> Now we are talking about protected \"variable\".  Is that a synonym\n> for \"config\", or are there some distinctions between them?\n\nSorry, that's an old term I was toying with (this somehow snuck through\nmy proofreading). I just meant \"variable that is only read from\nprotected config\", aka a \"protected config only variable\".\n\nA goal in this version was to introduce as little jargon as possible, so\n- \"protected config\" refers to the set of config sources, and\n- \"protected config only\" refers to config variables/settings that are\n  only read from protected config.\n\n>> - Protected config is stored in `the_repository` so that we don't need\n>>   to statically allocate it. But this might be confusing since protected\n>>   config ignores repository config by definition.\n>\n> Yes, it indeed is.  Is it because we were over-eager when we\n> introduced the \"struct repository *repo\" parameter to many functions\n> and the configuration system wants you to have some repository, even\n> when you know you are not reading from any repository?  \n\nAh no, I was just trying to avoid yet-another global variable (since\nIIRC we want to move towards a more lib-like Git), and the_repository\nwas a convenient global variable to (ab)use.\n\n> I am wondering if it is a cleaner solution *not* to hang the\n> protected config as a configset in the_repository, but keep the\n> configset as a separate global variable, perhaps static to config.c\n> and is meant to be only accessed via git_protected_config() and the\n> like.\n\nI think your suggestion to use a global variable is better, as much as I\nwant to avoid another global variable. Protected config would affect any\nrepositories that we work with in-core, so using a global sounds ok.\n\nenvironment.c might be a better place since we already make a concerted\neffort to put global config variables there instead of config.c.\n\nAs an aside, I wonder how we could get rid of all of the globals in\nenvironment.c in the long term. Maybe we would have yet-another all\nencompassing global, the_environment, and then figure out which\nvariables belong to the repository and which belong to the environment.\n\n>> @@ -295,6 +295,11 @@ void repo_clear(struct repository *repo)\n>>  \t\tFREE_AND_NULL(repo->remote_state);\n>>  \t}\n>>  \n>> +\tif (repo->protected_config) {\n>> +\t\tgit_configset_clear(repo->protected_config);\n>> +\t\tFREE_AND_NULL(repo->protected_config);\n>> +\t}\n>> +\n>\n> This becomes necessary only because each repository instance has\n> protected_config, even though we need only one instance, no matter\n> how many repositories we are accessing in this single invocation of\n> Git, no?\n\nYes.\n\n> How should \"git config -l\" interact with \"protected config\" and\n> \"protected variables\", by the way?  Should a user be able to tell\n> which ones are coming from protected scope?  Should we gain, next to\n> --global, --system, etc., --protected option to list only the\n> protected config/variable?\n\nI'll have to think about this some more. My initial thoughts are that we\nshould do this if we formalize 'protected' as a scope-like concept, but\nI don't see the lack of \"--protected\" as a significant hindrance to\nusers because they can use \"--global\" and \"--system\" (albeit in two\ninvocations instead of one).\n\n> This is another thing that I find iffy on terminology.  Should a\n> random variable, like user.name, be a \"protected config\", if it is\n> found in $HOME/.gitconfig?  If it comes from there, surely we can\n> trust its value, but unlike things like safe.directory, there is no\n> code that wants to enforce that we pay attention only to user.name\n> that came from trusted scopes.  Should such a variable be called\n> \"protected variable\"?\n\nAh.. I think it would be best to pretend that the \"Protected variable\"\ntypo never happened. That term was destined to be confusing and\nmeaningless.\n\nInstead, we can use \"protected config\" to refer to the config and\n\"protected config only\" to refer to variables. Since \"protected config\"\nis defined as (global + system + CLI) config, then yes, we would say\nthat it is \"protected config\". But since we do not enforce that\n\"user.name\" _must_ come from only protected config, it is not \"protected\nconfig only\".\n"},{"id":"456449","messageId":"xmqqwne0tlxe.fsf@gitster.g","threadId":"57859","inReplyTo":"kl6lv8tlegy4.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v3 2/5] config: read protected config with `git_protected_config()`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-06-01T15:58:53Z","receivedAt":"2022-06-01T15:58:59Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Glen Choo <chooglen@google.com> writes:\n\n> A goal in this version was to introduce as little jargon as possible, so\n> - \"protected config\" refers to the set of config sources, and\n> - \"protected config only\" refers to config variables/settings that are\n>   only read from protected config.\n\nOK.  Let's have such a clear pair of definitions somewhere in the\ndoc or at least in a proposed log message.\n\n>\n>>> - Protected config is stored in `the_repository` so that we don't need\n>>>   to statically allocate it. But this might be confusing since protected\n>>>   config ignores repository config by definition.\n>>\n>> Yes, it indeed is.  Is it because we were over-eager when we\n>> introduced the \"struct repository *repo\" parameter to many functions\n>> and the configuration system wants you to have some repository, even\n>> when you know you are not reading from any repository?  \n>\n> Ah no, I was just trying to avoid yet-another global variable (since\n> IIRC we want to move towards a more lib-like Git), and the_repository\n> was a convenient global variable to (ab)use.\n\nIf this does not have to be known only inside config.c, until we\nintroduce a more global bag of things, which may have the current\nthe_repository as one of its components, I do not think it hurts to\nhave a file-scope static there.  Then, perhaps git_configset_get*()\nhelper functions can recognize cs==NULL as a sign that the caller\nwants to grab from the \"protected config\", or something?  If we do\nnot want to expose the underying global variable to the public, that\nis.\n\n> As an aside, I wonder how we could get rid of all of the globals in\n> environment.c in the long term. Maybe we would have yet-another all\n> encompassing global, the_environment, and then figure out which\n> variables belong to the repository and which belong to the environment.\n\nI think we are on the same page, we'd probably need something called\nthe_world ;-)\n\n> Instead, we can use \"protected config\" to refer to the config and\n> \"protected config only\" to refer to variables. Since \"protected config\"\n> is defined as (global + system + CLI) config, then yes, we would say\n> that it is \"protected config\". But since we do not enforce that\n> \"user.name\" _must_ come from only protected config, it is not \"protected\n> config only\".\n\nVery clear.  Thanks.\n"},{"id":"456502","messageId":"2ed00ece-f8bb-c84c-0684-494692a71da2@github.com","threadId":"57859","inReplyTo":"xmqqh75a1rmd.fsf@gitster.g","subject":"Re: [PATCH v3 1/5] Documentation: define protected configuration","fromName":"Derrick Stolee","fromEmail":"derrickstolee@github.com","sentAt":"2022-06-02T12:42:30Z","receivedAt":"2022-06-02T12:42:36Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 5/27/2022 7:29 PM, Junio C Hamano wrote:\n> \"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>> +[[def_protected_config]]protected configuration::\n>> +\tProtected configuration is configuration that Git considers more\n>> +\ttrustworthy because it is unlikely to be tampered with by an\n>> +\tattacker. For security reasons, some configuration variables are\n>> +\tonly respected when they are defined in protected configuration.\n>> ++\n>> +Protected configuration includes:\n>> ++\n>> +- system-level config, e.g. `/etc/git/config`\n>> +- global config, e.g. `$XDG_CONFIG_HOME/git/config` and\n>> +  `$HOME/.gitconfig`\n>> +Protected configuration excludes:\n>> ++\n>> +- repository config, e.g. `$GIT_DIR/config` and\n>> +  `$GIT_DIR/config.worktree`\n>> +- the command line option `-c` and its equivalent environment variables\n> \n> The description is a bit unclear what \"protected configuration\"\n> refers.\n> \n> If it is the scopes (as in \"git config --show-scope\") Git can trust\n> more, in other words, a statement like this\n> \n>     safe.directory is honored only when it comes from a protected\n>     configuration.\n> \n> is what you want to make easier to write by introducing a new\n> phrase, perhaps use the word \"scope\" for more consistency?  E.g.\n> \n>     Only safe.directory that is defined in a trusted scope is\n>     honored.\n> \n> I dunno.\n> \n> It would make sense to give a rationale behind the seemingly\n> arbitrary choice of what is and what is not \"protected\".  Not\n> necessarily in the glossary, but in the proposed log message of the\n> commit that makes the decision.  The rationale must help readers to\n> be able to answer the following questions.\n> \n>  - The system level is \"protected\" because?  Is it because we do not\n>    even try to protect ourselves from those who can write anywhere\n>    in /etc/ or other system directories?\n> \n>  - The per-user config is \"protected\" because?  Is it because our\n>    primary interest in \"protection\" is to protect individual users\n>    from landmines laid in the filesystem by other users, and those\n>    who can already write into $HOME are not we try to guard against?\n\nI think the answers to these two questions is \"yes\", so they can\nbe turned into an affirmative sentence:\n\n\tWe do not event try to protect ourselves from those who can\n\twrite anywhere...\n\n>  - The per-repo config is not \"protected\" (i.e. \"trusted\"), because?\n>    If we are not honoring a configuration in the repository, why are\n>    we working in that repository in the first place?\n\nThis requires an example:\n\n\tSome workflows use repositories stored in shared directories,\n\twhich are writable by multiple unprivileged users.\n \n>  - The per invocation config is not \"protected\" (i.e. \"trusted\"),\n>    because?  If we cannot trusting our own command line, what\n>    prevents an attacker from mucking with our command line to say\n>    \"sudo whatever\" using the same attack vector?\n\nWith this argument, I agree that -c config can be considered\nprotected. At the very least, it is visible to the user when they\nare running a command. This would unify our expectations with\nuploadPack.packObjectsHook, too.\n\nThanks,\n-Stolee\n"},{"id":"456503","messageId":"802c3541-3301-43fc-c39e-edd44e61a4eb@github.com","threadId":"57859","inReplyTo":"7499a2809615d42eaf3649e1c33f38d099d27c1a.1653685761.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 2/5] config: read protected config with `git_protected_config()`","fromName":"Derrick Stolee","fromEmail":"derrickstolee@github.com","sentAt":"2022-06-02T12:56:56Z","receivedAt":"2022-06-02T12:57:09Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 5/27/2022 5:09 PM, Glen Choo via GitGitGadget wrote:\n> From: Glen Choo <chooglen@google.com>\n> \n> Protected config is read using `read_very_early_config()`, which has\n> several downsides:\n> \n> - Every call to `read_very_early_config()` parses global and\n>   system-level config files anew, but this can be optimized by just\n>   parsing them once [1].\n> - Protected variables should respect \"-c\" because we can reasonably\n>   assume that it comes from the user. But, `read_very_early_config()`\n>   can't use \"-c\" because it is called so early that it does not have\n>   access to command line arguments.\n> \n> Introduce `git_protected_config()`, which reads protected config and\n> caches the values in `the_repository.protected_config`. Then, refactor\n> `safe.directory` to use `git_protected_config()`.\n> \n> This implementation can still be improved, however:\n> \n> - `git_protected_config()` iterates through every variable in\n>   `the_repository.protected_config`, which may still be too expensive to\n>   be called in every \"git\" invocation. There exist constant time lookup\n>   functions for non-protected config (repo_config_get_*()), but for\n>   simplicity, this commit does not implement similar functions for\n>   protected config.\n\nI originally thought that we should jump to that \"right\" solution, but\nthe existing logic in ensure_valid_ownership() uses the iterator method,\nmostly because it uses a multi-valued string. There are helpers that\nallow iterating over a specific multi-valued key, but there is no reason\nto complicate the current patch with that amount of refactoring. That\ncan be handled as a completely separate topic.\n \n> - Protected config is stored in `the_repository` so that we don't need\n>   to statically allocate it. But this might be confusing since protected\n>   config ignores repository config by definition.\n\nI agree with Junio's suggestion of keeping this as a static global in\nconfig.c, accessible only by the public methods from config.h. A future\nwhere we have \"the_world\" might be nice for inventory on all these\nglobals. Definitely not something to hold up this series.\n\n> +/* Read protected config into the_repository->protected_config. */\n> +static void read_protected_config(void)\n> +{\n> +\tchar *xdg_config = NULL, *user_config = NULL, *system_config = NULL;\n> +\n> +\tCALLOC_ARRAY(the_repository->protected_config, 1);\n> +\tgit_configset_init(the_repository->protected_config);\n> +\n> +\tsystem_config = git_system_config();\n> +\tgit_global_config(&user_config, &xdg_config);\n> +\n> +\tgit_configset_add_file(the_repository->protected_config, system_config);\n> +\tgit_configset_add_file(the_repository->protected_config, xdg_config);\n> +\tgit_configset_add_file(the_repository->protected_config, user_config);\n> +\n> +\tfree(system_config);\n> +\tfree(xdg_config);\n> +\tfree(user_config);\n> +}\n\nThis loads the config from three files, including the xdg_config, which\nI wasn't thinking about before.\n\nThis implementation does not use the -c config yet, which you listed as\na downside of read_very_early_config(). I see that you include that in\nyour patch 4, but the commit message for this patch could list that as a\nstep that will be handled by a later change.\n\n(You could also do that as patch 3 and add a test near the existing\nsafe.directory tests instead of waiting for discovery.bare.)\n\n> +\n> +/* Ensure that the_repository->protected_config has been initialized. */\n> +static void git_protected_config_check_init(void)\n> +{\n> +\tif (the_repository->protected_config &&\n> +\t    the_repository->protected_config->hash_initialized)\n> +\t\treturn;\n> +\tread_protected_config();\n> +}\n> +\n> +void git_protected_config(config_fn_t fn, void *data)\n> +{\n> +\tgit_protected_config_check_init();\n> +\tconfigset_iter(the_repository->protected_config, fn, data);\n> +}\n\nThese two methods are clearly correct.\n\n..._check_init() is an OK name. I've seen us use \"prepare_...()\" in\nother areas as a way of making sure that we have the proper state\n(see prepare_packed_git() and the like), so maybe a rename here to\nmatch would be worthwhile. Feel free to ignore.\n\n> +\tif (repo->protected_config) {\n> +\t\tgit_configset_clear(repo->protected_config);\n> +\t\tFREE_AND_NULL(repo->protected_config);\n> +\t}\n\nThis will have no equivalent when protected_config is left as a\nstatic global, but that is fine. It only goes out of scope with\nthe end of the process, anyway.\n\n> @@ -1128,7 +1128,7 @@ static int ensure_valid_ownership(const char *path)\n>  \t    is_path_owned_by_current_user(path))\n>  \t\treturn 1;\n>  \n> -\tread_very_early_config(safe_directory_cb, &data);\n> +\tgit_protected_config(safe_directory_cb, &data);\n\nNice to have a very simple conversion here.\n\nThanks,\n-Stolee\n"},{"id":"456504","messageId":"7b37f3b7-58c5-1ac5-46eb-d995dc3cc33b@github.com","threadId":"57859","inReplyTo":"d5a3e9f98450a0d602cf21790b988c1259a3466d.1653685761.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 3/5] setup.c: create `discovery.bare`","fromName":"Derrick Stolee","fromEmail":"derrickstolee@github.com","sentAt":"2022-06-02T13:11:30Z","receivedAt":"2022-06-02T13:11:39Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 5/27/2022 5:09 PM, Glen Choo via GitGitGadget wrote:\n> +enum discovery_bare_config {\n> +\tDISCOVERY_BARE_UNKNOWN = -1,\n> +\tDISCOVERY_BARE_NEVER = 0,\n> +\tDISCOVERY_BARE_ALWAYS,\n> +};\n> +static enum discovery_bare_config discovery_bare_config =\n> +\tDISCOVERY_BARE_UNKNOWN;\n\nUsing this static global is fine, I think.\n\n> +static int discovery_bare_cb(const char *key, const char *value, void *d)\n> +{\n> +\tif (strcmp(key, \"discovery.bare\"))\n> +\t\treturn 0;\n> +\n> +\tif (!strcmp(value, \"never\")) {\n> +\t\tdiscovery_bare_config = DISCOVERY_BARE_NEVER;\n> +\t\treturn 0;\n> +\t}\n> +\tif (!strcmp(value, \"always\")) {\n> +\t\tdiscovery_bare_config = DISCOVERY_BARE_ALWAYS;\n> +\t\treturn 0;\n> +\t}\n> +\treturn -1;\n> +}\n\nHowever, I do think that this _cb method could benefit from interpreting\nthe 'd' pointer as a 'enum discovery_bare_config *' and assigning the\nvalue at the pointer. We can then pass the global to the\ngit_protected_config() call below.\n\nThis is probably over-defensive future-proofing, but this kind of change\nwould be necessary if we ever wanted to return the enum instead of\nsimply an integer, as below:\n\n> +\n> +static int check_bare_repo_allowed(void)\n> +{\n> +\tif (discovery_bare_config == DISCOVERY_BARE_UNKNOWN) {\n> +\t\tdiscovery_bare_config = DISCOVERY_BARE_ALWAYS;\n> +\t\tgit_protected_config(discovery_bare_cb, NULL);\n> +\t}\n> +\tswitch (discovery_bare_config) {\n> +\tcase DISCOVERY_BARE_NEVER:\n> +\t\treturn 0;\n> +\tcase DISCOVERY_BARE_ALWAYS:\n> +\t\treturn 1;\n> +\tcase DISCOVERY_BARE_UNKNOWN:\n> +\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n> +\t}\n> +\treturn 0;\n> +}\n\nWith the recommended change to the _cb method, we could rewrite this as\n\nstatic enum discovery_bare_config get_discovery_bare(void)\n{\n\tenum discovery_bare_config result = DISCOVERY_BARE_ALWAYS;\n\tgit_protected_config(discovery_bare_cb, &result);\n\treturn result;\n}\n\nWith this, we can drop the UNKNOWN and let the caller treat the response\nas a simple boolean.\n\nI think this is simpler overall, but also makes it easier to extend in the\nfuture to have \"discovery.bare=non-embedded\" by adding a new mode and\nadjusting the consumer in setup_git_directory_gently_1() to use a switch()\non the resurned enum.\n\n> +\n> +static const char *discovery_bare_config_to_string(void)\n> +{\n> +\tswitch (discovery_bare_config) {\n> +\tcase DISCOVERY_BARE_NEVER:\n> +\t\treturn \"never\";\n> +\tcase DISCOVERY_BARE_ALWAYS:\n> +\t\treturn \"always\";\n> +\tcase DISCOVERY_BARE_UNKNOWN:\n> +\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n\nThis case should be a \"default:\" in case somehow an arbitrary integer\nvalue was placed in the variable. This could also take an enum as a\nparameter, to avoid being coupled to the global.\n\n> +++ b/t/t0035-discovery-bare.sh\n> @@ -0,0 +1,64 @@\n> +#!/bin/sh\n> +\n> +test_description='verify discovery.bare checks'\n> +\n> +. ./test-lib.sh\n> +\n> +pwd=\"$(pwd)\"\n> +\n> +expect_rejected () {\n> +\ttest_must_fail git rev-parse --git-dir 2>err &&\n> +\tgrep \"discovery.bare\" err\n> +}\n\nShould we make a simple \"expect_accepted\" helper in case we ever\nwant to replace the \"git rev-parse --git-dir\" with anything else?\n\n> +\n> +test_expect_success 'setup bare repo in worktree' '\n> +\tgit init outer-repo &&\n> +\tgit init --bare outer-repo/bare-repo\n> +'\n> +\n> +test_expect_success 'discovery.bare unset' '\n> +\t(\n> +\t\tcd outer-repo/bare-repo &&\n> +\t\tgit rev-parse --git-dir\n> +\t)\n> +'\n> +\n> +test_expect_success 'discovery.bare=always' '\n> +\tgit config --global discovery.bare always &&\n> +\t(\n> +\t\tcd outer-repo/bare-repo &&\n> +\t\tgit rev-parse --git-dir\n> +\t)\n> +'\n> +\n> +test_expect_success 'discovery.bare=never' '\n> +\tgit config --global discovery.bare never &&\n> +\t(\n> +\t\tcd outer-repo/bare-repo &&\n> +\t\texpect_rejected\n> +\t)\n> +'\n> +\n> +test_expect_success 'discovery.bare in the repository' '\n> +\t(\n> +\t\tcd outer-repo/bare-repo &&\n> +\t\t# Temporarily set discovery.bare=always, otherwise git\n> +\t\t# config fails with \"fatal: not in a git directory\"\n> +\t\t# (like safe.directory)\n> +\t\tgit config --global discovery.bare always &&\n> +\t\tgit config discovery.bare always &&\n> +\t\tgit config --global discovery.bare never &&\n> +\t\texpect_rejected\n> +\t)\n> +'\n> +\n> +test_expect_success 'discovery.bare on the command line' '\n> +\tgit config --global discovery.bare never &&> +\t(\n> +\t\tcd outer-repo/bare-repo &&\n> +\t\ttest_must_fail git -c discovery.bare=always rev-parse --git-dir 2>err &&\n> +\t\tgrep \"discovery.bare\" err\n> +\t)\n\nOk, at the current place in the series, this test_must_fail matches\nexpectation. If you reorder to have this patch after your current patch 4,\nthen we can write this test immediately as a successful case.\n\nWe could also reuse some information from the expect_rejected helper by\nadding this:\n\nexpect_rejected () {\n\ttest_must_fail git $* rev-parse --git-dir 2>err &&\n\tgrep \"discovery.bare\" err\n}\n\nThen you can test the -c options in the tests as\n\n\texpect_rejected -c discovery.bare=always\n\nThanks,\n-Stolee\n"},{"id":"456505","messageId":"c9f11902-31aa-6b69-f4fc-06ed66887de1@github.com","threadId":"57859","inReplyTo":"66a0a208176094415d7b0ae1f686bdd1638b8a8a.1653685761.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 4/5] config: include \"-c\" in protected config","fromName":"Derrick Stolee","fromEmail":"derrickstolee@github.com","sentAt":"2022-06-02T13:15:36Z","receivedAt":"2022-06-02T13:15:53Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 5/27/2022 5:09 PM, Glen Choo via GitGitGadget wrote:\n  \n> +int git_configset_add_parameters(struct config_set *cs)\n> +{\n> +\treturn git_config_from_parameters(config_set_callback, cs);\n> +}\n> +\n\nThis one-line method could be inlined into the read_protected_config()\nmethod:\n\n> @@ -2628,6 +2633,7 @@ static void read_protected_config(void)\n>  \tgit_configset_add_file(the_repository->protected_config, system_config);\n>  \tgit_configset_add_file(the_repository->protected_config, xdg_config);\n>  \tgit_configset_add_file(the_repository->protected_config, user_config);\n> +\tgit_configset_add_parameters(the_repository->protected_config);\n\tgit_config_from_parameters(config_set_callback, the_repository->protected_config);\n\n...would be the way to inline it.\n\n> +/**\n> + * Parses command line options and environment variables, and adds the\n> + * variable-value pairs to the `config_set`. Returns 0 on success, or -1\n> + * if there is an error in parsing. The caller decides whether to free\n> + * the incomplete configset or continue using it when the function\n> + * returns -1.\n> + */\n> +int git_configset_add_parameters(struct config_set *cs);\n\nYou do make it public here. I wonder if we can think of other consumers\nof this method that justify the addition to the API.\n\nBut this is also a nitpick. I don't feel strongly one way or another. The\ncode definitely works as-is.\n\nThanks,\n-Stolee\n"},{"id":"456506","messageId":"3873f70e-0dd6-e469-5d0e-0f6847136f0a@github.com","threadId":"57859","inReplyTo":"e25d5907cd1e3894f19ffbfb3310175fd660563b.1653685761.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 5/5] upload-pack: make uploadpack.packObjectsHook protected","fromName":"Derrick Stolee","fromEmail":"derrickstolee@github.com","sentAt":"2022-06-02T13:18:16Z","receivedAt":"2022-06-02T13:18:21Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 5/27/2022 5:09 PM, Glen Choo via GitGitGadget wrote:\n> From: Glen Choo <chooglen@google.com>\n> \n> Now that protected config includes \"-c\", \"uploadpack.packObjectsHook\"\n> behaves identically to a 'Protected config only' variable. Refactor it\n> to use git_protected_config() and mark it 'Protected config only'.\n\nI'm really glad to see this simplification at the end of your series.\n\n> @@ -1321,18 +1321,21 @@ static int upload_pack_config(const char *var, const char *value, void *cb_data)\n>  \t\tdata->advertise_sid = git_config_bool(var, value);\n>  \t}\n>  \n> -\tif (current_config_scope() != CONFIG_SCOPE_LOCAL &&\n> -\t    current_config_scope() != CONFIG_SCOPE_WORKTREE) {\n> -\t\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n> -\t\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n> -\t}\n> -\n\n...\n\n> +static int upload_pack_protected_config(const char *var, const char *value, void *cb_data)\n> +{\n> +\tstruct upload_pack_data *data = cb_data;\n> +\n> +\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n> +\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n> +\treturn 0;\n> +}\n> +\n\nThis is much cleaner.\n\n> @@ -1342,6 +1345,7 @@ void upload_pack(const int advertise_refs, const int stateless_rpc,\n>  \tupload_pack_data_init(&data);\n>  \n>  \tgit_config(upload_pack_config, &data);\n> +\tgit_protected_config(upload_pack_protected_config, &data);\n>  \n>  \tdata.stateless_rpc = stateless_rpc;\n>  \tdata.timeout = timeout;\n> @@ -1697,6 +1701,7 @@ int upload_pack_v2(struct repository *r, struct packet_reader *request)\n>  \tdata.use_sideband = LARGE_PACKET_MAX;\n>  \n>  \tgit_config(upload_pack_config, &data);\n> +\tgit_protected_config(upload_pack_protected_config, &data);\n\nIt's unfortunate that there are two places that need this change.\nIs it worth adding a static helper that executes these?\n\nstatic void get_upload_pack_config(void *data)\n{\n\tgit_config(upload_pack_config, data);\n\tgit_protected_config(upload_pack_protected_config, data);\n}\n\nThanks,\n-Stolee\n"},{"id":"456529","messageId":"xmqq35gnovkp.fsf@gitster.g","threadId":"57859","inReplyTo":"2ed00ece-f8bb-c84c-0684-494692a71da2@github.com","subject":"Re: [PATCH v3 1/5] Documentation: define protected configuration","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-06-02T16:53:58Z","receivedAt":"2022-06-02T16:54:06Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Derrick Stolee <derrickstolee@github.com> writes:\n\n>> It would make sense to give a rationale behind the seemingly\n>> arbitrary choice of what is and what is not \"protected\".  Not\n>> necessarily in the glossary, but in the proposed log message of the\n>> commit that makes the decision.  The rationale must help readers to\n>> be able to answer the following questions.\n>> \n>>  - The system level is \"protected\" because?  Is it because we do not\n>>    even try to protect ourselves from those who can write anywhere\n>>    in /etc/ or other system directories?\n>> \n>>  - The per-user config is \"protected\" because?  Is it because our\n>>    primary interest in \"protection\" is to protect individual users\n>>    from landmines laid in the filesystem by other users, and those\n>>    who can already write into $HOME are not we try to guard against?\n>\n> I think the answers to these two questions is \"yes\", so they can\n> be turned into an affirmative sentence:\n>\n> \tWe do not event try to protect ourselves from those who can\n> \twrite anywhere...\n\ns/event/even/.\n\n>\n>>  - The per-repo config is not \"protected\" (i.e. \"trusted\"), because?\n>>    If we are not honoring a configuration in the repository, why are\n>>    we working in that repository in the first place?\n>\n> This requires an example:\n>\n> \tSome workflows use repositories stored in shared directories,\n> \twhich are writable by multiple unprivileged users.\n\nHmph, \"... and we do not trust these colleagues\"?  It might be true,\nbut sounds a bit weak rationale, at least to me.  A natural reaction\ncoming form a devil's advocate naïve me would be \"well, then I would\nnot be directly interacting with such a repository; I'd work in a\nclone of it of my own, and pull and push as needed\".\n\nIsn't the reason more like \"users may go spelunking random places in\nthe filesystem, with PS1 settings and the like that causes some\n\"git\" command invoked automatically in their current directory, and\nwe want to protect these users from getting harmed by a random\nrepository with hostile contents in their configuration and hooks\nwithout even realizing they have wandered into such a repository\"?\n\n>>  - The per invocation config is not \"protected\" (i.e. \"trusted\"),\n>>    because?  If we cannot trusting our own command line, what\n>>    prevents an attacker from mucking with our command line to say\n>>    \"sudo whatever\" using the same attack vector?\n>\n> With this argument, I agree that -c config can be considered\n> protected. At the very least, it is visible to the user when they\n> are running a command. This would unify our expectations with\n> uploadPack.packObjectsHook, too.\n\nYup, that matches my understanding.\n\nIn any case, I'd prefer to see not just the definition but the\nreasoning behind the decision that made some \"protected\" while\nleaving others not-\"protected\" clearly documented to help users.\n\nThanks.\n"},{"id":"456534","messageId":"kl6la6avdkx1.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"xmqq35gnovkp.fsf@gitster.g","subject":"Re: [PATCH v3 1/5] Documentation: define protected configuration","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-06-02T17:39:38Z","receivedAt":"2022-06-02T17:39:50Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Derrick Stolee <derrickstolee@github.com> writes:\n>>>  - The per-repo config is not \"protected\" (i.e. \"trusted\"), because?\n>>>    If we are not honoring a configuration in the repository, why are\n>>>    we working in that repository in the first place?\n>>\n>> This requires an example:\n>>\n>> \tSome workflows use repositories stored in shared directories,\n>> \twhich are writable by multiple unprivileged users.\n>\n> Isn't the reason more like \"users may go spelunking random places in\n> the filesystem, with PS1 settings and the like that causes some\n> \"git\" command invoked automatically in their current directory, and\n> we want to protect these users from getting harmed by a random\n> repository with hostile contents in their configuration and hooks\n> without even realizing they have wandered into such a repository\"?\n\nHm, this is my understanding as well, i.e. `safe.directory` is meant to\nprotect you from shared repositories that you didn't expect, but it lets\nyou trust the shared repositories that you need (and there is no\nprotection once you decide to trust the repo).\n"},{"id":"456591","messageId":"kl6l7d5xeo3s.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"xmqqh75a1rmd.fsf@gitster.g","subject":"Re: [PATCH v3 1/5] Documentation: define protected configuration","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-06-03T15:57:43Z","receivedAt":"2022-06-03T15:57:51Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> \"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>>  safe.directory::\n>> -\tThese config entries specify Git-tracked directories that are\n>> -\tconsidered safe even if they are owned by someone other than the\n>> -\tcurrent user. By default, Git will refuse to even parse a Git\n>> -\tconfig of a repository owned by someone else, let alone run its\n>> -\thooks, and this config setting allows users to specify exceptions,\n>> -\te.g. for intentionally shared repositories (see the `--shared`\n>> -\toption in linkgit:git-init[1]).\n>> +\t'(Protected config only) ' These config entries specify\n>\n> What's the SP in \"only) '\" doing?\n\nSilly typo. Thanks for the catch :)\n\n>> diff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt\n>> index aa2f41f5e70..a669983abd6 100644\n>> --- a/Documentation/glossary-content.txt\n>> +++ b/Documentation/glossary-content.txt\n>> @@ -483,6 +483,24 @@ exclude;;\n>>  \thead ref. If the remote <<def_head,head>> is not an\n>>  \tancestor to the local head, the push fails.\n>>  \n>> +[[def_protected_config]]protected configuration::\n>> +\tProtected configuration is configuration that Git considers more\n>> +\ttrustworthy because it is unlikely to be tampered with by an\n>> +\tattacker. For security reasons, some configuration variables are\n>> +\tonly respected when they are defined in protected configuration.\n>> ++\n>> +Protected configuration includes:\n>> ++\n>> +- system-level config, e.g. `/etc/git/config`\n>> +- global config, e.g. `$XDG_CONFIG_HOME/git/config` and\n>> +  `$HOME/.gitconfig`\n>> +Protected configuration excludes:\n>> ++\n>> +- repository config, e.g. `$GIT_DIR/config` and\n>> +  `$GIT_DIR/config.worktree`\n>> +- the command line option `-c` and its equivalent environment variables\n>\n> The description is a bit unclear what \"protected configuration\"\n> refers.\n>\n> If it is the scopes (as in \"git config --show-scope\") Git can trust\n> more, in other words, a statement like this\n>\n>     safe.directory is honored only when it comes from a protected\n>     configuration.\n>\n> is what you want to make easier to write by introducing a new\n> phrase, perhaps use the word \"scope\" for more consistency?  E.g.\n>\n>     Only safe.directory that is defined in a trusted scope is\n>     honored.\n\nGood point. I think using scope would be a lot clearer, and maybe I\nwill consider s/protected configuration/protected scope. I'm hesitant to\ncall the scope \"trusted\", because I don't want to insinuate that\nrepository config is \"untrusted\" since we _do_ trust it in most cases.\n\nI don't think Documentation/git-config.txt has adequately defined what a\n'scope' is though, even though scopes have been with us since 9acc591111\n(config: add a notion of \"scope\", 2016-05-18). The best I could find is\n\"--show-scope\", introduced in 145d59f482 (config: add '--show-scope' to\nprint the scope of a config value, 2020-02-10), which mentions scopes\nbut doesn't link the idea back to the specific files or CLI options\n(\"--system\", \"--global\", etc).\n\nSo I'll see if I can improve the docs around scopes since that will help\nthe language in this patch.\n"},{"id":"456813","messageId":"156817966fa87e0e3b94a1e8468ecfc6683b1946.1654635432.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v4.git.git.1654635432.gitgitgadget@gmail.com","subject":"[PATCH v4 4/5] safe.directory: use git_protected_config()","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-07T20:57:11Z","receivedAt":"2022-06-08T00:27:17Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nUse git_protected_config() to read `safe.directory` instead of\nread_very_early_config(), making it 'protected config only'. As a\nresult, `safe.directory` now respects \"-c\", so update the tests and docs\naccordingly.\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config/safe.txt |  6 +++---\n setup.c                       |  2 +-\n t/t0033-safe-directory.sh     | 24 ++++++++++--------------\n 3 files changed, 14 insertions(+), 18 deletions(-)\n\ndiff --git a/Documentation/config/safe.txt b/Documentation/config/safe.txt\nindex ae0e2e3bdb4..2a7d2324250 100644\n--- a/Documentation/config/safe.txt\n+++ b/Documentation/config/safe.txt\n@@ -12,9 +12,9 @@ via `git config --add`. To reset the list of safe directories (e.g. to\n override any such directories specified in the system config), add a\n `safe.directory` entry with an empty value.\n +\n-This config setting is only respected when specified in a system or global\n-config, not when it is specified in a repository config, via the command\n-line option `-c safe.directory=<path>`, or in environment variables.\n+This config setting is only respected in protected configuration (see\n+<<SCOPES>>). This prevents the untrusted repository from tampering with this\n+value.\n +\n The value of this setting is interpolated, i.e. `~/<path>` expands to a\n path relative to the home directory and `%(prefix)/<path>` expands to a\ndiff --git a/setup.c b/setup.c\nindex f818dd858c6..847d47f9195 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1128,7 +1128,7 @@ static int ensure_valid_ownership(const char *path)\n \t    is_path_owned_by_current_user(path))\n \t\treturn 1;\n \n-\tread_very_early_config(safe_directory_cb, &data);\n+\tgit_protected_config(safe_directory_cb, &data);\n \n \treturn data.is_safe;\n }\ndiff --git a/t/t0033-safe-directory.sh b/t/t0033-safe-directory.sh\nindex 238b25f91a3..5a1cd0d0947 100755\n--- a/t/t0033-safe-directory.sh\n+++ b/t/t0033-safe-directory.sh\n@@ -16,24 +16,20 @@ test_expect_success 'safe.directory is not set' '\n \texpect_rejected_dir\n '\n \n-test_expect_success 'ignoring safe.directory on the command line' '\n-\ttest_must_fail git -c safe.directory=\"$(pwd)\" status 2>err &&\n-\tgrep \"unsafe repository\" err\n+test_expect_success 'safe.directory on the command line' '\n+\tgit -c safe.directory=\"$(pwd)\" status\n '\n \n-test_expect_success 'ignoring safe.directory in the environment' '\n-\ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n-\t\tGIT_CONFIG_KEY_0=\"safe.directory\" \\\n-\t\tGIT_CONFIG_VALUE_0=\"$(pwd)\" \\\n-\t\tgit status 2>err &&\n-\tgrep \"unsafe repository\" err\n+test_expect_success 'safe.directory in the environment' '\n+\tenv GIT_CONFIG_COUNT=1 \\\n+\t    GIT_CONFIG_KEY_0=\"safe.directory\" \\\n+\t    GIT_CONFIG_VALUE_0=\"$(pwd)\" \\\n+\t    git status\n '\n \n-test_expect_success 'ignoring safe.directory in GIT_CONFIG_PARAMETERS' '\n-\ttest_must_fail env \\\n-\t\tGIT_CONFIG_PARAMETERS=\"${SQ}safe.directory${SQ}=${SQ}$(pwd)${SQ}\" \\\n-\t\tgit status 2>err &&\n-\tgrep \"unsafe repository\" err\n+test_expect_success 'safe.directory in GIT_CONFIG_PARAMETERS' '\n+\tenv GIT_CONFIG_PARAMETERS=\"${SQ}safe.directory${SQ}=${SQ}$(pwd)${SQ}\" \\\n+\t    git status\n '\n \n test_expect_success 'ignoring safe.directory in repo config' '\n-- \ngitgitgadget\n\n"},{"id":"456820","messageId":"94b40907e66b2f6e0874ab49f8b73fdd58eb06d5.1654635432.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v4.git.git.1654635432.gitgitgadget@gmail.com","subject":"[PATCH v4 3/5] config: read protected config with `git_protected_config()`","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-07T20:57:10Z","receivedAt":"2022-06-08T00:28:35Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\n`uploadpack.packObjectsHook` is the only 'protected config only'\nvariable today, but we've noted that `safe.directory` and the upcoming\n`discovery.bare` should also be 'protected config only'. So, for\nconsistency, we'd like to have a single implementation for protected\nconfig.\n\nThe primary constraints are:\n\n1. Reading from protected config should be as fast as possible. Nearly\n   all \"git\" commands inside a bare repository will read both\n   `safe.directory` and `discovery.bare`, so we cannot afford to be\n   slow.\n\n2. Protected config must be readable when the gitdir is not known.\n   `safe.directory` and `discovery.bare` both affect repository\n   discovery and the gitdir is not known at that point [1].\n\nThe chosen implementation in this commit is to read protected config and\ncache the values in a global configset. This is similar to the caching\nbehavior we get with the_repository->config.\n\nIntroduce git_protected_config(), which reads protected config and\ncaches them in the global configset protected_config. Then, refactor\n`uploadpack.packObjectsHook` to use git_protected_config().\n\nThe protected config functions are named similarly to their\nnon-protected counterparts, e.g. git_protected_config_check_init() vs\ngit_config_check_init().\n\nIn light of constraint 1, this implementation can still be improved\nsince git_protected_config() iterates through every variable in\nprotected_config, which may still be too expensive. There exist constant\ntime lookup functions for non-protected config (repo_config_get_*()),\nbut for simplicity, this commit does not implement similar functions for\nprotected config.\n\nAn alternative that avoids introducing another configset is to continue\nto read all config using git_config(), but only accept values that have\nthe correct config scope [2]. This technically fulfills constraint 2,\nbecause git_config() simply ignores the local and worktree config when\nthe gitdir is not known. However, this would read incomplete config into\nthe_repository->config, which would need to be reset when the gitdir is\nknown and git_config() needs to read the local and worktree config.\nResetting the_repository->config might be reasonable while we only have\nthese 'protected config only' variables, but it's not clear whether this\nextends well to future variables.\n\n[1] In this case, we do have a candidate gitdir though, so with a little\nrefactoring, it might be possible to provide a gitdir.\n[2] This is how `uploadpack.packObjectsHook` was implemented prior to\nthis commit.\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n config.c                     | 51 ++++++++++++++++++++++++++++++++++++\n config.h                     | 17 ++++++++++++\n t/t5544-pack-objects-hook.sh |  7 ++++-\n upload-pack.c                | 27 ++++++++++++-------\n 4 files changed, 91 insertions(+), 11 deletions(-)\n\ndiff --git a/config.c b/config.c\nindex fa471dbdb89..56b7ed5ffe8 100644\n--- a/config.c\n+++ b/config.c\n@@ -81,6 +81,18 @@ static enum config_scope current_parsing_scope;\n static int pack_compression_seen;\n static int zlib_compression_seen;\n \n+/*\n+ * Config that comes from trusted sources, namely:\n+ * - system config files (e.g. /etc/gitconfig)\n+ * - global config files (e.g. $HOME/.gitconfig,\n+ *   $XDG_CONFIG_HOME/git)\n+ * - the command line.\n+ *\n+ * This is declared here for code cleanliness, but unlike the other\n+ * static variables, this does not hold config parser state.\n+ */\n+static struct config_set protected_config;\n+\n static int config_file_fgetc(struct config_source *conf)\n {\n \treturn getc_unlocked(conf->u.file);\n@@ -2373,6 +2385,11 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n \treturn git_config_from_file(config_set_callback, filename, cs);\n }\n \n+int git_configset_add_parameters(struct config_set *cs)\n+{\n+\treturn git_config_from_parameters(config_set_callback, cs);\n+}\n+\n int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n {\n \tconst struct string_list *values = NULL;\n@@ -2614,6 +2631,40 @@ int repo_config_get_pathname(struct repository *repo,\n \treturn ret;\n }\n \n+/* Read values into protected_config. */\n+static void read_protected_config(void)\n+{\n+\tchar *xdg_config = NULL, *user_config = NULL, *system_config = NULL;\n+\n+\tgit_configset_init(&protected_config);\n+\n+\tsystem_config = git_system_config();\n+\tgit_global_config(&user_config, &xdg_config);\n+\n+\tgit_configset_add_file(&protected_config, system_config);\n+\tgit_configset_add_file(&protected_config, xdg_config);\n+\tgit_configset_add_file(&protected_config, user_config);\n+\tgit_configset_add_parameters(&protected_config);\n+\n+\tfree(system_config);\n+\tfree(xdg_config);\n+\tfree(user_config);\n+}\n+\n+/* Ensure that protected_config has been initialized. */\n+static void git_protected_config_check_init(void)\n+{\n+\tif (protected_config.hash_initialized)\n+\t\treturn;\n+\tread_protected_config();\n+}\n+\n+void git_protected_config(config_fn_t fn, void *data)\n+{\n+\tgit_protected_config_check_init();\n+\tconfigset_iter(&protected_config, fn, data);\n+}\n+\n /* Functions used historically to read configuration from 'the_repository' */\n void git_config(config_fn_t fn, void *data)\n {\ndiff --git a/config.h b/config.h\nindex 7654f61c634..e3ff1fcf683 100644\n--- a/config.h\n+++ b/config.h\n@@ -446,6 +446,15 @@ void git_configset_init(struct config_set *cs);\n  */\n int git_configset_add_file(struct config_set *cs, const char *filename);\n \n+/**\n+ * Parses command line options and environment variables, and adds the\n+ * variable-value pairs to the `config_set`. Returns 0 on success, or -1\n+ * if there is an error in parsing. The caller decides whether to free\n+ * the incomplete configset or continue using it when the function\n+ * returns -1.\n+ */\n+int git_configset_add_parameters(struct config_set *cs);\n+\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key` and config set `cs`. When the\n@@ -505,6 +514,14 @@ int repo_config_get_maybe_bool(struct repository *repo,\n int repo_config_get_pathname(struct repository *repo,\n \t\t\t     const char *key, const char **dest);\n \n+/*\n+ * Functions for reading protected config. By definition, protected\n+ * config ignores repository config, so it is unnecessary to read\n+ * protected config from any `struct repository` other than\n+ * the_repository.\n+ */\n+void git_protected_config(config_fn_t fn, void *data);\n+\n /**\n  * Querying For Specific Variables\n  * -------------------------------\ndiff --git a/t/t5544-pack-objects-hook.sh b/t/t5544-pack-objects-hook.sh\nindex dd5f44d986f..54f54f8d2eb 100755\n--- a/t/t5544-pack-objects-hook.sh\n+++ b/t/t5544-pack-objects-hook.sh\n@@ -56,7 +56,12 @@ test_expect_success 'hook does not run from repo config' '\n \t! grep \"hook running\" stderr &&\n \ttest_path_is_missing .git/hook.args &&\n \ttest_path_is_missing .git/hook.stdin &&\n-\ttest_path_is_missing .git/hook.stdout\n+\ttest_path_is_missing .git/hook.stdout &&\n+\n+\t# check that global config is used instead\n+\ttest_config_global uploadpack.packObjectsHook ./hook &&\n+\tgit clone --no-local . dst2.git 2>stderr &&\n+\tgrep \"hook running\" stderr\n '\n \n test_expect_success 'hook works with partial clone' '\ndiff --git a/upload-pack.c b/upload-pack.c\nindex 3a851b36066..09f48317b02 100644\n--- a/upload-pack.c\n+++ b/upload-pack.c\n@@ -1321,18 +1321,27 @@ static int upload_pack_config(const char *var, const char *value, void *cb_data)\n \t\tdata->advertise_sid = git_config_bool(var, value);\n \t}\n \n-\tif (current_config_scope() != CONFIG_SCOPE_LOCAL &&\n-\t    current_config_scope() != CONFIG_SCOPE_WORKTREE) {\n-\t\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n-\t\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n-\t}\n-\n \tif (parse_object_filter_config(var, value, data) < 0)\n \t\treturn -1;\n \n \treturn parse_hide_refs_config(var, value, \"uploadpack\");\n }\n \n+static int upload_pack_protected_config(const char *var, const char *value, void *cb_data)\n+{\n+\tstruct upload_pack_data *data = cb_data;\n+\n+\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n+\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n+\treturn 0;\n+}\n+\n+static void get_upload_pack_config(struct upload_pack_data *data)\n+{\n+\tgit_config(upload_pack_config, data);\n+\tgit_protected_config(upload_pack_protected_config, data);\n+}\n+\n void upload_pack(const int advertise_refs, const int stateless_rpc,\n \t\t const int timeout)\n {\n@@ -1340,8 +1349,7 @@ void upload_pack(const int advertise_refs, const int stateless_rpc,\n \tstruct upload_pack_data data;\n \n \tupload_pack_data_init(&data);\n-\n-\tgit_config(upload_pack_config, &data);\n+\tget_upload_pack_config(&data);\n \n \tdata.stateless_rpc = stateless_rpc;\n \tdata.timeout = timeout;\n@@ -1695,8 +1703,7 @@ int upload_pack_v2(struct repository *r, struct packet_reader *request)\n \n \tupload_pack_data_init(&data);\n \tdata.use_sideband = LARGE_PACKET_MAX;\n-\n-\tgit_config(upload_pack_config, &data);\n+\tget_upload_pack_config(&data);\n \n \twhile (state != FETCH_DONE) {\n \t\tswitch (state) {\n-- \ngitgitgadget\n\n"},{"id":"456824","messageId":"29053d029f8ec61095a2ad557be38b1d485a158f.1654635432.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v4.git.git.1654635432.gitgitgadget@gmail.com","subject":"[PATCH v4 5/5] setup.c: create `discovery.bare`","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-07T20:57:12Z","receivedAt":"2022-06-08T00:33:16Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nThere is a known social engineering attack that takes advantage of the\nfact that a working tree can include an entire bare repository,\nincluding a config file. A user could run a Git command inside the bare\nrepository thinking that the config file of the 'outer' repository would\nbe used, but in reality, the bare repository's config file (which is\nattacker-controlled) is used, which may result in arbitrary code\nexecution. See [1] for a fuller description and deeper discussion.\n\nA simple mitigation is to forbid bare repositories unless specified via\n`--git-dir` or `GIT_DIR`. In environments that don't use bare\nrepositories, this would be minimally disruptive.\n\nCreate a config variable, `discovery.bare`, that tells Git whether or\nnot to die() when it discovers a bare repository. This only affects\nrepository discovery, thus it has no effect if discovery was not\ndone (e.g. `--git-dir` was passed).\n\nThis config is an enum of:\n\n- \"always\": always allow bare repositories (this is the default)\n- \"never\": never allow bare repositories\n\nIf we want to protect users from such attacks by default, neither value\nwill suffice - \"always\" provides no protection, but \"never\" is\nimpractical for bare repository users. A more usable default would be to\nallow only non-embedded bare repositories ([2] contains one such\nproposal), but detecting if a repository is embedded is potentially\nnon-trivial, so this work is not implemented in this series.\n\n[1]: https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n[2]: https://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config.txt           |  2 +\n Documentation/config/discovery.txt | 19 +++++++++\n setup.c                            | 57 ++++++++++++++++++++++++-\n t/t0035-discovery-bare.sh          | 68 ++++++++++++++++++++++++++++++\n 4 files changed, 145 insertions(+), 1 deletion(-)\n create mode 100644 Documentation/config/discovery.txt\n create mode 100755 t/t0035-discovery-bare.sh\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex e284b042f22..9a5e1329772 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -409,6 +409,8 @@ include::config/diff.txt[]\n \n include::config/difftool.txt[]\n \n+include::config/discovery.txt[]\n+\n include::config/extensions.txt[]\n \n include::config/fastimport.txt[]\ndiff --git a/Documentation/config/discovery.txt b/Documentation/config/discovery.txt\nnew file mode 100644\nindex 00000000000..fbe93597e7c\n--- /dev/null\n+++ b/Documentation/config/discovery.txt\n@@ -0,0 +1,19 @@\n+discovery.bare::\n+\t'(Protected config only)' Specifies whether Git will work with a\n+\tbare repository that it found during repository discovery. This\n+\thas no effect if the repository is specified directly via the\n+\t--git-dir command-line option or the GIT_DIR environment\n+\tvariable (see linkgit:git[1]).\n++\n+The currently supported values are:\n++\n+* `always`: Git always works with bare repositories\n+* `never`: Git never works with bare repositories\n++\n+This defaults to `always`, but this default may change in the future.\n++\n+If you do not use bare repositories in your workflow, then it may be\n+beneficial to set `discovery.bare` to `never` in your global config.\n+This will protect you from attacks that involve cloning a repository\n+that contains a bare repository and running a Git command within that\n+directory.\ndiff --git a/setup.c b/setup.c\nindex 847d47f9195..4d8d3c1bc7d 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -10,6 +10,10 @@\n static int inside_git_dir = -1;\n static int inside_work_tree = -1;\n static int work_tree_config_is_bogus;\n+enum discovery_bare_allowed {\n+\tDISCOVERY_BARE_NEVER = 0,\n+\tDISCOVERY_BARE_ALWAYS,\n+};\n \n static struct startup_info the_startup_info;\n struct startup_info *startup_info = &the_startup_info;\n@@ -1133,6 +1137,46 @@ static int ensure_valid_ownership(const char *path)\n \treturn data.is_safe;\n }\n \n+static int discovery_bare_cb(const char *key, const char *value, void *d)\n+{\n+\tenum discovery_bare_allowed *discovery_bare_allowed = d;\n+\n+\tif (strcmp(key, \"discovery.bare\"))\n+\t\treturn 0;\n+\n+\tif (!strcmp(value, \"never\")) {\n+\t\t*discovery_bare_allowed = DISCOVERY_BARE_NEVER;\n+\t\treturn 0;\n+\t}\n+\tif (!strcmp(value, \"always\")) {\n+\t\t*discovery_bare_allowed = DISCOVERY_BARE_ALWAYS;\n+\t\treturn 0;\n+\t}\n+\treturn -1;\n+}\n+\n+static enum discovery_bare_allowed get_discovery_bare(void)\n+{\n+\tenum discovery_bare_allowed result = DISCOVERY_BARE_ALWAYS;\n+\tgit_protected_config(discovery_bare_cb, &result);\n+\treturn result;\n+}\n+\n+static const char *discovery_bare_allowed_to_string(\n+\tenum discovery_bare_allowed discovery_bare_allowed)\n+{\n+\tswitch (discovery_bare_allowed) {\n+\tcase DISCOVERY_BARE_NEVER:\n+\t\treturn \"never\";\n+\tcase DISCOVERY_BARE_ALWAYS:\n+\t\treturn \"always\";\n+\tdefault:\n+\t\tBUG(\"invalid discovery_bare_allowed %d\",\n+\t\t    discovery_bare_allowed);\n+\t}\n+\treturn NULL;\n+}\n+\n enum discovery_result {\n \tGIT_DIR_NONE = 0,\n \tGIT_DIR_EXPLICIT,\n@@ -1142,7 +1186,8 @@ enum discovery_result {\n \tGIT_DIR_HIT_CEILING = -1,\n \tGIT_DIR_HIT_MOUNT_POINT = -2,\n \tGIT_DIR_INVALID_GITFILE = -3,\n-\tGIT_DIR_INVALID_OWNERSHIP = -4\n+\tGIT_DIR_INVALID_OWNERSHIP = -4,\n+\tGIT_DIR_DISALLOWED_BARE = -5,\n };\n \n /*\n@@ -1239,6 +1284,8 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n \t\t}\n \n \t\tif (is_git_directory(dir->buf)) {\n+\t\t\tif (!get_discovery_bare())\n+\t\t\t\treturn GIT_DIR_DISALLOWED_BARE;\n \t\t\tif (!ensure_valid_ownership(dir->buf))\n \t\t\t\treturn GIT_DIR_INVALID_OWNERSHIP;\n \t\t\tstrbuf_addstr(gitdir, \".\");\n@@ -1385,6 +1432,14 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \t\t}\n \t\t*nongit_ok = 1;\n \t\tbreak;\n+\tcase GIT_DIR_DISALLOWED_BARE:\n+\t\tif (!nongit_ok) {\n+\t\t\tdie(_(\"cannot use bare repository '%s' (discovery.bare is '%s')\"),\n+\t\t\t    dir.buf,\n+\t\t\t    discovery_bare_allowed_to_string(get_discovery_bare()));\n+\t\t}\n+\t\t*nongit_ok = 1;\n+\t\tbreak;\n \tcase GIT_DIR_NONE:\n \t\t/*\n \t\t * As a safeguard against setup_git_directory_gently_1 returning\ndiff --git a/t/t0035-discovery-bare.sh b/t/t0035-discovery-bare.sh\nnew file mode 100755\nindex 00000000000..0b345d361e6\n--- /dev/null\n+++ b/t/t0035-discovery-bare.sh\n@@ -0,0 +1,68 @@\n+#!/bin/sh\n+\n+test_description='verify discovery.bare checks'\n+\n+. ./test-lib.sh\n+\n+pwd=\"$(pwd)\"\n+\n+expect_accepted () {\n+\tgit \"$@\" rev-parse --git-dir\n+}\n+\n+expect_rejected () {\n+\ttest_must_fail git \"$@\" rev-parse --git-dir 2>err &&\n+\tgrep \"discovery.bare\" err\n+}\n+\n+test_expect_success 'setup bare repo in worktree' '\n+\tgit init outer-repo &&\n+\tgit init --bare outer-repo/bare-repo\n+'\n+\n+test_expect_success 'discovery.bare unset' '\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\texpect_accepted\n+\t)\n+'\n+\n+test_expect_success 'discovery.bare=always' '\n+\tgit config --global discovery.bare always &&\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\texpect_accepted\n+\t)\n+'\n+\n+test_expect_success 'discovery.bare=never' '\n+\tgit config --global discovery.bare never &&\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\texpect_rejected\n+\t)\n+'\n+\n+test_expect_success 'discovery.bare in the repository' '\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\t# Temporarily set discovery.bare=always, otherwise git\n+\t\t# config fails with \"fatal: not in a git directory\"\n+\t\t# (like safe.directory)\n+\t\tgit config --global discovery.bare always &&\n+\t\tgit config discovery.bare always &&\n+\t\tgit config --global discovery.bare never &&\n+\t\texpect_rejected\n+\t)\n+'\n+\n+test_expect_success 'discovery.bare on the command line' '\n+\tgit config --global discovery.bare never &&\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\texpect_accepted -c discovery.bare=always &&\n+\t\texpect_rejected -c discovery.bare=\n+\t)\n+'\n+\n+test_done\n-- \ngitgitgadget\n"},{"id":"456829","messageId":"pull.1261.v4.git.git.1654635432.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v3.git.git.1653685761.gitgitgadget@gmail.com","subject":"[PATCH v4 0/5] config: introduce discovery.bare and protected config","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-07T20:57:07Z","receivedAt":"2022-06-08T00:33:40Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Thanks again for the kind feedback, everyone :)\n\nThe motivation has remained the same as the last round; you can find it in\nthe \"Description\" section in the previous cover letter [1].\n\nThis round doesn't introduce any major code changes. The most notable\nchanges are:\n\n * I've reorganized the patches so that protected config includes \"-c\" from\n   the beginning (3/5) (instead of trying to avoid changing safe.directory).\n   As a result, uploadpack.packObjectsHook becomes the first 'protected\n   config only' variable instead of safe.directory.\n   \n   Since we start the conversion with uploadpack.packObjectHook, I was\n   curious whether we might be able to reuse its approach of \"reading the\n   full set of config, but checking the scope of each value\", which might be\n   nice because we could reuse the cache in the_repository->config instead\n   of creating an entirely new configset. I didn't pursue it further, but\n   I've noted this alternative in 3/5's commit message.\n\n * 'Protected configuration' is now defined as a set of configuration scopes\n   (2/5). This follows a suggestion from Junio [2], which I thought read\n   very clearly. We haven't done a great job at describing 'scopes' in\n   Documentation/git-config.txt though, so I tried to remedy that by\n   cleaning up a little and adding a SCOPES section (1/5).\n   \n   Frankly, I'm not very happy with the end result - it's not nearly as\n   clear as I had hoped, and I think I might be introducing some confusion\n   between the words \"config\" and \"scope\". I'd appreciate any feedback that\n   helps us get to a good final wording.\n\n * I added a test for \"git config --show-scope\" and the 'worktree' scope,\n   since 'worktree' wasn't listed in Documentation/git-config.txt (6/5).\n\n= Patch organization\n\n * Patch 1 add a section on config scopes to our docs\n * Patches 2-3 define 'protected config' and create a shared implementation.\n * Patch 4 refactors safe.directory to use protected config\n * Patch 5 adds discovery.bare\n\n= Series history\n\nChanges in v4:\n\n * 2/5's commit message now justifies what scopes are included in protected\n   config\n * The global configset is now a file-scope static inside config.c\n   (previously it was a member of the_repository).\n * Rename discovery_bare_config to discovery_bare_allowed\n * Make discovery_bare_allowed function-scoped (instead of global).\n * Add an expect_accepted helper to the discovery.bare tests.\n * Add a helper to \"upload-pack\" that reads the protected and non-protected\n   config\n\nChanges in v3:\n\n * Rebase onto a more recent 'master'\n * Reframe this feature in only in terms of the 'embedded bare repo' attack.\n * Other docs improvements (thanks Stolee in particular!)\n * Protected config no longer uses read_very_early_config() and is only read\n   once\n * Protected config now includes \"-c\"\n * uploadpack.packObjectsHook now uses protected config instead of ignoring\n   repo config using config scopes\n\nChanges in v2:\n\n * Rename safe.barerepository to discovery.bare and make it die()\n * Move tests into t/t0034-discovery-bare.sh\n * Avoid unnecessary config reading by using a static variable\n * Add discovery.bare=cwd\n * Fix typos\n\n= Future work\n\n * This series does not implement the \"no-embedded\" option [3] and I won't\n   work on it any time soon, but I'd be more than happy to review if someone\n   sends patches.\n * With discovery.bare, if a builtin is marked RUN_SETUP_GENTLY, setup.c\n   doesn't die() and we don't tell users why their repository was rejected,\n   e.g. \"git config\" gives an opaque \"fatal: not in a git directory\". This\n   isn't a new problem though, since safe.directory has the same issue.\n\n[1]\nhttps://lore.kernel.org/git/pull.1261.v3.git.git.1653685761.gitgitgadget@gmail.com\n[2] https://lore.kernel.org/git/xmqqh75a1rmd.fsf@gitster.g [3] This was\nfirst suggested in\nhttps://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n\nGlen Choo (5):\n  Documentation/git-config.txt: add SCOPES section\n  Documentation: define protected configuration\n  config: read protected config with `git_protected_config()`\n  safe.directory: use git_protected_config()\n  setup.c: create `discovery.bare`\n\n Documentation/config.txt            |  2 +\n Documentation/config/discovery.txt  | 19 +++++++\n Documentation/config/safe.txt       |  6 +--\n Documentation/config/uploadpack.txt |  6 +--\n Documentation/git-config.txt        | 77 +++++++++++++++++++++++------\n config.c                            | 51 +++++++++++++++++++\n config.h                            | 17 +++++++\n setup.c                             | 59 +++++++++++++++++++++-\n t/t0033-safe-directory.sh           | 24 ++++-----\n t/t0035-discovery-bare.sh           | 68 +++++++++++++++++++++++++\n t/t5544-pack-objects-hook.sh        |  7 ++-\n upload-pack.c                       | 27 ++++++----\n 12 files changed, 316 insertions(+), 47 deletions(-)\n create mode 100644 Documentation/config/discovery.txt\n create mode 100755 t/t0035-discovery-bare.sh\n\n\nbase-commit: f9b95943b68b6b8ca5a6072f50a08411c6449b55\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1261%2Fchooglen%2Fsetup%2Fdisable-bare-repo-config-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1261/chooglen/setup/disable-bare-repo-config-v4\nPull-Request: https://github.com/git/git/pull/1261\n\nRange-diff vs v3:\n\n 1:  575676c760d < -:  ----------- Documentation: define protected configuration\n -:  ----------- > 1:  c0e27ab3b3e Documentation/git-config.txt: add SCOPES section\n 5:  e25d5907cd1 ! 2:  a5a1dcb03e1 upload-pack: make uploadpack.packObjectsHook protected\n     @@ Metadata\n      Author: Glen Choo <chooglen@google.com>\n      \n       ## Commit message ##\n     -    upload-pack: make uploadpack.packObjectsHook protected\n     +    Documentation: define protected configuration\n      \n     -    Now that protected config includes \"-c\", \"uploadpack.packObjectsHook\"\n     -    behaves identically to a 'Protected config only' variable. Refactor it\n     -    to use git_protected_config() and mark it 'Protected config only'.\n     +    For security reasons, there are config variables that are only trusted\n     +    when they are specified in extra-trustworthy configuration scopes, which\n     +    are sometimes referred to on-list as 'protected configuration' [1]. A\n     +    future commit will introduce another such variable, so let's define our\n     +    terms so that we can have consistent documentation and implementation.\n     +\n     +    In our documentation, define 'protected config' as the system, global\n     +    and command config scopes. As a shorthand, I will refer to variables\n     +    that are only respected in protected config as 'protected config only',\n     +    but this term is not used in the documentation.\n     +\n     +    This definition of protected configuration is based on whether or not\n     +    Git can reasonably protect the user by ignoring the configuration scope:\n     +\n     +    - System, global and command line config are considered protected\n     +      because an attacker who has control over any of those can do plenty of\n     +      harm without Git, so we gain very little by ignoring those scopes.\n     +    - On the other hand, local (and similarly, worktree) config are not\n     +      considered protected because it is relatively easy for an attacker to\n     +      control local config, e.g.:\n     +      - On some shared user environments, a non-admin attacker can create a\n     +        repository high up the directory hierarchy (e.g. C:\\.git on Windows),\n     +        and a user may accidentally use it when their PS1 automatically\n     +        invokes \"git\" commands.\n     +\n     +        `safe.directory` prevents attacks of this form by making sure that\n     +        the user intended to use the shared repository. It obviously\n     +        shouldn't be read from the repository, because that would end up\n     +        trusting the repository that Git was supposed to reject.\n     +      - \"git upload-pack\" is expected to run in repositories that may not be\n     +        controlled by the user. We cannot ignore all config in that\n     +        repository (because \"git upload-pack\" would fail), but we can limit\n     +        the risks by ignoring `uploadpack.packObjectsHook`.\n     +\n     +    Only `uploadpack.packObjectsHook` is 'protected config only'. The\n     +    following variables are intentionally excluded:\n     +\n     +    - `safe.directory` should be 'protected config only', but it does not\n     +      technically fit the definition because it is not respected in the\n     +      \"command\" scope. A future commit will fix this.\n     +\n     +    - `trace2.*` happens to read the same scopes as `safe.directory` because\n     +      they share an implementation. However, this is not for security\n     +      reasons; it is because we want to start tracing so early that\n     +      repository-level config and \"-c\" are not available [2].\n     +\n     +      This requirement is unique to `trace2.*`, so it does not makes sense\n     +      for protected configuration to be subject to the same constraints.\n     +\n     +    [1] For example,\n     +    https://lore.kernel.org/git/6af83767-576b-75c4-c778-0284344a8fe7@github.com/\n     +    [2] https://lore.kernel.org/git/a0c89d0d-669e-bf56-25d2-cbb09b012e70@jeffhostetler.com/\n      \n          Signed-off-by: Glen Choo <chooglen@google.com>\n      \n       ## Documentation/config/uploadpack.txt ##\n     -@@ Documentation/config/uploadpack.txt: uploadpack.keepAlive::\n     - \tdisables keepalive packets entirely. The default is 5 seconds.\n     - \n     - uploadpack.packObjectsHook::\n     --\tIf this option is set, when `upload-pack` would run\n     --\t`git pack-objects` to create a packfile for a client, it will\n     --\trun this shell command instead.  The `pack-objects` command and\n     --\targuments it _would_ have run (including the `git pack-objects`\n     --\tat the beginning) are appended to the shell command. The stdin\n     --\tand stdout of the hook are treated as if `pack-objects` itself\n     --\twas run. I.e., `upload-pack` will feed input intended for\n     --\t`pack-objects` to the hook, and expects a completed packfile on\n     --\tstdout.\n     --+\n     +@@ Documentation/config/uploadpack.txt: uploadpack.packObjectsHook::\n     + \t`pack-objects` to the hook, and expects a completed packfile on\n     + \tstdout.\n     + +\n      -Note that this configuration variable is ignored if it is seen in the\n      -repository-level config (this is a safety measure against fetching from\n      -untrusted repositories).\n     -+\t'(Protected config only)' If this option is set, when\n     -+\t`upload-pack` would run `git pack-objects` to create a packfile\n     -+\tfor a client, it will run this shell command instead. The\n     -+\t`pack-objects` command and arguments it _would_ have run\n     -+\t(including the `git pack-objects` at the beginning) are appended\n     -+\tto the shell command. The stdin and stdout of the hook are\n     -+\ttreated as if `pack-objects` itself was run. I.e., `upload-pack`\n     -+\twill feed input intended for `pack-objects` to the hook, and\n     -+\texpects a completed packfile on stdout.\n     ++Note that this configuration variable is only respected when it is specified\n     ++in protected config (see <<SCOPES>>). This is a safety measure against\n     ++fetching from untrusted repositories.\n       \n       uploadpack.allowFilter::\n       \tIf this option is set, `upload-pack` will support partial\n      \n     - ## upload-pack.c ##\n     -@@ upload-pack.c: static int upload_pack_config(const char *var, const char *value, void *cb_data)\n     - \t\tdata->advertise_sid = git_config_bool(var, value);\n     - \t}\n     + ## Documentation/git-config.txt ##\n     +@@ Documentation/git-config.txt: You can change the way options are read/written by specifying the path to a\n     + file (`--file`), or by specifying a configuration scope (`--system`,\n     + `--global`, `--local`, `--worktree`); see <<OPTIONS>> above.\n       \n     --\tif (current_config_scope() != CONFIG_SCOPE_LOCAL &&\n     --\t    current_config_scope() != CONFIG_SCOPE_WORKTREE) {\n     --\t\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n     --\t\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n     --\t}\n     --\n     - \tif (parse_object_filter_config(var, value, data) < 0)\n     - \t\treturn -1;\n     ++[[SCOPES]]\n     + SCOPES\n     + ------\n       \n     - \treturn parse_hide_refs_config(var, value, \"uploadpack\");\n     - }\n     +@@ Documentation/git-config.txt: Most configuration options are respected regardless of the scope it is\n     + defined in, but some options are only respected in certain scopes. See the\n     + option's documentation for the full details.\n       \n     -+static int upload_pack_protected_config(const char *var, const char *value, void *cb_data)\n     -+{\n     -+\tstruct upload_pack_data *data = cb_data;\n     ++Protected config\n     ++~~~~~~~~~~~~~~~~\n      +\n     -+\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n     -+\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n     -+\treturn 0;\n     -+}\n     ++Protected config refers to the 'system', 'global', and 'command' scopes. Git\n     ++considers these scopes to be especially trustworthy because they are likely\n     ++to be controlled by the user or a trusted administrator. An attacker who\n     ++controls these scopes can do substantial harm without using Git, so it is\n     ++assumed that the user's environment protects these scopes against attackers.\n      +\n     - void upload_pack(const int advertise_refs, const int stateless_rpc,\n     - \t\t const int timeout)\n     - {\n     -@@ upload-pack.c: void upload_pack(const int advertise_refs, const int stateless_rpc,\n     - \tupload_pack_data_init(&data);\n     - \n     - \tgit_config(upload_pack_config, &data);\n     -+\tgit_protected_config(upload_pack_protected_config, &data);\n     - \n     - \tdata.stateless_rpc = stateless_rpc;\n     - \tdata.timeout = timeout;\n     -@@ upload-pack.c: int upload_pack_v2(struct repository *r, struct packet_reader *request)\n     - \tdata.use_sideband = LARGE_PACKET_MAX;\n     - \n     - \tgit_config(upload_pack_config, &data);\n     -+\tgit_protected_config(upload_pack_protected_config, &data);\n     ++For security reasons, certain options are only respected when they are\n     ++specified in protected config, and ignored otherwise.\n     ++\n     + ENVIRONMENT\n     + -----------\n       \n     - \twhile (state != FETCH_DONE) {\n     - \t\tswitch (state) {\n 2:  7499a280961 ! 3:  94b40907e66 config: read protected config with `git_protected_config()`\n     @@ Metadata\n       ## Commit message ##\n          config: read protected config with `git_protected_config()`\n      \n     -    Protected config is read using `read_very_early_config()`, which has\n     -    several downsides:\n     -\n     -    - Every call to `read_very_early_config()` parses global and\n     -      system-level config files anew, but this can be optimized by just\n     -      parsing them once [1].\n     -    - Protected variables should respect \"-c\" because we can reasonably\n     -      assume that it comes from the user. But, `read_very_early_config()`\n     -      can't use \"-c\" because it is called so early that it does not have\n     -      access to command line arguments.\n     -\n     -    Introduce `git_protected_config()`, which reads protected config and\n     -    caches the values in `the_repository.protected_config`. Then, refactor\n     -    `safe.directory` to use `git_protected_config()`.\n     -\n     -    This implementation can still be improved, however:\n     -\n     -    - `git_protected_config()` iterates through every variable in\n     -      `the_repository.protected_config`, which may still be too expensive to\n     -      be called in every \"git\" invocation. There exist constant time lookup\n     -      functions for non-protected config (repo_config_get_*()), but for\n     -      simplicity, this commit does not implement similar functions for\n     -      protected config.\n     -\n     -    - Protected config is stored in `the_repository` so that we don't need\n     -      to statically allocate it. But this might be confusing since protected\n     -      config ignores repository config by definition.\n     -\n     -    [1] While `git_protected_config()` should save on file I/O, I wasn't\n     -    able to measure a meaningful difference between that and\n     -    `read_very_early_config()` on my machine (which has an SSD).\n     +    `uploadpack.packObjectsHook` is the only 'protected config only'\n     +    variable today, but we've noted that `safe.directory` and the upcoming\n     +    `discovery.bare` should also be 'protected config only'. So, for\n     +    consistency, we'd like to have a single implementation for protected\n     +    config.\n     +\n     +    The primary constraints are:\n     +\n     +    1. Reading from protected config should be as fast as possible. Nearly\n     +       all \"git\" commands inside a bare repository will read both\n     +       `safe.directory` and `discovery.bare`, so we cannot afford to be\n     +       slow.\n     +\n     +    2. Protected config must be readable when the gitdir is not known.\n     +       `safe.directory` and `discovery.bare` both affect repository\n     +       discovery and the gitdir is not known at that point [1].\n     +\n     +    The chosen implementation in this commit is to read protected config and\n     +    cache the values in a global configset. This is similar to the caching\n     +    behavior we get with the_repository->config.\n     +\n     +    Introduce git_protected_config(), which reads protected config and\n     +    caches them in the global configset protected_config. Then, refactor\n     +    `uploadpack.packObjectsHook` to use git_protected_config().\n     +\n     +    The protected config functions are named similarly to their\n     +    non-protected counterparts, e.g. git_protected_config_check_init() vs\n     +    git_config_check_init().\n     +\n     +    In light of constraint 1, this implementation can still be improved\n     +    since git_protected_config() iterates through every variable in\n     +    protected_config, which may still be too expensive. There exist constant\n     +    time lookup functions for non-protected config (repo_config_get_*()),\n     +    but for simplicity, this commit does not implement similar functions for\n     +    protected config.\n     +\n     +    An alternative that avoids introducing another configset is to continue\n     +    to read all config using git_config(), but only accept values that have\n     +    the correct config scope [2]. This technically fulfills constraint 2,\n     +    because git_config() simply ignores the local and worktree config when\n     +    the gitdir is not known. However, this would read incomplete config into\n     +    the_repository->config, which would need to be reset when the gitdir is\n     +    known and git_config() needs to read the local and worktree config.\n     +    Resetting the_repository->config might be reasonable while we only have\n     +    these 'protected config only' variables, but it's not clear whether this\n     +    extends well to future variables.\n     +\n     +    [1] In this case, we do have a candidate gitdir though, so with a little\n     +    refactoring, it might be possible to provide a gitdir.\n     +    [2] This is how `uploadpack.packObjectsHook` was implemented prior to\n     +    this commit.\n      \n          Signed-off-by: Glen Choo <chooglen@google.com>\n      \n       ## config.c ##\n     +@@ config.c: static enum config_scope current_parsing_scope;\n     + static int pack_compression_seen;\n     + static int zlib_compression_seen;\n     + \n     ++/*\n     ++ * Config that comes from trusted sources, namely:\n     ++ * - system config files (e.g. /etc/gitconfig)\n     ++ * - global config files (e.g. $HOME/.gitconfig,\n     ++ *   $XDG_CONFIG_HOME/git)\n     ++ * - the command line.\n     ++ *\n     ++ * This is declared here for code cleanliness, but unlike the other\n     ++ * static variables, this does not hold config parser state.\n     ++ */\n     ++static struct config_set protected_config;\n     ++\n     + static int config_file_fgetc(struct config_source *conf)\n     + {\n     + \treturn getc_unlocked(conf->u.file);\n     +@@ config.c: int git_configset_add_file(struct config_set *cs, const char *filename)\n     + \treturn git_config_from_file(config_set_callback, filename, cs);\n     + }\n     + \n     ++int git_configset_add_parameters(struct config_set *cs)\n     ++{\n     ++\treturn git_config_from_parameters(config_set_callback, cs);\n     ++}\n     ++\n     + int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n     + {\n     + \tconst struct string_list *values = NULL;\n      @@ config.c: int repo_config_get_pathname(struct repository *repo,\n       \treturn ret;\n       }\n       \n     -+/* Read protected config into the_repository->protected_config. */\n     ++/* Read values into protected_config. */\n      +static void read_protected_config(void)\n      +{\n      +\tchar *xdg_config = NULL, *user_config = NULL, *system_config = NULL;\n      +\n     -+\tCALLOC_ARRAY(the_repository->protected_config, 1);\n     -+\tgit_configset_init(the_repository->protected_config);\n     ++\tgit_configset_init(&protected_config);\n      +\n      +\tsystem_config = git_system_config();\n      +\tgit_global_config(&user_config, &xdg_config);\n      +\n     -+\tgit_configset_add_file(the_repository->protected_config, system_config);\n     -+\tgit_configset_add_file(the_repository->protected_config, xdg_config);\n     -+\tgit_configset_add_file(the_repository->protected_config, user_config);\n     ++\tgit_configset_add_file(&protected_config, system_config);\n     ++\tgit_configset_add_file(&protected_config, xdg_config);\n     ++\tgit_configset_add_file(&protected_config, user_config);\n     ++\tgit_configset_add_parameters(&protected_config);\n      +\n      +\tfree(system_config);\n      +\tfree(xdg_config);\n      +\tfree(user_config);\n      +}\n      +\n     -+/* Ensure that the_repository->protected_config has been initialized. */\n     ++/* Ensure that protected_config has been initialized. */\n      +static void git_protected_config_check_init(void)\n      +{\n     -+\tif (the_repository->protected_config &&\n     -+\t    the_repository->protected_config->hash_initialized)\n     ++\tif (protected_config.hash_initialized)\n      +\t\treturn;\n      +\tread_protected_config();\n      +}\n     @@ config.c: int repo_config_get_pathname(struct repository *repo,\n      +void git_protected_config(config_fn_t fn, void *data)\n      +{\n      +\tgit_protected_config_check_init();\n     -+\tconfigset_iter(the_repository->protected_config, fn, data);\n     ++\tconfigset_iter(&protected_config, fn, data);\n      +}\n      +\n       /* Functions used historically to read configuration from 'the_repository' */\n     @@ config.c: int repo_config_get_pathname(struct repository *repo,\n       {\n      \n       ## config.h ##\n     +@@ config.h: void git_configset_init(struct config_set *cs);\n     +  */\n     + int git_configset_add_file(struct config_set *cs, const char *filename);\n     + \n     ++/**\n     ++ * Parses command line options and environment variables, and adds the\n     ++ * variable-value pairs to the `config_set`. Returns 0 on success, or -1\n     ++ * if there is an error in parsing. The caller decides whether to free\n     ++ * the incomplete configset or continue using it when the function\n     ++ * returns -1.\n     ++ */\n     ++int git_configset_add_parameters(struct config_set *cs);\n     ++\n     + /**\n     +  * Finds and returns the value list, sorted in order of increasing priority\n     +  * for the configuration variable `key` and config set `cs`. When the\n      @@ config.h: int repo_config_get_maybe_bool(struct repository *repo,\n       int repo_config_get_pathname(struct repository *repo,\n       \t\t\t     const char *key, const char **dest);\n     @@ config.h: int repo_config_get_maybe_bool(struct repository *repo,\n        * Querying For Specific Variables\n        * -------------------------------\n      \n     - ## repository.c ##\n     -@@ repository.c: void repo_clear(struct repository *repo)\n     - \t\tFREE_AND_NULL(repo->remote_state);\n     - \t}\n     - \n     -+\tif (repo->protected_config) {\n     -+\t\tgit_configset_clear(repo->protected_config);\n     -+\t\tFREE_AND_NULL(repo->protected_config);\n     -+\t}\n     + ## t/t5544-pack-objects-hook.sh ##\n     +@@ t/t5544-pack-objects-hook.sh: test_expect_success 'hook does not run from repo config' '\n     + \t! grep \"hook running\" stderr &&\n     + \ttest_path_is_missing .git/hook.args &&\n     + \ttest_path_is_missing .git/hook.stdin &&\n     +-\ttest_path_is_missing .git/hook.stdout\n     ++\ttest_path_is_missing .git/hook.stdout &&\n      +\n     - \trepo_clear_path_cache(&repo->cached_paths);\n     - }\n     ++\t# check that global config is used instead\n     ++\ttest_config_global uploadpack.packObjectsHook ./hook &&\n     ++\tgit clone --no-local . dst2.git 2>stderr &&\n     ++\tgrep \"hook running\" stderr\n     + '\n       \n     + test_expect_success 'hook works with partial clone' '\n      \n     - ## repository.h ##\n     -@@ repository.h: struct repository {\n     + ## upload-pack.c ##\n     +@@ upload-pack.c: static int upload_pack_config(const char *var, const char *value, void *cb_data)\n     + \t\tdata->advertise_sid = git_config_bool(var, value);\n     + \t}\n       \n     - \tstruct repo_settings settings;\n     +-\tif (current_config_scope() != CONFIG_SCOPE_LOCAL &&\n     +-\t    current_config_scope() != CONFIG_SCOPE_WORKTREE) {\n     +-\t\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n     +-\t\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n     +-\t}\n     +-\n     + \tif (parse_object_filter_config(var, value, data) < 0)\n     + \t\treturn -1;\n     + \n     + \treturn parse_hide_refs_config(var, value, \"uploadpack\");\n     + }\n       \n     -+\t/*\n     -+\t * Config that comes from trusted sources, namely\n     -+\t * - system config files (e.g. /etc/gitconfig)\n     -+\t * - global config files (e.g. $HOME/.gitconfig,\n     -+\t *   $XDG_CONFIG_HOME/git)\n     -+\t */\n     -+\tstruct config_set *protected_config;\n     ++static int upload_pack_protected_config(const char *var, const char *value, void *cb_data)\n     ++{\n     ++\tstruct upload_pack_data *data = cb_data;\n     ++\n     ++\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n     ++\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n     ++\treturn 0;\n     ++}\n     ++\n     ++static void get_upload_pack_config(struct upload_pack_data *data)\n     ++{\n     ++\tgit_config(upload_pack_config, data);\n     ++\tgit_protected_config(upload_pack_protected_config, data);\n     ++}\n      +\n     - \t/* Subsystems */\n     - \t/*\n     - \t * Repository's config which contains key-value pairs from the usual\n     -\n     - ## setup.c ##\n     -@@ setup.c: static int ensure_valid_ownership(const char *path)\n     - \t    is_path_owned_by_current_user(path))\n     - \t\treturn 1;\n     + void upload_pack(const int advertise_refs, const int stateless_rpc,\n     + \t\t const int timeout)\n     + {\n     +@@ upload-pack.c: void upload_pack(const int advertise_refs, const int stateless_rpc,\n     + \tstruct upload_pack_data data;\n       \n     --\tread_very_early_config(safe_directory_cb, &data);\n     -+\tgit_protected_config(safe_directory_cb, &data);\n     + \tupload_pack_data_init(&data);\n     +-\n     +-\tgit_config(upload_pack_config, &data);\n     ++\tget_upload_pack_config(&data);\n       \n     - \treturn data.is_safe;\n     - }\n     + \tdata.stateless_rpc = stateless_rpc;\n     + \tdata.timeout = timeout;\n     +@@ upload-pack.c: int upload_pack_v2(struct repository *r, struct packet_reader *request)\n     + \n     + \tupload_pack_data_init(&data);\n     + \tdata.use_sideband = LARGE_PACKET_MAX;\n     +-\n     +-\tgit_config(upload_pack_config, &data);\n     ++\tget_upload_pack_config(&data);\n     + \n     + \twhile (state != FETCH_DONE) {\n     + \t\tswitch (state) {\n 4:  66a0a208176 ! 4:  156817966fa config: include \"-c\" in protected config\n     @@ Metadata\n      Author: Glen Choo <chooglen@google.com>\n      \n       ## Commit message ##\n     -    config: include \"-c\" in protected config\n     +    safe.directory: use git_protected_config()\n      \n     -    Protected config should include the command line (aka \"-c\") because we\n     -    can be quite certain that this config is specified by the user.\n     -\n     -    Introduce a function, `git_configset_add_parameters()`, that adds \"-c\"\n     -    config to a config_set, and use it to add \"-c\" to protected config.\n     +    Use git_protected_config() to read `safe.directory` instead of\n     +    read_very_early_config(), making it 'protected config only'. As a\n     +    result, `safe.directory` now respects \"-c\", so update the tests and docs\n     +    accordingly.\n      \n          Signed-off-by: Glen Choo <chooglen@google.com>\n      \n     - ## Documentation/config.txt ##\n     -@@ Documentation/config.txt: names do not conflict with those that are used by Git itself and\n     - other popular tools, and describe them in your documentation.\n     - \n     - Variables marked with '(Protected config only)' are only respected when\n     --they are specified in protected configuration. This includes global and\n     --system-level config, and excludes repository config, the command line\n     --option `-c`, and environment variables. For more details, see the\n     -+they are specified in protected configuration. This includes global,\n     -+system-level config, the command line option `-c`, and environment\n     -+variables, and excludes repository config. For more details, see the\n     - 'protected configuration' entry in linkgit:gitglossary[7].\n     - \n     - include::config/advice.txt[]\n     -\n     - ## Documentation/glossary-content.txt ##\n     -@@ Documentation/glossary-content.txt: Protected configuration includes:\n     - - system-level config, e.g. `/etc/git/config`\n     - - global config, e.g. `$XDG_CONFIG_HOME/git/config` and\n     -   `$HOME/.gitconfig`\n     -+- the command line option `-c` and its equivalent environment variables\n     + ## Documentation/config/safe.txt ##\n     +@@ Documentation/config/safe.txt: via `git config --add`. To reset the list of safe directories (e.g. to\n     + override any such directories specified in the system config), add a\n     + `safe.directory` entry with an empty value.\n       +\n     - Protected configuration excludes:\n     +-This config setting is only respected when specified in a system or global\n     +-config, not when it is specified in a repository config, via the command\n     +-line option `-c safe.directory=<path>`, or in environment variables.\n     ++This config setting is only respected in protected configuration (see\n     ++<<SCOPES>>). This prevents the untrusted repository from tampering with this\n     ++value.\n       +\n     - - repository config, e.g. `$GIT_DIR/config` and\n     -   `$GIT_DIR/config.worktree`\n     --- the command line option `-c` and its equivalent environment variables\n     - \n     - [[def_reachable]]reachable::\n     - \tAll of the ancestors of a given <<def_commit,commit>> are said to be\n     + The value of this setting is interpolated, i.e. `~/<path>` expands to a\n     + path relative to the home directory and `%(prefix)/<path>` expands to a\n      \n     - ## config.c ##\n     -@@ config.c: int git_configset_add_file(struct config_set *cs, const char *filename)\n     - \treturn git_config_from_file(config_set_callback, filename, cs);\n     - }\n     + ## setup.c ##\n     +@@ setup.c: static int ensure_valid_ownership(const char *path)\n     + \t    is_path_owned_by_current_user(path))\n     + \t\treturn 1;\n       \n     -+int git_configset_add_parameters(struct config_set *cs)\n     -+{\n     -+\treturn git_config_from_parameters(config_set_callback, cs);\n     -+}\n     -+\n     - int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n     - {\n     - \tconst struct string_list *values = NULL;\n     -@@ config.c: static void read_protected_config(void)\n     - \tgit_configset_add_file(the_repository->protected_config, system_config);\n     - \tgit_configset_add_file(the_repository->protected_config, xdg_config);\n     - \tgit_configset_add_file(the_repository->protected_config, user_config);\n     -+\tgit_configset_add_parameters(the_repository->protected_config);\n     +-\tread_very_early_config(safe_directory_cb, &data);\n     ++\tgit_protected_config(safe_directory_cb, &data);\n       \n     - \tfree(system_config);\n     - \tfree(xdg_config);\n     -\n     - ## config.h ##\n     -@@ config.h: void git_configset_init(struct config_set *cs);\n     -  */\n     - int git_configset_add_file(struct config_set *cs, const char *filename);\n     - \n     -+/**\n     -+ * Parses command line options and environment variables, and adds the\n     -+ * variable-value pairs to the `config_set`. Returns 0 on success, or -1\n     -+ * if there is an error in parsing. The caller decides whether to free\n     -+ * the incomplete configset or continue using it when the function\n     -+ * returns -1.\n     -+ */\n     -+int git_configset_add_parameters(struct config_set *cs);\n     -+\n     - /**\n     -  * Finds and returns the value list, sorted in order of increasing priority\n     -  * for the configuration variable `key` and config set `cs`. When the\n     + \treturn data.is_safe;\n     + }\n      \n       ## t/t0033-safe-directory.sh ##\n      @@ t/t0033-safe-directory.sh: test_expect_success 'safe.directory is not set' '\n     @@ t/t0033-safe-directory.sh: test_expect_success 'safe.directory is not set' '\n       '\n       \n       test_expect_success 'ignoring safe.directory in repo config' '\n     -\n     - ## t/t0035-discovery-bare.sh ##\n     -@@ t/t0035-discovery-bare.sh: test_expect_success 'discovery.bare on the command line' '\n     - \tgit config --global discovery.bare never &&\n     - \t(\n     - \t\tcd outer-repo/bare-repo &&\n     --\t\ttest_must_fail git -c discovery.bare=always rev-parse --git-dir 2>err &&\n     --\t\tgrep \"discovery.bare\" err\n     -+\t\tgit -c discovery.bare=always rev-parse --git-dir\n     - \t)\n     - '\n     - \n 3:  d5a3e9f9845 ! 5:  29053d029f8 setup.c: create `discovery.bare`\n     @@ setup.c\n       static int inside_git_dir = -1;\n       static int inside_work_tree = -1;\n       static int work_tree_config_is_bogus;\n     -+enum discovery_bare_config {\n     -+\tDISCOVERY_BARE_UNKNOWN = -1,\n     ++enum discovery_bare_allowed {\n      +\tDISCOVERY_BARE_NEVER = 0,\n      +\tDISCOVERY_BARE_ALWAYS,\n      +};\n     -+static enum discovery_bare_config discovery_bare_config =\n     -+\tDISCOVERY_BARE_UNKNOWN;\n       \n       static struct startup_info the_startup_info;\n       struct startup_info *startup_info = &the_startup_info;\n     @@ setup.c: static int ensure_valid_ownership(const char *path)\n       \n      +static int discovery_bare_cb(const char *key, const char *value, void *d)\n      +{\n     ++\tenum discovery_bare_allowed *discovery_bare_allowed = d;\n     ++\n      +\tif (strcmp(key, \"discovery.bare\"))\n      +\t\treturn 0;\n      +\n      +\tif (!strcmp(value, \"never\")) {\n     -+\t\tdiscovery_bare_config = DISCOVERY_BARE_NEVER;\n     ++\t\t*discovery_bare_allowed = DISCOVERY_BARE_NEVER;\n      +\t\treturn 0;\n      +\t}\n      +\tif (!strcmp(value, \"always\")) {\n     -+\t\tdiscovery_bare_config = DISCOVERY_BARE_ALWAYS;\n     ++\t\t*discovery_bare_allowed = DISCOVERY_BARE_ALWAYS;\n      +\t\treturn 0;\n      +\t}\n      +\treturn -1;\n      +}\n      +\n     -+static int check_bare_repo_allowed(void)\n     ++static enum discovery_bare_allowed get_discovery_bare(void)\n      +{\n     -+\tif (discovery_bare_config == DISCOVERY_BARE_UNKNOWN) {\n     -+\t\tdiscovery_bare_config = DISCOVERY_BARE_ALWAYS;\n     -+\t\tgit_protected_config(discovery_bare_cb, NULL);\n     -+\t}\n     -+\tswitch (discovery_bare_config) {\n     -+\tcase DISCOVERY_BARE_NEVER:\n     -+\t\treturn 0;\n     -+\tcase DISCOVERY_BARE_ALWAYS:\n     -+\t\treturn 1;\n     -+\tcase DISCOVERY_BARE_UNKNOWN:\n     -+\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n     -+\t}\n     -+\treturn 0;\n     ++\tenum discovery_bare_allowed result = DISCOVERY_BARE_ALWAYS;\n     ++\tgit_protected_config(discovery_bare_cb, &result);\n     ++\treturn result;\n      +}\n      +\n     -+static const char *discovery_bare_config_to_string(void)\n     ++static const char *discovery_bare_allowed_to_string(\n     ++\tenum discovery_bare_allowed discovery_bare_allowed)\n      +{\n     -+\tswitch (discovery_bare_config) {\n     ++\tswitch (discovery_bare_allowed) {\n      +\tcase DISCOVERY_BARE_NEVER:\n      +\t\treturn \"never\";\n      +\tcase DISCOVERY_BARE_ALWAYS:\n      +\t\treturn \"always\";\n     -+\tcase DISCOVERY_BARE_UNKNOWN:\n     -+\t\tBUG(\"invalid discovery_bare_config %d\", discovery_bare_config);\n     ++\tdefault:\n     ++\t\tBUG(\"invalid discovery_bare_allowed %d\",\n     ++\t\t    discovery_bare_allowed);\n      +\t}\n      +\treturn NULL;\n      +}\n     @@ setup.c: static enum discovery_result setup_git_directory_gently_1(struct strbuf\n       \t\t}\n       \n       \t\tif (is_git_directory(dir->buf)) {\n     -+\t\t\tif (!check_bare_repo_allowed())\n     ++\t\t\tif (!get_discovery_bare())\n      +\t\t\t\treturn GIT_DIR_DISALLOWED_BARE;\n       \t\t\tif (!ensure_valid_ownership(dir->buf))\n       \t\t\t\treturn GIT_DIR_INVALID_OWNERSHIP;\n     @@ setup.c: const char *setup_git_directory_gently(int *nongit_ok)\n      +\t\tif (!nongit_ok) {\n      +\t\t\tdie(_(\"cannot use bare repository '%s' (discovery.bare is '%s')\"),\n      +\t\t\t    dir.buf,\n     -+\t\t\t    discovery_bare_config_to_string());\n     ++\t\t\t    discovery_bare_allowed_to_string(get_discovery_bare()));\n      +\t\t}\n      +\t\t*nongit_ok = 1;\n      +\t\tbreak;\n     @@ t/t0035-discovery-bare.sh (new)\n      +\n      +pwd=\"$(pwd)\"\n      +\n     ++expect_accepted () {\n     ++\tgit \"$@\" rev-parse --git-dir\n     ++}\n     ++\n      +expect_rejected () {\n     -+\ttest_must_fail git rev-parse --git-dir 2>err &&\n     ++\ttest_must_fail git \"$@\" rev-parse --git-dir 2>err &&\n      +\tgrep \"discovery.bare\" err\n      +}\n      +\n     @@ t/t0035-discovery-bare.sh (new)\n      +test_expect_success 'discovery.bare unset' '\n      +\t(\n      +\t\tcd outer-repo/bare-repo &&\n     -+\t\tgit rev-parse --git-dir\n     ++\t\texpect_accepted\n      +\t)\n      +'\n      +\n     @@ t/t0035-discovery-bare.sh (new)\n      +\tgit config --global discovery.bare always &&\n      +\t(\n      +\t\tcd outer-repo/bare-repo &&\n     -+\t\tgit rev-parse --git-dir\n     ++\t\texpect_accepted\n      +\t)\n      +'\n      +\n     @@ t/t0035-discovery-bare.sh (new)\n      +\tgit config --global discovery.bare never &&\n      +\t(\n      +\t\tcd outer-repo/bare-repo &&\n     -+\t\ttest_must_fail git -c discovery.bare=always rev-parse --git-dir 2>err &&\n     -+\t\tgrep \"discovery.bare\" err\n     ++\t\texpect_accepted -c discovery.bare=always &&\n     ++\t\texpect_rejected -c discovery.bare=\n      +\t)\n      +'\n      +\n\n-- \ngitgitgadget\n"},{"id":"456831","messageId":"a5a1dcb03e18f3b9f3fd77ef94c17a05763a5f13.1654635432.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v4.git.git.1654635432.gitgitgadget@gmail.com","subject":"[PATCH v4 2/5] Documentation: define protected configuration","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-07T20:57:09Z","receivedAt":"2022-06-08T00:33:40Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nFor security reasons, there are config variables that are only trusted\nwhen they are specified in extra-trustworthy configuration scopes, which\nare sometimes referred to on-list as 'protected configuration' [1]. A\nfuture commit will introduce another such variable, so let's define our\nterms so that we can have consistent documentation and implementation.\n\nIn our documentation, define 'protected config' as the system, global\nand command config scopes. As a shorthand, I will refer to variables\nthat are only respected in protected config as 'protected config only',\nbut this term is not used in the documentation.\n\nThis definition of protected configuration is based on whether or not\nGit can reasonably protect the user by ignoring the configuration scope:\n\n- System, global and command line config are considered protected\n  because an attacker who has control over any of those can do plenty of\n  harm without Git, so we gain very little by ignoring those scopes.\n- On the other hand, local (and similarly, worktree) config are not\n  considered protected because it is relatively easy for an attacker to\n  control local config, e.g.:\n  - On some shared user environments, a non-admin attacker can create a\n    repository high up the directory hierarchy (e.g. C:\\.git on Windows),\n    and a user may accidentally use it when their PS1 automatically\n    invokes \"git\" commands.\n\n    `safe.directory` prevents attacks of this form by making sure that\n    the user intended to use the shared repository. It obviously\n    shouldn't be read from the repository, because that would end up\n    trusting the repository that Git was supposed to reject.\n  - \"git upload-pack\" is expected to run in repositories that may not be\n    controlled by the user. We cannot ignore all config in that\n    repository (because \"git upload-pack\" would fail), but we can limit\n    the risks by ignoring `uploadpack.packObjectsHook`.\n\nOnly `uploadpack.packObjectsHook` is 'protected config only'. The\nfollowing variables are intentionally excluded:\n\n- `safe.directory` should be 'protected config only', but it does not\n  technically fit the definition because it is not respected in the\n  \"command\" scope. A future commit will fix this.\n\n- `trace2.*` happens to read the same scopes as `safe.directory` because\n  they share an implementation. However, this is not for security\n  reasons; it is because we want to start tracing so early that\n  repository-level config and \"-c\" are not available [2].\n\n  This requirement is unique to `trace2.*`, so it does not makes sense\n  for protected configuration to be subject to the same constraints.\n\n[1] For example,\nhttps://lore.kernel.org/git/6af83767-576b-75c4-c778-0284344a8fe7@github.com/\n[2] https://lore.kernel.org/git/a0c89d0d-669e-bf56-25d2-cbb09b012e70@jeffhostetler.com/\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config/uploadpack.txt |  6 +++---\n Documentation/git-config.txt        | 13 +++++++++++++\n 2 files changed, 16 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/config/uploadpack.txt b/Documentation/config/uploadpack.txt\nindex 32fad5bbe81..029abbefdff 100644\n--- a/Documentation/config/uploadpack.txt\n+++ b/Documentation/config/uploadpack.txt\n@@ -49,9 +49,9 @@ uploadpack.packObjectsHook::\n \t`pack-objects` to the hook, and expects a completed packfile on\n \tstdout.\n +\n-Note that this configuration variable is ignored if it is seen in the\n-repository-level config (this is a safety measure against fetching from\n-untrusted repositories).\n+Note that this configuration variable is only respected when it is specified\n+in protected config (see <<SCOPES>>). This is a safety measure against\n+fetching from untrusted repositories.\n \n uploadpack.allowFilter::\n \tIf this option is set, `upload-pack` will support partial\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex 5e4c95f2423..2b4334faec9 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -343,6 +343,7 @@ You can change the way options are read/written by specifying the path to a\n file (`--file`), or by specifying a configuration scope (`--system`,\n `--global`, `--local`, `--worktree`); see <<OPTIONS>> above.\n \n+[[SCOPES]]\n SCOPES\n ------\n \n@@ -380,6 +381,18 @@ Most configuration options are respected regardless of the scope it is\n defined in, but some options are only respected in certain scopes. See the\n option's documentation for the full details.\n \n+Protected config\n+~~~~~~~~~~~~~~~~\n+\n+Protected config refers to the 'system', 'global', and 'command' scopes. Git\n+considers these scopes to be especially trustworthy because they are likely\n+to be controlled by the user or a trusted administrator. An attacker who\n+controls these scopes can do substantial harm without using Git, so it is\n+assumed that the user's environment protects these scopes against attackers.\n+\n+For security reasons, certain options are only respected when they are\n+specified in protected config, and ignored otherwise.\n+\n ENVIRONMENT\n -----------\n \n-- \ngitgitgadget\n\n"},{"id":"456832","messageId":"c0e27ab3b3e7344336580a2f5f34574a08a35a1f.1654635432.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v4.git.git.1654635432.gitgitgadget@gmail.com","subject":"[PATCH v4 1/5] Documentation/git-config.txt: add SCOPES section","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-07T20:57:08Z","receivedAt":"2022-06-08T00:33:47Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nIn a subsequent commit, we will introduce \"protected config\", which is\neasiest to describe in terms of configuration scopes (i.e. it's the\nunion of the 'system', 'global', and 'command' scopes). This description\nis fine for ML discussions, but it's inadequate for end users because we\ndon't provide a good description of \"config scopes\" in the public docs.\n\n145d59f482 (config: add '--show-scope' to print the scope of a config\nvalue, 2020-02-10) introduced the word \"scope\" to our public docs, but\nthat only enumerates the scopes and assumes the user can figure out\nthose values mean.\n\nAdd a SCOPES section to Documentation/git-config.txt that describes the\nconfig scopes, their corresponding CLI options, and mentions that some\nconfiguration options are only respected in certain scopes. Then,\nuse the word \"scope\" to simplify the FILES section and change some\nconfusing wording.\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/git-config.txt | 64 ++++++++++++++++++++++++++++--------\n 1 file changed, 50 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex bdcfd94b642..5e4c95f2423 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -297,8 +297,8 @@ The default is to use a pager.\n FILES\n -----\n \n-If not set explicitly with `--file`, there are four files where\n-'git config' will search for configuration options:\n+By default, 'git config' will read configuration options from multiple\n+files:\n \n $(prefix)/etc/gitconfig::\n \tSystem-wide configuration file.\n@@ -322,27 +322,63 @@ $GIT_DIR/config.worktree::\n \tThis is optional and is only searched when\n \t`extensions.worktreeConfig` is present in $GIT_DIR/config.\n \n-If no further options are given, all reading options will read all of these\n-files that are available. If the global or the system-wide configuration\n-file are not available they will be ignored. If the repository configuration\n-file is not available or readable, 'git config' will exit with a non-zero\n-error code. However, in neither case will an error message be issued.\n+You may also provide additional configuration parameters when running any\n+git command by using the `-c` option. See linkgit:git[1] for details.\n+\n+Options will be read from all of these files that are available. If the\n+global or the system-wide configuration file are not available they will be\n+ignored. If the repository configuration file is not available or readable,\n+'git config' will exit with a non-zero error code. However, in neither case\n+will an error message be issued.\n \n The files are read in the order given above, with last value found taking\n precedence over values read earlier.  When multiple values are taken then all\n values of a key from all files will be used.\n \n-You may override individual configuration parameters when running any git\n-command by using the `-c` option. See linkgit:git[1] for details.\n-\n-All writing options will per default write to the repository specific\n+By default, options are only written to the repository specific\n configuration file. Note that this also affects options like `--replace-all`\n and `--unset`. *'git config' will only ever change one file at a time*.\n \n-You can override these rules using the `--global`, `--system`,\n-`--local`, `--worktree`, and `--file` command-line options; see\n-<<OPTIONS>> above.\n+You can change the way options are read/written by specifying the path to a\n+file (`--file`), or by specifying a configuration scope (`--system`,\n+`--global`, `--local`, `--worktree`); see <<OPTIONS>> above.\n+\n+SCOPES\n+------\n+\n+Each configuration source falls within a configuration scope. The scopes\n+are:\n+\n+system::\n+\t$(prefix)/etc/gitconfig\n+\n+global::\n+\t$XDG_CONFIG_HOME/git/config\n++\n+~/.gitconfig\n+\n+local::\n+\t$GIT_DIR/config\n+\n+worktree::\n+\t$GIT_DIR/config.worktree\n+\n+command::\n+\tenvironment variables\n++\n+the `-c` option\n+\n+With the exception of 'command', each scope corresponds to a command line\n+option - `--system`, `--global`, `--local`, `--worktree`.\n+\n+When reading options, specifying a scope will only read options from the\n+files within that scope. When writing options, specifying a scope will write\n+to the files within that scope (instead of the repository specific\n+configuration file). See <<OPTIONS>> above for a complete description.\n \n+Most configuration options are respected regardless of the scope it is\n+defined in, but some options are only respected in certain scopes. See the\n+option's documentation for the full details.\n \n ENVIRONMENT\n -----------\n-- \ngitgitgadget\n\n"},{"id":"456835","messageId":"kl6lh74w6tot.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"29053d029f8ec61095a2ad557be38b1d485a158f.1654635432.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 5/5] setup.c: create `discovery.bare`","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-06-07T21:37:54Z","receivedAt":"2022-06-08T00:33:50Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"\"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> diff --git a/Documentation/config/discovery.txt b/Documentation/config/discovery.txt\n> new file mode 100644\n> index 00000000000..fbe93597e7c\n> --- /dev/null\n> +++ b/Documentation/config/discovery.txt\n> @@ -0,0 +1,19 @@\n> +discovery.bare::\n> +\t'(Protected config only)' Specifies whether Git will work with a\n> +\tbare repository that it found during repository discovery. This\n> +\thas no effect if the repository is specified directly via the\n> +\t--git-dir command-line option or the GIT_DIR environment\n> +\tvariable (see linkgit:git[1]).\n\nUgh, I forgot to update the docs for `discovery.bare`. This should be\nreworded to be consistent with `safe.directory` and\n`uploadpack.packObjectsHook`, e.g.\n\n   discovery.bare::\n   \tSpecifies whether Git will work with a bare repository that it found\n   \tduring repository discovery. This has no effect if the repository is\n   \tspecified directly via the --git-dir command-line option or the\n   \tGIT_DIR environment variable (see linkgit:git[1]).\n\n    This config setting is only respected in protected configuration\n    (see <<SCOPES>>). This prevents the untrusted repository from\n    tampering with this value.\n"},{"id":"456839","messageId":"xmqqbkv4t7gp.fsf@gitster.g","threadId":"57859","inReplyTo":"94b40907e66b2f6e0874ab49f8b73fdd58eb06d5.1654635432.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 3/5] config: read protected config with `git_protected_config()`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-06-07T22:49:26Z","receivedAt":"2022-06-08T01:21:06Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> diff --git a/upload-pack.c b/upload-pack.c\n> index 3a851b36066..09f48317b02 100644\n> --- a/upload-pack.c\n> +++ b/upload-pack.c\n> @@ -1321,18 +1321,27 @@ static int upload_pack_config(const char *var, const char *value, void *cb_data)\n>  \t\tdata->advertise_sid = git_config_bool(var, value);\n>  \t}\n>  \n> -\tif (current_config_scope() != CONFIG_SCOPE_LOCAL &&\n> -\t    current_config_scope() != CONFIG_SCOPE_WORKTREE) {\n> -\t\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n> -\t\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n> -\t}\n> -\n\nThe lossage of this block is because this general git_config()\ncallback routine that is used to read from any scope is no longer\nused to pick up the sensitive variable.  Instead, we need to get it\nwith a different API, namely, git_protected_config().\n\nIt is probably is good that in the new code we are not encouraging\nfolks to write random comparisons on current_config_scope(), and\ninstead uniformly use a git_protected_config().  That may promote\nconsistency.\n\nAn obvious alternative to achieve the same consistency would be to\nintroduce a helper, and rewrite (instead of removing) the above part\nlike so:\n\n\tif (in_protected_scope()) {\n\t\t... parse sensitive variable ...\n\t}\n\nWe would not need any other change to this file in this patch if we\ngo that route, I suspect.\n\n>  \tif (parse_object_filter_config(var, value, data) < 0)\n>  \t\treturn -1;\n>  \n>  \treturn parse_hide_refs_config(var, value, \"uploadpack\");\n>  }\n>  \n> +static int upload_pack_protected_config(const char *var, const char *value, void *cb_data)\n> +{\n> +\tstruct upload_pack_data *data = cb_data;\n> +\n> +\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n> +\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n> +\treturn 0;\n> +}\n> +\n> +static void get_upload_pack_config(struct upload_pack_data *data)\n> +{\n> +\tgit_config(upload_pack_config, data);\n> +\tgit_protected_config(upload_pack_protected_config, data);\n> +}\n\nWhere we used to just do git_config(upload_pack_config), we now need\nto do a separate git_protected_config().  It feels a bit wasteful to\niterate over the same configset twice, but it is not like we are\ndoing the IO and text file parsing multiple times.  This looks quite\nstraight-forward.\n"},{"id":"456849","messageId":"kl6lbkv46m1x.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"xmqqbkv4t7gp.fsf@gitster.g","subject":"Re: [PATCH v4 3/5] config: read protected config with `git_protected_config()`","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-06-08T00:22:50Z","receivedAt":"2022-06-08T02:48:02Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> \"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>> diff --git a/upload-pack.c b/upload-pack.c\n>> index 3a851b36066..09f48317b02 100644\n>> --- a/upload-pack.c\n>> +++ b/upload-pack.c\n>> @@ -1321,18 +1321,27 @@ static int upload_pack_config(const char *var, const char *value, void *cb_data)\n>>  \t\tdata->advertise_sid = git_config_bool(var, value);\n>>  \t}\n>>  \n>> -\tif (current_config_scope() != CONFIG_SCOPE_LOCAL &&\n>> -\t    current_config_scope() != CONFIG_SCOPE_WORKTREE) {\n>> -\t\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n>> -\t\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n>> -\t}\n>> -\n>\n> The lossage of this block is because this general git_config()\n> callback routine that is used to read from any scope is no longer\n> used to pick up the sensitive variable.  Instead, we need to get it\n> with a different API, namely, git_protected_config().\n>\n> It is probably is good that in the new code we are not encouraging\n> folks to write random comparisons on current_config_scope(), and\n> instead uniformly use a git_protected_config().  That may promote\n> consistency.\n>\n> An obvious alternative to achieve the same consistency would be to\n> introduce a helper, and rewrite (instead of removing) the above part\n> like so:\n>\n> \tif (in_protected_scope()) {\n> \t\t... parse sensitive variable ...\n> \t}\n>\n> We would not need any other change to this file in this patch if we\n> go that route, I suspect.\n\nYes, and as noted in the commit message, this approach seems to work for\n`safe.directory` and `discovery.bare` too.\n\n>>  \tif (parse_object_filter_config(var, value, data) < 0)\n>>  \t\treturn -1;\n>>  \n>>  \treturn parse_hide_refs_config(var, value, \"uploadpack\");\n>>  }\n>>  \n>> +static int upload_pack_protected_config(const char *var, const char *value, void *cb_data)\n>> +{\n>> +\tstruct upload_pack_data *data = cb_data;\n>> +\n>> +\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n>> +\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n>> +\treturn 0;\n>> +}\n>> +\n>> +static void get_upload_pack_config(struct upload_pack_data *data)\n>> +{\n>> +\tgit_config(upload_pack_config, data);\n>> +\tgit_protected_config(upload_pack_protected_config, data);\n>> +}\n>\n> Where we used to just do git_config(upload_pack_config), we now need\n> to do a separate git_protected_config().  It feels a bit wasteful to\n> iterate over the same configset twice, but it is not like we are\n> doing the IO and text file parsing multiple times.  This looks quite\n> straight-forward.\n\nYeah it's not optimal, but at the very least, I think it's easy enough\nto understand that we could replace it with something more economical in\nthe future.\n"},{"id":"457751","messageId":"20220622215848.3733308-1-jonathantanmy@google.com","threadId":"57859","inReplyTo":"a5a1dcb03e18f3b9f3fd77ef94c17a05763a5f13.1654635432.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 2/5] Documentation: define protected configuration","fromName":"Jonathan Tan","fromEmail":"jonathantanmy@google.com","sentAt":"2022-06-22T21:58:47Z","receivedAt":"2022-06-22T22:01:23Z","isPatch":true,"sender":{"key":"jonathantanmy@fastmail.com","avatar":null},"body":"\"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> From: Glen Choo <chooglen@google.com>\n> \n> For security reasons, there are config variables that are only trusted\n> when they are specified in extra-trustworthy configuration scopes, which\n\nProbably better to delete \"extra-trustworthy\", or at least \"extra-\" -\nit's better to explain why and how they're trustworthy, which you have\nalready done in the commit message.\n\n> diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\n> index 5e4c95f2423..2b4334faec9 100644\n> --- a/Documentation/git-config.txt\n> +++ b/Documentation/git-config.txt\n\n[snip]\n\n> +Protected config refers to the 'system', 'global', and 'command' scopes. Git\n> +considers these scopes to be especially trustworthy because they are likely\n> +to be controlled by the user or a trusted administrator. An attacker who\n> +controls these scopes can do substantial harm without using Git, so it is\n> +assumed that the user's environment protects these scopes against attackers.\n> +\n> +For security reasons, certain options are only respected when they are\n> +specified in protected config, and ignored otherwise.\n\nAlso \"especially trustworthy\" here.\n"},{"id":"457752","messageId":"20220622220331.3734584-1-jonathantanmy@google.com","threadId":"57859","inReplyTo":"pull.1261.v4.git.git.1654635432.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 0/5] config: introduce discovery.bare and protected config","fromName":"Jonathan Tan","fromEmail":"jonathantanmy@google.com","sentAt":"2022-06-22T22:03:31Z","receivedAt":"2022-06-22T22:03:37Z","isPatch":true,"sender":{"key":"jonathantanmy@fastmail.com","avatar":null},"body":"\"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> Glen Choo (5):\n>   Documentation/git-config.txt: add SCOPES section\n>   Documentation: define protected configuration\n\nForgot to mention when I was sending my comments on patch 2: we should\nstandardize on \"protected config\" and not use \"protected configuration\"\nanywhere.\n\n>   config: read protected config with `git_protected_config()`\n>   safe.directory: use git_protected_config()\n>   setup.c: create `discovery.bare`\n\nThanks - I think this is a nice feature to have. Everything looks good\nexcept for some minor comments on text in patch 2, which I have sent.\n\nOne alternative design would have been to have separate configsets for\nprotected config and non-protected config (or even better, separate\nconfigsets for trace2 config, protected config minus trace2 config, and\nnon-protected config) but that doesn't have to block the submission of\nthis patch set.\n"},{"id":"457806","messageId":"kl6lr13fi9qn.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"20220622220331.3734584-1-jonathantanmy@google.com","subject":"Re: [PATCH v4 0/5] config: introduce discovery.bare and protected config","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-06-23T17:13:20Z","receivedAt":"2022-06-23T17:53:59Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Jonathan Tan <jonathantanmy@google.com> writes:\n\n> \"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>> Glen Choo (5):\n>>   Documentation/git-config.txt: add SCOPES section\n>>   Documentation: define protected configuration\n>\n> Forgot to mention when I was sending my comments on patch 2: we should\n> standardize on \"protected config\" and not use \"protected configuration\"\n> anywhere.\n\nMakes sense.\n\n> One alternative design would have been to have separate configsets for\n> protected config and non-protected config (or even better, separate\n> configsets for trace2 config, protected config minus trace2 config, and\n> non-protected config) but that doesn't have to block the submission of\n> this patch set.\n\nI suppose that the idea behind this is that we only parse and store each\nconfig file exactly once. It's a good goal, but the whole point of the\nconfigset is that we can query a single struct to figure out the value\nof a config variable. Having multiple configsets starts to shift more of\nthe burden to the callers because they now have to query multiple\nconfigsets to find their desired config value, and we already start to\nsee some of this unpleasantness in this series.\n\nAn alternative that I'd been thinking about is to make a few changes to\nthe git_config_* + configset API to allow us to use a single configset\nfor all of our needs:\n\n1. Keep track of what config we've read when reading into\n   the_repository->config, i.e. instead of a boolean \"all config has\n   been [un]read\", we can express \"system and global config has been\n   read, but not local or command config\". Then, use this information to\n   load config from sources as they become available. This will allow us\n   to read incomplete config for trace2 and setup.c (discovery.bare and\n   safe.directory), and only read what we need later on.\n\n   This assumes that when Git reads config, that config is always valid\n   later on. So this is broken if, e.g. we read global config file A\n   during setup, but when we discover the repo, we discard A and read\n   global config file B instead. I don't know if we do this or if we are\n   planning to in the future.\n\n2. Add an additional argument that specifies what scopes to respect when\n   reading config (maybe as a set of flags). This gives us extra\n   specificity when using the git_config*() functions, so we could get\n   rid of git_protected_config() like so:\n\n    /* Change enum config_scope into flags first... */\n\n    #define WIP_SCOPES_PROTECTED = CONFIG_SCOPE_SYSTEM & \\\n      CONFIG_SCOPE_GLOBAL & CONFIG_SCOPE_COMMAND\n\n    static enum discovery_bare_allowed get_discovery_bare(void)\n    {\n      enum discovery_bare_allowed result = DISCOVERY_BARE_ALWAYS;\n      git_config(discovery_bare_cb, &result, WIP_SCOPES_PROTECTED);\n      return result;\n    }\n\n   And as an added bonus, this gives us an easy way to implement the\n   constant time git_config_*() functions for protected config. We could\n   even do this without doing 1. first. I haven't looked into whether\n   we could turn the enum into flags, but otherwise, I think this is\n   pretty feasible.\n"},{"id":"457809","messageId":"kl6lo7yji6kr.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"20220622215848.3733308-1-jonathantanmy@google.com","subject":"Re: [PATCH v4 2/5] Documentation: define protected configuration","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-06-23T18:21:40Z","receivedAt":"2022-06-23T19:17:40Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Jonathan Tan <jonathantanmy@google.com> writes:\n\n> \"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>> From: Glen Choo <chooglen@google.com>\n>> \n>> For security reasons, there are config variables that are only trusted\n>> when they are specified in extra-trustworthy configuration scopes, which\n>\n> Probably better to delete \"extra-trustworthy\", or at least \"extra-\" -\n> it's better to explain why and how they're trustworthy, which you have\n> already done in the commit message.\n\nHm, do you find it superfluous, misleading or something else entirely?\n\nThe use of \"extra-\" was quite intentional. I'm afraid that if we\ndescribe protected config as \"trustworthy\", we insinuate that\nlocal/worktree config is \"untrustworthy\" (but of course this isn't\nalways true, Git usually uses repo config.)\n\n>> diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\n>> index 5e4c95f2423..2b4334faec9 100644\n>> --- a/Documentation/git-config.txt\n>> +++ b/Documentation/git-config.txt\n>\n> [snip]\n>\n>> +Protected config refers to the 'system', 'global', and 'command' scopes. Git\n>> +considers these scopes to be especially trustworthy because they are likely\n>> +to be controlled by the user or a trusted administrator. An attacker who\n>> +controls these scopes can do substantial harm without using Git, so it is\n>> +assumed that the user's environment protects these scopes against attackers.\n>> +\n>> +For security reasons, certain options are only respected when they are\n>> +specified in protected config, and ignored otherwise.\n>\n> Also \"especially trustworthy\" here.\n\n"},{"id":"457810","messageId":"xmqqo7yjz0w4.fsf@gitster.g","threadId":"57859","inReplyTo":"kl6lr13fi9qn.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v4 0/5] config: introduce discovery.bare and protected config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-06-23T18:32:27Z","receivedAt":"2022-06-23T19:24:27Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Glen Choo <chooglen@google.com> writes:\n\n> Jonathan Tan <jonathantanmy@google.com> writes:\n>\n>> \"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>>> Glen Choo (5):\n>>>   Documentation/git-config.txt: add SCOPES section\n>>>   Documentation: define protected configuration\n>>\n>> Forgot to mention when I was sending my comments on patch 2: we should\n>> standardize on \"protected config\" and not use \"protected configuration\"\n>> anywhere.\n>\n> Makes sense.\n\nUsing a single word consistently does make sense, but why favor a\nnon-word over a proper word ;-)?\n\n> I suppose that the idea behind this is that we only parse and store each\n> config file exactly once. It's a good goal, but the whole point of the\n> configset is that we can query a single struct to figure out the value\n> of a config variable. Having multiple configsets starts to shift more of\n> the burden to the callers because they now have to query multiple\n> configsets to find their desired config value, and we already start to\n> see some of this unpleasantness in this series.\n\nYes, I was worried about this, too.  \"parse and store exactly once\"\nmay merely be a performance thing, but it still matters, even though\nit is not worse than making duplicate callbacks to overwrite globals\nthat have been already set earlier, which will affect correctness ;-)\n\n> An alternative that I'd been thinking about is to make a few changes to\n> the git_config_* + configset API to allow us to use a single configset\n> for all of our needs:\n>\n> 1. Keep track of what config we've read when reading into\n>    the_repository->config, i.e. instead of a boolean \"all config has\n>    been [un]read\", we can express \"system and global config has been\n>    read, but not local or command config\". Then, use this information to\n>    load config from sources as they become available. This will allow us\n>    to read incomplete config for trace2 and setup.c (discovery.bare and\n>    safe.directory), and only read what we need later on.\n\nThat is not a bad direction to go, but are we sure that we always\nread in the right order (and there is one single right order) and\nstop at the right step?\n\nconfig.c::do_git_config_sequence() reads the system and then the\nglobal before the local, the worktree, and the command line.  We\nwould allow the values of \"protected\" configuration variables to be\ninspected by stopping after the first two and inspecting the result\nbefore the local and the rest overrides them, but will we need\n*only* that kind of partial configuration reading that stops exactly\nthere?  Even with the proposed \"protected\" scheme, I thought we plan\nto honor the command line ones, so we may need to read\nsystem+global+command without reading anything else to grab the\nvalues only from the protected sources (ah, I like the application\nof the adjective \"protected\" to the source, not variables, because\nthat is what we are really talking about---alternatively we could\ncall it \"safe\").  But if we later read local and worktree ones\nlazily, unless we _insert_ them before what we read from the command\nline, we'll break the last-one-wins property, so we need to be\ncareful.  I guess each configuration value in the configset knows\nwhere it came from, so it probably is possible to insert the ones\nyou read lazily later in the right spot.\n\n> 2. Add an additional argument that specifies what scopes to respect when\n>    reading config (maybe as a set of flags). This gives us extra\n>    specificity when using the git_config*() functions, so we could get\n>    rid of git_protected_config() like so:\n>\n>     /* Change enum config_scope into flags first... */\n>\n>     #define WIP_SCOPES_PROTECTED = CONFIG_SCOPE_SYSTEM & \\\n>       CONFIG_SCOPE_GLOBAL & CONFIG_SCOPE_COMMAND\n>\n>     static enum discovery_bare_allowed get_discovery_bare(void)\n>     {\n>       enum discovery_bare_allowed result = DISCOVERY_BARE_ALWAYS;\n>       git_config(discovery_bare_cb, &result, WIP_SCOPES_PROTECTED);\n>       return result;\n>     }\n\nAlternatively, we could make the callback aware of the scope for\neach var-value it is called and have it filter, but that would be a\nbigger surgery.\n\nI think a new iterator git_config_in_scope(), instead of updating\ngit_config(), would make sense.  By definition, all existing\ngit_config() callers do not need the scope specifiers, and\n\"protected\" may be the first one but will not be the last one that\nneeds to read from particular scopes.\n"},{"id":"457918","messageId":"kl6lbkuehuxf.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"xmqqo7yjz0w4.fsf@gitster.g","subject":"Re: [PATCH v4 0/5] config: introduce discovery.bare and protected config","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-06-27T17:34:36Z","receivedAt":"2022-06-27T17:34:43Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Glen Choo <chooglen@google.com> writes:\n>\n>> Jonathan Tan <jonathantanmy@google.com> writes:\n>>\n>>> \"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>>>> Glen Choo (5):\n>>>>   Documentation/git-config.txt: add SCOPES section\n>>>>   Documentation: define protected configuration\n>>>\n>>> Forgot to mention when I was sending my comments on patch 2: we should\n>>> standardize on \"protected config\" and not use \"protected configuration\"\n>>> anywhere.\n>>\n>> Makes sense.\n>\n> Using a single word consistently does make sense, but why favor a\n> non-word over a proper word ;-)?\n\nHm, I guess there's an argument that \"config\" is a term of art that\nspecifically refers to things from \"git config\". From that lens, it's much less\nconfusing to see the CONFIGURATION section in\nDocumentation/git-config.txt. But the argument is a little flimsy\nbecause I don't think that's something we've stuck to anywhere.\n\nI'll use \"configuration\" if it's not too unwieldy.\n\n>> I suppose that the idea behind this is that we only parse and store each\n>> config file exactly once. It's a good goal, but the whole point of the\n>> configset is that we can query a single struct to figure out the value\n>> of a config variable. Having multiple configsets starts to shift more of\n>> the burden to the callers because they now have to query multiple\n>> configsets to find their desired config value, and we already start to\n>> see some of this unpleasantness in this series.\n>\n> Yes, I was worried about this, too.  \"parse and store exactly once\"\n> may merely be a performance thing, but it still matters, even though\n> it is not worse than making duplicate callbacks to overwrite globals\n> that have been already set earlier, which will affect correctness ;-)\n\nExactly.\n\n>> An alternative that I'd been thinking about is to make a few changes to\n>> the git_config_* + configset API to allow us to use a single configset\n>> for all of our needs:\n>>\n>> 1. Keep track of what config we've read when reading into\n>>    the_repository->config, i.e. instead of a boolean \"all config has\n>>    been [un]read\", we can express \"system and global config has been\n>>    read, but not local or command config\". Then, use this information to\n>>    load config from sources as they become available. This will allow us\n>>    to read incomplete config for trace2 and setup.c (discovery.bare and\n>>    safe.directory), and only read what we need later on.\n>\n> That is not a bad direction to go, but are we sure that we always\n> read in the right order (and there is one single right order) and\n> stop at the right step?\n>\n> config.c::do_git_config_sequence() reads the system and then the\n> global before the local, the worktree, and the command line.  We\n> would allow the values of \"protected\" configuration variables to be\n> inspected by stopping after the first two and inspecting the result\n> before the local and the rest overrides them, but will we need\n> *only* that kind of partial configuration reading that stops exactly\n> there?  Even with the proposed \"protected\" scheme, I thought we plan\n> to honor the command line ones, so we may need to read\n> system+global+command without reading anything else to grab the\n> values only from the protected sources (ah, I like the application\n> of the adjective \"protected\" to the source, not variables, because\n> that is what we are really talking about---alternatively we could\n> call it \"safe\").  But if we later read local and worktree ones\n> lazily, unless we _insert_ them before what we read from the command\n> line, we'll break the last-one-wins property, so we need to be\n> careful.  I guess each configuration value in the configset knows\n> where it came from, so it probably is possible to insert the ones\n> you read lazily later in the right spot.\n\nYeah, last-one-wins makes this a lot trickier. I thought that it would\nbe nice to have insert-with-priority because that also eliminates some\nof the correctness concerns in this series, i.e. that ensures protected\nconfig has the same priority as regular config, but that's a bigger\nundertaking and I'm not certain about the performance.\n\n>> 2. Add an additional argument that specifies what scopes to respect when\n>>    reading config (maybe as a set of flags). This gives us extra\n>>    specificity when using the git_config*() functions, so we could get\n>>    rid of git_protected_config() like so:\n>>\n>>     /* Change enum config_scope into flags first... */\n>>\n>>     #define WIP_SCOPES_PROTECTED = CONFIG_SCOPE_SYSTEM & \\\n>>       CONFIG_SCOPE_GLOBAL & CONFIG_SCOPE_COMMAND\n>>\n>>     static enum discovery_bare_allowed get_discovery_bare(void)\n>>     {\n>>       enum discovery_bare_allowed result = DISCOVERY_BARE_ALWAYS;\n>>       git_config(discovery_bare_cb, &result, WIP_SCOPES_PROTECTED);\n>>       return result;\n>>     }\n>\n> Alternatively, we could make the callback aware of the scope for\n> each var-value it is called and have it filter, but that would be a\n> bigger surgery.\n>\n> I think a new iterator git_config_in_scope(), instead of updating\n> git_config(), would make sense.  By definition, all existing\n> git_config() callers do not need the scope specifiers, and\n> \"protected\" may be the first one but will not be the last one that\n> needs to read from particular scopes.\n\nMakes sense. The signature of git_config() could stay the same, but we\ncould refactor it to use git_config_in_scope().\n"},{"id":"457923","messageId":"kl6l7d52hsv8.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"pull.1261.v4.git.git.1654635432.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 0/5] config: introduce discovery.bare and protected config","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-06-27T18:19:07Z","receivedAt":"2022-06-27T18:19:12Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"\"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> This round doesn't introduce any major code changes. The most notable\n> changes are:\n>\n\n[...]\n\n>\n>  * I added a test for \"git config --show-scope\" and the 'worktree' scope,\n>    since 'worktree' wasn't listed in Documentation/git-config.txt (6/5).\n\nErratum: This patch was sent as gc/document-config-worktree-scope [1].\nPatch 6/5 obviously doesn't exist.\n\n[1] https://lore.kernel.org/git/pull.1274.git.git.1654637044966.gitgitgadget@gmail.com\n"},{"id":"457949","messageId":"43627c05c0b997ea407c865b04994cba630297d6.1656354994.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v5.git.git.1656354994.gitgitgadget@gmail.com","subject":"[PATCH v5 2/5] Documentation: define protected configuration","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-27T18:36:31Z","receivedAt":"2022-06-27T18:38:32Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nFor security reasons, there are config variables that are only trusted\nwhen they are specified in certain configuration scopes, which are\nsometimes referred to on-list as 'protected configuration' [1]. A future\ncommit will introduce another such variable, so let's define our terms\nso that we can have consistent documentation and implementation.\n\nIn our documentation, define 'protected configuration' as the system,\nglobal and command config scopes. As a shorthand, I will refer to\nvariables that are only respected in protected config as 'protected\nconfiguration only', but this term is not used in the documentation.\n\nThis definition of protected config is based on whether or not Git can\nreasonably protect the user by ignoring the configuration scope:\n\n- System, global and command line config are considered protected\n  because an attacker who has control over any of those can do plenty of\n  harm without Git, so we gain very little by ignoring those scopes.\n- On the other hand, local (and similarly, worktree) config are not\n  considered protected because it is relatively easy for an attacker to\n  control local config, e.g.:\n  - On some shared user environments, a non-admin attacker can create a\n    repository high up the directory hierarchy (e.g. C:\\.git on\n    Windows), and a user may accidentally use it when their PS1\n    automatically invokes \"git\" commands.\n\n    `safe.directory` prevents attacks of this form by making sure that\n    the user intended to use the shared repository. It obviously\n    shouldn't be read from the repository, because that would end up\n    trusting the repository that Git was supposed to reject.\n  - \"git upload-pack\" is expected to run in repositories that may not be\n    controlled by the user. We cannot ignore all config in that\n    repository (because \"git upload-pack\" would fail), but we can limit\n    the risks by ignoring `uploadpack.packObjectsHook`.\n\nOnly `uploadpack.packObjectsHook` is 'protected configuration only'. The\nfollowing variables are intentionally excluded:\n\n- `safe.directory` should be 'protected configuration only', but it does\n  not technically fit the definition because it is not respected in the\n  \"command\" scope. A future commit will fix this.\n\n- `trace2.*` happens to read the same scopes as `safe.directory` because\n  they share an implementation. However, this is not for security\n  reasons; it is because we want to start tracing so early that\n  repository-level config and \"-c\" are not available [2].\n\n  This requirement is unique to `trace2.*`, so it does not makes sense\n  for protected configuration to be subject to the same constraints.\n\n[1] For example,\nhttps://lore.kernel.org/git/6af83767-576b-75c4-c778-0284344a8fe7@github.com/\n[2] https://lore.kernel.org/git/a0c89d0d-669e-bf56-25d2-cbb09b012e70@jeffhostetler.com/\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config/uploadpack.txt |  6 +++---\n Documentation/git-config.txt        | 13 +++++++++++++\n 2 files changed, 16 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/config/uploadpack.txt b/Documentation/config/uploadpack.txt\nindex 32fad5bbe81..029abbefdff 100644\n--- a/Documentation/config/uploadpack.txt\n+++ b/Documentation/config/uploadpack.txt\n@@ -49,9 +49,9 @@ uploadpack.packObjectsHook::\n \t`pack-objects` to the hook, and expects a completed packfile on\n \tstdout.\n +\n-Note that this configuration variable is ignored if it is seen in the\n-repository-level config (this is a safety measure against fetching from\n-untrusted repositories).\n+Note that this configuration variable is only respected when it is specified\n+in protected config (see <<SCOPES>>). This is a safety measure against\n+fetching from untrusted repositories.\n \n uploadpack.allowFilter::\n \tIf this option is set, `upload-pack` will support partial\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex f93d437b898..f1810952891 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -343,6 +343,7 @@ You can change the way options are read/written by specifying the path to a\n file (`--file`), or by specifying a configuration scope (`--system`,\n `--global`, `--local`, `--worktree`); see <<OPTIONS>> above.\n \n+[[SCOPES]]\n SCOPES\n ------\n \n@@ -380,6 +381,18 @@ Most configuration options are respected regardless of the scope it is\n defined in, but some options are only respected in certain scopes. See the\n option's documentation for the full details.\n \n+Protected configuration\n+~~~~~~~~~~~~~~~~~~~~~~~\n+\n+Protected configuration refers to the 'system', 'global', and 'command' scopes.\n+For security reasons, certain options are only respected when they are\n+specified in protected configuration, and ignored otherwise.\n+\n+Git treats these scopes as if they are controlled by the user or a trusted\n+administrator. This is because an attacker who controls these scopes can do\n+substantial harm without using Git, so it is assumed that the user's environment\n+protects these scopes against attackers.\n+\n ENVIRONMENT\n -----------\n \n-- \ngitgitgadget\n\n"},{"id":"457950","messageId":"ec9258234142c4d1d164cbf475cb86e7b826bb8f.1656354994.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v5.git.git.1656354994.gitgitgadget@gmail.com","subject":"[PATCH v5 4/5] safe.directory: use git_protected_config()","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-27T18:36:33Z","receivedAt":"2022-06-27T18:38:36Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nUse git_protected_config() to read `safe.directory` instead of\nread_very_early_config(), making it 'protected configuration only'.\n\nAs a result, `safe.directory` now respects \"-c\", so update the tests and\ndocs accordingly. It used to ignore \"-c\" due to how it was implemented,\nnot because of security or correctness concerns [1].\n\n[1] https://lore.kernel.org/git/xmqqlevabcsu.fsf@gitster.g/\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config/safe.txt |  6 +++---\n setup.c                       |  2 +-\n t/t0033-safe-directory.sh     | 24 ++++++++++--------------\n 3 files changed, 14 insertions(+), 18 deletions(-)\n\ndiff --git a/Documentation/config/safe.txt b/Documentation/config/safe.txt\nindex fa02f3ccc54..f72b4408798 100644\n--- a/Documentation/config/safe.txt\n+++ b/Documentation/config/safe.txt\n@@ -12,9 +12,9 @@ via `git config --add`. To reset the list of safe directories (e.g. to\n override any such directories specified in the system config), add a\n `safe.directory` entry with an empty value.\n +\n-This config setting is only respected when specified in a system or global\n-config, not when it is specified in a repository config, via the command\n-line option `-c safe.directory=<path>`, or in environment variables.\n+This config setting is only respected in protected configuration (see\n+<<SCOPES>>). This prevents the untrusted repository from tampering with this\n+value.\n +\n The value of this setting is interpolated, i.e. `~/<path>` expands to a\n path relative to the home directory and `%(prefix)/<path>` expands to a\ndiff --git a/setup.c b/setup.c\nindex faf5095e44d..c8e3c32814d 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1137,7 +1137,7 @@ static int ensure_valid_ownership(const char *path)\n \t    is_path_owned_by_current_user(path))\n \t\treturn 1;\n \n-\tread_very_early_config(safe_directory_cb, &data);\n+\tgit_protected_config(safe_directory_cb, &data);\n \n \treturn data.is_safe;\n }\ndiff --git a/t/t0033-safe-directory.sh b/t/t0033-safe-directory.sh\nindex 238b25f91a3..5a1cd0d0947 100755\n--- a/t/t0033-safe-directory.sh\n+++ b/t/t0033-safe-directory.sh\n@@ -16,24 +16,20 @@ test_expect_success 'safe.directory is not set' '\n \texpect_rejected_dir\n '\n \n-test_expect_success 'ignoring safe.directory on the command line' '\n-\ttest_must_fail git -c safe.directory=\"$(pwd)\" status 2>err &&\n-\tgrep \"unsafe repository\" err\n+test_expect_success 'safe.directory on the command line' '\n+\tgit -c safe.directory=\"$(pwd)\" status\n '\n \n-test_expect_success 'ignoring safe.directory in the environment' '\n-\ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n-\t\tGIT_CONFIG_KEY_0=\"safe.directory\" \\\n-\t\tGIT_CONFIG_VALUE_0=\"$(pwd)\" \\\n-\t\tgit status 2>err &&\n-\tgrep \"unsafe repository\" err\n+test_expect_success 'safe.directory in the environment' '\n+\tenv GIT_CONFIG_COUNT=1 \\\n+\t    GIT_CONFIG_KEY_0=\"safe.directory\" \\\n+\t    GIT_CONFIG_VALUE_0=\"$(pwd)\" \\\n+\t    git status\n '\n \n-test_expect_success 'ignoring safe.directory in GIT_CONFIG_PARAMETERS' '\n-\ttest_must_fail env \\\n-\t\tGIT_CONFIG_PARAMETERS=\"${SQ}safe.directory${SQ}=${SQ}$(pwd)${SQ}\" \\\n-\t\tgit status 2>err &&\n-\tgrep \"unsafe repository\" err\n+test_expect_success 'safe.directory in GIT_CONFIG_PARAMETERS' '\n+\tenv GIT_CONFIG_PARAMETERS=\"${SQ}safe.directory${SQ}=${SQ}$(pwd)${SQ}\" \\\n+\t    git status\n '\n \n test_expect_success 'ignoring safe.directory in repo config' '\n-- \ngitgitgadget\n\n"},{"id":"457951","messageId":"3efe282e6b94c3daed77590c5f601fad34137c9c.1656354994.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v5.git.git.1656354994.gitgitgadget@gmail.com","subject":"[PATCH v5 3/5] config: learn `git_protected_config()`","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-27T18:36:32Z","receivedAt":"2022-06-27T18:38:37Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\n`uploadpack.packObjectsHook` is the only 'protected configuration only'\nvariable today, but we've noted that `safe.directory` and the upcoming\n`discovery.bare` should also be 'protected configuration only'. So, for\nconsistency, we'd like to have a single implementation for protected\nconfig.\n\nThe primary constraints are:\n\n1. Reading from protected configuration should be as fast as possible.\n   Nearly all \"git\" commands inside a bare repository will read both\n   `safe.directory` and `discovery.bare`, so we cannot afford to be\n   slow.\n\n2. Protected config must be readable when the gitdir is not known.\n   `safe.directory` and `discovery.bare` both affect repository\n   discovery and the gitdir is not known at that point [1].\n\nThe chosen implementation in this commit is to read protected\nconfiguration and cache the values in a global configset. This is\nsimilar to the caching behavior we get with the_repository->config.\n\nIntroduce git_protected_config(), which reads protected configuration\nand caches them in the global configset protected_config. Then, refactor\n`uploadpack.packObjectsHook` to use git_protected_config().\n\nThe protected configuration functions are named similarly to their\nnon-protected counterparts, e.g. git_protected_config_check_init() vs\ngit_config_check_init().\n\nIn light of constraint 1, this implementation can still be improved\nsince git_protected_config() iterates through every variable in\nprotected_config, which may still be too expensive. There exist constant\ntime lookup functions for non-protected configuration\n(repo_config_get_*()), but for simplicity, this commit does not\nimplement similar functions for protected configuration.\n\nAn alternative that avoids introducing another configset is to continue\nto read all config using git_config(), but only accept values that have\nthe correct config scope [2]. This technically fulfills constraint 2,\nbecause git_config() simply ignores the local and worktree config when\nthe gitdir is not known. However, this would read incomplete config into\nthe_repository->config, which would need to be reset when the gitdir is\nknown and git_config() needs to read the local and worktree config.\nResetting the_repository->config might be reasonable while we only have\nthese 'protected configuration only' variables, but it's not clear\nwhether this extends well to future variables.\n\n[1] In this case, we do have a candidate gitdir though, so with a little\nrefactoring, it might be possible to provide a gitdir.\n[2] This is how `uploadpack.packObjectsHook` was implemented prior to\nthis commit.\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n config.c                     | 51 ++++++++++++++++++++++++++++++++++++\n config.h                     | 17 ++++++++++++\n t/t5544-pack-objects-hook.sh |  7 ++++-\n upload-pack.c                | 27 ++++++++++++-------\n 4 files changed, 91 insertions(+), 11 deletions(-)\n\ndiff --git a/config.c b/config.c\nindex 9b0e9c93285..29e62f5d0ed 100644\n--- a/config.c\n+++ b/config.c\n@@ -81,6 +81,18 @@ static enum config_scope current_parsing_scope;\n static int pack_compression_seen;\n static int zlib_compression_seen;\n \n+/*\n+ * Config that comes from trusted sources, namely:\n+ * - system config files (e.g. /etc/gitconfig)\n+ * - global config files (e.g. $HOME/.gitconfig,\n+ *   $XDG_CONFIG_HOME/git)\n+ * - the command line.\n+ *\n+ * This is declared here for code cleanliness, but unlike the other\n+ * static variables, this does not hold config parser state.\n+ */\n+static struct config_set protected_config;\n+\n static int config_file_fgetc(struct config_source *conf)\n {\n \treturn getc_unlocked(conf->u.file);\n@@ -2378,6 +2390,11 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n \treturn git_config_from_file(config_set_callback, filename, cs);\n }\n \n+int git_configset_add_parameters(struct config_set *cs)\n+{\n+\treturn git_config_from_parameters(config_set_callback, cs);\n+}\n+\n int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n {\n \tconst struct string_list *values = NULL;\n@@ -2619,6 +2636,40 @@ int repo_config_get_pathname(struct repository *repo,\n \treturn ret;\n }\n \n+/* Read values into protected_config. */\n+static void read_protected_config(void)\n+{\n+\tchar *xdg_config = NULL, *user_config = NULL, *system_config = NULL;\n+\n+\tgit_configset_init(&protected_config);\n+\n+\tsystem_config = git_system_config();\n+\tgit_global_config(&user_config, &xdg_config);\n+\n+\tgit_configset_add_file(&protected_config, system_config);\n+\tgit_configset_add_file(&protected_config, xdg_config);\n+\tgit_configset_add_file(&protected_config, user_config);\n+\tgit_configset_add_parameters(&protected_config);\n+\n+\tfree(system_config);\n+\tfree(xdg_config);\n+\tfree(user_config);\n+}\n+\n+/* Ensure that protected_config has been initialized. */\n+static void git_protected_config_check_init(void)\n+{\n+\tif (protected_config.hash_initialized)\n+\t\treturn;\n+\tread_protected_config();\n+}\n+\n+void git_protected_config(config_fn_t fn, void *data)\n+{\n+\tgit_protected_config_check_init();\n+\tconfigset_iter(&protected_config, fn, data);\n+}\n+\n /* Functions used historically to read configuration from 'the_repository' */\n void git_config(config_fn_t fn, void *data)\n {\ndiff --git a/config.h b/config.h\nindex 7654f61c634..e3ff1fcf683 100644\n--- a/config.h\n+++ b/config.h\n@@ -446,6 +446,15 @@ void git_configset_init(struct config_set *cs);\n  */\n int git_configset_add_file(struct config_set *cs, const char *filename);\n \n+/**\n+ * Parses command line options and environment variables, and adds the\n+ * variable-value pairs to the `config_set`. Returns 0 on success, or -1\n+ * if there is an error in parsing. The caller decides whether to free\n+ * the incomplete configset or continue using it when the function\n+ * returns -1.\n+ */\n+int git_configset_add_parameters(struct config_set *cs);\n+\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key` and config set `cs`. When the\n@@ -505,6 +514,14 @@ int repo_config_get_maybe_bool(struct repository *repo,\n int repo_config_get_pathname(struct repository *repo,\n \t\t\t     const char *key, const char **dest);\n \n+/*\n+ * Functions for reading protected config. By definition, protected\n+ * config ignores repository config, so it is unnecessary to read\n+ * protected config from any `struct repository` other than\n+ * the_repository.\n+ */\n+void git_protected_config(config_fn_t fn, void *data);\n+\n /**\n  * Querying For Specific Variables\n  * -------------------------------\ndiff --git a/t/t5544-pack-objects-hook.sh b/t/t5544-pack-objects-hook.sh\nindex dd5f44d986f..54f54f8d2eb 100755\n--- a/t/t5544-pack-objects-hook.sh\n+++ b/t/t5544-pack-objects-hook.sh\n@@ -56,7 +56,12 @@ test_expect_success 'hook does not run from repo config' '\n \t! grep \"hook running\" stderr &&\n \ttest_path_is_missing .git/hook.args &&\n \ttest_path_is_missing .git/hook.stdin &&\n-\ttest_path_is_missing .git/hook.stdout\n+\ttest_path_is_missing .git/hook.stdout &&\n+\n+\t# check that global config is used instead\n+\ttest_config_global uploadpack.packObjectsHook ./hook &&\n+\tgit clone --no-local . dst2.git 2>stderr &&\n+\tgrep \"hook running\" stderr\n '\n \n test_expect_success 'hook works with partial clone' '\ndiff --git a/upload-pack.c b/upload-pack.c\nindex 3a851b36066..09f48317b02 100644\n--- a/upload-pack.c\n+++ b/upload-pack.c\n@@ -1321,18 +1321,27 @@ static int upload_pack_config(const char *var, const char *value, void *cb_data)\n \t\tdata->advertise_sid = git_config_bool(var, value);\n \t}\n \n-\tif (current_config_scope() != CONFIG_SCOPE_LOCAL &&\n-\t    current_config_scope() != CONFIG_SCOPE_WORKTREE) {\n-\t\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n-\t\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n-\t}\n-\n \tif (parse_object_filter_config(var, value, data) < 0)\n \t\treturn -1;\n \n \treturn parse_hide_refs_config(var, value, \"uploadpack\");\n }\n \n+static int upload_pack_protected_config(const char *var, const char *value, void *cb_data)\n+{\n+\tstruct upload_pack_data *data = cb_data;\n+\n+\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n+\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n+\treturn 0;\n+}\n+\n+static void get_upload_pack_config(struct upload_pack_data *data)\n+{\n+\tgit_config(upload_pack_config, data);\n+\tgit_protected_config(upload_pack_protected_config, data);\n+}\n+\n void upload_pack(const int advertise_refs, const int stateless_rpc,\n \t\t const int timeout)\n {\n@@ -1340,8 +1349,7 @@ void upload_pack(const int advertise_refs, const int stateless_rpc,\n \tstruct upload_pack_data data;\n \n \tupload_pack_data_init(&data);\n-\n-\tgit_config(upload_pack_config, &data);\n+\tget_upload_pack_config(&data);\n \n \tdata.stateless_rpc = stateless_rpc;\n \tdata.timeout = timeout;\n@@ -1695,8 +1703,7 @@ int upload_pack_v2(struct repository *r, struct packet_reader *request)\n \n \tupload_pack_data_init(&data);\n \tdata.use_sideband = LARGE_PACKET_MAX;\n-\n-\tgit_config(upload_pack_config, &data);\n+\tget_upload_pack_config(&data);\n \n \twhile (state != FETCH_DONE) {\n \t\tswitch (state) {\n-- \ngitgitgadget\n\n"},{"id":"457952","messageId":"14411512783fd4e2cdcc8513690377b29262f6b8.1656354994.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v5.git.git.1656354994.gitgitgadget@gmail.com","subject":"[PATCH v5 5/5] setup.c: create `discovery.bare`","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-27T18:36:34Z","receivedAt":"2022-06-27T18:38:39Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nThere is a known social engineering attack that takes advantage of the\nfact that a working tree can include an entire bare repository,\nincluding a config file. A user could run a Git command inside the bare\nrepository thinking that the config file of the 'outer' repository would\nbe used, but in reality, the bare repository's config file (which is\nattacker-controlled) is used, which may result in arbitrary code\nexecution. See [1] for a fuller description and deeper discussion.\n\nA simple mitigation is to forbid bare repositories unless specified via\n`--git-dir` or `GIT_DIR`. In environments that don't use bare\nrepositories, this would be minimally disruptive.\n\nCreate a config variable, `discovery.bare`, that tells Git whether or\nnot to die() when it discovers a bare repository. This only affects\nrepository discovery, thus it has no effect if discovery was not\ndone, e.g. if the user passes `--git-dir=my-dir`, discovery will be\nskipped and my-dir will be used as the repo regardless of the\n`discovery.bare` value.\n\nThis config is an enum of:\n\n- \"always\": always allow bare repositories (this is the default)\n- \"never\": never allow bare repositories\n\nIf we want to protect users from such attacks by default, neither value\nwill suffice - \"always\" provides no protection, but \"never\" is\nimpractical for bare repository users. A more usable default would be to\nallow only non-embedded bare repositories ([2] contains one such\nproposal), but detecting if a repository is embedded is potentially\nnon-trivial, so this work is not implemented in this series.\n\n[1]: https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n[2]: https://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config.txt           |  2 +\n Documentation/config/discovery.txt | 23 ++++++++++\n setup.c                            | 57 ++++++++++++++++++++++++-\n t/t0035-discovery-bare.sh          | 68 ++++++++++++++++++++++++++++++\n 4 files changed, 149 insertions(+), 1 deletion(-)\n create mode 100644 Documentation/config/discovery.txt\n create mode 100755 t/t0035-discovery-bare.sh\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex e284b042f22..9a5e1329772 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -409,6 +409,8 @@ include::config/diff.txt[]\n \n include::config/difftool.txt[]\n \n+include::config/discovery.txt[]\n+\n include::config/extensions.txt[]\n \n include::config/fastimport.txt[]\ndiff --git a/Documentation/config/discovery.txt b/Documentation/config/discovery.txt\nnew file mode 100644\nindex 00000000000..bbcf89bb0b5\n--- /dev/null\n+++ b/Documentation/config/discovery.txt\n@@ -0,0 +1,23 @@\n+discovery.bare::\n+\tSpecifies whether Git will work with a bare repository that it\n+\tfound during repository discovery. If the repository is\n+\tspecified directly via the --git-dir command-line option or the\n+\tGIT_DIR environment variable (see linkgit:git[1]), Git will\n+\talways use the specified repository, regardless of this value.\n++\n+This config setting is only respected in protected configuration (see\n+<<SCOPES>>). This prevents the untrusted repository from tampering with\n+this value.\n++\n+The currently supported values are:\n++\n+* `always`: Git always works with bare repositories\n+* `never`: Git never works with bare repositories\n++\n+This defaults to `always`, but this default may change in the future.\n++\n+If you do not use bare repositories in your workflow, then it may be\n+beneficial to set `discovery.bare` to `never` in your global config.\n+This will protect you from attacks that involve cloning a repository\n+that contains a bare repository and running a Git command within that\n+directory.\ndiff --git a/setup.c b/setup.c\nindex c8e3c32814d..16938fd5a24 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -10,6 +10,10 @@\n static int inside_git_dir = -1;\n static int inside_work_tree = -1;\n static int work_tree_config_is_bogus;\n+enum discovery_bare_allowed {\n+\tDISCOVERY_BARE_NEVER = 0,\n+\tDISCOVERY_BARE_ALWAYS,\n+};\n \n static struct startup_info the_startup_info;\n struct startup_info *startup_info = &the_startup_info;\n@@ -1142,6 +1146,46 @@ static int ensure_valid_ownership(const char *path)\n \treturn data.is_safe;\n }\n \n+static int discovery_bare_cb(const char *key, const char *value, void *d)\n+{\n+\tenum discovery_bare_allowed *discovery_bare_allowed = d;\n+\n+\tif (strcmp(key, \"discovery.bare\"))\n+\t\treturn 0;\n+\n+\tif (!strcmp(value, \"never\")) {\n+\t\t*discovery_bare_allowed = DISCOVERY_BARE_NEVER;\n+\t\treturn 0;\n+\t}\n+\tif (!strcmp(value, \"always\")) {\n+\t\t*discovery_bare_allowed = DISCOVERY_BARE_ALWAYS;\n+\t\treturn 0;\n+\t}\n+\treturn -1;\n+}\n+\n+static enum discovery_bare_allowed get_discovery_bare(void)\n+{\n+\tenum discovery_bare_allowed result = DISCOVERY_BARE_ALWAYS;\n+\tgit_protected_config(discovery_bare_cb, &result);\n+\treturn result;\n+}\n+\n+static const char *discovery_bare_allowed_to_string(\n+\tenum discovery_bare_allowed discovery_bare_allowed)\n+{\n+\tswitch (discovery_bare_allowed) {\n+\tcase DISCOVERY_BARE_NEVER:\n+\t\treturn \"never\";\n+\tcase DISCOVERY_BARE_ALWAYS:\n+\t\treturn \"always\";\n+\tdefault:\n+\t\tBUG(\"invalid discovery_bare_allowed %d\",\n+\t\t    discovery_bare_allowed);\n+\t}\n+\treturn NULL;\n+}\n+\n enum discovery_result {\n \tGIT_DIR_NONE = 0,\n \tGIT_DIR_EXPLICIT,\n@@ -1151,7 +1195,8 @@ enum discovery_result {\n \tGIT_DIR_HIT_CEILING = -1,\n \tGIT_DIR_HIT_MOUNT_POINT = -2,\n \tGIT_DIR_INVALID_GITFILE = -3,\n-\tGIT_DIR_INVALID_OWNERSHIP = -4\n+\tGIT_DIR_INVALID_OWNERSHIP = -4,\n+\tGIT_DIR_DISALLOWED_BARE = -5,\n };\n \n /*\n@@ -1248,6 +1293,8 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n \t\t}\n \n \t\tif (is_git_directory(dir->buf)) {\n+\t\t\tif (!get_discovery_bare())\n+\t\t\t\treturn GIT_DIR_DISALLOWED_BARE;\n \t\t\tif (!ensure_valid_ownership(dir->buf))\n \t\t\t\treturn GIT_DIR_INVALID_OWNERSHIP;\n \t\t\tstrbuf_addstr(gitdir, \".\");\n@@ -1394,6 +1441,14 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \t\t}\n \t\t*nongit_ok = 1;\n \t\tbreak;\n+\tcase GIT_DIR_DISALLOWED_BARE:\n+\t\tif (!nongit_ok) {\n+\t\t\tdie(_(\"cannot use bare repository '%s' (discovery.bare is '%s')\"),\n+\t\t\t    dir.buf,\n+\t\t\t    discovery_bare_allowed_to_string(get_discovery_bare()));\n+\t\t}\n+\t\t*nongit_ok = 1;\n+\t\tbreak;\n \tcase GIT_DIR_NONE:\n \t\t/*\n \t\t * As a safeguard against setup_git_directory_gently_1 returning\ndiff --git a/t/t0035-discovery-bare.sh b/t/t0035-discovery-bare.sh\nnew file mode 100755\nindex 00000000000..0b345d361e6\n--- /dev/null\n+++ b/t/t0035-discovery-bare.sh\n@@ -0,0 +1,68 @@\n+#!/bin/sh\n+\n+test_description='verify discovery.bare checks'\n+\n+. ./test-lib.sh\n+\n+pwd=\"$(pwd)\"\n+\n+expect_accepted () {\n+\tgit \"$@\" rev-parse --git-dir\n+}\n+\n+expect_rejected () {\n+\ttest_must_fail git \"$@\" rev-parse --git-dir 2>err &&\n+\tgrep \"discovery.bare\" err\n+}\n+\n+test_expect_success 'setup bare repo in worktree' '\n+\tgit init outer-repo &&\n+\tgit init --bare outer-repo/bare-repo\n+'\n+\n+test_expect_success 'discovery.bare unset' '\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\texpect_accepted\n+\t)\n+'\n+\n+test_expect_success 'discovery.bare=always' '\n+\tgit config --global discovery.bare always &&\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\texpect_accepted\n+\t)\n+'\n+\n+test_expect_success 'discovery.bare=never' '\n+\tgit config --global discovery.bare never &&\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\texpect_rejected\n+\t)\n+'\n+\n+test_expect_success 'discovery.bare in the repository' '\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\t# Temporarily set discovery.bare=always, otherwise git\n+\t\t# config fails with \"fatal: not in a git directory\"\n+\t\t# (like safe.directory)\n+\t\tgit config --global discovery.bare always &&\n+\t\tgit config discovery.bare always &&\n+\t\tgit config --global discovery.bare never &&\n+\t\texpect_rejected\n+\t)\n+'\n+\n+test_expect_success 'discovery.bare on the command line' '\n+\tgit config --global discovery.bare never &&\n+\t(\n+\t\tcd outer-repo/bare-repo &&\n+\t\texpect_accepted -c discovery.bare=always &&\n+\t\texpect_rejected -c discovery.bare=\n+\t)\n+'\n+\n+test_done\n-- \ngitgitgadget\n"},{"id":"457953","messageId":"pull.1261.v5.git.git.1656354994.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v4.git.git.1654635432.gitgitgadget@gmail.com","subject":"[PATCH v5 0/5] config: introduce discovery.bare and protected config","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-27T18:36:29Z","receivedAt":"2022-06-27T18:38:40Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"The previous round of this series was picked up by the Google-hosted \"Review\nClub\" (the event is on http://tinyurl.com/gitcal). This round incorporates\nJonathan Tan's feedback (thanks!) as well some feedback from Review Club\nitself.\n\nThis round only contains changes to commit messages and documentation. As\nrequested in Review Club, I've included a full \"Description\" section in this\ncover letter for the convenience of new readers.\n\n= Description\n\nThere is a known social engineering attack that takes advantage of the fact\nthat a working tree can include an entire bare repository, including a\nconfig file. A user could run a Git command inside the bare repository\nthinking that the config file of the 'outer' repository would be used, but\nin reality, the bare repository's config file (which is attacker-controlled)\nis used, which may result in arbitrary code execution. See [1] for a fuller\ndescription and deeper discussion.\n\nThis series implements a simple way of preventing such attacks: create a\nconfig option, discovery.bare, that tells Git whether or not to die when it\nfinds a bare repository. discovery.bare has two values:\n\n * \"always\": always allow bare repositories (default), identical to current\n   behavior\n * \"never\": never allow bare repositories\n\nand users/system administrators who never expect to work with bare\nrepositories can secure their environments using \"never\". discovery.bare has\nno effect if --git-dir or GIT_DIR is passed because we are confident that\nthe user is not confused about which repository is being used.\n\nThis series does not change the default behavior, but in the long-run, a\n\"no-embedded\" option might be a safe and usable default [2]. \"never\" is too\nrestrictive and unlikely to be the default.\n\nFor security reasons, discovery.bare cannot be read from repository-level\nconfig (because we would end up trusting the embedded bare repository that\nwe aren't supposed to trust to begin with). Since this would introduce a 3rd\nvariable that is only read from 'protected/trusted configuration' (the\nothers are safe.directory and uploadpack.packObjectsHook) this series also\ndefines and creates a shared implementation for 'protected configuration'\n\n= Patch organization\n\n * Patch 1 add a section on configuration scopes to our docs\n * Patches 2-3 define 'protected configuration' and create a shared\n   implementation.\n * Patch 4 refactors safe.directory to use protected configuration\n * Patch 5 adds discovery.bare\n\n= Series history\n\nChanges in v5:\n\n * Standardize the usage of \"protected configuration\" instead of mixing\n   \"config\" and \"configuration\". This required some unfortunate rewrapping.\n * Remove mentions of \"trustworthiness\" when discussing protected\n   configuration and focus on what Git does instead.\n   * The rationale of protected vs non-protected is still kept.\n * Fix the stale documentation entry for discovery.bare.\n * Include a fuller description of how discovery.bare and \"--git-dir\"\n   interact instead of saying \"has no effect\".\n\nChanges in v4:\n\n * 2/5's commit message now justifies what scopes are included in protected\n   config\n * The global configset is now a file-scope static inside config.c\n   (previously it was a member of the_repository).\n * Rename discovery_bare_config to discovery_bare_allowed\n * Make discovery_bare_allowed function-scoped (instead of global).\n * Add an expect_accepted helper to the discovery.bare tests.\n * Add a helper to \"upload-pack\" that reads the protected and non-protected\n   config\n\nChanges in v3:\n\n * Rebase onto a more recent 'master'\n * Reframe this feature in only in terms of the 'embedded bare repo' attack.\n * Other docs improvements (thanks Stolee in particular!)\n * Protected config no longer uses read_very_early_config() and is only read\n   once\n * Protected config now includes \"-c\"\n * uploadpack.packObjectsHook now uses protected config instead of ignoring\n   repo config using config scopes\n\nChanges in v2:\n\n * Rename safe.barerepository to discovery.bare and make it die()\n * Move tests into t/t0034-discovery-bare.sh\n * Avoid unnecessary config reading by using a static variable\n * Add discovery.bare=cwd\n * Fix typos\n\n= Future work\n\n * This series does not implement the \"no-embedded\" option [2] and I won't\n   work on it any time soon, but I'd be more than happy to review if someone\n   sends patches.\n * With discovery.bare, if a builtin is marked RUN_SETUP_GENTLY, setup.c\n   doesn't die() and we don't tell users why their repository was rejected,\n   e.g. \"git config\" gives an opaque \"fatal: not in a git directory\". This\n   isn't a new problem though, since safe.directory has the same issue.\n\n[1]\nhttps://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n\n[2] This was first suggested in\nhttps://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n\nGlen Choo (5):\n  Documentation/git-config.txt: add SCOPES section\n  Documentation: define protected configuration\n  config: learn `git_protected_config()`\n  safe.directory: use git_protected_config()\n  setup.c: create `discovery.bare`\n\n Documentation/config.txt            |  2 +\n Documentation/config/discovery.txt  | 23 +++++++++\n Documentation/config/safe.txt       |  6 +--\n Documentation/config/uploadpack.txt |  6 +--\n Documentation/git-config.txt        | 77 +++++++++++++++++++++++------\n config.c                            | 51 +++++++++++++++++++\n config.h                            | 17 +++++++\n setup.c                             | 59 +++++++++++++++++++++-\n t/t0033-safe-directory.sh           | 24 ++++-----\n t/t0035-discovery-bare.sh           | 68 +++++++++++++++++++++++++\n t/t5544-pack-objects-hook.sh        |  7 ++-\n upload-pack.c                       | 27 ++++++----\n 12 files changed, 320 insertions(+), 47 deletions(-)\n create mode 100644 Documentation/config/discovery.txt\n create mode 100755 t/t0035-discovery-bare.sh\n\n\nbase-commit: f770e9f396d48b567ef7b37d273e91ad570a3522\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1261%2Fchooglen%2Fsetup%2Fdisable-bare-repo-config-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1261/chooglen/setup/disable-bare-repo-config-v5\nPull-Request: https://github.com/git/git/pull/1261\n\nRange-diff vs v4:\n\n 1:  c0e27ab3b3e ! 1:  ee9619f6ec0 Documentation/git-config.txt: add SCOPES section\n     @@ Metadata\n       ## Commit message ##\n          Documentation/git-config.txt: add SCOPES section\n      \n     -    In a subsequent commit, we will introduce \"protected config\", which is\n     -    easiest to describe in terms of configuration scopes (i.e. it's the\n     -    union of the 'system', 'global', and 'command' scopes). This description\n     -    is fine for ML discussions, but it's inadequate for end users because we\n     -    don't provide a good description of \"config scopes\" in the public docs.\n     +    In a subsequent commit, we will introduce \"protected configuration\",\n     +    which is easiest to describe in terms of configuration scopes (i.e. it's\n     +    the union of the 'system', 'global', and 'command' scopes). This\n     +    description is fine for ML discussions, but it's inadequate for end\n     +    users because we don't provide a good description of \"configuration\n     +    scopes\" in the public docs.\n      \n          145d59f482 (config: add '--show-scope' to print the scope of a config\n          value, 2020-02-10) introduced the word \"scope\" to our public docs, but\n     @@ Commit message\n          those values mean.\n      \n          Add a SCOPES section to Documentation/git-config.txt that describes the\n     -    config scopes, their corresponding CLI options, and mentions that some\n     -    configuration options are only respected in certain scopes. Then,\n     +    configuration scopes, their corresponding CLI options, and mentions that\n     +    some configuration options are only respected in certain scopes. Then,\n          use the word \"scope\" to simplify the FILES section and change some\n          confusing wording.\n      \n 2:  a5a1dcb03e1 ! 2:  43627c05c0b Documentation: define protected configuration\n     @@ Commit message\n          Documentation: define protected configuration\n      \n          For security reasons, there are config variables that are only trusted\n     -    when they are specified in extra-trustworthy configuration scopes, which\n     -    are sometimes referred to on-list as 'protected configuration' [1]. A\n     -    future commit will introduce another such variable, so let's define our\n     -    terms so that we can have consistent documentation and implementation.\n     +    when they are specified in certain configuration scopes, which are\n     +    sometimes referred to on-list as 'protected configuration' [1]. A future\n     +    commit will introduce another such variable, so let's define our terms\n     +    so that we can have consistent documentation and implementation.\n      \n     -    In our documentation, define 'protected config' as the system, global\n     -    and command config scopes. As a shorthand, I will refer to variables\n     -    that are only respected in protected config as 'protected config only',\n     -    but this term is not used in the documentation.\n     +    In our documentation, define 'protected configuration' as the system,\n     +    global and command config scopes. As a shorthand, I will refer to\n     +    variables that are only respected in protected config as 'protected\n     +    configuration only', but this term is not used in the documentation.\n      \n     -    This definition of protected configuration is based on whether or not\n     -    Git can reasonably protect the user by ignoring the configuration scope:\n     +    This definition of protected config is based on whether or not Git can\n     +    reasonably protect the user by ignoring the configuration scope:\n      \n          - System, global and command line config are considered protected\n            because an attacker who has control over any of those can do plenty of\n     @@ Commit message\n            considered protected because it is relatively easy for an attacker to\n            control local config, e.g.:\n            - On some shared user environments, a non-admin attacker can create a\n     -        repository high up the directory hierarchy (e.g. C:\\.git on Windows),\n     -        and a user may accidentally use it when their PS1 automatically\n     -        invokes \"git\" commands.\n     +        repository high up the directory hierarchy (e.g. C:\\.git on\n     +        Windows), and a user may accidentally use it when their PS1\n     +        automatically invokes \"git\" commands.\n      \n              `safe.directory` prevents attacks of this form by making sure that\n              the user intended to use the shared repository. It obviously\n     @@ Commit message\n              repository (because \"git upload-pack\" would fail), but we can limit\n              the risks by ignoring `uploadpack.packObjectsHook`.\n      \n     -    Only `uploadpack.packObjectsHook` is 'protected config only'. The\n     +    Only `uploadpack.packObjectsHook` is 'protected configuration only'. The\n          following variables are intentionally excluded:\n      \n     -    - `safe.directory` should be 'protected config only', but it does not\n     -      technically fit the definition because it is not respected in the\n     +    - `safe.directory` should be 'protected configuration only', but it does\n     +      not technically fit the definition because it is not respected in the\n            \"command\" scope. A future commit will fix this.\n      \n          - `trace2.*` happens to read the same scopes as `safe.directory` because\n     @@ Documentation/git-config.txt: Most configuration options are respected regardles\n       defined in, but some options are only respected in certain scopes. See the\n       option's documentation for the full details.\n       \n     -+Protected config\n     -+~~~~~~~~~~~~~~~~\n     -+\n     -+Protected config refers to the 'system', 'global', and 'command' scopes. Git\n     -+considers these scopes to be especially trustworthy because they are likely\n     -+to be controlled by the user or a trusted administrator. An attacker who\n     -+controls these scopes can do substantial harm without using Git, so it is\n     -+assumed that the user's environment protects these scopes against attackers.\n     ++Protected configuration\n     ++~~~~~~~~~~~~~~~~~~~~~~~\n      +\n     ++Protected configuration refers to the 'system', 'global', and 'command' scopes.\n      +For security reasons, certain options are only respected when they are\n     -+specified in protected config, and ignored otherwise.\n     ++specified in protected configuration, and ignored otherwise.\n     ++\n     ++Git treats these scopes as if they are controlled by the user or a trusted\n     ++administrator. This is because an attacker who controls these scopes can do\n     ++substantial harm without using Git, so it is assumed that the user's environment\n     ++protects these scopes against attackers.\n      +\n       ENVIRONMENT\n       -----------\n 3:  94b40907e66 ! 3:  3efe282e6b9 config: read protected config with `git_protected_config()`\n     @@ Metadata\n      Author: Glen Choo <chooglen@google.com>\n      \n       ## Commit message ##\n     -    config: read protected config with `git_protected_config()`\n     +    config: learn `git_protected_config()`\n      \n     -    `uploadpack.packObjectsHook` is the only 'protected config only'\n     +    `uploadpack.packObjectsHook` is the only 'protected configuration only'\n          variable today, but we've noted that `safe.directory` and the upcoming\n     -    `discovery.bare` should also be 'protected config only'. So, for\n     +    `discovery.bare` should also be 'protected configuration only'. So, for\n          consistency, we'd like to have a single implementation for protected\n          config.\n      \n          The primary constraints are:\n      \n     -    1. Reading from protected config should be as fast as possible. Nearly\n     -       all \"git\" commands inside a bare repository will read both\n     +    1. Reading from protected configuration should be as fast as possible.\n     +       Nearly all \"git\" commands inside a bare repository will read both\n             `safe.directory` and `discovery.bare`, so we cannot afford to be\n             slow.\n      \n     @@ Commit message\n             `safe.directory` and `discovery.bare` both affect repository\n             discovery and the gitdir is not known at that point [1].\n      \n     -    The chosen implementation in this commit is to read protected config and\n     -    cache the values in a global configset. This is similar to the caching\n     -    behavior we get with the_repository->config.\n     +    The chosen implementation in this commit is to read protected\n     +    configuration and cache the values in a global configset. This is\n     +    similar to the caching behavior we get with the_repository->config.\n      \n     -    Introduce git_protected_config(), which reads protected config and\n     -    caches them in the global configset protected_config. Then, refactor\n     +    Introduce git_protected_config(), which reads protected configuration\n     +    and caches them in the global configset protected_config. Then, refactor\n          `uploadpack.packObjectsHook` to use git_protected_config().\n      \n     -    The protected config functions are named similarly to their\n     +    The protected configuration functions are named similarly to their\n          non-protected counterparts, e.g. git_protected_config_check_init() vs\n          git_config_check_init().\n      \n          In light of constraint 1, this implementation can still be improved\n          since git_protected_config() iterates through every variable in\n          protected_config, which may still be too expensive. There exist constant\n     -    time lookup functions for non-protected config (repo_config_get_*()),\n     -    but for simplicity, this commit does not implement similar functions for\n     -    protected config.\n     +    time lookup functions for non-protected configuration\n     +    (repo_config_get_*()), but for simplicity, this commit does not\n     +    implement similar functions for protected configuration.\n      \n          An alternative that avoids introducing another configset is to continue\n          to read all config using git_config(), but only accept values that have\n     @@ Commit message\n          the_repository->config, which would need to be reset when the gitdir is\n          known and git_config() needs to read the local and worktree config.\n          Resetting the_repository->config might be reasonable while we only have\n     -    these 'protected config only' variables, but it's not clear whether this\n     -    extends well to future variables.\n     +    these 'protected configuration only' variables, but it's not clear\n     +    whether this extends well to future variables.\n      \n          [1] In this case, we do have a candidate gitdir though, so with a little\n          refactoring, it might be possible to provide a gitdir.\n 4:  156817966fa ! 4:  ec925823414 safe.directory: use git_protected_config()\n     @@ Commit message\n          safe.directory: use git_protected_config()\n      \n          Use git_protected_config() to read `safe.directory` instead of\n     -    read_very_early_config(), making it 'protected config only'. As a\n     -    result, `safe.directory` now respects \"-c\", so update the tests and docs\n     -    accordingly.\n     +    read_very_early_config(), making it 'protected configuration only'.\n     +\n     +    As a result, `safe.directory` now respects \"-c\", so update the tests and\n     +    docs accordingly. It used to ignore \"-c\" due to how it was implemented,\n     +    not because of security or correctness concerns [1].\n     +\n     +    [1] https://lore.kernel.org/git/xmqqlevabcsu.fsf@gitster.g/\n      \n          Signed-off-by: Glen Choo <chooglen@google.com>\n      \n 5:  29053d029f8 ! 5:  14411512783 setup.c: create `discovery.bare`\n     @@ Commit message\n          Create a config variable, `discovery.bare`, that tells Git whether or\n          not to die() when it discovers a bare repository. This only affects\n          repository discovery, thus it has no effect if discovery was not\n     -    done (e.g. `--git-dir` was passed).\n     +    done, e.g. if the user passes `--git-dir=my-dir`, discovery will be\n     +    skipped and my-dir will be used as the repo regardless of the\n     +    `discovery.bare` value.\n      \n          This config is an enum of:\n      \n     @@ Documentation/config.txt: include::config/diff.txt[]\n       ## Documentation/config/discovery.txt (new) ##\n      @@\n      +discovery.bare::\n     -+\t'(Protected config only)' Specifies whether Git will work with a\n     -+\tbare repository that it found during repository discovery. This\n     -+\thas no effect if the repository is specified directly via the\n     -+\t--git-dir command-line option or the GIT_DIR environment\n     -+\tvariable (see linkgit:git[1]).\n     ++\tSpecifies whether Git will work with a bare repository that it\n     ++\tfound during repository discovery. If the repository is\n     ++\tspecified directly via the --git-dir command-line option or the\n     ++\tGIT_DIR environment variable (see linkgit:git[1]), Git will\n     ++\talways use the specified repository, regardless of this value.\n     +++\n     ++This config setting is only respected in protected configuration (see\n     ++<<SCOPES>>). This prevents the untrusted repository from tampering with\n     ++this value.\n      ++\n      +The currently supported values are:\n      ++\n\n-- \ngitgitgadget\n"},{"id":"457954","messageId":"ee9619f6ec0608f399fc924cfe9254df5e7bc431.1656354994.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v5.git.git.1656354994.gitgitgadget@gmail.com","subject":"[PATCH v5 1/5] Documentation/git-config.txt: add SCOPES section","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-27T18:36:30Z","receivedAt":"2022-06-27T18:38:41Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nIn a subsequent commit, we will introduce \"protected configuration\",\nwhich is easiest to describe in terms of configuration scopes (i.e. it's\nthe union of the 'system', 'global', and 'command' scopes). This\ndescription is fine for ML discussions, but it's inadequate for end\nusers because we don't provide a good description of \"configuration\nscopes\" in the public docs.\n\n145d59f482 (config: add '--show-scope' to print the scope of a config\nvalue, 2020-02-10) introduced the word \"scope\" to our public docs, but\nthat only enumerates the scopes and assumes the user can figure out\nthose values mean.\n\nAdd a SCOPES section to Documentation/git-config.txt that describes the\nconfiguration scopes, their corresponding CLI options, and mentions that\nsome configuration options are only respected in certain scopes. Then,\nuse the word \"scope\" to simplify the FILES section and change some\nconfusing wording.\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/git-config.txt | 64 ++++++++++++++++++++++++++++--------\n 1 file changed, 50 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex 9376e39aef2..f93d437b898 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -297,8 +297,8 @@ The default is to use a pager.\n FILES\n -----\n \n-If not set explicitly with `--file`, there are four files where\n-'git config' will search for configuration options:\n+By default, 'git config' will read configuration options from multiple\n+files:\n \n $(prefix)/etc/gitconfig::\n \tSystem-wide configuration file.\n@@ -322,27 +322,63 @@ $GIT_DIR/config.worktree::\n \tThis is optional and is only searched when\n \t`extensions.worktreeConfig` is present in $GIT_DIR/config.\n \n-If no further options are given, all reading options will read all of these\n-files that are available. If the global or the system-wide configuration\n-file are not available they will be ignored. If the repository configuration\n-file is not available or readable, 'git config' will exit with a non-zero\n-error code. However, in neither case will an error message be issued.\n+You may also provide additional configuration parameters when running any\n+git command by using the `-c` option. See linkgit:git[1] for details.\n+\n+Options will be read from all of these files that are available. If the\n+global or the system-wide configuration file are not available they will be\n+ignored. If the repository configuration file is not available or readable,\n+'git config' will exit with a non-zero error code. However, in neither case\n+will an error message be issued.\n \n The files are read in the order given above, with last value found taking\n precedence over values read earlier.  When multiple values are taken then all\n values of a key from all files will be used.\n \n-You may override individual configuration parameters when running any git\n-command by using the `-c` option. See linkgit:git[1] for details.\n-\n-All writing options will per default write to the repository specific\n+By default, options are only written to the repository specific\n configuration file. Note that this also affects options like `--replace-all`\n and `--unset`. *'git config' will only ever change one file at a time*.\n \n-You can override these rules using the `--global`, `--system`,\n-`--local`, `--worktree`, and `--file` command-line options; see\n-<<OPTIONS>> above.\n+You can change the way options are read/written by specifying the path to a\n+file (`--file`), or by specifying a configuration scope (`--system`,\n+`--global`, `--local`, `--worktree`); see <<OPTIONS>> above.\n+\n+SCOPES\n+------\n+\n+Each configuration source falls within a configuration scope. The scopes\n+are:\n+\n+system::\n+\t$(prefix)/etc/gitconfig\n+\n+global::\n+\t$XDG_CONFIG_HOME/git/config\n++\n+~/.gitconfig\n+\n+local::\n+\t$GIT_DIR/config\n+\n+worktree::\n+\t$GIT_DIR/config.worktree\n+\n+command::\n+\tenvironment variables\n++\n+the `-c` option\n+\n+With the exception of 'command', each scope corresponds to a command line\n+option - `--system`, `--global`, `--local`, `--worktree`.\n+\n+When reading options, specifying a scope will only read options from the\n+files within that scope. When writing options, specifying a scope will write\n+to the files within that scope (instead of the repository specific\n+configuration file). See <<OPTIONS>> above for a complete description.\n \n+Most configuration options are respected regardless of the scope it is\n+defined in, but some options are only respected in certain scopes. See the\n+option's documentation for the full details.\n \n ENVIRONMENT\n -----------\n-- \ngitgitgadget\n\n"},{"id":"458187","messageId":"220630.864k02e14e.gmgdl@evledraar.gmail.com","threadId":"57859","inReplyTo":"14411512783fd4e2cdcc8513690377b29262f6b8.1656354994.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v5 5/5] setup.c: create `discovery.bare`","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-06-30T13:20:01Z","receivedAt":"2022-06-30T13:23:36Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Jun 27 2022, Glen Choo via GitGitGadget wrote:\n\n> From: Glen Choo <chooglen@google.com>\n\n> diff --git a/t/t0035-discovery-bare.sh b/t/t0035-discovery-bare.sh\n> new file mode 100755\n> index 00000000000..0b345d361e6\n> --- /dev/null\n> +++ b/t/t0035-discovery-bare.sh\n> @@ -0,0 +1,68 @@\n> +#!/bin/sh\n> +\n> +test_description='verify discovery.bare checks'\n> +\n\nYou're missing a:\n\n\tTEST_PASSES_SANITIZE_LEAK=true\n\nAbove this line:\n\n> +. ./test-lib.sh\n\nWhich tells us that this new test doesn't leak (yay!)\n\n> +expect_accepted () {\n> +\tgit \"$@\" rev-parse --git-dir\n> +}\n\nI think we can do away with this helper, we use the argument support\nonce, and for the rest we can inline the trivial command...\n\n> +\n> +expect_rejected () {\n> +\ttest_must_fail git \"$@\" rev-parse --git-dir 2>err &&\n> +\tgrep \"discovery.bare\" err\n\ngrep -F ?\n\nThis helper is less trivial, but more obvious would be a \"run command\nand assirt xyz about the output\" helper, see\ne.g. test_stdout_line_count.\n\n\n> +test_expect_success 'discovery.bare unset' '\n> +\t(\n> +\t\tcd outer-repo/bare-repo &&\n> +\t\texpect_accepted\n> +\t)\n\nAlso: Odd to use a sub-shell when the helper takes -C...\n\n> +'\n> +\n> +test_expect_success 'discovery.bare=always' '\n> +\tgit config --global discovery.bare always &&\n> +\t(\n> +\t\tcd outer-repo/bare-repo &&\n> +\t\texpect_accepted\n> +\t)\n> +'\n> +\n> +test_expect_success 'discovery.bare=never' '\n> +\tgit config --global discovery.bare never &&\n> +\t(\n> +\t\tcd outer-repo/bare-repo &&\n> +\t\texpect_rejected\n> +\t)\n\n...ditto...\n\n\n> +'\n> +\n> +test_expect_success 'discovery.bare in the repository' '\n> +\t(\n> +\t\tcd outer-repo/bare-repo &&\n> +\t\t# Temporarily set discovery.bare=always, otherwise git\n> +\t\t# config fails with \"fatal: not in a git directory\"\n> +\t\t# (like safe.directory)\n> +\t\tgit config --global discovery.bare always &&\n> +\t\tgit config discovery.bare always &&\n> +\t\tgit config --global discovery.bare never &&\n> +\t\texpect_rejected\n> +\t)\n\nDrop the sub-shell and use test_config?\n\n> +'\n> +\n> +test_expect_success 'discovery.bare on the command line' '\n> +\tgit config --global discovery.bare never &&\n> +\t(\n> +\t\tcd outer-repo/bare-repo &&\n> +\t\texpect_accepted -c discovery.bare=always &&\n> +\t\texpect_rejected -c discovery.bare=\n> +\t)\n> +'\n> +\n> +test_done\n\n"},{"id":"458213","messageId":"kl6l5ykit61g.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"220630.864k02e14e.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v5 5/5] setup.c: create `discovery.bare`","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-06-30T17:28:11Z","receivedAt":"2022-06-30T17:28:18Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> On Mon, Jun 27 2022, Glen Choo via GitGitGadget wrote:\n>\n>> From: Glen Choo <chooglen@google.com>\n>\n>> diff --git a/t/t0035-discovery-bare.sh b/t/t0035-discovery-bare.sh\n>> new file mode 100755\n>> index 00000000000..0b345d361e6\n>> --- /dev/null\n>> +++ b/t/t0035-discovery-bare.sh\n>> @@ -0,0 +1,68 @@\n>> +#!/bin/sh\n>> +\n>> +test_description='verify discovery.bare checks'\n>> +\n>\n> You're missing a:\n>\n> \tTEST_PASSES_SANITIZE_LEAK=true\n>\n> Above this line:\n>\n>> +. ./test-lib.sh\n>\n> Which tells us that this new test doesn't leak (yay!)\n\nAh, thanks! Hooray.\n\n>> +expect_accepted () {\n>> +\tgit \"$@\" rev-parse --git-dir\n>> +}\n>\n> I think we can do away with this helper, we use the argument support\n> once, and for the rest we can inline the trivial command...\n\nThat is true, having fewer test helpers can be a good idea. Though in\nthis case, the helper wins out slightly (IMO at least) because of the \nreadability/refactoring benefit.\n\n>> +\n>> +expect_rejected () {\n>> +\ttest_must_fail git \"$@\" rev-parse --git-dir 2>err &&\n>> +\tgrep \"discovery.bare\" err\n>\n> grep -F ?\n>\n> This helper is less trivial, but more obvious would be a \"run command\n> and assirt xyz about the output\" helper, see\n> e.g. test_stdout_line_count.\n\nThis takes precedent from t0033, which does the same \"run command and\ngrep the result\". And just as I typed this out, I remembered that\nt0033's corresponding test helper was made more specific in f62563988f\n(t0033-safe-directory: check the error message without matching the\ntrash dir, 2022-04-27), because just grep-ing for the config variable\nmasked some errors.\n\nIt turns out the same thing is happening in the last test - I forgot\nthat \"-c\" doesn't unset the variable (it sets the value to ''), and the\ntest_must_fail passes because we fail to parse \"discovery.bare\", _not_\nbecause we forbade the repo.\n\nSo besides -F, I think the only change here would be to grep on the\nspecific \"cannot use bare repository\" message (instead of grepping for\n\"discovery.bare\").\n\n>> +test_expect_success 'discovery.bare unset' '\n>> +\t(\n>> +\t\tcd outer-repo/bare-repo &&\n>> +\t\texpect_accepted\n>> +\t)\n>\n> Also: Odd to use a sub-shell when the helper takes -C...\n>\n>> +'\n>> +\n>> +test_expect_success 'discovery.bare=always' '\n>> +\tgit config --global discovery.bare always &&\n>> +\t(\n>> +\t\tcd outer-repo/bare-repo &&\n>> +\t\texpect_accepted\n>> +\t)\n>> +'\n>> +\n>> +test_expect_success 'discovery.bare=never' '\n>> +\tgit config --global discovery.bare never &&\n>> +\t(\n>> +\t\tcd outer-repo/bare-repo &&\n>> +\t\texpect_rejected\n>> +\t)\n>\n> ...ditto...\n\nOk, I'll drop the sub-shell.\n\n>\n>> +'\n>> +\n>> +test_expect_success 'discovery.bare in the repository' '\n>> +\t(\n>> +\t\tcd outer-repo/bare-repo &&\n>> +\t\t# Temporarily set discovery.bare=always, otherwise git\n>> +\t\t# config fails with \"fatal: not in a git directory\"\n>> +\t\t# (like safe.directory)\n>> +\t\tgit config --global discovery.bare always &&\n>> +\t\tgit config discovery.bare always &&\n>> +\t\tgit config --global discovery.bare never &&\n>> +\t\texpect_rejected\n>> +\t)\n>\n> Drop the sub-shell and use test_config?\n\nOh, I was so focused on t0033 that I hadn't realized that we had\ntest_config_global. Thanks :)\n\n>> +'\n>> +\n>> +test_expect_success 'discovery.bare on the command line' '\n>> +\tgit config --global discovery.bare never &&\n>> +\t(\n>> +\t\tcd outer-repo/bare-repo &&\n>> +\t\texpect_accepted -c discovery.bare=always &&\n>> +\t\texpect_rejected -c discovery.bare=\n>> +\t)\n>> +'\n>> +\n>> +test_done\n"},{"id":"458232","messageId":"pull.1261.v6.git.git.1656612839.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v5.git.git.1656354994.gitgitgadget@gmail.com","subject":"[PATCH v6 0/5] config: introduce discovery.bare and protected config","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-30T18:13:54Z","receivedAt":"2022-06-30T18:14:22Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"This is a quick re-roll to address Ævar's comments on the tests (thanks!).\n\nThis version is mostly simple refactoring, and I also removed a useless test\nassertion (see \"Series history\").\n\n= Description\n\nThere is a known social engineering attack that takes advantage of the fact\nthat a working tree can include an entire bare repository, including a\nconfig file. A user could run a Git command inside the bare repository\nthinking that the config file of the 'outer' repository would be used, but\nin reality, the bare repository's config file (which is attacker-controlled)\nis used, which may result in arbitrary code execution. See [1] for a fuller\ndescription and deeper discussion.\n\nThis series implements a simple way of preventing such attacks: create a\nconfig option, discovery.bare, that tells Git whether or not to die when it\nfinds a bare repository. discovery.bare has two values:\n\n * \"always\": always allow bare repositories (default), identical to current\n   behavior\n * \"never\": never allow bare repositories\n\nand users/system administrators who never expect to work with bare\nrepositories can secure their environments using \"never\". discovery.bare has\nno effect if --git-dir or GIT_DIR is passed because we are confident that\nthe user is not confused about which repository is being used.\n\nThis series does not change the default behavior, but in the long-run, a\n\"no-embedded\" option might be a safe and usable default [2]. \"never\" is too\nrestrictive and unlikely to be the default.\n\nFor security reasons, discovery.bare cannot be read from repository-level\nconfig (because we would end up trusting the embedded bare repository that\nwe aren't supposed to trust to begin with). Since this would introduce a 3rd\nvariable that is only read from 'protected/trusted configuration' (the\nothers are safe.directory and uploadpack.packObjectsHook) this series also\ndefines and creates a shared implementation for 'protected configuration'\n\n= Patch organization\n\n * Patch 1 add a section on configuration scopes to our docs\n * Patches 2-3 define 'protected configuration' and create a shared\n   implementation.\n * Patch 4 refactors safe.directory to use protected configuration\n * Patch 5 adds discovery.bare\n\n= Series history\n\nChanges in v6:\n\n * Add TEST_PASSES_SANITIZE_LEAK=true\n * Replace all sub-shells with -C and use test_config_global\n * Change the expect_rejected helper to use \"grep -F\" with a more specific\n   message.\n   * This reveals that the \"-c discovery.bare=\" assertion in the last test\n     was passing for the wrong reason (because '' is an invalid value for\n     \"discovery.bare\"). I removed it because it wasn't doing anything useful\n     anyway - I was trying to make discovery.bare unset in the command line,\n     but the whole point of that test is to assert that we respect the CLI\n     arg.\n\nChanges in v5:\n\n * Standardize the usage of \"protected configuration\" instead of mixing\n   \"config\" and \"configuration\". This required some unfortunate rewrapping.\n * Remove mentions of \"trustworthiness\" when discussing protected\n   configuration and focus on what Git does instead.\n   * The rationale of protected vs non-protected is still kept.\n * Fix the stale documentation entry for discovery.bare.\n * Include a fuller description of how discovery.bare and \"--git-dir\"\n   interact instead of saying \"has no effect\".\n\nChanges in v4:\n\n * 2/5's commit message now justifies what scopes are included in protected\n   config\n * The global configset is now a file-scope static inside config.c\n   (previously it was a member of the_repository).\n * Rename discovery_bare_config to discovery_bare_allowed\n * Make discovery_bare_allowed function-scoped (instead of global).\n * Add an expect_accepted helper to the discovery.bare tests.\n * Add a helper to \"upload-pack\" that reads the protected and non-protected\n   config\n\nChanges in v3:\n\n * Rebase onto a more recent 'master'\n * Reframe this feature in only in terms of the 'embedded bare repo' attack.\n * Other docs improvements (thanks Stolee in particular!)\n * Protected config no longer uses read_very_early_config() and is only read\n   once\n * Protected config now includes \"-c\"\n * uploadpack.packObjectsHook now uses protected config instead of ignoring\n   repo config using config scopes\n\nChanges in v2:\n\n * Rename safe.barerepository to discovery.bare and make it die()\n * Move tests into t/t0034-discovery-bare.sh\n * Avoid unnecessary config reading by using a static variable\n * Add discovery.bare=cwd\n * Fix typos\n\n= Future work\n\n * This series does not implement the \"no-embedded\" option [2] and I won't\n   work on it any time soon, but I'd be more than happy to review if someone\n   sends patches.\n * With discovery.bare, if a builtin is marked RUN_SETUP_GENTLY, setup.c\n   doesn't die() and we don't tell users why their repository was rejected,\n   e.g. \"git config\" gives an opaque \"fatal: not in a git directory\". This\n   isn't a new problem though, since safe.directory has the same issue.\n\n[1]\nhttps://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n\n[2] This was first suggested in\nhttps://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n\nGlen Choo (5):\n  Documentation/git-config.txt: add SCOPES section\n  Documentation: define protected configuration\n  config: learn `git_protected_config()`\n  safe.directory: use git_protected_config()\n  setup.c: create `discovery.bare`\n\n Documentation/config.txt            |  2 +\n Documentation/config/discovery.txt  | 23 +++++++++\n Documentation/config/safe.txt       |  6 +--\n Documentation/config/uploadpack.txt |  6 +--\n Documentation/git-config.txt        | 77 +++++++++++++++++++++++------\n config.c                            | 51 +++++++++++++++++++\n config.h                            | 17 +++++++\n setup.c                             | 59 +++++++++++++++++++++-\n t/t0033-safe-directory.sh           | 24 ++++-----\n t/t0035-discovery-bare.sh           | 52 +++++++++++++++++++\n t/t5544-pack-objects-hook.sh        |  7 ++-\n upload-pack.c                       | 27 ++++++----\n 12 files changed, 304 insertions(+), 47 deletions(-)\n create mode 100644 Documentation/config/discovery.txt\n create mode 100755 t/t0035-discovery-bare.sh\n\n\nbase-commit: f770e9f396d48b567ef7b37d273e91ad570a3522\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1261%2Fchooglen%2Fsetup%2Fdisable-bare-repo-config-v6\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1261/chooglen/setup/disable-bare-repo-config-v6\nPull-Request: https://github.com/git/git/pull/1261\n\nRange-diff vs v5:\n\n 1:  ee9619f6ec0 = 1:  ee9619f6ec0 Documentation/git-config.txt: add SCOPES section\n 2:  43627c05c0b = 2:  43627c05c0b Documentation: define protected configuration\n 3:  3efe282e6b9 = 3:  3efe282e6b9 config: learn `git_protected_config()`\n 4:  ec925823414 = 4:  ec925823414 safe.directory: use git_protected_config()\n 5:  14411512783 ! 5:  a1323d963f9 setup.c: create `discovery.bare`\n     @@ t/t0035-discovery-bare.sh (new)\n      +\n      +test_description='verify discovery.bare checks'\n      +\n     ++TEST_PASSES_SANITIZE_LEAK=true\n      +. ./test-lib.sh\n      +\n      +pwd=\"$(pwd)\"\n     @@ t/t0035-discovery-bare.sh (new)\n      +\n      +expect_rejected () {\n      +\ttest_must_fail git \"$@\" rev-parse --git-dir 2>err &&\n     -+\tgrep \"discovery.bare\" err\n     ++\tgrep -F \"cannot use bare repository\" err\n      +}\n      +\n      +test_expect_success 'setup bare repo in worktree' '\n     @@ t/t0035-discovery-bare.sh (new)\n      +'\n      +\n      +test_expect_success 'discovery.bare unset' '\n     -+\t(\n     -+\t\tcd outer-repo/bare-repo &&\n     -+\t\texpect_accepted\n     -+\t)\n     ++\texpect_accepted -C outer-repo/bare-repo\n      +'\n      +\n      +test_expect_success 'discovery.bare=always' '\n     -+\tgit config --global discovery.bare always &&\n     -+\t(\n     -+\t\tcd outer-repo/bare-repo &&\n     -+\t\texpect_accepted\n     -+\t)\n     ++\ttest_config_global discovery.bare always &&\n     ++\texpect_accepted -C outer-repo/bare-repo\n      +'\n      +\n      +test_expect_success 'discovery.bare=never' '\n     -+\tgit config --global discovery.bare never &&\n     -+\t(\n     -+\t\tcd outer-repo/bare-repo &&\n     -+\t\texpect_rejected\n     -+\t)\n     ++\ttest_config_global discovery.bare never &&\n     ++\texpect_rejected -C outer-repo/bare-repo\n      +'\n      +\n      +test_expect_success 'discovery.bare in the repository' '\n     -+\t(\n     -+\t\tcd outer-repo/bare-repo &&\n     -+\t\t# Temporarily set discovery.bare=always, otherwise git\n     -+\t\t# config fails with \"fatal: not in a git directory\"\n     -+\t\t# (like safe.directory)\n     -+\t\tgit config --global discovery.bare always &&\n     -+\t\tgit config discovery.bare always &&\n     -+\t\tgit config --global discovery.bare never &&\n     -+\t\texpect_rejected\n     -+\t)\n     ++\t# discovery.bare must not be \"never\", otherwise git config fails\n     ++\t# with \"fatal: not in a git directory\" (like safe.directory)\n     ++\ttest_config -C outer-repo/bare-repo discovery.bare always &&\n     ++\ttest_config_global discovery.bare never &&\n     ++\texpect_rejected -C outer-repo/bare-repo\n      +'\n      +\n      +test_expect_success 'discovery.bare on the command line' '\n     -+\tgit config --global discovery.bare never &&\n     -+\t(\n     -+\t\tcd outer-repo/bare-repo &&\n     -+\t\texpect_accepted -c discovery.bare=always &&\n     -+\t\texpect_rejected -c discovery.bare=\n     -+\t)\n     ++\ttest_config_global discovery.bare never &&\n     ++\texpect_accepted -C outer-repo/bare-repo \\\n     ++\t\t-c discovery.bare=always\n      +'\n      +\n      +test_done\n\n-- \ngitgitgadget\n"},{"id":"458233","messageId":"ee9619f6ec0608f399fc924cfe9254df5e7bc431.1656612839.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v6.git.git.1656612839.gitgitgadget@gmail.com","subject":"[PATCH v6 1/5] Documentation/git-config.txt: add SCOPES section","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-30T18:13:55Z","receivedAt":"2022-06-30T18:14:25Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nIn a subsequent commit, we will introduce \"protected configuration\",\nwhich is easiest to describe in terms of configuration scopes (i.e. it's\nthe union of the 'system', 'global', and 'command' scopes). This\ndescription is fine for ML discussions, but it's inadequate for end\nusers because we don't provide a good description of \"configuration\nscopes\" in the public docs.\n\n145d59f482 (config: add '--show-scope' to print the scope of a config\nvalue, 2020-02-10) introduced the word \"scope\" to our public docs, but\nthat only enumerates the scopes and assumes the user can figure out\nthose values mean.\n\nAdd a SCOPES section to Documentation/git-config.txt that describes the\nconfiguration scopes, their corresponding CLI options, and mentions that\nsome configuration options are only respected in certain scopes. Then,\nuse the word \"scope\" to simplify the FILES section and change some\nconfusing wording.\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/git-config.txt | 64 ++++++++++++++++++++++++++++--------\n 1 file changed, 50 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex 9376e39aef2..f93d437b898 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -297,8 +297,8 @@ The default is to use a pager.\n FILES\n -----\n \n-If not set explicitly with `--file`, there are four files where\n-'git config' will search for configuration options:\n+By default, 'git config' will read configuration options from multiple\n+files:\n \n $(prefix)/etc/gitconfig::\n \tSystem-wide configuration file.\n@@ -322,27 +322,63 @@ $GIT_DIR/config.worktree::\n \tThis is optional and is only searched when\n \t`extensions.worktreeConfig` is present in $GIT_DIR/config.\n \n-If no further options are given, all reading options will read all of these\n-files that are available. If the global or the system-wide configuration\n-file are not available they will be ignored. If the repository configuration\n-file is not available or readable, 'git config' will exit with a non-zero\n-error code. However, in neither case will an error message be issued.\n+You may also provide additional configuration parameters when running any\n+git command by using the `-c` option. See linkgit:git[1] for details.\n+\n+Options will be read from all of these files that are available. If the\n+global or the system-wide configuration file are not available they will be\n+ignored. If the repository configuration file is not available or readable,\n+'git config' will exit with a non-zero error code. However, in neither case\n+will an error message be issued.\n \n The files are read in the order given above, with last value found taking\n precedence over values read earlier.  When multiple values are taken then all\n values of a key from all files will be used.\n \n-You may override individual configuration parameters when running any git\n-command by using the `-c` option. See linkgit:git[1] for details.\n-\n-All writing options will per default write to the repository specific\n+By default, options are only written to the repository specific\n configuration file. Note that this also affects options like `--replace-all`\n and `--unset`. *'git config' will only ever change one file at a time*.\n \n-You can override these rules using the `--global`, `--system`,\n-`--local`, `--worktree`, and `--file` command-line options; see\n-<<OPTIONS>> above.\n+You can change the way options are read/written by specifying the path to a\n+file (`--file`), or by specifying a configuration scope (`--system`,\n+`--global`, `--local`, `--worktree`); see <<OPTIONS>> above.\n+\n+SCOPES\n+------\n+\n+Each configuration source falls within a configuration scope. The scopes\n+are:\n+\n+system::\n+\t$(prefix)/etc/gitconfig\n+\n+global::\n+\t$XDG_CONFIG_HOME/git/config\n++\n+~/.gitconfig\n+\n+local::\n+\t$GIT_DIR/config\n+\n+worktree::\n+\t$GIT_DIR/config.worktree\n+\n+command::\n+\tenvironment variables\n++\n+the `-c` option\n+\n+With the exception of 'command', each scope corresponds to a command line\n+option - `--system`, `--global`, `--local`, `--worktree`.\n+\n+When reading options, specifying a scope will only read options from the\n+files within that scope. When writing options, specifying a scope will write\n+to the files within that scope (instead of the repository specific\n+configuration file). See <<OPTIONS>> above for a complete description.\n \n+Most configuration options are respected regardless of the scope it is\n+defined in, but some options are only respected in certain scopes. See the\n+option's documentation for the full details.\n \n ENVIRONMENT\n -----------\n-- \ngitgitgadget\n\n"},{"id":"458234","messageId":"43627c05c0b997ea407c865b04994cba630297d6.1656612839.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v6.git.git.1656612839.gitgitgadget@gmail.com","subject":"[PATCH v6 2/5] Documentation: define protected configuration","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-30T18:13:56Z","receivedAt":"2022-06-30T18:14:26Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nFor security reasons, there are config variables that are only trusted\nwhen they are specified in certain configuration scopes, which are\nsometimes referred to on-list as 'protected configuration' [1]. A future\ncommit will introduce another such variable, so let's define our terms\nso that we can have consistent documentation and implementation.\n\nIn our documentation, define 'protected configuration' as the system,\nglobal and command config scopes. As a shorthand, I will refer to\nvariables that are only respected in protected config as 'protected\nconfiguration only', but this term is not used in the documentation.\n\nThis definition of protected config is based on whether or not Git can\nreasonably protect the user by ignoring the configuration scope:\n\n- System, global and command line config are considered protected\n  because an attacker who has control over any of those can do plenty of\n  harm without Git, so we gain very little by ignoring those scopes.\n- On the other hand, local (and similarly, worktree) config are not\n  considered protected because it is relatively easy for an attacker to\n  control local config, e.g.:\n  - On some shared user environments, a non-admin attacker can create a\n    repository high up the directory hierarchy (e.g. C:\\.git on\n    Windows), and a user may accidentally use it when their PS1\n    automatically invokes \"git\" commands.\n\n    `safe.directory` prevents attacks of this form by making sure that\n    the user intended to use the shared repository. It obviously\n    shouldn't be read from the repository, because that would end up\n    trusting the repository that Git was supposed to reject.\n  - \"git upload-pack\" is expected to run in repositories that may not be\n    controlled by the user. We cannot ignore all config in that\n    repository (because \"git upload-pack\" would fail), but we can limit\n    the risks by ignoring `uploadpack.packObjectsHook`.\n\nOnly `uploadpack.packObjectsHook` is 'protected configuration only'. The\nfollowing variables are intentionally excluded:\n\n- `safe.directory` should be 'protected configuration only', but it does\n  not technically fit the definition because it is not respected in the\n  \"command\" scope. A future commit will fix this.\n\n- `trace2.*` happens to read the same scopes as `safe.directory` because\n  they share an implementation. However, this is not for security\n  reasons; it is because we want to start tracing so early that\n  repository-level config and \"-c\" are not available [2].\n\n  This requirement is unique to `trace2.*`, so it does not makes sense\n  for protected configuration to be subject to the same constraints.\n\n[1] For example,\nhttps://lore.kernel.org/git/6af83767-576b-75c4-c778-0284344a8fe7@github.com/\n[2] https://lore.kernel.org/git/a0c89d0d-669e-bf56-25d2-cbb09b012e70@jeffhostetler.com/\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config/uploadpack.txt |  6 +++---\n Documentation/git-config.txt        | 13 +++++++++++++\n 2 files changed, 16 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/config/uploadpack.txt b/Documentation/config/uploadpack.txt\nindex 32fad5bbe81..029abbefdff 100644\n--- a/Documentation/config/uploadpack.txt\n+++ b/Documentation/config/uploadpack.txt\n@@ -49,9 +49,9 @@ uploadpack.packObjectsHook::\n \t`pack-objects` to the hook, and expects a completed packfile on\n \tstdout.\n +\n-Note that this configuration variable is ignored if it is seen in the\n-repository-level config (this is a safety measure against fetching from\n-untrusted repositories).\n+Note that this configuration variable is only respected when it is specified\n+in protected config (see <<SCOPES>>). This is a safety measure against\n+fetching from untrusted repositories.\n \n uploadpack.allowFilter::\n \tIf this option is set, `upload-pack` will support partial\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex f93d437b898..f1810952891 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -343,6 +343,7 @@ You can change the way options are read/written by specifying the path to a\n file (`--file`), or by specifying a configuration scope (`--system`,\n `--global`, `--local`, `--worktree`); see <<OPTIONS>> above.\n \n+[[SCOPES]]\n SCOPES\n ------\n \n@@ -380,6 +381,18 @@ Most configuration options are respected regardless of the scope it is\n defined in, but some options are only respected in certain scopes. See the\n option's documentation for the full details.\n \n+Protected configuration\n+~~~~~~~~~~~~~~~~~~~~~~~\n+\n+Protected configuration refers to the 'system', 'global', and 'command' scopes.\n+For security reasons, certain options are only respected when they are\n+specified in protected configuration, and ignored otherwise.\n+\n+Git treats these scopes as if they are controlled by the user or a trusted\n+administrator. This is because an attacker who controls these scopes can do\n+substantial harm without using Git, so it is assumed that the user's environment\n+protects these scopes against attackers.\n+\n ENVIRONMENT\n -----------\n \n-- \ngitgitgadget\n\n"},{"id":"458235","messageId":"3efe282e6b94c3daed77590c5f601fad34137c9c.1656612839.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v6.git.git.1656612839.gitgitgadget@gmail.com","subject":"[PATCH v6 3/5] config: learn `git_protected_config()`","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-30T18:13:57Z","receivedAt":"2022-06-30T18:14:28Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\n`uploadpack.packObjectsHook` is the only 'protected configuration only'\nvariable today, but we've noted that `safe.directory` and the upcoming\n`discovery.bare` should also be 'protected configuration only'. So, for\nconsistency, we'd like to have a single implementation for protected\nconfig.\n\nThe primary constraints are:\n\n1. Reading from protected configuration should be as fast as possible.\n   Nearly all \"git\" commands inside a bare repository will read both\n   `safe.directory` and `discovery.bare`, so we cannot afford to be\n   slow.\n\n2. Protected config must be readable when the gitdir is not known.\n   `safe.directory` and `discovery.bare` both affect repository\n   discovery and the gitdir is not known at that point [1].\n\nThe chosen implementation in this commit is to read protected\nconfiguration and cache the values in a global configset. This is\nsimilar to the caching behavior we get with the_repository->config.\n\nIntroduce git_protected_config(), which reads protected configuration\nand caches them in the global configset protected_config. Then, refactor\n`uploadpack.packObjectsHook` to use git_protected_config().\n\nThe protected configuration functions are named similarly to their\nnon-protected counterparts, e.g. git_protected_config_check_init() vs\ngit_config_check_init().\n\nIn light of constraint 1, this implementation can still be improved\nsince git_protected_config() iterates through every variable in\nprotected_config, which may still be too expensive. There exist constant\ntime lookup functions for non-protected configuration\n(repo_config_get_*()), but for simplicity, this commit does not\nimplement similar functions for protected configuration.\n\nAn alternative that avoids introducing another configset is to continue\nto read all config using git_config(), but only accept values that have\nthe correct config scope [2]. This technically fulfills constraint 2,\nbecause git_config() simply ignores the local and worktree config when\nthe gitdir is not known. However, this would read incomplete config into\nthe_repository->config, which would need to be reset when the gitdir is\nknown and git_config() needs to read the local and worktree config.\nResetting the_repository->config might be reasonable while we only have\nthese 'protected configuration only' variables, but it's not clear\nwhether this extends well to future variables.\n\n[1] In this case, we do have a candidate gitdir though, so with a little\nrefactoring, it might be possible to provide a gitdir.\n[2] This is how `uploadpack.packObjectsHook` was implemented prior to\nthis commit.\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n config.c                     | 51 ++++++++++++++++++++++++++++++++++++\n config.h                     | 17 ++++++++++++\n t/t5544-pack-objects-hook.sh |  7 ++++-\n upload-pack.c                | 27 ++++++++++++-------\n 4 files changed, 91 insertions(+), 11 deletions(-)\n\ndiff --git a/config.c b/config.c\nindex 9b0e9c93285..29e62f5d0ed 100644\n--- a/config.c\n+++ b/config.c\n@@ -81,6 +81,18 @@ static enum config_scope current_parsing_scope;\n static int pack_compression_seen;\n static int zlib_compression_seen;\n \n+/*\n+ * Config that comes from trusted sources, namely:\n+ * - system config files (e.g. /etc/gitconfig)\n+ * - global config files (e.g. $HOME/.gitconfig,\n+ *   $XDG_CONFIG_HOME/git)\n+ * - the command line.\n+ *\n+ * This is declared here for code cleanliness, but unlike the other\n+ * static variables, this does not hold config parser state.\n+ */\n+static struct config_set protected_config;\n+\n static int config_file_fgetc(struct config_source *conf)\n {\n \treturn getc_unlocked(conf->u.file);\n@@ -2378,6 +2390,11 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n \treturn git_config_from_file(config_set_callback, filename, cs);\n }\n \n+int git_configset_add_parameters(struct config_set *cs)\n+{\n+\treturn git_config_from_parameters(config_set_callback, cs);\n+}\n+\n int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n {\n \tconst struct string_list *values = NULL;\n@@ -2619,6 +2636,40 @@ int repo_config_get_pathname(struct repository *repo,\n \treturn ret;\n }\n \n+/* Read values into protected_config. */\n+static void read_protected_config(void)\n+{\n+\tchar *xdg_config = NULL, *user_config = NULL, *system_config = NULL;\n+\n+\tgit_configset_init(&protected_config);\n+\n+\tsystem_config = git_system_config();\n+\tgit_global_config(&user_config, &xdg_config);\n+\n+\tgit_configset_add_file(&protected_config, system_config);\n+\tgit_configset_add_file(&protected_config, xdg_config);\n+\tgit_configset_add_file(&protected_config, user_config);\n+\tgit_configset_add_parameters(&protected_config);\n+\n+\tfree(system_config);\n+\tfree(xdg_config);\n+\tfree(user_config);\n+}\n+\n+/* Ensure that protected_config has been initialized. */\n+static void git_protected_config_check_init(void)\n+{\n+\tif (protected_config.hash_initialized)\n+\t\treturn;\n+\tread_protected_config();\n+}\n+\n+void git_protected_config(config_fn_t fn, void *data)\n+{\n+\tgit_protected_config_check_init();\n+\tconfigset_iter(&protected_config, fn, data);\n+}\n+\n /* Functions used historically to read configuration from 'the_repository' */\n void git_config(config_fn_t fn, void *data)\n {\ndiff --git a/config.h b/config.h\nindex 7654f61c634..e3ff1fcf683 100644\n--- a/config.h\n+++ b/config.h\n@@ -446,6 +446,15 @@ void git_configset_init(struct config_set *cs);\n  */\n int git_configset_add_file(struct config_set *cs, const char *filename);\n \n+/**\n+ * Parses command line options and environment variables, and adds the\n+ * variable-value pairs to the `config_set`. Returns 0 on success, or -1\n+ * if there is an error in parsing. The caller decides whether to free\n+ * the incomplete configset or continue using it when the function\n+ * returns -1.\n+ */\n+int git_configset_add_parameters(struct config_set *cs);\n+\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key` and config set `cs`. When the\n@@ -505,6 +514,14 @@ int repo_config_get_maybe_bool(struct repository *repo,\n int repo_config_get_pathname(struct repository *repo,\n \t\t\t     const char *key, const char **dest);\n \n+/*\n+ * Functions for reading protected config. By definition, protected\n+ * config ignores repository config, so it is unnecessary to read\n+ * protected config from any `struct repository` other than\n+ * the_repository.\n+ */\n+void git_protected_config(config_fn_t fn, void *data);\n+\n /**\n  * Querying For Specific Variables\n  * -------------------------------\ndiff --git a/t/t5544-pack-objects-hook.sh b/t/t5544-pack-objects-hook.sh\nindex dd5f44d986f..54f54f8d2eb 100755\n--- a/t/t5544-pack-objects-hook.sh\n+++ b/t/t5544-pack-objects-hook.sh\n@@ -56,7 +56,12 @@ test_expect_success 'hook does not run from repo config' '\n \t! grep \"hook running\" stderr &&\n \ttest_path_is_missing .git/hook.args &&\n \ttest_path_is_missing .git/hook.stdin &&\n-\ttest_path_is_missing .git/hook.stdout\n+\ttest_path_is_missing .git/hook.stdout &&\n+\n+\t# check that global config is used instead\n+\ttest_config_global uploadpack.packObjectsHook ./hook &&\n+\tgit clone --no-local . dst2.git 2>stderr &&\n+\tgrep \"hook running\" stderr\n '\n \n test_expect_success 'hook works with partial clone' '\ndiff --git a/upload-pack.c b/upload-pack.c\nindex 3a851b36066..09f48317b02 100644\n--- a/upload-pack.c\n+++ b/upload-pack.c\n@@ -1321,18 +1321,27 @@ static int upload_pack_config(const char *var, const char *value, void *cb_data)\n \t\tdata->advertise_sid = git_config_bool(var, value);\n \t}\n \n-\tif (current_config_scope() != CONFIG_SCOPE_LOCAL &&\n-\t    current_config_scope() != CONFIG_SCOPE_WORKTREE) {\n-\t\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n-\t\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n-\t}\n-\n \tif (parse_object_filter_config(var, value, data) < 0)\n \t\treturn -1;\n \n \treturn parse_hide_refs_config(var, value, \"uploadpack\");\n }\n \n+static int upload_pack_protected_config(const char *var, const char *value, void *cb_data)\n+{\n+\tstruct upload_pack_data *data = cb_data;\n+\n+\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n+\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n+\treturn 0;\n+}\n+\n+static void get_upload_pack_config(struct upload_pack_data *data)\n+{\n+\tgit_config(upload_pack_config, data);\n+\tgit_protected_config(upload_pack_protected_config, data);\n+}\n+\n void upload_pack(const int advertise_refs, const int stateless_rpc,\n \t\t const int timeout)\n {\n@@ -1340,8 +1349,7 @@ void upload_pack(const int advertise_refs, const int stateless_rpc,\n \tstruct upload_pack_data data;\n \n \tupload_pack_data_init(&data);\n-\n-\tgit_config(upload_pack_config, &data);\n+\tget_upload_pack_config(&data);\n \n \tdata.stateless_rpc = stateless_rpc;\n \tdata.timeout = timeout;\n@@ -1695,8 +1703,7 @@ int upload_pack_v2(struct repository *r, struct packet_reader *request)\n \n \tupload_pack_data_init(&data);\n \tdata.use_sideband = LARGE_PACKET_MAX;\n-\n-\tgit_config(upload_pack_config, &data);\n+\tget_upload_pack_config(&data);\n \n \twhile (state != FETCH_DONE) {\n \t\tswitch (state) {\n-- \ngitgitgadget\n\n"},{"id":"458236","messageId":"ec9258234142c4d1d164cbf475cb86e7b826bb8f.1656612839.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v6.git.git.1656612839.gitgitgadget@gmail.com","subject":"[PATCH v6 4/5] safe.directory: use git_protected_config()","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-30T18:13:58Z","receivedAt":"2022-06-30T18:14:35Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nUse git_protected_config() to read `safe.directory` instead of\nread_very_early_config(), making it 'protected configuration only'.\n\nAs a result, `safe.directory` now respects \"-c\", so update the tests and\ndocs accordingly. It used to ignore \"-c\" due to how it was implemented,\nnot because of security or correctness concerns [1].\n\n[1] https://lore.kernel.org/git/xmqqlevabcsu.fsf@gitster.g/\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config/safe.txt |  6 +++---\n setup.c                       |  2 +-\n t/t0033-safe-directory.sh     | 24 ++++++++++--------------\n 3 files changed, 14 insertions(+), 18 deletions(-)\n\ndiff --git a/Documentation/config/safe.txt b/Documentation/config/safe.txt\nindex fa02f3ccc54..f72b4408798 100644\n--- a/Documentation/config/safe.txt\n+++ b/Documentation/config/safe.txt\n@@ -12,9 +12,9 @@ via `git config --add`. To reset the list of safe directories (e.g. to\n override any such directories specified in the system config), add a\n `safe.directory` entry with an empty value.\n +\n-This config setting is only respected when specified in a system or global\n-config, not when it is specified in a repository config, via the command\n-line option `-c safe.directory=<path>`, or in environment variables.\n+This config setting is only respected in protected configuration (see\n+<<SCOPES>>). This prevents the untrusted repository from tampering with this\n+value.\n +\n The value of this setting is interpolated, i.e. `~/<path>` expands to a\n path relative to the home directory and `%(prefix)/<path>` expands to a\ndiff --git a/setup.c b/setup.c\nindex faf5095e44d..c8e3c32814d 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1137,7 +1137,7 @@ static int ensure_valid_ownership(const char *path)\n \t    is_path_owned_by_current_user(path))\n \t\treturn 1;\n \n-\tread_very_early_config(safe_directory_cb, &data);\n+\tgit_protected_config(safe_directory_cb, &data);\n \n \treturn data.is_safe;\n }\ndiff --git a/t/t0033-safe-directory.sh b/t/t0033-safe-directory.sh\nindex 238b25f91a3..5a1cd0d0947 100755\n--- a/t/t0033-safe-directory.sh\n+++ b/t/t0033-safe-directory.sh\n@@ -16,24 +16,20 @@ test_expect_success 'safe.directory is not set' '\n \texpect_rejected_dir\n '\n \n-test_expect_success 'ignoring safe.directory on the command line' '\n-\ttest_must_fail git -c safe.directory=\"$(pwd)\" status 2>err &&\n-\tgrep \"unsafe repository\" err\n+test_expect_success 'safe.directory on the command line' '\n+\tgit -c safe.directory=\"$(pwd)\" status\n '\n \n-test_expect_success 'ignoring safe.directory in the environment' '\n-\ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n-\t\tGIT_CONFIG_KEY_0=\"safe.directory\" \\\n-\t\tGIT_CONFIG_VALUE_0=\"$(pwd)\" \\\n-\t\tgit status 2>err &&\n-\tgrep \"unsafe repository\" err\n+test_expect_success 'safe.directory in the environment' '\n+\tenv GIT_CONFIG_COUNT=1 \\\n+\t    GIT_CONFIG_KEY_0=\"safe.directory\" \\\n+\t    GIT_CONFIG_VALUE_0=\"$(pwd)\" \\\n+\t    git status\n '\n \n-test_expect_success 'ignoring safe.directory in GIT_CONFIG_PARAMETERS' '\n-\ttest_must_fail env \\\n-\t\tGIT_CONFIG_PARAMETERS=\"${SQ}safe.directory${SQ}=${SQ}$(pwd)${SQ}\" \\\n-\t\tgit status 2>err &&\n-\tgrep \"unsafe repository\" err\n+test_expect_success 'safe.directory in GIT_CONFIG_PARAMETERS' '\n+\tenv GIT_CONFIG_PARAMETERS=\"${SQ}safe.directory${SQ}=${SQ}$(pwd)${SQ}\" \\\n+\t    git status\n '\n \n test_expect_success 'ignoring safe.directory in repo config' '\n-- \ngitgitgadget\n\n"},{"id":"458237","messageId":"a1323d963f917df661a8701c305d84e781a8f550.1656612839.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v6.git.git.1656612839.gitgitgadget@gmail.com","subject":"[PATCH v6 5/5] setup.c: create `discovery.bare`","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-06-30T18:13:59Z","receivedAt":"2022-06-30T18:14:37Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nThere is a known social engineering attack that takes advantage of the\nfact that a working tree can include an entire bare repository,\nincluding a config file. A user could run a Git command inside the bare\nrepository thinking that the config file of the 'outer' repository would\nbe used, but in reality, the bare repository's config file (which is\nattacker-controlled) is used, which may result in arbitrary code\nexecution. See [1] for a fuller description and deeper discussion.\n\nA simple mitigation is to forbid bare repositories unless specified via\n`--git-dir` or `GIT_DIR`. In environments that don't use bare\nrepositories, this would be minimally disruptive.\n\nCreate a config variable, `discovery.bare`, that tells Git whether or\nnot to die() when it discovers a bare repository. This only affects\nrepository discovery, thus it has no effect if discovery was not\ndone, e.g. if the user passes `--git-dir=my-dir`, discovery will be\nskipped and my-dir will be used as the repo regardless of the\n`discovery.bare` value.\n\nThis config is an enum of:\n\n- \"always\": always allow bare repositories (this is the default)\n- \"never\": never allow bare repositories\n\nIf we want to protect users from such attacks by default, neither value\nwill suffice - \"always\" provides no protection, but \"never\" is\nimpractical for bare repository users. A more usable default would be to\nallow only non-embedded bare repositories ([2] contains one such\nproposal), but detecting if a repository is embedded is potentially\nnon-trivial, so this work is not implemented in this series.\n\n[1]: https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n[2]: https://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config.txt           |  2 ++\n Documentation/config/discovery.txt | 23 ++++++++++++\n setup.c                            | 57 +++++++++++++++++++++++++++++-\n t/t0035-discovery-bare.sh          | 52 +++++++++++++++++++++++++++\n 4 files changed, 133 insertions(+), 1 deletion(-)\n create mode 100644 Documentation/config/discovery.txt\n create mode 100755 t/t0035-discovery-bare.sh\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex e284b042f22..9a5e1329772 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -409,6 +409,8 @@ include::config/diff.txt[]\n \n include::config/difftool.txt[]\n \n+include::config/discovery.txt[]\n+\n include::config/extensions.txt[]\n \n include::config/fastimport.txt[]\ndiff --git a/Documentation/config/discovery.txt b/Documentation/config/discovery.txt\nnew file mode 100644\nindex 00000000000..bbcf89bb0b5\n--- /dev/null\n+++ b/Documentation/config/discovery.txt\n@@ -0,0 +1,23 @@\n+discovery.bare::\n+\tSpecifies whether Git will work with a bare repository that it\n+\tfound during repository discovery. If the repository is\n+\tspecified directly via the --git-dir command-line option or the\n+\tGIT_DIR environment variable (see linkgit:git[1]), Git will\n+\talways use the specified repository, regardless of this value.\n++\n+This config setting is only respected in protected configuration (see\n+<<SCOPES>>). This prevents the untrusted repository from tampering with\n+this value.\n++\n+The currently supported values are:\n++\n+* `always`: Git always works with bare repositories\n+* `never`: Git never works with bare repositories\n++\n+This defaults to `always`, but this default may change in the future.\n++\n+If you do not use bare repositories in your workflow, then it may be\n+beneficial to set `discovery.bare` to `never` in your global config.\n+This will protect you from attacks that involve cloning a repository\n+that contains a bare repository and running a Git command within that\n+directory.\ndiff --git a/setup.c b/setup.c\nindex c8e3c32814d..16938fd5a24 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -10,6 +10,10 @@\n static int inside_git_dir = -1;\n static int inside_work_tree = -1;\n static int work_tree_config_is_bogus;\n+enum discovery_bare_allowed {\n+\tDISCOVERY_BARE_NEVER = 0,\n+\tDISCOVERY_BARE_ALWAYS,\n+};\n \n static struct startup_info the_startup_info;\n struct startup_info *startup_info = &the_startup_info;\n@@ -1142,6 +1146,46 @@ static int ensure_valid_ownership(const char *path)\n \treturn data.is_safe;\n }\n \n+static int discovery_bare_cb(const char *key, const char *value, void *d)\n+{\n+\tenum discovery_bare_allowed *discovery_bare_allowed = d;\n+\n+\tif (strcmp(key, \"discovery.bare\"))\n+\t\treturn 0;\n+\n+\tif (!strcmp(value, \"never\")) {\n+\t\t*discovery_bare_allowed = DISCOVERY_BARE_NEVER;\n+\t\treturn 0;\n+\t}\n+\tif (!strcmp(value, \"always\")) {\n+\t\t*discovery_bare_allowed = DISCOVERY_BARE_ALWAYS;\n+\t\treturn 0;\n+\t}\n+\treturn -1;\n+}\n+\n+static enum discovery_bare_allowed get_discovery_bare(void)\n+{\n+\tenum discovery_bare_allowed result = DISCOVERY_BARE_ALWAYS;\n+\tgit_protected_config(discovery_bare_cb, &result);\n+\treturn result;\n+}\n+\n+static const char *discovery_bare_allowed_to_string(\n+\tenum discovery_bare_allowed discovery_bare_allowed)\n+{\n+\tswitch (discovery_bare_allowed) {\n+\tcase DISCOVERY_BARE_NEVER:\n+\t\treturn \"never\";\n+\tcase DISCOVERY_BARE_ALWAYS:\n+\t\treturn \"always\";\n+\tdefault:\n+\t\tBUG(\"invalid discovery_bare_allowed %d\",\n+\t\t    discovery_bare_allowed);\n+\t}\n+\treturn NULL;\n+}\n+\n enum discovery_result {\n \tGIT_DIR_NONE = 0,\n \tGIT_DIR_EXPLICIT,\n@@ -1151,7 +1195,8 @@ enum discovery_result {\n \tGIT_DIR_HIT_CEILING = -1,\n \tGIT_DIR_HIT_MOUNT_POINT = -2,\n \tGIT_DIR_INVALID_GITFILE = -3,\n-\tGIT_DIR_INVALID_OWNERSHIP = -4\n+\tGIT_DIR_INVALID_OWNERSHIP = -4,\n+\tGIT_DIR_DISALLOWED_BARE = -5,\n };\n \n /*\n@@ -1248,6 +1293,8 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n \t\t}\n \n \t\tif (is_git_directory(dir->buf)) {\n+\t\t\tif (!get_discovery_bare())\n+\t\t\t\treturn GIT_DIR_DISALLOWED_BARE;\n \t\t\tif (!ensure_valid_ownership(dir->buf))\n \t\t\t\treturn GIT_DIR_INVALID_OWNERSHIP;\n \t\t\tstrbuf_addstr(gitdir, \".\");\n@@ -1394,6 +1441,14 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \t\t}\n \t\t*nongit_ok = 1;\n \t\tbreak;\n+\tcase GIT_DIR_DISALLOWED_BARE:\n+\t\tif (!nongit_ok) {\n+\t\t\tdie(_(\"cannot use bare repository '%s' (discovery.bare is '%s')\"),\n+\t\t\t    dir.buf,\n+\t\t\t    discovery_bare_allowed_to_string(get_discovery_bare()));\n+\t\t}\n+\t\t*nongit_ok = 1;\n+\t\tbreak;\n \tcase GIT_DIR_NONE:\n \t\t/*\n \t\t * As a safeguard against setup_git_directory_gently_1 returning\ndiff --git a/t/t0035-discovery-bare.sh b/t/t0035-discovery-bare.sh\nnew file mode 100755\nindex 00000000000..8f802746530\n--- /dev/null\n+++ b/t/t0035-discovery-bare.sh\n@@ -0,0 +1,52 @@\n+#!/bin/sh\n+\n+test_description='verify discovery.bare checks'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n+. ./test-lib.sh\n+\n+pwd=\"$(pwd)\"\n+\n+expect_accepted () {\n+\tgit \"$@\" rev-parse --git-dir\n+}\n+\n+expect_rejected () {\n+\ttest_must_fail git \"$@\" rev-parse --git-dir 2>err &&\n+\tgrep -F \"cannot use bare repository\" err\n+}\n+\n+test_expect_success 'setup bare repo in worktree' '\n+\tgit init outer-repo &&\n+\tgit init --bare outer-repo/bare-repo\n+'\n+\n+test_expect_success 'discovery.bare unset' '\n+\texpect_accepted -C outer-repo/bare-repo\n+'\n+\n+test_expect_success 'discovery.bare=always' '\n+\ttest_config_global discovery.bare always &&\n+\texpect_accepted -C outer-repo/bare-repo\n+'\n+\n+test_expect_success 'discovery.bare=never' '\n+\ttest_config_global discovery.bare never &&\n+\texpect_rejected -C outer-repo/bare-repo\n+'\n+\n+test_expect_success 'discovery.bare in the repository' '\n+\t# discovery.bare must not be \"never\", otherwise git config fails\n+\t# with \"fatal: not in a git directory\" (like safe.directory)\n+\ttest_config -C outer-repo/bare-repo discovery.bare always &&\n+\ttest_config_global discovery.bare never &&\n+\texpect_rejected -C outer-repo/bare-repo\n+'\n+\n+test_expect_success 'discovery.bare on the command line' '\n+\ttest_config_global discovery.bare never &&\n+\texpect_accepted -C outer-repo/bare-repo \\\n+\t\t-c discovery.bare=always\n+'\n+\n+test_done\n-- \ngitgitgadget\n"},{"id":"458285","messageId":"Yr4gIBBWR0Yl9krH@nand.local","threadId":"57859","inReplyTo":"pull.1261.v6.git.git.1656612839.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 0/5] config: introduce discovery.bare and protected config","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2022-06-30T22:13:52Z","receivedAt":"2022-06-30T22:14:01Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Thu, Jun 30, 2022 at 06:13:54PM +0000, Glen Choo via GitGitGadget wrote:\n> This series does not change the default behavior, but in the long-run, a\n> \"no-embedded\" option might be a safe and usable default [2]. \"never\" is too\n> restrictive and unlikely to be the default.\n\nThanks for this summary, and sorry for not taking a look at this series\nearlier. I spent quite a bit of time with v1 and the RFC patches but\nhaven't had as much time as I would have liked to devote to reviewing\nthe later rounds.\n\nI'm happy with this direction. I think \"never\" is an understandable\ndirection to go in for compliance reasons, or in deployments where bare\nrepositories are not expected. And I am glad that we are treating it as\nunlikely to be the default in the future, I agree that it is likely too\nrestrictive for that to make sense.\n\nSo leaving the default behavior unchanged, and providing a big hammer to\nprevent bare repository discovery entirely seems like a good first step\nfor this feature. I'm interested to see a potential future no-embedded\nimplementation, since I think that has a reasonable chance of being a\nsensible default, depending on how it's implemented.\n\n> For security reasons, discovery.bare cannot be read from repository-level\n> config (because we would end up trusting the embedded bare repository that\n> we aren't supposed to trust to begin with). Since this would introduce a 3rd\n> variable that is only read from 'protected/trusted configuration' (the\n> others are safe.directory and uploadpack.packObjectsHook) this series also\n> defines and creates a shared implementation for 'protected configuration'\n\nThis concept is new from the earlier rounds, so I'll be curious to see\nhow it plays out as I take a closer look at the patches.\n\nThanks again for your work on this complicated and tricky-to-get-right\nfeature ;-).\n\nThanks,\nTaylor\n"},{"id":"458287","messageId":"Yr4ka485NzadFTto@nand.local","threadId":"57859","inReplyTo":"ee9619f6ec0608f399fc924cfe9254df5e7bc431.1656612839.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 1/5] Documentation/git-config.txt: add SCOPES section","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2022-06-30T22:32:11Z","receivedAt":"2022-06-30T22:32:16Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Thu, Jun 30, 2022 at 06:13:55PM +0000, Glen Choo via GitGitGadget wrote:\n> From: Glen Choo <chooglen@google.com>\n>\n> In a subsequent commit, we will introduce \"protected configuration\",\n> which is easiest to describe in terms of configuration scopes (i.e. it's\n> the union of the 'system', 'global', and 'command' scopes). This\n> description is fine for ML discussions, but it's inadequate for end\n> users because we don't provide a good description of \"configuration\n> scopes\" in the public docs.\n>\n> 145d59f482 (config: add '--show-scope' to print the scope of a config\n> value, 2020-02-10) introduced the word \"scope\" to our public docs, but\n> that only enumerates the scopes and assumes the user can figure out\n> those values mean.\n\nThanks, I think that \"scope\" is an appropriate term here. When I\noriginally read this patch, I was thinking that \"origin\" would be more\nappropriate, since I was recalling the `--show-origin` option to `git\nconfig`. But that shows the file name, and `--show-scope` is a separate\noption entirely.\n\nThe latter is definitely more appropriate here, so I think this choice\nof naming is good and makes sense.\n\n> diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\n> index 9376e39aef2..f93d437b898 100644\n> --- a/Documentation/git-config.txt\n> +++ b/Documentation/git-config.txt\n> @@ -297,8 +297,8 @@ The default is to use a pager.\n>  FILES\n>  -----\n>\n> -If not set explicitly with `--file`, there are four files where\n> -'git config' will search for configuration options:\n> +By default, 'git config' will read configuration options from multiple\n> +files:\n>\n>  $(prefix)/etc/gitconfig::\n>  \tSystem-wide configuration file.\n> @@ -322,27 +322,63 @@ $GIT_DIR/config.worktree::\n>  \tThis is optional and is only searched when\n>  \t`extensions.worktreeConfig` is present in $GIT_DIR/config.\n>\n> -If no further options are given, all reading options will read all of these\n> -files that are available. If the global or the system-wide configuration\n> -file are not available they will be ignored. If the repository configuration\n> -file is not available or readable, 'git config' will exit with a non-zero\n> -error code. However, in neither case will an error message be issued.\n> +You may also provide additional configuration parameters when running any\n> +git command by using the `-c` option. See linkgit:git[1] for details.\n> +\n> +Options will be read from all of these files that are available. If the\n> +global or the system-wide configuration file are not available they will be\n> +ignored. If the repository configuration file is not available or readable,\n> +'git config' will exit with a non-zero error code. However, in neither case\n> +will an error message be issued.\n\nNit: the last sentence is a little awkwardly worded. Perhaps just:\n\"Note that neither case produces an error message\".\n\n> -All writing options will per default write to the repository specific\n> +By default, options are only written to the repository specific\n>  configuration file. Note that this also affects options like `--replace-all`\n\nShould we mention that this is the same as the \"local\" scope below?\n\n>  and `--unset`. *'git config' will only ever change one file at a time*.\n>\n> -You can override these rules using the `--global`, `--system`,\n> -`--local`, `--worktree`, and `--file` command-line options; see\n> -<<OPTIONS>> above.\n> +You can change the way options are read/written by specifying the path to a\n> +file (`--file`), or by specifying a configuration scope (`--system`,\n> +`--global`, `--local`, `--worktree`); see <<OPTIONS>> above.\n\nI think this paragraph could be slightly more descriptive about what\n`--file` does while still linking out to <<OPTIONS>> above for more\ndetailed information. In the pre-image, we say:\n\n    If not set explicitly with `--file`, there are four files will `git\n    config will search`.\n\nSo I wonder if something more descriptive in this section might be:\n\n    You can limit which configuration sources are read to or written\n    from by specifying the path of a file with the `--file` option, or\n    by specifying a scope with `--system`, `--global`, `--local`, or\n    `--worktree`. For more, see <<OPTIONS>> above.\n\nI don't think that's so different form what you wrote, but I think it's\na little clearer particularly what `--file` does (instead of \"change the\nway options are read/written\" it \"limit[s] which configuration sources\nare read to or written from\").\n\n> +\n> +SCOPES\n> +------\n> +\n> +Each configuration source falls within a configuration scope. The scopes\n> +are:\n> +\n> +system::\n> +\t$(prefix)/etc/gitconfig\n> +\n> +global::\n> +\t$XDG_CONFIG_HOME/git/config\n> ++\n> +~/.gitconfig\n> +\n> +local::\n> +\t$GIT_DIR/config\n> +\n> +worktree::\n> +\t$GIT_DIR/config.worktree\n> +\n> +command::\n> +\tenvironment variables\n> ++\n> +the `-c` option\n> +\n> +With the exception of 'command', each scope corresponds to a command line\n> +option - `--system`, `--global`, `--local`, `--worktree`.\n\nI think a colon after \"option\" is more appropriate than a single \"-\"\ndash character, but this is definitely a trivial matter that I have no\nstrong opinion on.\n\nOne thing that this reminds me of (which I don't think is worth taking\nup here, but perhaps in a future series, or as #leftoverbits) would be\npromoting these scopes behind a single option. Back in the day, you\ncould ask for values out of `git config` by specifying their type with\n`--int`, `--bool`, or similar. In e3e042b185 (Merge branch\n'tb/config-type', 2018-05-08), we changed to\n`--type=<int|bool|color|etc>`, which unified things and made it clearer\nwhich options were grouped together by a single concept.\n\nI think a similar change would make sense here, that is to replace\n`--system`, `--global` (and so on) with `--scope=system`,\n`--scope=global`, etc.\n\nBut that's not material to this series, and just something to think\nabout for later on if you end up thinking it's a good idea.\n\n> +\n> +When reading options, specifying a scope will only read options from the\n> +files within that scope. When writing options, specifying a scope will write\n> +to the files within that scope (instead of the repository specific\n> +configuration file). See <<OPTIONS>> above for a complete description.\n>\n> +Most configuration options are respected regardless of the scope it is\n> +defined in, but some options are only respected in certain scopes. See the\n> +option's documentation for the full details.\n\nI assume \"the option's\" is referring to whichever configuration variable\nwe're talking about. So it may be clearer to say \"See the *respective*\noption's documentation for more information\" or similar.\n\nThanks,\nTaylor\n"},{"id":"458293","messageId":"220701.861qv5d8v7.gmgdl@evledraar.gmail.com","threadId":"57859","inReplyTo":"pull.1261.v6.git.git.1656612839.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 0/5] config: introduce discovery.bare and protected config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-06-30T23:07:54Z","receivedAt":"2022-06-30T23:33:57Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Jun 30 2022, Glen Choo via GitGitGadget wrote:\n\n> This is a quick re-roll to address Ævar's comments on the tests (thanks!).\n\nThanks!\n\n> = Description\n\nJust more generally on this series & approach. I know this is a v6 by\nnow, but I haven't kept up with this topic, but to be fair I did mention\npretty much this in:\nhttps://lore.kernel.org/git/220407.86lewhc6bz.gmgdl@evledraar.gmail.com/\n\nSo...\n\n> There is a known social engineering attack that takes advantage of the fact\n> that a working tree can include an entire bare repository, including a\n> config file. A user could run a Git command inside the bare repository\n> thinking that the config file of the 'outer' repository would be used, but\n> in reality, the bare repository's config file (which is attacker-controlled)\n> is used, which may result in arbitrary code execution. See [1] for a fuller\n> description and deeper discussion.\n>\n> This series implements a simple way of preventing such attacks: create a\n> config option, discovery.bare, that tells Git whether or not to die when it\n> finds a bare repository. discovery.bare has two values:\n>\n>  * \"always\": always allow bare repositories (default), identical to current\n>    behavior\n>  * \"never\": never allow bare repositories\n>\n> and users/system administrators who never expect to work with bare\n> repositories can secure their environments using \"never\". discovery.bare has\n> no effect if --git-dir or GIT_DIR is passed because we are confident that\n> the user is not confused about which repository is being used.\n\nI'm not insisting that the entire approach here should be changed, but\nin the above exchange you seemed to have performance concerns about the\n\"just walk up in setup.c\" approach I mentioned, but it's not clear if\nthat's still the only thing that necessitates taking this approach.\n\nThere may be security subtleties that I've missed, but from the\ndescription here it seems like that would work equally well, and\nwouldn't require configuration, except insofar as we'd need to opt-in to\nreading config from bare repositores *that also exist in a parent tree*.\n\nAnd it would be a more narrow & more secure solution, since it would\ne.g. allow you to intentionally navigate to /var/repos/git/git.git in a\nserver setup and read the config there, which it could distinguish from\na case of /var/repos/.git existing, and git/git.git being brought in as\na part of that \"parent\" repo.\n\nThe \"more narrow\" and \"more secure\" go hand-in-hand, since if you work\non such servers you'd turn this to \"always\" because you want to read\nsuch config, but then be left vulnerable to the actual (and muche rarer)\nexploit we're trying to prevent.\n\nWhich, it seems...\n\n> This series does not change the default behavior, but in the long-run, a\n> \"no-embedded\" option might be a safe and usable default [2]. \"never\" is too\n> restrictive and unlikely to be the default.\n\nThis series has (since v3?) been noting aspirations to have a\n\"no-embedded\" variant of this config, which your 5/5 here notes would be\nbetter, but isn't implemented by this series.\n\nBut your 5/5 also notes:\n\n    but detecting if a repository is embedded is potentially\n    non-trivial, so this work is not implemented in this series.\n\nHrm, well, the diff-stat isn't quite that trivial either :) :\n\n> [...]\n>  upload-pack.c                       | 27 ++++++----\n>  12 files changed, 304 insertions(+), 47 deletions(-)\n\nIn threads linked from the above ML link I linked to some POC code\nshowing how to hack a second .git discovery walk into setup.c. This was\nas part of the \"submodule parent dir\" proposal, which is a different\nfeature, but also needs such \"find the parent\" code:\nhttps://lore.kernel.org/git/211109.86v912dtfw.gmgdl@evledraar.gmail.com/\n\nNow, obviously that's a dirty hack, but it's not that hard to just\nchange the part of setup.c where we're satisfied that we've found the\ngit dir, then walk up \"$THAT_DIR/..\", and start our search again.\n\nThen:\n\n\tif (first_dir_was_bare() && found_parent_dir())\n        \tenforce_no_embedded();\n\nIsn't that what your proposed \"no embedded\" option would need to do?\nWell, maybe we'd also check if the \"first dir\" is in the index of the\nparent, as opposed to just being a bare .git somewhere in ~/Downloads,\ne.g. if you have a ~/.git and keep your dot-files in git.\n\nBut I think for an initial implementation just doing the walk would be\ngood enough, and would have a more narrow scope than this configuration\nsetting.\n\nAFAICT the performance concerns aren't supported by any data, in the\ncase of the \"submodule superproject\" feature it turned out to not be the\ndirectory walk, but us shelling out in a loop in git-submodule.sh.\n\nWell, *maybe* that's not the case, I think I have managed to read\nbetween the lines of some of these past exchanges that there's some odd\npropriterary internal NFS-like setup at Google where *parent dirs* are\nauto-mounted and searched on access, so a \"walk up\" pattern would be\nmuch more expensive.\n\nI do worry a bit about us ending up with design choices in git that we\nwouldn't have ended up with, if not to cater to some in-house setup\nsomwhere that 99.99% of git users will never see.\n\nBut I don't have the full picture on the \"submodule superproject\"\nproblem, or this one, and maybe I'm missing something. Just food for\nthought, and wondering where we're eventually taking this.\n\nThanks!\n\n"},{"id":"458309","messageId":"Yr42b+MYsuw8ihgG@nand.local","threadId":"57859","inReplyTo":"43627c05c0b997ea407c865b04994cba630297d6.1656612839.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 2/5] Documentation: define protected configuration","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2022-06-30T23:49:03Z","receivedAt":"2022-06-30T23:49:59Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Thu, Jun 30, 2022 at 06:13:56PM +0000, Glen Choo via GitGitGadget wrote:\n> @@ -380,6 +381,18 @@ Most configuration options are respected regardless of the scope it is\n>  defined in, but some options are only respected in certain scopes. See the\n>  option's documentation for the full details.\n>\n> +Protected configuration\n> +~~~~~~~~~~~~~~~~~~~~~~~\n> +\n> +Protected configuration refers to the 'system', 'global', and 'command' scopes.\n> +For security reasons, certain options are only respected when they are\n> +specified in protected configuration, and ignored otherwise.\n> +\n> +Git treats these scopes as if they are controlled by the user or a trusted\n> +administrator. This is because an attacker who controls these scopes can do\n> +substantial harm without using Git, so it is assumed that the user's environment\n> +protects these scopes against attackers.\n> +\n\nI think this description is a good starting point, but I think I would\nhave liked to see some more from the commit description make it into the\ndocumentation here.\n\nOne thing that I didn't see mentioned in either is that the list of\nprotected configuration is far from exhaustive. There are dozens upon\ndozens of configuration values that Git will happily execute as a\nsubprocess (core.editor, core.pager, core.alternateRefsCommand, to name\njust a few).\n\nI don't think we should try and enumerate every possible path from\nconfiguration to command execution. But it is worth noting in the\ndocumentation that the list of configuration values which are only read\nin the protected context is non-exhaustive and best-effort only.\n\nThanks,\nTaylor\n"},{"id":"458312","messageId":"Yr5MZhgTBFgZh/XB@nand.local","threadId":"57859","inReplyTo":"3efe282e6b94c3daed77590c5f601fad34137c9c.1656612839.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 3/5] config: learn `git_protected_config()`","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2022-07-01T01:22:46Z","receivedAt":"2022-07-01T01:22:52Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Thu, Jun 30, 2022 at 06:13:57PM +0000, Glen Choo via GitGitGadget wrote:\n> In light of constraint 1, this implementation can still be improved\n> since git_protected_config() iterates through every variable in\n> protected_config, which may still be too expensive. There exist constant\n> time lookup functions for non-protected configuration\n> (repo_config_get_*()), but for simplicity, this commit does not\n> implement similar functions for protected configuration.\n\nI don't quite follow along with this paragraph: it sounds like reading\nprotected configuration is supposed to be as fast as possible. But you\nnote that only the slower variant of reading each configuration variable\none at a time is implemented.\n\nIf we care about speed (and I think we should here), then would it make\nmore sense to implement only the lookup functions like\nrepo_config_get_*() for protected context? That would encourage usage by\nproviding a more limited set of options to callers.\n\n> Signed-off-by: Glen Choo <chooglen@google.com>\n> ---\n>  config.c                     | 51 ++++++++++++++++++++++++++++++++++++\n>  config.h                     | 17 ++++++++++++\n>  t/t5544-pack-objects-hook.sh |  7 ++++-\n>  upload-pack.c                | 27 ++++++++++++-------\n>  4 files changed, 91 insertions(+), 11 deletions(-)\n>\n> diff --git a/config.c b/config.c\n> index 9b0e9c93285..29e62f5d0ed 100644\n> --- a/config.c\n> +++ b/config.c\n> @@ -81,6 +81,18 @@ static enum config_scope current_parsing_scope;\n>  static int pack_compression_seen;\n>  static int zlib_compression_seen;\n>\n> +/*\n> + * Config that comes from trusted sources, namely:\n\nShould we be using the word \"scope\" here instead of sources? I think\nit's clear enough from the context what you're referring to, but in the\nspirit of being consistent...\n\n> + * - system config files (e.g. /etc/gitconfig)\n> + * - global config files (e.g. $HOME/.gitconfig,\n> + *   $XDG_CONFIG_HOME/git)\n> + * - the command line.\n> + *\n> + * This is declared here for code cleanliness, but unlike the other\n> + * static variables, this does not hold config parser state.\n> + */\n> +static struct config_set protected_config;\n> +\n>  static int config_file_fgetc(struct config_source *conf)\n>  {\n>  \treturn getc_unlocked(conf->u.file);\n> @@ -2378,6 +2390,11 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n>  \treturn git_config_from_file(config_set_callback, filename, cs);\n>  }\n>\n> +int git_configset_add_parameters(struct config_set *cs)\n> +{\n> +\treturn git_config_from_parameters(config_set_callback, cs);\n> +}\n> +\n>  int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n>  {\n>  \tconst struct string_list *values = NULL;\n> @@ -2619,6 +2636,40 @@ int repo_config_get_pathname(struct repository *repo,\n>  \treturn ret;\n>  }\n>\n> +/* Read values into protected_config. */\n> +static void read_protected_config(void)\n> +{\n> +\tchar *xdg_config = NULL, *user_config = NULL, *system_config = NULL;\n> +\n> +\tgit_configset_init(&protected_config);\n> +\n> +\tsystem_config = git_system_config();\n> +\tgit_global_config(&user_config, &xdg_config);\n> +\n> +\tgit_configset_add_file(&protected_config, system_config);\n> +\tgit_configset_add_file(&protected_config, xdg_config);\n> +\tgit_configset_add_file(&protected_config, user_config);\n> +\tgit_configset_add_parameters(&protected_config);\n> +\n> +\tfree(system_config);\n> +\tfree(xdg_config);\n> +\tfree(user_config);\n> +}\n> +\n> +/* Ensure that protected_config has been initialized. */\n> +static void git_protected_config_check_init(void)\n> +{\n> +\tif (protected_config.hash_initialized)\n> +\t\treturn;\n> +\tread_protected_config();\n> +}\n> +\n> +void git_protected_config(config_fn_t fn, void *data)\n> +{\n> +\tgit_protected_config_check_init();\n\nThis may be copying from an existing pattern, but I think you could\navoid the extra function declaration by writing git_protected_config()\nas:\n\n    if (!protected_config.hash_initialized)\n        read_protected_config();\n    configset_iter(&protected_config, fn, data);\n\nThanks,\nTaylor\n"},{"id":"458313","messageId":"Yr5OTq7s2qxxqsiY@nand.local","threadId":"57859","inReplyTo":"a1323d963f917df661a8701c305d84e781a8f550.1656612839.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 5/5] setup.c: create `discovery.bare`","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2022-07-01T01:30:54Z","receivedAt":"2022-07-01T01:30:59Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Thu, Jun 30, 2022 at 06:13:59PM +0000, Glen Choo via GitGitGadget wrote:\n> If we want to protect users from such attacks by default, neither value\n> will suffice - \"always\" provides no protection, but \"never\" is\n> impractical for bare repository users. A more usable default would be to\n> allow only non-embedded bare repositories ([2] contains one such\n> proposal), but detecting if a repository is embedded is potentially\n> non-trivial, so this work is not implemented in this series.\n\nI think that everything you said in your patch message makes sense, but\nI appreciate this paragraph in particular. The historical record is\ndefinitely important and worth preserving here, and I hope that it'll be\nhelpful to future readers who may wonder why the default wasn't chosen\nas \"never\".\n\n> [1]: https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n> [2]: https://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n>\n> Signed-off-by: Glen Choo <chooglen@google.com>\n> ---\n>  Documentation/config.txt           |  2 ++\n>  Documentation/config/discovery.txt | 23 ++++++++++++\n>  setup.c                            | 57 +++++++++++++++++++++++++++++-\n>  t/t0035-discovery-bare.sh          | 52 +++++++++++++++++++++++++++\n>  4 files changed, 133 insertions(+), 1 deletion(-)\n>  create mode 100644 Documentation/config/discovery.txt\n>  create mode 100755 t/t0035-discovery-bare.sh\n>\n> diff --git a/Documentation/config.txt b/Documentation/config.txt\n> index e284b042f22..9a5e1329772 100644\n> --- a/Documentation/config.txt\n> +++ b/Documentation/config.txt\n> @@ -409,6 +409,8 @@ include::config/diff.txt[]\n>\n>  include::config/difftool.txt[]\n>\n> +include::config/discovery.txt[]\n> +\n>  include::config/extensions.txt[]\n>\n>  include::config/fastimport.txt[]\n> diff --git a/Documentation/config/discovery.txt b/Documentation/config/discovery.txt\n> new file mode 100644\n> index 00000000000..bbcf89bb0b5\n> --- /dev/null\n> +++ b/Documentation/config/discovery.txt\n> @@ -0,0 +1,23 @@\n> +discovery.bare::\n> +\tSpecifies whether Git will work with a bare repository that it\n> +\tfound during repository discovery. If the repository is\n\nIs it clear from the context what \"discovery\" means here? It's probably\neasier to describe what it isn't, which you kind of do in the next\nsentence. But it may be clearer to say something like:\n\n    Specifies whether Git will recognize bare repositories that aren't\n    specified via the top-level `--git-dir` command-line option, or the\n    `GIT_DIR` environment variable (see linkgit:git[1]).\n\n> +This defaults to `always`, but this default may change in the future.\n\nI think the default being subject to change is par for the course. It's\nprobably easy enough to just say \"Defaults to 'always'\" and leave it at\nthat.\n\n> ++\n> +If you do not use bare repositories in your workflow, then it may be\n> +beneficial to set `discovery.bare` to `never` in your global config.\n> +This will protect you from attacks that involve cloning a repository\n> +that contains a bare repository and running a Git command within that\n> +directory.\n\nI think we still don't have a great answer for people who trust some\nbare repositories (e.g., known-embedded repositories that are used for\ntesting) but not others. To be clear, I think that is a fine point to\nconcede with this direction.\n\nBut we should be clear about that limitation by stating that Git does\nnot support the \"I trust some bare repositories to be safely\ndiscoverable but not others\".\n\n> +static enum discovery_bare_allowed get_discovery_bare(void)\n> +{\n> +\tenum discovery_bare_allowed result = DISCOVERY_BARE_ALWAYS;\n> +\tgit_protected_config(discovery_bare_cb, &result);\n> +\treturn result;\n> +}\n> +\n> +static const char *discovery_bare_allowed_to_string(\n> +\tenum discovery_bare_allowed discovery_bare_allowed)\n> +{\n> +\tswitch (discovery_bare_allowed) {\n> +\tcase DISCOVERY_BARE_NEVER:\n> +\t\treturn \"never\";\n> +\tcase DISCOVERY_BARE_ALWAYS:\n> +\t\treturn \"always\";\n\n> +\tdefault:\n> +\t\tBUG(\"invalid discovery_bare_allowed %d\",\n> +\t\t    discovery_bare_allowed);\n\nShould we have a default case here since the case arms above are\nexhaustive?\n\n> +\t}\n> +\treturn NULL;\n> +}\n> +\n>  enum discovery_result {\n>  \tGIT_DIR_NONE = 0,\n>  \tGIT_DIR_EXPLICIT,\n> @@ -1151,7 +1195,8 @@ enum discovery_result {\n>  \tGIT_DIR_HIT_CEILING = -1,\n>  \tGIT_DIR_HIT_MOUNT_POINT = -2,\n>  \tGIT_DIR_INVALID_GITFILE = -3,\n> -\tGIT_DIR_INVALID_OWNERSHIP = -4\n> +\tGIT_DIR_INVALID_OWNERSHIP = -4,\n> +\tGIT_DIR_DISALLOWED_BARE = -5,\n>  };\n>\n>  /*\n> @@ -1248,6 +1293,8 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n>  \t\t}\n>\n>  \t\tif (is_git_directory(dir->buf)) {\n> +\t\t\tif (!get_discovery_bare())\n\nRelying on NEVER being the zero value here seems fragile to me. Should\nwe check that `if (get_discovery_bare() == DISCOVERY_BARE_NEVER)` to be\nmore explicit here?\n\n> +\t\t\t\treturn GIT_DIR_DISALLOWED_BARE;\n>  \t\t\tif (!ensure_valid_ownership(dir->buf))\n>  \t\t\t\treturn GIT_DIR_INVALID_OWNERSHIP;\n>  \t\t\tstrbuf_addstr(gitdir, \".\");\n> @@ -1394,6 +1441,14 @@ const char *setup_git_directory_gently(int *nongit_ok)\n>  \t\t}\n>  \t\t*nongit_ok = 1;\n>  \t\tbreak;\n> +\tcase GIT_DIR_DISALLOWED_BARE:\n> +\t\tif (!nongit_ok) {\n> +\t\t\tdie(_(\"cannot use bare repository '%s' (discovery.bare is '%s')\"),\n> +\t\t\t    dir.buf,\n> +\t\t\t    discovery_bare_allowed_to_string(get_discovery_bare()));\n> +\t\t}\n> +\t\t*nongit_ok = 1;\n> +\t\tbreak;\n>  \tcase GIT_DIR_NONE:\n>  \t\t/*\n>  \t\t * As a safeguard against setup_git_directory_gently_1 returning\n\nThanks,\nTaylor\n"},{"id":"458394","messageId":"kl6lr134spi0.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"220701.861qv5d8v7.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v6 0/5] config: introduce discovery.bare and protected config","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-07-01T17:37:43Z","receivedAt":"2022-07-01T17:37:48Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Thanks for weighing in :) Despite the different proposed approaches, I\nthink we actually are in broad agreement.\n\nÆvar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> On Thu, Jun 30 2022, Glen Choo via GitGitGadget wrote:\n>\n>> This is a quick re-roll to address Ævar's comments on the tests (thanks!).\n>\n> Thanks!\n>\n>> = Description\n>\n> Just more generally on this series & approach. I know this is a v6 by\n> now, but I haven't kept up with this topic, but to be fair I did mention\n> pretty much this in:\n> https://lore.kernel.org/git/220407.86lewhc6bz.gmgdl@evledraar.gmail.com/\n>\n> So...\n>\n>> There is a known social engineering attack that takes advantage of the fact\n>> that a working tree can include an entire bare repository, including a\n>> config file. A user could run a Git command inside the bare repository\n>> thinking that the config file of the 'outer' repository would be used, but\n>> in reality, the bare repository's config file (which is attacker-controlled)\n>> is used, which may result in arbitrary code execution. See [1] for a fuller\n>> description and deeper discussion.\n>>\n>> This series implements a simple way of preventing such attacks: create a\n>> config option, discovery.bare, that tells Git whether or not to die when it\n>> finds a bare repository. discovery.bare has two values:\n>>\n>>  * \"always\": always allow bare repositories (default), identical to current\n>>    behavior\n>>  * \"never\": never allow bare repositories\n>>\n>> and users/system administrators who never expect to work with bare\n>> repositories can secure their environments using \"never\". discovery.bare has\n>> no effect if --git-dir or GIT_DIR is passed because we are confident that\n>> the user is not confused about which repository is being used.\n>\n> I'm not insisting that the entire approach here should be changed, but\n> in the above exchange you seemed to have performance concerns about the\n> \"just walk up in setup.c\" approach I mentioned, but it's not clear if\n> that's still the only thing that necessitates taking this approach.\n>\n> There may be security subtleties that I've missed, but from the\n> description here it seems like that would work equally well, and\n> wouldn't require configuration, except insofar as we'd need to opt-in to\n> reading config from bare repositores *that also exist in a parent tree*.\n>\n> And it would be a more narrow & more secure solution, since it would\n> e.g. allow you to intentionally navigate to /var/repos/git/git.git in a\n> server setup and read the config there, which it could distinguish from\n> a case of /var/repos/.git existing, and git/git.git being brought in as\n> a part of that \"parent\" repo.\n\nPerformance is one major concern, yes, and I agree that your findings\nshow that the \"just walk up\" approach is cheap enough to consider doing.\nThough in the few cases where it isn't cheap to walk, wouldn't it still\nbe useful to be able to opt out of it?\n\nThe other concern is simplicity and correctness. Are we confident that\nwe'll get the design of \"just walk up\" correct (including edge cases\nlike \"bare repo in bare repo in non bare repo\")? I'm 100% confident that\nwe'll get it right eventually, and that this approach will be a good\ndefault for all users. But in comparison, \"never\" is so much easier to\nunderstand and implement that I don't see why we shouldn't start by\npresenting this option to the 0.1-1% of users who would find it useful.\n\nAnd on the topic of simplicity, there's significant interest in\nmaintaining backwards-compatibility with repos with workflows that\nabsolutely depend on embedded bare repos, e.g. libgit2 and Git-LFS.\nThat's yet another special case that we'd have to get right. Stolee's\n\"no-embedded\" proposal [1] pretty much covers that, but I don't see the\nharm in simplifying the design space by making bare repo support a\nnon-goal.\n\n[1] https://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n\n> The \"more narrow\" and \"more secure\" go hand-in-hand, since if you work\n> on such servers you'd turn this to \"always\" because you want to read\n> such config, but then be left vulnerable to the actual (and muche rarer)\n> exploit we're trying to prevent.\n\nThe point that we're not defending bare repo users is fair, but maybe\nthe group we're trying to protect isn't really dedicated Git-serving\nservers. This exploit requires you to have a bare repo inside the\nworking tree of a non-bare repo. So I think this is less of an issue for\na server, and more for \"mixed-use\" environments with both regular and\nbare clones.\n\n> Which, it seems...\n>\n>> This series does not change the default behavior, but in the long-run, a\n>> \"no-embedded\" option might be a safe and usable default [2]. \"never\" is too\n>> restrictive and unlikely to be the default.\n>\n> This series has (since v3?) been noting aspirations to have a\n> \"no-embedded\" variant of this config, which your 5/5 here notes would be\n> better, but isn't implemented by this series.\n>\n> But your 5/5 also notes:\n>\n>     but detecting if a repository is embedded is potentially\n>     non-trivial, so this work is not implemented in this series.\n>\n> Hrm, well, the diff-stat isn't quite that trivial either :) :\n\nWell.. a lot of it is refactoring :P\n\n>> [...]\n>>  upload-pack.c                       | 27 ++++++----\n>>  12 files changed, 304 insertions(+), 47 deletions(-)\n>\n> In threads linked from the above ML link I linked to some POC code\n> showing how to hack a second .git discovery walk into setup.c. This was\n> as part of the \"submodule parent dir\" proposal, which is a different\n> feature, but also needs such \"find the parent\" code:\n> https://lore.kernel.org/git/211109.86v912dtfw.gmgdl@evledraar.gmail.com/\n>\n> Now, obviously that's a dirty hack, but it's not that hard to just\n> change the part of setup.c where we're satisfied that we've found the\n> git dir, then walk up \"$THAT_DIR/..\", and start our search again.\n>\n> Then:\n>\n> \tif (first_dir_was_bare() && found_parent_dir())\n>         \tenforce_no_embedded();\n>\n> Isn't that what your proposed \"no embedded\" option would need to do?\n> Well, maybe we'd also check if the \"first dir\" is in the index of the\n> parent, as opposed to just being a bare .git somewhere in ~/Downloads,\n> e.g. if you have a ~/.git and keep your dot-files in git.\n>\n> But I think for an initial implementation just doing the walk would be\n> good enough, and would have a more narrow scope than this configuration\n> setting.\n\nA narrow scope is good, but I don't agree on this definition of\n\"narrow\". My preference is to give an obvious solution to a 'narrow'\ngroup of users, instead of a more tricky solution that affects all users\nin a 'narrow' set of cases.\n\n> AFAICT the performance concerns aren't supported by any data, in the\n> case of the \"submodule superproject\" feature it turned out to not be the\n> directory walk, but us shelling out in a loop in git-submodule.sh.\n>\n> Well, *maybe* that's not the case, I think I have managed to read\n> between the lines of some of these past exchanges that there's some odd\n> propriterary internal NFS-like setup at Google where *parent dirs* are\n> auto-mounted and searched on access, so a \"walk up\" pattern would be\n> much more expensive.\n>\n> I do worry a bit about us ending up with design choices in git that we\n> wouldn't have ended up with, if not to cater to some in-house setup\n> somwhere that 99.99% of git users will never see.\n\nAt the very least, I don't think you're saying that it's a bad idea to\nhave \"never\", just that we might not have come up with it if not for\nsome Google NFS thing.\n\nAnother use case I can think of is CI bots, which have no need for bare\nrepos. To some folks (maybe in very security-sensitive environments),\n\"never\" might give more peace of mind than \"no-embedded\".\n\n> But I don't have the full picture on the \"submodule superproject\"\n> problem, or this one, and maybe I'm missing something. Just food for\n> thought, and wondering where we're eventually taking this.\n>\n> Thanks!\n"},{"id":"458532","messageId":"kl6llet6rv9w.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"Yr4ka485NzadFTto@nand.local","subject":"Re: [PATCH v6 1/5] Documentation/git-config.txt: add SCOPES section","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-07-06T17:44:11Z","receivedAt":"2022-07-06T17:45:17Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"\nHi! Thanks so much for lending your attention to this version again, I\nreally appreciate this wording feedback in particular, because the\nReview Club reviewers and I agonized a lot over the wording and couldn't\ncome up with great alternatives to what I wrote in the patch, and your\nsuggestions are super helpful.\n\nTaylor Blau <me@ttaylorr.com> writes:\n\n> On Thu, Jun 30, 2022 at 06:13:55PM +0000, Glen Choo via GitGitGadget wrote:\n>> From: Glen Choo <chooglen@google.com>\n>> diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\n>> index 9376e39aef2..f93d437b898 100644\n>> --- a/Documentation/git-config.txt\n>> +++ b/Documentation/git-config.txt\n>> @@ -297,8 +297,8 @@ The default is to use a pager.\n>>  FILES\n>>  -----\n>>\n>> -If not set explicitly with `--file`, there are four files where\n>> -'git config' will search for configuration options:\n>> +By default, 'git config' will read configuration options from multiple\n>> +files:\n>>\n>>  $(prefix)/etc/gitconfig::\n>>  \tSystem-wide configuration file.\n>> @@ -322,27 +322,63 @@ $GIT_DIR/config.worktree::\n>>  \tThis is optional and is only searched when\n>>  \t`extensions.worktreeConfig` is present in $GIT_DIR/config.\n>>\n>> -If no further options are given, all reading options will read all of these\n>> -files that are available. If the global or the system-wide configuration\n>> -file are not available they will be ignored. If the repository configuration\n>> -file is not available or readable, 'git config' will exit with a non-zero\n>> -error code. However, in neither case will an error message be issued.\n>> +You may also provide additional configuration parameters when running any\n>> +git command by using the `-c` option. See linkgit:git[1] for details.\n>> +\n>> +Options will be read from all of these files that are available. If the\n>> +global or the system-wide configuration file are not available they will be\n>> +ignored. If the repository configuration file is not available or readable,\n>> +'git config' will exit with a non-zero error code. However, in neither case\n>> +will an error message be issued.\n>\n> Nit: the last sentence is a little awkwardly worded. Perhaps just:\n> \"Note that neither case produces an error message\".\n\nGood suggestion. I didn't change this sentence, but I agree that it's\nworth improving.\n\n>> -All writing options will per default write to the repository specific\n>> +By default, options are only written to the repository specific\n>>  configuration file. Note that this also affects options like `--replace-all`\n>\n> Should we mention that this is the same as the \"local\" scope below?\n\nAlso a good idea.\n\n>>  and `--unset`. *'git config' will only ever change one file at a time*.\n>>\n>> -You can override these rules using the `--global`, `--system`,\n>> -`--local`, `--worktree`, and `--file` command-line options; see\n>> -<<OPTIONS>> above.\n>> +You can change the way options are read/written by specifying the path to a\n>> +file (`--file`), or by specifying a configuration scope (`--system`,\n>> +`--global`, `--local`, `--worktree`); see <<OPTIONS>> above.\n>\n> I think this paragraph could be slightly more descriptive about what\n> `--file` does while still linking out to <<OPTIONS>> above for more\n> detailed information. In the pre-image, we say:\n>\n>     If not set explicitly with `--file`, there are four files will `git\n>     config will search`.\n>\n> So I wonder if something more descriptive in this section might be:\n>\n>     You can limit which configuration sources are read to or written\n>     from by specifying the path of a file with the `--file` option, or\n>     by specifying a scope with `--system`, `--global`, `--local`, or\n>     `--worktree`. For more, see <<OPTIONS>> above.\n>\n> I don't think that's so different form what you wrote, but I think it's\n> a little clearer particularly what `--file` does (instead of \"change the\n> way options are read/written\" it \"limit[s] which configuration sources\n> are read to or written from\").\n\nI think this is _much_ clearer, actually. Thanks!\n\n>> +\n>> +SCOPES\n>> +------\n>> +\n>> +Each configuration source falls within a configuration scope. The scopes\n>> +are:\n>> +\n>> +system::\n>> +\t$(prefix)/etc/gitconfig\n>> +\n>> +global::\n>> +\t$XDG_CONFIG_HOME/git/config\n>> ++\n>> +~/.gitconfig\n>> +\n>> +local::\n>> +\t$GIT_DIR/config\n>> +\n>> +worktree::\n>> +\t$GIT_DIR/config.worktree\n>> +\n>> +command::\n>> +\tenvironment variables\n>> ++\n>> +the `-c` option\n>> +\n>> +With the exception of 'command', each scope corresponds to a command line\n>> +option - `--system`, `--global`, `--local`, `--worktree`.\n>\n> I think a colon after \"option\" is more appropriate than a single \"-\"\n> dash character, but this is definitely a trivial matter that I have no\n> strong opinion on.\n>\n> One thing that this reminds me of (which I don't think is worth taking\n> up here, but perhaps in a future series, or as #leftoverbits) would be\n> promoting these scopes behind a single option. Back in the day, you\n> could ask for values out of `git config` by specifying their type with\n> `--int`, `--bool`, or similar. In e3e042b185 (Merge branch\n> 'tb/config-type', 2018-05-08), we changed to\n> `--type=<int|bool|color|etc>`, which unified things and made it clearer\n> which options were grouped together by a single concept.\n>\n> I think a similar change would make sense here, that is to replace\n> `--system`, `--global` (and so on) with `--scope=system`,\n> `--scope=global`, etc.\n>\n> But that's not material to this series, and just something to think\n> about for later on if you end up thinking it's a good idea.\n\nThis sounds like a great idea, actually. I agree that `--scope` is\nprobably a lot easier to reason about than having N scope flags, and\nthat this probably belongs in a future series.\n\n>> +\n>> +When reading options, specifying a scope will only read options from the\n>> +files within that scope. When writing options, specifying a scope will write\n>> +to the files within that scope (instead of the repository specific\n>> +configuration file). See <<OPTIONS>> above for a complete description.\n>>\n>> +Most configuration options are respected regardless of the scope it is\n>> +defined in, but some options are only respected in certain scopes. See the\n>> +option's documentation for the full details.\n>\n> I assume \"the option's\" is referring to whichever configuration variable\n> we're talking about. So it may be clearer to say \"See the *respective*\n> option's documentation for more information\" or similar.\n\nGood idea. Thanks again!\n\n>\n> Thanks,\n> Taylor\n"},{"id":"458538","messageId":"kl6lfsjertj2.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"Yr42b+MYsuw8ihgG@nand.local","subject":"Re: [PATCH v6 2/5] Documentation: define protected configuration","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-07-06T18:21:53Z","receivedAt":"2022-07-06T18:22:03Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Taylor Blau <me@ttaylorr.com> writes:\n\n> On Thu, Jun 30, 2022 at 06:13:56PM +0000, Glen Choo via GitGitGadget wrote:\n>> @@ -380,6 +381,18 @@ Most configuration options are respected regardless of the scope it is\n>>  defined in, but some options are only respected in certain scopes. See the\n>>  option's documentation for the full details.\n>>\n>> +Protected configuration\n>> +~~~~~~~~~~~~~~~~~~~~~~~\n>> +\n>> +Protected configuration refers to the 'system', 'global', and 'command' scopes.\n>> +For security reasons, certain options are only respected when they are\n>> +specified in protected configuration, and ignored otherwise.\n>> +\n>> +Git treats these scopes as if they are controlled by the user or a trusted\n>> +administrator. This is because an attacker who controls these scopes can do\n>> +substantial harm without using Git, so it is assumed that the user's environment\n>> +protects these scopes against attackers.\n>> +\n>\n> I think this description is a good starting point, but I think I would\n> have liked to see some more from the commit description make it into the\n> documentation here.\n\nYeah, there's a bit of a tradeoff here. Glossing over some of the\ndetails helps keep the documentation briefer and easier to understand\nfor the less experienced/invested, but is bound to frustrate others. I'd\nappreciate any wording suggestions if you have any.\n\n> One thing that I didn't see mentioned in either is that the list of\n> protected configuration is far from exhaustive. There are dozens upon\n> dozens of configuration values that Git will happily execute as a\n> subprocess (core.editor, core.pager, core.alternateRefsCommand, to name\n> just a few).\n>\n> I don't think we should try and enumerate every possible path from\n> configuration to command execution. But it is worth noting in the\n> documentation that the list of configuration values which are only read\n> in the protected context is non-exhaustive and best-effort only.\n\nBy referencing command execution, I think you are alluding to Stolee's\nSecurity Boundary discussion thread [1], and in particular, the \"Example\nSecurity Boundary Question: Unprotected Config and Executing Code\"\nsection?\n\nThat section discusses the problem of arbitrary command execution based\non repository-local config, and how protected configuration might give\nus a way to prevent that. That's a reasonable extension to this series,\nthough it seems a little premature to include allusions to command\nexecution, especially since I don't think we're anywhere close to a\nlong-term direction on what should/shouldn't be inside protected\nconfiguration. For example, Stolee noted that, most of the command\nexecution options really do want per-repository customization, so if we\nwant to continue to support that, we'll need to use protected\nconfiguration in a somewhat sophisticated manner (and not, e.g. only\nrespect command execution options in protected configuration). Perhaps\nwe could shelve this wording change until we've committed to such a\ndirection.\n\n[1] https://lore.kernel.org/git/6af83767-576b-75c4-c778-0284344a8fe7@github.com\n\n> Thanks,\n> Taylor\n"},{"id":"458545","messageId":"kl6lczehsw0p.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"Yr5MZhgTBFgZh/XB@nand.local","subject":"Re: [PATCH v6 3/5] config: learn `git_protected_config()`","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-07-06T22:42:46Z","receivedAt":"2022-07-06T22:42:57Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Taylor Blau <me@ttaylorr.com> writes:\n\n> On Thu, Jun 30, 2022 at 06:13:57PM +0000, Glen Choo via GitGitGadget wrote:\n>> In light of constraint 1, this implementation can still be improved\n>> since git_protected_config() iterates through every variable in\n>> protected_config, which may still be too expensive. There exist constant\n>> time lookup functions for non-protected configuration\n>> (repo_config_get_*()), but for simplicity, this commit does not\n>> implement similar functions for protected configuration.\n>\n> I don't quite follow along with this paragraph: it sounds like reading\n> protected configuration is supposed to be as fast as possible. But you\n> note that only the slower variant of reading each configuration variable\n> one at a time is implemented.\n\nRight. I should have been clearer that this implementation is \"fast\nenough without introducing too much noise/complexity\", and not \"as fast\nas possible\".\n\n> If we care about speed (and I think we should here), then would it make\n> more sense to implement only the lookup functions like\n> repo_config_get_*() for protected context? That would encourage usage by\n> providing a more limited set of options to callers.\n\nI held off on implementing these functions because:\n\n- It requires rewriting `safe.directory`, which reads a multivalued\n  string using a config iterator. It's not onerous to do (I had a POC\n  of this at some point), but it seemed pretty noisy.\n- It seems too noisy to implement all of the protected_config_get_*()\n  functions, and a little inconsistent to only implement the ones used\n  in this series (but maybe a little inconsistency is ok?)\n\nBut maybe a little noise and inconsistency is worth the performance\nimprovement, especially since it's been brought up ~1.5 times before\nthis [1] [2]. I'll do this for sure if you feel strongly about it,\notherwise I'll just try it out just to see what I think about it.\n\n[1] https://lore.kernel.org/git/802c3541-3301-43fc-c39e-edd44e61a4eb@github.com\n[2] https://lore.kernel.org/git/xmqqbkv4t7gp.fsf@gitster.g\n\n>> Signed-off-by: Glen Choo <chooglen@google.com>\n>> ---\n>>  config.c                     | 51 ++++++++++++++++++++++++++++++++++++\n>>  config.h                     | 17 ++++++++++++\n>>  t/t5544-pack-objects-hook.sh |  7 ++++-\n>>  upload-pack.c                | 27 ++++++++++++-------\n>>  4 files changed, 91 insertions(+), 11 deletions(-)\n>>\n>> diff --git a/config.c b/config.c\n>> index 9b0e9c93285..29e62f5d0ed 100644\n>> --- a/config.c\n>> +++ b/config.c\n>> @@ -81,6 +81,18 @@ static enum config_scope current_parsing_scope;\n>>  static int pack_compression_seen;\n>>  static int zlib_compression_seen;\n>>\n>> +/*\n>> + * Config that comes from trusted sources, namely:\n>\n> Should we be using the word \"scope\" here instead of sources? I think\n> it's clear enough from the context what you're referring to, but in the\n> spirit of being consistent...\n\nGood catch.\n\n>> + * - system config files (e.g. /etc/gitconfig)\n>> + * - global config files (e.g. $HOME/.gitconfig,\n>> + *   $XDG_CONFIG_HOME/git)\n>> + * - the command line.\n>> + *\n>> + * This is declared here for code cleanliness, but unlike the other\n>> + * static variables, this does not hold config parser state.\n>> + */\n>> +static struct config_set protected_config;\n>> +\n>>  static int config_file_fgetc(struct config_source *conf)\n>>  {\n>>  \treturn getc_unlocked(conf->u.file);\n>> @@ -2378,6 +2390,11 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n>>  \treturn git_config_from_file(config_set_callback, filename, cs);\n>>  }\n>>\n>> +int git_configset_add_parameters(struct config_set *cs)\n>> +{\n>> +\treturn git_config_from_parameters(config_set_callback, cs);\n>> +}\n>> +\n>>  int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n>>  {\n>>  \tconst struct string_list *values = NULL;\n>> @@ -2619,6 +2636,40 @@ int repo_config_get_pathname(struct repository *repo,\n>>  \treturn ret;\n>>  }\n>>\n>> +/* Read values into protected_config. */\n>> +static void read_protected_config(void)\n>> +{\n>> +\tchar *xdg_config = NULL, *user_config = NULL, *system_config = NULL;\n>> +\n>> +\tgit_configset_init(&protected_config);\n>> +\n>> +\tsystem_config = git_system_config();\n>> +\tgit_global_config(&user_config, &xdg_config);\n>> +\n>> +\tgit_configset_add_file(&protected_config, system_config);\n>> +\tgit_configset_add_file(&protected_config, xdg_config);\n>> +\tgit_configset_add_file(&protected_config, user_config);\n>> +\tgit_configset_add_parameters(&protected_config);\n>> +\n>> +\tfree(system_config);\n>> +\tfree(xdg_config);\n>> +\tfree(user_config);\n>> +}\n>> +\n>> +/* Ensure that protected_config has been initialized. */\n>> +static void git_protected_config_check_init(void)\n>> +{\n>> +\tif (protected_config.hash_initialized)\n>> +\t\treturn;\n>> +\tread_protected_config();\n>> +}\n>> +\n>> +void git_protected_config(config_fn_t fn, void *data)\n>> +{\n>> +\tgit_protected_config_check_init();\n>\n> This may be copying from an existing pattern, but I think you could\n> avoid the extra function declaration by writing git_protected_config()\n> as:\n>\n>     if (!protected_config.hash_initialized)\n>         read_protected_config();\n>     configset_iter(&protected_config, fn, data);\n\nYou're right, I can drop this if I don't implement\nprotected_config_get_*(); this pattern only makes sense for\ngit_config_check_init() because it's called by multiple functions.\n\n> Thanks,\n> Taylor\n"},{"id":"458599","messageId":"kl6lh73svgt5.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"Yr5OTq7s2qxxqsiY@nand.local","subject":"Re: [PATCH v6 5/5] setup.c: create `discovery.bare`","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-07-07T19:55:18Z","receivedAt":"2022-07-07T19:55:29Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Taylor Blau <me@ttaylorr.com> writes:\n\n> On Thu, Jun 30, 2022 at 06:13:59PM +0000, Glen Choo via GitGitGadget wrote:\n>> [1]: https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n>> [2]: https://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n>>\n>> Signed-off-by: Glen Choo <chooglen@google.com>\n>> ---\n>>  Documentation/config.txt           |  2 ++\n>>  Documentation/config/discovery.txt | 23 ++++++++++++\n>>  setup.c                            | 57 +++++++++++++++++++++++++++++-\n>>  t/t0035-discovery-bare.sh          | 52 +++++++++++++++++++++++++++\n>>  4 files changed, 133 insertions(+), 1 deletion(-)\n>>  create mode 100644 Documentation/config/discovery.txt\n>>  create mode 100755 t/t0035-discovery-bare.sh\n>>\n>> diff --git a/Documentation/config.txt b/Documentation/config.txt\n>> index e284b042f22..9a5e1329772 100644\n>> --- a/Documentation/config.txt\n>> +++ b/Documentation/config.txt\n>> @@ -409,6 +409,8 @@ include::config/diff.txt[]\n>>\n>>  include::config/difftool.txt[]\n>>\n>> +include::config/discovery.txt[]\n>> +\n>>  include::config/extensions.txt[]\n>>\n>>  include::config/fastimport.txt[]\n>> diff --git a/Documentation/config/discovery.txt b/Documentation/config/discovery.txt\n>> new file mode 100644\n>> index 00000000000..bbcf89bb0b5\n>> --- /dev/null\n>> +++ b/Documentation/config/discovery.txt\n>> @@ -0,0 +1,23 @@\n>> +discovery.bare::\n>> +\tSpecifies whether Git will work with a bare repository that it\n>> +\tfound during repository discovery. If the repository is\n>\n> Is it clear from the context what \"discovery\" means here? It's probably\n> easier to describe what it isn't, which you kind of do in the next\n> sentence. But it may be clearer to say something like:\n>\n>     Specifies whether Git will recognize bare repositories that aren't\n>     specified via the top-level `--git-dir` command-line option, or the\n>     `GIT_DIR` environment variable (see linkgit:git[1]).\n\nHm that's a good point and the suggestion is very well-worded. In\naddition to what you have, I think we should make reference to\n\"discovery\" _somewhere_ in here since the option is named\n`discovery.bare`, and this seems like a good teaching opportunity.\n\n>> +This defaults to `always`, but this default may change in the future.\n>\n> I think the default being subject to change is par for the course. It's\n> probably easy enough to just say \"Defaults to 'always'\" and leave it at\n> that.\n\nMakes sense.\n\n>> +static enum discovery_bare_allowed get_discovery_bare(void)\n>> +{\n>> +\tenum discovery_bare_allowed result = DISCOVERY_BARE_ALWAYS;\n>> +\tgit_protected_config(discovery_bare_cb, &result);\n>> +\treturn result;\n>> +}\n>> +\n>> +static const char *discovery_bare_allowed_to_string(\n>> +\tenum discovery_bare_allowed discovery_bare_allowed)\n>> +{\n>> +\tswitch (discovery_bare_allowed) {\n>> +\tcase DISCOVERY_BARE_NEVER:\n>> +\t\treturn \"never\";\n>> +\tcase DISCOVERY_BARE_ALWAYS:\n>> +\t\treturn \"always\";\n>\n>> +\tdefault:\n>> +\t\tBUG(\"invalid discovery_bare_allowed %d\",\n>> +\t\t    discovery_bare_allowed);\n>\n> Should we have a default case here since the case arms above are\n> exhaustive?\n\nAh, this \"default:\" was suggested by Stolee in\nhttps://lore.kernel.org/git/7b37f3b7-58c5-1ac5-46eb-d995dc3cc33b@github.com\n\n  This case should be a \"default:\" in case somehow an arbitrary integer\n  value was placed in the variable. [...]\n\nI'm not sure where we stand on this kind of defensiveness. It's not\nreally necessary, but I suppose having a \"default:\" won't hurt here,\nespecially if it BUG()-s instead of silently passing.\n\n>> +\t}\n>> +\treturn NULL;\n>> +}\n>> +\n>>  enum discovery_result {\n>>  \tGIT_DIR_NONE = 0,\n>>  \tGIT_DIR_EXPLICIT,\n>> @@ -1151,7 +1195,8 @@ enum discovery_result {\n>>  \tGIT_DIR_HIT_CEILING = -1,\n>>  \tGIT_DIR_HIT_MOUNT_POINT = -2,\n>>  \tGIT_DIR_INVALID_GITFILE = -3,\n>> -\tGIT_DIR_INVALID_OWNERSHIP = -4\n>> +\tGIT_DIR_INVALID_OWNERSHIP = -4,\n>> +\tGIT_DIR_DISALLOWED_BARE = -5,\n>>  };\n>>\n>>  /*\n>> @@ -1248,6 +1293,8 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n>>  \t\t}\n>>\n>>  \t\tif (is_git_directory(dir->buf)) {\n>> +\t\t\tif (!get_discovery_bare())\n>\n> Relying on NEVER being the zero value here seems fragile to me. Should\n> we check that `if (get_discovery_bare() == DISCOVERY_BARE_NEVER)` to be\n> more explicit here?\n\nThis was also originally suggested by Stolee in \nhttps://lore.kernel.org/git/7b37f3b7-58c5-1ac5-46eb-d995dc3cc33b@github.com\n\n  With (some changes to return the enum), we can [...] let the caller\n  treat the response as a simple boolean.\n\nbut.. your suggestion does seem less fragile. It won't really matter\nwhen we add a third enum and replace the \"if\" with a \"switch\", but it\ndoes matter if we ever muck around with the integer values of\nDISCOVER_BARE_*.\n\n>> +\t\t\t\treturn GIT_DIR_DISALLOWED_BARE;\n>>  \t\t\tif (!ensure_valid_ownership(dir->buf))\n>>  \t\t\t\treturn GIT_DIR_INVALID_OWNERSHIP;\n>>  \t\t\tstrbuf_addstr(gitdir, \".\");\n>> @@ -1394,6 +1441,14 @@ const char *setup_git_directory_gently(int *nongit_ok)\n>>  \t\t}\n>>  \t\t*nongit_ok = 1;\n>>  \t\tbreak;\n>> +\tcase GIT_DIR_DISALLOWED_BARE:\n>> +\t\tif (!nongit_ok) {\n>> +\t\t\tdie(_(\"cannot use bare repository '%s' (discovery.bare is '%s')\"),\n>> +\t\t\t    dir.buf,\n>> +\t\t\t    discovery_bare_allowed_to_string(get_discovery_bare()));\n>> +\t\t}\n>> +\t\t*nongit_ok = 1;\n>> +\t\tbreak;\n>>  \tcase GIT_DIR_NONE:\n>>  \t\t/*\n>>  \t\t * As a safeguard against setup_git_directory_gently_1 returning\n>\n> Thanks,\n> Taylor\n"},{"id":"458608","messageId":"5c58db3bb2189f3b4193a682aa3b43f3bfa95796.1657234914.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v7.git.git.1657234914.gitgitgadget@gmail.com","subject":"[PATCH v7 1/5] Documentation/git-config.txt: add SCOPES section","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-07-07T23:01:50Z","receivedAt":"2022-07-07T23:02:03Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nIn a subsequent commit, we will introduce \"protected configuration\",\nwhich is easiest to describe in terms of configuration scopes (i.e. it's\nthe union of the 'system', 'global', and 'command' scopes). This\ndescription is fine for ML discussions, but it's inadequate for end\nusers because we don't provide a good description of \"configuration\nscopes\" in the public docs.\n\n145d59f482 (config: add '--show-scope' to print the scope of a config\nvalue, 2020-02-10) introduced the word \"scope\" to our public docs, but\nthat only enumerates the scopes and assumes the user can figure out\nthose values mean.\n\nAdd a SCOPES section to Documentation/git-config.txt that describes the\nconfiguration scopes, their corresponding CLI options, and mentions that\nsome configuration options are only respected in certain scopes. Then,\nuse the word \"scope\" to simplify the FILES section and change some\nconfusing wording.\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/git-config.txt | 65 ++++++++++++++++++++++++++++--------\n 1 file changed, 51 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex 9376e39aef2..c4ce61a0493 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -297,8 +297,8 @@ The default is to use a pager.\n FILES\n -----\n \n-If not set explicitly with `--file`, there are four files where\n-'git config' will search for configuration options:\n+By default, 'git config' will read configuration options from multiple\n+files:\n \n $(prefix)/etc/gitconfig::\n \tSystem-wide configuration file.\n@@ -322,27 +322,64 @@ $GIT_DIR/config.worktree::\n \tThis is optional and is only searched when\n \t`extensions.worktreeConfig` is present in $GIT_DIR/config.\n \n-If no further options are given, all reading options will read all of these\n-files that are available. If the global or the system-wide configuration\n-file are not available they will be ignored. If the repository configuration\n-file is not available or readable, 'git config' will exit with a non-zero\n-error code. However, in neither case will an error message be issued.\n+You may also provide additional configuration parameters when running any\n+git command by using the `-c` option. See linkgit:git[1] for details.\n+\n+Options will be read from all of these files that are available. If the\n+global or the system-wide configuration file are not available they will be\n+ignored. If the repository configuration file is not available or readable,\n+'git config' will exit with a non-zero error code. Note that neither case\n+produces an error message.\n \n The files are read in the order given above, with last value found taking\n precedence over values read earlier.  When multiple values are taken then all\n values of a key from all files will be used.\n \n-You may override individual configuration parameters when running any git\n-command by using the `-c` option. See linkgit:git[1] for details.\n-\n-All writing options will per default write to the repository specific\n+By default, options are only written to the repository specific\n configuration file. Note that this also affects options like `--replace-all`\n and `--unset`. *'git config' will only ever change one file at a time*.\n \n-You can override these rules using the `--global`, `--system`,\n-`--local`, `--worktree`, and `--file` command-line options; see\n-<<OPTIONS>> above.\n+You can limit which configuration sources are read from or written to by\n+specifying the path of a file with the `--file` option, or by specifying a\n+configuration scope with `--system`, `--global`, `--local`, or `--worktree`.\n+For more, see <<OPTIONS>> above.\n+\n+SCOPES\n+------\n+\n+Each configuration source falls within a configuration scope. The scopes\n+are:\n+\n+system::\n+\t$(prefix)/etc/gitconfig\n+\n+global::\n+\t$XDG_CONFIG_HOME/git/config\n++\n+~/.gitconfig\n+\n+local::\n+\t$GIT_DIR/config\n+\n+worktree::\n+\t$GIT_DIR/config.worktree\n+\n+command::\n+\tenvironment variables\n++\n+the `-c` option\n+\n+With the exception of 'command', each scope corresponds to a command line\n+option: `--system`, `--global`, `--local`, `--worktree`.\n+\n+When reading options, specifying a scope will only read options from the\n+files within that scope. When writing options, specifying a scope will write\n+to the files within that scope (instead of the repository specific\n+configuration file). See <<OPTIONS>> above for a complete description.\n \n+Most configuration options are respected regardless of the scope it is\n+defined in, but some options are only respected in certain scopes. See the\n+respective option's documentation for the full details.\n \n ENVIRONMENT\n -----------\n-- \ngitgitgadget\n\n"},{"id":"458609","messageId":"pull.1261.v7.git.git.1657234914.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v6.git.git.1656612839.gitgitgadget@gmail.com","subject":"[PATCH v7 0/5] config: introduce discovery.bare and protected config","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-07-07T23:01:49Z","receivedAt":"2022-07-07T23:02:05Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"This version incorporates most of Taylor's comments and suggestions. Thanks\nespecially for the wording suggestions, I struggled with those a lot :)\n\n(I believe) I've responded upthread with my intention for each comment. The\nonly differences between that and the actual changes are:\n\n * In Documentation/git-config.txt, I dropped a suggestion to mention that\n   \"git config --local\" is identical to the default behavior when writing\n   options because I found it too hard to fit in.\n\n * In Documentation/config/discovery.txt, I took Taylor's suggestion, but\n   didn't mention \"discovery\" for the same reasons.\n\n * I decided to leave out the protected config lookup functions. I made some\n   POC patches at:\n   \n   https://github.com/chooglen/git/compare/setup/disable-bare-repo-config...chooglen:git:config/protected-config-lookup-fns\n   \n   which show that the code ends up cleaner with the lookup functions (in\n   particular, it lets us remove struct safe_directory_data, which we only\n   needed to maintain the state of the config iteration). But, I ultimately\n   decided to leave them out of this series because the safe.directory\n   conversion is pretty noisy and might end up becoming a distraction from\n   the discussion here. If there are no strong objections, I'll send them as\n   a follow up series instead.\n\n= Description\n\nThere is a known social engineering attack that takes advantage of the fact\nthat a working tree can include an entire bare repository, including a\nconfig file. A user could run a Git command inside the bare repository\nthinking that the config file of the 'outer' repository would be used, but\nin reality, the bare repository's config file (which is attacker-controlled)\nis used, which may result in arbitrary code execution. See [1] for a fuller\ndescription and deeper discussion.\n\nThis series implements a simple way of preventing such attacks: create a\nconfig option, discovery.bare, that tells Git whether or not to die when it\nfinds a bare repository. discovery.bare has two values:\n\n * \"always\": always allow bare repositories (default), identical to current\n   behavior\n * \"never\": never allow bare repositories\n\nand users/system administrators who never expect to work with bare\nrepositories can secure their environments using \"never\". discovery.bare has\nno effect if --git-dir or GIT_DIR is passed because we are confident that\nthe user is not confused about which repository is being used.\n\nThis series does not change the default behavior, but in the long-run, a\n\"no-embedded\" option might be a safe and usable default [2]. \"never\" is too\nrestrictive and unlikely to be the default.\n\nFor security reasons, discovery.bare cannot be read from repository-level\nconfig (because we would end up trusting the embedded bare repository that\nwe aren't supposed to trust to begin with). Since this would introduce a 3rd\nvariable that is only read from 'protected/trusted configuration' (the\nothers are safe.directory and uploadpack.packObjectsHook) this series also\ndefines and creates a shared implementation for 'protected configuration'\n\n= Patch organization\n\n * Patch 1 add a section on configuration scopes to our docs\n * Patches 2-3 define 'protected configuration' and create a shared\n   implementation.\n * Patch 4 refactors safe.directory to use protected configuration\n * Patch 5 adds discovery.bare\n\n= Series history\n\nChanges in v7:\n\n * Numerous docs improvements and code cleanup.\n * In 3/5's commit message, drop \"as fast as possible\" and allude to lookup\n   functions coming in a later series.\n * Remove a comment in 3/5 about repository.protected_config. That was stale\n   since v4, but slipped under the radar until now.\n * Fix some s/protected config/protected configuration (leftover from v5).\n\nChanges in v6:\n\n * Add TEST_PASSES_SANITIZE_LEAK=true\n * Replace all sub-shells with -C and use test_config_global\n * Change the expect_rejected helper to use \"grep -F\" with a more specific\n   message.\n   * This reveals that the \"-c discovery.bare=\" assertion in the last test\n     was passing for the wrong reason (because '' is an invalid value for\n     \"discovery.bare\"). I removed it because it wasn't doing anything useful\n     anyway - I was trying to make discovery.bare unset in the command line,\n     but the whole point of that test is to assert that we respect the CLI\n     arg.\n\nChanges in v5:\n\n * Standardize the usage of \"protected configuration\" instead of mixing\n   \"config\" and \"configuration\". This required some unfortunate rewrapping.\n * Remove mentions of \"trustworthiness\" when discussing protected\n   configuration and focus on what Git does instead.\n   * The rationale of protected vs non-protected is still kept.\n * Fix the stale documentation entry for discovery.bare.\n * Include a fuller description of how discovery.bare and \"--git-dir\"\n   interact instead of saying \"has no effect\".\n\nChanges in v4:\n\n * 2/5's commit message now justifies what scopes are included in protected\n   config\n * The global configset is now a file-scope static inside config.c\n   (previously it was a member of the_repository).\n * Rename discovery_bare_config to discovery_bare_allowed\n * Make discovery_bare_allowed function-scoped (instead of global).\n * Add an expect_accepted helper to the discovery.bare tests.\n * Add a helper to \"upload-pack\" that reads the protected and non-protected\n   config\n\nChanges in v3:\n\n * Rebase onto a more recent 'master'\n * Reframe this feature in only in terms of the 'embedded bare repo' attack.\n * Other docs improvements (thanks Stolee in particular!)\n * Protected config no longer uses read_very_early_config() and is only read\n   once\n * Protected config now includes \"-c\"\n * uploadpack.packObjectsHook now uses protected config instead of ignoring\n   repo config using config scopes\n\nChanges in v2:\n\n * Rename safe.barerepository to discovery.bare and make it die()\n * Move tests into t/t0034-discovery-bare.sh\n * Avoid unnecessary config reading by using a static variable\n * Add discovery.bare=cwd\n * Fix typos\n\n= Future work\n\n * This series doesn't implement config lookup functions for protected\n   config. This will be done in a follow up series.\n * This series does not implement the \"no-embedded\" option [2] and I won't\n   work on it any time soon, but I'd be more than happy to review if someone\n   sends patches.\n * With discovery.bare, if a builtin is marked RUN_SETUP_GENTLY, setup.c\n   doesn't die() and we don't tell users why their repository was rejected,\n   e.g. \"git config\" gives an opaque \"fatal: not in a git directory\". This\n   isn't a new problem though, since safe.directory has the same issue.\n\n[1]\nhttps://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n\n[2] This was first suggested in\nhttps://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n\nGlen Choo (5):\n  Documentation/git-config.txt: add SCOPES section\n  Documentation: define protected configuration\n  config: learn `git_protected_config()`\n  safe.directory: use git_protected_config()\n  setup.c: create `discovery.bare`\n\n Documentation/config.txt            |  2 +\n Documentation/config/discovery.txt  | 21 ++++++++\n Documentation/config/safe.txt       |  6 +--\n Documentation/config/uploadpack.txt |  6 +--\n Documentation/git-config.txt        | 78 +++++++++++++++++++++++------\n config.c                            | 43 ++++++++++++++++\n config.h                            | 16 ++++++\n setup.c                             | 59 +++++++++++++++++++++-\n t/t0033-safe-directory.sh           | 24 ++++-----\n t/t0035-discovery-bare.sh           | 52 +++++++++++++++++++\n t/t5544-pack-objects-hook.sh        |  7 ++-\n upload-pack.c                       | 27 ++++++----\n 12 files changed, 294 insertions(+), 47 deletions(-)\n create mode 100644 Documentation/config/discovery.txt\n create mode 100755 t/t0035-discovery-bare.sh\n\n\nbase-commit: f770e9f396d48b567ef7b37d273e91ad570a3522\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1261%2Fchooglen%2Fsetup%2Fdisable-bare-repo-config-v7\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1261/chooglen/setup/disable-bare-repo-config-v7\nPull-Request: https://github.com/git/git/pull/1261\n\nRange-diff vs v6:\n\n 1:  ee9619f6ec0 ! 1:  5c58db3bb21 Documentation/git-config.txt: add SCOPES section\n     @@ Documentation/git-config.txt: $GIT_DIR/config.worktree::\n      +Options will be read from all of these files that are available. If the\n      +global or the system-wide configuration file are not available they will be\n      +ignored. If the repository configuration file is not available or readable,\n     -+'git config' will exit with a non-zero error code. However, in neither case\n     -+will an error message be issued.\n     ++'git config' will exit with a non-zero error code. Note that neither case\n     ++produces an error message.\n       \n       The files are read in the order given above, with last value found taking\n       precedence over values read earlier.  When multiple values are taken then all\n     @@ Documentation/git-config.txt: $GIT_DIR/config.worktree::\n      -You can override these rules using the `--global`, `--system`,\n      -`--local`, `--worktree`, and `--file` command-line options; see\n      -<<OPTIONS>> above.\n     -+You can change the way options are read/written by specifying the path to a\n     -+file (`--file`), or by specifying a configuration scope (`--system`,\n     -+`--global`, `--local`, `--worktree`); see <<OPTIONS>> above.\n     ++You can limit which configuration sources are read from or written to by\n     ++specifying the path of a file with the `--file` option, or by specifying a\n     ++configuration scope with `--system`, `--global`, `--local`, or `--worktree`.\n     ++For more, see <<OPTIONS>> above.\n      +\n      +SCOPES\n      +------\n     @@ Documentation/git-config.txt: $GIT_DIR/config.worktree::\n      +the `-c` option\n      +\n      +With the exception of 'command', each scope corresponds to a command line\n     -+option - `--system`, `--global`, `--local`, `--worktree`.\n     ++option: `--system`, `--global`, `--local`, `--worktree`.\n      +\n      +When reading options, specifying a scope will only read options from the\n      +files within that scope. When writing options, specifying a scope will write\n     @@ Documentation/git-config.txt: $GIT_DIR/config.worktree::\n       \n      +Most configuration options are respected regardless of the scope it is\n      +defined in, but some options are only respected in certain scopes. See the\n     -+option's documentation for the full details.\n     ++respective option's documentation for the full details.\n       \n       ENVIRONMENT\n       -----------\n 2:  43627c05c0b ! 2:  58f25612aa3 Documentation: define protected configuration\n     @@ Commit message\n      \n          In our documentation, define 'protected configuration' as the system,\n          global and command config scopes. As a shorthand, I will refer to\n     -    variables that are only respected in protected config as 'protected\n     -    configuration only', but this term is not used in the documentation.\n     +    variables that are only respected in protected configuration as\n     +    'protected configuration only', but this term is not used in the\n     +    documentation.\n      \n     -    This definition of protected config is based on whether or not Git can\n     -    reasonably protect the user by ignoring the configuration scope:\n     +    This definition of protected configuration is based on whether or not\n     +    Git can reasonably protect the user by ignoring the configuration scope:\n      \n          - System, global and command line config are considered protected\n            because an attacker who has control over any of those can do plenty of\n     @@ Documentation/config/uploadpack.txt: uploadpack.packObjectsHook::\n      -repository-level config (this is a safety measure against fetching from\n      -untrusted repositories).\n      +Note that this configuration variable is only respected when it is specified\n     -+in protected config (see <<SCOPES>>). This is a safety measure against\n     -+fetching from untrusted repositories.\n     ++in protected configuration (see <<SCOPES>>). This is a safety measure\n     ++against fetching from untrusted repositories.\n       \n       uploadpack.allowFilter::\n       \tIf this option is set, `upload-pack` will support partial\n      \n       ## Documentation/git-config.txt ##\n     -@@ Documentation/git-config.txt: You can change the way options are read/written by specifying the path to a\n     - file (`--file`), or by specifying a configuration scope (`--system`,\n     - `--global`, `--local`, `--worktree`); see <<OPTIONS>> above.\n     +@@ Documentation/git-config.txt: specifying the path of a file with the `--file` option, or by specifying a\n     + configuration scope with `--system`, `--global`, `--local`, or `--worktree`.\n     + For more, see <<OPTIONS>> above.\n       \n      +[[SCOPES]]\n       SCOPES\n     @@ Documentation/git-config.txt: You can change the way options are read/written by\n       \n      @@ Documentation/git-config.txt: Most configuration options are respected regardless of the scope it is\n       defined in, but some options are only respected in certain scopes. See the\n     - option's documentation for the full details.\n     + respective option's documentation for the full details.\n       \n      +Protected configuration\n      +~~~~~~~~~~~~~~~~~~~~~~~\n 3:  3efe282e6b9 ! 3:  3683d20f232 config: learn `git_protected_config()`\n     @@ Commit message\n          variable today, but we've noted that `safe.directory` and the upcoming\n          `discovery.bare` should also be 'protected configuration only'. So, for\n          consistency, we'd like to have a single implementation for protected\n     -    config.\n     +    configuration.\n      \n          The primary constraints are:\n      \n     -    1. Reading from protected configuration should be as fast as possible.\n     -       Nearly all \"git\" commands inside a bare repository will read both\n     -       `safe.directory` and `discovery.bare`, so we cannot afford to be\n     -       slow.\n     +    1. Reading from protected configuration should be fast. Nearly all \"git\"\n     +       commands inside a bare repository will read both `safe.directory` and\n     +       `discovery.bare`, so we cannot afford to be slow.\n      \n     -    2. Protected config must be readable when the gitdir is not known.\n     -       `safe.directory` and `discovery.bare` both affect repository\n     +    2. Protected configuration must be readable when the gitdir is not\n     +       known. `safe.directory` and `discovery.bare` both affect repository\n             discovery and the gitdir is not known at that point [1].\n      \n          The chosen implementation in this commit is to read protected\n     @@ Commit message\n          non-protected counterparts, e.g. git_protected_config_check_init() vs\n          git_config_check_init().\n      \n     -    In light of constraint 1, this implementation can still be improved\n     -    since git_protected_config() iterates through every variable in\n     -    protected_config, which may still be too expensive. There exist constant\n     -    time lookup functions for non-protected configuration\n     -    (repo_config_get_*()), but for simplicity, this commit does not\n     -    implement similar functions for protected configuration.\n     +    In light of constraint 1, this implementation can still be improved.\n     +    git_protected_config() iterates through every variable in\n     +    protected_config, which is wasteful, but it makes the conversion simple\n     +    because it matches existing patterns. We will likely implement constant\n     +    time lookup functions for protected configuration in a future series\n     +    (such functions already exist for non-protected configuration, i.e.\n     +    repo_config_get_*()).\n      \n          An alternative that avoids introducing another configset is to continue\n          to read all config using git_config(), but only accept values that have\n     @@ config.c: static enum config_scope current_parsing_scope;\n       static int zlib_compression_seen;\n       \n      +/*\n     -+ * Config that comes from trusted sources, namely:\n     -+ * - system config files (e.g. /etc/gitconfig)\n     -+ * - global config files (e.g. $HOME/.gitconfig,\n     -+ *   $XDG_CONFIG_HOME/git)\n     -+ * - the command line.\n     ++ * Config that comes from trusted scopes, namely:\n     ++ * - CONFIG_SCOPE_SYSTEM (e.g. /etc/gitconfig)\n     ++ * - CONFIG_SCOPE_GLOBAL (e.g. $HOME/.gitconfig, $XDG_CONFIG_HOME/git)\n     ++ * - CONFIG_SCOPE_COMMAND (e.g. \"-c\" option, environment variables)\n      + *\n      + * This is declared here for code cleanliness, but unlike the other\n      + * static variables, this does not hold config parser state.\n     @@ config.c: int repo_config_get_pathname(struct repository *repo,\n      +\tfree(user_config);\n      +}\n      +\n     -+/* Ensure that protected_config has been initialized. */\n     -+static void git_protected_config_check_init(void)\n     -+{\n     -+\tif (protected_config.hash_initialized)\n     -+\t\treturn;\n     -+\tread_protected_config();\n     -+}\n     -+\n      +void git_protected_config(config_fn_t fn, void *data)\n      +{\n     -+\tgit_protected_config_check_init();\n     ++\tif (!protected_config.hash_initialized)\n     ++\t\tread_protected_config();\n      +\tconfigset_iter(&protected_config, fn, data);\n      +}\n      +\n     @@ config.h: int repo_config_get_maybe_bool(struct repository *repo,\n       \n      +/*\n      + * Functions for reading protected config. By definition, protected\n     -+ * config ignores repository config, so it is unnecessary to read\n     -+ * protected config from any `struct repository` other than\n     -+ * the_repository.\n     ++ * config ignores repository config, so these do not take a `struct\n     ++ * repository` parameter.\n      + */\n      +void git_protected_config(config_fn_t fn, void *data);\n      +\n 4:  ec925823414 = 4:  6394818ffd8 safe.directory: use git_protected_config()\n 5:  a1323d963f9 ! 5:  eff4b07480e setup.c: create `discovery.bare`\n     @@ Documentation/config.txt: include::config/diff.txt[]\n       ## Documentation/config/discovery.txt (new) ##\n      @@\n      +discovery.bare::\n     -+\tSpecifies whether Git will work with a bare repository that it\n     -+\tfound during repository discovery. If the repository is\n     -+\tspecified directly via the --git-dir command-line option or the\n     -+\tGIT_DIR environment variable (see linkgit:git[1]), Git will\n     -+\talways use the specified repository, regardless of this value.\n     ++\tSpecifies whether Git will work with a bare repository that\n     ++\twasn't specified via the top-level `--git-dir` command-line\n     ++\toption, or the `GIT_DIR` environment variable (see\n     ++\tlinkgit:git[1]). If the repository is specified, Git will always\n     ++\tuse the specified repository, regardless of this value.\n      ++\n      +This config setting is only respected in protected configuration (see\n      +<<SCOPES>>). This prevents the untrusted repository from tampering with\n     @@ Documentation/config/discovery.txt (new)\n      +* `always`: Git always works with bare repositories\n      +* `never`: Git never works with bare repositories\n      ++\n     -+This defaults to `always`, but this default may change in the future.\n     -++\n      +If you do not use bare repositories in your workflow, then it may be\n      +beneficial to set `discovery.bare` to `never` in your global config.\n      +This will protect you from attacks that involve cloning a repository\n     @@ setup.c: static enum discovery_result setup_git_directory_gently_1(struct strbuf\n       \t\t}\n       \n       \t\tif (is_git_directory(dir->buf)) {\n     -+\t\t\tif (!get_discovery_bare())\n     ++\t\t\tif (get_discovery_bare() == DISCOVERY_BARE_NEVER)\n      +\t\t\t\treturn GIT_DIR_DISALLOWED_BARE;\n       \t\t\tif (!ensure_valid_ownership(dir->buf))\n       \t\t\t\treturn GIT_DIR_INVALID_OWNERSHIP;\n\n-- \ngitgitgadget\n"},{"id":"458610","messageId":"58f25612aa385c3ac9f48f908ccc4d0d02d58b8c.1657234914.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v7.git.git.1657234914.gitgitgadget@gmail.com","subject":"[PATCH v7 2/5] Documentation: define protected configuration","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-07-07T23:01:51Z","receivedAt":"2022-07-07T23:02:08Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nFor security reasons, there are config variables that are only trusted\nwhen they are specified in certain configuration scopes, which are\nsometimes referred to on-list as 'protected configuration' [1]. A future\ncommit will introduce another such variable, so let's define our terms\nso that we can have consistent documentation and implementation.\n\nIn our documentation, define 'protected configuration' as the system,\nglobal and command config scopes. As a shorthand, I will refer to\nvariables that are only respected in protected configuration as\n'protected configuration only', but this term is not used in the\ndocumentation.\n\nThis definition of protected configuration is based on whether or not\nGit can reasonably protect the user by ignoring the configuration scope:\n\n- System, global and command line config are considered protected\n  because an attacker who has control over any of those can do plenty of\n  harm without Git, so we gain very little by ignoring those scopes.\n- On the other hand, local (and similarly, worktree) config are not\n  considered protected because it is relatively easy for an attacker to\n  control local config, e.g.:\n  - On some shared user environments, a non-admin attacker can create a\n    repository high up the directory hierarchy (e.g. C:\\.git on\n    Windows), and a user may accidentally use it when their PS1\n    automatically invokes \"git\" commands.\n\n    `safe.directory` prevents attacks of this form by making sure that\n    the user intended to use the shared repository. It obviously\n    shouldn't be read from the repository, because that would end up\n    trusting the repository that Git was supposed to reject.\n  - \"git upload-pack\" is expected to run in repositories that may not be\n    controlled by the user. We cannot ignore all config in that\n    repository (because \"git upload-pack\" would fail), but we can limit\n    the risks by ignoring `uploadpack.packObjectsHook`.\n\nOnly `uploadpack.packObjectsHook` is 'protected configuration only'. The\nfollowing variables are intentionally excluded:\n\n- `safe.directory` should be 'protected configuration only', but it does\n  not technically fit the definition because it is not respected in the\n  \"command\" scope. A future commit will fix this.\n\n- `trace2.*` happens to read the same scopes as `safe.directory` because\n  they share an implementation. However, this is not for security\n  reasons; it is because we want to start tracing so early that\n  repository-level config and \"-c\" are not available [2].\n\n  This requirement is unique to `trace2.*`, so it does not makes sense\n  for protected configuration to be subject to the same constraints.\n\n[1] For example,\nhttps://lore.kernel.org/git/6af83767-576b-75c4-c778-0284344a8fe7@github.com/\n[2] https://lore.kernel.org/git/a0c89d0d-669e-bf56-25d2-cbb09b012e70@jeffhostetler.com/\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config/uploadpack.txt |  6 +++---\n Documentation/git-config.txt        | 13 +++++++++++++\n 2 files changed, 16 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/config/uploadpack.txt b/Documentation/config/uploadpack.txt\nindex 32fad5bbe81..16264d82a72 100644\n--- a/Documentation/config/uploadpack.txt\n+++ b/Documentation/config/uploadpack.txt\n@@ -49,9 +49,9 @@ uploadpack.packObjectsHook::\n \t`pack-objects` to the hook, and expects a completed packfile on\n \tstdout.\n +\n-Note that this configuration variable is ignored if it is seen in the\n-repository-level config (this is a safety measure against fetching from\n-untrusted repositories).\n+Note that this configuration variable is only respected when it is specified\n+in protected configuration (see <<SCOPES>>). This is a safety measure\n+against fetching from untrusted repositories.\n \n uploadpack.allowFilter::\n \tIf this option is set, `upload-pack` will support partial\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex c4ce61a0493..2dc74f510f2 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -344,6 +344,7 @@ specifying the path of a file with the `--file` option, or by specifying a\n configuration scope with `--system`, `--global`, `--local`, or `--worktree`.\n For more, see <<OPTIONS>> above.\n \n+[[SCOPES]]\n SCOPES\n ------\n \n@@ -381,6 +382,18 @@ Most configuration options are respected regardless of the scope it is\n defined in, but some options are only respected in certain scopes. See the\n respective option's documentation for the full details.\n \n+Protected configuration\n+~~~~~~~~~~~~~~~~~~~~~~~\n+\n+Protected configuration refers to the 'system', 'global', and 'command' scopes.\n+For security reasons, certain options are only respected when they are\n+specified in protected configuration, and ignored otherwise.\n+\n+Git treats these scopes as if they are controlled by the user or a trusted\n+administrator. This is because an attacker who controls these scopes can do\n+substantial harm without using Git, so it is assumed that the user's environment\n+protects these scopes against attackers.\n+\n ENVIRONMENT\n -----------\n \n-- \ngitgitgadget\n\n"},{"id":"458611","messageId":"3683d20f23218260bf4f4555a385205f03012565.1657234914.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v7.git.git.1657234914.gitgitgadget@gmail.com","subject":"[PATCH v7 3/5] config: learn `git_protected_config()`","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-07-07T23:01:52Z","receivedAt":"2022-07-07T23:02:18Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\n`uploadpack.packObjectsHook` is the only 'protected configuration only'\nvariable today, but we've noted that `safe.directory` and the upcoming\n`discovery.bare` should also be 'protected configuration only'. So, for\nconsistency, we'd like to have a single implementation for protected\nconfiguration.\n\nThe primary constraints are:\n\n1. Reading from protected configuration should be fast. Nearly all \"git\"\n   commands inside a bare repository will read both `safe.directory` and\n   `discovery.bare`, so we cannot afford to be slow.\n\n2. Protected configuration must be readable when the gitdir is not\n   known. `safe.directory` and `discovery.bare` both affect repository\n   discovery and the gitdir is not known at that point [1].\n\nThe chosen implementation in this commit is to read protected\nconfiguration and cache the values in a global configset. This is\nsimilar to the caching behavior we get with the_repository->config.\n\nIntroduce git_protected_config(), which reads protected configuration\nand caches them in the global configset protected_config. Then, refactor\n`uploadpack.packObjectsHook` to use git_protected_config().\n\nThe protected configuration functions are named similarly to their\nnon-protected counterparts, e.g. git_protected_config_check_init() vs\ngit_config_check_init().\n\nIn light of constraint 1, this implementation can still be improved.\ngit_protected_config() iterates through every variable in\nprotected_config, which is wasteful, but it makes the conversion simple\nbecause it matches existing patterns. We will likely implement constant\ntime lookup functions for protected configuration in a future series\n(such functions already exist for non-protected configuration, i.e.\nrepo_config_get_*()).\n\nAn alternative that avoids introducing another configset is to continue\nto read all config using git_config(), but only accept values that have\nthe correct config scope [2]. This technically fulfills constraint 2,\nbecause git_config() simply ignores the local and worktree config when\nthe gitdir is not known. However, this would read incomplete config into\nthe_repository->config, which would need to be reset when the gitdir is\nknown and git_config() needs to read the local and worktree config.\nResetting the_repository->config might be reasonable while we only have\nthese 'protected configuration only' variables, but it's not clear\nwhether this extends well to future variables.\n\n[1] In this case, we do have a candidate gitdir though, so with a little\nrefactoring, it might be possible to provide a gitdir.\n[2] This is how `uploadpack.packObjectsHook` was implemented prior to\nthis commit.\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n config.c                     | 43 ++++++++++++++++++++++++++++++++++++\n config.h                     | 16 ++++++++++++++\n t/t5544-pack-objects-hook.sh |  7 +++++-\n upload-pack.c                | 27 +++++++++++++---------\n 4 files changed, 82 insertions(+), 11 deletions(-)\n\ndiff --git a/config.c b/config.c\nindex 9b0e9c93285..015bec360f5 100644\n--- a/config.c\n+++ b/config.c\n@@ -81,6 +81,17 @@ static enum config_scope current_parsing_scope;\n static int pack_compression_seen;\n static int zlib_compression_seen;\n \n+/*\n+ * Config that comes from trusted scopes, namely:\n+ * - CONFIG_SCOPE_SYSTEM (e.g. /etc/gitconfig)\n+ * - CONFIG_SCOPE_GLOBAL (e.g. $HOME/.gitconfig, $XDG_CONFIG_HOME/git)\n+ * - CONFIG_SCOPE_COMMAND (e.g. \"-c\" option, environment variables)\n+ *\n+ * This is declared here for code cleanliness, but unlike the other\n+ * static variables, this does not hold config parser state.\n+ */\n+static struct config_set protected_config;\n+\n static int config_file_fgetc(struct config_source *conf)\n {\n \treturn getc_unlocked(conf->u.file);\n@@ -2378,6 +2389,11 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n \treturn git_config_from_file(config_set_callback, filename, cs);\n }\n \n+int git_configset_add_parameters(struct config_set *cs)\n+{\n+\treturn git_config_from_parameters(config_set_callback, cs);\n+}\n+\n int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n {\n \tconst struct string_list *values = NULL;\n@@ -2619,6 +2635,33 @@ int repo_config_get_pathname(struct repository *repo,\n \treturn ret;\n }\n \n+/* Read values into protected_config. */\n+static void read_protected_config(void)\n+{\n+\tchar *xdg_config = NULL, *user_config = NULL, *system_config = NULL;\n+\n+\tgit_configset_init(&protected_config);\n+\n+\tsystem_config = git_system_config();\n+\tgit_global_config(&user_config, &xdg_config);\n+\n+\tgit_configset_add_file(&protected_config, system_config);\n+\tgit_configset_add_file(&protected_config, xdg_config);\n+\tgit_configset_add_file(&protected_config, user_config);\n+\tgit_configset_add_parameters(&protected_config);\n+\n+\tfree(system_config);\n+\tfree(xdg_config);\n+\tfree(user_config);\n+}\n+\n+void git_protected_config(config_fn_t fn, void *data)\n+{\n+\tif (!protected_config.hash_initialized)\n+\t\tread_protected_config();\n+\tconfigset_iter(&protected_config, fn, data);\n+}\n+\n /* Functions used historically to read configuration from 'the_repository' */\n void git_config(config_fn_t fn, void *data)\n {\ndiff --git a/config.h b/config.h\nindex 7654f61c634..ca994d77147 100644\n--- a/config.h\n+++ b/config.h\n@@ -446,6 +446,15 @@ void git_configset_init(struct config_set *cs);\n  */\n int git_configset_add_file(struct config_set *cs, const char *filename);\n \n+/**\n+ * Parses command line options and environment variables, and adds the\n+ * variable-value pairs to the `config_set`. Returns 0 on success, or -1\n+ * if there is an error in parsing. The caller decides whether to free\n+ * the incomplete configset or continue using it when the function\n+ * returns -1.\n+ */\n+int git_configset_add_parameters(struct config_set *cs);\n+\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key` and config set `cs`. When the\n@@ -505,6 +514,13 @@ int repo_config_get_maybe_bool(struct repository *repo,\n int repo_config_get_pathname(struct repository *repo,\n \t\t\t     const char *key, const char **dest);\n \n+/*\n+ * Functions for reading protected config. By definition, protected\n+ * config ignores repository config, so these do not take a `struct\n+ * repository` parameter.\n+ */\n+void git_protected_config(config_fn_t fn, void *data);\n+\n /**\n  * Querying For Specific Variables\n  * -------------------------------\ndiff --git a/t/t5544-pack-objects-hook.sh b/t/t5544-pack-objects-hook.sh\nindex dd5f44d986f..54f54f8d2eb 100755\n--- a/t/t5544-pack-objects-hook.sh\n+++ b/t/t5544-pack-objects-hook.sh\n@@ -56,7 +56,12 @@ test_expect_success 'hook does not run from repo config' '\n \t! grep \"hook running\" stderr &&\n \ttest_path_is_missing .git/hook.args &&\n \ttest_path_is_missing .git/hook.stdin &&\n-\ttest_path_is_missing .git/hook.stdout\n+\ttest_path_is_missing .git/hook.stdout &&\n+\n+\t# check that global config is used instead\n+\ttest_config_global uploadpack.packObjectsHook ./hook &&\n+\tgit clone --no-local . dst2.git 2>stderr &&\n+\tgrep \"hook running\" stderr\n '\n \n test_expect_success 'hook works with partial clone' '\ndiff --git a/upload-pack.c b/upload-pack.c\nindex 3a851b36066..09f48317b02 100644\n--- a/upload-pack.c\n+++ b/upload-pack.c\n@@ -1321,18 +1321,27 @@ static int upload_pack_config(const char *var, const char *value, void *cb_data)\n \t\tdata->advertise_sid = git_config_bool(var, value);\n \t}\n \n-\tif (current_config_scope() != CONFIG_SCOPE_LOCAL &&\n-\t    current_config_scope() != CONFIG_SCOPE_WORKTREE) {\n-\t\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n-\t\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n-\t}\n-\n \tif (parse_object_filter_config(var, value, data) < 0)\n \t\treturn -1;\n \n \treturn parse_hide_refs_config(var, value, \"uploadpack\");\n }\n \n+static int upload_pack_protected_config(const char *var, const char *value, void *cb_data)\n+{\n+\tstruct upload_pack_data *data = cb_data;\n+\n+\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n+\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n+\treturn 0;\n+}\n+\n+static void get_upload_pack_config(struct upload_pack_data *data)\n+{\n+\tgit_config(upload_pack_config, data);\n+\tgit_protected_config(upload_pack_protected_config, data);\n+}\n+\n void upload_pack(const int advertise_refs, const int stateless_rpc,\n \t\t const int timeout)\n {\n@@ -1340,8 +1349,7 @@ void upload_pack(const int advertise_refs, const int stateless_rpc,\n \tstruct upload_pack_data data;\n \n \tupload_pack_data_init(&data);\n-\n-\tgit_config(upload_pack_config, &data);\n+\tget_upload_pack_config(&data);\n \n \tdata.stateless_rpc = stateless_rpc;\n \tdata.timeout = timeout;\n@@ -1695,8 +1703,7 @@ int upload_pack_v2(struct repository *r, struct packet_reader *request)\n \n \tupload_pack_data_init(&data);\n \tdata.use_sideband = LARGE_PACKET_MAX;\n-\n-\tgit_config(upload_pack_config, &data);\n+\tget_upload_pack_config(&data);\n \n \twhile (state != FETCH_DONE) {\n \t\tswitch (state) {\n-- \ngitgitgadget\n\n"},{"id":"458612","messageId":"6394818ffd84928910273eedcd9f84e7e47f9dce.1657234915.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v7.git.git.1657234914.gitgitgadget@gmail.com","subject":"[PATCH v7 4/5] safe.directory: use git_protected_config()","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-07-07T23:01:53Z","receivedAt":"2022-07-07T23:02:20Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nUse git_protected_config() to read `safe.directory` instead of\nread_very_early_config(), making it 'protected configuration only'.\n\nAs a result, `safe.directory` now respects \"-c\", so update the tests and\ndocs accordingly. It used to ignore \"-c\" due to how it was implemented,\nnot because of security or correctness concerns [1].\n\n[1] https://lore.kernel.org/git/xmqqlevabcsu.fsf@gitster.g/\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config/safe.txt |  6 +++---\n setup.c                       |  2 +-\n t/t0033-safe-directory.sh     | 24 ++++++++++--------------\n 3 files changed, 14 insertions(+), 18 deletions(-)\n\ndiff --git a/Documentation/config/safe.txt b/Documentation/config/safe.txt\nindex fa02f3ccc54..f72b4408798 100644\n--- a/Documentation/config/safe.txt\n+++ b/Documentation/config/safe.txt\n@@ -12,9 +12,9 @@ via `git config --add`. To reset the list of safe directories (e.g. to\n override any such directories specified in the system config), add a\n `safe.directory` entry with an empty value.\n +\n-This config setting is only respected when specified in a system or global\n-config, not when it is specified in a repository config, via the command\n-line option `-c safe.directory=<path>`, or in environment variables.\n+This config setting is only respected in protected configuration (see\n+<<SCOPES>>). This prevents the untrusted repository from tampering with this\n+value.\n +\n The value of this setting is interpolated, i.e. `~/<path>` expands to a\n path relative to the home directory and `%(prefix)/<path>` expands to a\ndiff --git a/setup.c b/setup.c\nindex faf5095e44d..c8e3c32814d 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1137,7 +1137,7 @@ static int ensure_valid_ownership(const char *path)\n \t    is_path_owned_by_current_user(path))\n \t\treturn 1;\n \n-\tread_very_early_config(safe_directory_cb, &data);\n+\tgit_protected_config(safe_directory_cb, &data);\n \n \treturn data.is_safe;\n }\ndiff --git a/t/t0033-safe-directory.sh b/t/t0033-safe-directory.sh\nindex 238b25f91a3..5a1cd0d0947 100755\n--- a/t/t0033-safe-directory.sh\n+++ b/t/t0033-safe-directory.sh\n@@ -16,24 +16,20 @@ test_expect_success 'safe.directory is not set' '\n \texpect_rejected_dir\n '\n \n-test_expect_success 'ignoring safe.directory on the command line' '\n-\ttest_must_fail git -c safe.directory=\"$(pwd)\" status 2>err &&\n-\tgrep \"unsafe repository\" err\n+test_expect_success 'safe.directory on the command line' '\n+\tgit -c safe.directory=\"$(pwd)\" status\n '\n \n-test_expect_success 'ignoring safe.directory in the environment' '\n-\ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n-\t\tGIT_CONFIG_KEY_0=\"safe.directory\" \\\n-\t\tGIT_CONFIG_VALUE_0=\"$(pwd)\" \\\n-\t\tgit status 2>err &&\n-\tgrep \"unsafe repository\" err\n+test_expect_success 'safe.directory in the environment' '\n+\tenv GIT_CONFIG_COUNT=1 \\\n+\t    GIT_CONFIG_KEY_0=\"safe.directory\" \\\n+\t    GIT_CONFIG_VALUE_0=\"$(pwd)\" \\\n+\t    git status\n '\n \n-test_expect_success 'ignoring safe.directory in GIT_CONFIG_PARAMETERS' '\n-\ttest_must_fail env \\\n-\t\tGIT_CONFIG_PARAMETERS=\"${SQ}safe.directory${SQ}=${SQ}$(pwd)${SQ}\" \\\n-\t\tgit status 2>err &&\n-\tgrep \"unsafe repository\" err\n+test_expect_success 'safe.directory in GIT_CONFIG_PARAMETERS' '\n+\tenv GIT_CONFIG_PARAMETERS=\"${SQ}safe.directory${SQ}=${SQ}$(pwd)${SQ}\" \\\n+\t    git status\n '\n \n test_expect_success 'ignoring safe.directory in repo config' '\n-- \ngitgitgadget\n\n"},{"id":"458613","messageId":"eff4b07480ebfadb072faccc36a527cf78c9d2c3.1657234915.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v7.git.git.1657234914.gitgitgadget@gmail.com","subject":"[PATCH v7 5/5] setup.c: create `discovery.bare`","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-07-07T23:01:54Z","receivedAt":"2022-07-07T23:02:23Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nThere is a known social engineering attack that takes advantage of the\nfact that a working tree can include an entire bare repository,\nincluding a config file. A user could run a Git command inside the bare\nrepository thinking that the config file of the 'outer' repository would\nbe used, but in reality, the bare repository's config file (which is\nattacker-controlled) is used, which may result in arbitrary code\nexecution. See [1] for a fuller description and deeper discussion.\n\nA simple mitigation is to forbid bare repositories unless specified via\n`--git-dir` or `GIT_DIR`. In environments that don't use bare\nrepositories, this would be minimally disruptive.\n\nCreate a config variable, `discovery.bare`, that tells Git whether or\nnot to die() when it discovers a bare repository. This only affects\nrepository discovery, thus it has no effect if discovery was not\ndone, e.g. if the user passes `--git-dir=my-dir`, discovery will be\nskipped and my-dir will be used as the repo regardless of the\n`discovery.bare` value.\n\nThis config is an enum of:\n\n- \"always\": always allow bare repositories (this is the default)\n- \"never\": never allow bare repositories\n\nIf we want to protect users from such attacks by default, neither value\nwill suffice - \"always\" provides no protection, but \"never\" is\nimpractical for bare repository users. A more usable default would be to\nallow only non-embedded bare repositories ([2] contains one such\nproposal), but detecting if a repository is embedded is potentially\nnon-trivial, so this work is not implemented in this series.\n\n[1]: https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n[2]: https://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config.txt           |  2 ++\n Documentation/config/discovery.txt | 21 +++++++++++\n setup.c                            | 57 +++++++++++++++++++++++++++++-\n t/t0035-discovery-bare.sh          | 52 +++++++++++++++++++++++++++\n 4 files changed, 131 insertions(+), 1 deletion(-)\n create mode 100644 Documentation/config/discovery.txt\n create mode 100755 t/t0035-discovery-bare.sh\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex e284b042f22..9a5e1329772 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -409,6 +409,8 @@ include::config/diff.txt[]\n \n include::config/difftool.txt[]\n \n+include::config/discovery.txt[]\n+\n include::config/extensions.txt[]\n \n include::config/fastimport.txt[]\ndiff --git a/Documentation/config/discovery.txt b/Documentation/config/discovery.txt\nnew file mode 100644\nindex 00000000000..6f38b86884b\n--- /dev/null\n+++ b/Documentation/config/discovery.txt\n@@ -0,0 +1,21 @@\n+discovery.bare::\n+\tSpecifies whether Git will work with a bare repository that\n+\twasn't specified via the top-level `--git-dir` command-line\n+\toption, or the `GIT_DIR` environment variable (see\n+\tlinkgit:git[1]). If the repository is specified, Git will always\n+\tuse the specified repository, regardless of this value.\n++\n+This config setting is only respected in protected configuration (see\n+<<SCOPES>>). This prevents the untrusted repository from tampering with\n+this value.\n++\n+The currently supported values are:\n++\n+* `always`: Git always works with bare repositories\n+* `never`: Git never works with bare repositories\n++\n+If you do not use bare repositories in your workflow, then it may be\n+beneficial to set `discovery.bare` to `never` in your global config.\n+This will protect you from attacks that involve cloning a repository\n+that contains a bare repository and running a Git command within that\n+directory.\ndiff --git a/setup.c b/setup.c\nindex c8e3c32814d..84cd02a1209 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -10,6 +10,10 @@\n static int inside_git_dir = -1;\n static int inside_work_tree = -1;\n static int work_tree_config_is_bogus;\n+enum discovery_bare_allowed {\n+\tDISCOVERY_BARE_NEVER = 0,\n+\tDISCOVERY_BARE_ALWAYS,\n+};\n \n static struct startup_info the_startup_info;\n struct startup_info *startup_info = &the_startup_info;\n@@ -1142,6 +1146,46 @@ static int ensure_valid_ownership(const char *path)\n \treturn data.is_safe;\n }\n \n+static int discovery_bare_cb(const char *key, const char *value, void *d)\n+{\n+\tenum discovery_bare_allowed *discovery_bare_allowed = d;\n+\n+\tif (strcmp(key, \"discovery.bare\"))\n+\t\treturn 0;\n+\n+\tif (!strcmp(value, \"never\")) {\n+\t\t*discovery_bare_allowed = DISCOVERY_BARE_NEVER;\n+\t\treturn 0;\n+\t}\n+\tif (!strcmp(value, \"always\")) {\n+\t\t*discovery_bare_allowed = DISCOVERY_BARE_ALWAYS;\n+\t\treturn 0;\n+\t}\n+\treturn -1;\n+}\n+\n+static enum discovery_bare_allowed get_discovery_bare(void)\n+{\n+\tenum discovery_bare_allowed result = DISCOVERY_BARE_ALWAYS;\n+\tgit_protected_config(discovery_bare_cb, &result);\n+\treturn result;\n+}\n+\n+static const char *discovery_bare_allowed_to_string(\n+\tenum discovery_bare_allowed discovery_bare_allowed)\n+{\n+\tswitch (discovery_bare_allowed) {\n+\tcase DISCOVERY_BARE_NEVER:\n+\t\treturn \"never\";\n+\tcase DISCOVERY_BARE_ALWAYS:\n+\t\treturn \"always\";\n+\tdefault:\n+\t\tBUG(\"invalid discovery_bare_allowed %d\",\n+\t\t    discovery_bare_allowed);\n+\t}\n+\treturn NULL;\n+}\n+\n enum discovery_result {\n \tGIT_DIR_NONE = 0,\n \tGIT_DIR_EXPLICIT,\n@@ -1151,7 +1195,8 @@ enum discovery_result {\n \tGIT_DIR_HIT_CEILING = -1,\n \tGIT_DIR_HIT_MOUNT_POINT = -2,\n \tGIT_DIR_INVALID_GITFILE = -3,\n-\tGIT_DIR_INVALID_OWNERSHIP = -4\n+\tGIT_DIR_INVALID_OWNERSHIP = -4,\n+\tGIT_DIR_DISALLOWED_BARE = -5,\n };\n \n /*\n@@ -1248,6 +1293,8 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n \t\t}\n \n \t\tif (is_git_directory(dir->buf)) {\n+\t\t\tif (get_discovery_bare() == DISCOVERY_BARE_NEVER)\n+\t\t\t\treturn GIT_DIR_DISALLOWED_BARE;\n \t\t\tif (!ensure_valid_ownership(dir->buf))\n \t\t\t\treturn GIT_DIR_INVALID_OWNERSHIP;\n \t\t\tstrbuf_addstr(gitdir, \".\");\n@@ -1394,6 +1441,14 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \t\t}\n \t\t*nongit_ok = 1;\n \t\tbreak;\n+\tcase GIT_DIR_DISALLOWED_BARE:\n+\t\tif (!nongit_ok) {\n+\t\t\tdie(_(\"cannot use bare repository '%s' (discovery.bare is '%s')\"),\n+\t\t\t    dir.buf,\n+\t\t\t    discovery_bare_allowed_to_string(get_discovery_bare()));\n+\t\t}\n+\t\t*nongit_ok = 1;\n+\t\tbreak;\n \tcase GIT_DIR_NONE:\n \t\t/*\n \t\t * As a safeguard against setup_git_directory_gently_1 returning\ndiff --git a/t/t0035-discovery-bare.sh b/t/t0035-discovery-bare.sh\nnew file mode 100755\nindex 00000000000..8f802746530\n--- /dev/null\n+++ b/t/t0035-discovery-bare.sh\n@@ -0,0 +1,52 @@\n+#!/bin/sh\n+\n+test_description='verify discovery.bare checks'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n+. ./test-lib.sh\n+\n+pwd=\"$(pwd)\"\n+\n+expect_accepted () {\n+\tgit \"$@\" rev-parse --git-dir\n+}\n+\n+expect_rejected () {\n+\ttest_must_fail git \"$@\" rev-parse --git-dir 2>err &&\n+\tgrep -F \"cannot use bare repository\" err\n+}\n+\n+test_expect_success 'setup bare repo in worktree' '\n+\tgit init outer-repo &&\n+\tgit init --bare outer-repo/bare-repo\n+'\n+\n+test_expect_success 'discovery.bare unset' '\n+\texpect_accepted -C outer-repo/bare-repo\n+'\n+\n+test_expect_success 'discovery.bare=always' '\n+\ttest_config_global discovery.bare always &&\n+\texpect_accepted -C outer-repo/bare-repo\n+'\n+\n+test_expect_success 'discovery.bare=never' '\n+\ttest_config_global discovery.bare never &&\n+\texpect_rejected -C outer-repo/bare-repo\n+'\n+\n+test_expect_success 'discovery.bare in the repository' '\n+\t# discovery.bare must not be \"never\", otherwise git config fails\n+\t# with \"fatal: not in a git directory\" (like safe.directory)\n+\ttest_config -C outer-repo/bare-repo discovery.bare always &&\n+\ttest_config_global discovery.bare never &&\n+\texpect_rejected -C outer-repo/bare-repo\n+'\n+\n+test_expect_success 'discovery.bare on the command line' '\n+\ttest_config_global discovery.bare never &&\n+\texpect_accepted -C outer-repo/bare-repo \\\n+\t\t-c discovery.bare=always\n+'\n+\n+test_done\n-- \ngitgitgadget\n"},{"id":"458614","messageId":"xmqq4jzsxze4.fsf@gitster.g","threadId":"57859","inReplyTo":"5c58db3bb2189f3b4193a682aa3b43f3bfa95796.1657234914.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 1/5] Documentation/git-config.txt: add SCOPES section","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-07-07T23:43:15Z","receivedAt":"2022-07-07T23:43:23Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Glen Choo <chooglen@google.com>\n>\n> In a subsequent commit, we will introduce \"protected configuration\",\n> which is easiest to describe in terms of configuration scopes (i.e. it's\n> the union of the 'system', 'global', and 'command' scopes). This\n> description is fine for ML discussions, but it's inadequate for end\n> users because we don't provide a good description of \"configuration\n> scopes\" in the public docs.\n>\n> 145d59f482 (config: add '--show-scope' to print the scope of a config\n> value, 2020-02-10) introduced the word \"scope\" to our public docs, but\n> that only enumerates the scopes and assumes the user can figure out\n> those values mean.\n\nProbably: \"figure out those values mean\" -> \"figure out what those\nvalues mean\"\n\n> Add a SCOPES section to Documentation/git-config.txt that describes the\n> configuration scopes, their corresponding CLI options, and mentions that\n> some configuration options are only respected in certain scopes. Then,\n> use the word \"scope\" to simplify the FILES section and change some\n> confusing wording.\n>\n> Signed-off-by: Glen Choo <chooglen@google.com>\n> ---\n>  Documentation/git-config.txt | 65 ++++++++++++++++++++++++++++--------\n>  1 file changed, 51 insertions(+), 14 deletions(-)\n>\n> diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\n> index 9376e39aef2..c4ce61a0493 100644\n> --- a/Documentation/git-config.txt\n> +++ b/Documentation/git-config.txt\n> @@ -297,8 +297,8 @@ The default is to use a pager.\n>  FILES\n>  -----\n>  \n> -If not set explicitly with `--file`, there are four files where\n> -'git config' will search for configuration options:\n> +By default, 'git config' will read configuration options from multiple\n> +files:\n>  \n>  $(prefix)/etc/gitconfig::\n>  \tSystem-wide configuration file.\n> @@ -322,27 +322,64 @@ $GIT_DIR/config.worktree::\n>  \tThis is optional and is only searched when\n>  \t`extensions.worktreeConfig` is present in $GIT_DIR/config.\n>  \n> -If no further options are given, all reading options will read all of these\n> -files that are available. If the global or the system-wide configuration\n> -file are not available they will be ignored. If the repository configuration\n> -file is not available or readable, 'git config' will exit with a non-zero\n> -error code. However, in neither case will an error message be issued.\n> +You may also provide additional configuration parameters when running any\n> +git command by using the `-c` option. See linkgit:git[1] for details.\n\nListing \"-c\" as another one in addition to these files is probably a\ngood simplification, instead of \"-c override others\" in the original,\nas we would need to say \"more specific ones override less specific\nones\" anyway.\n\n> +Options will be read from all of these files that are available. If the\n> +global or the system-wide configuration file are not available they will be\n> +ignored. If the repository configuration file is not available or readable,\n> +'git config' will exit with a non-zero error code. Note that neither case\n> +produces an error message.\n\nProblem inherited from the original, but I suspect that rephrasing\n\"not available\" to \"missing\" (or \"does not exist\") may make it\neasier to follow.  Also, \"global\" in the preceding description is\nexplained as one of the user-specific configuration files, so it may\nbe better to avoid it, e.g.\n\n\tIf the user-specific or the system-wide configuration files\n\tare missing, they will be ignored.  If the repository\n\tconfiguration file is missing or unreadable, ...\n\nAlternatively, we may want to tighten the description of\n$XDG_CONFIG_HOME/git/config and ~/.gitconfig a bit better,\ne.g. something along the lines of ...\n\n\t$XDG_CONFIG_HOME/git/config::\n\t$HOME/.gitconfig::\n\t\tUser-specific configuration file.  When the\n\t\tXDG_CONFIG_HOME environment variable is not set or\n\t\tempty, $HOME/.config/ is used as $XDG_CONFIG_HOME.\n\t+\n\tThese are often called the \"global\" configuration file.\n\tWhen either or both of them exist(s), both files are read.\n\nNote that I deliberately omitted the mehtion that our $XDG support\nmay be too recent and $HOME/.gitconfig may be preferred for\nportability.  It came from 21cf3227 (config: read (but not write)\nfrom $XDG_CONFIG_HOME/git/config file, 2012-06-22) but sufficient\nnumber of years have passed.\n\nAlso note that I originally wrote the following immediately after\nthe above\n\n\t+\n\tWhen writing to the \"--global\" scope (see below),\n\t$XDG_CONFIG_HOME/git/config is used if it exists; otherwise\n\t$HOME/.gitconfig is used.\n\nbut decided to discard it, since the OPTIONS -> \"--global\" covers\nit well enough.\n\nWith a tightening of the definition of what \"global\" is,, we can\nrephrase \"If the global or the system configuration files are\nmissing...\".\n\n> +You can limit which configuration sources are read from or written to by\n> +specifying the path of a file with the `--file` option, or by specifying a\n> +configuration scope with `--system`, `--global`, `--local`, or `--worktree`.\n> +For more, see <<OPTIONS>> above.\n> +\n> +SCOPES\n> +------\n> +\n> +Each configuration source falls within a configuration scope. The scopes\n> +are:\n> +\n> +system::\n> +\t$(prefix)/etc/gitconfig\n> +\n> +global::\n> +\t$XDG_CONFIG_HOME/git/config\n> ++\n> +~/.gitconfig\n> +\n> +local::\n> +\t$GIT_DIR/config\n> +\n> +worktree::\n> +\t$GIT_DIR/config.worktree\n> +\n> +command::\n> +\tenvironment variables\n\nWe'd need to tighten this a bit, like:\n\n\tGIT_CONFIG_{COUNT,KEY,VALUE} environment varialbes (see below)\n\nGIT_CONFIG_GLOBAL or GIT_CONFIG_SYSTEM environment varialbes and\nothers are listed in the ENVIRONMENT section that comes after this\nsection, but you do not want the readers to be confused into\nthinking that we'd give them more precedence over others.\n\n> ++\n> +the `-c` option\n\n> +With the exception of 'command', each scope corresponds to a command line\n> +option: `--system`, `--global`, `--local`, `--worktree`.\n> +\n> +When reading options, specifying a scope will only read options from the\n> +files within that scope. When writing options, specifying a scope will write\n> +to the files within that scope (instead of the repository specific\n> +configuration file). See <<OPTIONS>> above for a complete description.\n>  \n> +Most configuration options are respected regardless of the scope it is\n> +defined in, but some options are only respected in certain scopes. See the\n> +respective option's documentation for the full details.\n>  \n>  ENVIRONMENT\n>  -----------\n\nOverall it was a pleasant read.  Thanks, will queue.\n"},{"id":"458619","messageId":"xmqqy1x4wi7z.fsf@gitster.g","threadId":"57859","inReplyTo":"58f25612aa385c3ac9f48f908ccc4d0d02d58b8c.1657234914.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 2/5] Documentation: define protected configuration","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-07-08T00:39:28Z","receivedAt":"2022-07-08T00:39:39Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Glen Choo <chooglen@google.com>\n>\n> For security reasons, there are config variables that are only trusted\n> when they are specified in certain configuration scopes, which are\n> sometimes referred to on-list as 'protected configuration' [1]. A future\n> commit will introduce another such variable, so let's define our terms\n> so that we can have consistent documentation and implementation.\n>\n> In our documentation, define 'protected configuration' as the system,\n> global and command config scopes. As a shorthand, I will refer to\n> variables that are only respected in protected configuration as\n> 'protected configuration only', but this term is not used in the\n> documentation.\n>\n> This definition of protected configuration is based on whether or not\n> Git can reasonably protect the user by ignoring the configuration scope:\n>\n> - System, global and command line config are considered protected\n>   because an attacker who has control over any of those can do plenty of\n>   harm without Git, so we gain very little by ignoring those scopes.\n> - On the other hand, local (and similarly, worktree) config are not\n>   considered protected because it is relatively easy for an attacker to\n>   control local config, e.g.:\n>   - On some shared user environments, a non-admin attacker can create a\n>     repository high up the directory hierarchy (e.g. C:\\.git on\n>     Windows), and a user may accidentally use it when their PS1\n>     automatically invokes \"git\" commands.\n>\n>     `safe.directory` prevents attacks of this form by making sure that\n>     the user intended to use the shared repository. It obviously\n>     shouldn't be read from the repository, because that would end up\n>     trusting the repository that Git was supposed to reject.\n>   - \"git upload-pack\" is expected to run in repositories that may not be\n>     controlled by the user. We cannot ignore all config in that\n>     repository (because \"git upload-pack\" would fail), but we can limit\n>     the risks by ignoring `uploadpack.packObjectsHook`.\n\nThis is only about the formatting, but have a blank line between\neach bullet-point (e.g. before the line that talks about \"On some\nshared user enviornments, ...\" and \"git upload-pack\").  A paragraph\nbreak within a single bullet-point (i.e. the paragraph that talks\nabout `safe.directory` is a second paragraph of hte same bullet\npoint as the paragraph before it) looks like a stronger break than\nseparation between each bullet-point, which you wrote without any\nblank lines in between.\n\n> Only `uploadpack.packObjectsHook` is 'protected configuration only'. The\n> following variables are intentionally excluded:\n>\n> - `safe.directory` should be 'protected configuration only', but it does\n>   not technically fit the definition because it is not respected in the\n>   \"command\" scope. A future commit will fix this.\n>\n> - `trace2.*` happens to read the same scopes as `safe.directory` because\n>   they share an implementation. However, this is not for security\n>   reasons; it is because we want to start tracing so early that\n>   repository-level config and \"-c\" are not available [2].\n>\n>   This requirement is unique to `trace2.*`, so it does not makes sense\n>   for protected configuration to be subject to the same constraints.\n\nVery well reasoned.\n\n"},{"id":"458620","messageId":"xmqqtu7swgxd.fsf@gitster.g","threadId":"57859","inReplyTo":"pull.1261.v7.git.git.1657234914.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 0/5] config: introduce discovery.bare and protected config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-07-08T01:07:26Z","receivedAt":"2022-07-08T01:07:34Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> This version incorporates most of Taylor's comments and suggestions. Thanks\n> especially for the wording suggestions, I struggled with those a lot :)\n>\n> (I believe) I've responded upthread with my intention for each comment. The\n> only differences between that and the actual changes are:\n>\n>  * In Documentation/git-config.txt, I dropped a suggestion to mention that\n>    \"git config --local\" is identical to the default behavior when writing\n>    options because I found it too hard to fit in.\n>\n>  * In Documentation/config/discovery.txt, I took Taylor's suggestion, but\n>    didn't mention \"discovery\" for the same reasons.\n>\n>  * I decided to leave out the protected config lookup functions. I made some\n>    POC patches at:\n\nThese patches overall looked ok.  I am not very happy to see the\nproliferation of namespaces like safe.* and discovery.* that would\nnot likely to get the second variable, though.\n"},{"id":"458651","messageId":"kl6l5yk77d4b.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"xmqq4jzsxze4.fsf@gitster.g","subject":"Re: [PATCH v7 1/5] Documentation/git-config.txt: add SCOPES section","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-07-08T17:01:08Z","receivedAt":"2022-07-08T17:01:15Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> \"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>> From: Glen Choo <chooglen@google.com>\n>>\n>> In a subsequent commit, we will introduce \"protected configuration\",\n>> which is easiest to describe in terms of configuration scopes (i.e. it's\n>> the union of the 'system', 'global', and 'command' scopes). This\n>> description is fine for ML discussions, but it's inadequate for end\n>> users because we don't provide a good description of \"configuration\n>> scopes\" in the public docs.\n>>\n>> 145d59f482 (config: add '--show-scope' to print the scope of a config\n>> value, 2020-02-10) introduced the word \"scope\" to our public docs, but\n>> that only enumerates the scopes and assumes the user can figure out\n>> those values mean.\n>\n> Probably: \"figure out those values mean\" -> \"figure out what those\n> values mean\"\n>\n>> Add a SCOPES section to Documentation/git-config.txt that describes the\n>> configuration scopes, their corresponding CLI options, and mentions that\n>> some configuration options are only respected in certain scopes. Then,\n>> use the word \"scope\" to simplify the FILES section and change some\n>> confusing wording.\n>>\n>> Signed-off-by: Glen Choo <chooglen@google.com>\n>> ---\n>>  Documentation/git-config.txt | 65 ++++++++++++++++++++++++++++--------\n>>  1 file changed, 51 insertions(+), 14 deletions(-)\n>>\n>> diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\n>> index 9376e39aef2..c4ce61a0493 100644\n>> --- a/Documentation/git-config.txt\n>> +++ b/Documentation/git-config.txt\n>> @@ -297,8 +297,8 @@ The default is to use a pager.\n>>  FILES\n>>  -----\n>>  \n>> -If not set explicitly with `--file`, there are four files where\n>> -'git config' will search for configuration options:\n>> +By default, 'git config' will read configuration options from multiple\n>> +files:\n>>  \n>>  $(prefix)/etc/gitconfig::\n>>  \tSystem-wide configuration file.\n>> @@ -322,27 +322,64 @@ $GIT_DIR/config.worktree::\n>>  \tThis is optional and is only searched when\n>>  \t`extensions.worktreeConfig` is present in $GIT_DIR/config.\n>>  \n>> -If no further options are given, all reading options will read all of these\n>> -files that are available. If the global or the system-wide configuration\n>> -file are not available they will be ignored. If the repository configuration\n>> -file is not available or readable, 'git config' will exit with a non-zero\n>> -error code. However, in neither case will an error message be issued.\n>> +You may also provide additional configuration parameters when running any\n>> +git command by using the `-c` option. See linkgit:git[1] for details.\n>\n> Listing \"-c\" as another one in addition to these files is probably a\n> good simplification, instead of \"-c override others\" in the original,\n> as we would need to say \"more specific ones override less specific\n> ones\" anyway.\n>\n>> +Options will be read from all of these files that are available. If the\n>> +global or the system-wide configuration file are not available they will be\n>> +ignored. If the repository configuration file is not available or readable,\n>> +'git config' will exit with a non-zero error code. Note that neither case\n>> +produces an error message.\n>\n> Problem inherited from the original, but I suspect that rephrasing\n> \"not available\" to \"missing\" (or \"does not exist\") may make it\n> easier to follow.  Also, \"global\" in the preceding description is\n> explained as one of the user-specific configuration files, so it may\n> be better to avoid it, e.g.\n>\n> \tIf the user-specific or the system-wide configuration files\n> \tare missing, they will be ignored.  If the repository\n> \tconfiguration file is missing or unreadable, ...\n>\n> Alternatively, we may want to tighten the description of\n> $XDG_CONFIG_HOME/git/config and ~/.gitconfig a bit better,\n> e.g. something along the lines of ...\n>\n> \t$XDG_CONFIG_HOME/git/config::\n> \t$HOME/.gitconfig::\n> \t\tUser-specific configuration file.  When the\n> \t\tXDG_CONFIG_HOME environment variable is not set or\n> \t\tempty, $HOME/.config/ is used as $XDG_CONFIG_HOME.\n> \t+\n> \tThese are often called the \"global\" configuration file.\n> \tWhen either or both of them exist(s), both files are read.\n>\n> Note that I deliberately omitted the mehtion that our $XDG support\n> may be too recent and $HOME/.gitconfig may be preferred for\n> portability.  It came from 21cf3227 (config: read (but not write)\n> from $XDG_CONFIG_HOME/git/config file, 2012-06-22) but sufficient\n> number of years have passed.\n>\n> Also note that I originally wrote the following immediately after\n> the above\n>\n> \t+\n> \tWhen writing to the \"--global\" scope (see below),\n> \t$XDG_CONFIG_HOME/git/config is used if it exists; otherwise\n> \t$HOME/.gitconfig is used.\n>\n> but decided to discard it, since the OPTIONS -> \"--global\" covers\n> it well enough.\n>\n> With a tightening of the definition of what \"global\" is,, we can\n> rephrase \"If the global or the system configuration files are\n> missing...\".\n\nMakes sense. I think this is simpler and more coherent with your\nsuggested changes.\n\nThe only change I'd suggest is to expand \"missing\" -> \"missing or\nunreadable\". The original wording is \"not available\", which could be\ninterpreted to cover both cases. We'd obviously also have to amend\n\"not available or readable\" accordingly.\n\n>> +You can limit which configuration sources are read from or written to by\n>> +specifying the path of a file with the `--file` option, or by specifying a\n>> +configuration scope with `--system`, `--global`, `--local`, or `--worktree`.\n>> +For more, see <<OPTIONS>> above.\n>> +\n>> +SCOPES\n>> +------\n>> +\n>> +Each configuration source falls within a configuration scope. The scopes\n>> +are:\n>> +\n>> +system::\n>> +\t$(prefix)/etc/gitconfig\n>> +\n>> +global::\n>> +\t$XDG_CONFIG_HOME/git/config\n>> ++\n>> +~/.gitconfig\n>> +\n>> +local::\n>> +\t$GIT_DIR/config\n>> +\n>> +worktree::\n>> +\t$GIT_DIR/config.worktree\n>> +\n>> +command::\n>> +\tenvironment variables\n>\n> We'd need to tighten this a bit, like:\n>\n> \tGIT_CONFIG_{COUNT,KEY,VALUE} environment varialbes (see below)\n>\n> GIT_CONFIG_GLOBAL or GIT_CONFIG_SYSTEM environment varialbes and\n> others are listed in the ENVIRONMENT section that comes after this\n> section, but you do not want the readers to be confused into\n> thinking that we'd give them more precedence over others.\n\nAh, good point.\n\n>> ++\n>> +the `-c` option\n>\n>> +With the exception of 'command', each scope corresponds to a command line\n>> +option: `--system`, `--global`, `--local`, `--worktree`.\n>> +\n>> +When reading options, specifying a scope will only read options from the\n>> +files within that scope. When writing options, specifying a scope will write\n>> +to the files within that scope (instead of the repository specific\n>> +configuration file). See <<OPTIONS>> above for a complete description.\n>>  \n>> +Most configuration options are respected regardless of the scope it is\n>> +defined in, but some options are only respected in certain scopes. See the\n>> +respective option's documentation for the full details.\n>>  \n>>  ENVIRONMENT\n>>  -----------\n>\n> Overall it was a pleasant read.  Thanks, will queue.\n\nThanks! Shall I apply your suggestions, or were you planning to apply\nthem yourself?\n"},{"id":"458657","messageId":"xmqq1quvwhsd.fsf@gitster.g","threadId":"57859","inReplyTo":"kl6l5yk77d4b.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v7 1/5] Documentation/git-config.txt: add SCOPES section","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2022-07-08T19:01:06Z","receivedAt":"2022-07-08T19:01:17Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Glen Choo <chooglen@google.com> writes:\n\n>> Problem inherited from the original, but I suspect that rephrasing\n>> \"not available\" to \"missing\" (or \"does not exist\") may make it\n>> easier to follow.\n>> ...\n>\n> The only change I'd suggest is to expand \"missing\" -> \"missing or\n> unreadable\". The original wording is \"not available\", which could be\n> interpreted to cover both cases. We'd obviously also have to amend\n> \"not available or readable\" accordingly.\n\nProbably.  I wonder if we should document that we at least warn when\na file we are expected to read exists but is not readable (instead\nof simply saying \"is ignored\"), but other than that I agree with you.\n\n> Thanks! Shall I apply your suggestions, or were you planning to apply\n> them yourself?\n\nDefinitely not the latter ;-)\n"},{"id":"458663","messageId":"kl6l1quv737u.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"xmqqtu7swgxd.fsf@gitster.g","subject":"Re: [PATCH v7 0/5] config: introduce discovery.bare and protected config","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-07-08T20:35:01Z","receivedAt":"2022-07-08T20:35:25Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> \"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>> This version incorporates most of Taylor's comments and suggestions. Thanks\n>> especially for the wording suggestions, I struggled with those a lot :)\n>>\n>> (I believe) I've responded upthread with my intention for each comment. The\n>> only differences between that and the actual changes are:\n>>\n>>  * In Documentation/git-config.txt, I dropped a suggestion to mention that\n>>    \"git config --local\" is identical to the default behavior when writing\n>>    options because I found it too hard to fit in.\n>>\n>>  * In Documentation/config/discovery.txt, I took Taylor's suggestion, but\n>>    didn't mention \"discovery\" for the same reasons.\n>>\n>>  * I decided to leave out the protected config lookup functions. I made some\n>>    POC patches at:\n>\n> These patches overall looked ok.  I am not very happy to see the\n> proliferation of namespaces like safe.* and discovery.* that would\n> not likely to get the second variable, though.\n\nFair. I think `discovery.bare` is similar enough to `safe.directory`\nthat it could belong in the safe.* namespace if we find a good name for\nit.\n\nWe rejected \"safe.bareRepository\" earlier because of the insinuation\nthat bare repos are unsafe. Maybe:\n\n- safe.bareDiscovery\n- safe.bareRepositoryDiscovery\n- safe.unspecifiedBareRepository\n- safe.discoveredBareRepository\n\n\"safe.unspecifiedBareRepository\" is sounding pretty good to me\nactually.. Any thoughts?\n"},{"id":"458665","messageId":"kl6ly1x35lpw.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"xmqq1quvwhsd.fsf@gitster.g","subject":"Re: [PATCH v7 1/5] Documentation/git-config.txt: add SCOPES section","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-07-08T21:38:19Z","receivedAt":"2022-07-08T21:38:31Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Glen Choo <chooglen@google.com> writes:\n>\n>> Thanks! Shall I apply your suggestions, or were you planning to apply\n>> them yourself?\n>\n> Definitely not the latter ;-)\n\n;) Ok, I'll give others some time to weigh in before rerolling.\n"},{"id":"458669","messageId":"220709.86zghj8d6i.gmgdl@evledraar.gmail.com","threadId":"57859","inReplyTo":"kl6lr134spi0.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v6 0/5] config: introduce discovery.bare and protected config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-07-08T21:58:22Z","receivedAt":"2022-07-08T22:14:37Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Jul 01 2022, Glen Choo wrote:\n\nSorry for the late reply, I see there's a v7, but this seems to also\napply to it, so...\n\n> Thanks for weighing in :) Despite the different proposed approaches, I\n> think we actually are in broad agreement.\n>\n> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n>\n>> On Thu, Jun 30 2022, Glen Choo via GitGitGadget wrote:\n>>\n>>> This is a quick re-roll to address Ævar's comments on the tests (thanks!).\n>>\n>> Thanks!\n>>\n>>> = Description\n>>\n>> Just more generally on this series & approach. I know this is a v6 by\n>> now, but I haven't kept up with this topic, but to be fair I did mention\n>> pretty much this in:\n>> https://lore.kernel.org/git/220407.86lewhc6bz.gmgdl@evledraar.gmail.com/\n>>\n>> So...\n>>\n>>> There is a known social engineering attack that takes advantage of the fact\n>>> that a working tree can include an entire bare repository, including a\n>>> config file. A user could run a Git command inside the bare repository\n>>> thinking that the config file of the 'outer' repository would be used, but\n>>> in reality, the bare repository's config file (which is attacker-controlled)\n>>> is used, which may result in arbitrary code execution. See [1] for a fuller\n>>> description and deeper discussion.\n>>>\n>>> This series implements a simple way of preventing such attacks: create a\n>>> config option, discovery.bare, that tells Git whether or not to die when it\n>>> finds a bare repository. discovery.bare has two values:\n>>>\n>>>  * \"always\": always allow bare repositories (default), identical to current\n>>>    behavior\n>>>  * \"never\": never allow bare repositories\n>>>\n>>> and users/system administrators who never expect to work with bare\n>>> repositories can secure their environments using \"never\". discovery.bare has\n>>> no effect if --git-dir or GIT_DIR is passed because we are confident that\n>>> the user is not confused about which repository is being used.\n>>\n>> I'm not insisting that the entire approach here should be changed, but\n>> in the above exchange you seemed to have performance concerns about the\n>> \"just walk up in setup.c\" approach I mentioned, but it's not clear if\n>> that's still the only thing that necessitates taking this approach.\n>>\n>> There may be security subtleties that I've missed, but from the\n>> description here it seems like that would work equally well, and\n>> wouldn't require configuration, except insofar as we'd need to opt-in to\n>> reading config from bare repositores *that also exist in a parent tree*.\n>>\n>> And it would be a more narrow & more secure solution, since it would\n>> e.g. allow you to intentionally navigate to /var/repos/git/git.git in a\n>> server setup and read the config there, which it could distinguish from\n>> a case of /var/repos/.git existing, and git/git.git being brought in as\n>> a part of that \"parent\" repo.\n>\n> Performance is one major concern, yes, and I agree that your findings\n> show that the \"just walk up\" approach is cheap enough to consider doing.\n> Though in the few cases where it isn't cheap to walk, wouldn't it still\n> be useful to be able to opt out of it?\n\nMaybe, but until we at least have a reason to think that there is a\nperformance concern this all seems rather hypothetical.\n\nIn the thread(s) I linked to I noted that I tested my POC implementation\non AIX, which has a sloooooooow filesystem.\n\nWhy are you concerned about this having a performance impact?\n\n> The other concern is simplicity and correctness. Are we confident that\n> we'll get the design of \"just walk up\" correct (including edge cases\n> like \"bare repo in bare repo in non bare repo\")? I'm 100% confident that\n> we'll get it right eventually, and that this approach will be a good\n> default for all users. But in comparison, \"never\" is so much easier to\n> understand and implement that I don't see why we shouldn't start by\n> presenting this option to the 0.1-1% of users who would find it useful.\n\nIf you run \"git status\" or whatever in a directory anywhere on your FS\nnow it'll confidently tell you if you're not within a git repository.\n\nWe're just talking about re-using that code, why would we be concerned\nthat:\n\n 1. Finding a repo at a given <PATH>\n 2. Appending \"/..\" to that <PATH>\n 3. Feeding it to a \"given a path, find me the git repo\" (code setup.c\n    already has)\n\nIs something we'd get wrong?\n\n> And on the topic of simplicity, there's significant interest in\n> maintaining backwards-compatibility with repos with workflows that\n> absolutely depend on embedded bare repos, e.g. libgit2 and Git-LFS.\n> That's yet another special case that we'd have to get right. Stolee's\n> \"no-embedded\" proposal [1] pretty much covers that, but I don't see the\n> harm in simplifying the design space by making bare repo support a\n> non-goal.\n>\n> [1] https://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n\nSure, and we could have a config know to still use such repos (for\nconfig or otherwise) with the \"walk up\" method, does that address your\nconcern here?\n\n>> The \"more narrow\" and \"more secure\" go hand-in-hand, since if you work\n>> on such servers you'd turn this to \"always\" because you want to read\n>> such config, but then be left vulnerable to the actual (and muche rarer)\n>> exploit we're trying to prevent.\n>\n> The point that we're not defending bare repo users is fair, but maybe\n> the group we're trying to protect isn't really dedicated Git-serving\n> servers. This exploit requires you to have a bare repo inside the\n> working tree of a non-bare repo. So I think this is less of an issue for\n> a server, and more for \"mixed-use\" environments with both regular and\n> bare clones.\n\nYes, but this is only something that's even a question because of an\nartificial limitation your proposal here suffers from.\n\nI.e. in trying to detect nefarious repos where you've got \"looks like\nbare\" content *tracked* in another repo you're conflating it with *any\nbare repo*.\n\nAnd the only reason we're doing that seems to me to be a premature\noptimization.\n\n>> Which, it seems...\n>>\n>>> This series does not change the default behavior, but in the long-run, a\n>>> \"no-embedded\" option might be a safe and usable default [2]. \"never\" is too\n>>> restrictive and unlikely to be the default.\n>>\n>> This series has (since v3?) been noting aspirations to have a\n>> \"no-embedded\" variant of this config, which your 5/5 here notes would be\n>> better, but isn't implemented by this series.\n>>\n>> But your 5/5 also notes:\n>>\n>>     but detecting if a repository is embedded is potentially\n>>     non-trivial, so this work is not implemented in this series.\n>>\n>> Hrm, well, the diff-stat isn't quite that trivial either :) :\n>\n> Well.. a lot of it is refactoring :P\n>\n>>> [...]\n>>>  upload-pack.c                       | 27 ++++++----\n>>>  12 files changed, 304 insertions(+), 47 deletions(-)\n>>\n>> In threads linked from the above ML link I linked to some POC code\n>> showing how to hack a second .git discovery walk into setup.c. This was\n>> as part of the \"submodule parent dir\" proposal, which is a different\n>> feature, but also needs such \"find the parent\" code:\n>> https://lore.kernel.org/git/211109.86v912dtfw.gmgdl@evledraar.gmail.com/\n>>\n>> Now, obviously that's a dirty hack, but it's not that hard to just\n>> change the part of setup.c where we're satisfied that we've found the\n>> git dir, then walk up \"$THAT_DIR/..\", and start our search again.\n>>\n>> Then:\n>>\n>> \tif (first_dir_was_bare() && found_parent_dir())\n>>         \tenforce_no_embedded();\n>>\n>> Isn't that what your proposed \"no embedded\" option would need to do?\n>> Well, maybe we'd also check if the \"first dir\" is in the index of the\n>> parent, as opposed to just being a bare .git somewhere in ~/Downloads,\n>> e.g. if you have a ~/.git and keep your dot-files in git.\n>>\n>> But I think for an initial implementation just doing the walk would be\n>> good enough, and would have a more narrow scope than this configuration\n>> setting.\n>\n> A narrow scope is good, but I don't agree on this definition of\n> \"narrow\". My preference is to give an obvious solution to a 'narrow'\n> group of users, instead of a more tricky solution that affects all users\n> in a 'narrow' set of cases.\n\nWe could still have an option to say \"I never want to consider bare\nrepos from config\", but if we're able to distinguish *tracked* bare\nrepos from non-tracked ones this is entirely unrelated to the initial\nstated motivation for this series, isn't it?\n\nI.e. to solve the security issue at hand.\n\nI've got nothing against *also* providing a \"I never want config from\nxyz repos\", but that seems to be orthagonal.\n\n>> AFAICT the performance concerns aren't supported by any data, in the\n>> case of the \"submodule superproject\" feature it turned out to not be the\n>> directory walk, but us shelling out in a loop in git-submodule.sh.\n>>\n>> Well, *maybe* that's not the case, I think I have managed to read\n>> between the lines of some of these past exchanges that there's some odd\n>> propriterary internal NFS-like setup at Google where *parent dirs* are\n>> auto-mounted and searched on access, so a \"walk up\" pattern would be\n>> much more expensive.\n>>\n>> I do worry a bit about us ending up with design choices in git that we\n>> wouldn't have ended up with, if not to cater to some in-house setup\n>> somwhere that 99.99% of git users will never see.\n>\n> At the very least, I don't think you're saying that it's a bad idea to\n> have \"never\", just that we might not have come up with it if not for\n> some Google NFS thing.\n\nI'm just speculating as to why we've ended up with this approach, but\nmaybe I'm wrong.\n\n> Another use case I can think of is CI bots, which have no need for bare\n> repos. To some folks (maybe in very security-sensitive environments),\n> \"never\" might give more peace of mind than \"no-embedded\".\n\nSure, but again, I've got nothing against having *more config knobs*,\nand I've often e.g. wanted some way to tell git to read no config at all\n(which you can sort of do with the right environment variables, but it's\na hassle).\n\nBut I don't see how that's not unrelated to addressing the issue this\nseries aims to address, and what I'm pointing out that it's doing so\nwith a method that's less accurate than the \"walk up\" method, and less\nsecure.\n"},{"id":"458917","messageId":"kl6lv8s25a80.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"220709.86zghj8d6i.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH v6 0/5] config: introduce discovery.bare and protected config","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-07-12T20:47:59Z","receivedAt":"2022-07-12T20:48:17Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"\nThanks for following up. I'm a concerned that this thread will be\nunproductive if all we're doing is reiterating our own opinions. I'm ok\nif the conclusion is \"agree to disagree\", but let's not spend too much\ntime talking circles around one another (myself included, of course:)).\n\nÆvar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> On Fri, Jul 01 2022, Glen Choo wrote:\n>>> The \"more narrow\" and \"more secure\" go hand-in-hand, since if you work\n>>> on such servers you'd turn this to \"always\" because you want to read\n>>> such config, but then be left vulnerable to the actual (and muche rarer)\n>>> exploit we're trying to prevent.\n>>\n>> The point that we're not defending bare repo users is fair, but maybe\n>> the group we're trying to protect isn't really dedicated Git-serving\n>> servers. This exploit requires you to have a bare repo inside the\n>> working tree of a non-bare repo. So I think this is less of an issue for\n>> a server, and more for \"mixed-use\" environments with both regular and\n>> bare clones.\n>\n> Yes, but this is only something that's even a question because of an\n> artificial limitation your proposal here suffers from.\n>\n> I.e. in trying to detect nefarious repos where you've got \"looks like\n> bare\" content *tracked* in another repo you're conflating it with *any\n> bare repo*.\n>\n> And the only reason we're doing that seems to me to be a premature\n> optimization.\n\nRight, I hear you. Besides performance, let me offer the perspective\nthat I should have led with in the previous email. In this thread and\nthe original \"embedded bare repo\" one [1], there is a huge diversity of\nopinion on what the default behavior should be, e.g.:\n\n- How do we detect an embedded bare repo (fsck check? walk up [and check\n  if it's tracked]?)\n- What to do when we detect one (ignore the config? block the repo?)\n- How to preserve workflows that rely on embedded bare repos (some kind\n  of (global|per-repo) exception list? allow the repo but not the\n  config?)\n\nAnd rightfully so! There are a lot of options here, so we want to make\nsure we get the defaults right. But at the same time that implies a\npretty slow, difficult process.\n\nOn the other hand, I haven't seen nearly as much disagreement on \"just\nrefuse to work with bare repos\" because it's so restrictive that it\nprobably won't be the default. So it'll have no effect on most users,\nbut still confers protection for the subset of users who can benefit\nfrom it. For those who want the problem fixed _today_ (e.g. my\nemployer), this seems like simple, low-hanging fruit that buys time for\nus to find good default.\n\nFWIW, when time permits I'd be happy to work on that good default (which\nwill probably be some variant of \"walk up\"), and to pay off the tech\ndebt introduced by this implementation (I have some ideas about how we\ncould improve the config API to achieve this [2]). Hopefully that helps\nallay some of your concerns?\n\n[1] https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n[2] https://lore.kernel.org/git/kl6lr13fi9qn.fsf@chooglen-macbookpro.roam.corp.google.com\n"},{"id":"458918","messageId":"kl6lsfn656cw.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"kl6l1quv737u.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v7 0/5] config: introduce discovery.bare and protected config","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-07-12T22:11:27Z","receivedAt":"2022-07-12T22:11:39Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Glen Choo <chooglen@google.com> writes:\n\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n>> \"Glen Choo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>>\n>>> This version incorporates most of Taylor's comments and suggestions. Thanks\n>>> especially for the wording suggestions, I struggled with those a lot :)\n>>>\n>>> (I believe) I've responded upthread with my intention for each comment. The\n>>> only differences between that and the actual changes are:\n>>>\n>>>  * In Documentation/git-config.txt, I dropped a suggestion to mention that\n>>>    \"git config --local\" is identical to the default behavior when writing\n>>>    options because I found it too hard to fit in.\n>>>\n>>>  * In Documentation/config/discovery.txt, I took Taylor's suggestion, but\n>>>    didn't mention \"discovery\" for the same reasons.\n>>>\n>>>  * I decided to leave out the protected config lookup functions. I made some\n>>>    POC patches at:\n>>\n>> These patches overall looked ok.  I am not very happy to see the\n>> proliferation of namespaces like safe.* and discovery.* that would\n>> not likely to get the second variable, though.\n>\n> Fair. I think `discovery.bare` is similar enough to `safe.directory`\n> that it could belong in the safe.* namespace if we find a good name for\n> it.\n>\n> We rejected \"safe.bareRepository\" earlier because of the insinuation\n> that bare repos are unsafe. Maybe:\n>\n> - safe.bareDiscovery\n> - safe.bareRepositoryDiscovery\n> - safe.unspecifiedBareRepository\n> - safe.discoveredBareRepository\n>\n> \"safe.unspecifiedBareRepository\" is sounding pretty good to me\n> actually.. Any thoughts?\n\n(+CC Johannes Schindelin for thoughts on what should go into `safe.*`\nand/or design considerations that went into it.)\n\nAnother thought is that `discovery.bare` and `safe.directory` should\nboth indeed live in the same namespace, but that namespace should be\nnamed something other than `safe.*`, e.g. if we had\n`allowedRepositories.otherOwner` instead of `safe.directory`, it would\nhave been a no-brainer for me to put this in the `allowedRepositories.*`\nnamespace.\n\nSo an alternative proposal would be:\n\n- rename this to `allowedRepositories.discoveredBare`\n- (possibly not in this series, but at some point) create a\n  `safe.directory` alias in that namespace, e.g.\n  `allowedRepositories.otherOwner`\n\n*But* I don't see the former making sense without the latter (I really\nthink both should be in the same namespace), so if we think that's\nunnecessary churn, I'll drop this idea entirely.\n"},{"id":"458929","messageId":"220713.86v8s14y3w.gmgdl@evledraar.gmail.com","threadId":"57859","inReplyTo":"kl6lv8s25a80.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v6 0/5] config: introduce discovery.bare and protected config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-07-12T23:53:24Z","receivedAt":"2022-07-13T01:09:46Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Jul 12 2022, Glen Choo wrote:\n\n> Thanks for following up. I'm a concerned that this thread will be\n> unproductive if all we're doing is reiterating our own opinions. I'm ok\n> if the conclusion is \"agree to disagree\", but let's not spend too much\n> time talking circles around one another (myself included, of course:)).\n\nYes, I have not been following up here to merely repeat what's been said\nbefore, but...\n\n> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n>\n>> On Fri, Jul 01 2022, Glen Choo wrote:\n>>>> The \"more narrow\" and \"more secure\" go hand-in-hand, since if you work\n>>>> on such servers you'd turn this to \"always\" because you want to read\n>>>> such config, but then be left vulnerable to the actual (and muche rarer)\n>>>> exploit we're trying to prevent.\n>>>\n>>> The point that we're not defending bare repo users is fair, but maybe\n>>> the group we're trying to protect isn't really dedicated Git-serving\n>>> servers. This exploit requires you to have a bare repo inside the\n>>> working tree of a non-bare repo. So I think this is less of an issue for\n>>> a server, and more for \"mixed-use\" environments with both regular and\n>>> bare clones.\n>>\n>> Yes, but this is only something that's even a question because of an\n>> artificial limitation your proposal here suffers from.\n>>\n>> I.e. in trying to detect nefarious repos where you've got \"looks like\n>> bare\" content *tracked* in another repo you're conflating it with *any\n>> bare repo*.\n>>\n>> And the only reason we're doing that seems to me to be a premature\n>> optimization.\n>\n> Right, I hear you. Besides performance,[...]\n\n...have been following up because it's still genuinely unclear to me\nwhat data or design constraints led to this solution. I.e. in [1] you\nnoted (\"[...]\" interjection is mine):\n\n\t\"I don't see how we could implement this [the \"walk-up\" method]\n\twithout imposing a big penalty to all bare repo users[...].\"\n\n[Continued below]\n\n> let me offer the perspective\n> that I should have led with in the previous email. In this thread and\n> the original \"embedded bare repo\" one [1], there is a huge diversity of\n> opinion on what the default behavior should be, e.g.:\n\nI read that thread over again, and some of the highlights were:\n\n * brian asking if we can't basically do the \"walk up\" method:\n   https://lore.kernel.org/git/Yk9hONuCIVIq6ieV@camp.crustytoothpaste.net/\n\n * Taylor wondering how much we need to worry about this attack (among\n   other things) & worrying about legitimate \"bare repo\" workflows being\n   broken: https://lore.kernel.org/git/YloTQH35r2xVdPm1@nand.local/ &\n   https://lore.kernel.org/git/Ylobp7sntKeWTLDX@nand.local/\n\nBut most importantly, here's something I hadn't noticed before:\n\n * Emily talking about the supposed slowness of the \"walk up\" method:\n   https://lore.kernel.org/git/CAJoAoZkgnnvdymuBsM9Ja3+eYSnyohr=FQZMVX_uzZ_pkQhgaw@mail.gmail.com/\n\nI.e.:\n\n\t\"wantonly scanning up the filesystem for any gitdir above the\n\tcurrent one is really expensive. When I tried that approach for\n\tthe purposes of including some shared config between\n\tsuperproject and submodules, it slowed down the Git test suite\n\tby something like 3-5x.\"\n\nWhich I'm now 99.99% certain based on past context[2] is a misstatement\nor misrecollection about an early version of\nsubmodule.superprojectGitDir v.s. what setup.c would do.\n\nI.e. that 3-5x slowness referred to git-submodule.sh shelling out to\n\"git rev-parse\", it's not a reference to the expense of the few syscalls\nwe'd need to make to discover a parent git directory.\n\nDid you hear about the directory walking being a performance concern\nfrom Emily, or was it an independent discovery?\n\nIt seems as though this might have come about because of a\nmisrecollection about the git-rev-parse(1)/git-submodule.sh v.s. setup.c\nperformance with reference to submodule.superprojectGitDir, and that\nwe've now got a design that's optimized to avoid a performance problem\nthat doesn't exist, at the cost of accuracy.\n\nAnd not to reiterate, but I think the performance isn't a concern\nper-se, but rather that performance concerns seem to have driven one\ndesign over another.\n\n> - How do we detect an embedded bare repo (fsck check? walk up [and check\n>   if it's tracked]?)\n> - What to do when we detect one (ignore the config? block the repo?)\n> - How to preserve workflows that rely on embedded bare repos (some kind\n>   of (global|per-repo) exception list? allow the repo but not the\n>   config?)\n>\n> And rightfully so! There are a lot of options here, so we want to make\n> sure we get the defaults right. But at the same time that implies a\n> pretty slow, difficult process.\n\nI saw some implementation discussion about how we'd do this with fsck,\nwhich is one thing, but I don't really see the trickyness or ambiguity\non the client side.\n\nI.e. we know when we'd \"find a repo\", so that's the criteria we'd use to\nignore such a contained repo or not. The only trickyness seems to come\nabout if the approach we pick is one where we conflate embedded bare\nrepos v.s. non-embedded bare repos.\n\n> On the other hand, I haven't seen nearly as much disagreement on \"just\n> refuse to work with bare repos\" because it's so restrictive that it\n> probably won't be the default. So it'll have no effect on most users,\n> but still confers protection for the subset of users who can benefit\n> from it. For those who want the problem fixed _today_ (e.g. my\n> employer), this seems like simple, low-hanging fruit that buys time for\n> us to find good default.\n>\n> FWIW, when time permits I'd be happy to work on that good default (which\n> will probably be some variant of \"walk up\"), and to pay off the tech\n> debt introduced by this implementation (I have some ideas about how we\n> could improve the config API to achieve this [2]). Hopefully that helps\n> allay some of your concerns?\n\nIt really just seems like a dead end to me, sorry.\n\nI.e. we know what the security problem is, but the side-effects of this\napproach are such that we'll probably never turn it on by default.\n\nSo that'll mean that the vast majority of users who could benefit from\nthe security mitigation won't even know about the config, or if they do\nmight not have it turned on.\n\nAnd yes, we might end up with a better design later, but then we'll have\nto still support this config mechanism, potentially deprecate it etc.\n\n> [1] https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n> [2] https://lore.kernel.org/git/kl6lr13fi9qn.fsf@chooglen-macbookpro.roam.corp.google.com\n\n1. https://lore.kernel.org/git/kl6lee1z8mcm.fsf@chooglen-macbookpro.roam.corp.google.com/\n2. https://lore.kernel.org/git/211109.86v912dtfw.gmgdl@evledraar.gmail.com/\n"},{"id":"459087","messageId":"pull.1261.v8.git.git.1657834081.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v7.git.git.1657234914.gitgitgadget@gmail.com","subject":"[PATCH v8 0/5] config: introduce safe.bareRepository and protected config","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-07-14T21:27:56Z","receivedAt":"2022-07-14T21:28:11Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Thanks all! This version takes Junio's wording suggestions on the previous\nround, renames the config variable and, because of conflicts with 2.37.1, is\nnewly rebased onto master.\n\nThe config variable is now named \"safe.bareRepository\"; I discarded this\nname earlier in this series, but the intent behind the series has come\naround to the point where \"safe.bareRepository\" makes sense again. And\nthanks to Dscho's suggestion to explicitly create an option for \"no\ndiscovered bare repos\" [1], the UI is self-documenting enough that we can\nwrite terser docs.\n\nAs mentioned upthread, I would love to see \"safe.*\" namespace renamed (it's\nan adjective, not a Git command/entity), but I'm doubling down on it anyway.\nThis config option does such a similar thing to \"safe.directory\" that they\nreally should be siblings, and I don't foresee a world where\n\"safe.directory\" gets renamed.\n\nI've triple-checked to make sure I've scrubbed the commits of the old name,\nbut I'd appreciate the extra eyes :)\n\n= Description\n\nThere is a known social engineering attack that takes advantage of the fact\nthat a working tree can include an entire bare repository, including a\nconfig file. A user could run a Git command inside the bare repository\nthinking that the config file of the 'outer' repository would be used, but\nin reality, the bare repository's config file (which is attacker-controlled)\nis used, which may result in arbitrary code execution. See [2] for a fuller\ndescription and deeper discussion.\n\nThis series implements a simple way of preventing such attacks: create a\nconfig option, safe.bareRepository, that tells Git whether or not to die\nwhen it finds a bare repository. safe.bareRepository has two values:\n\n * \"all\": allow all bare repositories (default), identical to current\n   behavior\n * \"explicit\": only allow bare repositories specified via --git-dir or\n   GIT_DIR\n\nand users/system administrators who never expect to work with bare\nrepositories can secure their environments using \"explicit\". We still trust\nexplicit bare repositories because we are confident that the user is not\nconfused about which repository is being used.\n\nThis series does not change the default behavior, but in the long-run, a\n\"no-embedded\" option might be a safe and usable default [3]. \"never\" is too\nrestrictive and unlikely to be the default.\n\nFor security reasons, safe.bareRepository cannot be read from\nrepository-level config (because we would end up trusting the embedded bare\nrepository that we aren't supposed to trust to begin with). Since this would\nintroduce a 3rd variable that is only read from 'protected/trusted\nconfiguration' (the others are safe.directory and\nuploadpack.packObjectsHook) this series also defines and creates a shared\nimplementation for 'protected configuration'\n\n= Patch organization\n\n * Patch 1 add a section on configuration scopes to our docs\n * Patches 2-3 define 'protected configuration' and create a shared\n   implementation.\n * Patch 4 refactors safe.directory to use protected configuration\n * Patch 5 adds safe.bareRepository\n\n= Series history\n\nChanges in v8:\n\n * Rename discovery.bare -> safe.bareRepository, change values from\n   \"always|never\" -> \"all|explicit\"\n * Numerous docs improvements\n * Rebase onto post-2.37.1 master\n\nChanges in v7:\n\n * Numerous docs improvements and code cleanup.\n * In 3/5's commit message, drop \"as fast as possible\" and allude to lookup\n   functions coming in a later series.\n * Remove a comment in 3/5 about repository.protected_config. That was stale\n   since v4, but slipped under the radar until now.\n * Fix some s/protected config/protected configuration (leftover from v5).\n\nChanges in v6:\n\n * Add TEST_PASSES_SANITIZE_LEAK=true\n * Replace all sub-shells with -C and use test_config_global\n * Change the expect_rejected helper to use \"grep -F\" with a more specific\n   message.\n   * This reveals that the \"-c discovery.bare=\" assertion in the last test\n     was passing for the wrong reason (because '' is an invalid value for\n     \"discovery.bare\"). I removed it because it wasn't doing anything useful\n     anyway - I was trying to make discovery.bare unset in the command line,\n     but the whole point of that test is to assert that we respect the CLI\n     arg.\n\nChanges in v5:\n\n * Standardize the usage of \"protected configuration\" instead of mixing\n   \"config\" and \"configuration\". This required some unfortunate rewrapping.\n * Remove mentions of \"trustworthiness\" when discussing protected\n   configuration and focus on what Git does instead.\n   * The rationale of protected vs non-protected is still kept.\n * Fix the stale documentation entry for discovery.bare.\n * Include a fuller description of how discovery.bare and \"--git-dir\"\n   interact instead of saying \"has no effect\".\n\nChanges in v4:\n\n * 2/5's commit message now justifies what scopes are included in protected\n   config\n * The global configset is now a file-scope static inside config.c\n   (previously it was a member of the_repository).\n * Rename discovery_bare_config to discovery_bare_allowed\n * Make discovery_bare_allowed function-scoped (instead of global).\n * Add an expect_accepted helper to the discovery.bare tests.\n * Add a helper to \"upload-pack\" that reads the protected and non-protected\n   config\n\nChanges in v3:\n\n * Rebase onto a more recent 'master'\n * Reframe this feature in only in terms of the 'embedded bare repo' attack.\n * Other docs improvements (thanks Stolee in particular!)\n * Protected config no longer uses read_very_early_config() and is only read\n   once\n * Protected config now includes \"-c\"\n * uploadpack.packObjectsHook now uses protected config instead of ignoring\n   repo config using config scopes\n\nChanges in v2:\n\n * Rename safe.barerepository to discovery.bare and make it die()\n * Move tests into t/t0034-discovery-bare.sh\n * Avoid unnecessary config reading by using a static variable\n * Add discovery.bare=cwd\n * Fix typos\n\n= Future work\n\n * This series doesn't implement config lookup functions for protected\n   config. This will be done in a follow up series.\n * This series does not implement the \"no-embedded\" option [3] and I won't\n   work on it any time soon, but I'd be more than happy to review if someone\n   sends patches.\n * With discovery.bare, if a builtin is marked RUN_SETUP_GENTLY, setup.c\n   doesn't die() and we don't tell users why their repository was rejected,\n   e.g. \"git config\" gives an opaque \"fatal: not in a git directory\". This\n   isn't a new problem though, since safe.directory has the same issue.\n\n[1] https://lore.kernel.org/git/5ps2q552-1rr3-7161-4181-31556pp2ns12@tzk.qr\n\n[2]\nhttps://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n\n[3] This was first suggested in\nhttps://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n\nGlen Choo (5):\n  Documentation/git-config.txt: add SCOPES section\n  Documentation: define protected configuration\n  config: learn `git_protected_config()`\n  safe.directory: use git_protected_config()\n  setup.c: create `safe.bareRepository`\n\n Documentation/config/safe.txt       | 25 +++++++-\n Documentation/config/uploadpack.txt |  6 +-\n Documentation/git-config.txt        | 95 ++++++++++++++++++++++-------\n config.c                            | 43 +++++++++++++\n config.h                            | 16 +++++\n setup.c                             | 59 +++++++++++++++++-\n t/t0033-safe-directory.sh           | 24 +++-----\n t/t0035-safe-bare-repository.sh     | 54 ++++++++++++++++\n t/t5544-pack-objects-hook.sh        |  7 ++-\n upload-pack.c                       | 27 +++++---\n 10 files changed, 300 insertions(+), 56 deletions(-)\n create mode 100755 t/t0035-safe-bare-repository.sh\n\n\nbase-commit: 4e2a4d1dd44367d7783f33b169698f2930ff13c0\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1261%2Fchooglen%2Fsetup%2Fdisable-bare-repo-config-v8\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1261/chooglen/setup/disable-bare-repo-config-v8\nPull-Request: https://github.com/git/git/pull/1261\n\nRange-diff vs v7:\n\n 1:  5c58db3bb21 ! 1:  6147751c9c1 Documentation/git-config.txt: add SCOPES section\n     @@ Commit message\n          145d59f482 (config: add '--show-scope' to print the scope of a config\n          value, 2020-02-10) introduced the word \"scope\" to our public docs, but\n          that only enumerates the scopes and assumes the user can figure out\n     -    those values mean.\n     +    what those values mean.\n      \n          Add a SCOPES section to Documentation/git-config.txt that describes the\n          configuration scopes, their corresponding CLI options, and mentions that\n     @@ Documentation/git-config.txt: The default is to use a pager.\n       \n       $(prefix)/etc/gitconfig::\n       \tSystem-wide configuration file.\n     + \n     + $XDG_CONFIG_HOME/git/config::\n     +-\tSecond user-specific configuration file. If $XDG_CONFIG_HOME is not set\n     +-\tor empty, `$HOME/.config/git/config` will be used. Any single-valued\n     +-\tvariable set in this file will be overwritten by whatever is in\n     +-\t`~/.gitconfig`.  It is a good idea not to create this file if\n     +-\tyou sometimes use older versions of Git, as support for this\n     +-\tfile was added fairly recently.\n     +-\n     + ~/.gitconfig::\n     +-\tUser-specific configuration file. Also called \"global\"\n     +-\tconfiguration file.\n     ++\tUser-specific configuration files. When the XDG_CONFIG_HOME environment\n     ++\tvariable is not set or empty, $HOME/.config/ is used as\n     ++\t$XDG_CONFIG_HOME.\n     +++\n     ++These are also called \"global\" configuration files. If both files exist, both\n     ++files are read in the order given above.\n     + \n     + $GIT_DIR/config::\n     + \tRepository specific configuration file.\n      @@ Documentation/git-config.txt: $GIT_DIR/config.worktree::\n       \tThis is optional and is only searched when\n       \t`extensions.worktreeConfig` is present in $GIT_DIR/config.\n     @@ Documentation/git-config.txt: $GIT_DIR/config.worktree::\n      +git command by using the `-c` option. See linkgit:git[1] for details.\n      +\n      +Options will be read from all of these files that are available. If the\n     -+global or the system-wide configuration file are not available they will be\n     -+ignored. If the repository configuration file is not available or readable,\n     -+'git config' will exit with a non-zero error code. Note that neither case\n     -+produces an error message.\n     ++global or the system-wide configuration files are missing or unreadable they\n     ++will be ignored. If the repository configuration file is missing or unreadable,\n     ++'git config' will exit with a non-zero error code. An error message is produced\n     ++if the file is unreadable, but not if it is missing.\n       \n       The files are read in the order given above, with last value found taking\n       precedence over values read earlier.  When multiple values are taken then all\n     @@ Documentation/git-config.txt: $GIT_DIR/config.worktree::\n      +\t$GIT_DIR/config.worktree\n      +\n      +command::\n     -+\tenvironment variables\n     ++\tGIT_CONFIG_{COUNT,KEY,VALUE} environment variables (see <<ENVIRONMENT>>\n     ++\tbelow)\n      ++\n      +the `-c` option\n      +\n     @@ Documentation/git-config.txt: $GIT_DIR/config.worktree::\n      +defined in, but some options are only respected in certain scopes. See the\n      +respective option's documentation for the full details.\n       \n     ++[[ENVIRONMENT]]\n       ENVIRONMENT\n       -----------\n     + \n 2:  58f25612aa3 ! 2:  df8a1a78d53 Documentation: define protected configuration\n     @@ Commit message\n          - System, global and command line config are considered protected\n            because an attacker who has control over any of those can do plenty of\n            harm without Git, so we gain very little by ignoring those scopes.\n     +\n          - On the other hand, local (and similarly, worktree) config are not\n            considered protected because it is relatively easy for an attacker to\n            control local config, e.g.:\n     +\n            - On some shared user environments, a non-admin attacker can create a\n              repository high up the directory hierarchy (e.g. C:\\.git on\n              Windows), and a user may accidentally use it when their PS1\n     @@ Commit message\n              the user intended to use the shared repository. It obviously\n              shouldn't be read from the repository, because that would end up\n              trusting the repository that Git was supposed to reject.\n     +\n            - \"git upload-pack\" is expected to run in repositories that may not be\n              controlled by the user. We cannot ignore all config in that\n              repository (because \"git upload-pack\" would fail), but we can limit\n     @@ Documentation/git-config.txt: Most configuration options are respected regardles\n      +substantial harm without using Git, so it is assumed that the user's environment\n      +protects these scopes against attackers.\n      +\n     + [[ENVIRONMENT]]\n       ENVIRONMENT\n       -----------\n     - \n 3:  3683d20f232 ! 3:  30ac73716cb config: learn `git_protected_config()`\n     @@ Commit message\n      \n          `uploadpack.packObjectsHook` is the only 'protected configuration only'\n          variable today, but we've noted that `safe.directory` and the upcoming\n     -    `discovery.bare` should also be 'protected configuration only'. So, for\n     -    consistency, we'd like to have a single implementation for protected\n     +    `safe.bareRepository` should also be 'protected configuration only'. So,\n     +    for consistency, we'd like to have a single implementation for protected\n          configuration.\n      \n          The primary constraints are:\n      \n          1. Reading from protected configuration should be fast. Nearly all \"git\"\n             commands inside a bare repository will read both `safe.directory` and\n     -       `discovery.bare`, so we cannot afford to be slow.\n     +       `safe.bareRepository`, so we cannot afford to be slow.\n      \n          2. Protected configuration must be readable when the gitdir is not\n     -       known. `safe.directory` and `discovery.bare` both affect repository\n     -       discovery and the gitdir is not known at that point [1].\n     +       known. `safe.directory` and `safe.bareRepository` both affect\n     +       repository discovery and the gitdir is not known at that point [1].\n      \n          The chosen implementation in this commit is to read protected\n          configuration and cache the values in a global configset. This is\n 4:  6394818ffd8 ! 4:  b3256d68f84 safe.directory: use git_protected_config()\n     @@ Documentation/config/safe.txt: via `git config --add`. To reset the list of safe\n       path relative to the home directory and `%(prefix)/<path>` expands to a\n      \n       ## setup.c ##\n     -@@ setup.c: static int ensure_valid_ownership(const char *path)\n     - \t    is_path_owned_by_current_user(path))\n     - \t\treturn 1;\n     - \n     +@@ setup.c: static int ensure_valid_ownership(const char *gitfile,\n     + \t * constant regardless of what failed above. data.is_safe should be\n     + \t * initialized to false, and might be changed by the callback.\n     + \t */\n      -\tread_very_early_config(safe_directory_cb, &data);\n      +\tgit_protected_config(safe_directory_cb, &data);\n       \n     @@ t/t0033-safe-directory.sh: test_expect_success 'safe.directory is not set' '\n       \n      -test_expect_success 'ignoring safe.directory on the command line' '\n      -\ttest_must_fail git -c safe.directory=\"$(pwd)\" status 2>err &&\n     --\tgrep \"unsafe repository\" err\n     +-\tgrep \"dubious ownership\" err\n      +test_expect_success 'safe.directory on the command line' '\n      +\tgit -c safe.directory=\"$(pwd)\" status\n       '\n     @@ t/t0033-safe-directory.sh: test_expect_success 'safe.directory is not set' '\n      -\t\tGIT_CONFIG_KEY_0=\"safe.directory\" \\\n      -\t\tGIT_CONFIG_VALUE_0=\"$(pwd)\" \\\n      -\t\tgit status 2>err &&\n     --\tgrep \"unsafe repository\" err\n     +-\tgrep \"dubious ownership\" err\n      +test_expect_success 'safe.directory in the environment' '\n      +\tenv GIT_CONFIG_COUNT=1 \\\n      +\t    GIT_CONFIG_KEY_0=\"safe.directory\" \\\n     @@ t/t0033-safe-directory.sh: test_expect_success 'safe.directory is not set' '\n      -\ttest_must_fail env \\\n      -\t\tGIT_CONFIG_PARAMETERS=\"${SQ}safe.directory${SQ}=${SQ}$(pwd)${SQ}\" \\\n      -\t\tgit status 2>err &&\n     --\tgrep \"unsafe repository\" err\n     +-\tgrep \"dubious ownership\" err\n      +test_expect_success 'safe.directory in GIT_CONFIG_PARAMETERS' '\n      +\tenv GIT_CONFIG_PARAMETERS=\"${SQ}safe.directory${SQ}=${SQ}$(pwd)${SQ}\" \\\n      +\t    git status\n 5:  eff4b07480e ! 5:  50069bba9a5 setup.c: create `discovery.bare`\n     @@ Metadata\n      Author: Glen Choo <chooglen@google.com>\n      \n       ## Commit message ##\n     -    setup.c: create `discovery.bare`\n     +    setup.c: create `safe.bareRepository`\n      \n          There is a known social engineering attack that takes advantage of the\n          fact that a working tree can include an entire bare repository,\n     @@ Commit message\n          `--git-dir` or `GIT_DIR`. In environments that don't use bare\n          repositories, this would be minimally disruptive.\n      \n     -    Create a config variable, `discovery.bare`, that tells Git whether or\n     -    not to die() when it discovers a bare repository. This only affects\n     -    repository discovery, thus it has no effect if discovery was not\n     -    done, e.g. if the user passes `--git-dir=my-dir`, discovery will be\n     -    skipped and my-dir will be used as the repo regardless of the\n     -    `discovery.bare` value.\n     +    Create a config variable, `safe.bareRepository`, that tells Git whether\n     +    or not to die() when working with a bare repository. This config is an\n     +    enum of:\n      \n     -    This config is an enum of:\n     -\n     -    - \"always\": always allow bare repositories (this is the default)\n     -    - \"never\": never allow bare repositories\n     +    - \"all\": allow all bare repositories (this is the default)\n     +    - \"explicit\": only allow bare repositories specified via --git-dir\n     +      or GIT_DIR.\n      \n          If we want to protect users from such attacks by default, neither value\n     -    will suffice - \"always\" provides no protection, but \"never\" is\n     +    will suffice - \"all\" provides no protection, but \"explicit\" is\n          impractical for bare repository users. A more usable default would be to\n          allow only non-embedded bare repositories ([2] contains one such\n          proposal), but detecting if a repository is embedded is potentially\n     @@ Commit message\n      \n          Signed-off-by: Glen Choo <chooglen@google.com>\n      \n     - ## Documentation/config.txt ##\n     -@@ Documentation/config.txt: include::config/diff.txt[]\n     - \n     - include::config/difftool.txt[]\n     - \n     -+include::config/discovery.txt[]\n     -+\n     - include::config/extensions.txt[]\n     - \n     - include::config/fastimport.txt[]\n     -\n     - ## Documentation/config/discovery.txt (new) ##\n     + ## Documentation/config/safe.txt ##\n      @@\n     -+discovery.bare::\n     -+\tSpecifies whether Git will work with a bare repository that\n     -+\twasn't specified via the top-level `--git-dir` command-line\n     -+\toption, or the `GIT_DIR` environment variable (see\n     -+\tlinkgit:git[1]). If the repository is specified, Git will always\n     -+\tuse the specified repository, regardless of this value.\n     ++safe.bareRepository::\n     ++\tSpecifies which bare repositories Git will work with. The currently\n     ++\tsupported values are:\n     +++\n     ++* `all`: Git works with all bare repositories. This is the default.\n     ++* `explicit`: Git only works with bare repositories specified via\n     ++  the top-level `--git-dir` command-line option, or the `GIT_DIR`\n     ++  environment variable (see linkgit:git[1]).\n     +++\n     ++If you do not use bare repositories in your workflow, then it may be\n     ++beneficial to set `safe.bareRepository` to `explicit` in your global\n     ++config. This will protect you from attacks that involve cloning a\n     ++repository that contains a bare repository and running a Git command\n     ++within that directory.\n      ++\n      +This config setting is only respected in protected configuration (see\n      +<<SCOPES>>). This prevents the untrusted repository from tampering with\n      +this value.\n     -++\n     -+The currently supported values are:\n     -++\n     -+* `always`: Git always works with bare repositories\n     -+* `never`: Git never works with bare repositories\n     -++\n     -+If you do not use bare repositories in your workflow, then it may be\n     -+beneficial to set `discovery.bare` to `never` in your global config.\n     -+This will protect you from attacks that involve cloning a repository\n     -+that contains a bare repository and running a Git command within that\n     -+directory.\n     ++\n     + safe.directory::\n     + \tThese config entries specify Git-tracked directories that are\n     + \tconsidered safe even if they are owned by someone other than the\n      \n       ## setup.c ##\n      @@\n       static int inside_git_dir = -1;\n       static int inside_work_tree = -1;\n       static int work_tree_config_is_bogus;\n     -+enum discovery_bare_allowed {\n     -+\tDISCOVERY_BARE_NEVER = 0,\n     -+\tDISCOVERY_BARE_ALWAYS,\n     ++enum allowed_bare_repo {\n     ++\tALLOWED_BARE_REPO_EXPLICIT = 0,\n     ++\tALLOWED_BARE_REPO_ALL,\n      +};\n       \n       static struct startup_info the_startup_info;\n       struct startup_info *startup_info = &the_startup_info;\n     -@@ setup.c: static int ensure_valid_ownership(const char *path)\n     +@@ setup.c: static int ensure_valid_ownership(const char *gitfile,\n       \treturn data.is_safe;\n       }\n       \n     -+static int discovery_bare_cb(const char *key, const char *value, void *d)\n     ++static int allowed_bare_repo_cb(const char *key, const char *value, void *d)\n      +{\n     -+\tenum discovery_bare_allowed *discovery_bare_allowed = d;\n     ++\tenum allowed_bare_repo *allowed_bare_repo = d;\n      +\n     -+\tif (strcmp(key, \"discovery.bare\"))\n     ++\tif (strcasecmp(key, \"safe.bareRepository\"))\n      +\t\treturn 0;\n      +\n     -+\tif (!strcmp(value, \"never\")) {\n     -+\t\t*discovery_bare_allowed = DISCOVERY_BARE_NEVER;\n     ++\tif (!strcmp(value, \"explicit\")) {\n     ++\t\t*allowed_bare_repo = ALLOWED_BARE_REPO_EXPLICIT;\n      +\t\treturn 0;\n      +\t}\n     -+\tif (!strcmp(value, \"always\")) {\n     -+\t\t*discovery_bare_allowed = DISCOVERY_BARE_ALWAYS;\n     ++\tif (!strcmp(value, \"all\")) {\n     ++\t\t*allowed_bare_repo = ALLOWED_BARE_REPO_ALL;\n      +\t\treturn 0;\n      +\t}\n      +\treturn -1;\n      +}\n      +\n     -+static enum discovery_bare_allowed get_discovery_bare(void)\n     ++static enum allowed_bare_repo get_allowed_bare_repo(void)\n      +{\n     -+\tenum discovery_bare_allowed result = DISCOVERY_BARE_ALWAYS;\n     -+\tgit_protected_config(discovery_bare_cb, &result);\n     ++\tenum allowed_bare_repo result = ALLOWED_BARE_REPO_ALL;\n     ++\tgit_protected_config(allowed_bare_repo_cb, &result);\n      +\treturn result;\n      +}\n      +\n     -+static const char *discovery_bare_allowed_to_string(\n     -+\tenum discovery_bare_allowed discovery_bare_allowed)\n     ++static const char *allowed_bare_repo_to_string(\n     ++\tenum allowed_bare_repo allowed_bare_repo)\n      +{\n     -+\tswitch (discovery_bare_allowed) {\n     -+\tcase DISCOVERY_BARE_NEVER:\n     -+\t\treturn \"never\";\n     -+\tcase DISCOVERY_BARE_ALWAYS:\n     -+\t\treturn \"always\";\n     ++\tswitch (allowed_bare_repo) {\n     ++\tcase ALLOWED_BARE_REPO_EXPLICIT:\n     ++\t\treturn \"explicit\";\n     ++\tcase ALLOWED_BARE_REPO_ALL:\n     ++\t\treturn \"all\";\n      +\tdefault:\n     -+\t\tBUG(\"invalid discovery_bare_allowed %d\",\n     -+\t\t    discovery_bare_allowed);\n     ++\t\tBUG(\"invalid allowed_bare_repo %d\",\n     ++\t\t    allowed_bare_repo);\n      +\t}\n      +\treturn NULL;\n      +}\n     @@ setup.c: static enum discovery_result setup_git_directory_gently_1(struct strbuf\n       \t\t}\n       \n       \t\tif (is_git_directory(dir->buf)) {\n     -+\t\t\tif (get_discovery_bare() == DISCOVERY_BARE_NEVER)\n     ++\t\t\tif (get_allowed_bare_repo() == ALLOWED_BARE_REPO_EXPLICIT)\n      +\t\t\t\treturn GIT_DIR_DISALLOWED_BARE;\n     - \t\t\tif (!ensure_valid_ownership(dir->buf))\n     + \t\t\tif (!ensure_valid_ownership(NULL, NULL, dir->buf))\n       \t\t\t\treturn GIT_DIR_INVALID_OWNERSHIP;\n       \t\t\tstrbuf_addstr(gitdir, \".\");\n      @@ setup.c: const char *setup_git_directory_gently(int *nongit_ok)\n     @@ setup.c: const char *setup_git_directory_gently(int *nongit_ok)\n       \t\tbreak;\n      +\tcase GIT_DIR_DISALLOWED_BARE:\n      +\t\tif (!nongit_ok) {\n     -+\t\t\tdie(_(\"cannot use bare repository '%s' (discovery.bare is '%s')\"),\n     ++\t\t\tdie(_(\"cannot use bare repository '%s' (safe.bareRepository is '%s')\"),\n      +\t\t\t    dir.buf,\n     -+\t\t\t    discovery_bare_allowed_to_string(get_discovery_bare()));\n     ++\t\t\t    allowed_bare_repo_to_string(get_allowed_bare_repo()));\n      +\t\t}\n      +\t\t*nongit_ok = 1;\n      +\t\tbreak;\n     @@ setup.c: const char *setup_git_directory_gently(int *nongit_ok)\n       \t\t/*\n       \t\t * As a safeguard against setup_git_directory_gently_1 returning\n      \n     - ## t/t0035-discovery-bare.sh (new) ##\n     + ## t/t0035-safe-bare-repository.sh (new) ##\n      @@\n      +#!/bin/sh\n      +\n     -+test_description='verify discovery.bare checks'\n     ++test_description='verify safe.bareRepository checks'\n      +\n      +TEST_PASSES_SANITIZE_LEAK=true\n      +. ./test-lib.sh\n     @@ t/t0035-discovery-bare.sh (new)\n      +\tgit init --bare outer-repo/bare-repo\n      +'\n      +\n     -+test_expect_success 'discovery.bare unset' '\n     ++test_expect_success 'safe.bareRepository unset' '\n      +\texpect_accepted -C outer-repo/bare-repo\n      +'\n      +\n     -+test_expect_success 'discovery.bare=always' '\n     -+\ttest_config_global discovery.bare always &&\n     ++test_expect_success 'safe.bareRepository=all' '\n     ++\ttest_config_global safe.bareRepository all &&\n      +\texpect_accepted -C outer-repo/bare-repo\n      +'\n      +\n     -+test_expect_success 'discovery.bare=never' '\n     -+\ttest_config_global discovery.bare never &&\n     ++test_expect_success 'safe.bareRepository=explicit' '\n     ++\ttest_config_global safe.bareRepository explicit &&\n      +\texpect_rejected -C outer-repo/bare-repo\n      +'\n      +\n     -+test_expect_success 'discovery.bare in the repository' '\n     -+\t# discovery.bare must not be \"never\", otherwise git config fails\n     -+\t# with \"fatal: not in a git directory\" (like safe.directory)\n     -+\ttest_config -C outer-repo/bare-repo discovery.bare always &&\n     -+\ttest_config_global discovery.bare never &&\n     ++test_expect_success 'safe.bareRepository in the repository' '\n     ++\t# safe.bareRepository must not be \"explicit\", otherwise\n     ++\t# git config fails with \"fatal: not in a git directory\" (like\n     ++\t# safe.directory)\n     ++\ttest_config -C outer-repo/bare-repo safe.bareRepository \\\n     ++\t\tall &&\n     ++\ttest_config_global safe.bareRepository explicit &&\n      +\texpect_rejected -C outer-repo/bare-repo\n      +'\n      +\n     -+test_expect_success 'discovery.bare on the command line' '\n     -+\ttest_config_global discovery.bare never &&\n     ++test_expect_success 'safe.bareRepository on the command line' '\n     ++\ttest_config_global safe.bareRepository explicit &&\n      +\texpect_accepted -C outer-repo/bare-repo \\\n     -+\t\t-c discovery.bare=always\n     ++\t\t-c safe.bareRepository=all\n      +'\n      +\n      +test_done\n\n-- \ngitgitgadget\n"},{"id":"459088","messageId":"6147751c9c13cba5afbf72e1107fdfeef91edcaa.1657834081.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v8.git.git.1657834081.gitgitgadget@gmail.com","subject":"[PATCH v8 1/5] Documentation/git-config.txt: add SCOPES section","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-07-14T21:27:57Z","receivedAt":"2022-07-14T21:28:13Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nIn a subsequent commit, we will introduce \"protected configuration\",\nwhich is easiest to describe in terms of configuration scopes (i.e. it's\nthe union of the 'system', 'global', and 'command' scopes). This\ndescription is fine for ML discussions, but it's inadequate for end\nusers because we don't provide a good description of \"configuration\nscopes\" in the public docs.\n\n145d59f482 (config: add '--show-scope' to print the scope of a config\nvalue, 2020-02-10) introduced the word \"scope\" to our public docs, but\nthat only enumerates the scopes and assumes the user can figure out\nwhat those values mean.\n\nAdd a SCOPES section to Documentation/git-config.txt that describes the\nconfiguration scopes, their corresponding CLI options, and mentions that\nsome configuration options are only respected in certain scopes. Then,\nuse the word \"scope\" to simplify the FILES section and change some\nconfusing wording.\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/git-config.txt | 82 ++++++++++++++++++++++++++----------\n 1 file changed, 59 insertions(+), 23 deletions(-)\n\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex 9376e39aef2..53c7c65f9ed 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -297,23 +297,20 @@ The default is to use a pager.\n FILES\n -----\n \n-If not set explicitly with `--file`, there are four files where\n-'git config' will search for configuration options:\n+By default, 'git config' will read configuration options from multiple\n+files:\n \n $(prefix)/etc/gitconfig::\n \tSystem-wide configuration file.\n \n $XDG_CONFIG_HOME/git/config::\n-\tSecond user-specific configuration file. If $XDG_CONFIG_HOME is not set\n-\tor empty, `$HOME/.config/git/config` will be used. Any single-valued\n-\tvariable set in this file will be overwritten by whatever is in\n-\t`~/.gitconfig`.  It is a good idea not to create this file if\n-\tyou sometimes use older versions of Git, as support for this\n-\tfile was added fairly recently.\n-\n ~/.gitconfig::\n-\tUser-specific configuration file. Also called \"global\"\n-\tconfiguration file.\n+\tUser-specific configuration files. When the XDG_CONFIG_HOME environment\n+\tvariable is not set or empty, $HOME/.config/ is used as\n+\t$XDG_CONFIG_HOME.\n++\n+These are also called \"global\" configuration files. If both files exist, both\n+files are read in the order given above.\n \n $GIT_DIR/config::\n \tRepository specific configuration file.\n@@ -322,28 +319,67 @@ $GIT_DIR/config.worktree::\n \tThis is optional and is only searched when\n \t`extensions.worktreeConfig` is present in $GIT_DIR/config.\n \n-If no further options are given, all reading options will read all of these\n-files that are available. If the global or the system-wide configuration\n-file are not available they will be ignored. If the repository configuration\n-file is not available or readable, 'git config' will exit with a non-zero\n-error code. However, in neither case will an error message be issued.\n+You may also provide additional configuration parameters when running any\n+git command by using the `-c` option. See linkgit:git[1] for details.\n+\n+Options will be read from all of these files that are available. If the\n+global or the system-wide configuration files are missing or unreadable they\n+will be ignored. If the repository configuration file is missing or unreadable,\n+'git config' will exit with a non-zero error code. An error message is produced\n+if the file is unreadable, but not if it is missing.\n \n The files are read in the order given above, with last value found taking\n precedence over values read earlier.  When multiple values are taken then all\n values of a key from all files will be used.\n \n-You may override individual configuration parameters when running any git\n-command by using the `-c` option. See linkgit:git[1] for details.\n-\n-All writing options will per default write to the repository specific\n+By default, options are only written to the repository specific\n configuration file. Note that this also affects options like `--replace-all`\n and `--unset`. *'git config' will only ever change one file at a time*.\n \n-You can override these rules using the `--global`, `--system`,\n-`--local`, `--worktree`, and `--file` command-line options; see\n-<<OPTIONS>> above.\n+You can limit which configuration sources are read from or written to by\n+specifying the path of a file with the `--file` option, or by specifying a\n+configuration scope with `--system`, `--global`, `--local`, or `--worktree`.\n+For more, see <<OPTIONS>> above.\n+\n+SCOPES\n+------\n+\n+Each configuration source falls within a configuration scope. The scopes\n+are:\n+\n+system::\n+\t$(prefix)/etc/gitconfig\n+\n+global::\n+\t$XDG_CONFIG_HOME/git/config\n++\n+~/.gitconfig\n+\n+local::\n+\t$GIT_DIR/config\n+\n+worktree::\n+\t$GIT_DIR/config.worktree\n+\n+command::\n+\tGIT_CONFIG_{COUNT,KEY,VALUE} environment variables (see <<ENVIRONMENT>>\n+\tbelow)\n++\n+the `-c` option\n+\n+With the exception of 'command', each scope corresponds to a command line\n+option: `--system`, `--global`, `--local`, `--worktree`.\n+\n+When reading options, specifying a scope will only read options from the\n+files within that scope. When writing options, specifying a scope will write\n+to the files within that scope (instead of the repository specific\n+configuration file). See <<OPTIONS>> above for a complete description.\n \n+Most configuration options are respected regardless of the scope it is\n+defined in, but some options are only respected in certain scopes. See the\n+respective option's documentation for the full details.\n \n+[[ENVIRONMENT]]\n ENVIRONMENT\n -----------\n \n-- \ngitgitgadget\n\n"},{"id":"459089","messageId":"df8a1a78d533924b88f9513df3f328738b1589bc.1657834081.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v8.git.git.1657834081.gitgitgadget@gmail.com","subject":"[PATCH v8 2/5] Documentation: define protected configuration","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-07-14T21:27:58Z","receivedAt":"2022-07-14T21:28:24Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nFor security reasons, there are config variables that are only trusted\nwhen they are specified in certain configuration scopes, which are\nsometimes referred to on-list as 'protected configuration' [1]. A future\ncommit will introduce another such variable, so let's define our terms\nso that we can have consistent documentation and implementation.\n\nIn our documentation, define 'protected configuration' as the system,\nglobal and command config scopes. As a shorthand, I will refer to\nvariables that are only respected in protected configuration as\n'protected configuration only', but this term is not used in the\ndocumentation.\n\nThis definition of protected configuration is based on whether or not\nGit can reasonably protect the user by ignoring the configuration scope:\n\n- System, global and command line config are considered protected\n  because an attacker who has control over any of those can do plenty of\n  harm without Git, so we gain very little by ignoring those scopes.\n\n- On the other hand, local (and similarly, worktree) config are not\n  considered protected because it is relatively easy for an attacker to\n  control local config, e.g.:\n\n  - On some shared user environments, a non-admin attacker can create a\n    repository high up the directory hierarchy (e.g. C:\\.git on\n    Windows), and a user may accidentally use it when their PS1\n    automatically invokes \"git\" commands.\n\n    `safe.directory` prevents attacks of this form by making sure that\n    the user intended to use the shared repository. It obviously\n    shouldn't be read from the repository, because that would end up\n    trusting the repository that Git was supposed to reject.\n\n  - \"git upload-pack\" is expected to run in repositories that may not be\n    controlled by the user. We cannot ignore all config in that\n    repository (because \"git upload-pack\" would fail), but we can limit\n    the risks by ignoring `uploadpack.packObjectsHook`.\n\nOnly `uploadpack.packObjectsHook` is 'protected configuration only'. The\nfollowing variables are intentionally excluded:\n\n- `safe.directory` should be 'protected configuration only', but it does\n  not technically fit the definition because it is not respected in the\n  \"command\" scope. A future commit will fix this.\n\n- `trace2.*` happens to read the same scopes as `safe.directory` because\n  they share an implementation. However, this is not for security\n  reasons; it is because we want to start tracing so early that\n  repository-level config and \"-c\" are not available [2].\n\n  This requirement is unique to `trace2.*`, so it does not makes sense\n  for protected configuration to be subject to the same constraints.\n\n[1] For example,\nhttps://lore.kernel.org/git/6af83767-576b-75c4-c778-0284344a8fe7@github.com/\n[2] https://lore.kernel.org/git/a0c89d0d-669e-bf56-25d2-cbb09b012e70@jeffhostetler.com/\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config/uploadpack.txt |  6 +++---\n Documentation/git-config.txt        | 13 +++++++++++++\n 2 files changed, 16 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/config/uploadpack.txt b/Documentation/config/uploadpack.txt\nindex 32fad5bbe81..16264d82a72 100644\n--- a/Documentation/config/uploadpack.txt\n+++ b/Documentation/config/uploadpack.txt\n@@ -49,9 +49,9 @@ uploadpack.packObjectsHook::\n \t`pack-objects` to the hook, and expects a completed packfile on\n \tstdout.\n +\n-Note that this configuration variable is ignored if it is seen in the\n-repository-level config (this is a safety measure against fetching from\n-untrusted repositories).\n+Note that this configuration variable is only respected when it is specified\n+in protected configuration (see <<SCOPES>>). This is a safety measure\n+against fetching from untrusted repositories.\n \n uploadpack.allowFilter::\n \tIf this option is set, `upload-pack` will support partial\ndiff --git a/Documentation/git-config.txt b/Documentation/git-config.txt\nindex 53c7c65f9ed..7a2bcb2f6cb 100644\n--- a/Documentation/git-config.txt\n+++ b/Documentation/git-config.txt\n@@ -341,6 +341,7 @@ specifying the path of a file with the `--file` option, or by specifying a\n configuration scope with `--system`, `--global`, `--local`, or `--worktree`.\n For more, see <<OPTIONS>> above.\n \n+[[SCOPES]]\n SCOPES\n ------\n \n@@ -379,6 +380,18 @@ Most configuration options are respected regardless of the scope it is\n defined in, but some options are only respected in certain scopes. See the\n respective option's documentation for the full details.\n \n+Protected configuration\n+~~~~~~~~~~~~~~~~~~~~~~~\n+\n+Protected configuration refers to the 'system', 'global', and 'command' scopes.\n+For security reasons, certain options are only respected when they are\n+specified in protected configuration, and ignored otherwise.\n+\n+Git treats these scopes as if they are controlled by the user or a trusted\n+administrator. This is because an attacker who controls these scopes can do\n+substantial harm without using Git, so it is assumed that the user's environment\n+protects these scopes against attackers.\n+\n [[ENVIRONMENT]]\n ENVIRONMENT\n -----------\n-- \ngitgitgadget\n\n"},{"id":"459090","messageId":"30ac73716cbc234a1f176d2d417bf0e2b0b335cf.1657834081.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v8.git.git.1657834081.gitgitgadget@gmail.com","subject":"[PATCH v8 3/5] config: learn `git_protected_config()`","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-07-14T21:27:59Z","receivedAt":"2022-07-14T21:28:25Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\n`uploadpack.packObjectsHook` is the only 'protected configuration only'\nvariable today, but we've noted that `safe.directory` and the upcoming\n`safe.bareRepository` should also be 'protected configuration only'. So,\nfor consistency, we'd like to have a single implementation for protected\nconfiguration.\n\nThe primary constraints are:\n\n1. Reading from protected configuration should be fast. Nearly all \"git\"\n   commands inside a bare repository will read both `safe.directory` and\n   `safe.bareRepository`, so we cannot afford to be slow.\n\n2. Protected configuration must be readable when the gitdir is not\n   known. `safe.directory` and `safe.bareRepository` both affect\n   repository discovery and the gitdir is not known at that point [1].\n\nThe chosen implementation in this commit is to read protected\nconfiguration and cache the values in a global configset. This is\nsimilar to the caching behavior we get with the_repository->config.\n\nIntroduce git_protected_config(), which reads protected configuration\nand caches them in the global configset protected_config. Then, refactor\n`uploadpack.packObjectsHook` to use git_protected_config().\n\nThe protected configuration functions are named similarly to their\nnon-protected counterparts, e.g. git_protected_config_check_init() vs\ngit_config_check_init().\n\nIn light of constraint 1, this implementation can still be improved.\ngit_protected_config() iterates through every variable in\nprotected_config, which is wasteful, but it makes the conversion simple\nbecause it matches existing patterns. We will likely implement constant\ntime lookup functions for protected configuration in a future series\n(such functions already exist for non-protected configuration, i.e.\nrepo_config_get_*()).\n\nAn alternative that avoids introducing another configset is to continue\nto read all config using git_config(), but only accept values that have\nthe correct config scope [2]. This technically fulfills constraint 2,\nbecause git_config() simply ignores the local and worktree config when\nthe gitdir is not known. However, this would read incomplete config into\nthe_repository->config, which would need to be reset when the gitdir is\nknown and git_config() needs to read the local and worktree config.\nResetting the_repository->config might be reasonable while we only have\nthese 'protected configuration only' variables, but it's not clear\nwhether this extends well to future variables.\n\n[1] In this case, we do have a candidate gitdir though, so with a little\nrefactoring, it might be possible to provide a gitdir.\n[2] This is how `uploadpack.packObjectsHook` was implemented prior to\nthis commit.\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n config.c                     | 43 ++++++++++++++++++++++++++++++++++++\n config.h                     | 16 ++++++++++++++\n t/t5544-pack-objects-hook.sh |  7 +++++-\n upload-pack.c                | 27 +++++++++++++---------\n 4 files changed, 82 insertions(+), 11 deletions(-)\n\ndiff --git a/config.c b/config.c\nindex 9b0e9c93285..015bec360f5 100644\n--- a/config.c\n+++ b/config.c\n@@ -81,6 +81,17 @@ static enum config_scope current_parsing_scope;\n static int pack_compression_seen;\n static int zlib_compression_seen;\n \n+/*\n+ * Config that comes from trusted scopes, namely:\n+ * - CONFIG_SCOPE_SYSTEM (e.g. /etc/gitconfig)\n+ * - CONFIG_SCOPE_GLOBAL (e.g. $HOME/.gitconfig, $XDG_CONFIG_HOME/git)\n+ * - CONFIG_SCOPE_COMMAND (e.g. \"-c\" option, environment variables)\n+ *\n+ * This is declared here for code cleanliness, but unlike the other\n+ * static variables, this does not hold config parser state.\n+ */\n+static struct config_set protected_config;\n+\n static int config_file_fgetc(struct config_source *conf)\n {\n \treturn getc_unlocked(conf->u.file);\n@@ -2378,6 +2389,11 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n \treturn git_config_from_file(config_set_callback, filename, cs);\n }\n \n+int git_configset_add_parameters(struct config_set *cs)\n+{\n+\treturn git_config_from_parameters(config_set_callback, cs);\n+}\n+\n int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n {\n \tconst struct string_list *values = NULL;\n@@ -2619,6 +2635,33 @@ int repo_config_get_pathname(struct repository *repo,\n \treturn ret;\n }\n \n+/* Read values into protected_config. */\n+static void read_protected_config(void)\n+{\n+\tchar *xdg_config = NULL, *user_config = NULL, *system_config = NULL;\n+\n+\tgit_configset_init(&protected_config);\n+\n+\tsystem_config = git_system_config();\n+\tgit_global_config(&user_config, &xdg_config);\n+\n+\tgit_configset_add_file(&protected_config, system_config);\n+\tgit_configset_add_file(&protected_config, xdg_config);\n+\tgit_configset_add_file(&protected_config, user_config);\n+\tgit_configset_add_parameters(&protected_config);\n+\n+\tfree(system_config);\n+\tfree(xdg_config);\n+\tfree(user_config);\n+}\n+\n+void git_protected_config(config_fn_t fn, void *data)\n+{\n+\tif (!protected_config.hash_initialized)\n+\t\tread_protected_config();\n+\tconfigset_iter(&protected_config, fn, data);\n+}\n+\n /* Functions used historically to read configuration from 'the_repository' */\n void git_config(config_fn_t fn, void *data)\n {\ndiff --git a/config.h b/config.h\nindex 7654f61c634..ca994d77147 100644\n--- a/config.h\n+++ b/config.h\n@@ -446,6 +446,15 @@ void git_configset_init(struct config_set *cs);\n  */\n int git_configset_add_file(struct config_set *cs, const char *filename);\n \n+/**\n+ * Parses command line options and environment variables, and adds the\n+ * variable-value pairs to the `config_set`. Returns 0 on success, or -1\n+ * if there is an error in parsing. The caller decides whether to free\n+ * the incomplete configset or continue using it when the function\n+ * returns -1.\n+ */\n+int git_configset_add_parameters(struct config_set *cs);\n+\n /**\n  * Finds and returns the value list, sorted in order of increasing priority\n  * for the configuration variable `key` and config set `cs`. When the\n@@ -505,6 +514,13 @@ int repo_config_get_maybe_bool(struct repository *repo,\n int repo_config_get_pathname(struct repository *repo,\n \t\t\t     const char *key, const char **dest);\n \n+/*\n+ * Functions for reading protected config. By definition, protected\n+ * config ignores repository config, so these do not take a `struct\n+ * repository` parameter.\n+ */\n+void git_protected_config(config_fn_t fn, void *data);\n+\n /**\n  * Querying For Specific Variables\n  * -------------------------------\ndiff --git a/t/t5544-pack-objects-hook.sh b/t/t5544-pack-objects-hook.sh\nindex dd5f44d986f..54f54f8d2eb 100755\n--- a/t/t5544-pack-objects-hook.sh\n+++ b/t/t5544-pack-objects-hook.sh\n@@ -56,7 +56,12 @@ test_expect_success 'hook does not run from repo config' '\n \t! grep \"hook running\" stderr &&\n \ttest_path_is_missing .git/hook.args &&\n \ttest_path_is_missing .git/hook.stdin &&\n-\ttest_path_is_missing .git/hook.stdout\n+\ttest_path_is_missing .git/hook.stdout &&\n+\n+\t# check that global config is used instead\n+\ttest_config_global uploadpack.packObjectsHook ./hook &&\n+\tgit clone --no-local . dst2.git 2>stderr &&\n+\tgrep \"hook running\" stderr\n '\n \n test_expect_success 'hook works with partial clone' '\ndiff --git a/upload-pack.c b/upload-pack.c\nindex 3a851b36066..09f48317b02 100644\n--- a/upload-pack.c\n+++ b/upload-pack.c\n@@ -1321,18 +1321,27 @@ static int upload_pack_config(const char *var, const char *value, void *cb_data)\n \t\tdata->advertise_sid = git_config_bool(var, value);\n \t}\n \n-\tif (current_config_scope() != CONFIG_SCOPE_LOCAL &&\n-\t    current_config_scope() != CONFIG_SCOPE_WORKTREE) {\n-\t\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n-\t\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n-\t}\n-\n \tif (parse_object_filter_config(var, value, data) < 0)\n \t\treturn -1;\n \n \treturn parse_hide_refs_config(var, value, \"uploadpack\");\n }\n \n+static int upload_pack_protected_config(const char *var, const char *value, void *cb_data)\n+{\n+\tstruct upload_pack_data *data = cb_data;\n+\n+\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n+\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n+\treturn 0;\n+}\n+\n+static void get_upload_pack_config(struct upload_pack_data *data)\n+{\n+\tgit_config(upload_pack_config, data);\n+\tgit_protected_config(upload_pack_protected_config, data);\n+}\n+\n void upload_pack(const int advertise_refs, const int stateless_rpc,\n \t\t const int timeout)\n {\n@@ -1340,8 +1349,7 @@ void upload_pack(const int advertise_refs, const int stateless_rpc,\n \tstruct upload_pack_data data;\n \n \tupload_pack_data_init(&data);\n-\n-\tgit_config(upload_pack_config, &data);\n+\tget_upload_pack_config(&data);\n \n \tdata.stateless_rpc = stateless_rpc;\n \tdata.timeout = timeout;\n@@ -1695,8 +1703,7 @@ int upload_pack_v2(struct repository *r, struct packet_reader *request)\n \n \tupload_pack_data_init(&data);\n \tdata.use_sideband = LARGE_PACKET_MAX;\n-\n-\tgit_config(upload_pack_config, &data);\n+\tget_upload_pack_config(&data);\n \n \twhile (state != FETCH_DONE) {\n \t\tswitch (state) {\n-- \ngitgitgadget\n\n"},{"id":"459091","messageId":"b3256d68f8425d8b16ccb7deedfdb02118e26f5b.1657834081.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v8.git.git.1657834081.gitgitgadget@gmail.com","subject":"[PATCH v8 4/5] safe.directory: use git_protected_config()","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-07-14T21:28:00Z","receivedAt":"2022-07-14T21:28:27Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nUse git_protected_config() to read `safe.directory` instead of\nread_very_early_config(), making it 'protected configuration only'.\n\nAs a result, `safe.directory` now respects \"-c\", so update the tests and\ndocs accordingly. It used to ignore \"-c\" due to how it was implemented,\nnot because of security or correctness concerns [1].\n\n[1] https://lore.kernel.org/git/xmqqlevabcsu.fsf@gitster.g/\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config/safe.txt |  6 +++---\n setup.c                       |  2 +-\n t/t0033-safe-directory.sh     | 24 ++++++++++--------------\n 3 files changed, 14 insertions(+), 18 deletions(-)\n\ndiff --git a/Documentation/config/safe.txt b/Documentation/config/safe.txt\nindex fa02f3ccc54..f72b4408798 100644\n--- a/Documentation/config/safe.txt\n+++ b/Documentation/config/safe.txt\n@@ -12,9 +12,9 @@ via `git config --add`. To reset the list of safe directories (e.g. to\n override any such directories specified in the system config), add a\n `safe.directory` entry with an empty value.\n +\n-This config setting is only respected when specified in a system or global\n-config, not when it is specified in a repository config, via the command\n-line option `-c safe.directory=<path>`, or in environment variables.\n+This config setting is only respected in protected configuration (see\n+<<SCOPES>>). This prevents the untrusted repository from tampering with this\n+value.\n +\n The value of this setting is interpolated, i.e. `~/<path>` expands to a\n path relative to the home directory and `%(prefix)/<path>` expands to a\ndiff --git a/setup.c b/setup.c\nindex 09b6549ba9e..ec5b9139e32 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1155,7 +1155,7 @@ static int ensure_valid_ownership(const char *gitfile,\n \t * constant regardless of what failed above. data.is_safe should be\n \t * initialized to false, and might be changed by the callback.\n \t */\n-\tread_very_early_config(safe_directory_cb, &data);\n+\tgit_protected_config(safe_directory_cb, &data);\n \n \treturn data.is_safe;\n }\ndiff --git a/t/t0033-safe-directory.sh b/t/t0033-safe-directory.sh\nindex 3908597d42d..f4d737dadd0 100755\n--- a/t/t0033-safe-directory.sh\n+++ b/t/t0033-safe-directory.sh\n@@ -16,24 +16,20 @@ test_expect_success 'safe.directory is not set' '\n \texpect_rejected_dir\n '\n \n-test_expect_success 'ignoring safe.directory on the command line' '\n-\ttest_must_fail git -c safe.directory=\"$(pwd)\" status 2>err &&\n-\tgrep \"dubious ownership\" err\n+test_expect_success 'safe.directory on the command line' '\n+\tgit -c safe.directory=\"$(pwd)\" status\n '\n \n-test_expect_success 'ignoring safe.directory in the environment' '\n-\ttest_must_fail env GIT_CONFIG_COUNT=1 \\\n-\t\tGIT_CONFIG_KEY_0=\"safe.directory\" \\\n-\t\tGIT_CONFIG_VALUE_0=\"$(pwd)\" \\\n-\t\tgit status 2>err &&\n-\tgrep \"dubious ownership\" err\n+test_expect_success 'safe.directory in the environment' '\n+\tenv GIT_CONFIG_COUNT=1 \\\n+\t    GIT_CONFIG_KEY_0=\"safe.directory\" \\\n+\t    GIT_CONFIG_VALUE_0=\"$(pwd)\" \\\n+\t    git status\n '\n \n-test_expect_success 'ignoring safe.directory in GIT_CONFIG_PARAMETERS' '\n-\ttest_must_fail env \\\n-\t\tGIT_CONFIG_PARAMETERS=\"${SQ}safe.directory${SQ}=${SQ}$(pwd)${SQ}\" \\\n-\t\tgit status 2>err &&\n-\tgrep \"dubious ownership\" err\n+test_expect_success 'safe.directory in GIT_CONFIG_PARAMETERS' '\n+\tenv GIT_CONFIG_PARAMETERS=\"${SQ}safe.directory${SQ}=${SQ}$(pwd)${SQ}\" \\\n+\t    git status\n '\n \n test_expect_success 'ignoring safe.directory in repo config' '\n-- \ngitgitgadget\n\n"},{"id":"459092","messageId":"50069bba9a5933fd968a8aa52acefc2a305204f5.1657834082.git.gitgitgadget@gmail.com","threadId":"57859","inReplyTo":"pull.1261.v8.git.git.1657834081.gitgitgadget@gmail.com","subject":"[PATCH v8 5/5] setup.c: create `safe.bareRepository`","fromName":"Glen Choo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2022-07-14T21:28:01Z","receivedAt":"2022-07-14T21:28:27Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"From: Glen Choo <chooglen@google.com>\n\nThere is a known social engineering attack that takes advantage of the\nfact that a working tree can include an entire bare repository,\nincluding a config file. A user could run a Git command inside the bare\nrepository thinking that the config file of the 'outer' repository would\nbe used, but in reality, the bare repository's config file (which is\nattacker-controlled) is used, which may result in arbitrary code\nexecution. See [1] for a fuller description and deeper discussion.\n\nA simple mitigation is to forbid bare repositories unless specified via\n`--git-dir` or `GIT_DIR`. In environments that don't use bare\nrepositories, this would be minimally disruptive.\n\nCreate a config variable, `safe.bareRepository`, that tells Git whether\nor not to die() when working with a bare repository. This config is an\nenum of:\n\n- \"all\": allow all bare repositories (this is the default)\n- \"explicit\": only allow bare repositories specified via --git-dir\n  or GIT_DIR.\n\nIf we want to protect users from such attacks by default, neither value\nwill suffice - \"all\" provides no protection, but \"explicit\" is\nimpractical for bare repository users. A more usable default would be to\nallow only non-embedded bare repositories ([2] contains one such\nproposal), but detecting if a repository is embedded is potentially\nnon-trivial, so this work is not implemented in this series.\n\n[1]: https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com\n[2]: https://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com\n\nSigned-off-by: Glen Choo <chooglen@google.com>\n---\n Documentation/config/safe.txt   | 19 +++++++++++\n setup.c                         | 57 ++++++++++++++++++++++++++++++++-\n t/t0035-safe-bare-repository.sh | 54 +++++++++++++++++++++++++++++++\n 3 files changed, 129 insertions(+), 1 deletion(-)\n create mode 100755 t/t0035-safe-bare-repository.sh\n\ndiff --git a/Documentation/config/safe.txt b/Documentation/config/safe.txt\nindex f72b4408798..bde7f31459b 100644\n--- a/Documentation/config/safe.txt\n+++ b/Documentation/config/safe.txt\n@@ -1,3 +1,22 @@\n+safe.bareRepository::\n+\tSpecifies which bare repositories Git will work with. The currently\n+\tsupported values are:\n++\n+* `all`: Git works with all bare repositories. This is the default.\n+* `explicit`: Git only works with bare repositories specified via\n+  the top-level `--git-dir` command-line option, or the `GIT_DIR`\n+  environment variable (see linkgit:git[1]).\n++\n+If you do not use bare repositories in your workflow, then it may be\n+beneficial to set `safe.bareRepository` to `explicit` in your global\n+config. This will protect you from attacks that involve cloning a\n+repository that contains a bare repository and running a Git command\n+within that directory.\n++\n+This config setting is only respected in protected configuration (see\n+<<SCOPES>>). This prevents the untrusted repository from tampering with\n+this value.\n+\n safe.directory::\n \tThese config entries specify Git-tracked directories that are\n \tconsidered safe even if they are owned by someone other than the\ndiff --git a/setup.c b/setup.c\nindex ec5b9139e32..8c683e92b62 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -10,6 +10,10 @@\n static int inside_git_dir = -1;\n static int inside_work_tree = -1;\n static int work_tree_config_is_bogus;\n+enum allowed_bare_repo {\n+\tALLOWED_BARE_REPO_EXPLICIT = 0,\n+\tALLOWED_BARE_REPO_ALL,\n+};\n \n static struct startup_info the_startup_info;\n struct startup_info *startup_info = &the_startup_info;\n@@ -1160,6 +1164,46 @@ static int ensure_valid_ownership(const char *gitfile,\n \treturn data.is_safe;\n }\n \n+static int allowed_bare_repo_cb(const char *key, const char *value, void *d)\n+{\n+\tenum allowed_bare_repo *allowed_bare_repo = d;\n+\n+\tif (strcasecmp(key, \"safe.bareRepository\"))\n+\t\treturn 0;\n+\n+\tif (!strcmp(value, \"explicit\")) {\n+\t\t*allowed_bare_repo = ALLOWED_BARE_REPO_EXPLICIT;\n+\t\treturn 0;\n+\t}\n+\tif (!strcmp(value, \"all\")) {\n+\t\t*allowed_bare_repo = ALLOWED_BARE_REPO_ALL;\n+\t\treturn 0;\n+\t}\n+\treturn -1;\n+}\n+\n+static enum allowed_bare_repo get_allowed_bare_repo(void)\n+{\n+\tenum allowed_bare_repo result = ALLOWED_BARE_REPO_ALL;\n+\tgit_protected_config(allowed_bare_repo_cb, &result);\n+\treturn result;\n+}\n+\n+static const char *allowed_bare_repo_to_string(\n+\tenum allowed_bare_repo allowed_bare_repo)\n+{\n+\tswitch (allowed_bare_repo) {\n+\tcase ALLOWED_BARE_REPO_EXPLICIT:\n+\t\treturn \"explicit\";\n+\tcase ALLOWED_BARE_REPO_ALL:\n+\t\treturn \"all\";\n+\tdefault:\n+\t\tBUG(\"invalid allowed_bare_repo %d\",\n+\t\t    allowed_bare_repo);\n+\t}\n+\treturn NULL;\n+}\n+\n enum discovery_result {\n \tGIT_DIR_NONE = 0,\n \tGIT_DIR_EXPLICIT,\n@@ -1169,7 +1213,8 @@ enum discovery_result {\n \tGIT_DIR_HIT_CEILING = -1,\n \tGIT_DIR_HIT_MOUNT_POINT = -2,\n \tGIT_DIR_INVALID_GITFILE = -3,\n-\tGIT_DIR_INVALID_OWNERSHIP = -4\n+\tGIT_DIR_INVALID_OWNERSHIP = -4,\n+\tGIT_DIR_DISALLOWED_BARE = -5,\n };\n \n /*\n@@ -1297,6 +1342,8 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,\n \t\t}\n \n \t\tif (is_git_directory(dir->buf)) {\n+\t\t\tif (get_allowed_bare_repo() == ALLOWED_BARE_REPO_EXPLICIT)\n+\t\t\t\treturn GIT_DIR_DISALLOWED_BARE;\n \t\t\tif (!ensure_valid_ownership(NULL, NULL, dir->buf))\n \t\t\t\treturn GIT_DIR_INVALID_OWNERSHIP;\n \t\t\tstrbuf_addstr(gitdir, \".\");\n@@ -1443,6 +1490,14 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \t\t}\n \t\t*nongit_ok = 1;\n \t\tbreak;\n+\tcase GIT_DIR_DISALLOWED_BARE:\n+\t\tif (!nongit_ok) {\n+\t\t\tdie(_(\"cannot use bare repository '%s' (safe.bareRepository is '%s')\"),\n+\t\t\t    dir.buf,\n+\t\t\t    allowed_bare_repo_to_string(get_allowed_bare_repo()));\n+\t\t}\n+\t\t*nongit_ok = 1;\n+\t\tbreak;\n \tcase GIT_DIR_NONE:\n \t\t/*\n \t\t * As a safeguard against setup_git_directory_gently_1 returning\ndiff --git a/t/t0035-safe-bare-repository.sh b/t/t0035-safe-bare-repository.sh\nnew file mode 100755\nindex 00000000000..ecbdc8238db\n--- /dev/null\n+++ b/t/t0035-safe-bare-repository.sh\n@@ -0,0 +1,54 @@\n+#!/bin/sh\n+\n+test_description='verify safe.bareRepository checks'\n+\n+TEST_PASSES_SANITIZE_LEAK=true\n+. ./test-lib.sh\n+\n+pwd=\"$(pwd)\"\n+\n+expect_accepted () {\n+\tgit \"$@\" rev-parse --git-dir\n+}\n+\n+expect_rejected () {\n+\ttest_must_fail git \"$@\" rev-parse --git-dir 2>err &&\n+\tgrep -F \"cannot use bare repository\" err\n+}\n+\n+test_expect_success 'setup bare repo in worktree' '\n+\tgit init outer-repo &&\n+\tgit init --bare outer-repo/bare-repo\n+'\n+\n+test_expect_success 'safe.bareRepository unset' '\n+\texpect_accepted -C outer-repo/bare-repo\n+'\n+\n+test_expect_success 'safe.bareRepository=all' '\n+\ttest_config_global safe.bareRepository all &&\n+\texpect_accepted -C outer-repo/bare-repo\n+'\n+\n+test_expect_success 'safe.bareRepository=explicit' '\n+\ttest_config_global safe.bareRepository explicit &&\n+\texpect_rejected -C outer-repo/bare-repo\n+'\n+\n+test_expect_success 'safe.bareRepository in the repository' '\n+\t# safe.bareRepository must not be \"explicit\", otherwise\n+\t# git config fails with \"fatal: not in a git directory\" (like\n+\t# safe.directory)\n+\ttest_config -C outer-repo/bare-repo safe.bareRepository \\\n+\t\tall &&\n+\ttest_config_global safe.bareRepository explicit &&\n+\texpect_rejected -C outer-repo/bare-repo\n+'\n+\n+test_expect_success 'safe.bareRepository on the command line' '\n+\ttest_config_global safe.bareRepository explicit &&\n+\texpect_accepted -C outer-repo/bare-repo \\\n+\t\t-c safe.bareRepository=all\n+'\n+\n+test_done\n-- \ngitgitgadget\n"},{"id":"459901","messageId":"220725.861qu9oxl4.gmgdl@evledraar.gmail.com","threadId":"57859","inReplyTo":"30ac73716cbc234a1f176d2d417bf0e2b0b335cf.1657834081.git.gitgitgadget@gmail.com","subject":"SANITIZE=address failure on master (was: [PATCH v8 3/5] config: learn `git_protected_config()`)","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-07-25T18:26:41Z","receivedAt":"2022-07-25T18:29:18Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Jul 14 2022, Glen Choo via GitGitGadget wrote:\n\n> From: Glen Choo <chooglen@google.com>\n>\n> `uploadpack.packObjectsHook` is the only 'protected configuration only'\n> variable today, but we've noted that `safe.directory` and the upcoming\n> `safe.bareRepository` should also be 'protected configuration only'. So,\n> for consistency, we'd like to have a single implementation for protected\n> configuration.\n>\n> The primary constraints are:\n>\n> 1. Reading from protected configuration should be fast. Nearly all \"git\"\n>    commands inside a bare repository will read both `safe.directory` and\n>    `safe.bareRepository`, so we cannot afford to be slow.\n>\n> 2. Protected configuration must be readable when the gitdir is not\n>    known. `safe.directory` and `safe.bareRepository` both affect\n>    repository discovery and the gitdir is not known at that point [1].\n>\n> The chosen implementation in this commit is to read protected\n> configuration and cache the values in a global configset. This is\n> similar to the caching behavior we get with the_repository->config.\n>\n> Introduce git_protected_config(), which reads protected configuration\n> and caches them in the global configset protected_config. Then, refactor\n> `uploadpack.packObjectsHook` to use git_protected_config().\n>\n> The protected configuration functions are named similarly to their\n> non-protected counterparts, e.g. git_protected_config_check_init() vs\n> git_config_check_init().\n>\n> In light of constraint 1, this implementation can still be improved.\n> git_protected_config() iterates through every variable in\n> protected_config, which is wasteful, but it makes the conversion simple\n> because it matches existing patterns. We will likely implement constant\n> time lookup functions for protected configuration in a future series\n> (such functions already exist for non-protected configuration, i.e.\n> repo_config_get_*()).\n>\n> An alternative that avoids introducing another configset is to continue\n> to read all config using git_config(), but only accept values that have\n> the correct config scope [2]. This technically fulfills constraint 2,\n> because git_config() simply ignores the local and worktree config when\n> the gitdir is not known. However, this would read incomplete config into\n> the_repository->config, which would need to be reset when the gitdir is\n> known and git_config() needs to read the local and worktree config.\n> Resetting the_repository->config might be reasonable while we only have\n> these 'protected configuration only' variables, but it's not clear\n> whether this extends well to future variables.\n>\n> [1] In this case, we do have a candidate gitdir though, so with a little\n> refactoring, it might be possible to provide a gitdir.\n> [2] This is how `uploadpack.packObjectsHook` was implemented prior to\n> this commit.\n>\n> Signed-off-by: Glen Choo <chooglen@google.com>\n> ---\n>  config.c                     | 43 ++++++++++++++++++++++++++++++++++++\n>  config.h                     | 16 ++++++++++++++\n>  t/t5544-pack-objects-hook.sh |  7 +++++-\n>  upload-pack.c                | 27 +++++++++++++---------\n>  4 files changed, 82 insertions(+), 11 deletions(-)\n>\n> diff --git a/config.c b/config.c\n> index 9b0e9c93285..015bec360f5 100644\n> --- a/config.c\n> +++ b/config.c\n> @@ -81,6 +81,17 @@ static enum config_scope current_parsing_scope;\n>  static int pack_compression_seen;\n>  static int zlib_compression_seen;\n>  \n> +/*\n> + * Config that comes from trusted scopes, namely:\n> + * - CONFIG_SCOPE_SYSTEM (e.g. /etc/gitconfig)\n> + * - CONFIG_SCOPE_GLOBAL (e.g. $HOME/.gitconfig, $XDG_CONFIG_HOME/git)\n> + * - CONFIG_SCOPE_COMMAND (e.g. \"-c\" option, environment variables)\n> + *\n> + * This is declared here for code cleanliness, but unlike the other\n> + * static variables, this does not hold config parser state.\n> + */\n> +static struct config_set protected_config;\n> +\n>  static int config_file_fgetc(struct config_source *conf)\n>  {\n>  \treturn getc_unlocked(conf->u.file);\n> @@ -2378,6 +2389,11 @@ int git_configset_add_file(struct config_set *cs, const char *filename)\n>  \treturn git_config_from_file(config_set_callback, filename, cs);\n>  }\n>  \n> +int git_configset_add_parameters(struct config_set *cs)\n> +{\n> +\treturn git_config_from_parameters(config_set_callback, cs);\n> +}\n> +\n>  int git_configset_get_value(struct config_set *cs, const char *key, const char **value)\n>  {\n>  \tconst struct string_list *values = NULL;\n> @@ -2619,6 +2635,33 @@ int repo_config_get_pathname(struct repository *repo,\n>  \treturn ret;\n>  }\n>  \n> +/* Read values into protected_config. */\n> +static void read_protected_config(void)\n> +{\n> +\tchar *xdg_config = NULL, *user_config = NULL, *system_config = NULL;\n> +\n> +\tgit_configset_init(&protected_config);\n> +\n> +\tsystem_config = git_system_config();\n> +\tgit_global_config(&user_config, &xdg_config);\n> +\n> +\tgit_configset_add_file(&protected_config, system_config);\n> +\tgit_configset_add_file(&protected_config, xdg_config);\n> +\tgit_configset_add_file(&protected_config, user_config);\n> +\tgit_configset_add_parameters(&protected_config);\n> +\n> +\tfree(system_config);\n> +\tfree(xdg_config);\n> +\tfree(user_config);\n> +}\n> +\n> +void git_protected_config(config_fn_t fn, void *data)\n> +{\n> +\tif (!protected_config.hash_initialized)\n> +\t\tread_protected_config();\n> +\tconfigset_iter(&protected_config, fn, data);\n> +}\n> +\n>  /* Functions used historically to read configuration from 'the_repository' */\n>  void git_config(config_fn_t fn, void *data)\n>  {\n> diff --git a/config.h b/config.h\n> index 7654f61c634..ca994d77147 100644\n> --- a/config.h\n> +++ b/config.h\n> @@ -446,6 +446,15 @@ void git_configset_init(struct config_set *cs);\n>   */\n>  int git_configset_add_file(struct config_set *cs, const char *filename);\n>  \n> +/**\n> + * Parses command line options and environment variables, and adds the\n> + * variable-value pairs to the `config_set`. Returns 0 on success, or -1\n> + * if there is an error in parsing. The caller decides whether to free\n> + * the incomplete configset or continue using it when the function\n> + * returns -1.\n> + */\n> +int git_configset_add_parameters(struct config_set *cs);\n> +\n>  /**\n>   * Finds and returns the value list, sorted in order of increasing priority\n>   * for the configuration variable `key` and config set `cs`. When the\n> @@ -505,6 +514,13 @@ int repo_config_get_maybe_bool(struct repository *repo,\n>  int repo_config_get_pathname(struct repository *repo,\n>  \t\t\t     const char *key, const char **dest);\n>  \n> +/*\n> + * Functions for reading protected config. By definition, protected\n> + * config ignores repository config, so these do not take a `struct\n> + * repository` parameter.\n> + */\n> +void git_protected_config(config_fn_t fn, void *data);\n> +\n>  /**\n>   * Querying For Specific Variables\n>   * -------------------------------\n> diff --git a/t/t5544-pack-objects-hook.sh b/t/t5544-pack-objects-hook.sh\n> index dd5f44d986f..54f54f8d2eb 100755\n> --- a/t/t5544-pack-objects-hook.sh\n> +++ b/t/t5544-pack-objects-hook.sh\n> @@ -56,7 +56,12 @@ test_expect_success 'hook does not run from repo config' '\n>  \t! grep \"hook running\" stderr &&\n>  \ttest_path_is_missing .git/hook.args &&\n>  \ttest_path_is_missing .git/hook.stdin &&\n> -\ttest_path_is_missing .git/hook.stdout\n> +\ttest_path_is_missing .git/hook.stdout &&\n> +\n> +\t# check that global config is used instead\n> +\ttest_config_global uploadpack.packObjectsHook ./hook &&\n> +\tgit clone --no-local . dst2.git 2>stderr &&\n> +\tgrep \"hook running\" stderr\n>  '\n>  \n>  test_expect_success 'hook works with partial clone' '\n> diff --git a/upload-pack.c b/upload-pack.c\n> index 3a851b36066..09f48317b02 100644\n> --- a/upload-pack.c\n> +++ b/upload-pack.c\n> @@ -1321,18 +1321,27 @@ static int upload_pack_config(const char *var, const char *value, void *cb_data)\n>  \t\tdata->advertise_sid = git_config_bool(var, value);\n>  \t}\n>  \n> -\tif (current_config_scope() != CONFIG_SCOPE_LOCAL &&\n> -\t    current_config_scope() != CONFIG_SCOPE_WORKTREE) {\n> -\t\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n> -\t\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n> -\t}\n> -\n>  \tif (parse_object_filter_config(var, value, data) < 0)\n>  \t\treturn -1;\n>  \n>  \treturn parse_hide_refs_config(var, value, \"uploadpack\");\n>  }\n>  \n> +static int upload_pack_protected_config(const char *var, const char *value, void *cb_data)\n> +{\n> +\tstruct upload_pack_data *data = cb_data;\n> +\n> +\tif (!strcmp(\"uploadpack.packobjectshook\", var))\n> +\t\treturn git_config_string(&data->pack_objects_hook, var, value);\n> +\treturn 0;\n> +}\n> +\n> +static void get_upload_pack_config(struct upload_pack_data *data)\n> +{\n> +\tgit_config(upload_pack_config, data);\n> +\tgit_protected_config(upload_pack_protected_config, data);\n> +}\n> +\n>  void upload_pack(const int advertise_refs, const int stateless_rpc,\n>  \t\t const int timeout)\n>  {\n> @@ -1340,8 +1349,7 @@ void upload_pack(const int advertise_refs, const int stateless_rpc,\n>  \tstruct upload_pack_data data;\n>  \n>  \tupload_pack_data_init(&data);\n> -\n> -\tgit_config(upload_pack_config, &data);\n> +\tget_upload_pack_config(&data);\n>  \n>  \tdata.stateless_rpc = stateless_rpc;\n>  \tdata.timeout = timeout;\n> @@ -1695,8 +1703,7 @@ int upload_pack_v2(struct repository *r, struct packet_reader *request)\n>  \n>  \tupload_pack_data_init(&data);\n>  \tdata.use_sideband = LARGE_PACKET_MAX;\n> -\n> -\tgit_config(upload_pack_config, &data);\n> +\tget_upload_pack_config(&data);\n>  \n>  \twhile (state != FETCH_DONE) {\n>  \t\tswitch (state) {\n\nNoticed after it landed on master: This change fails with:\n\n\tmake SANITIZE=address test T=t0410*.sh\n\nRunning that manually shows that we fail like this:\n\t\n\t$ cat trash\\ directory.t0410-partial-clone/httpd/error.log | grep -o AH0.*\n\tAH00163: Apache/2.4.54 (Debian) configured -- resuming normal operations\n\tAH00094: Command line: '/usr/sbin/apache2 -d /home/avar/g/git/t/trash directory.t0410-partial-clone/httpd -f /home/avar/g/git/t/lib-httpd/apache.conf -c Listen 127.0.0.1:10410'\n\tAH01215: AddressSanitizer:DEADLYSIGNAL: /home/avar/g/git/git-http-backend\n\tAH01215: =================================================================: /home/avar/g/git/git-http-backend\n\tAH01215: ==27820==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x7f7af5dc0d66 bp 0x7fff11964450 sp 0x7fff11963be8 T0): /home/avar/g/git/git-http-backend\n\tAH01215: ==27820==The signal is caused by a READ memory access.: /home/avar/g/git/git-http-backend\n\tAH01215: ==27820==Hint: address points to the zero page.: /home/avar/g/git/git-http-backend\n\tAH01215:     #0 0x7f7af5dc0d66 in __sanitizer::internal_strlen(char const*) ../../../../src/libsanitizer/sanitizer_common/sanitizer_libc.cpp:167: /home/avar/g/git/git-http-backend\n\tAH01215:     #1 0x7f7af5d512f2 in __interceptor_fopen64 ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:6220: /home/avar/g/git/git-http-backend\n\tAH01215:     #2 0x562a65e37cc8 in git_fopen compat/fopen.c:22: /home/avar/g/git/git-http-backend\n\tAH01215:     #3 0x562a65df3879 in fopen_or_warn wrapper.c:431: /home/avar/g/git/git-http-backend\n\tAH01215:     #4 0x562a65a12476 in git_config_from_file_with_options config.c:1982: /home/avar/g/git/git-http-backend\n\tAH01215:     #5 0x562a65a124f4 in git_config_from_file config.c:1993: /home/avar/g/git/git-http-backend\n\tAH01215:     #6 0x562a65a15288 in git_configset_add_file config.c:2389: /home/avar/g/git/git-http-backend\n\tAH01215:     #7 0x562a65a16a37 in read_protected_config config.c:2649: /home/avar/g/git/git-http-backend\n\tAH01215:     #8 0x562a65a16b5c in git_protected_config config.c:2661: /home/avar/g/git/git-http-backend\n\tAH01215:     #9 0x562a65dd9f9a in get_upload_pack_config upload-pack.c:1342: /home/avar/g/git/git-http-backend\n\tAH01215:     #10 0x562a65ddc1cb in upload_pack_v2 upload-pack.c:1706: /home/avar/g/git/git-http-backend\n\tAH01215:     #11 0x562a65d2eb8a in process_request serve.c:308: /home/avar/g/git/git-http-backend\n\tAH01215:     #12 0x562a65d2ec18 in protocol_v2_serve_loop serve.c:323: /home/avar/g/git/git-http-backend\n\tAH01215:     #13 0x562a6593c5ae in cmd_upload_pack builtin/upload-pack.c:55: /home/avar/g/git/git-http-backend\n\tAH01215:     #14 0x562a656cf8ff in run_builtin git.c:466: /home/avar/g/git/git-http-backend\n\tAH01215:     #15 0x562a656d02ab in handle_builtin git.c:720: /home/avar/g/git/git-http-backend\n\tAH01215:     #16 0x562a656d09d5 in run_argv git.c:787: /home/avar/g/git/git-http-backend\n\tAH01215:     #17 0x562a656d174f in cmd_main git.c:920: /home/avar/g/git/git-http-backend\n\tAH01215:     #18 0x562a6594b0b9 in main common-main.c:56: /home/avar/g/git/git-http-backend\n\tAH01215:     #19 0x7f7af5a5681c in __libc_start_main ../csu/libc-start.c:332: /home/avar/g/git/git-http-backend\n\tAH01215:     #20 0x562a656cb209 in _start (git+0x1d1209): /home/avar/g/git/git-http-backend\n\tAH01215: : /home/avar/g/git/git-http-backend\n\tAH01215: AddressSanitizer can not provide additional info.: /home/avar/g/git/git-http-backend\n\tAH01215: SUMMARY: AddressSanitizer: SEGV ../../../../src/libsanitizer/sanitizer_common/sanitizer_libc.cpp:167 in __sanitizer::internal_strlen(char const*): /home/avar/g/git/git-http-backend\n\tAH01215: ==27820==ABORTING: /home/avar/g/git/git-http-backend\n\tAH01215: error: upload-pack died of signal 6: /home/avar/g/git/git-http-backend\n\n(We really should have a SANITIZE=address in CI, but it takes a while...)\n"},{"id":"459912","messageId":"kl6l35ept0e1.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"220725.861qu9oxl4.gmgdl@evledraar.gmail.com","subject":"Re: SANITIZE=address failure on master (was: [PATCH v8 3/5] config: learn `git_protected_config()`)","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-07-25T20:15:02Z","receivedAt":"2022-07-25T20:16:30Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> On Thu, Jul 14 2022, Glen Choo via GitGitGadget wrote:\n>\n>> +/* Read values into protected_config. */\n>> +static void read_protected_config(void)\n>> +{\n>> +\tchar *xdg_config = NULL, *user_config = NULL, *system_config = NULL;\n>> +\n>> +\tgit_configset_init(&protected_config);\n>> +\n>> +\tsystem_config = git_system_config();\n>> +\tgit_global_config(&user_config, &xdg_config);\n>> +\n>> +\tgit_configset_add_file(&protected_config, system_config);\n>> +\tgit_configset_add_file(&protected_config, xdg_config);\n>> +\tgit_configset_add_file(&protected_config, user_config);\n>> +\tgit_configset_add_parameters(&protected_config);\n>> +\n>> +\tfree(system_config);\n>> +\tfree(xdg_config);\n>> +\tfree(user_config);\n>> +}\n>\n> Noticed after it landed on master: This change fails with:\n>\n> \tmake SANITIZE=address test T=t0410*.sh\n>\n> Running that manually shows that we fail like this:\n> \t\n> \t$ cat trash\\ directory.t0410-partial-clone/httpd/error.log | grep -o AH0.*\n> \tAH00163: Apache/2.4.54 (Debian) configured -- resuming normal operations\n> \tAH00094: Command line: '/usr/sbin/apache2 -d /home/avar/g/git/t/trash directory.t0410-partial-clone/httpd -f /home/avar/g/git/t/lib-httpd/apache.conf -c Listen 127.0.0.1:10410'\n> \tAH01215: AddressSanitizer:DEADLYSIGNAL: /home/avar/g/git/git-http-backend\n> \tAH01215: =================================================================: /home/avar/g/git/git-http-backend\n> \tAH01215: ==27820==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x7f7af5dc0d66 bp 0x7fff11964450 sp 0x7fff11963be8 T0): /home/avar/g/git/git-http-backend\n> \tAH01215: ==27820==The signal is caused by a READ memory access.: /home/avar/g/git/git-http-backend\n> \tAH01215: ==27820==Hint: address points to the zero page.: /home/avar/g/git/git-http-backend\n> \tAH01215:     #0 0x7f7af5dc0d66 in __sanitizer::internal_strlen(char const*) ../../../../src/libsanitizer/sanitizer_common/sanitizer_libc.cpp:167: /home/avar/g/git/git-http-backend\n> \tAH01215:     #1 0x7f7af5d512f2 in __interceptor_fopen64 ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:6220: /home/avar/g/git/git-http-backend\n> \tAH01215:     #2 0x562a65e37cc8 in git_fopen compat/fopen.c:22: /home/avar/g/git/git-http-backend\n> \tAH01215:     #3 0x562a65df3879 in fopen_or_warn wrapper.c:431: /home/avar/g/git/git-http-backend\n> \tAH01215:     #4 0x562a65a12476 in git_config_from_file_with_options config.c:1982: /home/avar/g/git/git-http-backend\n> \tAH01215:     #5 0x562a65a124f4 in git_config_from_file config.c:1993: /home/avar/g/git/git-http-backend\n> \tAH01215:     #6 0x562a65a15288 in git_configset_add_file config.c:2389: /home/avar/g/git/git-http-backend\n> \tAH01215:     #7 0x562a65a16a37 in read_protected_config config.c:2649: /home/avar/g/git/git-http-backend\n> \tAH01215:     #8 0x562a65a16b5c in git_protected_config config.c:2661: /home/avar/g/git/git-http-backend\n> \tAH01215:     #9 0x562a65dd9f9a in get_upload_pack_config upload-pack.c:1342: /home/avar/g/git/git-http-backend\n> \tAH01215:     #10 0x562a65ddc1cb in upload_pack_v2 upload-pack.c:1706: /home/avar/g/git/git-http-backend\n> \tAH01215:     #11 0x562a65d2eb8a in process_request serve.c:308: /home/avar/g/git/git-http-backend\n> \tAH01215:     #12 0x562a65d2ec18 in protocol_v2_serve_loop serve.c:323: /home/avar/g/git/git-http-backend\n> \tAH01215:     #13 0x562a6593c5ae in cmd_upload_pack builtin/upload-pack.c:55: /home/avar/g/git/git-http-backend\n> \tAH01215:     #14 0x562a656cf8ff in run_builtin git.c:466: /home/avar/g/git/git-http-backend\n> \tAH01215:     #15 0x562a656d02ab in handle_builtin git.c:720: /home/avar/g/git/git-http-backend\n> \tAH01215:     #16 0x562a656d09d5 in run_argv git.c:787: /home/avar/g/git/git-http-backend\n> \tAH01215:     #17 0x562a656d174f in cmd_main git.c:920: /home/avar/g/git/git-http-backend\n> \tAH01215:     #18 0x562a6594b0b9 in main common-main.c:56: /home/avar/g/git/git-http-backend\n> \tAH01215:     #19 0x7f7af5a5681c in __libc_start_main ../csu/libc-start.c:332: /home/avar/g/git/git-http-backend\n> \tAH01215:     #20 0x562a656cb209 in _start (git+0x1d1209): /home/avar/g/git/git-http-backend\n> \tAH01215: : /home/avar/g/git/git-http-backend\n> \tAH01215: AddressSanitizer can not provide additional info.: /home/avar/g/git/git-http-backend\n> \tAH01215: SUMMARY: AddressSanitizer: SEGV ../../../../src/libsanitizer/sanitizer_common/sanitizer_libc.cpp:167 in __sanitizer::internal_strlen(char const*): /home/avar/g/git/git-http-backend\n> \tAH01215: ==27820==ABORTING: /home/avar/g/git/git-http-backend\n> \tAH01215: error: upload-pack died of signal 6: /home/avar/g/git/git-http-backend\n>\n> (We really should have a SANITIZE=address in CI, but it takes a while...)\n\nThanks. I narrowed the failure down to the hunk above, specifically this\nline:\n\n  git_configset_add_file(&protected_config, xdg_config);\n\nSince xdg_config can be NULL, this results in the failing call\nfopen_or_warn(NULL, \"r\").\n\nThis logic was lifted  from do_git_config_sequence(), which checks that\neach of the paths are not NULL. So a fix might be something like:\n\n----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----\n\n  diff --git a/config.c b/config.c\n  index 015bec360f..208a3dd7a7 100644\n  --- a/config.c\n  +++ b/config.c\n  @@ -2645,9 +2645,13 @@ static void read_protected_config(void)\n    system_config = git_system_config();\n    git_global_config(&user_config, &xdg_config);\n\n  -\tgit_configset_add_file(&protected_config, system_config);\n  -\tgit_configset_add_file(&protected_config, xdg_config);\n  -\tgit_configset_add_file(&protected_config, user_config);\n  +\n  +\tif (system_config)\n  +\t\tgit_configset_add_file(&protected_config, system_config);\n  +\tif (xdg_config)\n  +\t\tgit_configset_add_file(&protected_config, xdg_config);\n  +\tif (user_config)\n  +\t\tgit_configset_add_file(&protected_config, user_config);\n    git_configset_add_parameters(&protected_config);\n\n    free(system_config);\n\n----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----\n\nI'm not sure if system_config can ever be NULL, but (xdg|user)_config is\nNULL when $HOME is unset, and xdg_config is also unset if\n$GIT_CONFIG_GLOBAL is set.\n"},{"id":"459920","messageId":"220725.86o7xcords.gmgdl@evledraar.gmail.com","threadId":"57859","inReplyTo":"kl6l35ept0e1.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: SANITIZE=address failure on master (was: [PATCH v8 3/5] config: learn `git_protected_config()`)","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-07-25T20:41:30Z","receivedAt":"2022-07-25T20:43:20Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Jul 25 2022, Glen Choo wrote:\n\n> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n>\n>> On Thu, Jul 14 2022, Glen Choo via GitGitGadget wrote:\n>>\n>>> +/* Read values into protected_config. */\n>>> +static void read_protected_config(void)\n>>> +{\n>>> +\tchar *xdg_config = NULL, *user_config = NULL, *system_config = NULL;\n>>> +\n>>> +\tgit_configset_init(&protected_config);\n>>> +\n>>> +\tsystem_config = git_system_config();\n>>> +\tgit_global_config(&user_config, &xdg_config);\n>>> +\n>>> +\tgit_configset_add_file(&protected_config, system_config);\n>>> +\tgit_configset_add_file(&protected_config, xdg_config);\n>>> +\tgit_configset_add_file(&protected_config, user_config);\n>>> +\tgit_configset_add_parameters(&protected_config);\n>>> +\n>>> +\tfree(system_config);\n>>> +\tfree(xdg_config);\n>>> +\tfree(user_config);\n>>> +}\n>>\n>> Noticed after it landed on master: This change fails with:\n>>\n>> \tmake SANITIZE=address test T=t0410*.sh\n>>\n>> Running that manually shows that we fail like this:\n>> \t\n>> \t$ cat trash\\ directory.t0410-partial-clone/httpd/error.log | grep -o AH0.*\n>> \tAH00163: Apache/2.4.54 (Debian) configured -- resuming normal operations\n>> \tAH00094: Command line: '/usr/sbin/apache2 -d /home/avar/g/git/t/trash directory.t0410-partial-clone/httpd -f /home/avar/g/git/t/lib-httpd/apache.conf -c Listen 127.0.0.1:10410'\n>> \tAH01215: AddressSanitizer:DEADLYSIGNAL: /home/avar/g/git/git-http-backend\n>> \tAH01215: =================================================================: /home/avar/g/git/git-http-backend\n>> \tAH01215: ==27820==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x7f7af5dc0d66 bp 0x7fff11964450 sp 0x7fff11963be8 T0): /home/avar/g/git/git-http-backend\n>> \tAH01215: ==27820==The signal is caused by a READ memory access.: /home/avar/g/git/git-http-backend\n>> \tAH01215: ==27820==Hint: address points to the zero page.: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #0 0x7f7af5dc0d66 in __sanitizer::internal_strlen(char const*) ../../../../src/libsanitizer/sanitizer_common/sanitizer_libc.cpp:167: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #1 0x7f7af5d512f2 in __interceptor_fopen64 ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:6220: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #2 0x562a65e37cc8 in git_fopen compat/fopen.c:22: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #3 0x562a65df3879 in fopen_or_warn wrapper.c:431: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #4 0x562a65a12476 in git_config_from_file_with_options config.c:1982: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #5 0x562a65a124f4 in git_config_from_file config.c:1993: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #6 0x562a65a15288 in git_configset_add_file config.c:2389: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #7 0x562a65a16a37 in read_protected_config config.c:2649: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #8 0x562a65a16b5c in git_protected_config config.c:2661: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #9 0x562a65dd9f9a in get_upload_pack_config upload-pack.c:1342: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #10 0x562a65ddc1cb in upload_pack_v2 upload-pack.c:1706: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #11 0x562a65d2eb8a in process_request serve.c:308: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #12 0x562a65d2ec18 in protocol_v2_serve_loop serve.c:323: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #13 0x562a6593c5ae in cmd_upload_pack builtin/upload-pack.c:55: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #14 0x562a656cf8ff in run_builtin git.c:466: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #15 0x562a656d02ab in handle_builtin git.c:720: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #16 0x562a656d09d5 in run_argv git.c:787: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #17 0x562a656d174f in cmd_main git.c:920: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #18 0x562a6594b0b9 in main common-main.c:56: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #19 0x7f7af5a5681c in __libc_start_main ../csu/libc-start.c:332: /home/avar/g/git/git-http-backend\n>> \tAH01215:     #20 0x562a656cb209 in _start (git+0x1d1209): /home/avar/g/git/git-http-backend\n>> \tAH01215: : /home/avar/g/git/git-http-backend\n>> \tAH01215: AddressSanitizer can not provide additional info.: /home/avar/g/git/git-http-backend\n>> \tAH01215: SUMMARY: AddressSanitizer: SEGV\n>> ../../../../src/libsanitizer/sanitizer_common/sanitizer_libc.cpp:167\n>> in __sanitizer::internal_strlen(char const*):\n>> /home/avar/g/git/git-http-backend\n>> \tAH01215: ==27820==ABORTING: /home/avar/g/git/git-http-backend\n>> \tAH01215: error: upload-pack died of signal 6: /home/avar/g/git/git-http-backend\n>>\n>> (We really should have a SANITIZE=address in CI, but it takes a while...)\n>\n> Thanks. I narrowed the failure down to the hunk above, specifically this\n> line:\n>\n>   git_configset_add_file(&protected_config, xdg_config);\n>\n> Since xdg_config can be NULL, this results in the failing call\n> fopen_or_warn(NULL, \"r\").\n>\n> This logic was lifted  from do_git_config_sequence(), which checks that\n> each of the paths are not NULL. So a fix might be something like:\n>\n> ----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----\n>\n>   diff --git a/config.c b/config.c\n>   index 015bec360f..208a3dd7a7 100644\n>   --- a/config.c\n>   +++ b/config.c\n>   @@ -2645,9 +2645,13 @@ static void read_protected_config(void)\n>     system_config = git_system_config();\n>     git_global_config(&user_config, &xdg_config);\n>\n>   -\tgit_configset_add_file(&protected_config, system_config);\n>   -\tgit_configset_add_file(&protected_config, xdg_config);\n>   -\tgit_configset_add_file(&protected_config, user_config);\n>   +\n>   +\tif (system_config)\n>   +\t\tgit_configset_add_file(&protected_config, system_config);\n>   +\tif (xdg_config)\n>   +\t\tgit_configset_add_file(&protected_config, xdg_config);\n>   +\tif (user_config)\n>   +\t\tgit_configset_add_file(&protected_config, user_config);\n>     git_configset_add_parameters(&protected_config);\n>\n>     free(system_config);\n>\n> ----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----\n>\n> I'm not sure if system_config can ever be NULL, but (xdg|user)_config is\n> NULL when $HOME is unset, and xdg_config is also unset if\n> $GIT_CONFIG_GLOBAL is set.\n\nNot having looked into it much at all: Doesn't this then introduce\nanother logic error where git_protected_config() is now buggy, i.e. it's\na \"lazy load\" method where we'll expect to read_protected_config()\nfirst.\n\nThe assumption with that seems to have been that it's invariant within a\nsingle process, is that still the case, or can e.g. HOME be set during\nour runtime when we rely on these functions?\n\n(I don't know)\n"},{"id":"459924","messageId":"kl6lzggwsyh1.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"57859","inReplyTo":"220725.86o7xcords.gmgdl@evledraar.gmail.com","subject":"Re: SANITIZE=address failure on master (was: [PATCH v8 3/5] config: learn `git_protected_config()`)","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2022-07-25T20:56:26Z","receivedAt":"2022-07-25T20:56:30Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> On Mon, Jul 25 2022, Glen Choo wrote:\n>\n>> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n>>\n>>> On Thu, Jul 14 2022, Glen Choo via GitGitGadget wrote:\n>>>\n>>>> +/* Read values into protected_config. */\n>>>> +static void read_protected_config(void)\n>>>> +{\n>>>> +\tchar *xdg_config = NULL, *user_config = NULL, *system_config = NULL;\n>>>> +\n>>>> +\tgit_configset_init(&protected_config);\n>>>> +\n>>>> +\tsystem_config = git_system_config();\n>>>> +\tgit_global_config(&user_config, &xdg_config);\n>>>> +\n>>>> +\tgit_configset_add_file(&protected_config, system_config);\n>>>> +\tgit_configset_add_file(&protected_config, xdg_config);\n>>>> +\tgit_configset_add_file(&protected_config, user_config);\n>>>> +\tgit_configset_add_parameters(&protected_config);\n>>>> +\n>>>> +\tfree(system_config);\n>>>> +\tfree(xdg_config);\n>>>> +\tfree(user_config);\n>>>> +}\n>>>\n>>> Noticed after it landed on master: This change fails with:\n>>>\n>>> \tmake SANITIZE=address test T=t0410*.sh\n>>>\n>>> Running that manually shows that we fail like this:\n>>> \t\n>>> \t$ cat trash\\ directory.t0410-partial-clone/httpd/error.log | grep -o AH0.*\n>>> \tAH00163: Apache/2.4.54 (Debian) configured -- resuming normal operations\n>>> \tAH00094: Command line: '/usr/sbin/apache2 -d /home/avar/g/git/t/trash directory.t0410-partial-clone/httpd -f /home/avar/g/git/t/lib-httpd/apache.conf -c Listen 127.0.0.1:10410'\n>>> \tAH01215: AddressSanitizer:DEADLYSIGNAL: /home/avar/g/git/git-http-backend\n>>> \tAH01215: =================================================================: /home/avar/g/git/git-http-backend\n>>> \tAH01215: ==27820==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x7f7af5dc0d66 bp 0x7fff11964450 sp 0x7fff11963be8 T0): /home/avar/g/git/git-http-backend\n>>> \tAH01215: ==27820==The signal is caused by a READ memory access.: /home/avar/g/git/git-http-backend\n>>> \tAH01215: ==27820==Hint: address points to the zero page.: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #0 0x7f7af5dc0d66 in __sanitizer::internal_strlen(char const*) ../../../../src/libsanitizer/sanitizer_common/sanitizer_libc.cpp:167: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #1 0x7f7af5d512f2 in __interceptor_fopen64 ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:6220: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #2 0x562a65e37cc8 in git_fopen compat/fopen.c:22: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #3 0x562a65df3879 in fopen_or_warn wrapper.c:431: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #4 0x562a65a12476 in git_config_from_file_with_options config.c:1982: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #5 0x562a65a124f4 in git_config_from_file config.c:1993: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #6 0x562a65a15288 in git_configset_add_file config.c:2389: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #7 0x562a65a16a37 in read_protected_config config.c:2649: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #8 0x562a65a16b5c in git_protected_config config.c:2661: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #9 0x562a65dd9f9a in get_upload_pack_config upload-pack.c:1342: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #10 0x562a65ddc1cb in upload_pack_v2 upload-pack.c:1706: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #11 0x562a65d2eb8a in process_request serve.c:308: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #12 0x562a65d2ec18 in protocol_v2_serve_loop serve.c:323: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #13 0x562a6593c5ae in cmd_upload_pack builtin/upload-pack.c:55: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #14 0x562a656cf8ff in run_builtin git.c:466: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #15 0x562a656d02ab in handle_builtin git.c:720: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #16 0x562a656d09d5 in run_argv git.c:787: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #17 0x562a656d174f in cmd_main git.c:920: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #18 0x562a6594b0b9 in main common-main.c:56: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #19 0x7f7af5a5681c in __libc_start_main ../csu/libc-start.c:332: /home/avar/g/git/git-http-backend\n>>> \tAH01215:     #20 0x562a656cb209 in _start (git+0x1d1209): /home/avar/g/git/git-http-backend\n>>> \tAH01215: : /home/avar/g/git/git-http-backend\n>>> \tAH01215: AddressSanitizer can not provide additional info.: /home/avar/g/git/git-http-backend\n>>> \tAH01215: SUMMARY: AddressSanitizer: SEGV\n>>> ../../../../src/libsanitizer/sanitizer_common/sanitizer_libc.cpp:167\n>>> in __sanitizer::internal_strlen(char const*):\n>>> /home/avar/g/git/git-http-backend\n>>> \tAH01215: ==27820==ABORTING: /home/avar/g/git/git-http-backend\n>>> \tAH01215: error: upload-pack died of signal 6: /home/avar/g/git/git-http-backend\n>>>\n>>> (We really should have a SANITIZE=address in CI, but it takes a while...)\n>>\n>> Thanks. I narrowed the failure down to the hunk above, specifically this\n>> line:\n>>\n>>   git_configset_add_file(&protected_config, xdg_config);\n>>\n>> Since xdg_config can be NULL, this results in the failing call\n>> fopen_or_warn(NULL, \"r\").\n>>\n>> This logic was lifted  from do_git_config_sequence(), which checks that\n>> each of the paths are not NULL. So a fix might be something like:\n>>\n>> ----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----\n>>\n>>   diff --git a/config.c b/config.c\n>>   index 015bec360f..208a3dd7a7 100644\n>>   --- a/config.c\n>>   +++ b/config.c\n>>   @@ -2645,9 +2645,13 @@ static void read_protected_config(void)\n>>     system_config = git_system_config();\n>>     git_global_config(&user_config, &xdg_config);\n>>\n>>   -\tgit_configset_add_file(&protected_config, system_config);\n>>   -\tgit_configset_add_file(&protected_config, xdg_config);\n>>   -\tgit_configset_add_file(&protected_config, user_config);\n>>   +\n>>   +\tif (system_config)\n>>   +\t\tgit_configset_add_file(&protected_config, system_config);\n>>   +\tif (xdg_config)\n>>   +\t\tgit_configset_add_file(&protected_config, xdg_config);\n>>   +\tif (user_config)\n>>   +\t\tgit_configset_add_file(&protected_config, user_config);\n>>     git_configset_add_parameters(&protected_config);\n>>\n>>     free(system_config);\n>>\n>> ----- >8 --------- >8 --------- >8 --------- >8 --------- >8 ----\n>>\n>> I'm not sure if system_config can ever be NULL, but (xdg|user)_config is\n>> NULL when $HOME is unset, and xdg_config is also unset if\n>> $GIT_CONFIG_GLOBAL is set.\n>\n> Not having looked into it much at all: Doesn't this then introduce\n> another logic error where git_protected_config() is now buggy, i.e. it's\n> a \"lazy load\" method where we'll expect to read_protected_config()\n> first.\n>\n> The assumption with that seems to have been that it's invariant within a\n> single process, is that still the case, or can e.g. HOME be set during\n> our runtime when we rely on these functions?\n>\n> (I don't know)\n\nI don't think this introduces an error, or at least, not one that we\ndon't already have. This mimics do_git_config_sequence() (which also\nassumes this invariant), which is used under the hood by\n(git|repo)_read_config(),\n\nIn retrospect, it might have been a good idea to implement\nread_protected_config() using do_git_config_sequence() /\nconfig_with_options(); those functions are a bit bloated, but at least\nwe'd only have one implementation.\n"}]}