[BUG] `index_blob_lines` error used as a line count · Issue #7419 · libgit2/libgit2 · GitHub
Skip to content

[BUG] index_blob_lines error used as a line count #7419

Open

Description

@bunch-suffice-ivy

libgit2 commit 07b89d1 src/libgit2/blame.c:454.

Bug

blame_internal stores the return value of index_blob_lines as the line count without checking it:

ent->num_lines = index_blob_lines(blame);                         /* 454: may be -1 */
ent->lno = blame->options.min_line - 1;
ent->num_lines = ent->num_lines - blame->options.min_line + 1;

index_blob_lines returns -1 when a git_array_alloc fails (GIT_ERROR_CHECK_ALLOC).
The -1 becomes the entry's line count and git_blame_file returns 0,
so the caller gets a successful blame whose hunks add up to (size_t)-1 lines instead of an error.
There is no crash.

Reproduce

04.c installs a counting allocator through GIT_OPT_SET_ALLOCATOR that returns NULL for the Nth allocation, which looks exactly like a real OOM to libgit2. It creates a one-commit repo with a 400-line file.txt, then runs git_blame_file with the Nth allocation failing, for N = 1..1000, and prints every run that returns 0 with a total line count other than 400. The N values depend on the build, which is why the program scans instead of hard-coding them.

gcc -g -O0 04.c -I"$LIBGIT2_INCLUDE" "$LIBGIT2_LIBDIR/libgit2.a" -lz -lpthread -ldl -o 04_c
./04_c
reference: git_blame_file -> 0, lines = 400
alloc #30: git_blame_file -> 0, lines = 18446744073709551615 (expected 400)
alloc #31: git_blame_file -> 0, lines = 18446744073709551615 (expected 400)
...
alloc #51: git_blame_file -> 0, lines = 18446744073709551615 (expected 400)
42 clean errors, 22 wrong results reported as success

All 22 wrong results fail inside index_blob_lines. The 42 clean errors come from checked allocations elsewhere in git_blame_file.

04.c
#include <git2.h>
#include <git2/sys/alloc.h>
#include <stdio.h>
#include <stdlib.h>

#define NLINES 400

static long fail_at, count; /* fail the fail_at-th allocation; 0 = never */

static int should_fail(void) { return fail_at && ++count == fail_at; }

static void *x_malloc(size_t n, const char *file, int line)
{
	(void)file; (void)line;
	return should_fail() ? NULL : malloc(n ? n : 1);
}

static void *x_realloc(void *p, size_t n, const char *file, int line)
{
	(void)file; (void)line;
	return should_fail() ? NULL : realloc(p, n ? n : 1);
}

/* A repo with one commit of an NLINES-line file.txt. */
static int make_repo(git_repository **repo, const char *dir)
{
	char path[4096];
	git_signature *sig = NULL;
	git_index *idx = NULL;
	git_tree *tree = NULL;
	git_oid id;
	FILE *f;
	int i, rc;

	snprintf(path, sizeof(path), "%s/file.txt", dir);
	if ((rc = git_repository_init(repo, dir, 0)) < 0 || !(f = fopen(path, "w")))
		return -1;
	for (i = 0; i < NLINES; i++)
		fprintf(f, "line %d\n", i);
	fclose(f);

	if ((rc = git_signature_new(&sig, "r", "r@example.com", 1234567890, 0)) == 0 &&
	    (rc = git_repository_index(&idx, *repo)) == 0 &&
	    (rc = git_index_add_bypath(idx, "file.txt")) == 0 &&
	    (rc = git_index_write_tree(&id, idx)) == 0 &&
	    (rc = git_tree_lookup(&tree, *repo, &id)) == 0)
		rc = git_commit_create_v(&id, *repo, "HEAD", sig, sig, NULL, "init", tree, 0);

	git_tree_free(tree);
	git_index_free(idx);
	git_signature_free(sig);
	return rc;
}

/* Blame file.txt with the nth allocation failing; returns rc, sets *lines. */
static int blame_file(git_repository *repo, long n, size_t *lines)
{
	git_blame *blame;
	size_t i;
	int rc;

	fail_at = n, count = 0;
	rc = git_blame_file(&blame, repo, "file.txt", NULL);
	fail_at = 0;
	if (rc == 0) {
		for (*lines = i = 0; i < git_blame_hunkcount(blame); i++)
			*lines += git_blame_hunk_byindex(blame, i)->lines_in_hunk;
		git_blame_free(blame);
	}
	return rc;
}

int main(void)
{
	git_allocator alloc = { x_malloc, x_realloc, free };
	char dir[] = "/tmp/04_c_XXXXXX";
	git_repository *repo;
	size_t lines;
	int wrong = 0, clean = 0;
	long n;

	setvbuf(stdout, NULL, _IONBF, 0);
	if (!mkdtemp(dir) || git_libgit2_init() < 0 ||
	    git_libgit2_opts(GIT_OPT_SET_ALLOCATOR, &alloc) ||
	    make_repo(&repo, dir) || blame_file(repo, 0, &lines) || lines != NLINES)
		return fprintf(stderr, "04_c: setup failed\n"), 1;
	printf("reference: git_blame_file -> 0, lines = %zu\n", lines);

	for (n = 1; n <= 1000; n++) {
		if (blame_file(repo, n, &lines))
			clean++;
		else if (lines != NLINES)
			wrong++, printf("alloc #%ld: git_blame_file -> 0, lines = %zu (expected %d)\n",
			                n, lines, NLINES);
	}
	printf("%d clean errors, %d wrong results reported as success\n", clean, wrong);
	return wrong ? 0 : 1;
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions