From a96b05e63f92c835976a51499e73fea5b9591fdf Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Wed, 30 Jul 2025 19:04:15 +0200 Subject: [PATCH] Fix GH-19300: Nested array_multisort invocation with error breaks There are 2 issues: 1. When a MULTISORT_ABORT happens, it frees func, but func may point to ARRAYG(multisort_func), which would be a problem with nested invocations as it can destroy that of the "parent" invocation. To solve this, delay assigning to the globals. 2. The old globals were not restored which means that nested invocations with different flags will cause a wrong sorting function to be used. Closes GH-19319. --- NEWS | 2 ++ ext/standard/array.c | 12 +++++--- ext/standard/tests/array/gh19300_1.phpt | 40 +++++++++++++++++++++++++ ext/standard/tests/array/gh19300_2.phpt | 40 +++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 ext/standard/tests/array/gh19300_1.phpt create mode 100644 ext/standard/tests/array/gh19300_2.phpt diff --git a/NEWS b/NEWS index 0b261679e78..e89bc61148d 100644 --- a/NEWS +++ b/NEWS @@ -68,6 +68,8 @@ PHP NEWS . Fixed OSS Fuzz #433303828 (Leak in failed unserialize() with opcache). (ilutov) . Fix theoretical issues with hrtime() not being available. (nielsdos) + . Fixed bug GH-19300 (Nested array_multisort invocation with error breaks). + (nielsdos) - Windows: . Free opened_path when opened_path_len >= MAXPATHLEN. (dixyes) diff --git a/ext/standard/array.c b/ext/standard/array.c index 4d3c03bce52..a2a0459ec3b 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -5911,7 +5911,7 @@ PHP_FUNCTION(array_multisort) for (i = 0; i < MULTISORT_LAST; i++) { parse_state[i] = 0; } - func = ARRAYG(multisort_func) = ecalloc(argc, sizeof(bucket_compare_func_t)); + func = ecalloc(argc, sizeof(bucket_compare_func_t)); /* Here we go through the input arguments and parse them. Each one can * be either an array or a sort flag which follows an array. If not @@ -5927,7 +5927,7 @@ PHP_FUNCTION(array_multisort) /* We see the next array, so we update the sort flags of * the previous array and reset the sort flags. */ if (i > 0) { - ARRAYG(multisort_func)[num_arrays - 1] = php_get_data_compare_func_unstable(sort_type, sort_order != PHP_SORT_ASC); + func[num_arrays - 1] = php_get_data_compare_func_unstable(sort_type, sort_order != PHP_SORT_ASC); sort_order = PHP_SORT_ASC; sort_type = PHP_SORT_REGULAR; } @@ -5979,8 +5979,6 @@ PHP_FUNCTION(array_multisort) MULTISORT_ABORT; } } - /* Take care of the last array sort flags. */ - ARRAYG(multisort_func)[num_arrays - 1] = php_get_data_compare_func_unstable(sort_type, sort_order != PHP_SORT_ASC); /* Make sure the arrays are of the same size. */ array_size = zend_hash_num_elements(Z_ARRVAL_P(arrays[0])); @@ -5998,6 +5996,11 @@ PHP_FUNCTION(array_multisort) RETURN_TRUE; } + /* Take care of the last array sort flags. */ + func[num_arrays - 1] = php_get_data_compare_func_unstable(sort_type, sort_order != PHP_SORT_ASC); + bucket_compare_func_t *old_multisort_func = ARRAYG(multisort_func); + ARRAYG(multisort_func) = func; + /* Create the indirection array. This array is of size MxN, where * M is the number of entries in each input array and N is the number * of the input arrays + 1. The last column is UNDEF to indicate the end @@ -6074,6 +6077,7 @@ clean_up: efree(indirect); efree(func); efree(arrays); + ARRAYG(multisort_func) = old_multisort_func; } /* }}} */ diff --git a/ext/standard/tests/array/gh19300_1.phpt b/ext/standard/tests/array/gh19300_1.phpt new file mode 100644 index 00000000000..18b639bf425 --- /dev/null +++ b/ext/standard/tests/array/gh19300_1.phpt @@ -0,0 +1,40 @@ +--TEST-- +GH-19300 (Nested array_multisort invocation with error breaks) - correct invocation variation +--FILE-- +data; + } +} + +$inputs = [ + new MyStringable('3'), + new MyStringable('1'), + new MyStringable('2'), +]; + +var_dump(array_multisort($inputs, SORT_STRING)); +var_dump($inputs); +?> +--EXPECT-- +bool(true) +array(3) { + [0]=> + object(MyStringable)#2 (1) { + ["data":"MyStringable":private]=> + string(1) "1" + } + [1]=> + object(MyStringable)#3 (1) { + ["data":"MyStringable":private]=> + string(1) "2" + } + [2]=> + object(MyStringable)#1 (1) { + ["data":"MyStringable":private]=> + string(1) "3" + } +} diff --git a/ext/standard/tests/array/gh19300_2.phpt b/ext/standard/tests/array/gh19300_2.phpt new file mode 100644 index 00000000000..41ae7e82bb7 --- /dev/null +++ b/ext/standard/tests/array/gh19300_2.phpt @@ -0,0 +1,40 @@ +--TEST-- +GH-19300 (Nested array_multisort invocation with error breaks) - error variation +--FILE-- +getMessage(), "\n"; + } +} +set_error_handler('error_handle'); + +$inputs = [ + new stdClass, + new stdClass, + new stdClass, +]; + +var_dump(array_multisort($inputs, SORT_NUMERIC)); +var_dump($inputs); +?> +--EXPECT-- +array_multisort(): Argument #1 ($array) must be an array or a sort flag +array_multisort(): Argument #1 ($array) must be an array or a sort flag +array_multisort(): Argument #1 ($array) must be an array or a sort flag +array_multisort(): Argument #1 ($array) must be an array or a sort flag +bool(true) +array(3) { + [0]=> + object(stdClass)#1 (0) { + } + [1]=> + object(stdClass)#2 (0) { + } + [2]=> + object(stdClass)#3 (0) { + } +}