Commit message: Fix #79371: mb_strtolower (UTF-32LE): stack-buffer-overflow

We make sure that negative values are properly compared.
CVE: CVE-2020-7065
CWE: CWE-787
Vulnerability description: In PHP versions 7.3.x below 7.3.16 and 7.4.x below 7.4.4, while using mb_strtolower() function with UTF-32LE encoding, certain invalid strings could cause PHP to overwrite stack-allocated buffer. This could lead to memory corruption, crashes and potentially code execution.

Project: php-src
File: php_unicode.c

unsigned php_unicode_tofold_raw(unsigned code, enum mbfl_no_encoding enc)
{
	if (code < 0x80) {
		/* Fast path for ASCII */
		if (code >= 0x41 && code <= 0x5A) {
			if (UNEXPECTED(enc == mbfl_no_encoding_8859_9 && code == 0x49)) {
				return 0x131;
			}
			return code + 0x20;
		}
		return code;
	} else {
		unsigned new_code = CASE_LOOKUP(code, fold);
		if (new_code != CODE_NOT_FOUND) {
			if (UNEXPECTED(enc == mbfl_no_encoding_8859_9 && code == 0x130)) {
				return 0x69;
			}
			return new_code;
		}
		return code;
	}
}

Project: php-src
File: php_unicode.c

static inline unsigned php_unicode_tolower_simple(unsigned code, enum mbfl_no_encoding enc) {
	code = php_unicode_tolower_raw(code, enc);
	if (UNEXPECTED(code > 0xffffff)) {
		return _uccase_extra_table[code & 0xffffff];
	}
	return code;
}

Project: php-src
File: php_unicode.c

static unsigned php_unicode_toupper_raw(unsigned code, enum mbfl_no_encoding enc)
{
	if (code < 0x80) {
		/* Fast path for ASCII */
		if (code >= 0x61 && code <= 0x7A) {
			if (UNEXPECTED(enc == mbfl_no_encoding_8859_9 && code == 0x69)) {
				return 0x130;
			}
			return code - 0x20;
		}
		return code;
	} else {
		unsigned new_code = CASE_LOOKUP(code, upper);
		if (new_code != CODE_NOT_FOUND) {
			return new_code;
		}
		return code;
	}
}

Project: php-src
File: php_unicode.c

static unsigned php_unicode_tolower_raw(unsigned code, enum mbfl_no_encoding enc)
{
	if (code < 0x80) {
		/* Fast path for ASCII */
		if (code >= 0x41 && code <= 0x5A) {
			if (UNEXPECTED(enc == mbfl_no_encoding_8859_9 && code == 0x0049L)) {
				return 0x0131L;
			}
			return code + 0x20;
		}
		return code;
	} else {
		unsigned new_code = CASE_LOOKUP(code, lower);
		if (new_code != CODE_NOT_FOUND) {
			if (UNEXPECTED(enc == mbfl_no_encoding_8859_9 && code == 0x130)) {
				return 0x69;
			}
			return new_code;
		}
		return code;
	}
}

Project: php-src
File: php_unicode.c

static unsigned php_unicode_totitle_raw(unsigned code, enum mbfl_no_encoding enc)
{
	unsigned new_code = CASE_LOOKUP(code, title);
	if (new_code != CODE_NOT_FOUND) {
		return new_code;
	}

	/* No dedicated title-case variant, use to-upper instead */
	return php_unicode_toupper_raw(code, enc);
}

Project: php-src
File: php_unicode.c

static inline unsigned php_unicode_tolower_full(
		unsigned code, enum mbfl_no_encoding enc, unsigned *out) {
	code = php_unicode_tolower_raw(code, enc);
	if (UNEXPECTED(code > 0xffffff)) {
		unsigned len = code >> 24;
		const unsigned *p = &_uccase_extra_table[code & 0xffffff];
		memcpy(out, p + 1, len * sizeof(unsigned));
		return len;
	}
	*out = code;
	return 1;
}

Project: php-src
File: php_unicode.c

