promisor-remote: prevent lazy-fetch recursion in child fetch

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 <github@paulisageek.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Paul Tarjan
2026-03-04 18:27:25 +00:00
committed by Junio C Hamano
parent 67ad42147a
commit 087cc88450
2 changed files with 33 additions and 0 deletions

View File

@@ -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",

View File

@@ -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