From 087cc88450a59674c809b5ce646347b2ed953d71 Mon Sep 17 00:00:00 2001 From: Paul Tarjan Date: Wed, 4 Mar 2026 18:27:25 +0000 Subject: [PATCH] promisor-remote: prevent lazy-fetch recursion in child fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetch_objects() spawns a child `git fetch` to lazily fill in missing objects. That child's index-pack, when it receives a thin pack containing a REF_DELTA against a still-missing base, explicitly calls promisor_remote_get_direct() — which is fetch_objects() again. If the base is truly unavailable (e.g. because many refs in the local store point at objects that have been garbage-collected on the server), each recursive lazy-fetch can trigger another, leading to unbounded recursion with runaway disk and process consumption. The GIT_NO_LAZY_FETCH guard (introduced by e6d5479e7a (git: add --no-lazy-fetch option, 2021-08-31)) already exists at the top of fetch_objects(); the missing piece is propagating it into the child fetch's environment. Add that propagation so the child's index-pack, if it encounters a REF_DELTA against a missing base, hits the guard and fails fast instead of recursing. Depth-1 lazy fetch (the whole point of fetch_objects()) is unaffected: only the child and its descendants see the variable. With negotiationAlgorithm=noop the client advertises no "have" lines, so a well-behaved server sends requested objects un-deltified or deltified only against objects in the same pack; the child's index-pack should never need a depth-2 fetch. If it does, the server response was broken or the local store is already corrupt, and further fetching would not help. This is the same bug shape that 3a1ea94a49 (commit-graph.c: no lazy fetch in lookup_commit_in_graph(), 2022-07-01) addressed at a different entry point. Add a test that verifies the child fetch environment contains GIT_NO_LAZY_FETCH=1 via a reference-transaction hook. Signed-off-by: Paul Tarjan Signed-off-by: Junio C Hamano --- promisor-remote.c | 7 +++++++ t/t0411-clone-from-partial.sh | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/promisor-remote.c b/promisor-remote.c index 77ebf537e2..2f56e89404 100644 --- a/promisor-remote.c +++ b/promisor-remote.c @@ -42,6 +42,13 @@ static int fetch_objects(struct repository *repo, child.in = -1; if (repo != the_repository) prepare_other_repo_env(&child.env, repo->gitdir); + /* + * Prevent the child's index-pack from recursing back into + * fetch_objects() when resolving REF_DELTA bases it does not + * have. With noop negotiation the server should never need + * to send such deltas, so a depth-2 fetch would not help. + */ + strvec_pushf(&child.env, "%s=1", NO_LAZY_FETCH_ENVIRONMENT); strvec_pushl(&child.args, "-c", "fetch.negotiationAlgorithm=noop", "fetch", remote_name, "--no-tags", "--no-write-fetch-head", "--recurse-submodules=no", diff --git a/t/t0411-clone-from-partial.sh b/t/t0411-clone-from-partial.sh index 9e6bca5625..10a829fb80 100755 --- a/t/t0411-clone-from-partial.sh +++ b/t/t0411-clone-from-partial.sh @@ -78,4 +78,30 @@ test_expect_success 'promisor lazy-fetching can be re-enabled' ' test_path_is_file script-executed ' +test_expect_success 'lazy-fetch child has GIT_NO_LAZY_FETCH=1' ' + test_create_repo nolazy-server && + test_commit -C nolazy-server foo && + git -C nolazy-server repack -a -d --write-bitmap-index && + + git clone "file://$(pwd)/nolazy-server" nolazy-client && + HASH=$(git -C nolazy-client rev-parse foo) && + rm -rf nolazy-client/.git/objects/* && + + git -C nolazy-client config core.repositoryformatversion 1 && + git -C nolazy-client config extensions.partialclone "origin" && + + # Install a reference-transaction hook to record the env var + # as seen by processes inside the child fetch. + test_hook -C nolazy-client reference-transaction <<-\EOF && + echo "$GIT_NO_LAZY_FETCH" >>../env-in-child + EOF + + rm -f env-in-child && + git -C nolazy-client cat-file -p "$HASH" && + + # The hook runs inside the child fetch, which should have + # GIT_NO_LAZY_FETCH=1 in its environment. + grep "^1$" env-in-child +' + test_done