[php-src] Issue #24728: Use-after-free when a destructor runs during compound assignment ($obj->prop .= ...)
| From: | IzaiahSun | Date: | Mon, 26 Oct 2026 08:46:52 +0000 |
| Subject: | [php-src] Issue #24728: Use-after-free when a destructor runs during compound assignment ($obj->prop .= ...) | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-253206@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/24728
Author: IzaiahSun
### Description
The following code:
```php
<?php
class Old {
public function __toString(): string { return "old"; }
public function __destruct() { $GLOBALS['c'] = null; }
}
class C { public mixed $p; }
$c = new C;
$c->p = new Old;
$c->p .= "tail";
```
Resulted in this output:
```
==72016==ERROR: AddressSanitizer: heap-use-after-free on address 0x604000029190 at pc 0x000105acb2f4
bp 0x00016b34fe80 sp 0x00016b34fe78
READ of size 4 at 0x604000029190 thread T0
#0 zend_gc_delref zend_types.h:1370
#1 zend_objects_store_del zend_objects_API.c:182
#2 rc_dtor_func zend_variables.c:57
#3 i_zval_ptr_dtor zend_variables.h:45
#4 zval_ptr_dtor zend_variables.c:84
#5 zend_binary_assign_op_typed_prop zend_execute.c:1723
#6 ZEND_ASSIGN_OBJ_OP_SPEC_CV_CONST_HANDLER zend_vm_execute.h:42677
#7 execute_ex zend_vm_execute.h:58729
#8 zend_execute zend_vm_execute.h:64381
#9 zend_execute_script zend.c:1943
#10 php_execute_script_ex main.c:2623
freed by thread T0 here:
#0 free(libclang_rt.asan_osx_dynamic.dylib:arm64e)
#1 __zend_free zend_alloc.c:3728
#2 _efree zend_alloc.c:2889
#3 zend_objects_store_del zend_objects_API.c:200
#4 zend_object_release zend_objects_API.h:77
#5 zend_objects_destroy_object zend_objects.c:209
#6 zend_objects_store_del zend_objects_API.c:181
#7 rc_dtor_func zend_variables.c:57
#8 i_zval_ptr_dtor zend_variables.h:45
#9 zval_ptr_dtor zend_variables.c:84
#10 zend_binary_assign_op_typed_prop zend_execute.c:1723
SUMMARY: AddressSanitizer: heap-use-after-free zend_types.h:1370 in zend_gc_delref
```
But I expected this output instead:
```
(no output, clean exit)
```
---
Debug + ASan build, run with
USE_ZEND_ALLOC=0.
Compound assignment to a property whose current value is an object drops that object's
refcount while the pointer is still in the slot, so the destructor can free the slot before
the result is stored.
Three paths are affected. The one above is zend_binary_assign_op_typed_prop
(Zend/zend_execute.c:1723). The other two:
**zend_binary_assign_op_typed_ref, Zend/zend_execute.c:1703**
```php
<?php
class Old {
public function __toString(): string { return "old"; }
public function __destruct() { unset($GLOBALS['r']); $GLOBALS['holder'] =
null; }
}
class C { public mixed $p; }
$holder = new C;
$holder->p = new Old;
$r = &$holder->p;
$r .= "tail";
```
**concat_function, Zend/zend_operators.c:2089** — the in-place concat
path:
```c
if (result == op1) {
/* Destroy the old result first to drop the refcount, such that $x .= ...; may happen in-place.
*/
if (free_op1_string) {
/* op1_string will be used as the result, so we should not free it */
i_zval_ptr_dtor(result);
```
```php
<?php
class Old {
public function __toString(): string { return "old"; }
public function __destruct() { $GLOBALS['c'] = null; }
}
class C { public $p; }
$c = new C;
$c->p = new Old;
$c->p .= "tail";
```
Both produce the same report with frame #5 replaced accordingly.
Plain assignment is already correct, which makes a useful contrast — this does **not** crash:
```php
<?php
class Old { public function __destruct() { $GLOBALS['c'] = null; } }
class C { public $p; }
$c = new C;
$c->p = new Old;
$c->p = "replacement";
```
because zend_assign_to_typed_ref was changed in b39107c774 ("Delay
destructor for
zend_assign_to_typed_ref") to stash the old value in garbage and release it only
after the
slot has been written. The same treatment does not appear on the three paths above.
## PHP Version
```
PHP 8.4.28-dev (cli) (built: Oct 25 2026 01:02:13) (NTS DEBUG)
```
Also reproduced on master at 29b5e168bf.
### PHP Version
```plain
PHP 8.4.28-dev (cli) (built: Oct 25 2026 01:02:13) (NTS DEBUG)
Copyright (c) The PHP Group
Zend Engine v4.4.28-dev, Copyright (c) Zend Technologies
PHP 8.7.0-dev (cli) (built: Oct 24 2026 21:13:17) (NTS DEBUG)
Copyright © The PHP Group and Contributors
Zend Engine v4.7.0-dev, Copyright © Zend by Perforce
with Zend OPcache v8.7.0-dev, Copyright ©, by Zend by Perforce
```
### Operating System
macOS 27.0.1 (arm64)