Skip to content

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 #7313

Description

@damienmeur

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

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:

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):

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:

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 git_blame_buffer() segfault with null summary #7311.

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

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