Skip to content

Commit 2bdce61

Browse files
committed
Fix array going away during sorting
Fixes GH-16648 Closes GH-16654
1 parent 2985de7 commit 2bdce61

File tree

6 files changed

+94
-10
lines changed

6 files changed

+94
-10
lines changed

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ PHP NEWS
1919
(ilutov)
2020
. Fixed bug GH-16508 (Incorrect line number in inheritance errors of delayed
2121
early bound classes). (ilutov)
22+
. Fixed bug GH-16648 (Use-after-free during array sorting). (ilutov)
2223

2324
- Curl:
2425
. Fixed bug GH-16302 (CurlMultiHandle holds a reference to CurlHandle if

Zend/tests/gh16648.phpt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
--TEST--
2+
GH-16648: Use-after-free during array sorting
3+
--FILE--
4+
<?php
5+
6+
function resize_arr() {
7+
global $arr;
8+
for ($i = 0; $i < 10; $i++) {
9+
$arr[$i] = $i;
10+
}
11+
}
12+
13+
class C {
14+
function __toString() {
15+
resize_arr();
16+
return '3';
17+
}
18+
}
19+
20+
$arr = ['a' => '1', '3' => new C, '2' => '2'];
21+
asort($arr);
22+
var_dump($arr);
23+
24+
?>
25+
--EXPECT--
26+
array(11) {
27+
["a"]=>
28+
string(1) "1"
29+
[3]=>
30+
int(3)
31+
[2]=>
32+
int(2)
33+
[0]=>
34+
int(0)
35+
[1]=>
36+
int(1)
37+
[4]=>
38+
int(4)
39+
[5]=>
40+
int(5)
41+
[6]=>
42+
int(6)
43+
[7]=>
44+
int(7)
45+
[8]=>
46+
int(8)
47+
[9]=>
48+
int(9)
49+
}

Zend/zend_hash.c

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2863,13 +2863,12 @@ ZEND_API void zend_hash_bucket_packed_swap(Bucket *p, Bucket *q)
28632863
q->h = h;
28642864
}
28652865

