Thread (3 messages) flat view 3 messages, 2 authors, 4d ago
DORMANTno replies

[PATCH v2] pull: avoid segfault when commit lookup fails

From: Jiri Kuncar via GitGitGadget <hidden>
Date: 2026-09-21 14:32:48
Subsystem: the rest · Maintainer: Linus Torvalds

From: Jiri Kuncar <redacted>

get_can_ff() and already_up_to_date() pass the result of
lookup_commit_reference() straight to commit_list_insert() and
repo_is_descendant_of() without checking it.  When the object
behind HEAD or one of the merge heads cannot be parsed, e.g. because
a loose object was left truncated by a fetch or gc racing on the
same repository, lookup_commit_reference() returns NULL and
"git pull" segfaults instead of reporting the corruption.

Treat a failed lookup as "cannot fast-forward" and "not up to date",
so that the caller falls through to the normal merge path, which
already diagnoses the broken object and fails cleanly.

An alternative would be to report the breakage at each lookup site,
which could give a more precise diagnosis.  The minimal guards are
preferred because they do no more than is needed to avoid the
crash, and will be easy to drop once "git pull" is reworked to
resolve object names into commit objects early and pass those
around, at which point there will not be multiple lookups of the
same object name to guard in the first place.

The test corrupts the loose object in place rather than removing
it: a missing object that is still recorded in the commit-graph is
caught by the consistency check in fetch-pack before "git pull"
reaches the fast-forward check, so removing it would not exercise
the crash.

