[php-src] Issue #24728: Use-after-free when a destructor runs during compound assignment ($obj->prop .= ...)

From: 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)

« previous php.bugs (#253206) next »