Message ID | d5a3e9f98450a0d602cf21790b988c1259a3466d.1653685761.git.gitgitgadget@gmail.com (mailing list archive) |
---|---|
State | New, archived |
Headers | show |
Series | config: introduce discovery.bare and protected config | expand |
"Glen Choo via GitGitGadget" <gitgitgadget@gmail.com> writes: > +enum discovery_bare_config { > + DISCOVERY_BARE_UNKNOWN = -1, > + DISCOVERY_BARE_NEVER = 0, > + DISCOVERY_BARE_ALWAYS, > +}; > +static enum discovery_bare_config discovery_bare_config = > + DISCOVERY_BARE_UNKNOWN; Can discovery_bare come from anywhere other than config? I am wondering if both the variable and the type should be called "discovery_bare_allowed" instead. That it comes from the config is not the more important part. That it determines if it is allowed is. > +static int check_bare_repo_allowed(void) > +{ > + if (discovery_bare_config == DISCOVERY_BARE_UNKNOWN) { > + discovery_bare_config = DISCOVERY_BARE_ALWAYS; > + git_protected_config(discovery_bare_cb, NULL); > + } OK, so the thing is initialized to "unknown", and the first time we want to use the value of it, we read from the file (or default to "always"). Makes sense. And then ... > + switch (discovery_bare_config) { > + case DISCOVERY_BARE_NEVER: > + return 0; > + case DISCOVERY_BARE_ALWAYS: > + return 1; > + case DISCOVERY_BARE_UNKNOWN: > + BUG("invalid discovery_bare_config %d", discovery_bare_config); ... this is being defensive; we know discovery_bare_cb() won't give UNKNOWN, but we want to make sure. > + } > + return 0; > +} > + > +static const char *discovery_bare_config_to_string(void) > +{ But this one feels strangely asymmetrical, as there is no inherent reason why one must be called before the other. I would expect it to either * take a parameter of type "enum discovery_bare" and return "never", "always", or "unset", without calling any BUG(). or * have the same "we lazily figure out the discovery_bare_config variable on demand" logic. As both of these functions are file-scope static, we can live with it, though. > + switch (discovery_bare_config) { > + case DISCOVERY_BARE_NEVER: > + return "never"; > + case DISCOVERY_BARE_ALWAYS: > + return "always"; > + case DISCOVERY_BARE_UNKNOWN: > + BUG("invalid discovery_bare_config %d", discovery_bare_config); > + } > + return NULL; > +}
On 5/27/2022 5:09 PM, Glen Choo via GitGitGadget wrote: > +enum discovery_bare_config { > + DISCOVERY_BARE_UNKNOWN = -1, > + DISCOVERY_BARE_NEVER = 0, > + DISCOVERY_BARE_ALWAYS, > +}; > +static enum discovery_bare_config discovery_bare_config = > + DISCOVERY_BARE_UNKNOWN; Using this static global is fine, I think. > +static int discovery_bare_cb(const char *key, const char *value, void *d) > +{ > + if (strcmp(key, "discovery.bare")) > + return 0; > + > + if (!strcmp(value, "never")) { > + discovery_bare_config = DISCOVERY_BARE_NEVER; > + return 0; > + } > + if (!strcmp(value, "always")) { > + discovery_bare_config = DISCOVERY_BARE_ALWAYS; > + return 0; > + } > + return -1; > +} However, I do think that this _cb method could benefit from interpreting the 'd' pointer as a 'enum discovery_bare_config *' and assigning the value at the pointer. We can then pass the global to the git_protected_config() call below. This is probably over-defensive future-proofing, but this kind of change would be necessary if we ever wanted to return the enum instead of simply an integer, as below: > + > +static int check_bare_repo_allowed(void) > +{ > + if (discovery_bare_config == DISCOVERY_BARE_UNKNOWN) { > + discovery_bare_config = DISCOVERY_BARE_ALWAYS; > + git_protected_config(discovery_bare_cb, NULL); > + } > + switch (discovery_bare_config) { > + case DISCOVERY_BARE_NEVER: > + return 0; > + case DISCOVERY_BARE_ALWAYS: > + return 1; > + case DISCOVERY_BARE_UNKNOWN: > + BUG("invalid discovery_bare_config %d", discovery_bare_config); > + } > + return 0; > +} With the recommended change to the _cb method, we could rewrite this as static enum discovery_bare_config get_discovery_bare(void) { enum discovery_bare_config result = DISCOVERY_BARE_ALWAYS; git_protected_config(discovery_bare_cb, &result); return result; } With this, we can drop the UNKNOWN and let the caller treat the response as a simple boolean. I think this is simpler overall, but also makes it easier to extend in the future to have "discovery.bare=non-embedded" by adding a new mode and adjusting the consumer in setup_git_directory_gently_1() to use a switch() on the resurned enum. > + > +static const char *discovery_bare_config_to_string(void) > +{ > + switch (discovery_bare_config) { > + case DISCOVERY_BARE_NEVER: > + return "never"; > + case DISCOVERY_BARE_ALWAYS: > + return "always"; > + case DISCOVERY_BARE_UNKNOWN: > + BUG("invalid discovery_bare_config %d", discovery_bare_config); This case should be a "default:" in case somehow an arbitrary integer value was placed in the variable. This could also take an enum as a parameter, to avoid being coupled to the global. > +++ b/t/t0035-discovery-bare.sh > @@ -0,0 +1,64 @@ > +#!/bin/sh > + > +test_description='verify discovery.bare checks' > + > +. ./test-lib.sh > + > +pwd="$(pwd)" > + > +expect_rejected () { > + test_must_fail git rev-parse --git-dir 2>err && > + grep "discovery.bare" err > +} Should we make a simple "expect_accepted" helper in case we ever want to replace the "git rev-parse --git-dir" with anything else? > + > +test_expect_success 'setup bare repo in worktree' ' > + git init outer-repo && > + git init --bare outer-repo/bare-repo > +' > + > +test_expect_success 'discovery.bare unset' ' > + ( > + cd outer-repo/bare-repo && > + git rev-parse --git-dir > + ) > +' > + > +test_expect_success 'discovery.bare=always' ' > + git config --global discovery.bare always && > + ( > + cd outer-repo/bare-repo && > + git rev-parse --git-dir > + ) > +' > + > +test_expect_success 'discovery.bare=never' ' > + git config --global discovery.bare never && > + ( > + cd outer-repo/bare-repo && > + expect_rejected > + ) > +' > + > +test_expect_success 'discovery.bare in the repository' ' > + ( > + cd outer-repo/bare-repo && > + # Temporarily set discovery.bare=always, otherwise git > + # config fails with "fatal: not in a git directory" > + # (like safe.directory) > + git config --global discovery.bare always && > + git config discovery.bare always && > + git config --global discovery.bare never && > + expect_rejected > + ) > +' > + > +test_expect_success 'discovery.bare on the command line' ' > + git config --global discovery.bare never &&> + ( > + cd outer-repo/bare-repo && > + test_must_fail git -c discovery.bare=always rev-parse --git-dir 2>err && > + grep "discovery.bare" err > + ) Ok, at the current place in the series, this test_must_fail matches expectation. If you reorder to have this patch after your current patch 4, then we can write this test immediately as a successful case. We could also reuse some information from the expect_rejected helper by adding this: expect_rejected () { test_must_fail git $* rev-parse --git-dir 2>err && grep "discovery.bare" err } Then you can test the -c options in the tests as expect_rejected -c discovery.bare=always Thanks, -Stolee
diff --git a/Documentation/config.txt b/Documentation/config.txt index 07832de1a6c..34133288d75 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -415,6 +415,8 @@ include::config/diff.txt[] include::config/difftool.txt[] +include::config/discovery.txt[] + include::config/extensions.txt[] include::config/fastimport.txt[] diff --git a/Documentation/config/discovery.txt b/Documentation/config/discovery.txt new file mode 100644 index 00000000000..fbe93597e7c --- /dev/null +++ b/Documentation/config/discovery.txt @@ -0,0 +1,19 @@ +discovery.bare:: + '(Protected config only)' Specifies whether Git will work with a + bare repository that it found during repository discovery. This + has no effect if the repository is specified directly via the + --git-dir command-line option or the GIT_DIR environment + variable (see linkgit:git[1]). ++ +The currently supported values are: ++ +* `always`: Git always works with bare repositories +* `never`: Git never works with bare repositories ++ +This defaults to `always`, but this default may change in the future. ++ +If you do not use bare repositories in your workflow, then it may be +beneficial to set `discovery.bare` to `never` in your global config. +This will protect you from attacks that involve cloning a repository +that contains a bare repository and running a Git command within that +directory. diff --git a/setup.c b/setup.c index 847d47f9195..6686743ab7d 100644 --- a/setup.c +++ b/setup.c @@ -10,6 +10,13 @@ static int inside_git_dir = -1; static int inside_work_tree = -1; static int work_tree_config_is_bogus; +enum discovery_bare_config { + DISCOVERY_BARE_UNKNOWN = -1, + DISCOVERY_BARE_NEVER = 0, + DISCOVERY_BARE_ALWAYS, +}; +static enum discovery_bare_config discovery_bare_config = + DISCOVERY_BARE_UNKNOWN; static struct startup_info the_startup_info; struct startup_info *startup_info = &the_startup_info; @@ -1133,6 +1140,52 @@ static int ensure_valid_ownership(const char *path) return data.is_safe; } +static int discovery_bare_cb(const char *key, const char *value, void *d) +{ + if (strcmp(key, "discovery.bare")) + return 0; + + if (!strcmp(value, "never")) { + discovery_bare_config = DISCOVERY_BARE_NEVER; + return 0; + } + if (!strcmp(value, "always")) { + discovery_bare_config = DISCOVERY_BARE_ALWAYS; + return 0; + } + return -1; +} + +static int check_bare_repo_allowed(void) +{ + if (discovery_bare_config == DISCOVERY_BARE_UNKNOWN) { + discovery_bare_config = DISCOVERY_BARE_ALWAYS; + git_protected_config(discovery_bare_cb, NULL); + } + switch (discovery_bare_config) { + case DISCOVERY_BARE_NEVER: + return 0; + case DISCOVERY_BARE_ALWAYS: + return 1; + case DISCOVERY_BARE_UNKNOWN: + BUG("invalid discovery_bare_config %d", discovery_bare_config); + } + return 0; +} + +static const char *discovery_bare_config_to_string(void) +{ + switch (discovery_bare_config) { + case DISCOVERY_BARE_NEVER: + return "never"; + case DISCOVERY_BARE_ALWAYS: + return "always"; + case DISCOVERY_BARE_UNKNOWN: + BUG("invalid discovery_bare_config %d", discovery_bare_config); + } + return NULL; +} + enum discovery_result { GIT_DIR_NONE = 0, GIT_DIR_EXPLICIT, @@ -1142,7 +1195,8 @@ enum discovery_result { GIT_DIR_HIT_CEILING = -1, GIT_DIR_HIT_MOUNT_POINT = -2, GIT_DIR_INVALID_GITFILE = -3, - GIT_DIR_INVALID_OWNERSHIP = -4 + GIT_DIR_INVALID_OWNERSHIP = -4, + GIT_DIR_DISALLOWED_BARE = -5, }; /* @@ -1239,6 +1293,8 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir, } if (is_git_directory(dir->buf)) { + if (!check_bare_repo_allowed()) + return GIT_DIR_DISALLOWED_BARE; if (!ensure_valid_ownership(dir->buf)) return GIT_DIR_INVALID_OWNERSHIP; strbuf_addstr(gitdir, "."); @@ -1385,6 +1441,14 @@ const char *setup_git_directory_gently(int *nongit_ok) } *nongit_ok = 1; break; + case GIT_DIR_DISALLOWED_BARE: + if (!nongit_ok) { + die(_("cannot use bare repository '%s' (discovery.bare is '%s')"), + dir.buf, + discovery_bare_config_to_string()); + } + *nongit_ok = 1; + break; case GIT_DIR_NONE: /* * As a safeguard against setup_git_directory_gently_1 returning diff --git a/t/t0035-discovery-bare.sh b/t/t0035-discovery-bare.sh new file mode 100755 index 00000000000..94c2f76d774 --- /dev/null +++ b/t/t0035-discovery-bare.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +test_description='verify discovery.bare checks' + +. ./test-lib.sh + +pwd="$(pwd)" + +expect_rejected () { + test_must_fail git rev-parse --git-dir 2>err && + grep "discovery.bare" err +} + +test_expect_success 'setup bare repo in worktree' ' + git init outer-repo && + git init --bare outer-repo/bare-repo +' + +test_expect_success 'discovery.bare unset' ' + ( + cd outer-repo/bare-repo && + git rev-parse --git-dir + ) +' + +test_expect_success 'discovery.bare=always' ' + git config --global discovery.bare always && + ( + cd outer-repo/bare-repo && + git rev-parse --git-dir + ) +' + +test_expect_success 'discovery.bare=never' ' + git config --global discovery.bare never && + ( + cd outer-repo/bare-repo && + expect_rejected + ) +' + +test_expect_success 'discovery.bare in the repository' ' + ( + cd outer-repo/bare-repo && + # Temporarily set discovery.bare=always, otherwise git + # config fails with "fatal: not in a git directory" + # (like safe.directory) + git config --global discovery.bare always && + git config discovery.bare always && + git config --global discovery.bare never && + expect_rejected + ) +' + +test_expect_success 'discovery.bare on the command line' ' + git config --global discovery.bare never && + ( + cd outer-repo/bare-repo && + test_must_fail git -c discovery.bare=always rev-parse --git-dir 2>err && + grep "discovery.bare" err + ) +' + +test_done