Signed-off-by: Jiri Kuncar <redacted>
---
    pull: avoid segfault when commit lookup fails
    
    Changes since v1:
    
     * Rewrite the commit message per SubmittingPatches (imperative mood,
       present-tense problem statement, alternatives considered), as pointed
       out by Junio.
     * Drop the "test -f"/"chmod"/truncate steps from the test in favour of
       "rm -f && echo garbage >", the idiom already used in t1450. Plain "rm
       -f" alone does not reproduce the crash: a missing object that is
       still in the commit-graph is caught by fetch-pack's consistency check
       before "git pull" reaches get_can_ff(), so the object has to remain
       present but unparseable. Documented this in a test comment and in the
       log message.
     * Drop the redundant "git fetch" in the test setup; "git clone" already
       populates refs/remotes/origin/*.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2223%2Fjirikuncar%2Fjk%2Fpull-null-merge-head-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2223/jirikuncar/jk/pull-null-merge-head-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2223

Range-diff vs v1:

 1:  db6ecf62ec ! 1:  c00ae9d699 pull: avoid crash of invalid merge head
     @@ Metadata
      Author: Jiri Kuncar [off-list ref]
      
       ## Commit message ##
     -    pull: avoid crash of invalid merge head
     +    pull: avoid segfault when commit lookup fails
      
     -    Adds NULL guards for lookup_commit_reference() to avoid segfaults.
     +    get_can_ff() and already_up_to_date() pass the result of
     +    lookup_commit_reference() straight to commit_list_insert() and
     +    repo_is_descendant_of() without checking it.  When the object
     +    behind HEAD or one of the merge heads cannot be parsed, e.g. because
     +    a loose object was left truncated by a fetch or gc racing on the
     +    same repository, lookup_commit_reference() returns NULL and
     +    "git pull" segfaults instead of reporting the corruption.
      
     -    Those invalid references are possibly caused by parallel fetches or
     -    gc racing on the same repository.
     +    Treat a failed lookup as "cannot fast-forward" and "not up to date",
     +    so that the caller falls through to the normal merge path, which
     +    already diagnoses the broken object and fails cleanly.
      
     -    This effectively treats failed lookup as "not up to date" so caller
     -    falls to a normal merge, which reports the broken object instead of
     -    crashing.
     +    An alternative would be to report the breakage at each lookup site,
     +    which could give a more precise diagnosis.  The minimal guards are
     +    preferred because they do no more than is needed to avoid the
     +    crash, and will be easy to drop once "git pull" is reworked to
     +    resolve object names into commit objects early and pass those
     +    around, at which point there will not be multiple lookups of the
     +    same object name to guard in the first place.
     +
     +    The test corrupts the loose object in place rather than removing
     +    it: a missing object that is still recorded in the commit-graph is
     +    caught by the consistency check in fetch-pack before "git pull"
     +    reaches the fast-forward check, so removing it would not exercise
     +    the crash.
      
          Signed-off-by: Jiri Kuncar [off-list ref]
      
     @@ t/t5520-pull.sh: test_expect_success 'git pull --rebase against local branch' '
      +	git clone up dn &&
      +	(
      +		cd dn &&
     -+		git -c fetch.unpackLimit=1000 fetch origin \
     -+			"+refs/heads/*:refs/remotes/origin/*" &&
      +		git commit-graph write --reachable &&
      +		oid=$(git rev-parse refs/remotes/origin/sideA) &&
      +		obj=.git/objects/$(test_oid_to_path "$oid") &&
     -+		test -f "$obj" &&
     -+		chmod u+w "$obj" &&
     -+		>"$obj" &&
     ++
     ++		# Corrupt the object instead of removing it: a missing
     ++		# object that is still in the commit-graph is caught by
     ++		# fetch before pull ever reaches the fast-forward check.
     ++		rm -f "$obj" &&
     ++		echo garbage >"$obj" &&
      +		test_must_fail git pull --no-rebase origin sideA sideB
      +	)
      +'


 builtin/pull.c  | 10 +++++++++-
 t/t5520-pull.sh | 27 +++++++++++++++++++++++++++
 2 files changed, 36 insertions(+), 1 deletion(-)
diff --git a/builtin/pull.c b/builtin/pull.c
index db3ee0aab3..80e79daeb9 100644
--- a/builtin/pull.c
+++ b/builtin/pull.c
@@ -800,8 +800,12 @@ static int get_can_ff(struct object_id *orig_head,
 
 	orig_merge_head = &merge_heads->oid[0];
 	head = lookup_commit_reference(the_repository, orig_head);
-	commit_list_insert(head, &list);
+	if (!head)
+		return 0;
 	merge_head = lookup_commit_reference(the_repository, orig_merge_head);
+	if (!merge_head)
+		return 0;
+	commit_list_insert(head, &list);
 	ret = repo_is_descendant_of(the_repository, merge_head, list);
 	commit_list_free(list);
 	if (ret < 0)
@@ -820,12 +824,16 @@ static int already_up_to_date(struct object_id *orig_head,
 	struct commit *ours;
 
 	ours = lookup_commit_reference(the_repository, orig_head);
+	if (!ours)
+		return 0;
 	for (size_t i = 0; i < merge_heads->nr; i++) {
 		struct commit_list *list = NULL;
 		struct commit *theirs;
 		int ok;
 
 		theirs = lookup_commit_reference(the_repository, &merge_heads->oid[i]);
+		if (!theirs)
+			return 0;
 		commit_list_insert(theirs, &list);
 		ok = repo_is_descendant_of(the_repository, ours, list);
 		commit_list_free(list);
diff --git a/t/t5520-pull.sh b/t/t5520-pull.sh
index 27f38ab3c8..b3ab8f4c94 100755
--- a/t/t5520-pull.sh
+++ b/t/t5520-pull.sh
@@ -888,4 +888,31 @@ test_expect_success 'git pull --rebase against local branch' '
 	test_cmp expect file2
 '
 
+test_expect_success 'pull does not crash when a merge head does not resolve' '
+	test_when_finished "rm -rf up dn" &&
+	git init up &&
+	(
+		cd up &&
+		test_commit base &&
+		git switch -c sideA &&
+		test_commit a &&
+		git switch -c sideB base &&
+		test_commit b
+	) &&
+	git clone up dn &&
+	(
+		cd dn &&
+		git commit-graph write --reachable &&
+		oid=$(git rev-parse refs/remotes/origin/sideA) &&
+		obj=.git/objects/$(test_oid_to_path "$oid") &&
+
+		# Corrupt the object instead of removing it: a missing
+		# object that is still in the commit-graph is caught by
+		# fetch before pull ever reaches the fast-forward check.
+		rm -f "$obj" &&
+		echo garbage >"$obj" &&
+		test_must_fail git pull --no-rebase origin sideA sideB
+	)
+'
+
 test_done
base-commit: fa7f9290efe2bd22dd736689597b474b93798e11
-- 
gitgitgadget
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help