diff --git a/Documentation/config/uploadpack.adoc b/Documentation/config/uploadpack.adoc index 0e1dda944a..e143de93aa 100644 --- a/Documentation/config/uploadpack.adoc +++ b/Documentation/config/uploadpack.adoc @@ -86,3 +86,52 @@ uploadpack.allowRefInWant:: is intended for the benefit of load-balanced servers which may not have the same view of what OIDs their refs point to due to replication delay. + +uploadpack.lazyFetchTrusted:: + A multi-valued configuration variable, each of which contains the + absolute local path of a repository that `upload-pack` is allowed to + lazily fetch missing objects for. ++ +A repository is identified by its git directory, i.e. the `.git` +directory of a repository that has a worktree, or the repository itself +if it is bare. So a non-bare repository served as `/srv/repo` has to be +allowlisted as `/srv/repo/.git`. Giving a path with `/*` appended to it +will trust all repositories under the named directory. To trust all +served repositories, set `uploadpack.lazyFetchTrusted` to the string +`*`. ++ +The value of this setting is interpolated, i.e. `~/` expands to a +path relative to the home directory and `%(prefix)/` expands to a +path relative to Git's (runtime) prefix. ++ +By default, `upload-pack` refuses to lazily fetch (see the description +of the `GIT_NO_LAZY_FETCH` environment variable in +linkgit:git-upload-pack[1]), because doing so would run `git fetch`, +which may execute arbitrary commands specified in the configuration +and hooks of the served repository. Listing a repository here tells +`upload-pack` that it is trusted, so lazy fetching from the promisor +remotes configured in it is allowed. This is equivalent to setting +`GIT_NO_LAZY_FETCH` to `0` for the matching repositories. An +explicitly set `GIT_NO_LAZY_FETCH` takes precedence over this setting. ++ +Note that this allows lazy fetching from any promisor remote +configured in the served repository, not only from the promisor +remotes that the client accepted using the "promisor-remote" protocol +v2 capability (see linkgit:gitprotocol-v2[5]). The served repository +is trusted as a whole, including its configuration, so the promisor +remotes it configures are trusted too. It is the server operator's +responsibility to make sure that the promisor remotes of a trusted +repository are also trustworthy. In particular, a trusted repository +should not be configured as its own promisor remote, as `upload-pack` +would then try to lazily fetch missing objects from the repository +itself, which is pointless. ++ +As this is a multi-valued setting, you can add more than one +repository via `git config (--global|--system) --add`. To reset the +list of trusted repositories (e.g. to override any such repositories +specified in the system config), add an `uploadpack.lazyFetchTrusted` +entry with an empty value. ++ +Note that this configuration variable is only respected when it is +specified in protected configuration (see <>). This prevents +untrusted repositories from tampering with this value. diff --git a/Documentation/git-upload-pack.adoc b/Documentation/git-upload-pack.adoc index 9167a321d0..90c2ba1194 100644 --- a/Documentation/git-upload-pack.adoc +++ b/Documentation/git-upload-pack.adoc @@ -71,6 +71,11 @@ This is implemented by having `upload-pack` internally set the (because you are fetching from a partial clone, and you are sure you trust it), you can explicitly set `GIT_NO_LAZY_FETCH` to `0`. ++ +Instead of setting `GIT_NO_LAZY_FETCH` to `0` in the environment, a +server operator can allow lazy fetching on a per-repository basis by +listing trusted repositories in the `uploadpack.lazyFetchTrusted` +configuration variable. See linkgit:git-config[1]. SECURITY -------- diff --git a/Documentation/git.adoc b/Documentation/git.adoc index 3abb4701c3..033de8d67d 100644 --- a/Documentation/git.adoc +++ b/Documentation/git.adoc @@ -958,7 +958,9 @@ for full details. `GIT_NO_LAZY_FETCH`:: Setting this Boolean environment variable to true tells Git not to lazily fetch missing objects from the promisor remote - on demand. + on demand. On the server side, the `uploadpack.lazyFetchTrusted` + configuration variable can control this per-repository. See + linkgit:git-upload-pack[1]. `GIT_REFLOG_ACTION`:: When a ref is updated, reflog entries are created to keep diff --git a/builtin/upload-pack.c b/builtin/upload-pack.c index 32831fb879..8b531ca724 100644 --- a/builtin/upload-pack.c +++ b/builtin/upload-pack.c @@ -42,10 +42,13 @@ int cmd_upload_pack(int argc, OPT_END() }; unsigned enter_repo_flags = ENTER_REPO_ANY_OWNER_OK; + bool no_lazy_fetch_set; packet_trace_identity("upload-pack"); disable_replace_refs(); save_commit_buffer = 0; + + no_lazy_fetch_set = !!getenv(NO_LAZY_FETCH_ENVIRONMENT); xsetenv(NO_LAZY_FETCH_ENVIRONMENT, "1", 0); argc = parse_options(argc, argv, prefix, options, upload_pack_usage, 0); @@ -62,6 +65,14 @@ int cmd_upload_pack(int argc, if (!enter_repo(the_repository, dir, enter_repo_flags)) die("'%s' does not appear to be a git repository", dir); + /* + * Relax the GIT_NO_LAZY_FETCH=1 default if the served repo is in + * the "uploadpack.lazyFetchTrusted" protected allowlist and + * GIT_NO_LAZY_FETCH was not already set explicitly. + */ + if (!no_lazy_fetch_set && upload_pack_lazy_fetch_trusted(the_repository)) + xsetenv(NO_LAZY_FETCH_ENVIRONMENT, "0", 1); + switch (determine_protocol_version_server()) { case protocol_v2: if (advertise_refs) diff --git a/environment.h b/environment.h index 42e086b89d..a82b442dde 100644 --- a/environment.h +++ b/environment.h @@ -53,6 +53,14 @@ */ #define GIT_ADVICE_ENVIRONMENT "GIT_ADVICE" +/* + * Environment variable used to detect that a lazy fetch is already in + * progress in a parent process, to prevent infinite recursion when a + * promisor remote resolves back to the repository being served. + * This is an internal variable that should not be set by the user. + */ +#define LAZY_FETCH_DEPTH_ENVIRONMENT "GIT_INTERNAL_LAZY_FETCH_DEPTH" + /* * Environment variable used in handshaking the wire protocol. * Contains a colon ':' separated list of keys with optional values diff --git a/promisor-remote.c b/promisor-remote.c index 43505d1e1a..e9c5b413f1 100644 --- a/promisor-remote.c +++ b/promisor-remote.c @@ -24,22 +24,13 @@ struct promisor_remote_config { static int fetch_objects(struct repository *repo, const char *remote_name, const struct object_id *oids, - int oid_nr) + int oid_nr, unsigned long depth) { struct child_process child = CHILD_PROCESS_INIT; int i; FILE *child_in; int quiet; - if (git_env_bool(NO_LAZY_FETCH_ENVIRONMENT, 0)) { - static int warning_shown; - if (!warning_shown) { - warning_shown = 1; - warning(_("lazy fetching disabled; some objects may not be available")); - } - return -1; - } - child.git_cmd = 1; child.in = -1; if (repo != the_repository) @@ -50,6 +41,7 @@ static int fetch_objects(struct repository *repo, "--filter=blob:none", "--stdin", NULL); if (!repo_config_get_bool(repo, "promisor.quiet", &quiet) && quiet) strvec_push(&child.args, "--quiet"); + strvec_pushf(&child.env, "%s=%lu", LAZY_FETCH_DEPTH_ENVIRONMENT, depth + 1); if (start_command(&child)) die(_("promisor-remote: unable to fork off fetch subprocess")); child_in = xfdopen(child.in, "w"); @@ -270,17 +262,24 @@ static int remove_fetched_oids(struct repository *repo, return remaining_nr; } -static int try_promisor_remotes(struct repository *repo, - struct object_id **remaining_oids, - int *remaining_nr, int *to_free, - bool accepted_only) +/* + * Return 'true' if all the objects could be fetched from the + * (non-)accepted remotes, 'false' otherwise. + */ +static bool try_promisor_remotes(struct repository *repo, + struct object_id **remaining_oids, + int *remaining_nr, + int *to_free, + unsigned long depth, + bool accepted_only) { struct promisor_remote *r = repo->promisor_remote_config->promisors; for (; r; r = r->next) { if (accepted_only != r->accepted) continue; - if (fetch_objects(repo, r->name, *remaining_oids, *remaining_nr) < 0) { + if (fetch_objects(repo, r->name, + *remaining_oids, *remaining_nr, depth) < 0) { if (*remaining_nr == 1) continue; *remaining_nr = remove_fetched_oids(repo, remaining_oids, @@ -290,9 +289,50 @@ static int try_promisor_remotes(struct repository *repo, continue; } } - return 1; /* all fetched */ + return true; /* all fetched */ } - return 0; + return false; +} + +#define MAX_LAZY_FETCH_DEPTH 5 + +/* + * Return 'true' if all the objects could be fetched, 'false' otherwise. + */ +static bool lazy_fetch_objects(struct repository *repo, + struct object_id **remaining_oids, + int *remaining_nr, + int *to_free) +{ + unsigned long depth = git_env_ulong(LAZY_FETCH_DEPTH_ENVIRONMENT, 0); + + if (git_env_bool(NO_LAZY_FETCH_ENVIRONMENT, 0)) { + static int warning_shown; + if (!warning_shown) { + warning_shown = 1; + warning(_("lazy fetching disabled; some objects may not be available")); + } + return false; + } + + if (depth >= MAX_LAZY_FETCH_DEPTH) { + static int warning_shown; + if (!warning_shown) { + warning_shown = 1; + warning(_("too many nested lazy fetches (%lu); " + "is a promisor remote pointing at the repository itself?"), + depth); + } + return false; + } + + promisor_remote_init(repo); + + /* Try accepted remotes first (those the server told us to use) */ + return try_promisor_remotes(repo, remaining_oids, remaining_nr, + to_free, depth, true) || + try_promisor_remotes(repo, remaining_oids, remaining_nr, + to_free, depth, false); } void promisor_remote_get_direct(struct repository *repo, @@ -302,28 +342,18 @@ void promisor_remote_get_direct(struct repository *repo, struct object_id *remaining_oids = (struct object_id *)oids; int remaining_nr = oid_nr; int to_free = 0; - int i; if (oid_nr == 0) return; - promisor_remote_init(repo); - - /* Try accepted remotes first (those the server told us to use) */ - if (try_promisor_remotes(repo, &remaining_oids, &remaining_nr, - &to_free, true)) - goto all_fetched; - if (try_promisor_remotes(repo, &remaining_oids, &remaining_nr, - &to_free, false)) - goto all_fetched; - - for (i = 0; i < remaining_nr; i++) { - if (is_promisor_object(repo, &remaining_oids[i])) - die(_("could not fetch %s from promisor remote"), - oid_to_hex(&remaining_oids[i])); + if (!lazy_fetch_objects(repo, &remaining_oids, &remaining_nr, &to_free)) { + for (int i = 0; i < remaining_nr; i++) { + if (is_promisor_object(repo, &remaining_oids[i])) + die(_("could not fetch %s from promisor remote"), + oid_to_hex(&remaining_oids[i])); + } } -all_fetched: if (to_free) free(remaining_oids); } diff --git a/setup.c b/setup.c index 29474fc292..c6faf3e5ac 100644 --- a/setup.c +++ b/setup.c @@ -1337,67 +1337,105 @@ static int canonicalize_ceiling_entry(struct string_list_item *item, } } +void path_allowlist_apply(const char *allowed, const char *target_path, + bool *matches, + bool (*allow_path)(const char *path, void *cbdata), + void *allow_path_cbdata) +{ + char *normalized = NULL; + + if (!allowed || !*allowed) { + *matches = false; + return; + } + + if (!strcmp(allowed, "*")) { + *matches = true; + return; + } + + if (!allow_path(allowed, allow_path_cbdata)) + return; + + /* + * A .gitconfig in $HOME may be shared across different + * machines and the config variable entries may or may not + * exist as paths on all of these machines. In other words, + * it is not a warning worthy event when there is no such path + * on this machine---the entry may be useful elsewhere. + */ + normalized = real_pathdup(allowed, 0); + if (!normalized) + return; + + if (ends_with(normalized, "/*")) { + size_t len = strlen(normalized); + if (!fspathncmp(normalized, target_path, len - 1)) + *matches = true; + } else if (!fspathcmp(target_path, normalized)) { + *matches = true; + } + + free(normalized); +} + +void path_allowlist_config_apply(const char *key, const char *value, + const char *target_path, bool *matches, + bool (*allow_path)(const char *path, void *cbdata), + void *allow_path_cbdata) +{ + char *allowed = NULL; + + if (!value || !*value || !strcmp(value, "*")) { + path_allowlist_apply(value, target_path, matches, + allow_path, allow_path_cbdata); + return; + } + + if (git_config_pathname(&allowed, key, value) || !allowed) + return; + + path_allowlist_apply(allowed, target_path, matches, + allow_path, allow_path_cbdata); + + free(allowed); +} + +/* + * Setting the config variable to a non-absolute path makes + * little sense---it won't be relative to the configuration + * file the item is defined in. Except for ".", which means + * "if we are at the top level of a repository, then it is + * OK", which is slightly tighter than "*" that allows + * discovery. + */ +static bool allow_safe_dir(const char *path, void *cbdata_) +{ + struct path_allowlist_cb_data *cbdata = cbdata_; + + if (is_absolute_path(path) || !strcmp(path, ".")) + return true; + + warning(_("%s '%s' not absolute"), cbdata->key, path); + return false; +} + struct safe_directory_data { char *path; - int is_safe; + bool safe; }; static int safe_directory_cb(const char *key, const char *value, const struct config_context *ctx UNUSED, void *d) { struct safe_directory_data *data = d; + struct path_allowlist_cb_data cbdata = { .key = key }; if (strcmp(key, "safe.directory")) return 0; - if (!value || !*value) { - data->is_safe = 0; - } else if (!strcmp(value, "*")) { - data->is_safe = 1; - } else { - char *allowed = NULL; - - if (!git_config_pathname(&allowed, key, value) && allowed) { - char *normalized = NULL; - - /* - * Setting safe.directory to a non-absolute path - * makes little sense---it won't be relative to - * the configuration file the item is defined in. - * Except for ".", which means "if we are at the top - * level of a repository, then it is OK", which is - * slightly tighter than "*" that allows discovery. - */ - if (!is_absolute_path(allowed) && strcmp(allowed, ".")) { - warning(_("safe.directory '%s' not absolute"), - allowed); - goto next; - } - - /* - * A .gitconfig in $HOME may be shared across - * different machines and safe.directory entries - * may or may not exist as paths on all of these - * machines. In other words, it is not a warning - * worthy event when there is no such path on this - * machine---the entry may be useful elsewhere. - */ - normalized = real_pathdup(allowed, 0); - if (!normalized) - goto next; - - if (ends_with(normalized, "/*")) { - size_t len = strlen(normalized); - if (!fspathncmp(normalized, data->path, len - 1)) - data->is_safe = 1; - } else if (!fspathcmp(data->path, normalized)) { - data->is_safe = 1; - } - next: - free(normalized); - free(allowed); - } - } + path_allowlist_config_apply(key, value, data->path, &data->safe, + allow_safe_dir, &cbdata); return 0; } @@ -1439,7 +1477,7 @@ static int ensure_valid_ownership(const char *gitfile, git_protected_config(safe_directory_cb, &data); free(data.path); - return data.is_safe; + return data.safe; } void die_upon_dubious_ownership(const char *gitfile, const char *worktree, diff --git a/setup.h b/setup.h index 7a3efe5e48..871c5d0e78 100644 --- a/setup.h +++ b/setup.h @@ -328,4 +328,54 @@ struct startup_info { extern struct startup_info *startup_info; extern const char *tmp_original_cwd; +/* Path allowlist */ + +struct path_allowlist_cb_data { + const char *key; +}; + +/* + * Check the allowlist entry in `allowed` against `target_path`, + * updating `*matches` accordingly. + * + * `allowed` is a single entry of an allowlist of paths, typically one + * value of a multi-valued config variable, already expanded by + * git_config_pathname(). `target_path` is the (normalized) path being + * tested. `*matches` is updated in place: + * + * - an empty `allowed` resets it to 'false' (so a later, more + * specific config scope can clear entries from a broader one), + * - "*" sets it to 'true' (allow everything), + * - "" sets it to 'true' if equals `target_path`, + * - "" + "/" + "*" sets it to 'true' if is a leading + * directory of `target_path`, + * - anything else leaves `*matches` unchanged. + * + * `allow_path` is called with `allowed` and `allow_path_cbdata`, and + * should return 'true' if the entry is acceptable to the caller. It + * lets each caller decide which paths it is willing to consider, and + * whether to warn about the ones it rejects. Returning 'false' leaves + * `*matches` unchanged. + * + * Callers are expected to invoke this once per allowlist entry, + * typically from a protected-config callback, so that untrusted + * repository config cannot influence the decision. + */ +void path_allowlist_apply(const char *allowed, const char *target_path, + bool *matches, + bool (*allow_path)(const char *path, void *cbdata), + void *allow_path_cbdata); + +/* + * Apply one value of a multi-valued config variable holding an + * allowlist of paths, expanding it with git_config_pathname() before + * checking it against `target_path`. Empty and "*" values are passed + * through without expansion, as interpolating them is not + * meaningful. See path_allowlist_apply(). + */ +void path_allowlist_config_apply(const char *key, const char *value, + const char *target_path, bool *matches, + bool (*allow_path)(const char *path, void *cbdata), + void *allow_path_cbdata); + #endif /* SETUP_H */ diff --git a/t/t0410-partial-clone.sh b/t/t0410-partial-clone.sh index 788e9a1631..a54685e3c7 100755 --- a/t/t0410-partial-clone.sh +++ b/t/t0410-partial-clone.sh @@ -709,6 +709,39 @@ test_expect_success 'lazy-fetch when accessing object not in the_repository' ' test_grep ! "[?]$FILE_HASH" out ' +test_expect_success 'lazy-fetch does not recurse infinitely between two promisor remotes' ' + rm -rf full partial1.git partial2.git && + + # Create a repo with a blob + test_create_repo full && + test_config -C full uploadpack.allowfilter 1 && + test_config -C full uploadpack.allowanysha1inwant 1 && + test_commit -C full create-a-file file.txt && + FILE_HASH=$(git -C full rev-parse HEAD:file.txt) && + + # Create partial clone repos without blobs + git clone --filter=blob:none --bare "file://$(pwd)/full" partial1.git && + git clone --filter=blob:none --bare "file://$(pwd)/full" partial2.git && + test_config -C partial1.git uploadpack.allowfilter 1 && + test_config -C partial1.git uploadpack.allowanysha1inwant 1 && + test_config -C partial2.git uploadpack.allowfilter 1 && + test_config -C partial2.git uploadpack.allowanysha1inwant 1 && + + # Configure the partial repos as remotes of each other + git -C partial2.git remote set-url origin "file://$(pwd)/partial1.git" && + git -C partial1.git remote set-url origin "file://$(pwd)/partial2.git" && + + # Make sure lazy fetching fails + test_must_fail env GIT_TRACE="$(pwd)/trace" GIT_NO_LAZY_FETCH=0 \ + git -C partial1.git cat-file -e "$FILE_HASH" 2>err && + test_grep "too many nested lazy fetches" err && + + # Make sure the recursion was bounded, i.e. that only + # MAX_LAZY_FETCH_DEPTH "git fetch" subprocesses were spawned + grep "run_command: GIT_INTERNAL_LAZY_FETCH_DEPTH" trace >fetches && + test_line_count = 5 fetches +' + test_expect_success 'push should not fetch new commit objects' ' rm -rf server client && test_create_repo server && diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh index 549acff23f..62f4b56006 100755 --- a/t/t5710-promisor-remote-capability.sh +++ b/t/t5710-promisor-remote-capability.sh @@ -173,6 +173,148 @@ test_expect_success "clone with promisor.acceptfromserver set to 'None'" ' initialize_server 1 "$oid" ' +test_expect_success "clone with uploadpack.lazyFetchTrusted" ' + # No promisors are advertised + git -C server config promisor.advertise false && + test_when_finished "rm -rf client" && + + # The served repo is trusted for lazy fetching + test_config_global uploadpack.lazyFetchTrusted "$(pwd)/server" && + + # Clone without GIT_NO_LAZY_FETCH=0 + git clone --no-local --filter="blob:limit=5k" server client && + + # Check that the largest object is not missing on the server + # This means the server lazy fetched it + check_missing_objects server 0 "" && + + # Reinitialize server so that the largest object is missing again + initialize_server 1 "$oid" +' + +test_expect_success "clone without uploadpack.lazyFetchTrusted fails" ' + # No promisors are advertised + git -C server config promisor.advertise false && + test_when_finished "rm -rf client" && + + # Note: no uploadpack.lazyFetchTrusted config is set here, so + # the served repo is NOT trusted for lazy fetching. + + # Clone without GIT_NO_LAZY_FETCH=0 fails + test_must_fail git clone --no-local --filter="blob:limit=5k" server client 2>err && + test_grep "lazy fetching disabled" err && + + # Check that the largest object is still missing on the server + check_missing_objects server 1 "$oid" +' + +test_expect_success "uploadpack.lazyFetchTrusted is ignored in repo config" ' + # No promisors are advertised + git -C server config promisor.advertise false && + test_when_finished "rm -rf client" && + + # The served repo is trusted for lazy fetching, but this is + # done in the repo config, not in protected config, so this is + # ignored. + test_config -C server uploadpack.lazyFetchTrusted "$(pwd)/server" && + + # Clone without GIT_NO_LAZY_FETCH=0 fails + test_must_fail git clone --no-local --filter="blob:limit=5k" server client 2>err && + test_grep "lazy fetching disabled" err && + + # Check that the largest object is still missing on the server + check_missing_objects server 1 "$oid" +' + +test_expect_success "explicit GIT_NO_LAZY_FETCH overrides uploadpack.lazyFetchTrusted" ' + # No promisors are advertised + git -C server config promisor.advertise false && + test_when_finished "rm -rf client" && + + # The served repo is trusted for lazy fetching + test_config_global uploadpack.lazyFetchTrusted "$(pwd)/server" && + + # But GIT_NO_LAZY_FETCH=1 disables lazy fetching, so clone fails + test_must_fail env GIT_NO_LAZY_FETCH=1 git clone --no-local \ + --filter="blob:limit=5k" server client 2>err && + test_grep "lazy fetching disabled" err && + + # Check that the largest object is still missing on the server + check_missing_objects server 1 "$oid" +' + +test_expect_success "trusted repo as its own promisor remote does not recurse" ' + # No promisors are advertised + git -C server config promisor.advertise false && + test_when_finished "rm -rf client" && + + # Add itself as its own remote + git -C server remote add self "$TRASH_DIRECTORY_URL/server" && + git -C server config remote.self.promisor true && + test_when_finished "git -C server remote remove self" && + + # Make "self" the only promisor remote of the server, so that it + # cannot get the missing object from "lop". Note that + # "remote.lop.partialCloneFilter" also makes "lop" a promisor + # remote, so it has to be unset too. + git -C server config --unset remote.lop.promisor && + test_when_finished "git -C server config remote.lop.promisor true" && + lop_filter="$(git -C server config remote.lop.partialCloneFilter)" && + git -C server config --unset remote.lop.partialCloneFilter && + test_when_finished "git -C server config remote.lop.partialCloneFilter \"$lop_filter\"" && + + # Allow lazy fetching from itself + test_config_global uploadpack.lazyFetchTrusted "$(pwd)/server" && + + # Check that lazy fetching fails + test_must_fail git clone --no-local --filter="blob:limit=5k" server client 2>err && + test_grep "too many nested lazy fetches" err && + + # Check that the largest object is still missing on the server + check_missing_objects server 1 "$oid" +' + +test_expect_success "uploadpack.lazyFetchTrusted needs the git dir of a non-bare repo" ' + test_when_finished "rm -rf nonbare client client2" && + + # Create a non-bare repo, without any worktree content, so that + # its largest object can be filtered out below + git init nonbare && + git -C nonbare remote add origin "$TRASH_DIRECTORY_URL/template" && + git -C nonbare fetch origin && + git -C nonbare update-ref HEAD FETCH_HEAD && + + git -C nonbare remote add lop "$TRASH_DIRECTORY_URL/lop" && + git -C nonbare config remote.lop.promisor true && + git -C nonbare config uploadpack.allowFilter true && + git -C nonbare config uploadpack.allowAnySHA1InWant true && + git -C nonbare config promisor.advertise false && + + # Repack everything, then repack without the largest object and + # create a promisor pack, like initialize_server() does + git -C nonbare -c repack.writebitmaps=false repack -a -d && + rm -f nonbare/.git/objects/pack/*.promisor && + git -C nonbare -c repack.writebitmaps=false repack -a -d \ + --filter=blob:limit=5k --filter-to="$(pwd)/nonbare-pack" && + promisor_file=$(ls nonbare/.git/objects/pack/*.pack | sed "s/\.pack/.promisor/") && + >"$promisor_file" && + check_missing_objects nonbare 1 "$oid" && + + # The worktree path does not identify the repo, so it is not + # trusted and the clone fails + test_config_global uploadpack.lazyFetchTrusted "$(pwd)/nonbare" && + test_must_fail git clone --no-local --filter="blob:limit=1k" \ + nonbare client 2>err && + test_grep "lazy fetching disabled" err && + check_missing_objects nonbare 1 "$oid" && + + # The git dir identifies the repo, so it is trusted and the + # clone succeeds + test_config_global uploadpack.lazyFetchTrusted "$(pwd)/nonbare/.git" && + git clone --no-local --filter="blob:limit=1k" nonbare client2 && + check_missing_objects nonbare 0 "" +' + test_expect_success "init + fetch with promisor.advertise set to 'true'" ' git -C server config promisor.advertise true && test_when_finished "rm -rf client" && diff --git a/upload-pack.c b/upload-pack.c index 22573ad365..a300870fa9 100644 --- a/upload-pack.c +++ b/upload-pack.c @@ -34,6 +34,8 @@ #include "json-writer.h" #include "strmap.h" #include "promisor-remote.h" +#include "setup.h" +#include "abspath.h" /* Remember to update object flag allocation in object.h */ #define THEY_HAVE (1u << 11) @@ -1343,6 +1345,63 @@ static int upload_pack_config(const char *var, const char *value, return parse_hide_refs_config(var, value, "uploadpack", &data->hidden_refs); } +/* + * Only absolute paths make sense here. Unlike 'safe.directory', "." + * is not accepted, as the served repository is always identified by + * an absolute path. + */ +static bool allow_trusted_path(const char *path, void *cbdata_) +{ + struct path_allowlist_cb_data *cbdata = cbdata_; + + if (is_absolute_path(path)) + return true; + + warning(_("%s '%s' not absolute"), cbdata->key, path); + return false; +} + +struct lazy_fetch_trusted { + char *repo_path; + bool trusted; +}; + +static int upload_pack_protected_lazy_fetch_config(const char *var, const char *value, + const struct config_context *ctx UNUSED, + void *cb_data) +{ + struct lazy_fetch_trusted *data = cb_data; + struct path_allowlist_cb_data cbdata = { .key = var }; + + if (strcmp("uploadpack.lazyfetchtrusted", var)) + return 0; + + path_allowlist_config_apply(var, value, data->repo_path, &data->trusted, + allow_trusted_path, &cbdata); + + return 0; +} + +bool upload_pack_lazy_fetch_trusted(struct repository *r) +{ + struct lazy_fetch_trusted data = { 0 }; + + /* + * A served repository is identified by its git directory, as + * `upload-pack` uses enter_repo() instead of the usual repository + * discovery, so its worktree, if any, is never known here. + */ + data.repo_path = real_pathdup(r->gitdir, 0); + if (!data.repo_path) + return false; + + git_protected_config(upload_pack_protected_lazy_fetch_config, &data); + + free(data.repo_path); + + return !!data.trusted; +} + static int upload_pack_protected_config(const char *var, const char *value, const struct config_context *ctx UNUSED, void *cb_data) diff --git a/upload-pack.h b/upload-pack.h index d6ee25ea98..b2212992c3 100644 --- a/upload-pack.h +++ b/upload-pack.h @@ -12,4 +12,7 @@ struct strbuf; int upload_pack_advertise(struct repository *r, struct strbuf *value); +/* Is this repo trusted for lazy fetching? */ +bool upload_pack_lazy_fetch_trusted(struct repository *r); + #endif /* UPLOAD_PACK_H */