Commit message: commit_list: fix possible buffer overflow in `commit_quick_parse`

The function `commit_quick_parse` provides a way to quickly parse
parts of a commit without storing or verifying most of its
metadata. The first thing it does is calculating the number of
parents by skipping "parent " lines until it finds the first
non-parent line. Afterwards, this parent count is passed to
`alloc_parents`, which will allocate an array to store all the
parent.

To calculate the amount of storage required for the parents
array, `alloc_parents` simply multiplicates the number of parents
with the respective elements's size. This already screams "buffer
overflow", and in fact this problem is getting worse by the
result being cast to an `uint32_t`.

In fact, triggering this is possible: git-hash-object(1) will
happily write a commit with multiple millions of parents for you.
I've stopped at 67,108,864 parents as git-hash-object(1)
unfortunately soaks up the complete object without streaming
anything to disk and thus will cause an OOM situation at a later
point. The point here is: this commit was about 4.1GB of size but
compressed down to 24MB and thus easy to distribute.

The above doesn't yet trigger the buffer overflow, thus. As the
array's elements are all pointers which are 8 bytes on 64 bit, we
need a total of 536,870,912 parents to trigger the overflow to
`0`. The effect is that we're now underallocating the array
and do an out-of-bound writes. As the buffer is kindly provided
by the adversary, this may easily result in code execution.

Extrapolating from the test file with 67m commits to the one with
536m commits results in a factor of 8. Thus the uncompressed
contents would be about 32GB in size and the compressed ones
192MB. While still easily distributable via the network, only
servers will have that amount of RAM and not cause an
out-of-memory condition previous to triggering the overflow. This
at least makes this attack not an easy vector for client-side use
of libgit2.
CVE: None
Vulnerability description: None

Project: libgit2
File: commit_list.c

static git_commit_list_node **alloc_parents(
	git_revwalk *walk, git_commit_list_node *commit, size_t n_parents)
{
	size_t bytes;

	if (n_parents <= PARENTS_PER_COMMIT)
		return (git_commit_list_node **)((char *)commit + sizeof(git_commit_list_node));

	if (git__multiply_sizet_overflow(&bytes, n_parents, sizeof(git_commit_list_node *)))
		return NULL;

	return (git_commit_list_node **)git_pool_malloc(&walk->commit_pool, bytes);
}

Project: libgit2
File: commit_list.c

git_commit_list *git_commit_list_insert(git_commit_list_node *item, git_commit_list **list_p)
{
	git_commit_list *new_list = git__malloc(sizeof(git_commit_list));
	if (new_list != NULL) {
		new_list->item = item;
		new_list->next = *list_p;
	}
	*list_p = new_list;
	return new_list;
}

Project: libgit2
File: commit_list.c

git_commit_list_node *git_commit_list_alloc_node(git_revwalk *walk)
{
	return (git_commit_list_node *)git_pool_mallocz(&walk->commit_pool, 1);
}

Project: libgit2
File: commit_list.c

void git_commit_list_free(git_commit_list **list_p)
{
	git_commit_list *list = *list_p;

	if (list == NULL)
		return;

	while (list) {
		git_commit_list *temp = list;
		list = temp->next;
		git__free(temp);
	}

	*list_p = NULL;
}

Project: libgit2
File: commit_list.c

int git_commit_list_time_cmp(const void *a, const void *b)
{
	int64_t time_a = ((git_commit_list_node *) a)->time;
	int64_t time_b = ((git_commit_list_node *) b)->time;

	if (time_a < time_b)
		return 1;
	if (time_a > time_b)
		return -1;

	return 0;
}

Project: libgit2
File: commit_list.c

git_commit_list *git_commit_list_insert_by_date(git_commit_list_node *item, git_commit_list **list_p)
{
	git_commit_list **pp = list_p;
	git_commit_list *p;

	while ((p = *pp) != NULL) {
		if (git_commit_list_time_cmp(p->item, item) > 0)
			break;

		pp = &p->next;
	}

	return git_commit_list_insert(item, pp);
}

