#7313·libgit2

blame: segfault in git_blame_free() when a commit in the blame graph has an empty author email (NULL hunk inserted by blame_internal) — 1.9 regression

Author: damienmeurCreated Jul 14, 2026Updated Jul 14, 2026

Summary

git_blame_file() + git_blame_free() segfaults when any commit in the blame graph has an empty author email (author name <> …). Such commits are accepted by git itself (plumbing writes them, git blame handles them fine) and exist in real-world histories — e.g. rubocop/rubocop has several (3bff9d37116fbe0981fa3f97c1f623397847db45 is authored 5hun-s <>).

This is a 1.9.x regression: 1.8.1 blames the same history cleanly. It reproduces on every 1.9.x we tried, including v1.9.4.

Root cause

Since 1.9, blame hunks carry duplicated commit metadata. In hunk_from_entry() (src/libgit2/blame.c, ~382–409 at v1.9.4), the chain

c
if (git_commit_author_with_mailmap(&h->final_signature, e->suspect->commit, blame->mailmap) < 0 ||
    git_commit_committer_with_mailmap(&h->final_committer, ...) < 0 ||
    ...
    (h->summary = git__strdup(summary)) == NULL) {
	free_hunk(h);
	return NULL;
}

fails for a commit whose author signature can't be parsed (empty email), so hunk_from_entry returns NULL. The caller in blame_internal() (~461–464) inserts it unchecked:

c
for (ent = blame->ent; ent; ent = ent->next) {
	git_blame_hunk *h = hunk_from_entry(ent, blame);
	git_vector_insert(&blame->hunks, h);   /* h may be NULL */
}

git_blame_free() then walks the hunks vector and calls free_hunk(NULL), dereferencing the NULL hunk (EXC_BAD_ACCESS, fault address 0x50 on arm64 — a field offset off the NULL pointer).

Note this makes the whole blame unusable, not just the metadata: 1.8.x attributed these commits fine (the hunk still carried final_commit_id), so this is also a functional regression for histories that contain such commits, independent of the crash.

Likely the same family as #7311 (git_blame_buffer() segfault via dup_hunk() on a NULL summary): hunk metadata that can legitimately be absent is assumed present in the dup/free paths.

Reproduction

Pure-shell repo construction (the empty-email commit is forged through plumbing, exactly how they appear in the wild):

bash
git init repro && cd repro
printf 'x = 1\n' > a.py && git add a.py && git commit -m base
printf 'x = 1\ny = 2\n' > a.py && git add a.py
TREE=$(git write-tree)
C1=$(printf 'tree %s\nparent %s\nauthor no-email <> 1700000000 +0000\ncommitter no-email <> 1700000000 +0000\n\nempty-email edit\n' \
      "$TREE" "$(git rev-parse HEAD)" | git hash-object -t commit -w --stdin --literally)
git update-ref HEAD "$C1"
printf 'x = 1\ny = 2\nz = 3\n' > a.py && git add a.py && git commit -m head
git blame a.py    # git itself: fine

Then any libgit2 blame of a.py at HEAD crashes on free:

c
git_repository *repo; git_blame *blame;
git_libgit2_init();
git_repository_open(&repo, "repro");
git_blame_file(&blame, repo, "a.py", NULL);   /* succeeds */
git_blame_free(blame);                        /* SIGSEGV in free_hunk(NULL) */

Verified through the thin git2-rs binding against vendored v1.9.4 (crashes, with or without newest_commit/oldest_commit bounds set) and against v1.8.1 (passes, correct attribution). Originally hit in production on rubocop/rubocop's real history.

Suggested fix

Two independent hardening points:

  1. blame_internal(): check hunk_from_entry()'s return instead of inserting NULL (either propagate an error, or better — degrade: keep the hunk with final_commit_id set and NULL signature/summary fields, which restores 1.8.x's attribution behaviour for these commits).
  2. free_hunk() / dup_hunk(): tolerate NULL hunks and NULL metadata fields, which also covers #7311.

Happy to provide more detail; the reproduction above is self-contained.