The problem is that, phpdbg accesses to this watchpoint again through its parent element at the real shutdown php_module_shutdown, which calls zend_shutdown → … → phpdbg_unwatch_parent_ht
void phpdbg_unwatch_parent_ht(phpdbg_watch_element *element) {
if (element->watch->type == WATCH_ON_BUCKET) {
#0 phpdbg_remove_watchpoint (watch=0x7ffff5260460) at sapi/phpdbg/phpdbg_watch.c:950
#1 0x00005555559ed309 in phpdbg_update_watch_element_watch (element=0x7ffff52613c0) at sapi/phpdbg/phpdbg_watch.c:707
#2 0x00005555559ed3f2 in phpdbg_update_watch_collision_elements (watch=watch@entry=0x7ffff5265180)
at sapi/phpdbg/phpdbg_watch.c:944
#3 0x00005555559ed4b1 in phpdbg_remove_watchpoint (watch=watch@entry=0x7ffff5265180) at sapi/phpdbg/phpdbg_watch.c:958
#4 0x00005555559edec3 in phpdbg_watch_efree (ptr=ptr@entry=0x7ffff5259188) at sapi/phpdbg/phpdbg_watch.c:1210
#5 0x00005555559d402c in phpdbg_free_wrapper (p=0x7ffff5259188) at sapi/phpdbg/phpdbg.c:1088
#6 0x00005555558fee95 in _zend_hash_del_el_ex (ht=0x555556a1ac10 <executor_globals+304>, idx=8, prev=<optimized out>,
p=<optimized out>) at Zend/zend_hash.c:1488
#7 _zend_hash_del_el (ht=0x555556a1ac10 <executor_globals+304>, idx=8, p=<optimized out>) at Zend/zend_hash.c:1515
#8 zend_hash_graceful_reverse_destroy (ht=ht@entry=0x555556a1ac10 <executor_globals+304>) at Zend/zend_hash.c:2040
#9 0x00005555558d74a9 in zend_shutdown_executor_values (fast_shutdown=false) at Zend/zend_execute_API.c:285
#10 0x00005555558d7a5d in shutdown_executor () at Zend/zend_execute_API.c:417
#11 0x00005555558e7f9e in zend_deactivate () at Zend/zend.c:1290
#12 0x000055555587b146 in php_request_shutdown (dummy=dummy@entry=0x0) at main/main.c:1883
#13 0x00005555559d5adb in main (argc=82, argv=0x7fffffffda88) at sapi/phpdbg/phpdbg.c:1685
Callstack 2 (access): watch=0x7ffff5260460
Breakpoint 2, phpdbg_unwatch_parent_ht (element=element@entry=0x7ffff52613c0) at sapi/phpdbg/phpdbg_watch.c:670
670 void phpdbg_unwatch_parent_ht(phpdbg_watch_element *element) {
(gdb) p element->watch
$6 = (phpdbg_watchpoint_t *) 0x7ffff5260460
(gdb) bt
#0 phpdbg_unwatch_parent_ht (element=element@entry=0x7ffff52613c0) at sapi/phpdbg/phpdbg_watch.c:670
#1 0x00005555559ece54 in phpdbg_clean_watch_element (element=0x7ffff52613c0) at sapi/phpdbg/phpdbg_watch.c:973
#2 phpdbg_free_watch_element_tree (element=element@entry=0x7ffff5261480) at sapi/phpdbg/phpdbg_watch.c:902
#3 0x00005555559ee8d1 in phpdbg_automatic_dequeue_free (element=0x7ffff5261480) at sapi/phpdbg/phpdbg_watch.c:771
#4 phpdbg_destroy_watchpoints () at sapi/phpdbg/phpdbg_watch.c:1500
#5 0x00005555559d6812 in zm_shutdown_phpdbg (type=<optimized out>, module_number=<optimized out>)
at sapi/phpdbg/phpdbg.c:181
#6 0x00005555558f2831 in module_destructor (module=0x555556a02f88 <sapi_phpdbg_module_entry>) at Zend/zend_API.c:3163
#7 0x00005555558fee95 in _zend_hash_del_el_ex (ht=0x555556a1b2c8 <module_registry>, idx=26, prev=<optimized out>,
p=<optimized out>) at Zend/zend_hash.c:1488
#8 _zend_hash_del_el (ht=0x555556a1b2c8 <module_registry>, idx=26, p=<optimized out>) at Zend/zend_hash.c:1515
#9 zend_hash_graceful_reverse_destroy (ht=0x555556a1b2c8 <module_registry>) at Zend/zend_hash.c:2040
#10 0x00005555558e7bf1 in zend_shutdown () at Zend/zend.c:1125
#11 0x000055555587caa6 in php_module_shutdown () at main/main.c:2376
#12 0x00005555559d5dd2 in main (argc=82, argv=0x7fffffffda88) at sapi/phpdbg/phpdbg.c:1748
PoC
This bug is also triggered by the test case sapi/phpdbg/tests/bug73927.phpt
Summary
phpdbg accesses freed watchpoint object at
php_module_shutdown
which is already freed fromphp_request_shutdown
.Details
This bug is triggered by the test case
sapi/phpdbg/tests/bug73927.phpt
The PoC creates 4 watch points from $value and $lower[]
After receiving “q”, phpdbg calls
php_request_shutdown
, which callszend_deactivate
→ … →phpdbg_remove_warchpoint
. then, it frees the watchpoint 0x7ffff5260460 atphpdbg_remove_warchpoint
(sapi/phpdbg/phpdbg_watch.c:968)The problem is that, phpdbg accesses to this watchpoint again through its parent element at the real shutdown
php_module_shutdown
, which callszend_shutdown
→ … →phpdbg_unwatch_parent_ht
Thus, it is use-after-free.
Callstack 1 (free): php_request_shutdown > … > phpdbg_remove_watchpoint
Callstack 2 (access): watch=0x7ffff5260460
PoC
This bug is also triggered by the test case
sapi/phpdbg/tests/bug73927.phpt
Command
In phpdbg
Analyze using GDB
Thank you for taking the time to check this bug report!:)
PHP Version
PHP 8.4.0-dev
Operating System
Ubuntu 22.04