]> git.ipfire.org Git - thirdparty/git.git/commit
diff: fix out-of-bounds reads and NULL deref in diffstat UTF-8 truncation
authorElijah Newren <newren@gmail.com>
Mon, 20 Apr 2026 15:42:58 +0000 (15:42 +0000)
committerJunio C Hamano <gitster@pobox.com>
Mon, 20 Apr 2026 16:30:55 +0000 (09:30 -0700)
commit09d86a3b98573cba6d7c813ec97eadb69e3e2dfd
treeeabb7427da9dbb6ba1d3a6b11be5d0738fd9d176
parente8955061076952cc5eab0300424fc48b601fe12d
diff: fix out-of-bounds reads and NULL deref in diffstat UTF-8 truncation

f85b49f3d4a (diff: improve scaling of filenames in diffstat to handle
UTF-8 chars, 2026-01-16) introduced a loop in show_stats() that calls
utf8_width() repeatedly to skip leading characters until the displayed
width fits.  However, utf8_width() can return problematic values:

  - For invalid UTF-8 sequences, pick_one_utf8_char() sets the name
    pointer to NULL and utf8_width() returns 0.  Since name_len does
    not change, the loop iterates once more and pick_one_utf8_char()
    dereferences the NULL pointer, crashing.

  - For control characters, utf8_width() returns -1, so name_len
    grows when it is expected to shrink.  This can cause the loop to
    consume more characters than the string contains, reading past
    the trailing NUL.

By default, fill_print_name() will C-quote filenames which escapes
control characters and invalid bytes to printable text.  That avoids
this bug from being triggered; however, with core.quotePath=false,
most characters are no longer escaped (though some control characters
still are) and raw bytes can reach this code.

Add tests exercising both failure modes with core.quotePath=false and
a narrow --stat-name-width to force truncation: one with a bare 0xC0
byte (invalid UTF-8 lead byte, triggers NULL deref) and one with
several C1 control characters (repeats of 0xC2 0x9F, causing
the loop to read past the end of the string).  The second test
reliably catches the out-of-bounds read when run under ASan, though
it may pass silently without sanitizers.

Fix both issues by introducing utf8_ish_width(), a thin wrapper
around utf8_width() that guarantees the pointer always advances and
the returned width is never negative:

  - On invalid UTF-8 it restores the pointer, advances by one byte,
    and returns width 1 (matching the strlen()-based fallback used
    by utf8_strwidth()).
  - On a control character it returns 0 (matching utf8_strnwidth()
    which skips them).

Also add a "&& *name" guard to the while-loop condition so it
terminates at end-of-string even when utf8_strwidth()'s strlen()
fallback causes name_len to exceed the sum of per-character widths.

Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
diff.c
t/t4052-stat-output.sh