Skip to content

Commit 42ed046

Browse files
peffgitster
authored andcommitted
attr: avoid recursion when expanding attribute macros
Given a set of attribute macros like: [attr]a1 a2 [attr]a2 a3 ... [attr]a300000 -text file a1 expanding the attributes for "file" requires expanding "a1" to "a2", "a2" to "a3", and so on until hitting a non-macro expansion ("-text", in this case). We implement this via recursion: fill_one() calls macroexpand_one(), which then recurses back to fill_one(). As a result, very deep macro chains like the one above can run out of stack space and cause us to segfault. The required stack space is fairly small; I needed on the order of 200,000 entries to get a segfault on Linux. So it's unlikely anybody would hit this accidentally, leaving only malicious inputs. There you can easily construct a repo which will segfault on clone (we look at attributes during the checkout step, but you'd see the same trying to do other operations, like diff in a bare repo). It's mostly harmless, since anybody constructing such a repo is only preventing victims from cloning their evil garbage, but it could be a nuisance for hosting sites. One option to prevent this is to limit the depth of recursion we'll allow. This is conceptually easy to implement, but it raises other questions: what should the limit be, and do we need a configuration knob for it? The recursion here is simple enough that we can avoid those questions by just converting it to iteration instead. Rather than iterate over the states of a match_attr in fill_one(), we'll put them all in a queue, and the expansion of each can add to the queue rather than recursing. Note that this is a LIFO queue in order to keep the same depth-first order we did with the recursive implementation. I've avoided using the word "stack" in the code because the term is already heavily used to refer to the stack of .gitattribute files that matches the tree structure of the repository. The test uses a limited stack size so we can trigger the problem with a much smaller input than the one shown above. The value here (3000) is enough to trigger the issue on my x86_64 Linux machine. Reported-by: Ben Stav <[email protected]> Signed-off-by: Jeff King <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent c44beea commit 42ed046

File tree

2 files changed

+54
-16
lines changed

2 files changed

+54
-16
lines changed

attr.c

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,24 +1064,52 @@ static int path_matches(const char *pathname, int pathlen,
10641064
pattern, prefix, pat->patternlen);
10651065
}
10661066

1067-
static int macroexpand_one(struct all_attrs_item *all_attrs, int nr, int rem);
1067+
struct attr_state_queue {
1068+
const struct attr_state **items;
1069+
size_t alloc, nr;
1070+
};
1071+
1072+
static void attr_state_queue_push(struct attr_state_queue *t,
1073+
const struct match_attr *a)
1074+
{
1075+
for (size_t i = 0; i < a->num_attr; i++) {
1076+
ALLOC_GROW(t->items, t->nr + 1, t->alloc);
1077+
t->items[t->nr++] = &a->state[i];
1078+
}
1079+
}
1080+
1081+
static const struct attr_state *attr_state_queue_pop(struct attr_state_queue *t)
1082+
{
1083+
return t->nr ? t->items[--t->nr] : NULL;
1084+
}
1085+
1086+
static void attr_state_queue_release(struct attr_state_queue *t)
1087+
{
1088+
free(t->items);
1089+
}
10681090

10691091
static int fill_one(struct all_attrs_item *all_attrs,
10701092
const struct match_attr *a, int rem)
10711093
{
1072-
size_t i;
1094+
struct attr_state_queue todo = { 0 };
1095+
const struct attr_state *state;
10731096

1074-
for (i = a->num_attr; rem > 0 && i > 0; i--) {
1075-
const struct git_attr *attr = a->state[i - 1].attr;
1097+
attr_state_queue_push(&todo, a);
1098+
while (rem > 0 && (state = attr_state_queue_pop(&todo))) {
1099+
const struct git_attr *attr = state->attr;
10761100
const char **n = &(all_attrs[attr->attr_nr].value);
1077-
const char *v = a->state[i - 1].setto;
1101+
const char *v = state->setto;
10781102

10791103
if (*n == ATTR__UNKNOWN) {
1104+
const struct all_attrs_item *item =
1105+
&all_attrs[attr->attr_nr];
10801106
*n = v;
10811107
rem--;
1082-
rem = macroexpand_one(all_attrs, attr->attr_nr, rem);
1108+
if (item->macro && item->value == ATTR__TRUE)
1109+
attr_state_queue_push(&todo, item->macro);
10831110
}
10841111
}
1112+
attr_state_queue_release(&todo);
10851113
return rem;
10861114
}
10871115

@@ -1106,16 +1134,6 @@ static int fill(const char *path, int pathlen, int basename_offset,
11061134
return rem;
11071135
}
11081136

1109-
static int macroexpand_one(struct all_attrs_item *all_attrs, int nr, int rem)
1110-
{
1111-
const struct all_attrs_item *item = &all_attrs[nr];
1112-
1113-
if (item->macro && item->value == ATTR__TRUE)
1114-
return fill_one(all_attrs, item->macro, rem);
1115-
else
1116-
return rem;
1117-
}
1118-
11191137
/*
11201138
* Marks the attributes which are macros based on the attribute stack.
11211139
* This prevents having to search through the attribute stack each time

t/t0003-attributes.sh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,4 +664,24 @@ test_expect_success 'user defined builtin_objectmode values are ignored' '
664664
test_cmp expect err
665665
'
666666

667+
test_expect_success ULIMIT_STACK_SIZE 'deep macro recursion' '
668+
n=3000 &&
669+
{
670+
i=0 &&
671+
while test $i -lt $n; do
672+
echo "[attr]a$i a$((i+1))" &&
673+
i=$((i+1)) ||
674+
return 1
675+
done &&
676+
echo "[attr]a$n -text" &&
677+
echo "file a0"
678+
} >.gitattributes &&
679+
{
680+
echo "file: text: unset" &&
681+
test_seq -f "file: a%d: set" 0 $n
682+
} >expect &&
683+
run_with_limited_stack git check-attr -a file >actual &&
684+
test_cmp expect actual
685+
'
686+
667687
test_done

0 commit comments

Comments
 (0)