[php-src] Issue #20405: Accessing the object after WeakRef unreferencing results in use-after-free
| From: | bwoebi | Date: | Thu, 06 Nov 2025 13:42:11 +0000 |
| Subject: | [php-src] Issue #20405: Accessing the object after WeakRef unreferencing results in use-after-free | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-251185@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/20405
Author: bwoebi
### Description
The following code:
```php
<?php
$w = new WeakMap;
$o = new stdClass;
$w[$o] = new class($r) {
function __construct(public &$r) {}
function __destruct() {
$o = $this->r->get();
register_shutdown_function(function() use ($o) {
var_dump($o);
});
}
};
$r = WeakReference::create($o);
```
Resulted in this output:
```
==36789== Invalid read of size 4
==36789== at 0x54C4404: zend_gc_delref (zend_types.h:1342)
==36789== by 0x54BFAC7: i_zval_ptr_dtor (zend_variables.h:43)
==36789== by 0x54BF86B: zend_array_destroy (/usr/local/src/php/Zend/zend_hash.c:1840)
==36789== by 0x54862CF: zend_destroy_static_vars (/usr/local/src/php/Zend/zend_opcode.c:532)
==36789== by 0x55C4DE7: zend_closure_free_storage (/usr/local/src/php/Zend/zend_closures.c:522)
==36789== by 0xBA0918F: dd_uhook_closure_free_wrapper (uhook.c:1158)
==36789== by 0x55EA76F: zend_objects_store_del (/usr/local/src/php/Zend/zend_objects_API.c:200)
==36789== by 0x5499757: rc_dtor_func (/usr/local/src/php/Zend/zend_variables.c:57)
==36789== by 0x54997F3: i_zval_ptr_dtor (zend_variables.h:44)
==36789== by 0x5499793: zval_ptr_dtor (/usr/local/src/php/Zend/zend_variables.c:84)
==36789== by 0x52333E3: fci_release (/usr/local/src/php/ext/standard/basic_functions.c:1611)
==36789== by 0x522B973: user_shutdown_function_dtor
(/usr/local/src/php/ext/standard/basic_functions.c:1622)
==36789== Address 0xe813150 is 0 bytes inside a block of size 40 free'd
==36789== at 0x604B614: free (m_replacemalloc/vg_replace_malloc.c:989)
==36789== by 0x544D673: _efree_custom (/usr/local/src/php/Zend/zend_alloc.c:2500)
==36789== by 0x544D5C7: _efree (/usr/local/src/php/Zend/zend_alloc.c:2620)
==36789== by 0x55EA7DF: zend_objects_store_del (/usr/local/src/php/Zend/zend_objects_API.c:204)
==36789== by 0x5499757: rc_dtor_func (/usr/local/src/php/Zend/zend_variables.c:57)
==36789== by 0x54997F3: i_zval_ptr_dtor (zend_variables.h:44)
==36789== by 0x5499793: zval_ptr_dtor (/usr/local/src/php/Zend/zend_variables.c:84)
==36789== by 0x54BE46F: _zend_hash_del_el_ex (/usr/local/src/php/Zend/zend_hash.c:1488)
==36789== by 0x54BDF6B: _zend_hash_del_el (/usr/local/src/php/Zend/zend_hash.c:1515)
==36789== by 0x54C15C3: zend_hash_reverse_apply (/usr/local/src/php/Zend/zend_hash.c:2231)
==36789== by 0x547A843: shutdown_destructors (/usr/local/src/php/Zend/zend_execute_API.c:260)
==36789== by 0x549C227: zend_call_destructors (/usr/local/src/php/Zend/zend.c:1281)
==36789== Block was alloc'd at
==36789== at 0x60485D4: malloc (m_replacemalloc/vg_replace_malloc.c:446)
==36789== by 0x544DA1B: __zend_malloc (/usr/local/src/php/Zend/zend_alloc.c:3128)
==36789== by 0x544D54F: _malloc_custom (/usr/local/src/php/Zend/zend_alloc.c:2491)
==36789== by 0x544D493: _emalloc (/usr/local/src/php/Zend/zend_alloc.c:2610)
==36789== by 0x55E0907: zend_objects_new (/usr/local/src/php/Zend/zend_objects.c:189)
==36789== by 0x54A6143: _object_and_properties_init (/usr/local/src/php/Zend/zend_API.c:1773)
==36789== by 0x54A621B: object_init_ex (/usr/local/src/php/Zend/zend_API.c:1796)
==36789== by 0x552F62F: ZEND_NEW_SPEC_CONST_UNUSED_HANDLER (zend_vm_execute.h:10557)
==36789== by 0x54E2073: execute_ex (zend_vm_execute.h:57037)
==36789== by 0x54E2483: zend_execute (zend_vm_execute.h:61634)
==36789== by 0x549E34B: zend_execute_scripts (/usr/local/src/php/Zend/zend.c:1895)
==36789== by 0x53CB8E3: php_execute_script (/usr/local/src/php/main/main.c:2529)
```
Not exactly sure what to expect - either we just decide to make WeakReference->get() return NULL
when the targeted object has IS_OBJ_FREE_CALLED. Which would fix #20404 trivially as well.
But it would be a BC break and render the object inaccessible in "externally associated
destructors" (I mean dummy classes with destructors in weakmaps to observe an object to be
freed).
Or we strip the IS_OBJ_FREE_CALLED flag in the case the refcount is increased again in
zend_object_std_dtor and skip freeing the object in case the flag is no longer present after the
free_obj() call.
The latter solution is more flexible and does not have a BC break.
### PHP Version
```plain
PHP 8.2+
```
### Operating System
_No response_