Skip to content

Commit b7e4cbb

Browse files
committed
Merge branch 'en/diffstat-utf8-truncation-fix' into seen
The computation to shorten the filenames shown in diffstat measured width of individual UTF-8 characters to add up, but forgot to take into account error cases (e.g., an invalid UTF-8 sequence, or a control character). * en/diffstat-utf8-truncation-fix: diff: fix out-of-bounds reads and NULL deref in diffstat UTF-8 truncation
2 parents 8dfa4d6 + 41f1ee0 commit b7e4cbb

2 files changed

Lines changed: 49 additions & 2 deletions

File tree

diff.c

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2927,6 +2927,28 @@ void print_stat_summary(FILE *fp, int files,
29272927
print_stat_summary_inserts_deletes(&o, files, insertions, deletions);
29282928
}
29292929

2930+
/*
2931+
* Like utf8_width(), but guaranteed safe for use in loops that subtract
2932+
* per-character widths:
2933+
*
2934+
* - utf8_width() sets *start to NULL on invalid UTF-8 and returns 0;
2935+
* we restore the pointer and advance by one byte, returning width 1
2936+
* (matching the strlen()-based fallback in utf8_strwidth()).
2937+
*
2938+
* - utf8_width() returns -1 for control characters; we return 0
2939+
* (matching utf8_strnwidth() which skips them).
2940+
*/
2941+
static int utf8_ish_width(const char **start)
2942+
{
2943+
const char *old = *start;
2944+
int w = utf8_width(start, NULL);
2945+
if (!*start) {
2946+
*start = old + 1;
2947+
return 1;
2948+
}
2949+
return (w < 0) ? 0 : w;
2950+
}
2951+
29302952
static void show_stats(struct diffstat_t *data, struct diff_options *options)
29312953
{
29322954
int i, len, add, del, adds = 0, dels = 0;
@@ -3093,8 +3115,8 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
30933115
if (len < 0)
30943116
len = 0;
30953117

3096-
while (name_len > len)
3097-
name_len -= utf8_width((const char**)&name, NULL);
3118+
while (name_len > len && *name)
3119+
name_len -= utf8_ish_width((const char**)&name);
30983120

30993121
slash = strchr(name, '/');
31003122
if (slash)

t/t4052-stat-output.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,4 +445,29 @@ test_expect_success 'diffstat where line_prefix contains ANSI escape codes is co
445445
test_grep "<RED>|<RESET> ${FILENAME_TRIMMED} | 0" out
446446
'
447447

448+
test_expect_success 'diffstat truncation with invalid UTF-8 does not crash' '
449+
empty_blob=$(git hash-object -w --stdin </dev/null) &&
450+
printf "100644 blob $empty_blob\taaa-\300-aaa\n" |
451+
git mktree >tree_file &&
452+
tree=$(cat tree_file) &&
453+
empty_tree=$(git mktree </dev/null) &&
454+
c1=$(git commit-tree -m before $empty_tree) &&
455+
c2=$(git commit-tree -m after -p $c1 $tree) &&
456+
git -c core.quotepath=false diff --stat --stat-name-width=5 $c1..$c2 >output &&
457+
test_grep "| 0" output
458+
'
459+
460+
test_expect_success FUNNYNAMES 'diffstat truncation with control chars does not crash' '
461+
FNAME=$(printf "aaa-\x01-aaa") &&
462+
git commit --allow-empty -m setup &&
463+
>$FNAME &&
464+
git add -- $FNAME &&
465+
git commit -m "add file with control char name" &&
466+
git -c core.quotepath=false diff --stat --stat-name-width=5 HEAD~1..HEAD >output &&
467+
test_grep "| 0" output &&
468+
rm -- $FNAME &&
469+
git rm -- $FNAME &&
470+
git commit -m "remove test file"
471+
'
472+
448473
test_done

0 commit comments

Comments
 (0)