Skip to content

Commit 5bd3978

Browse files
tamirdgitster
authored andcommitted
commit-reach: reject cycles in contains walk
The memoized contains traversal used by git tag assumes that commit ancestry is acyclic. Replacement refs can violate that assumption, causing it to keep pushing an already active commit until memory is exhausted. Mark commits while they are active and die if the traversal encounters an active commit. Other failures in this walk already die through parse_commit_or_die(); using a second reachability walk would only add a separate policy for malformed history. Suggested-by: Kristofer Karlsson <krka@spotify.com> Signed-off-by: Tamir Duberstein <tamird@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 600fe74 commit 5bd3978

3 files changed

Lines changed: 29 additions & 4 deletions

File tree

commit-reach.c

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,8 @@ static int in_commit_list(const struct commit_list *want, struct commit *c)
757757

758758
/*
759759
* Test whether the candidate is contained in the list.
760-
* Do not recurse to find out, though, but return -1 if inconclusive.
760+
* Do not recurse to find out, though, but return CONTAINS_UNKNOWN if
761+
* inconclusive.
761762
*/
762763
static enum contains_result contains_test(struct commit *candidate,
763764
const struct commit_list *want,
@@ -814,6 +815,7 @@ static enum contains_result contains_tag_algo(struct commit *candidate,
814815
if (result != CONTAINS_UNKNOWN)
815816
return result;
816817

818+
*contains_cache_at(cache, candidate) = CONTAINS_IN_PROGRESS;
817819
push_to_contains_stack(candidate, &contains_stack);
818820
while (contains_stack.nr) {
819821
struct contains_stack_entry *entry = &contains_stack.contains_stack[contains_stack.nr - 1];
@@ -825,8 +827,8 @@ static enum contains_result contains_tag_algo(struct commit *candidate,
825827
contains_stack.nr--;
826828
}
827829
/*
828-
* If we just popped the stack, parents->item has been marked,
829-
* therefore contains_test will return a meaningful yes/no.
830+
* A parent may have just been popped and marked, or may still
831+
* be active when replacement refs create a cycle.
830832
*/
831833
else switch (contains_test(parents->item, want, cache, cutoff)) {
832834
case CONTAINS_YES:
@@ -836,7 +838,11 @@ static enum contains_result contains_tag_algo(struct commit *candidate,
836838
case CONTAINS_NO:
837839
entry->parents = parents->next;
838840
break;
841+
case CONTAINS_IN_PROGRESS:
842+
die(_("commit ancestry contains a cycle"));
839843
case CONTAINS_UNKNOWN:
844+
*contains_cache_at(cache, parents->item) =
845+
CONTAINS_IN_PROGRESS;
840846
push_to_contains_stack(parents->item, &contains_stack);
841847
break;
842848
}

commit-reach.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ int ref_newer(const struct object_id *new_oid, const struct object_id *old_oid);
7373
enum contains_result {
7474
CONTAINS_UNKNOWN = 0,
7575
CONTAINS_NO,
76-
CONTAINS_YES
76+
CONTAINS_YES,
77+
CONTAINS_IN_PROGRESS
7778
};
7879

7980
define_commit_slab(contains_cache, enum contains_result);

t/t7004-tag.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,6 +1611,24 @@ test_expect_success 'checking that first commit is in all tags (hash)' '
16111611
test_cmp expected actual
16121612
'
16131613

1614+
test_expect_success 'tag --contains rejects cyclic replacement histories' '
1615+
first=$(git rev-parse HEAD~2) &&
1616+
second=$(git rev-parse HEAD~) &&
1617+
third=$(git rev-parse HEAD) &&
1618+
test_when_finished "
1619+
git replace -d $first &&
1620+
git replace -d $third &&
1621+
git tag -d cycle-a cycle-b
1622+
" &&
1623+
git tag cycle-a "$first" &&
1624+
git tag cycle-b "$third" &&
1625+
git replace --graft "$first" "$third" "$second" &&
1626+
git replace --graft "$third" "$first" &&
1627+
test_must_fail git tag --contains="$second" --list "cycle-*" \
1628+
>/dev/null 2>err &&
1629+
test_grep "fatal: commit ancestry contains a cycle" err
1630+
'
1631+
16141632
# other ways of specifying the commit
16151633
test_expect_success 'checking that first commit is in all tags (tag)' '
16161634
cat >expected <<-\EOF &&

0 commit comments

Comments
 (0)