Re: [PATCH v2] utf8: use size_t for string width methods and callee sites.

Junio C Hamano <[email protected]>
Newsgroups org.kernel.vger.git
Message-ID <[email protected]>
Hardik Kumar <[email protected]> writes:

> utf8_strwidth() and utf8_strnwidth() return int, even though the
> return value is always non-negative:
>
> - utf8_strnwidth() accumulates the width into a size_t and otherwise
>   returns its size_t len parameter,
> - utf8_strwidth() just forwards its result.
>
> Change their signatures to return size_t instead.
>
> Update the types of the variables the said method is used to avoid
> potential UB caused by implicit conversion from size_t to int.

The goal looks attractive on the surface, and the change to make
utf8_strwidth() and utf8_strnwidth() return 'size_t' clears an
existing TODO.  However, the updates to the call sites to support
this change introduce several bugs due to unsigned integer underflow
and incorrect mixed-sign comparisons.

Consider just one example:

> diff --git a/diff.c b/diff.c
> index 589c196..4887958 100644
> --- a/diff.c
> +++ b/diff.c
> @@ -2952,7 +2952,8 @@ static int utf8_ish_width(const char **start)
>  
>  static void show_stats(struct diffstat_t *data, struct diff_options *options)
>  {
> -	int i, len, add, del, adds = 0, dels = 0;
> +	int i, add, del, adds = 0, dels = 0;
> +	size_t len;
>  	uintmax_t max_change = 0, max_len = 0;
>  	int total_files = data->nr, count;
>  	int width, name_width, graph_width, number_width = 0, bin_width = 0;

The above change impacts code later in the function (among other
things):

		/*
		 * "scale" the filename
		 */
		len = name_width;
		name_len = utf8_strwidth(name);
		if (name_width < name_len) {
			char *slash;
			prefix = "...";
			len -= 3;
			if (len < 0)
				len = 0;

Here, 'len' used to be an 'int', but now it is 'size_t', which is
unsigned.  The safeguard to prevent 'len' from going down to an
unacceptably low value by clipping it to 0 never triggers, because
'if (len < 0)' can never be true.  If len is less than 3, len -= 3
will result in a fairly large value, and the subsequent computation
would go bananas to see a value with little relation to name_len.

Another example.

> diff --git a/pretty.c b/pretty.c
> index d8a9f37..f7d392d 100644
> --- a/pretty.c
> +++ b/pretty.c
> @@ -1805,11 +1805,12 @@ static size_t format_and_pad_commit(struct strbuf *sb, /* in UTF-8 */
>  {
>  	struct strbuf local_sb = STRBUF_INIT;
>  	size_t total_consumed = 0;
> -	int len, padding = c->padding;
> +	int padding = c->padding;
> +	size_t len;
>  
>  	if (padding < 0) {
>  		const char *start = strrchr(sb->buf, '\n');
> -		int occupied;
> +		size_t occupied;
>  		if (!start)
>  			start = sb->buf;
>  		occupied = utf8_strnwidth(start, strlen(start), 1);

After this post-context, 'occupied' is incremented, and then we have
this:

		padding = (-padding) - occupied;

If 'occupied' is sufficiently large, 'padding' can become negative
here.  Because padding remains an 'int' and can become negative, it
impacts code a bit further down in the same function (among other
similar comparisons):

	if (c->flush_type == flush_left_and_steal) {
		const char *ch = sb->buf + sb->len - 1;
		while (len > padding && ch > sb->buf) {
			const char *p;
			if (*ch == ' ') {
				ch--;
				padding++;
				continue;
			}

We compare 'len' and 'padding', first promoting 'padding' to
'size_t', so when 'padding' is negative, we compare 'len' with a
fairly large number due to unsigned wraparound.  We will fail to
"steal" spaces as we will not loop here.

I will stop here.  What makes reviewing this change so unpleasant is
that on the surface, changing variable definitions to flip int to
size_t looks pretty, yet the real breakage appears in places that
are not shown in the patch at all.

So, this needs more work to become acceptable, I am afraid.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.