php / php/php-src

use-after-free at phpdbg shutdown

Open
#13,230 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Bug SAPI: phpdbg Status: Verified
Dominant language
C
Stars
40.4k
Forks
8.1k
Avg merge
2d 13h
Merged PRs (30d)
96

Description

Summary

phpdbg accesses freed watchpoint object at php_module_shutdown which is already freed from php_request_shutdown.

Details
  • OS: Ubuntu 22.04
  • commit: 6fa4286ba4712963c2a07fdb178c239e04ad4da4 (8.4.0-dev)
  • compiler: Clang 15
  • build command: ./buildconf && ./configure && make
  • Credit: Junwha Hong from S2Lab in UNIST

This bug is triggered by the test case sapi/phpdbg/tests/bug73927.phpt

  1. The PoC creates 4 watch points from $value and $lower[]

  2. After receiving “q”, phpdbg calls php_request_shutdown, which calls zend_deactivate → … → phpdbg_remove_warchpoint. then, it frees the watchpoint 0x7ffff5260460 atphpdbg_remove_warchpoint (sapi/phpdbg/phpdbg_watch.c:968)

    void phpdbg_remove_watchpoint(phpdbg_watchpoint_t *watch) {
    	...
    	efree(watch);
    }
    
  3. 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) {
    

    Thus, it is use-after-free.

Callstack 1 (free): php_request_shutdown > … > phpdbg_remove_watchpoint

#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

Command

sapi/phpdbg/phpdbg -qIb -d opcache.cache_id=worker1 -d output_handler= -d open_basedir= -d disable_functions= -d output_buffering=Off -d error_reporting=32767 -d display_errors=1 -d display_startup_errors=1 -d log_errors=0 -d html_errors=0 -d track_errors=0 -d report_memleaks=1 -d report_zend_debug=0 -d docref_root= -d docref_ext=.html -d error_prepend_string= -d error_append_string= -d auto_prepend_file= -d auto_append_file= -d ignore_repeated_errors=0 -d precision=14 -d serialize_precision=-1 -d memory_limit=128M -d opcache.fast_shutdown=0 -d opcache.file_update_protection=0 -d opcache.revalidate_freq=0 -d opcache.jit_hot_loop=1 -d opcache.jit_hot_func=1 -d opcache.jit_hot_return=1 -d opcache.jit_hot_side_exit=1 -d opcache.jit_max_root_traces=100000 -d opcache.jit_max_side_traces=100000 -d opcache.jit_max_exit_counters=100000 -d opcache.protect_memory=1 -d zend.assertions=1 -d zend.exception_ignore_args=0 -d zend.exception_string_param_max_len=15 -d short_open_tag=0 -d session.auto_start=0 -f sapi/phpdbg/tests/bug73927.php

In phpdbg

prompt> b 18
prompt>r
prompt>c
prompt>w $value
prompt>w $lower[]
prompt>q

Analyze using GDB

b phpdbg_remove_watchpoint

Breakpoint 1, phpdbg_remove_watchpoint (watch=0x7ffff5260500) at sapi/phpdbg/phpdbg_watch.c:950
Breakpoint 1, phpdbg_remove_watchpoint (watch=watch@entry=0x7ffff5265180) at sapi/phpdbg/phpdbg_watch.c:950
Breakpoint 1, phpdbg_remove_watchpoint (watch=0x7ffff5260460) at sapi/phpdbg/phpdbg_watch.c:950
Breakpoint 1, phpdbg_remove_watchpoint (watch=0x7ffff5260320) at sapi/phpdbg/phpdbg_watch.c:950

b phpdbg_unwatch_parent_ht

Breakpoint 2, phpdbg_unwatch_parent_ht (element=element@entry=0x7ffff5261240) at sapi/phpdbg/phpdbg_watch.c:670
670     void phpdbg_unwatch_parent_ht(phpdbg_watch_element *element) {
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

Thank you for taking the time to check this bug report!:)

PHP Version

PHP 8.4.0-dev

Operating System

Ubuntu 22.04

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with sapi/phpdbg/phpdbg_watch.c, especially phpdbg_remove_watchpoint, phpdbg_unwatch_parent_ht, phpdbg_clean_watch_element, and phpdbg_destroy_watchpoints; then inspect the shutdown paths in sapi/phpdbg/phpdbg.c and the referenced Zend and PHP shutdown calls. Run sapi/phpdbg/tests/bug73927.phpt with the reported phpdbg interaction and verify shutdown no longer accesses the freed watchpoint.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
cli, devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.