static inline unsigned php_unicode_tofold_full(
		unsigned code, enum mbfl_no_encoding enc, unsigned *out) {
	code = php_unicode_tofold_raw(code, enc);
	if (UNEXPECTED(code > 0xffffff)) {
		unsigned len = code >> 24;
		const unsigned *p = &_uccase_extra_table[code & 0xffffff];
		memcpy(out, p + 1, len * sizeof(unsigned));
		return len;
	}
	*out = code;
	return 1;
}

Project: php-src
File: php_unicode.c

static inline unsigned mph_hash(unsigned d, unsigned x) {
    x ^= d;
    x = ((x >> 16) ^ x) * 0x45d9f3b;
    return x;
}

Project: php-src
File: php_unicode.c

static int convert_case_filter(int c, void *void_data)
{
	struct convert_case_data *data = (struct convert_case_data *) void_data;
	unsigned out[3];
	unsigned len, i;

	/* Handle invalid characters early, as we assign special meaning to
	 * codepoints above 0xffffff. */
	if (UNEXPECTED((unsigned) c > 0xffffff)) {
		(*data->next_filter->filter_function)(c, data->next_filter);
		return 0;
	}

	switch (data->case_mode) {
		case PHP_UNICODE_CASE_UPPER_SIMPLE:
			out[0] = php_unicode_toupper_simple(c, data->no_encoding);
			len = 1;
			break;

		case PHP_UNICODE_CASE_UPPER:
			len = php_unicode_toupper_full(c, data->no_encoding, out);
			break;

		case PHP_UNICODE_CASE_LOWER_SIMPLE:
			out[0] = php_unicode_tolower_simple(c, data->no_encoding);
			len = 1;
			break;

		case PHP_UNICODE_CASE_LOWER:
			len = php_unicode_tolower_full(c, data->no_encoding, out);
			break;

		case PHP_UNICODE_CASE_FOLD:
			len = php_unicode_tofold_full(c, data->no_encoding, out);
			break;

		case PHP_UNICODE_CASE_FOLD_SIMPLE:
			out[0] = php_unicode_tofold_simple(c, data->no_encoding);
			len = 1;
			break;

		case PHP_UNICODE_CASE_TITLE_SIMPLE:
		case PHP_UNICODE_CASE_TITLE:
		{
			if (data->title_mode) {
				if (data->case_mode == PHP_UNICODE_CASE_TITLE_SIMPLE) {
					out[0] = php_unicode_tolower_simple(c, data->no_encoding);
					len = 1;
				} else {
					len = php_unicode_tolower_full(c, data->no_encoding, out);
				}
			} else {
				if (data->case_mode == PHP_UNICODE_CASE_TITLE_SIMPLE) {
					out[0] = php_unicode_totitle_simple(c, data->no_encoding);
					len = 1;
				} else {
					len = php_unicode_totitle_full(c, data->no_encoding, out);
				}
			}
			if (!php_unicode_is_case_ignorable(c)) {
				data->title_mode = php_unicode_is_cased(c);
			}
			break;
		}
		EMPTY_SWITCH_DEFAULT_CASE()
	}

	for (i = 0; i < len; i++) {
		(*data->next_filter->filter_function)(out[i], data->next_filter);
	}
	return 0;
}

Project: php-src
File: php_unicode.c

static inline unsigned php_unicode_totitle_full(
		unsigned code, enum mbfl_no_encoding enc, unsigned *out) {
	code = php_unicode_totitle_raw(code, enc);
	if (UNEXPECTED(code > 0xffffff)) {
		unsigned len = code >> 24;
		const unsigned *p = &_uccase_extra_table[code & 0xffffff];
		memcpy(out, p + 1, len * sizeof(unsigned));
		return len;
	}
	*out = code;
	return 1;
}

Project: php-src
File: php_unicode.c

static inline unsigned mph_lookup(
		unsigned code,
		const short *g_table, unsigned g_table_size,
		const unsigned *table, unsigned table_size)
{
	short g = g_table[mph_hash(0, code) % g_table_size];

	unsigned idx;
	if (g <= 0) {
		idx = -g;
	} else {
		idx = mph_hash(g, code) % table_size;
	}

	if (table[2*idx] == code) {
		return table[2*idx + 1];
	}
	return CODE_NOT_FOUND;
}

Project: php-src
File: php_unicode.c