2866-
ZEND_API void ZEND_FASTCALL zend_hash_sort_ex(HashTable *ht, sort_func_t sort, bucket_compare_func_t compar, bool renumber)
2866+
static void zend_hash_sort_internal(HashTable *ht, sort_func_t sort, bucket_compare_func_t compar, bool renumber)
28672867
{
28682868
Bucket *p;
28692869
uint32_t i, j;
28702870

28712871
IS_CONSISTENT(ht);
2872-
HT_ASSERT_RC1(ht);
28732872

28742873
if (!(ht->nNumOfElements>1) && !(renumber && ht->nNumOfElements>0)) {
28752874
/* Doesn't require sorting */
@@ -2956,6 +2955,33 @@ ZEND_API void ZEND_FASTCALL zend_hash_sort_ex(HashTable *ht, sort_func_t sort, b
29562955
}
29572956
}
29582957

2958+
ZEND_API void ZEND_FASTCALL zend_hash_sort_ex(HashTable *ht, sort_func_t sort, bucket_compare_func_t compar, bool renumber)
2959+
{
2960+
HT_ASSERT_RC1(ht);
2961+
zend_hash_sort_internal(ht, sort, compar, renumber);
2962+
}
2963+
2964+
void ZEND_FASTCALL zend_array_sort_ex(HashTable *ht, sort_func_t sort, bucket_compare_func_t compar, bool renumber)
2965+
{
2966+
HT_ASSERT_RC1(ht);
2967+
2968+
/* Unpack the array early to avoid RCn assertion failures. */
2969+
if (HT_IS_PACKED(ht)) {
2970+
zend_hash_packed_to_hash(ht);
2971+
}
2972+
2973+
/* Adding a refcount prevents the array from going away. */
2974+
GC_ADDREF(ht);
2975+
2976+
zend_hash_sort_internal(ht, sort, compar, renumber);
2977+
2978+
if (UNEXPECTED(GC_DELREF(ht) == 0)) {
2979+
zend_array_destroy(ht);
2980+
} else {
2981+
gc_check_possible_root((zend_refcounted *)ht);
2982+
}
2983+
}
2984+
29592985
static zend_always_inline int zend_hash_compare_impl(HashTable *ht1, HashTable *ht2, compare_func_t compar, bool ordered) {
29602986
uint32_t idx1, idx2;
29612987
zend_string *key1, *key2;

Zend/zend_hash.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,12 +296,20 @@ ZEND_API void zend_hash_bucket_packed_swap(Bucket *p, Bucket *q);
296296
typedef int (*bucket_compare_func_t)(Bucket *a, Bucket *b);
297297
ZEND_API int zend_hash_compare(HashTable *ht1, HashTable *ht2, compare_func_t compar, bool ordered);
298298
ZEND_API void ZEND_FASTCALL zend_hash_sort_ex(HashTable *ht, sort_func_t sort_func, bucket_compare_func_t compare_func, bool renumber);
299+
void ZEND_FASTCALL zend_array_sort_ex(HashTable *ht, sort_func_t sort_func, bucket_compare_func_t compare_func, bool renumber);
299300
ZEND_API zval* ZEND_FASTCALL zend_hash_minmax(const HashTable *ht, compare_func_t compar, uint32_t flag);
300301

301302
static zend_always_inline void ZEND_FASTCALL zend_hash_sort(HashTable *ht, bucket_compare_func_t compare_func, zend_bool renumber) {
302303
zend_hash_sort_ex(ht, zend_sort, compare_func, renumber);
303304
}
304305

306+
/* Use this variant over zend_hash_sort() when sorting user arrays that may
307+
* trigger user code. It will ensure the user code cannot free the array during
308+
* sorting. */
309+
static zend_always_inline void zend_array_sort(HashTable *ht, bucket_compare_func_t compare_func, zend_bool renumber) {
310+
zend_array_sort_ex(ht, zend_sort, compare_func, renumber);
311+
}
312+
305313
static zend_always_inline uint32_t zend_hash_num_elements(const HashTable *ht) {
306314
return ht->nNumOfElements;
307315
}

ext/intl/collator/collator_sort.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ static void collator_sort_internal( int renumber, INTERNAL_FUNCTION_PARAMETERS )
292292
INTL_G( current_collator ) = co->ucoll;
293293

294294
/* Sort specified array. */
295-
zend_hash_sort(hash, collator_compare_func, renumber);
295+
zend_array_sort(hash, collator_compare_func, renumber);
296296

297297
/* Restore saved collator. */
298298
INTL_G( current_collator ) = saved_collator;

ext/standard/array.c

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -705,9 +705,9 @@ static void php_natsort(INTERNAL_FUNCTION_PARAMETERS, int fold_case) /* {{{ */
705705
ZEND_PARSE_PARAMETERS_END();
706706

707707
if (fold_case) {
708-
zend_hash_sort(Z_ARRVAL_P(array), php_array_natural_case_compare, 0);
708+
zend_array_sort(Z_ARRVAL_P(array), php_array_natural_case_compare, 0);
709709
} else {
710-
zend_hash_sort(Z_ARRVAL_P(array), php_array_natural_compare, 0);
710+
zend_array_sort(Z_ARRVAL_P(array), php_array_natural_compare, 0);
711711
}
712712

713713
RETURN_TRUE;
@@ -743,7 +743,7 @@ PHP_FUNCTION(asort)
743743

744744
cmp = php_get_data_compare_func(sort_type, 0);
745745

746-
zend_hash_sort(Z_ARRVAL_P(array), cmp, 0);
746+
zend_array_sort(Z_ARRVAL_P(array), cmp, 0);
747747

748748
RETURN_TRUE;
749749
}
@@ -764,7 +764,7 @@ PHP_FUNCTION(arsort)
764764

765765
cmp = php_get_data_compare_func(sort_type, 1);
766766

767-
zend_hash_sort(Z_ARRVAL_P(array), cmp, 0);
767+
zend_array_sort(Z_ARRVAL_P(array), cmp, 0);
768768

769769
RETURN_TRUE;
770770
}
@@ -785,7 +785,7 @@ PHP_FUNCTION(sort)
785785

786786
cmp = php_get_data_compare_func(sort_type, 0);
787787

788-
zend_hash_sort(Z_ARRVAL_P(array), cmp, 1);
788+
zend_array_sort(Z_ARRVAL_P(array), cmp, 1);
789789

790790
RETURN_TRUE;
791791
}
@@ -806,7 +806,7 @@ PHP_FUNCTION(rsort)
806806

807807
cmp = php_get_data_compare_func(sort_type, 1);
808808

809-
zend_hash_sort(Z_ARRVAL_P(array), cmp, 1);
809+
zend_array_sort(Z_ARRVAL_P(array), cmp, 1);
810810

811811
RETURN_TRUE;
812812
}
@@ -904,7 +904,7 @@ static void php_usort(INTERNAL_FUNCTION_PARAMETERS, bucket_compare_func_t compar
904904
/* Copy array, so the in-place modifications will not be visible to the callback function */
905905
arr = zend_array_dup(arr);
906906

907-
zend_hash_sort(arr, compare_func, renumber);
907+
zend_array_sort(arr, compare_func, renumber);
908908

909909
zval garbage;
910910
ZVAL_COPY_VALUE(&garbage, array);

0 commit comments

Comments
 (0)