diff: fix out-of-bounds reads and NULL deref in diffstat UTF-8 truncation#2093
Open
newren wants to merge 1 commit intogitgitgadget:masterfrom
Open
diff: fix out-of-bounds reads and NULL deref in diffstat UTF-8 truncation#2093newren wants to merge 1 commit intogitgitgadget:masterfrom
newren wants to merge 1 commit intogitgitgadget:masterfrom
Conversation
c0e31d0 to
fcd44d6
Compare
Author
|
/submit |
|
Submitted as pull.2093.git.1776443163041.gitgitgadget@gmail.com To fetch this version into To fetch this version to local tag |
|
Junio C Hamano wrote on the Git mailing list (how to reply to this email): "Elijah Newren via GitGitGadget" <gitgitgadget@gmail.com> writes:
> 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.
A tangent, but I get a datestamp for the same f85b49f3 (diff:
improve scaling of filenames in diffstat to handle UTF-8 chars,
2026-01-16) that is different from what you showed above. Did you
find a bug in "git show -s --pretty=reference"?
> 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;
> + }
IOW, we punt on "scaling" and instead use the full string? I was
wondering if we can punt on only this segment by replacing this
segment with just "..." and resync at the next slash.
> + if (w < 0) /* control character */
> + break;
When we have a control characer, we instead chomp immediately before
that byte, which sounds good. But then wouldn't the loop that found
an Invalid UTF-8 sequence in the middle of a name want to do the
same, i.e., take the good bits found so far and chomp at the broken
byte?
> + name_len -= w;
> + }
>
> slash = strchr(name, '/');
> if (slash)
Thanks. |
…tion f85b49f (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-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 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>
fcd44d6 to
4a72647
Compare
|
Elijah Newren wrote on the Git mailing list (how to reply to this email): On Fri, Apr 17, 2026 at 12:21 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> "Elijah Newren via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
> > 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.
>
> A tangent, but I get a datestamp for the same f85b49f3 (diff:
> improve scaling of filenames in diffstat to handle UTF-8 chars,
> 2026-01-16) that is different from what you showed above. Did you
> find a bug in "git show -s --pretty=reference"?
Hmm, indeed I get 2026-01-16 as well; I'm not sure what happened there.
> > 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;
> > + }
>
> IOW, we punt on "scaling" and instead use the full string? I was
> wondering if we can punt on only this segment by replacing this
> segment with just "..." and resync at the next slash.
Good point. Alternatively, perhaps I could just add a wrapper around
utf8_width() which never sets name to NULL and never returns a
negative value, and then use the original loop as-is other than
calling the new function?
>
> > + if (w < 0) /* control character */
> > + break;
>
> When we have a control characer, we instead chomp immediately before
> that byte, which sounds good. But then wouldn't the loop that found
> an Invalid UTF-8 sequence in the middle of a name want to do the
> same, i.e., take the good bits found so far and chomp at the broken
> byte?
Makes sense, though I think my simpler alternative might be easier.
I'll send in a re-roll.
>
> > + name_len -= w;
> > + }
> >
> > slash = strchr(name, '/');
> > if (slash)
>
> Thanks. |
|
User |
|
Junio C Hamano wrote on the Git mailing list (how to reply to this email): Elijah Newren <newren@gmail.com> writes:
> Makes sense, though I think my simpler alternative might be easier.
> I'll send in a re-roll.
As long as "an invalid UTF-8" and "a control character" behaves more
or less the same (i.e., "eek, we cannot measure the width of the
UTF-8 character at this byte position, so let's do X as a fallback",
where X is the same regardless of the exact reason why we cannot
measure the width), I'll be happy. If we see a slash after the
problematic position, advancing to that slash might be the simplest,
as that is in line with how the code works when there is no such
problem, but we also need to be prepared for a filename whose last
component is sufficiently long that we see no such slash after the
problematic byte. |
Author
|
/submit |
|
Submitted as pull.2093.v2.git.1776465910538.gitgitgadget@gmail.com To fetch this version into To fetch this version to local tag |
|
This branch is now known as |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes since v1:
cc: LorenzoPegorari lorenzo.pegorari2002@gmail.com
cc: Elijah Newren newren@gmail.com