git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH] diff: fix out-of-bounds reads and NULL deref in diffstat UTF-8 truncation

From
Elijah Newren via GitGitGadget <gitgitgadget@gmail.com>
Date
Apr 17, 2026, 16:26 UTC
Message-ID
<pull.2093.git.1776443163041.gitgitgadget@gmail.com>
From: Elijah Newren <newren@gmail.com>

f85b49f3d4a (diff: improve scaling of filenames in diffstat to handle UTF-8 chars, 2024-10-27) 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-quotes filenames which escapes control characters and invalid bytes to printable text. That avoids this bug from being triggered; however, with core.quotePath=false, 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 a 0x01 byte (control character, causes the loop to read past the end of the string).

Fix the bug by:
  - Adding a *name check to terminate the loop at end-of-string
  - Detecting the NULL pointer from invalid UTF-8 and falling back to
    showing the full untruncated name
  - Breaking on negative width (control characters)
Signed-off-by: Elijah Newren <newren@gmail.com>
---
    diff: fix out-of-bounds reads and NULL deref in diffstat UTF-8
    truncation
    
    Maintainer note: This is a new bug from the v2.54 cycle
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2093%2Fnewren%2Ffix%2Fdiffstat-utf8-loop-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2093/newren/fix/diffstat-utf8-loop-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2093
 diff.c                 | 13 +++++++++++--
 t/t4052-stat-output.sh | 25 +++++++++++++++++++++++++
 2 files changed, 36 insertions(+), 2 deletions(-)
diff --git a/diff.c b/diff.c
index 397e38b41c..7b27241733 100644
--- a/diff.c
+++ b/diff.c
@@ -3093,8 +3093,17 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
 			if (len < 0)
 				len = 0;
 
-			while (name_len > len)
-				name_len -= utf8_width((const char**)&name, NULL);
+			while (name_len > len && *name) {
+				int w = utf8_width((const char **)&name, NULL);
+				if (!name) { /* Invalid UTF-8 */
+					name = file->print_name;
+					name_len = utf8_strwidth(name);
+					break;
+				}
+				if (w < 0)  /* control character */
+					break;
+				name_len -= w;
+			}
 
 			slash = strchr(name, '/');
 			if (slash)
diff --git a/t/t4052-stat-output.sh b/t/t4052-stat-output.sh
index 7c749062e2..84c53c1a51 100755
--- a/t/t4052-stat-output.sh
+++ b/t/t4052-stat-output.sh
@@ -445,4 +445,29 @@ test_expect_success 'diffstat where line_prefix contains ANSI escape codes is co
 	test_grep "<RED>|<RESET>  ${FILENAME_TRIMMED} | 0" out
 '
 
+test_expect_success 'diffstat truncation with invalid UTF-8 does not crash' '
+	empty_blob=$(git hash-object -w --stdin </dev/null) &&
+	printf "100644 blob $empty_blob\taaa-\300-aaa\n" |
+	git mktree >tree_file &&
+	tree=$(cat tree_file) &&
+	empty_tree=$(git mktree </dev/null) &&
+	c1=$(git commit-tree -m before $empty_tree) &&
+	c2=$(git commit-tree -m after -p $c1 $tree) &&
+	git -c core.quotepath=false diff --stat --stat-name-width=5 $c1..$c2 >output &&
+	test_grep "| 0" output
+'
+
+test_expect_success FUNNYNAMES 'diffstat truncation with control chars does not crash' '
+	FNAME=$(printf "aaa-\x01-aaa") &&
+	git commit --allow-empty -m setup &&
+	>$FNAME &&
+	git add -- $FNAME &&
+	git commit -m "add file with control char name" &&
+	git -c core.quotepath=false diff --stat --stat-name-width=5 HEAD~1..HEAD >output &&
+	test_grep "| 0" output &&
+	rm -- $FNAME &&
+	git rm -- $FNAME &&
+	git commit -m "remove test file"
+'
+
 test_done

base-commit: 9f223ef1c026d91c7ac68cc0211bde255dda6199
-- 
gitgitgadget
Next: Junio C Hamano
Message 1 of 9 in “diff: fix out-of-bounds reads and NULL deref in diffstat UTF-8 truncation”
  1. diff: fix out-of-bounds reads and NULL deref in diffstat UTF-8 truncationElijah Newren via GitGitGadget, Apr 17, 2026
  2. Junio C HamanoApr 17, 2026
  3. Elijah NewrenApr 17, 2026
  4. Junio C HamanoApr 17, 2026
  5. diff: fix out-of-bounds reads and NULL deref in diffstat UTF-8 truncationElijah Newren via GitGitGadget, Apr 17, 2026
  6. Lorenzo PegorariApr 19, 2026
  7. Elijah NewrenApr 20, 2026
  8. diff: fix out-of-bounds reads and NULL deref in diffstat UTF-8 truncationElijah Newren via GitGitGadget, Apr 20, 2026
  9. Junio C HamanoApr 20, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.