MBSTRING_API int php_unicode_is_prop(unsigned long code, ...)
{
	int result = 0;
	va_list va;
	va_start(va, code);

	while (1) {
		int prop = va_arg(va, int);
		if (prop < 0) {
			break;
		}

		if (prop_lookup(code, prop)) {
			result = 1;
			break;
		}
	}

	va_end(va);
	return result;
}

Project: php-src
File: php_unicode.c

static inline unsigned php_unicode_toupper_simple(unsigned code, enum mbfl_no_encoding enc) {
	code = php_unicode_toupper_raw(code, enc);
	if (UNEXPECTED(code > 0xffffff)) {
		return _uccase_extra_table[code & 0xffffff];
	}
	return code;
}

Project: php-src
File: php_unicode.c

static inline unsigned php_unicode_totitle_simple(unsigned code, enum mbfl_no_encoding enc) {
	code = php_unicode_totitle_raw(code, enc);
	if (UNEXPECTED(code > 0xffffff)) {
		return _uccase_extra_table[code & 0xffffff];
	}
	return code;
}

Project: php-src
File: php_unicode.c

static inline unsigned php_unicode_tofold_simple(unsigned code, enum mbfl_no_encoding enc) {
	code = php_unicode_tofold_raw(code, enc);
	if (UNEXPECTED(code > 0xffffff)) {
		return _uccase_extra_table[code & 0xffffff];
	}
	return code;
}

Project: php-src
File: php_unicode.c

MBSTRING_API int php_unicode_is_prop1(unsigned long code, int prop)
{
	return prop_lookup(code, prop);
}

Project: php-src
File: php_unicode.c

static inline unsigned php_unicode_toupper_full(
		unsigned code, enum mbfl_no_encoding enc, unsigned *out) {
	code = php_unicode_toupper_raw(code, enc);
	if (UNEXPECTED(code > 0xffffff)) {
		unsigned len = code >> 24;
		const unsigned *p = &_uccase_extra_table[code & 0xffffff];
		memcpy(out, p + 1, len * sizeof(unsigned));
		return len;
	}
	*out = code;
	return 1;
}

Project: php-src
File: php_unicode.c

MBSTRING_API char *php_unicode_convert_case(
		int case_mode, const char *srcstr, size_t srclen, size_t *ret_len,
		const mbfl_encoding *src_encoding, int illegal_mode, int illegal_substchar)
{
	struct convert_case_data data;
	mbfl_convert_filter *from_wchar, *to_wchar;
	mbfl_string result, *result_ptr;

	mbfl_memory_device device;
	mbfl_memory_device_init(&device, srclen + 1, 0);

	/* encoding -> wchar filter */
	to_wchar = mbfl_convert_filter_new(src_encoding,
			&mbfl_encoding_wchar, convert_case_filter, NULL, &data);
	if (to_wchar == NULL) {
		mbfl_memory_device_clear(&device);
		return NULL;
	}

	/* wchar -> encoding filter */
	from_wchar = mbfl_convert_filter_new(
			&mbfl_encoding_wchar, src_encoding,
			mbfl_memory_device_output, NULL, &device);
	if (from_wchar == NULL) {
		mbfl_convert_filter_delete(to_wchar);
		mbfl_memory_device_clear(&device);
		return NULL;
	}

	to_wchar->illegal_mode = illegal_mode;
	to_wchar->illegal_substchar = illegal_substchar;
	from_wchar->illegal_mode = illegal_mode;
	from_wchar->illegal_substchar = illegal_substchar;

	data.next_filter = from_wchar;
	data.no_encoding = src_encoding->no_encoding;
	data.case_mode = case_mode;
	data.title_mode = 0;

	{
		/* feed data */
		const unsigned char *p = (const unsigned char *) srcstr;
		size_t n = srclen;
		while (n > 0) {
			if ((*to_wchar->filter_function)(*p++, to_wchar) < 0) {
				break;
			}
			n--;
		}
	}

	mbfl_convert_filter_flush(to_wchar);
	mbfl_convert_filter_flush(from_wchar);
	result_ptr = mbfl_memory_device_result(&device, &result);
	mbfl_convert_filter_delete(to_wchar);
	mbfl_convert_filter_delete(from_wchar);

	if (!result_ptr) {
		return NULL;
	}

	*ret_len = result.len;
	return (char *) result.val;
}