Skip to content
This repository was archived by the owner on Nov 9, 2017. It is now read-only.

Commit d01bac0

Browse files
lanfeust69kasal
authored andcommitted
git tag --contains: avoid stack overflow
In large repos, the recursion implementation of contains(commit, commit_list) may result in a stack overflow. Replace the recursion with a loop to fix it. This problem is more apparent on Windows than on Linux, where the stack is more limited by default. See also this thread on the msysGit list: https://groups.google.com/d/topic/msysgit/FqT6boJrb2g/discussion [jes: re-written to imitate the original recursion more closely] Thomas Braun pointed out several documentation shortcomings. Tests are run only if ulimit -s is available. This means they cannot be run on Windows. Signed-off-by: Jean-Jacques Lafay <[email protected]> Signed-off-by: Johannes Schindelin <[email protected]> Tested-by: Stepan Kasal <[email protected]> Reviewed-by: Jeff King <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 9469666 commit d01bac0

File tree

2 files changed

+101
-15
lines changed

2 files changed

+101
-15
lines changed

builtin/tag.c

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -73,38 +73,98 @@ static int in_commit_list(const struct commit_list *want, struct commit *c)
7373
return 0;
7474
}
7575

76-
static int contains_recurse(struct commit *candidate,
76+
enum contains_result {
77+
CONTAINS_UNKNOWN = -1,
78+
CONTAINS_NO = 0,
79+
CONTAINS_YES = 1,
80+
};
81+
82+
/*
83+
* Test whether the candidate or one of its parents is contained in the list.
84+
* Do not recurse to find out, though, but return -1 if inconclusive.
85+
*/
86+
static enum contains_result contains_test(struct commit *candidate,
7787
const struct commit_list *want)
7888
{
79-
struct commit_list *p;
80-
8189
/* was it previously marked as containing a want commit? */
8290
if (candidate->object.flags & TMP_MARK)
8391
return 1;
8492
/* or marked as not possibly containing a want commit? */
8593
if (candidate->object.flags & UNINTERESTING)
8694
return 0;
8795
/* or are we it? */
88-
if (in_commit_list(want, candidate))
96+
if (in_commit_list(want, candidate)) {
97+
candidate->object.flags |= TMP_MARK;
8998
return 1;
99+
}
90100

91101
if (parse_commit(candidate) < 0)
92102
return 0;
93103

94-
/* Otherwise recurse and mark ourselves for future traversals. */
95-
for (p = candidate->parents; p; p = p->next) {
96-
if (contains_recurse(p->item, want)) {
97-
candidate->object.flags |= TMP_MARK;
98-
return 1;
99-
}
100-
}
101-
candidate->object.flags |= UNINTERESTING;
102-
return 0;
104+
return -1;
103105
}
104106

105-
static int contains(struct commit *candidate, const struct commit_list *want)
107+
/*
108+
* Mimicking the real stack, this stack lives on the heap, avoiding stack
109+
* overflows.
110+
*
111+
* At each recursion step, the stack items points to the commits whose
112+
* ancestors are to be inspected.
113+
*/
114+
struct stack {
115+
int nr, alloc;
116+
struct stack_entry {
117+
struct commit *commit;
118+
struct commit_list *parents;
119+
} *stack;
120+
};
121+
122+
static void push_to_stack(struct commit *candidate, struct stack *stack)
123+
{
124+
int index = stack->nr++;
125+
ALLOC_GROW(stack->stack, stack->nr, stack->alloc);
126+
stack->stack[index].commit = candidate;
127+
stack->stack[index].parents = candidate->parents;
128+
}
129+
130+
static enum contains_result contains(struct commit *candidate,
131+
const struct commit_list *want)
106132
{
107-
return contains_recurse(candidate, want);
133+
struct stack stack = { 0, 0, NULL };
134+
int result = contains_test(candidate, want);
135+
136+
if (result != CONTAINS_UNKNOWN)
137+
return result;
138+
139+
push_to_stack(candidate, &stack);
140+
while (stack.nr) {
141+
struct stack_entry *entry = &stack.stack[stack.nr - 1];
142+
struct commit *commit = entry->commit;
143+
struct commit_list *parents = entry->parents;
144+
145+
if (!parents) {
146+
commit->object.flags |= UNINTERESTING;
147+
stack.nr--;
148+
}
149+
/*
150+
* If we just popped the stack, parents->item has been marked,
151+
* therefore contains_test will return a meaningful 0 or 1.
152+
*/
153+
else switch (contains_test(parents->item, want)) {
154+
case CONTAINS_YES:
155+
commit->object.flags |= TMP_MARK;
156+
stack.nr--;
157+
break;
158+
case CONTAINS_NO:
159+
entry->parents = parents->next;
160+
break;
161+
case CONTAINS_UNKNOWN:
162+
push_to_stack(parents->item, &stack);
163+
break;
164+
}
165+
}
166+
free(stack.stack);
167+
return contains_test(candidate, want);
108168
}
109169

110170
static void show_tag_lines(const unsigned char *sha1, int lines)

t/t7004-tag.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1380,4 +1380,30 @@ test_expect_success 'multiple --points-at are OR-ed together' '
13801380
test_cmp expect actual
13811381
'
13821382
1383+
run_with_limited_stack () {
1384+
(ulimit -s 64 && "$@")
1385+
}
1386+
1387+
test_lazy_prereq ULIMIT 'run_with_limited_stack true'
1388+
1389+
# we require ulimit, this excludes Windows
1390+
test_expect_success ULIMIT '--contains works in a deep repo' '
1391+
>expect &&
1392+
i=1 &&
1393+
while test $i -lt 4000
1394+
do
1395+
echo "commit refs/heads/master
1396+
committer A U Thor <[email protected]> $((1000000000 + $i * 100)) +0200
1397+
data <<EOF
1398+
commit #$i
1399+
EOF"
1400+
test $i = 1 && echo "from refs/heads/master^0"
1401+
i=$(($i + 1))
1402+
done | git fast-import &&
1403+
git checkout master &&
1404+
git tag far-far-away HEAD^ &&
1405+
run_with_limited_stack git tag --contains HEAD >actual &&
1406+
test_cmp expect actual
1407+
'
1408+
13831409
test_done

0 commit comments

Comments
 (0)