Skip to content

Commit f761d44

Browse files
committed
Fix GH-20482: heap use-after-free in ZEND_ASSIGN_DIM via re-entrant user output handler
When ZEND_ASSIGN_DIM evaluates an undefined CV as the RHS, the warning emitted by zval_undefined_cv() can be routed through a user output handler (e.g. via ob_start with a small chunk size). The handler is allowed to run arbitrary PHP code, including reassigning the variable that holds the array we are writing to. That drops the array's last reference and zend_array_destroy frees its buckets while the VM still holds the previously-fetched variable_ptr, causing a UAF on the subsequent zend_assign_to_variable_ex. Mirror the protection already used in the IS_UNUSED branch of ZEND_ASSIGN_DIM (and in slow_index_convert): temporarily addref the target HashTable around zval_undefined_cv() so the array survives the reentrant user code, then refetch the dimension address afterwards (the previous variable_ptr may have been invalidated even when the array as a whole survived, e.g. if user code added entries that triggered a rehash). Includes a phpt regression covering the output-handler reentrancy path.
1 parent 1462499 commit f761d44

4 files changed

Lines changed: 1010 additions & 147 deletions

File tree

NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ PHP NEWS
1818
initialization). (Arnaud)
1919
. Enabled the TAILCALL VM on Windows when compiling with Clang >= 19 x86_64.
2020
(henderkes)
21+
. Fixed bug GH-20482 (Heap use-after-free in ZEND_ASSIGN_DIM when a user
22+
output handler clobbers the array during the undefined-variable
23+
warning). (lacatoire)
2124

2225
- BCMath:
2326
. Added NUL-byte validation to BCMath functions. (jorgsowa)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
--TEST--
2+
GH-20482: Heap use-after-free in ZEND_ASSIGN_DIM when an output handler clobbers the array during the undefined-variable warning
3+
--FILE--
4+
<?php
5+
$a = [1, 2, 3];
6+
7+
// Chunk size 1 forces the handler to run synchronously when the warning
8+
// emitted by the undefined RHS is buffered.
9+
ob_start(function () use (&$a) {
10+
// Drop the only reference to the original array, freeing it.
11+
$a = "freed";
12+
return '';
13+
}, 1);
14+
15+
// $undef is undefined: emitting the "Undefined variable" warning recurses
16+
// into the output handler above, which frees $a. Without the fix the next
17+
// store would write into the freed bucket.
18+
$a[0] = $undef;
19+
20+
ob_end_clean();
21+
var_dump($a);
22+
echo "ok\n";
23+
?>
24+
--EXPECTF--
25+
%AWarning: Undefined variable $undef in %s on line %d
26+
string(5) "freed"
27+
ok

Zend/zend_vm_def.h

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2730,16 +2730,33 @@ ZEND_VM_C_LABEL(try_assign_dim_array):
27302730
}
27312731
}
27322732
} else {
2733+
HashTable *ht = Z_ARRVAL_P(object_ptr);
27332734
dim = GET_OP2_ZVAL_PTR_UNDEF(BP_VAR_R);
2735+
value = GET_OP_DATA_ZVAL_PTR_UNDEF(BP_VAR_R);
2736+
if (OP_DATA_TYPE == IS_CV && UNEXPECTED(Z_TYPE_P(value) == IS_UNDEF)) {
2737+
/* The undefined-variable warning may recurse into user code
2738+
* (e.g. a user output handler) and clobber or destroy the
2739+
* array we are about to write to. Temporarily addref the
2740+
* array to detect destruction, and refetch the dim address
2741+
* after the warning since the previous variable_ptr may have
2742+
* been invalidated by hash table mutations. */
2743+
if (!(GC_FLAGS(ht) & IS_ARRAY_IMMUTABLE)) {
2744+
GC_ADDREF(ht);
2745+
}
2746+
value = zval_undefined_cv((opline+1)->op1.var EXECUTE_DATA_CC);
2747+
if (!(GC_FLAGS(ht) & IS_ARRAY_IMMUTABLE) && !GC_DELREF(ht)) {
2748+
zend_array_destroy(ht);
2749+
ZEND_VM_C_GOTO(assign_dim_error);
2750+
}
2751+
}
27342752
if (OP2_TYPE == IS_CONST) {
2735-
variable_ptr = zend_fetch_dimension_address_inner_W_CONST(Z_ARRVAL_P(object_ptr), dim EXECUTE_DATA_CC);
2753+
variable_ptr = zend_fetch_dimension_address_inner_W_CONST(ht, dim EXECUTE_DATA_CC);
27362754
} else {
2737-
variable_ptr = zend_fetch_dimension_address_inner_W(Z_ARRVAL_P(object_ptr), dim EXECUTE_DATA_CC);
2755+
variable_ptr = zend_fetch_dimension_address_inner_W(ht, dim EXECUTE_DATA_CC);
27382756
}
27392757
if (UNEXPECTED(variable_ptr == NULL)) {
27402758
ZEND_VM_C_GOTO(assign_dim_error);
27412759
}
2742-
value = GET_OP_DATA_ZVAL_PTR(BP_VAR_R);
27432760
value = zend_assign_to_variable_ex(variable_ptr, value, OP_DATA_TYPE, EX_USES_STRICT_TYPES(), &garbage);
27442761
}
27452762
if (UNEXPECTED(RETURN_VALUE_USED(opline))) {

0 commit comments

Comments
 (0)