[BUG] `git_blame_buffer` ignores diff errors and returns wrong hunks · Issue #7418 · libgit2/libgit2 · GitHub
Skip to content

[BUG] git_blame_buffer ignores diff errors and returns wrong hunks #7418

Open

Description

@bunch-suffice-ivy

libgit2 commit 07b89d1 src/libgit2/blame.c:643-648.

Bug

git_blame_buffer discards the return value of git_diff_blob_to_buffer and reports success unconditionally:

/* Diff to the reference blob */
git_diff_blob_to_buffer(reference->final_blob, blame->path,       /* 643: result ignored */
    buffer, buffer_len, blame->path, &diffopts,
    NULL, NULL, buffer_hunk_cb, buffer_line_cb, blame);

*out = blame;
return 0;                                                          /* 648 */

If the diff fails for any reason (for example out of memory during attribute lookup, patch generation or xdiff), the caller gets 0 and a blame whose hunks do not reflect the buffer, with no way to detect it. There is no crash. The fix is to return the error from git_diff_blob_to_buffer and free the partial blame.

Reproduce

03.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, runs git_blame_file, and records the hunk count of git_blame_buffer on a modified buffer. It then repeats git_blame_buffer with the Nth allocation failing, for N = 1..800, and prints every run that returns 0 with a different hunk count.

gcc -g -O0 03.c -I"$LIBGIT2_INCLUDE" "$LIBGIT2_LIBDIR/libgit2.a" -lz -lpthread -ldl -o 03_c
./03_c
reference: git_blame_buffer -> 0, hunks = 3
alloc #21: git_blame_buffer -> 0, hunks = 1 (expected 3)
alloc #22: git_blame_buffer -> 0, hunks = 1 (expected 3)
...
alloc #85: git_blame_buffer -> 0, hunks = 2 (expected 3)
18 clean errors, 63 wrong results reported as success

The 18 clean errors come from allocations git_blame_buffer does check (git_blame__alloc, dup_hunk). All 63 wrong results come from allocations inside git_diff_blob_to_buffer.

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

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 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 rc;

	snprintf(path, sizeof(path), "%s/file.txt", dir);
	if ((rc = git_repository_init(repo, dir, 0)) < 0 || !(f = fopen(path, "w")))
		return -1;
	fputs("line one\nline two\nline three\n", f);
	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 BUF against ref with the nth allocation failing; returns rc, sets *hunks. */
static int blame_buffer(git_blame *ref, long n, size_t *hunks)
{
	static const char BUF[] = "line one\nline two CHANGED\nline three\n";
	git_blame *blame;
	int rc;

	fail_at = n, count = 0;
	rc = git_blame_buffer(&blame, ref, BUF, strlen(BUF));
	fail_at = 0;
	if (rc == 0) {
		*hunks = git_blame_hunkcount(blame);
		git_blame_free(blame);
	}
	return rc;
}

int main(void)
{
	git_allocator alloc = { x_malloc, x_realloc, free };
	char dir[] = "/tmp/03_c_XXXXXX";
	git_repository *repo;
	git_blame *ref;
	size_t good, hunks;
	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) || git_blame_file(&ref, repo, "file.txt", NULL) ||
	    blame_buffer(ref, 0, &good))
		return fprintf(stderr, "03_c: setup failed\n"), 1;
	printf("reference: git_blame_buffer -> 0, hunks = %zu\n", good);

	for (n = 1; n <= 800; n++) {
		if (blame_buffer(ref, n, &hunks))
			clean++;
		else if (hunks != good)
			wrong++, printf("alloc #%ld: git_blame_buffer -> 0, hunks = %zu (expected %zu)\n",
			                n, hunks, good);
	}
	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