Project: libgit2
File: commit_list.c

static int commit_error(git_commit_list_node *commit, const char *msg)
{
	char commit_oid[GIT_OID_HEXSZ + 1];
	git_oid_fmt(commit_oid, &commit->oid);
	commit_oid[GIT_OID_HEXSZ] = '\0';

	git_error_set(GIT_ERROR_ODB, "failed to parse commit %s - %s", commit_oid, msg);

	return -1;
}

Project: libgit2
File: commit_list.c

git_commit_list_node *git_commit_list_pop(git_commit_list **stack)
{
	git_commit_list *top = *stack;
	git_commit_list_node *item = top ? top->item : NULL;

	if (top) {
		*stack = top->next;
		git__free(top);
	}
	return item;
}

Project: libgit2
File: commit_list.c

int git_commit_list_parse(git_revwalk *walk, git_commit_list_node *commit)
{
	git_odb_object *obj;
	int error;

	if (commit->parsed)
		return 0;

	if ((error = git_odb_read(&obj, walk->odb, &commit->oid)) < 0)
		return error;

	if (obj->cached.type != GIT_OBJECT_COMMIT) {
		git_error_set(GIT_ERROR_INVALID, "object is no commit object");
		error = -1;
	} else
		error = commit_quick_parse(
			walk, commit,
			(const uint8_t *)git_odb_object_data(obj),
			git_odb_object_size(obj));

	git_odb_object_free(obj);
	return error;
}

Project: libgit2
File: commit_list.c

static int commit_quick_parse(
	git_revwalk *walk,
	git_commit_list_node *commit,
	const uint8_t *buffer,
	size_t buffer_len)
{
	const size_t parent_len = strlen("parent ") + GIT_OID_HEXSZ + 1;
	const uint8_t *buffer_end = buffer + buffer_len;
	const uint8_t *parents_start, *committer_start;
	int i, parents = 0;
	int64_t commit_time;

	buffer += strlen("tree ") + GIT_OID_HEXSZ + 1;

	parents_start = buffer;
	while (buffer + parent_len < buffer_end && memcmp(buffer, "parent ", strlen("parent ")) == 0) {
		parents++;
		buffer += parent_len;
	}

	commit->parents = alloc_parents(walk, commit, parents);
	GIT_ERROR_CHECK_ALLOC(commit->parents);

	buffer = parents_start;
	for (i = 0; i < parents; ++i) {
		git_oid oid;

		if (git_oid_fromstr(&oid, (const char *)buffer + strlen("parent ")) < 0)
			return -1;

		commit->parents[i] = git_revwalk__commit_lookup(walk, &oid);
		if (commit->parents[i] == NULL)
			return -1;

		buffer += parent_len;
	}

	commit->out_degree = (unsigned short)parents;

	if ((committer_start = buffer = memchr(buffer, '\n', buffer_end - buffer)) == NULL)
		return commit_error(commit, "object is corrupted");

	buffer++;

	if ((buffer = memchr(buffer, '\n', buffer_end - buffer)) == NULL)
		return commit_error(commit, "object is corrupted");

	/* Skip trailing spaces */
	while (buffer > committer_start && git__isspace(*buffer))
		buffer--;

	/* Seek for the beginning of the pack of digits */
	while (buffer > committer_start && git__isdigit(*buffer))
		buffer--;

	/* Skip potential timezone offset */
	if ((buffer > committer_start) && (*buffer == '+' || *buffer == '-')) {
		buffer--;

		while (buffer > committer_start && git__isspace(*buffer))
			buffer--;

		while (buffer > committer_start && git__isdigit(*buffer))
			buffer--;
	}

	if ((buffer == committer_start) ||
	    (git__strntol64(&commit_time, (char *)(buffer + 1),
			    buffer_end - buffer + 1, NULL, 10) < 0))
		return commit_error(commit, "cannot parse commit time");

	commit->time = commit_time;
	commit->parsed = 1;
	return 0;
}